import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { api } from "../api/client"; import type { ReviewStory } from "../api/workflow"; import { workflowApi } from "../api/workflow"; import { Chat } from "./Chat"; vi.mock("../api/client", () => { const api = { getOllamaModels: vi.fn(), getAnthropicApiKeyExists: vi.fn(), getAnthropicModels: vi.fn(), getModelPreference: vi.fn(), setModelPreference: vi.fn(), cancelChat: vi.fn(), setAnthropicApiKey: vi.fn(), }; class ChatWebSocket { connect() {} close() {} sendChat() {} cancel() {} } return { api, ChatWebSocket }; }); vi.mock("../api/workflow", () => { return { workflowApi: { getAcceptance: vi.fn(), getReviewQueue: vi.fn(), getReviewQueueAll: vi.fn(), ensureAcceptance: vi.fn(), recordCoverage: vi.fn(), collectCoverage: vi.fn(), }, }; }); const mockedApi = { getOllamaModels: vi.mocked(api.getOllamaModels), getAnthropicApiKeyExists: vi.mocked(api.getAnthropicApiKeyExists), getAnthropicModels: vi.mocked(api.getAnthropicModels), getModelPreference: vi.mocked(api.getModelPreference), setModelPreference: vi.mocked(api.setModelPreference), cancelChat: vi.mocked(api.cancelChat), setAnthropicApiKey: vi.mocked(api.setAnthropicApiKey), }; const mockedWorkflow = { getAcceptance: vi.mocked(workflowApi.getAcceptance), getReviewQueue: vi.mocked(workflowApi.getReviewQueue), getReviewQueueAll: vi.mocked(workflowApi.getReviewQueueAll), ensureAcceptance: vi.mocked(workflowApi.ensureAcceptance), }; describe("Chat review panel", () => { beforeEach(() => { mockedApi.getOllamaModels.mockResolvedValue(["llama3.1"]); mockedApi.getAnthropicApiKeyExists.mockResolvedValue(true); mockedApi.getAnthropicModels.mockResolvedValue([]); mockedApi.getModelPreference.mockResolvedValue(null); mockedApi.setModelPreference.mockResolvedValue(true); mockedApi.cancelChat.mockResolvedValue(true); mockedApi.setAnthropicApiKey.mockResolvedValue(true); mockedWorkflow.getAcceptance.mockResolvedValue({ can_accept: false, reasons: ["No test results recorded for the story."], warning: null, summary: { total: 0, passed: 0, failed: 0 }, missing_categories: ["unit", "integration"], }); mockedWorkflow.getReviewQueueAll.mockResolvedValue({ stories: [] }); mockedWorkflow.ensureAcceptance.mockResolvedValue(true); }); it("shows an empty review queue state", async () => { render(); expect( await screen.findByText("Stories Awaiting Review"), ).toBeInTheDocument(); expect(await screen.findByText("0 ready / 0 total")).toBeInTheDocument(); expect( await screen.findByText("No stories waiting for review."), ).toBeInTheDocument(); const updatedLabels = await screen.findAllByText(/Updated/i); expect(updatedLabels.length).toBeGreaterThanOrEqual(2); }); it("renders review stories and proceeds", async () => { const story: ReviewStory = { story_id: "26_establish_tdd_workflow_and_gates", can_accept: true, reasons: [], warning: null, summary: { total: 3, passed: 3, failed: 0 }, missing_categories: [], }; mockedWorkflow.getReviewQueueAll .mockResolvedValueOnce({ stories: [story] }) .mockResolvedValueOnce({ stories: [] }); render(); expect(await screen.findByText(story.story_id)).toBeInTheDocument(); const proceedButton = screen.getByRole("button", { name: "Proceed" }); await userEvent.click(proceedButton); await waitFor(() => { expect(mockedWorkflow.ensureAcceptance).toHaveBeenCalledWith({ story_id: story.story_id, }); }); expect( await screen.findByText("No stories waiting for review."), ).toBeInTheDocument(); }); it("shows a review error when the queue fails to load", async () => { mockedWorkflow.getReviewQueueAll.mockRejectedValueOnce( new Error("Review queue failed"), ); render(); expect(await screen.findByText(/Review queue failed/i)).toBeInTheDocument(); expect( await screen.findByText(/Use Refresh to try again\./i), ).toBeInTheDocument(); expect( await screen.findByRole("button", { name: "Retry" }), ).toBeInTheDocument(); }); it("refreshes the review queue when clicking refresh", async () => { mockedWorkflow.getReviewQueueAll .mockResolvedValueOnce({ stories: [] }) .mockResolvedValueOnce({ stories: [] }); render(); const refreshButtons = await screen.findAllByRole("button", { name: "Refresh", }); const refreshButton = refreshButtons[0]; await userEvent.click(refreshButton); await waitFor(() => { expect(mockedWorkflow.getReviewQueueAll).toHaveBeenCalled(); }); }); it("disables proceed when a story is blocked", async () => { const story: ReviewStory = { story_id: "26_establish_tdd_workflow_and_gates", can_accept: false, reasons: ["Missing unit tests"], warning: null, summary: { total: 1, passed: 0, failed: 1 }, missing_categories: ["unit"], }; mockedWorkflow.getReviewQueueAll.mockResolvedValueOnce({ stories: [story], }); render(); expect(await screen.findByText(story.story_id)).toBeInTheDocument(); const blockedButton = screen.getByRole("button", { name: "Blocked" }); expect(blockedButton).toBeDisabled(); expect(await screen.findByText("Missing: unit")).toBeInTheDocument(); expect(await screen.findByText("Missing unit tests")).toBeInTheDocument(); }); it("shows gate panel blocked status with reasons (AC1/AC3)", async () => { mockedWorkflow.getAcceptance.mockResolvedValueOnce({ can_accept: false, reasons: ["No approved test plan for the story."], warning: null, summary: { total: 0, passed: 0, failed: 0 }, missing_categories: ["unit", "integration"], }); render(); expect(await screen.findByText("Blocked")).toBeInTheDocument(); expect( await screen.findByText("No approved test plan for the story."), ).toBeInTheDocument(); expect( await screen.findByText("Missing: unit, integration"), ).toBeInTheDocument(); expect( await screen.findByText(/0\/0 passing, 0 failing/), ).toBeInTheDocument(); }); it("shows gate panel ready status when all tests pass (AC1/AC3)", async () => { mockedWorkflow.getAcceptance.mockResolvedValueOnce({ can_accept: true, reasons: [], warning: null, summary: { total: 5, passed: 5, failed: 0 }, missing_categories: [], }); render(); expect(await screen.findByText("Ready to accept")).toBeInTheDocument(); expect( await screen.findByText(/5\/5 passing, 0 failing/), ).toBeInTheDocument(); }); it("shows failing badge and count in review panel (AC4/AC5)", async () => { const story: ReviewStory = { story_id: "26_establish_tdd_workflow_and_gates", can_accept: false, reasons: ["3 tests are failing."], warning: "Multiple tests failing — fix one at a time.", summary: { total: 5, passed: 2, failed: 3 }, missing_categories: [], }; mockedWorkflow.getReviewQueueAll.mockResolvedValueOnce({ stories: [story], }); render(); expect(await screen.findByText("Failing 3")).toBeInTheDocument(); expect(await screen.findByText("Warning")).toBeInTheDocument(); expect( await screen.findByText("Multiple tests failing — fix one at a time."), ).toBeInTheDocument(); expect(await screen.findByText("3 tests are failing.")).toBeInTheDocument(); expect( await screen.findByText(/2\/5 passing, 3 failing/), ).toBeInTheDocument(); const blockedButton = screen.getByRole("button", { name: "Blocked" }); expect(blockedButton).toBeDisabled(); }); it("shows gate warning when multiple tests fail (AC5)", async () => { mockedWorkflow.getAcceptance.mockResolvedValueOnce({ can_accept: false, reasons: ["2 tests are failing."], warning: "Multiple tests failing — fix one at a time.", summary: { total: 4, passed: 2, failed: 2 }, missing_categories: [], }); render(); expect(await screen.findByText("Blocked")).toBeInTheDocument(); expect( await screen.findByText("Multiple tests failing — fix one at a time."), ).toBeInTheDocument(); expect( await screen.findByText(/2\/4 passing, 2 failing/), ).toBeInTheDocument(); expect(await screen.findByText("2 tests are failing.")).toBeInTheDocument(); }); it("does not call ensureAcceptance when clicking a blocked proceed button (AC4)", async () => { const story: ReviewStory = { story_id: "26_establish_tdd_workflow_and_gates", can_accept: false, reasons: ["Tests are failing."], warning: null, summary: { total: 3, passed: 1, failed: 2 }, missing_categories: [], }; mockedWorkflow.getReviewQueueAll.mockResolvedValueOnce({ stories: [story], }); render(); const blockedButton = await screen.findByRole("button", { name: "Blocked", }); expect(blockedButton).toBeDisabled(); // Clear any prior calls then attempt click on disabled button mockedWorkflow.ensureAcceptance.mockClear(); await userEvent.click(blockedButton); expect(mockedWorkflow.ensureAcceptance).not.toHaveBeenCalled(); }); it("shows proceed error when ensureAcceptance fails", async () => { const story: ReviewStory = { story_id: "26_establish_tdd_workflow_and_gates", can_accept: true, reasons: [], warning: null, summary: { total: 3, passed: 3, failed: 0 }, missing_categories: [], }; mockedWorkflow.getReviewQueueAll.mockResolvedValueOnce({ stories: [story], }); mockedWorkflow.ensureAcceptance.mockRejectedValueOnce( new Error("Acceptance blocked: tests still failing"), ); render(); const proceedButton = await screen.findByRole("button", { name: "Proceed", }); await userEvent.click(proceedButton); expect( await screen.findByText("Acceptance blocked: tests still failing"), ).toBeInTheDocument(); }); it("shows gate error when acceptance endpoint fails", async () => { mockedWorkflow.getAcceptance.mockRejectedValueOnce( new Error("Server unreachable"), ); render(); expect(await screen.findByText("Server unreachable")).toBeInTheDocument(); const retryButtons = await screen.findAllByRole("button", { name: "Retry", }); expect(retryButtons.length).toBeGreaterThanOrEqual(1); }); it("refreshes gate status after proceeding on the current story", async () => { const story: ReviewStory = { story_id: "26_establish_tdd_workflow_and_gates", can_accept: true, reasons: [], warning: null, summary: { total: 2, passed: 2, failed: 0 }, missing_categories: [], }; 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: 2, passed: 2, failed: 0 }, missing_categories: [], }); mockedWorkflow.getReviewQueueAll .mockResolvedValueOnce({ stories: [story] }) .mockResolvedValueOnce({ stories: [] }); render(); const proceedButton = await screen.findByRole("button", { name: "Proceed", }); await userEvent.click(proceedButton); await waitFor(() => { expect(mockedWorkflow.ensureAcceptance).toHaveBeenCalledWith({ story_id: story.story_id, }); }); 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(); 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(); 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(); 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(); 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(); }); });