feat(FN-2201): make Run Now available for active agents
- Show Run Now controls in AgentsView for active and running agents without requiring taskId - Rename Run Heartbeat copy to Run Now and update runs-tab accessibility labels in AgentDetailView - Add AgentsView coverage for Run Now visibility and startAgentRun invocation when no task is assigned - Update agent runs UI static-analysis tests and add a patch changeset for @gsxdsm/fusion
This commit is contained in:
@@ -34,7 +34,7 @@ describe("Agent runs UI — static analysis", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("AgentsView Run Heartbeat button", () => {
|
||||
describe("AgentsView Run Now button", () => {
|
||||
it("has handleRunHeartbeat function", () => {
|
||||
expect(agentsViewContent).toContain("handleRunHeartbeat");
|
||||
});
|
||||
@@ -44,12 +44,12 @@ describe("Agent runs UI — static analysis", () => {
|
||||
expect(agentsViewContent).toMatch(/startAgentRun.*Triggered from dashboard/);
|
||||
});
|
||||
|
||||
it("shows Run Heartbeat button for active agents with taskId", () => {
|
||||
it("shows Run Now button for active agents", () => {
|
||||
// The button should appear in the active state block
|
||||
// and should be conditioned on agent.taskId
|
||||
// and always be available for active agents.
|
||||
const activeBlock = agentsViewContent.match(/agent\.state === "active"[\s\S]*?agent\.state === "paused"/)?.[0] ?? "";
|
||||
expect(activeBlock).toContain("handleRunHeartbeat");
|
||||
expect(activeBlock).toContain("agent.taskId");
|
||||
expect(activeBlock).toContain("Run Now");
|
||||
});
|
||||
|
||||
it("shows disabled button for running agents", () => {
|
||||
@@ -57,7 +57,7 @@ describe("Agent runs UI — static analysis", () => {
|
||||
expect(runningBlock).toContain("disabled");
|
||||
});
|
||||
|
||||
it("uses Activity icon for the Run Heartbeat button", () => {
|
||||
it("uses Activity icon for the Run Now button", () => {
|
||||
expect(agentsViewContent).toContain("handleRunHeartbeat");
|
||||
// Activity icon is imported and used in run heartbeat buttons
|
||||
expect(agentsViewContent).toMatch(/from.*lucide-react/);
|
||||
@@ -74,12 +74,12 @@ describe("Agent runs UI — static analysis", () => {
|
||||
expect(agentDetailViewContent).toMatch(/fetchAgentRunDetail/);
|
||||
});
|
||||
|
||||
it("has startAgentRun import for Run Heartbeat button", () => {
|
||||
it("has startAgentRun import for Run Now button", () => {
|
||||
expect(agentDetailViewContent).toMatch(/import.*startAgentRun.*from.*api/);
|
||||
});
|
||||
|
||||
it("has Run Heartbeat button in runs tab", () => {
|
||||
expect(agentDetailViewContent).toMatch(/Run Heartbeat/);
|
||||
it("has Run Now button in runs tab", () => {
|
||||
expect(agentDetailViewContent).toMatch(/Run Now/);
|
||||
expect(agentDetailViewContent).toMatch(/handleRunHeartbeat/);
|
||||
});
|
||||
|
||||
|
||||
@@ -1130,9 +1130,9 @@ function RunsTab({
|
||||
<button
|
||||
className="btn btn--sm btn--primary"
|
||||
onClick={() => void handleRunHeartbeat()}
|
||||
aria-label={`Run heartbeat for ${agentName ?? agentId}`}
|
||||
aria-label={`Run now for ${agentName ?? agentId}`}
|
||||
>
|
||||
<Activity size={14} /> Run Heartbeat
|
||||
<Activity size={14} /> Run Now
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -1405,9 +1405,9 @@ function RunsTab({
|
||||
<button
|
||||
className="btn btn--sm btn--primary"
|
||||
onClick={() => void handleRunHeartbeat()}
|
||||
aria-label={`Run heartbeat for ${agentName ?? agentId}`}
|
||||
aria-label={`Run now for ${agentName ?? agentId}`}
|
||||
>
|
||||
<Activity size={14} /> Run Heartbeat
|
||||
<Activity size={14} /> Run Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -730,16 +730,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
)}
|
||||
{agent.state === "active" && (
|
||||
<>
|
||||
{agent.taskId && (
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
|
||||
title="Run Heartbeat"
|
||||
aria-label={`Run heartbeat for ${agent.name}`}
|
||||
>
|
||||
<Activity size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
|
||||
title="Run Now"
|
||||
aria-label={`Run now for ${agent.name}`}
|
||||
>
|
||||
<Activity size={14} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||
@@ -776,16 +774,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
)}
|
||||
{agent.state === "running" && (
|
||||
<>
|
||||
{agent.taskId && (
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
disabled
|
||||
title="Run in progress"
|
||||
aria-label={`Heartbeat run in progress for ${agent.name}`}
|
||||
>
|
||||
<Activity size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
disabled
|
||||
title="Run in progress"
|
||||
aria-label={`Heartbeat run in progress for ${agent.name}`}
|
||||
>
|
||||
<Activity size={14} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||
@@ -990,16 +986,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
)}
|
||||
{agent.state === "active" && (
|
||||
<>
|
||||
{agent.taskId && (
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
|
||||
title="Run Heartbeat"
|
||||
aria-label={`Run heartbeat for ${agent.name}`}
|
||||
>
|
||||
<Activity size={14} /> Run
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
|
||||
title="Run Now"
|
||||
aria-label={`Run now for ${agent.name}`}
|
||||
>
|
||||
<Activity size={14} /> Run Now
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||
@@ -1036,16 +1030,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
)}
|
||||
{agent.state === "running" && (
|
||||
<>
|
||||
{agent.taskId && (
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
disabled
|
||||
title="Run in progress"
|
||||
aria-label={`Heartbeat run in progress for ${agent.name}`}
|
||||
>
|
||||
<Activity size={14} /> Running
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
disabled
|
||||
title="Run in progress"
|
||||
aria-label={`Heartbeat run in progress for ${agent.name}`}
|
||||
>
|
||||
<Activity size={14} /> Running
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||
|
||||
@@ -853,6 +853,55 @@ describe("AgentsView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Run Now button", () => {
|
||||
it("shows Run Now button for active agent without taskId", async () => {
|
||||
const activeWithoutTaskId = { ...mockAgents[1] };
|
||||
delete activeWithoutTaskId.taskId;
|
||||
mockFetchAgents.mockResolvedValue([
|
||||
mockAgents[0],
|
||||
activeWithoutTaskId,
|
||||
mockAgents[2],
|
||||
mockAgents[3],
|
||||
]);
|
||||
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTitle("Run Now")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("Run Now button calls startAgentRun for active agent without taskId", async () => {
|
||||
const activeWithoutTaskId = { ...mockAgents[1] };
|
||||
delete activeWithoutTaskId.taskId;
|
||||
mockFetchAgents.mockResolvedValue([
|
||||
mockAgents[0],
|
||||
activeWithoutTaskId,
|
||||
mockAgents[2],
|
||||
mockAgents[3],
|
||||
]);
|
||||
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTitle("Run Now")).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTitle("Run Now"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockStartAgentRun).toHaveBeenCalledWith(
|
||||
"agent-002",
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
source: "on_demand",
|
||||
triggerDetail: "Triggered from dashboard",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete agent", () => {
|
||||
it("shows Delete button for idle and terminated agents in default view", async () => {
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
Reference in New Issue
Block a user