Story 26: Establish TDD workflow and quality gates
Add workflow engine with acceptance gates, test recording, and review queue. Frontend displays gate status (blocked/ready), test summaries, failing badges, and warnings. Proceed action is disabled when gates are not met. Includes 13 unit tests (Vitest) and 9 E2E tests (Playwright) covering all five acceptance criteria. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
13
frontend/tests/e2e/review-panel.spec.ts
Normal file
13
frontend/tests/e2e/review-panel.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("App boot smoke test", () => {
|
||||
test("renders the project selection screen", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByText("StorkIt")).toBeVisible();
|
||||
await expect(page.getByPlaceholder("/path/to/project")).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Open Project" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
344
frontend/tests/e2e/tdd-gates.spec.ts
Normal file
344
frontend/tests/e2e/tdd-gates.spec.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
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: [] }),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/** 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user