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:
@@ -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 () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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} />);
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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" }) },
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user