fix(dashboard): drop terminated-state leftovers and AgentListModal duplicate Resume

Removes no-op `agents.filter((a) => true)` calls (a leftover from the
terminated AgentState refactor) flagged by eslint and updates affected
tests and fixtures so `terminated` is no longer referenced. Also:

- Deletes the duplicate `state === "paused"` render branch in
  AgentListModal list view that produced two "Resume" buttons.
- Updates the AgentDetailView help text to reflect the current
  deletable states ("idle or paused").
- Aligns the bundled-plugin-install test with the new auto-load
  behavior for already-installed enabled plugins.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-05 20:41:29 -07:00
parent 041eb894d8
commit d47e48fa78
14 changed files with 22 additions and 399 deletions

View File

@@ -221,7 +221,7 @@ describe("ensureBundledDependencyGraphPluginInstalled", () => {
expect(result).toBe("already-installed"); expect(result).toBe("already-installed");
expect(store.updatePlugin).not.toHaveBeenCalled(); expect(store.updatePlugin).not.toHaveBeenCalled();
expect(store.registerPlugin).not.toHaveBeenCalled(); expect(store.registerPlugin).not.toHaveBeenCalled();
expect(loader.loadPlugin).not.toHaveBeenCalled(); expect(loader.loadPlugin).toHaveBeenCalledWith(BUNDLED_PLUGIN_ID);
}); });
it("already installed with stale path → updates path to current bundled path", async () => { it("already installed with stale path → updates path to current bundled path", async () => {

View File

@@ -682,25 +682,6 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
</button> </button>
</> </>
)} )}
{agent.state === "paused" && (
<>
<button
className="btn btn--sm btn-task-create"
onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)}
title="Resume"
>
<Play size={14} /> Resume
</button>
<button
className="btn btn--sm btn--danger"
onClick={() => void handleDelete(agent.id, agent.name)}
title="Delete"
>
<Trash2 size={14} /> Delete
</button>
</>
)}
</div> </div>
</div> </div>
); );

View File

