Files
storkit/frontend/tests/e2e/tdd-gates.spec.ts

348 lines
9.4 KiB
TypeScript
Raw Normal View History

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);
});
});