import { act, render, screen } from "@testing-library/react"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { AgentConfigInfo, AgentEvent, AgentInfo } from "../api/agents"; import { agentsApi, subscribeAgentStream } from "../api/agents"; vi.mock("../api/agents", () => { const agentsApi = { listAgents: vi.fn(), getAgentConfig: vi.fn(), startAgent: vi.fn(), stopAgent: vi.fn(), reloadConfig: vi.fn(), }; return { agentsApi, subscribeAgentStream: vi.fn(() => () => {}) }; }); // Dynamic import so the mock is in place before the module loads const { AgentPanel } = await import("./AgentPanel"); const mockedSubscribeAgentStream = vi.mocked(subscribeAgentStream); const mockedAgents = { listAgents: vi.mocked(agentsApi.listAgents), getAgentConfig: vi.mocked(agentsApi.getAgentConfig), startAgent: vi.mocked(agentsApi.startAgent), }; const ROSTER: AgentConfigInfo[] = [ { name: "coder-1", role: "Full-stack engineer", model: "sonnet", allowed_tools: null, max_turns: 50, max_budget_usd: 5.0, }, ]; describe("AgentPanel active work list removed", () => { beforeAll(() => { Element.prototype.scrollIntoView = vi.fn(); }); beforeEach(() => { mockedAgents.getAgentConfig.mockResolvedValue(ROSTER); mockedAgents.listAgents.mockResolvedValue([]); }); it("does not render active agent entries even when agents are running", async () => { const agentList: AgentInfo[] = [ { story_id: "83_active", agent_name: "coder-1", status: "running", session_id: null, worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedAgents.listAgents.mockResolvedValue(agentList); const { container } = render(); // Roster badge should still be visible await screen.findByTestId("roster-badge-coder-1"); // No agent entry divs should exist expect( container.querySelector('[data-testid^="agent-entry-"]'), ).not.toBeInTheDocument(); }); }); describe("Running count visibility in header", () => { beforeAll(() => { Element.prototype.scrollIntoView = vi.fn(); }); beforeEach(() => { mockedAgents.getAgentConfig.mockResolvedValue(ROSTER); mockedAgents.listAgents.mockResolvedValue([]); }); // AC1: When no agents are running, "0 running" is NOT visible it("does not show running count when no agents are running", async () => { render(); // Wait for roster to load await screen.findByTestId("roster-badge-coder-1"); expect(screen.queryByText(/0 running/)).not.toBeInTheDocument(); }); // AC2: When agents are running, "N running" IS visible it("shows running count when agents are running", async () => { const agentList: AgentInfo[] = [ { story_id: "99_active", agent_name: "coder-1", status: "running", session_id: null, worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedAgents.listAgents.mockResolvedValue(agentList); render(); await screen.findByText(/1 running/); }); }); describe("RosterBadge availability state", () => { beforeAll(() => { Element.prototype.scrollIntoView = vi.fn(); }); beforeEach(() => { mockedAgents.getAgentConfig.mockResolvedValue(ROSTER); mockedAgents.listAgents.mockResolvedValue([]); }); it("shows a green dot for an idle agent", async () => { render(); const dot = await screen.findByTestId("roster-dot-coder-1"); // JSDOM normalizes #3fb950 to rgb(63, 185, 80) expect(dot.style.background).toBe("rgb(63, 185, 80)"); expect(dot.style.animation).toBe(""); }); it("shows grey badge styling for an idle agent", async () => { render(); const badge = await screen.findByTestId("roster-badge-coder-1"); // JSDOM normalizes #aaa18 to rgba(170, 170, 170, 0.094) and #aaa to rgb(170, 170, 170) expect(badge.style.background).toBe("rgba(170, 170, 170, 0.094)"); expect(badge.style.color).toBe("rgb(170, 170, 170)"); }); // AC1: roster badge always shows idle (grey) even when agent is running it("shows a static green dot for a running agent (roster always idle)", async () => { const agentList: AgentInfo[] = [ { story_id: "81_active", agent_name: "coder-1", status: "running", session_id: null, worktree_path: null, base_branch: null, log_session_id: null, }, ]; mockedAgents.listAgents.mockResolvedValue(agentList); render(); const dot = await screen.findByTestId("roster-dot-coder-1"); expect(dot.style.background).toBe("rgb(63, 185, 80)"); // Roster is always idle — no pulsing animation expect(dot.style.animation).toBe(""); }); // AC1: roster badge always shows idle (grey) even when agent is running it("shows grey (idle) badge styling for a running agent", async () => { const agentList: AgentInfo[] = [ { story_id: "81_active", agent_name: "coder-1", status: "running", session_id: null, worktree_path: null, base_branch: null, log_session_id: null, }, ]; mockedAgents.listAgents.mockResolvedValue(agentList); render(); const badge = await screen.findByTestId("roster-badge-coder-1"); // Always idle: grey background and grey text expect(badge.style.background).toBe("rgba(170, 170, 170, 0.094)"); expect(badge.style.color).toBe("rgb(170, 170, 170)"); }); // AC2: after agent completes and returns to roster, badge shows idle it("shows idle state after agent status changes from running to completed", async () => { const agentList: AgentInfo[] = [ { story_id: "81_completed", agent_name: "coder-1", status: "completed", session_id: null, worktree_path: null, base_branch: null, log_session_id: null, }, ]; mockedAgents.listAgents.mockResolvedValue(agentList); render(); const badge = await screen.findByTestId("roster-badge-coder-1"); const dot = screen.getByTestId("roster-dot-coder-1"); // Completed agent: badge is idle expect(badge.style.background).toBe("rgba(170, 170, 170, 0.094)"); expect(badge.style.color).toBe("rgb(170, 170, 170)"); expect(dot.style.animation).toBe(""); }); }); describe("Thinking trace block in agent stream UI", () => { beforeAll(() => { Element.prototype.scrollIntoView = vi.fn(); }); beforeEach(() => { mockedAgents.getAgentConfig.mockResolvedValue(ROSTER); mockedAgents.listAgents.mockResolvedValue([]); mockedSubscribeAgentStream.mockReturnValue(() => {}); }); // AC1+AC2: thinking block renders with fixed max-height and is visually distinct it("renders thinking block with max-height 96px when thinking event arrives", async () => { let emitEvent: ((e: AgentEvent) => void) | null = null; mockedSubscribeAgentStream.mockImplementation( (_storyId, _agentName, onEvent) => { emitEvent = onEvent; return () => {}; }, ); const agentList: AgentInfo[] = [ { story_id: "160_thinking", agent_name: "coder-1", status: "running", session_id: null, worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedAgents.listAgents.mockResolvedValue(agentList); render(); // Wait for the subscription to be set up await screen.findByTestId("roster-badge-coder-1"); // Fire a thinking event await act(async () => { emitEvent?.({ type: "thinking", story_id: "160_thinking", agent_name: "coder-1", text: "Let me consider the problem carefully...", }); }); const block = screen.getByTestId("thinking-block"); expect(block).toBeInTheDocument(); // AC2: fixed max-height expect(block.style.maxHeight).toBe("96px"); // AC2: overflow scrolling expect(block.style.overflowY).toBe("auto"); // AC1: visually distinct — italic monospace font expect(block.style.fontStyle).toBe("italic"); expect(block.style.fontFamily).toBe("monospace"); // Contains the thinking text expect(block.textContent).toContain( "Let me consider the problem carefully...", ); }); // AC3: thinking block renders the "thinking" label it("shows a thinking label in the block header", async () => { let emitEvent: ((e: AgentEvent) => void) | null = null; mockedSubscribeAgentStream.mockImplementation( (_storyId, _agentName, onEvent) => { emitEvent = onEvent; return () => {}; }, ); const agentList: AgentInfo[] = [ { story_id: "160_label", agent_name: "coder-1", status: "running", session_id: null, worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedAgents.listAgents.mockResolvedValue(agentList); render(); await screen.findByTestId("roster-badge-coder-1"); await act(async () => { emitEvent?.({ type: "thinking", story_id: "160_label", agent_name: "coder-1", text: "thinking...", }); }); const block = screen.getByTestId("thinking-block"); expect(block.textContent).toContain("thinking"); }); // AC4: regular text output renders outside the thinking container it("renders regular output outside the thinking block", async () => { let emitEvent: ((e: AgentEvent) => void) | null = null; mockedSubscribeAgentStream.mockImplementation( (_storyId, _agentName, onEvent) => { emitEvent = onEvent; return () => {}; }, ); const agentList: AgentInfo[] = [ { story_id: "160_output", agent_name: "coder-1", status: "running", session_id: null, worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedAgents.listAgents.mockResolvedValue(agentList); render(); await screen.findByTestId("roster-badge-coder-1"); // First: thinking event await act(async () => { emitEvent?.({ type: "thinking", story_id: "160_output", agent_name: "coder-1", text: "thinking deeply", }); }); // Then: text output event await act(async () => { emitEvent?.({ type: "output", story_id: "160_output", agent_name: "coder-1", text: "Here is the result.", }); }); const thinkingBlock = screen.getByTestId("thinking-block"); const outputArea = screen.getByTestId("agent-output-coder-1"); // Thinking still visible expect(thinkingBlock).toBeInTheDocument(); expect(thinkingBlock.textContent).toContain("thinking deeply"); // Output renders in a separate element, not inside the thinking block expect(outputArea).toBeInTheDocument(); expect(outputArea.textContent).toContain("Here is the result."); expect(thinkingBlock.contains(outputArea)).toBe(false); }); // AC5: thinking block remains visible when text starts it("keeps thinking block visible after output arrives", async () => { let emitEvent: ((e: AgentEvent) => void) | null = null; mockedSubscribeAgentStream.mockImplementation( (_storyId, _agentName, onEvent) => { emitEvent = onEvent; return () => {}; }, ); const agentList: AgentInfo[] = [ { story_id: "160_persist", agent_name: "coder-1", status: "running", session_id: null, worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedAgents.listAgents.mockResolvedValue(agentList); render(); await screen.findByTestId("roster-badge-coder-1"); await act(async () => { emitEvent?.({ type: "thinking", story_id: "160_persist", agent_name: "coder-1", text: "my thoughts", }); }); await act(async () => { emitEvent?.({ type: "output", story_id: "160_persist", agent_name: "coder-1", text: "final answer", }); }); // Thinking block still in the DOM after output arrives expect(screen.getByTestId("thinking-block")).toBeInTheDocument(); }); });