diff --git a/frontend/src/components/WorkItemDetailPanel.agents.test.tsx b/frontend/src/components/WorkItemDetailPanel.agents.test.tsx new file mode 100644 index 00000000..4f99df28 --- /dev/null +++ b/frontend/src/components/WorkItemDetailPanel.agents.test.tsx @@ -0,0 +1,379 @@ +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(); + }); +}); diff --git a/frontend/src/components/WorkItemDetailPanel.data.test.tsx b/frontend/src/components/WorkItemDetailPanel.data.test.tsx new file mode 100644 index 00000000..9d762890 --- /dev/null +++ b/frontend/src/components/WorkItemDetailPanel.data.test.tsx @@ -0,0 +1,329 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { TestResultsResponse, TokenCostResponse } from "../api/client"; + +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, +}; + +const sampleTestResults: TestResultsResponse = { + unit: [ + { name: "test_add", status: "pass", details: null }, + { name: "test_subtract", status: "fail", details: "expected 3, got 4" }, + ], + integration: [{ name: "test_api_endpoint", status: "pass", details: 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 - Test Results", () => { + it("shows empty test results message when no results exist", async () => { + mockedGetTestResults.mockResolvedValue(null); + + render( + {}} + />, + ); + + await waitFor(() => { + expect(screen.getByTestId("test-results-empty")).toBeInTheDocument(); + }); + expect(screen.getByText("No test results recorded")).toBeInTheDocument(); + }); + + it("shows unit and integration test results when available", async () => { + mockedGetTestResults.mockResolvedValue(sampleTestResults); + + render( + {}} + />, + ); + + await waitFor(() => { + expect(screen.getByTestId("test-results-content")).toBeInTheDocument(); + }); + + // Unit test section + expect(screen.getByTestId("test-section-unit")).toBeInTheDocument(); + expect( + screen.getByText("Unit Tests (1 passed, 1 failed)"), + ).toBeInTheDocument(); + + // Integration test section + expect(screen.getByTestId("test-section-integration")).toBeInTheDocument(); + expect( + screen.getByText("Integration Tests (1 passed, 0 failed)"), + ).toBeInTheDocument(); + }); + + it("shows pass/fail status and details for each test", async () => { + mockedGetTestResults.mockResolvedValue(sampleTestResults); + + render( + {}} + />, + ); + + await waitFor(() => { + expect(screen.getByTestId("test-case-test_add")).toBeInTheDocument(); + }); + + // Passing test + expect(screen.getByTestId("test-status-test_add")).toHaveTextContent( + "PASS", + ); + expect(screen.getByText("test_add")).toBeInTheDocument(); + + // Failing test with details + expect(screen.getByTestId("test-status-test_subtract")).toHaveTextContent( + "FAIL", + ); + expect(screen.getByText("test_subtract")).toBeInTheDocument(); + expect(screen.getByTestId("test-details-test_subtract")).toHaveTextContent( + "expected 3, got 4", + ); + + // Integration test + expect( + screen.getByTestId("test-status-test_api_endpoint"), + ).toHaveTextContent("PASS"); + }); + + it("re-fetches test results when pipelineVersion changes", async () => { + mockedGetTestResults.mockResolvedValue(null); + + const { rerender } = render( + {}} + />, + ); + + await waitFor(() => { + expect(mockedGetTestResults).toHaveBeenCalledTimes(1); + }); + + // Update with new results and bump pipelineVersion. + mockedGetTestResults.mockResolvedValue(sampleTestResults); + + rerender( + {}} + />, + ); + + await waitFor(() => { + expect(mockedGetTestResults).toHaveBeenCalledTimes(2); + }); + + await waitFor(() => { + expect(screen.getByTestId("test-results-content")).toBeInTheDocument(); + }); + }); +}); + +describe("WorkItemDetailPanel - Token Cost", () => { + const sampleTokenCost: TokenCostResponse = { + total_cost_usd: 0.012345, + agents: [ + { + agent_name: "coder-1", + model: "claude-sonnet-4-6", + input_tokens: 1000, + output_tokens: 500, + cache_creation_input_tokens: 200, + cache_read_input_tokens: 100, + total_cost_usd: 0.009, + }, + { + agent_name: "coder-2", + model: null, + input_tokens: 800, + output_tokens: 300, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + total_cost_usd: 0.003345, + }, + ], + }; + + it("shows empty state when no token data exists", async () => { + mockedGetTokenCost.mockResolvedValue({ total_cost_usd: 0, agents: [] }); + + render( + {}} + />, + ); + + await waitFor(() => { + expect(screen.getByTestId("token-cost-empty")).toBeInTheDocument(); + }); + expect(screen.getByText("No token data recorded")).toBeInTheDocument(); + }); + + it("shows per-agent breakdown and total cost when data exists", async () => { + mockedGetTokenCost.mockResolvedValue(sampleTokenCost); + + render( + {}} + />, + ); + + await waitFor(() => { + expect(screen.getByTestId("token-cost-content")).toBeInTheDocument(); + }); + + expect(screen.getByTestId("token-cost-total")).toHaveTextContent( + "$0.012345", + ); + expect(screen.getByTestId("token-cost-agent-coder-1")).toBeInTheDocument(); + expect(screen.getByTestId("token-cost-agent-coder-2")).toBeInTheDocument(); + }); + + it("shows agent name and model when model is present", async () => { + mockedGetTokenCost.mockResolvedValue(sampleTokenCost); + + render( + {}} + />, + ); + + await waitFor(() => { + expect( + screen.getByTestId("token-cost-agent-coder-1"), + ).toBeInTheDocument(); + }); + + const agentRow = screen.getByTestId("token-cost-agent-coder-1"); + expect(agentRow).toHaveTextContent("coder-1"); + expect(agentRow).toHaveTextContent("claude-sonnet-4-6"); + }); + + it("shows agent name without model when model is null", async () => { + mockedGetTokenCost.mockResolvedValue(sampleTokenCost); + + render( + {}} + />, + ); + + await waitFor(() => { + expect( + screen.getByTestId("token-cost-agent-coder-2"), + ).toBeInTheDocument(); + }); + + const agentRow = screen.getByTestId("token-cost-agent-coder-2"); + expect(agentRow).toHaveTextContent("coder-2"); + expect(agentRow).not.toHaveTextContent("null"); + }); + + it("re-fetches token cost when pipelineVersion changes", async () => { + mockedGetTokenCost.mockResolvedValue({ total_cost_usd: 0, agents: [] }); + + const { rerender } = render( + {}} + />, + ); + + await waitFor(() => { + expect(mockedGetTokenCost).toHaveBeenCalledTimes(1); + }); + + mockedGetTokenCost.mockResolvedValue(sampleTokenCost); + + rerender( + {}} + />, + ); + + await waitFor(() => { + expect(mockedGetTokenCost).toHaveBeenCalledTimes(2); + }); + + await waitFor(() => { + expect(screen.getByTestId("token-cost-content")).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/components/WorkItemDetailPanel.test.tsx b/frontend/src/components/WorkItemDetailPanel.test.tsx index c4e3ce59..6a4de598 100644 --- a/frontend/src/components/WorkItemDetailPanel.test.tsx +++ b/frontend/src/components/WorkItemDetailPanel.test.tsx @@ -1,7 +1,5 @@ -import { act, render, screen, waitFor } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { AgentEvent, AgentInfo } from "../api/agents"; -import type { TestResultsResponse, TokenCostResponse } from "../api/client"; vi.mock("../api/client", async () => { const actual = @@ -46,14 +44,6 @@ const DEFAULT_CONTENT = { agent: null, }; -const sampleTestResults: TestResultsResponse = { - unit: [ - { name: "test_add", status: "pass", details: null }, - { name: "test_subtract", status: "fail", details: "expected 3, got 4" }, - ], - integration: [{ name: "test_api_endpoint", status: "pass", details: null }], -}; - beforeEach(() => { vi.clearAllMocks(); mockedGetWorkItemContent.mockResolvedValue(DEFAULT_CONTENT); @@ -214,325 +204,6 @@ describe("WorkItemDetailPanel", () => { }); }); -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(); - }); -}); - describe("WorkItemDetailPanel - Assigned Agent", () => { it("shows assigned agent name when agent front matter field is set", async () => { mockedGetWorkItemContent.mockResolvedValue({ @@ -586,264 +257,3 @@ describe("WorkItemDetailPanel - Assigned Agent", () => { expect(agentEl).not.toHaveTextContent("assigned"); }); }); - -describe("WorkItemDetailPanel - Test Results", () => { - it("shows empty test results message when no results exist", async () => { - mockedGetTestResults.mockResolvedValue(null); - - render( - {}} - />, - ); - - await waitFor(() => { - expect(screen.getByTestId("test-results-empty")).toBeInTheDocument(); - }); - expect(screen.getByText("No test results recorded")).toBeInTheDocument(); - }); - - it("shows unit and integration test results when available", async () => { - mockedGetTestResults.mockResolvedValue(sampleTestResults); - - render( - {}} - />, - ); - - await waitFor(() => { - expect(screen.getByTestId("test-results-content")).toBeInTheDocument(); - }); - - // Unit test section - expect(screen.getByTestId("test-section-unit")).toBeInTheDocument(); - expect( - screen.getByText("Unit Tests (1 passed, 1 failed)"), - ).toBeInTheDocument(); - - // Integration test section - expect(screen.getByTestId("test-section-integration")).toBeInTheDocument(); - expect( - screen.getByText("Integration Tests (1 passed, 0 failed)"), - ).toBeInTheDocument(); - }); - - it("shows pass/fail status and details for each test", async () => { - mockedGetTestResults.mockResolvedValue(sampleTestResults); - - render( - {}} - />, - ); - - await waitFor(() => { - expect(screen.getByTestId("test-case-test_add")).toBeInTheDocument(); - }); - - // Passing test - expect(screen.getByTestId("test-status-test_add")).toHaveTextContent( - "PASS", - ); - expect(screen.getByText("test_add")).toBeInTheDocument(); - - // Failing test with details - expect(screen.getByTestId("test-status-test_subtract")).toHaveTextContent( - "FAIL", - ); - expect(screen.getByText("test_subtract")).toBeInTheDocument(); - expect(screen.getByTestId("test-details-test_subtract")).toHaveTextContent( - "expected 3, got 4", - ); - - // Integration test - expect( - screen.getByTestId("test-status-test_api_endpoint"), - ).toHaveTextContent("PASS"); - }); - - it("re-fetches test results when pipelineVersion changes", async () => { - mockedGetTestResults.mockResolvedValue(null); - - const { rerender } = render( - {}} - />, - ); - - await waitFor(() => { - expect(mockedGetTestResults).toHaveBeenCalledTimes(1); - }); - - // Update with new results and bump pipelineVersion. - mockedGetTestResults.mockResolvedValue(sampleTestResults); - - rerender( - {}} - />, - ); - - await waitFor(() => { - expect(mockedGetTestResults).toHaveBeenCalledTimes(2); - }); - - await waitFor(() => { - expect(screen.getByTestId("test-results-content")).toBeInTheDocument(); - }); - }); -}); - -describe("WorkItemDetailPanel - Token Cost", () => { - const sampleTokenCost: TokenCostResponse = { - total_cost_usd: 0.012345, - agents: [ - { - agent_name: "coder-1", - model: "claude-sonnet-4-6", - input_tokens: 1000, - output_tokens: 500, - cache_creation_input_tokens: 200, - cache_read_input_tokens: 100, - total_cost_usd: 0.009, - }, - { - agent_name: "coder-2", - model: null, - input_tokens: 800, - output_tokens: 300, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - total_cost_usd: 0.003345, - }, - ], - }; - - it("shows empty state when no token data exists", async () => { - mockedGetTokenCost.mockResolvedValue({ total_cost_usd: 0, agents: [] }); - - render( - {}} - />, - ); - - await waitFor(() => { - expect(screen.getByTestId("token-cost-empty")).toBeInTheDocument(); - }); - expect(screen.getByText("No token data recorded")).toBeInTheDocument(); - }); - - it("shows per-agent breakdown and total cost when data exists", async () => { - mockedGetTokenCost.mockResolvedValue(sampleTokenCost); - - render( - {}} - />, - ); - - await waitFor(() => { - expect(screen.getByTestId("token-cost-content")).toBeInTheDocument(); - }); - - expect(screen.getByTestId("token-cost-total")).toHaveTextContent( - "$0.012345", - ); - expect(screen.getByTestId("token-cost-agent-coder-1")).toBeInTheDocument(); - expect(screen.getByTestId("token-cost-agent-coder-2")).toBeInTheDocument(); - }); - - it("shows agent name and model when model is present", async () => { - mockedGetTokenCost.mockResolvedValue(sampleTokenCost); - - render( - {}} - />, - ); - - await waitFor(() => { - expect( - screen.getByTestId("token-cost-agent-coder-1"), - ).toBeInTheDocument(); - }); - - const agentRow = screen.getByTestId("token-cost-agent-coder-1"); - expect(agentRow).toHaveTextContent("coder-1"); - expect(agentRow).toHaveTextContent("claude-sonnet-4-6"); - }); - - it("shows agent name without model when model is null", async () => { - mockedGetTokenCost.mockResolvedValue(sampleTokenCost); - - render( - {}} - />, - ); - - await waitFor(() => { - expect( - screen.getByTestId("token-cost-agent-coder-2"), - ).toBeInTheDocument(); - }); - - const agentRow = screen.getByTestId("token-cost-agent-coder-2"); - expect(agentRow).toHaveTextContent("coder-2"); - expect(agentRow).not.toHaveTextContent("null"); - }); - - it("re-fetches token cost when pipelineVersion changes", async () => { - mockedGetTokenCost.mockResolvedValue({ total_cost_usd: 0, agents: [] }); - - const { rerender } = render( - {}} - />, - ); - - await waitFor(() => { - expect(mockedGetTokenCost).toHaveBeenCalledTimes(1); - }); - - mockedGetTokenCost.mockResolvedValue(sampleTokenCost); - - rerender( - {}} - />, - ); - - await waitFor(() => { - expect(mockedGetTokenCost).toHaveBeenCalledTimes(2); - }); - - await waitFor(() => { - expect(screen.getByTestId("token-cost-content")).toBeInTheDocument(); - }); - }); -});