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:
@@ -682,25 +682,6 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -926,7 +926,7 @@ export function InlineCreateCard({
|
||||
<div className="dep-dropdown agent-picker-dropdown" onMouseDown={(e) => e.preventDefault()}>
|
||||
<div className="dep-dropdown-search-header">Select agent</div>
|
||||
{agentsLoading && <div className="dep-dropdown-empty">Loading agents...</div>}
|
||||
{!agentsLoading && agents.filter((a) => true).map((a) => (
|
||||
{!agentsLoading && agents.map((a) => (
|
||||
<div
|
||||
key={a.id}
|
||||
className={`dep-dropdown-item${selectedAgentId === a.id ? " selected" : ""}`}
|
||||
@@ -941,7 +941,7 @@ export function InlineCreateCard({
|
||||
<span className="dep-dropdown-title">{a.name}</span>
|
||||
</div>
|
||||
))}
|
||||
{!agentsLoading && agents.filter((a) => true).length === 0 && (
|
||||
{!agentsLoading && agents.length === 0 && (
|
||||
<div className="dep-dropdown-empty">No agents available</div>
|
||||
)}
|
||||
{selectedAgentId && (
|
||||
|
||||
@@ -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-search-header">Select agent</div>
|
||||
{agentsLoading && <div className="dep-dropdown-empty">Loading agents...</div>}
|
||||
{!agentsLoading && agents.filter((a) => true).map((a) => (
|
||||
{!agentsLoading && agents.map((a) => (
|
||||
<div
|
||||
key={a.id}
|
||||
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>
|
||||
</div>
|
||||
))}
|
||||
{!agentsLoading && agents.filter((a) => true).length === 0 && (
|
||||
{!agentsLoading && agents.length === 0 && (
|
||||
<div className="dep-dropdown-empty">No agents available</div>
|
||||
)}
|
||||
{selectedAgentId && (
|
||||
|
||||
@@ -1720,7 +1720,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
>
|
||||
<div className="dep-dropdown-search-header">Select agent</div>
|
||||
{agentsLoading && <div className="dep-dropdown-empty">Loading agents...</div>}
|
||||
{!agentsLoading && agents.filter((a) => true).map((a) => (
|
||||
{!agentsLoading && agents.map((a) => (
|
||||
<div
|
||||
key={a.id}
|
||||
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>
|
||||
</div>
|
||||
))}
|
||||
{!agentsLoading && agents.filter((a) => true).length === 0 && (
|
||||
{!agentsLoading && agents.length === 0 && (
|
||||
<div className="dep-dropdown-empty">No agents available</div>
|
||||
)}
|
||||
{selectedAgentId && (
|
||||
|
||||
@@ -2136,7 +2136,7 @@ export function TaskDetailContent({
|
||||
{showAgentPicker && (
|
||||
<div className="agent-picker-dropdown">
|
||||
{agentsLoading && <div className="agent-picker-loading">Loading agents...</div>}
|
||||
{!agentsLoading && agents.filter((a) => true).map((a) => (
|
||||
{!agentsLoading && agents.map((a) => (
|
||||
<button
|
||||
key={a.id}
|
||||
className={`agent-picker-item${task.assignedAgentId === a.id ? " selected" : ""}`}
|
||||
@@ -2147,7 +2147,7 @@ export function TaskDetailContent({
|
||||
<span className="agent-picker-role">{a.role}</span>
|
||||
</button>
|
||||
))}
|
||||
{!agentsLoading && agents.filter((a) => true).length === 0 && (
|
||||
{!agentsLoading && agents.length === 0 && (
|
||||
<div className="agent-picker-empty">No agents available</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -673,9 +673,8 @@ export function TodoView({ projectId, addToast, onPlanningMode, onTaskCreated }:
|
||||
>
|
||||
{agentsLoading ? (
|
||||
<div className="todo-agent-picker-loading">Loading agents...</div>
|
||||
) : agents.filter((agent) => true).length > 0 ? (
|
||||
) : agents.length > 0 ? (
|
||||
agents
|
||||
.filter((agent) => true)
|
||||
.map((agent) => (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -70,7 +70,7 @@ describe("AgentListModal", () => {
|
||||
id: "agent-004",
|
||||
name: "Test Agent 4",
|
||||
role: "reviewer" as AgentCapability,
|
||||
state: "terminated" as AgentState,
|
||||
state: "error" as AgentState,
|
||||
createdAt: new Date(Date.now() - 259200000).toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
metadata: {},
|
||||
@@ -202,8 +202,7 @@ describe("AgentListModal", () => {
|
||||
expect(screen.getByText("idle")).toBeTruthy();
|
||||
expect(screen.getByText("active")).toBeTruthy();
|
||||
expect(screen.getByText("paused")).toBeTruthy();
|
||||
// Terminated agents are hidden in default "All States" view
|
||||
expect(screen.queryByText("terminated")).toBeNull();
|
||||
expect(screen.getByText("error")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -234,29 +233,6 @@ describe("AgentListModal", () => {
|
||||
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 () => {
|
||||
render(
|
||||
<AgentListModal
|
||||
@@ -619,7 +595,7 @@ describe("AgentListModal", () => {
|
||||
fireEvent.click(stopButton);
|
||||
|
||||
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 () => {
|
||||
let resolveTransition!: () => void;
|
||||
const transitionPromise = new Promise<Agent>((resolve) => {
|
||||
@@ -846,7 +774,7 @@ describe("AgentListModal", () => {
|
||||
});
|
||||
|
||||
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(
|
||||
<AgentListModal
|
||||
isOpen={true}
|
||||
@@ -856,14 +784,9 @@ describe("AgentListModal", () => {
|
||||
);
|
||||
|
||||
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");
|
||||
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 () => {
|
||||
@@ -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 () => {
|
||||
|
||||
render(
|
||||
@@ -1022,8 +836,6 @@ describe("AgentListModal", () => {
|
||||
);
|
||||
|
||||
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");
|
||||
expect(deleteButtons.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
@@ -1376,10 +1188,8 @@ describe("AgentListModal", () => {
|
||||
fireEvent.click(screen.getByTitle("Board view"));
|
||||
|
||||
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");
|
||||
expect(boardCards.length).toBe(3);
|
||||
expect(boardCards.length).toBe(4);
|
||||
});
|
||||
|
||||
// Check that board view elements are present
|
||||
@@ -1440,10 +1250,8 @@ describe("AgentListModal", () => {
|
||||
const listContainer = document.querySelector(".agent-list");
|
||||
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");
|
||||
expect(agentCards.length).toBe(3);
|
||||
expect(agentCards.length).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,7 +112,7 @@ describe("AgentsView", () => {
|
||||
id: "agent-004",
|
||||
name: "Test Agent 4",
|
||||
role: "reviewer" as AgentCapability,
|
||||
state: "terminated" as AgentState,
|
||||
state: "error" as AgentState,
|
||||
totalInputTokens: 1,
|
||||
totalOutputTokens: 1,
|
||||
createdAt: new Date(Date.now() - 259200000).toISOString(),
|
||||
@@ -542,7 +542,7 @@ describe("AgentsView", () => {
|
||||
expect(screen.getAllByText("idle").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("active").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 () => {
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
await waitFor(() => {
|
||||
@@ -1949,30 +1936,13 @@ describe("AgentsView", () => {
|
||||
});
|
||||
|
||||
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} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const deleteButtons = screen.getAllByTitle("Delete");
|
||||
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 () => {
|
||||
@@ -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 () => {
|
||||
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
@@ -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 () => {
|
||||
const { fetchAgents } = await import("../../api");
|
||||
vi.mocked(fetchAgents).mockResolvedValueOnce([
|
||||
|
||||
@@ -73,7 +73,6 @@ describe("TodoView", () => {
|
||||
mockCreateTask.mockResolvedValue({ id: "FN-999" });
|
||||
mockFetchAgents.mockResolvedValue([
|
||||
{ id: "agent-1", name: "Builder", role: "engineer", state: "active" },
|
||||
{ id: "agent-2", name: "Terminated", role: "reviewer", state: "terminated" },
|
||||
]);
|
||||
mockUseTodoLists.mockReturnValue(createMockTodoLists());
|
||||
});
|
||||
@@ -460,7 +459,6 @@ describe("TodoView", () => {
|
||||
expect(mockFetchAgents).toHaveBeenCalledWith(undefined, "project-1");
|
||||
});
|
||||
expect(screen.getByText("Builder")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Terminated")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("selecting an agent creates task assigned to that agent", async () => {
|
||||
|
||||
@@ -222,7 +222,7 @@ describe("AgentsView mobile adaptations", () => {
|
||||
expect(select).toBeTruthy();
|
||||
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -36,28 +36,6 @@ describe("getAgentHealthStatus", () => {
|
||||
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", () => {
|
||||
it('returns "Error" for error agents without lastError', () => {
|
||||
const agent = makeAgent({ state: "error" });
|
||||
@@ -217,13 +195,6 @@ describe("getAgentHealthStatus", () => {
|
||||
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 ───────────────────────────────────────────────
|
||||
@@ -372,12 +343,6 @@ describe("getAgentHealthStatus", () => {
|
||||
expectedLabel: "OOM",
|
||||
expectedStateDerived: false,
|
||||
},
|
||||
{
|
||||
name: "terminated",
|
||||
agent: makeAgent({ state: "terminated" }),
|
||||
expectedLabel: "Terminated",
|
||||
expectedStateDerived: true,
|
||||
},
|
||||
{
|
||||
name: "healthy",
|
||||
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", () => {
|
||||
const testCases: Array<{ agent: ReturnType<typeof makeAgent>; expectedIconType: string }> = [
|
||||
{ agent: makeAgent({ state: "terminated" }), expectedIconType: "Square" },
|
||||
{ agent: makeAgent({ state: "error" }), expectedIconType: "Activity" },
|
||||
{ agent: makeAgent({ state: "paused" }), expectedIconType: "Pause" },
|
||||
{ agent: makeAgent({ state: "running" }), expectedIconType: "Activity" },
|
||||
@@ -509,14 +473,14 @@ describe("getAgentHealthColorVar", () => {
|
||||
});
|
||||
|
||||
it("extracts CSS variable name from health status color", () => {
|
||||
const agent = makeAgent({ state: "terminated" });
|
||||
const agent = makeAgent({ state: "error" });
|
||||
const colorVar = getAgentHealthColorVar(agent);
|
||||
expect(colorVar).toBe("--state-error-text");
|
||||
});
|
||||
|
||||
it("returns full color for non-variable colors (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);
|
||||
// The function should return the variable name in var() format
|
||||
expect(getAgentHealthColorVar(agent)).toBe(status.color.replace(/var\((--[^)]+)\)/, "$1"));
|
||||
@@ -557,7 +521,6 @@ describe("AgentHealthStatus reason field", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "terminated", agent: makeAgent({ state: "terminated" }) },
|
||||
{ name: "error", agent: makeAgent({ state: "error" }) },
|
||||
{ name: "paused", agent: makeAgent({ state: "paused" }) },
|
||||
{ name: "running", agent: makeAgent({ state: "running" }) },
|
||||
|
||||
Reference in New Issue
Block a user