@@ -926,7 +926,7 @@ export function InlineCreateCard({
<div className="dep-dropdown agent-picker-dropdown" onMouseDown={(e) => e.preventDefault()}> <div className="dep-dropdown agent-picker-dropdown" onMouseDown={(e) => e.preventDefault()}>
<div className="dep-dropdown-search-header">Select agent</div> <div className="dep-dropdown-search-header">Select agent</div>
{agentsLoading && <div className="dep-dropdown-empty">Loading agents...</div>} {agentsLoading && <div className="dep-dropdown-empty">Loading agents...</div>}
{!agentsLoading && agents.filter((a) => true).map((a) => ( {!agentsLoading && agents.map((a) => (
<div <div
key={a.id} key={a.id}
className={`dep-dropdown-item${selectedAgentId === a.id ? " selected" : ""}`} className={`dep-dropdown-item${selectedAgentId === a.id ? " selected" : ""}`}
@@ -941,7 +941,7 @@ export function InlineCreateCard({
<span className="dep-dropdown-title">{a.name}</span> <span className="dep-dropdown-title">{a.name}</span>
</div> </div>
))} ))}
{!agentsLoading && agents.filter((a) => true).length === 0 && ( {!agentsLoading && agents.length === 0 && (
<div className="dep-dropdown-empty">No agents available</div> <div className="dep-dropdown-empty">No agents available</div>
)} )}
{selectedAgentId && ( {selectedAgentId && (

View File

@@ -386,7 +386,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
<div className="dep-dropdown agent-picker-dropdown" onMouseDown={(e) => e.preventDefault()}> <div className="dep-dropdown agent-picker-dropdown" onMouseDown={(e) => e.preventDefault()}>
<div className="dep-dropdown-search-header">Select agent</div> <div className="dep-dropdown-search-header">Select agent</div>
{agentsLoading && <div className="dep-dropdown-empty">Loading agents...</div>} {agentsLoading && <div className="dep-dropdown-empty">Loading agents...</div>}
{!agentsLoading && agents.filter((a) => true).map((a) => ( {!agentsLoading && agents.map((a) => (
<div <div
key={a.id} key={a.id}
className={`dep-dropdown-item${selectedAgentId === a.id ? " selected" : ""}`} className={`dep-dropdown-item${selectedAgentId === a.id ? " selected" : ""}`}
@@ -402,7 +402,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
<span className="dep-dropdown-title">{a.name}</span> <span className="dep-dropdown-title">{a.name}</span>
</div> </div>
))} ))}
{!agentsLoading && agents.filter((a) => true).length === 0 && ( {!agentsLoading && agents.length === 0 && (
<div className="dep-dropdown-empty">No agents available</div> <div className="dep-dropdown-empty">No agents available</div>
)} )}
{selectedAgentId && ( {selectedAgentId && (

View File

@@ -1720,7 +1720,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
> >
<div className="dep-dropdown-search-header">Select agent</div> <div className="dep-dropdown-search-header">Select agent</div>
{agentsLoading && <div className="dep-dropdown-empty">Loading agents...</div>} {agentsLoading && <div className="dep-dropdown-empty">Loading agents...</div>}
{!agentsLoading && agents.filter((a) => true).map((a) => ( {!agentsLoading && agents.map((a) => (
<div <div
key={a.id} key={a.id}
className={`dep-dropdown-item${selectedAgentId === a.id ? " selected" : ""}`} className={`dep-dropdown-item${selectedAgentId === a.id ? " selected" : ""}`}
@@ -1736,7 +1736,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
<span className="dep-dropdown-title">{a.name}</span> <span className="dep-dropdown-title">{a.name}</span>
</div> </div>
))} ))}
{!agentsLoading && agents.filter((a) => true).length === 0 && ( {!agentsLoading && agents.length === 0 && (
<div className="dep-dropdown-empty">No agents available</div> <div className="dep-dropdown-empty">No agents available</div>
)} )}
{selectedAgentId && ( {selectedAgentId && (

View File

@@ -2136,7 +2136,7 @@ export function TaskDetailContent({
{showAgentPicker && ( {showAgentPicker && (
<div className="agent-picker-dropdown"> <div className="agent-picker-dropdown">
{agentsLoading && <div className="agent-picker-loading">Loading agents...</div>} {agentsLoading && <div className="agent-picker-loading">Loading agents...</div>}
{!agentsLoading && agents.filter((a) => true).map((a) => ( {!agentsLoading && agents.map((a) => (
<button <button
key={a.id} key={a.id}
className={`agent-picker-item${task.assignedAgentId === a.id ? " selected" : ""}`} className={`agent-picker-item${task.assignedAgentId === a.id ? " selected" : ""}`}
@@ -2147,7 +2147,7 @@ export function TaskDetailContent({
<span className="agent-picker-role">{a.role}</span> <span className="agent-picker-role">{a.role}</span>
</button> </button>
))} ))}
{!agentsLoading && agents.filter((a) => true).length === 0 && ( {!agentsLoading && agents.length === 0 && (
<div className="agent-picker-empty">No agents available</div> <div className="agent-picker-empty">No agents available</div>
)} )}
</div> </div>

View File

@@ -673,9 +673,8 @@ export function TodoView({ projectId, addToast, onPlanningMode, onTaskCreated }:
> >
{agentsLoading ? ( {agentsLoading ? (
<div className="todo-agent-picker-loading">Loading agents...</div> <div className="todo-agent-picker-loading">Loading agents...</div>
) : agents.filter((agent) => true).length > 0 ? ( ) : agents.length > 0 ? (
agents agents
.filter((agent) => true)
.map((agent) => ( .map((agent) => (
<button <button
type="button" type="button"

View File

@@ -70,7 +70,7 @@ describe("AgentListModal", () => {
id: "agent-004", id: "agent-004",
name: "Test Agent 4", name: "Test Agent 4",
role: "reviewer" as AgentCapability, role: "reviewer" as AgentCapability,
state: "terminated" as AgentState, state: "error" as AgentState,
createdAt: new Date(Date.now() - 259200000).toISOString(), createdAt: new Date(Date.now() - 259200000).toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
metadata: {}, metadata: {},
@@ -202,8 +202,7 @@ describe("AgentListModal", () => {
expect(screen.getByText("idle")).toBeTruthy(); expect(screen.getByText("idle")).toBeTruthy();
expect(screen.getByText("active")).toBeTruthy(); expect(screen.getByText("active")).toBeTruthy();
expect(screen.getByText("paused")).toBeTruthy(); expect(screen.getByText("paused")).toBeTruthy();
// Terminated agents are hidden in default "All States" view expect(screen.getByText("error")).toBeTruthy();
expect(screen.queryByText("terminated")).toBeNull();
}); });
}); });
@@ -234,29 +233,6 @@ describe("AgentListModal", () => {
localStorage.removeItem(AGENT_VIEW_KEY); localStorage.removeItem(AGENT_VIEW_KEY);
}); });
it("shows terminated agents when explicitly filtered", async () => {
render(
<AgentListModal
isOpen={true}
onClose={mockOnClose}
addToast={mockAddToast}
/>
);
await waitFor(() => {
expect(screen.getByText("All States")).toBeTruthy();
});
// Switch to terminated filter
const filterSelect = screen.getByDisplayValue("All States");
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getByText("terminated")).toBeTruthy();
expect(screen.getByText("Test Agent 4")).toBeTruthy();
});
});
it("displays agent roles", async () => { it("displays agent roles", async () => {
render( render(
<AgentListModal <AgentListModal
@@ -619,7 +595,7 @@ describe("AgentListModal", () => {
fireEvent.click(stopButton); fireEvent.click(stopButton);
await waitFor(() => { await waitFor(() => {
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-002", "terminated", undefined); expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-002", "paused", undefined);
}); });
}); });
@@ -657,54 +633,6 @@ describe("AgentListModal", () => {
}); });
}); });
it("shows Start button for terminated agents", async () => {
render(
<AgentListModal
isOpen={true}
onClose={mockOnClose}
addToast={mockAddToast}
/>
);
await waitFor(() => {
expect(screen.getByText("All States")).toBeTruthy();
});
const filterSelect = screen.getByDisplayValue("All States");
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getByTitle("Start")).toBeTruthy();
});
});
it("starts terminated agent", async () => {
render(
<AgentListModal
isOpen={true}
onClose={mockOnClose}
addToast={mockAddToast}
/>
);
await waitFor(() => {
expect(screen.getByText("All States")).toBeTruthy();
});
const filterSelect = screen.getByDisplayValue("All States");
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getByTitle("Start")).toBeTruthy();
});
fireEvent.click(screen.getByTitle("Start"));
await waitFor(() => {
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-004", "active", undefined);
});
});
it("optimistically updates list card state before API resolves", async () => { it("optimistically updates list card state before API resolves", async () => {
let resolveTransition!: () => void; let resolveTransition!: () => void;
const transitionPromise = new Promise<Agent>((resolve) => { const transitionPromise = new Promise<Agent>((resolve) => {
@@ -846,7 +774,7 @@ describe("AgentListModal", () => {
}); });
describe("agent deletion", () => { describe("agent deletion", () => {
it("shows Delete button for idle agents in default view (terminated filtered out)", async () => { it("shows Delete button for idle agents in default view", async () => {
render( render(
<AgentListModal <AgentListModal
isOpen={true} isOpen={true}
@@ -856,14 +784,9 @@ describe("AgentListModal", () => {
); );
await waitFor(() => { await waitFor(() => {
// In default "All States" view, only idle agent (agent-001) should have delete button
// Terminated agents (agent-004) are filtered out
const deleteButtons = screen.getAllByTitle("Delete"); const deleteButtons = screen.getAllByTitle("Delete");
expect(deleteButtons.length).toBeGreaterThanOrEqual(1); expect(deleteButtons.length).toBeGreaterThanOrEqual(1);
}); });
// Verify terminated agent is not visible
expect(screen.queryByText("Test Agent 4")).toBeNull();
}); });
it("shows Delete button for paused agents in list view", async () => { it("shows Delete button for paused agents in list view", async () => {
@@ -902,115 +825,6 @@ describe("AgentListModal", () => {
}); });
}); });
it("shows Delete button for terminated agents when explicitly filtered", async () => {
render(
<AgentListModal
isOpen={true}
onClose={mockOnClose}
addToast={mockAddToast}
/>
);
await waitFor(() => {
expect(screen.getByText("All States")).toBeTruthy();
});
// Switch to terminated filter
const filterSelect = screen.getByDisplayValue("All States");
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getByText("Test Agent 4")).toBeTruthy();
// Now we should see the Delete button for terminated agent
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(1);
});
// Verify terminated agent exposes delete action
const terminatedCard = Array.from(document.querySelectorAll(".agent-card, .agent-board-card")).find(
(card) => card.textContent?.includes("agent-004")
) ?? null;
const terminatedDeleteBtn = (terminatedCard as Element | null)?.querySelector('[title="Delete"]');
expect(terminatedDeleteBtn).toBeTruthy();
});
it("confirms before deleting terminated agent (from terminated filter)", async () => {
mockConfirm.mockResolvedValueOnce(false);
render(
<AgentListModal
isOpen={true}
onClose={mockOnClose}
addToast={mockAddToast}
/>
);
await waitFor(() => {
expect(screen.getByText("All States")).toBeTruthy();
});
// Switch to terminated filter to see terminated agent
const filterSelect = screen.getByDisplayValue("All States");
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getByText("Test Agent 4")).toBeTruthy();
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(1);
});
// Find delete button for terminated agent (agent-004)
const terminatedCard = Array.from(document.querySelectorAll(".agent-card, .agent-board-card")).find(
(card) => card.textContent?.includes("agent-004")
) ?? null;
const terminatedDeleteBtn = (terminatedCard as Element | null)?.querySelector('[title="Delete"]') as HTMLElement;
fireEvent.click(terminatedDeleteBtn);
expect(mockConfirm).toHaveBeenCalledWith({
title: "Delete Agent",
message: 'Delete agent "Test Agent 4"? This cannot be undone.',
danger: true,
});
expect(mockDeleteAgent).not.toHaveBeenCalled();
});
it("deletes terminated agent after confirmation (from terminated filter)", async () => {
render(
<AgentListModal
isOpen={true}
onClose={mockOnClose}
addToast={mockAddToast}
/>
);
await waitFor(() => {
expect(screen.getByText("All States")).toBeTruthy();
});
// Switch to terminated filter to see terminated agent
const filterSelect = screen.getByDisplayValue("All States");
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getByText("Test Agent 4")).toBeTruthy();
});
// Find delete button for terminated agent (agent-004)
const terminatedCard = Array.from(document.querySelectorAll(".agent-card, .agent-board-card")).find(
(card) => card.textContent?.includes("agent-004")
) ?? null;
const terminatedDeleteBtn = (terminatedCard as Element | null)?.querySelector('[title="Delete"]') as HTMLElement;
fireEvent.click(terminatedDeleteBtn);
await waitFor(() => {
expect(mockDeleteAgent).toHaveBeenCalledWith("agent-004", undefined);
});
expect(mockAddToast).toHaveBeenCalledWith(
expect.stringContaining("deleted"),
"success"
);
});
it("deletes idle agent after confirmation (from default view)", async () => { it("deletes idle agent after confirmation (from default view)", async () => {
render( render(
@@ -1022,8 +836,6 @@ describe("AgentListModal", () => {
); );
await waitFor(() => { await waitFor(() => {
// Only idle agent (agent-001) should have delete button in default view
// Terminated agent (agent-004) is filtered out
const deleteButtons = screen.getAllByTitle("Delete"); const deleteButtons = screen.getAllByTitle("Delete");
expect(deleteButtons.length).toBeGreaterThanOrEqual(1); expect(deleteButtons.length).toBeGreaterThanOrEqual(1);
}); });
@@ -1376,10 +1188,8 @@ describe("AgentListModal", () => {
fireEvent.click(screen.getByTitle("Board view")); fireEvent.click(screen.getByTitle("Board view"));
await waitFor(() => { await waitFor(() => {
// Board view should render compact cards
// 4 agents total, but terminated (agent-004) is filtered out in default view
const boardCards = document.querySelectorAll(".agent-board-card"); const boardCards = document.querySelectorAll(".agent-board-card");
expect(boardCards.length).toBe(3); expect(boardCards.length).toBe(4);
}); });
// Check that board view elements are present // Check that board view elements are present
@@ -1440,10 +1250,8 @@ describe("AgentListModal", () => {
const listContainer = document.querySelector(".agent-list"); const listContainer = document.querySelector(".agent-list");
expect(listContainer).toBeTruthy(); expect(listContainer).toBeTruthy();
// Detailed cards should be present
// 4 agents total, but terminated (agent-004) is filtered out in default view
const agentCards = document.querySelectorAll(".agent-card"); const agentCards = document.querySelectorAll(".agent-card");
expect(agentCards.length).toBe(3); expect(agentCards.length).toBe(4);
}); });
}); });
}); });

View File

@@ -112,7 +112,7 @@ describe("AgentsView", () => {
id: "agent-004", id: "agent-004",
name: "Test Agent 4", name: "Test Agent 4",
role: "reviewer" as AgentCapability, role: "reviewer" as AgentCapability,
state: "terminated" as AgentState, state: "error" as AgentState,
totalInputTokens: 1, totalInputTokens: 1,
totalOutputTokens: 1, totalOutputTokens: 1,
createdAt: new Date(Date.now() - 259200000).toISOString(), createdAt: new Date(Date.now() - 259200000).toISOString(),
@@ -542,7 +542,7 @@ describe("AgentsView", () => {
expect(screen.getAllByText("idle").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("idle").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("active").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("active").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("paused").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("paused").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("terminated").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("error").length).toBeGreaterThanOrEqual(1);
}); });
}); });
@@ -591,19 +591,6 @@ describe("AgentsView", () => {
}); });
}); });
it("shows terminated agents when explicitly filtered", async () => {
render(<AgentsView addToast={mockAddToast} />);
await openControlsPanel();
// Switch to terminated filter
const filterSelect = screen.getByLabelText("Filter agents by state");
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getAllByText("terminated").length).toBeGreaterThanOrEqual(1);
});
});
it("displays agent task when working on one", async () => { it("displays agent task when working on one", async () => {
render(<AgentsView addToast={mockAddToast} />); render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => { await waitFor(() => {
@@ -1949,30 +1936,13 @@ describe("AgentsView", () => {
}); });
describe("delete agent", () => { describe("delete agent", () => {
it("shows Delete button for idle and terminated agents in default view", async () => { it("shows Delete button for idle and paused agents in default view", async () => {
render(<AgentsView addToast={mockAddToast} />); render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => { await waitFor(() => {
const deleteButtons = screen.getAllByTitle("Delete"); const deleteButtons = screen.getAllByTitle("Delete");
expect(deleteButtons.length).toBeGreaterThanOrEqual(2); expect(deleteButtons.length).toBeGreaterThanOrEqual(2);
}); });
expect(screen.getAllByText("Test Agent 4").length).toBeGreaterThan(0);
});
it("shows Delete button for terminated agents when explicitly filtered", async () => {
render(<AgentsView addToast={mockAddToast} />);
await openControlsPanel();
// Switch to terminated filter
const filterSelect = screen.getByLabelText("Filter agents by state");
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getAllByText("Test Agent 4").length).toBeGreaterThan(0);
// Now we should see the Delete button for terminated agent
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(1);
});
}); });
it("does not show Delete button for active agents", async () => { it("does not show Delete button for active agents", async () => {
@@ -1997,65 +1967,6 @@ describe("AgentsView", () => {
}); });
}); });
it("confirms before deleting terminated agent (from terminated filter)", async () => {
mockConfirm.mockResolvedValueOnce(false);
render(<AgentsView addToast={mockAddToast} />);
await openControlsPanel();
// Switch to terminated filter to see terminated agent
const filterSelect = screen.getByLabelText("Filter agents by state");
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getAllByText("Test Agent 4").length).toBeGreaterThan(0);
// Click the delete button for the terminated agent (agent-004)
const terminatedCard = Array.from(document.querySelectorAll(".agent-card")).find(
(card) => card.textContent?.includes("agent-004")
) ?? null;
const terminatedDeleteBtn = (terminatedCard as Element | null)?.querySelector('[title="Delete"]') as HTMLElement;
expect(terminatedDeleteBtn).toBeTruthy();
fireEvent.click(terminatedDeleteBtn);
});
expect(mockConfirm).toHaveBeenCalledWith({
title: "Delete Agent",
message: 'Delete agent "Test Agent 4"? This cannot be undone.',
danger: true,
});
expect(mockDeleteAgent).not.toHaveBeenCalled();
});
it("deletes terminated agent after confirmation (from terminated filter)", async () => {
render(<AgentsView addToast={mockAddToast} />);
await openControlsPanel();
// Switch to terminated filter to see terminated agent
const filterSelect = screen.getByLabelText("Filter agents by state");
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getAllByText("Test Agent 4").length).toBeGreaterThan(0);
});
// Find the delete button for terminated agent (agent-004)
const terminatedCard = Array.from(document.querySelectorAll(".agent-card")).find(
(card) => card.textContent?.includes("agent-004")
) ?? null;
const terminatedDeleteBtn = (terminatedCard as Element | null)?.querySelector('[title="Delete"]') as HTMLElement;
fireEvent.click(terminatedDeleteBtn);
await waitFor(() => {
expect(mockDeleteAgent).toHaveBeenCalledWith("agent-004", undefined);
});
expect(mockAddToast).toHaveBeenCalledWith(
expect.stringContaining("deleted"),
"success"
);
});
it("deletes idle agent after confirmation (from default view)", async () => { it("deletes idle agent after confirmation (from default view)", async () => {
render(<AgentsView addToast={mockAddToast} />); render(<AgentsView addToast={mockAddToast} />);

View File

@@ -865,23 +865,6 @@ describe("NewTaskModal", () => {
}); });
}); });
it("excludes terminated agents from picker", async () => {
const { fetchAgents } = await import("../../api");
vi.mocked(fetchAgents).mockResolvedValueOnce([
{ id: "agent-1", name: "Active Agent", role: "executor", state: "active" as const, metadata: {}, createdAt: "", updatedAt: "" },
{ id: "agent-2", name: "Terminated Agent", role: "executor", state: "terminated" as const, metadata: {}, createdAt: "", updatedAt: "" },
]);
renderNewTaskModal();
fireEvent.click(screen.getByTestId("new-task-agent-button"));
await waitFor(() => {
expect(screen.getByText("Active Agent")).toBeTruthy();
expect(screen.queryByText("Terminated Agent")).toBeNull();
});
});
it("shows selected agent name in button", async () => { it("shows selected agent name in button", async () => {
const { fetchAgents } = await import("../../api"); const { fetchAgents } = await import("../../api");
vi.mocked(fetchAgents).mockResolvedValueOnce([ vi.mocked(fetchAgents).mockResolvedValueOnce([

View File

@@ -73,7 +73,6 @@ describe("TodoView", () => {
mockCreateTask.mockResolvedValue({ id: "FN-999" }); mockCreateTask.mockResolvedValue({ id: "FN-999" });
mockFetchAgents.mockResolvedValue([ mockFetchAgents.mockResolvedValue([
{ id: "agent-1", name: "Builder", role: "engineer", state: "active" }, { id: "agent-1", name: "Builder", role: "engineer", state: "active" },
{ id: "agent-2", name: "Terminated", role: "reviewer", state: "terminated" },
]); ]);
mockUseTodoLists.mockReturnValue(createMockTodoLists()); mockUseTodoLists.mockReturnValue(createMockTodoLists());
}); });
@@ -460,7 +459,6 @@ describe("TodoView", () => {
expect(mockFetchAgents).toHaveBeenCalledWith(undefined, "project-1"); expect(mockFetchAgents).toHaveBeenCalledWith(undefined, "project-1");
}); });
expect(screen.getByText("Builder")).toBeInTheDocument(); expect(screen.getByText("Builder")).toBeInTheDocument();
expect(screen.queryByText("Terminated")).not.toBeInTheDocument();
}); });
it("selecting an agent creates task assigned to that agent", async () => { it("selecting an agent creates task assigned to that agent", async () => {

View File

@@ -222,7 +222,7 @@ describe("AgentsView mobile adaptations", () => {
expect(select).toBeTruthy(); expect(select).toBeTruthy();
const optionValues = Array.from(select.options).map((option) => option.value); const optionValues = Array.from(select.options).map((option) => option.value);
expect(optionValues).toEqual(["all", "idle", "active", "running", "paused", "error", "terminated"]); expect(optionValues).toEqual(["all", "idle", "active", "running", "paused", "error"]);
}); });
}); });

