330 lines
8.3 KiB
TypeScript
330 lines
8.3 KiB
TypeScript
|
|
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<typeof import("../api/client")>("../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(
|
||
|
|
<WorkItemDetailPanel
|
||
|
|
storyId="42_story_foo"
|
||
|
|
pipelineVersion={0}
|
||
|
|
onClose={() => {}}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
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(
|
||
|
|
<WorkItemDetailPanel
|
||
|
|
storyId="42_story_foo"
|
||
|
|
pipelineVersion={0}
|
||
|
|
onClose={() => {}}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
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(
|
||
|
|
<WorkItemDetailPanel
|
||
|
|
storyId="42_story_foo"
|
||
|
|
pipelineVersion={0}
|
||
|
|
onClose={() => {}}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
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(
|
||
|
|
<WorkItemDetailPanel
|
||
|
|
storyId="42_story_foo"
|
||
|
|
pipelineVersion={0}
|
||
|
|
onClose={() => {}}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(mockedGetTestResults).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Update with new results and bump pipelineVersion.
|
||
|
|
mockedGetTestResults.mockResolvedValue(sampleTestResults);
|
||
|
|
|
||
|
|
rerender(
|
||
|
|
<WorkItemDetailPanel
|
||
|
|
storyId="42_story_foo"
|
||
|
|
pipelineVersion={1}
|
||
|
|
onClose={() => {}}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
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(
|
||
|
|
<WorkItemDetailPanel
|
||
|
|
storyId="42_story_foo"
|
||
|
|
pipelineVersion={0}
|
||
|
|
onClose={() => {}}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
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(
|
||
|
|
<WorkItemDetailPanel
|
||
|
|
storyId="42_story_foo"
|
||
|
|
pipelineVersion={0}
|
||
|
|
onClose={() => {}}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
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(
|
||
|
|
<WorkItemDetailPanel
|
||
|
|
storyId="42_story_foo"
|
||
|
|
pipelineVersion={0}
|
||
|
|
onClose={() => {}}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
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(
|
||
|
|
<WorkItemDetailPanel
|
||
|
|
storyId="42_story_foo"
|
||
|
|
pipelineVersion={0}
|
||
|
|
onClose={() => {}}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
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(
|
||
|
|
<WorkItemDetailPanel
|
||
|
|
storyId="42_story_foo"
|
||
|
|
pipelineVersion={0}
|
||
|
|
onClose={() => {}}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(mockedGetTokenCost).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
mockedGetTokenCost.mockResolvedValue(sampleTokenCost);
|
||
|
|
|
||
|
|
rerender(
|
||
|
|
<WorkItemDetailPanel
|
||
|
|
storyId="42_story_foo"
|
||
|
|
pipelineVersion={1}
|
||
|
|
onClose={() => {}}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(mockedGetTokenCost).toHaveBeenCalledTimes(2);
|
||
|
|
});
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByTestId("token-cost-content")).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|