import { act, render, screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { AgentEvent, AgentInfo } from "../api/agents"; vi.mock("../api/client", async () => { const actual = await vi.importActual("../api/client"); return { ...actual, api: { ...actual.api, getWorkItemContent: vi.fn(), getTestResults: vi.fn(), getTokenCost: vi.fn(), }, }; }); vi.mock("../api/agents", () => ({ agentsApi: { listAgents: vi.fn(), getAgentConfig: vi.fn(), stopAgent: vi.fn(), startAgent: vi.fn(), }, subscribeAgentStream: vi.fn(() => () => {}), })); import { agentsApi, subscribeAgentStream } from "../api/agents"; import { api } from "../api/client"; const { WorkItemDetailPanel } = await import("./WorkItemDetailPanel"); const mockedGetWorkItemContent = vi.mocked(api.getWorkItemContent); const mockedGetTestResults = vi.mocked(api.getTestResults); const mockedGetTokenCost = vi.mocked(api.getTokenCost); const mockedListAgents = vi.mocked(agentsApi.listAgents); const mockedGetAgentConfig = vi.mocked(agentsApi.getAgentConfig); const mockedSubscribeAgentStream = vi.mocked(subscribeAgentStream); const DEFAULT_CONTENT = { content: "# Big Title\n\nSome content here.", stage: "current", name: "Big Title Story", agent: null, }; beforeEach(() => { vi.clearAllMocks(); mockedGetWorkItemContent.mockResolvedValue(DEFAULT_CONTENT); mockedGetTestResults.mockResolvedValue(null); mockedGetTokenCost.mockResolvedValue({ total_cost_usd: 0, agents: [] }); mockedListAgents.mockResolvedValue([]); mockedGetAgentConfig.mockResolvedValue([]); mockedSubscribeAgentStream.mockReturnValue(() => {}); }); afterEach(() => { vi.restoreAllMocks(); }); describe("WorkItemDetailPanel - Agent Logs", () => { it("shows placeholder when no agent is assigned to the story", async () => { render( {}} />, ); await screen.findByTestId("detail-panel-content"); const placeholder = screen.getByTestId("placeholder-agent-logs"); expect(placeholder).toBeInTheDocument(); expect(placeholder).toHaveTextContent("Coming soon"); }); it("shows agent name and running status when agent is running", async () => { const agentList: AgentInfo[] = [ { story_id: "42_story_test", agent_name: "coder-1", status: "running", session_id: null, worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedListAgents.mockResolvedValue(agentList); render( {}} />, ); const statusBadge = await screen.findByTestId("agent-status-badge"); expect(statusBadge).toHaveTextContent("coder-1"); expect(statusBadge).toHaveTextContent("running"); }); it("shows log output when agent emits output events", async () => { let emitEvent: ((e: AgentEvent) => void) | null = null; mockedSubscribeAgentStream.mockImplementation( (_storyId, _agentName, onEvent) => { emitEvent = onEvent; return () => {}; }, ); const agentList: AgentInfo[] = [ { story_id: "42_story_test", agent_name: "coder-1", status: "running", session_id: null, worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedListAgents.mockResolvedValue(agentList); render( {}} />, ); await screen.findByTestId("agent-status-badge"); await act(async () => { emitEvent?.({ type: "output", story_id: "42_story_test", agent_name: "coder-1", text: "Writing tests...", }); }); const logOutput = screen.getByTestId("agent-log-output"); expect(logOutput).toHaveTextContent("Writing tests..."); }); it("appends multiple output events to the log", async () => { let emitEvent: ((e: AgentEvent) => void) | null = null; mockedSubscribeAgentStream.mockImplementation( (_storyId, _agentName, onEvent) => { emitEvent = onEvent; return () => {}; }, ); const agentList: AgentInfo[] = [ { story_id: "42_story_test", agent_name: "coder-1", status: "running", session_id: null, worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedListAgents.mockResolvedValue(agentList); render( {}} />, ); await screen.findByTestId("agent-status-badge"); await act(async () => { emitEvent?.({ type: "output", story_id: "42_story_test", agent_name: "coder-1", text: "Line one\n", }); }); await act(async () => { emitEvent?.({ type: "output", story_id: "42_story_test", agent_name: "coder-1", text: "Line two\n", }); }); const logOutput = screen.getByTestId("agent-log-output"); expect(logOutput.textContent).toContain("Line one"); expect(logOutput.textContent).toContain("Line two"); }); it("updates status to completed after done event", async () => { let emitEvent: ((e: AgentEvent) => void) | null = null; mockedSubscribeAgentStream.mockImplementation( (_storyId, _agentName, onEvent) => { emitEvent = onEvent; return () => {}; }, ); const agentList: AgentInfo[] = [ { story_id: "42_story_test", agent_name: "coder-1", status: "running", session_id: null, worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedListAgents.mockResolvedValue(agentList); render( {}} />, ); await screen.findByTestId("agent-status-badge"); await act(async () => { emitEvent?.({ type: "done", story_id: "42_story_test", agent_name: "coder-1", session_id: "session-123", }); }); const statusBadge = screen.getByTestId("agent-status-badge"); expect(statusBadge).toHaveTextContent("completed"); }); it("shows failed status after error event", async () => { let emitEvent: ((e: AgentEvent) => void) | null = null; mockedSubscribeAgentStream.mockImplementation( (_storyId, _agentName, onEvent) => { emitEvent = onEvent; return () => {}; }, ); const agentList: AgentInfo[] = [ { story_id: "42_story_test", agent_name: "coder-1", status: "running", session_id: null, worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedListAgents.mockResolvedValue(agentList); render( {}} />, ); await screen.findByTestId("agent-status-badge"); await act(async () => { emitEvent?.({ type: "error", story_id: "42_story_test", agent_name: "coder-1", message: "Process failed", }); }); const statusBadge = screen.getByTestId("agent-status-badge"); expect(statusBadge).toHaveTextContent("failed"); const logOutput = screen.getByTestId("agent-log-output"); expect(logOutput.textContent).toContain("[ERROR] Process failed"); }); it("shows completed agent status without subscribing to stream", async () => { const agentList: AgentInfo[] = [ { story_id: "42_story_test", agent_name: "coder-1", status: "completed", session_id: "session-123", worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedListAgents.mockResolvedValue(agentList); render( {}} />, ); const statusBadge = await screen.findByTestId("agent-status-badge"); expect(statusBadge).toHaveTextContent("completed"); expect(mockedSubscribeAgentStream).not.toHaveBeenCalled(); }); it("shows failed agent status for a failed agent without subscribing to stream", async () => { const agentList: AgentInfo[] = [ { story_id: "42_story_test", agent_name: "coder-1", status: "failed", session_id: null, worktree_path: null, base_branch: "master", log_session_id: null, }, ]; mockedListAgents.mockResolvedValue(agentList); render( {}} />, ); const statusBadge = await screen.findByTestId("agent-status-badge"); expect(statusBadge).toHaveTextContent("failed"); expect(mockedSubscribeAgentStream).not.toHaveBeenCalled(); }); it("shows agent logs section (not placeholder) when agent is assigned", async () => { const agentList: AgentInfo[] = [ { story_id: "42_story_test", agent_name: "coder-1", status: "running", session_id: null, worktree_path: "/tmp/wt", base_branch: "master", log_session_id: null, }, ]; mockedListAgents.mockResolvedValue(agentList); render( {}} />, ); await screen.findByTestId("agent-logs-section"); expect( screen.queryByTestId("placeholder-agent-logs"), ).not.toBeInTheDocument(); }); });