import { expect, test } from "@playwright/test"; import type { AcceptanceResponse, ReviewListResponse, } from "../../src/api/workflow"; /** * Helper: mock all API routes needed to render the Chat view. * Accepts overrides for the workflow-specific endpoints. */ function mockChatApis( page: import("@playwright/test").Page, overrides: { acceptance?: AcceptanceResponse; reviewQueue?: ReviewListResponse; ensureAcceptance?: { status: number; body: unknown }; } = {}, ) { const acceptance: AcceptanceResponse = overrides.acceptance ?? { can_accept: false, reasons: ["No test results recorded for the story."], warning: null, summary: { total: 0, passed: 0, failed: 0 }, missing_categories: ["unit", "integration"], }; const reviewQueue: ReviewListResponse = overrides.reviewQueue ?? { stories: [], }; const ensureResp = overrides.ensureAcceptance ?? { status: 200, body: true, }; return Promise.all([ // Selection screen APIs page.route("**/api/projects", (route) => route.fulfill({ json: ["/tmp/test-project"] }), ), page.route("**/api/io/fs/home", (route) => route.fulfill({ json: "/tmp" })), page.route("**/api/project", (route) => { if (route.request().method() === "POST") { return route.fulfill({ json: "/tmp/test-project" }); } if (route.request().method() === "DELETE") { return route.fulfill({ json: true }); } return route.fulfill({ json: null }); }), // Chat init APIs page.route("**/api/ollama/models**", (route) => route.fulfill({ json: ["llama3.1"] }), ), page.route("**/api/anthropic/key/exists", (route) => route.fulfill({ json: false }), ), page.route("**/api/anthropic/models", (route) => route.fulfill({ json: [] }), ), page.route("**/api/model", (route) => { if (route.request().method() === "POST") { return route.fulfill({ json: true }); } return route.fulfill({ json: null }); }), // Workflow APIs page.route("**/api/workflow/acceptance", (route) => { if (route.request().url().includes("/ensure")) return route.fallback(); return route.fulfill({ json: acceptance }); }), page.route("**/api/workflow/review/all", (route) => route.fulfill({ json: reviewQueue }), ), page.route("**/api/workflow/acceptance/ensure", (route) => route.fulfill({ status: ensureResp.status, json: ensureResp.body, }), ), page.route("**/api/io/fs/list/absolute**", (route) => route.fulfill({ json: [] }), ), page.route("**/api/workflow/todos", (route) => route.fulfill({ json: { stories: [] } }), ), ]); } /** Navigate past the selection screen into the Chat view. */ async function openProject(page: import("@playwright/test").Page) { await page.goto("/"); await page.getByPlaceholder("/path/to/project").fill("/tmp/test-project"); await page.getByRole("button", { name: "Open Project" }).click(); // Wait for the Chat view to appear await expect(page.getByText("Workflow Gates", { exact: true })).toBeVisible(); } test.describe("TDD gate panel (AC1/AC3)", () => { test("shows Blocked status with reasons when no test plan is approved", async ({ page, }) => { await mockChatApis(page, { acceptance: { can_accept: false, reasons: ["No approved test plan for the story."], warning: null, summary: { total: 0, passed: 0, failed: 0 }, missing_categories: ["unit", "integration"], }, }); await openProject(page); await expect( page.getByText("Workflow Gates", { exact: true }), ).toBeVisible(); await expect(page.getByText("Blocked").first()).toBeVisible(); await expect( page.getByText("No approved test plan for the story."), ).toBeVisible(); await expect( page.getByText("Missing: unit, integration").first(), ).toBeVisible(); }); test("shows Ready to accept when all gates pass", async ({ page }) => { await mockChatApis(page, { acceptance: { can_accept: true, reasons: [], warning: null, summary: { total: 5, passed: 5, failed: 0 }, missing_categories: [], }, }); await openProject(page); await expect(page.getByText("Ready to accept")).toBeVisible(); await expect(page.getByText(/5\/5 passing, 0 failing/)).toBeVisible(); }); }); test.describe("Acceptance blocked by failing tests (AC4)", () => { test("Proceed button is disabled and shows Blocked when tests fail", async ({ page, }) => { await mockChatApis(page, { acceptance: { can_accept: false, reasons: ["Tests are failing."], warning: null, summary: { total: 4, passed: 2, failed: 2 }, missing_categories: [], }, reviewQueue: { stories: [ { story_id: "26_establish_tdd_workflow_and_gates", can_accept: false, reasons: ["Tests are failing."], warning: null, summary: { total: 4, passed: 2, failed: 2 }, missing_categories: [], }, ], }, }); await openProject(page); const blockedButton = page.getByRole("button", { name: "Blocked" }); await expect(blockedButton).toBeVisible(); await expect(blockedButton).toBeDisabled(); await expect(page.getByText("Tests are failing.").first()).toBeVisible(); await expect(page.getByText(/2\/4 passing/).first()).toBeVisible(); }); test("Proceed button is disabled when missing test categories", async ({ page, }) => { await mockChatApis(page, { reviewQueue: { stories: [ { story_id: "26_establish_tdd_workflow_and_gates", can_accept: false, reasons: ["Missing required test categories."], warning: null, summary: { total: 0, passed: 0, failed: 0 }, missing_categories: ["unit", "integration"], }, ], }, }); await openProject(page); const blockedButton = page.getByRole("button", { name: "Blocked" }); await expect(blockedButton).toBeDisabled(); await expect(page.getByText("Missing").first()).toBeVisible(); }); }); test.describe("Red test count and warnings (AC5)", () => { test("shows Failing badge with count when tests fail", async ({ page }) => { await mockChatApis(page, { reviewQueue: { stories: [ { 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: [], }, ], }, }); await openProject(page); await expect(page.getByText("Failing 3")).toBeVisible(); await expect(page.getByText("Warning")).toBeVisible(); await expect( page.getByText("Multiple tests failing — fix one at a time."), ).toBeVisible(); }); test("gate panel shows warning for multiple failing tests", async ({ page, }) => { await mockChatApis(page, { acceptance: { 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: [], }, }); await openProject(page); await expect( page.getByText("Multiple tests failing — fix one at a time."), ).toBeVisible(); await expect(page.getByText(/2\/4 passing, 2 failing/)).toBeVisible(); }); }); test.describe("Blocked actions do not execute (E2E)", () => { test("clicking a blocked Proceed button does not call ensureAcceptance", async ({ page, }) => { let ensureCalled = false; await mockChatApis(page, { reviewQueue: { stories: [ { 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: [], }, ], }, }); // Override the ensure route to track calls await page.route("**/api/workflow/acceptance/ensure", (route) => { ensureCalled = true; return route.fulfill({ json: true }); }); await openProject(page); const blockedButton = page.getByRole("button", { name: "Blocked" }); await expect(blockedButton).toBeDisabled(); // Force-click to attempt bypass — should still not fire await blockedButton.click({ force: true }); // Give a moment for any potential network call await page.waitForTimeout(500); expect(ensureCalled).toBe(false); }); test("ready story proceeds successfully", async ({ page }) => { let ensureCalled = false; await mockChatApis(page, { acceptance: { can_accept: true, reasons: [], warning: null, summary: { total: 3, passed: 3, failed: 0 }, missing_categories: [], }, reviewQueue: { stories: [ { story_id: "26_establish_tdd_workflow_and_gates", can_accept: true, reasons: [], warning: null, summary: { total: 3, passed: 3, failed: 0 }, missing_categories: [], }, ], }, ensureAcceptance: { status: 200, body: true }, }); // Intercept ensure to track it was called await page.route("**/api/workflow/acceptance/ensure", (route) => { ensureCalled = true; return route.fulfill({ json: true }); }); await openProject(page); const proceedButton = page.getByRole("button", { name: "Proceed" }); await expect(proceedButton).toBeEnabled(); // Before clicking, swap review queue to return empty on next fetch await page.unroute("**/api/workflow/review/all"); await page.route("**/api/workflow/review/all", (route) => route.fulfill({ json: { stories: [] } }), ); await proceedButton.click(); // After proceed, the review queue refreshes — now returns empty await expect( page.getByText("No stories waiting for review."), ).toBeVisible(); expect(ensureCalled).toBe(true); }); });