Story 27: Coverage tracking (full-stack)

Add end-to-end coverage tracking: backend collects vitest coverage,
records metrics with threshold/baseline tracking, and blocks acceptance
on regression. Frontend displays coverage in gate/review panels with
a "Collect Coverage" button. Includes 20 Rust tests, 17 Vitest tests,
and 14 Playwright E2E tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-19 14:45:57 +00:00
parent 8f0bc971bf
commit 8f684a6ca4
20 changed files with 1216 additions and 34 deletions

View File

@@ -33,6 +33,8 @@ vi.mock("../api/workflow", () => {
getReviewQueue: vi.fn(),
getReviewQueueAll: vi.fn(),
ensureAcceptance: vi.fn(),
recordCoverage: vi.fn(),
collectCoverage: vi.fn(),
},
};
});
@@ -390,4 +392,122 @@ describe("Chat review panel", () => {
expect(await screen.findByText("Ready to accept")).toBeInTheDocument();
});
it("shows coverage below threshold in gate panel (AC3)", async () => {
mockedWorkflow.getAcceptance.mockResolvedValueOnce({
can_accept: false,
reasons: ["Coverage below threshold (55.0% < 80.0%)."],
warning: null,
summary: { total: 3, passed: 3, failed: 0 },
missing_categories: [],
coverage_report: {
current_percent: 55.0,
threshold_percent: 80.0,
baseline_percent: null,
},
});
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
expect(await screen.findByText("Blocked")).toBeInTheDocument();
expect(await screen.findByText(/Coverage: 55\.0%/)).toBeInTheDocument();
expect(await screen.findByText(/threshold: 80\.0%/)).toBeInTheDocument();
expect(
await screen.findByText("Coverage below threshold (55.0% < 80.0%)."),
).toBeInTheDocument();
});
it("shows coverage regression in review panel (AC4)", async () => {
const story: ReviewStory = {
story_id: "27_protect_tests_and_coverage",
can_accept: false,
reasons: ["Coverage regression: 90.0% → 82.0% (threshold: 80.0%)."],
warning: null,
summary: { total: 4, passed: 4, failed: 0 },
missing_categories: [],
coverage_report: {
current_percent: 82.0,
threshold_percent: 80.0,
baseline_percent: 90.0,
},
};
mockedWorkflow.getReviewQueueAll.mockResolvedValueOnce({
stories: [story],
});
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
expect(
await screen.findByText(
"Coverage regression: 90.0% → 82.0% (threshold: 80.0%).",
),
).toBeInTheDocument();
expect(await screen.findByText(/Coverage: 82\.0%/)).toBeInTheDocument();
});
it("shows green coverage when above threshold (AC3)", async () => {
mockedWorkflow.getAcceptance.mockResolvedValueOnce({
can_accept: true,
reasons: [],
warning: null,
summary: { total: 5, passed: 5, failed: 0 },
missing_categories: [],
coverage_report: {
current_percent: 92.0,
threshold_percent: 80.0,
baseline_percent: 90.0,
},
});
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
expect(await screen.findByText("Ready to accept")).toBeInTheDocument();
expect(await screen.findByText(/Coverage: 92\.0%/)).toBeInTheDocument();
});
it("collect coverage button triggers collection and refreshes gate", async () => {
const mockedCollectCoverage = vi.mocked(workflowApi.collectCoverage);
mockedCollectCoverage.mockResolvedValueOnce({
current_percent: 85.0,
threshold_percent: 80.0,
baseline_percent: null,
});
mockedWorkflow.getAcceptance
.mockResolvedValueOnce({
can_accept: false,
reasons: ["No test results recorded for the story."],
warning: null,
summary: { total: 0, passed: 0, failed: 0 },
missing_categories: ["unit", "integration"],
})
.mockResolvedValueOnce({
can_accept: true,
reasons: [],
warning: null,
summary: { total: 5, passed: 5, failed: 0 },
missing_categories: [],
coverage_report: {
current_percent: 85.0,
threshold_percent: 80.0,
baseline_percent: null,
},
});
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
const collectButton = await screen.findByRole("button", {
name: "Collect Coverage",
});
await userEvent.click(collectButton);
await waitFor(() => {
expect(mockedCollectCoverage).toHaveBeenCalledWith({
story_id: "26_establish_tdd_workflow_and_gates",
});
});
expect(await screen.findByText(/Coverage: 85\.0%/)).toBeInTheDocument();
});
});