View File

@@ -36,28 +36,6 @@ describe("getAgentHealthStatus", () => {
vi.useRealTimers(); vi.useRealTimers();
}); });
// ── Terminal states ──────────────────────────────────────────────────────
describe("terminated state", () => {
it('returns "Terminated" for terminated agents', () => {
const agent = makeAgent({ state: "terminated" });
const status = getAgentHealthStatus(agent);
expect(status.label).toBe("Terminated");
expect(status.stateDerived).toBe(true);
expect(status.color).toBe("var(--state-error-text)");
});
it("ignores heartbeat data for terminated agents", () => {
const agent = makeAgent({
state: "terminated",
lastHeartbeatAt: new Date(FIXED_NOW - 1000).toISOString(),
});
const status = getAgentHealthStatus(agent);
expect(status.label).toBe("Terminated");
expect(status.stateDerived).toBe(true);
});
});
describe("error state", () => { describe("error state", () => {
it('returns "Error" for error agents without lastError', () => { it('returns "Error" for error agents without lastError', () => {
const agent = makeAgent({ state: "error" }); const agent = makeAgent({ state: "error" });
@@ -217,13 +195,6 @@ describe("getAgentHealthStatus", () => {
expect(status.color).toBe("var(--text-secondary)"); expect(status.color).toBe("var(--text-secondary)");
}); });
it('returns "Idle" for terminated agents without heartbeat (edge case)', () => {
// Although terminated state takes precedence, testing the fallback
const agent = makeAgent({ state: "idle", lastHeartbeatAt: undefined });
const status = getAgentHealthStatus(agent);
expect(status.label).toBe("Idle");
expect(status.stateDerived).toBe(false);
});
}); });
// ── Healthy vs Unresponsive ─────────────────────────────────────────────── // ── Healthy vs Unresponsive ───────────────────────────────────────────────
@@ -372,12 +343,6 @@ describe("getAgentHealthStatus", () => {
expectedLabel: "OOM", expectedLabel: "OOM",
expectedStateDerived: false, expectedStateDerived: false,
}, },
{
name: "terminated",
agent: makeAgent({ state: "terminated" }),
expectedLabel: "Terminated",
expectedStateDerived: true,
},
{ {
name: "healthy", name: "healthy",
agent: makeAgent({ state: "active", lastHeartbeatAt: new Date(FIXED_NOW - 10_000).toISOString() }), agent: makeAgent({ state: "active", lastHeartbeatAt: new Date(FIXED_NOW - 10_000).toISOString() }),
@@ -461,7 +426,6 @@ describe("getAgentHealthStatus", () => {
it("returns consistent icons for all states", () => { it("returns consistent icons for all states", () => {
const testCases: Array<{ agent: ReturnType<typeof makeAgent>; expectedIconType: string }> = [ const testCases: Array<{ agent: ReturnType<typeof makeAgent>; expectedIconType: string }> = [
{ agent: makeAgent({ state: "terminated" }), expectedIconType: "Square" },
{ agent: makeAgent({ state: "error" }), expectedIconType: "Activity" }, { agent: makeAgent({ state: "error" }), expectedIconType: "Activity" },
{ agent: makeAgent({ state: "paused" }), expectedIconType: "Pause" }, { agent: makeAgent({ state: "paused" }), expectedIconType: "Pause" },
{ agent: makeAgent({ state: "running" }), expectedIconType: "Activity" }, { agent: makeAgent({ state: "running" }), expectedIconType: "Activity" },
@@ -509,14 +473,14 @@ describe("getAgentHealthColorVar", () => {
}); });
it("extracts CSS variable name from health status color", () => { it("extracts CSS variable name from health status color", () => {
const agent = makeAgent({ state: "terminated" }); const agent = makeAgent({ state: "error" });
const colorVar = getAgentHealthColorVar(agent); const colorVar = getAgentHealthColorVar(agent);
expect(colorVar).toBe("--state-error-text"); expect(colorVar).toBe("--state-error-text");
}); });
it("returns full color for non-variable colors (fallback)", () => { it("returns full color for non-variable colors (fallback)", () => {
// This shouldn't happen in practice, but testing the fallback // This shouldn't happen in practice, but testing the fallback
const agent = makeAgent({ state: "terminated" }); const agent = makeAgent({ state: "error" });
const status = getAgentHealthStatus(agent); const status = getAgentHealthStatus(agent);
// The function should return the variable name in var() format // The function should return the variable name in var() format
expect(getAgentHealthColorVar(agent)).toBe(status.color.replace(/var\((--[^)]+)\)/, "$1")); expect(getAgentHealthColorVar(agent)).toBe(status.color.replace(/var\((--[^)]+)\)/, "$1"));
@@ -557,7 +521,6 @@ describe("AgentHealthStatus reason field", () => {
}); });
it.each([ it.each([
{ name: "terminated", agent: makeAgent({ state: "terminated" }) },
{ name: "error", agent: makeAgent({ state: "error" }) }, { name: "error", agent: makeAgent({ state: "error" }) },
{ name: "paused", agent: makeAgent({ state: "paused" }) }, { name: "paused", agent: makeAgent({ state: "paused" }) },
{ name: "running", agent: makeAgent({ state: "running" }) }, { name: "running", agent: makeAgent({ state: "running" }) },

View File

@@ -562,26 +562,6 @@ describe("Agent runs routes (with HeartbeatMonitor)", () => {
}); });
expect(mockExecuteHeartbeat).toHaveBeenCalledTimes(1); expect(mockExecuteHeartbeat).toHaveBeenCalledTimes(1);
}); });
it("terminated agent also unpauses tasks paused by that agent", async () => {
(store.getTasksByAssignedAgent as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
{ id: "FN-9", paused: true, pausedByAgentId: "agent-001" },
]);
mockUpdateAgentState.mockResolvedValue({ id: "agent-001", state: "terminated" });
const response = await request(
app,
"POST",
"/api/agents/agent-001/state",
JSON.stringify({ state: "terminated" }),
{ "content-type": "application/json" },
);
expect(response.status).toBe(200);
await vi.waitFor(() => {
expect(store.pauseTask).toHaveBeenCalledWith("FN-9", false);
});
});
it("resuming to active does not auto-trigger heartbeat when disabled", async () => { it("resuming to active does not auto-trigger heartbeat when disabled", async () => {
mockGetAgent.mockResolvedValue({ mockGetAgent.mockResolvedValue({
id: "agent-001", id: "agent-001",