import { act, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { AgentEvent, AgentInfo } from "../api/agents"; import { agentsApi, subscribeAgentStream } from "../api/agents"; import { api } from "../api/client"; vi.mock("../api/client", () => ({ api: { getWorkItemContent: vi.fn(), }, })); vi.mock("../api/agents", () => ({ agentsApi: { listAgents: vi.fn(), }, subscribeAgentStream: vi.fn(() => () => {}), })); // Dynamic import so mocks are in place before the module loads const { WorkItemDetailPanel } = await import("./WorkItemDetailPanel"); const mockedGetWorkItemContent = vi.mocked(api.getWorkItemContent); const mockedListAgents = vi.mocked(agentsApi.listAgents); const mockedSubscribeAgentStream = vi.mocked(subscribeAgentStream); const DEFAULT_CONTENT = { content: "# Big Title\n\nSome content here.", stage: "current", name: "Big Title Story", }; describe("WorkItemDetailPanel", () => { beforeEach(() => { vi.clearAllMocks(); mockedGetWorkItemContent.mockResolvedValue(DEFAULT_CONTENT); mockedListAgents.mockResolvedValue([]); mockedSubscribeAgentStream.mockReturnValue(() => {}); }); it("renders the story name in the header", async () => { render( {}} />); await waitFor(() => { expect(screen.getByTestId("detail-panel-title")).toHaveTextContent( "Big Title Story", ); }); }); it("shows loading state initially", () => { render( {}} />); expect(screen.getByTestId("detail-panel-loading")).toBeInTheDocument(); }); it("calls onClose when close button is clicked", async () => { const onClose = vi.fn(); render(); const closeButton = screen.getByTestId("detail-panel-close"); closeButton.click(); expect(onClose).toHaveBeenCalledTimes(1); }); it("renders markdown headings with constrained inline font size", async () => { render( {}} />); await waitFor(() => { const content = screen.getByTestId("detail-panel-content"); const h1 = content.querySelector("h1"); expect(h1).not.toBeNull(); expect(h1?.style.fontSize).toBeTruthy(); }); }); }); describe("WorkItemDetailPanel - Agent Logs", () => { beforeEach(() => { vi.clearAllMocks(); mockedGetWorkItemContent.mockResolvedValue(DEFAULT_CONTENT); mockedListAgents.mockResolvedValue([]); mockedSubscribeAgentStream.mockReturnValue(() => {}); }); 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(); }); });