story-kit: merge 309_story_show_token_cost_breakdown_in_expanded_work_item_detail_panel
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { act, 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 } from "../api/client";
|
||||
import type { TestResultsResponse, TokenCostResponse } from "../api/client";
|
||||
|
||||
vi.mock("../api/client", async () => {
|
||||
const actual =
|
||||
@@ -12,6 +12,7 @@ vi.mock("../api/client", async () => {
|
||||
...actual.api,
|
||||
getWorkItemContent: vi.fn(),
|
||||
getTestResults: vi.fn(),
|
||||
getTokenCost: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -30,6 +31,7 @@ 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 mockedSubscribeAgentStream = vi.mocked(subscribeAgentStream);
|
||||
|
||||
@@ -52,6 +54,7 @@ beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockedGetWorkItemContent.mockResolvedValue(DEFAULT_CONTENT);
|
||||
mockedGetTestResults.mockResolvedValue(null);
|
||||
mockedGetTokenCost.mockResolvedValue({ total_cost_usd: 0, agents: [] });
|
||||
mockedListAgents.mockResolvedValue([]);
|
||||
mockedSubscribeAgentStream.mockReturnValue(() => {});
|
||||
});
|
||||
@@ -608,3 +611,146 @@ describe("WorkItemDetailPanel - Test Results", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user