import AxeBuilder from "@axe-core/playwright"; import { expect, test } from "@playwright/test"; import type { AutoModeState } from "../src/shared/api/autonomyApi"; const projectId = "widefrog"; const envelope = (data: unknown) => ({ data, meta: { requestId: "attention-auto-e2e", timestamp: new Date().toISOString() } }); const autoModeState = (): AutoModeState => ({ version: 1, enabled: true, status: "running", autoBrainstorm: true, maxBrainstormIterations: 5, maxConsecutiveFailures: 3, leaseMinutes: 30, maxWorkItemsPerRun: 8, maxRunMinutes: 45, roadmap: { id: "roadmap-e2e", title: "E2E-roadmap", sourceMarkdown: "# E2E-roadmap\n\n## Eerste fase\n- [ ] Exact werkitem", status: "active", currentPhaseIndex: 0, importedAt: "2026-09-01T00:00:00.000Z", phases: [{ id: "phase-e2e", title: "Eerste fase", objective: "Eerste fase", status: "active", brainstormIterations: 0, brainstormConverged: false, validationFailures: 0, items: [{ id: "item-e2e", title: "Exact werkitem", status: "pending", attempts: 0 }], }], }, completedLeases: [], events: [], updatedAt: "2026-09-01T00:00:00.000Z", updatedBy: "owner", }); const queueState = (manualOrder = false): AutoModeState => { const base = autoModeState(); const roadmap = base.roadmap!; return { ...base, roadmap: { ...roadmap, phases: roadmap.phases.map((phase) => ({ ...phase, items: [ { id: "item-a", title: "Item A", status: "pending", attempts: 0, ...(manualOrder ? { manualOrder: 2 } : {}) }, { id: "item-b", title: "Item B", status: "pending", attempts: 0, ...(manualOrder ? { manualOrder: 1 } : {}) }, ], })), }, }; }; const expectQueueOrder = async (page: import("@playwright/test").Page, firstTitle: string) => { const rows = page.locator('section[aria-labelledby="auto-queue-order-heading"] > div.space-y-2 > div'); await expect(rows).toHaveCount(2); await expect(rows.nth(0)).toContainText(firstTitle); }; const exerciseStaleQueueRead = async ( page: import("@playwright/test").Page, mutation: "reorder" | "reset", ) => { const beforeMutation = queueState(mutation === "reset"); let persistedState = beforeMutation; let getCount = 0; let releaseStaleGet!: () => void; let markStaleGetStarted!: () => void; let releaseMutation!: () => void; let markMutationStarted!: () => void; const staleGetGate = new Promise((resolve) => { releaseStaleGet = resolve; }); const staleGetStarted = new Promise((resolve) => { markStaleGetStarted = resolve; }); const mutationGate = new Promise((resolve) => { releaseMutation = resolve; }); const mutationStarted = new Promise((resolve) => { markMutationStarted = resolve; }); await page.route(`**/api/projects/${projectId}/autonomy**`, async (route) => { const request = route.request(); if (request.method() === "GET") { getCount += 1; if (getCount === 2) { markStaleGetStarted(); await staleGetGate; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope(beforeMutation)) }); return; } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope(persistedState)) }); return; } if (mutation === "reorder" && request.method() === "PUT") { const { itemIds } = request.postDataJSON() as { itemIds: string[] }; markMutationStarted(); await mutationGate; const positions = new Map(itemIds.map((id, index) => [id, index])); persistedState = { ...beforeMutation, roadmap: { ...beforeMutation.roadmap!, phases: beforeMutation.roadmap!.phases.map((phase) => ({ ...phase, items: phase.items.map((item) => positions.has(item.id) ? { ...item, manualOrder: positions.get(item.id) } : item), })), }, }; } else if (mutation === "reset" && request.method() === "DELETE") { markMutationStarted(); await mutationGate; persistedState = queueState(); } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope(persistedState)) }); }); await page.goto(`/projects/${projectId}/auto`); await expect(page.getByRole("heading", { name: "Auto Mode", exact: true })).toBeVisible(); await page.getByText("Roadmap en wachtrij", { exact: true }).click(); await expectQueueOrder(page, mutation === "reset" ? "Item B" : "Item A"); await page.getByRole("button", { name: "Vernieuwen", exact: true }).click(); await staleGetStarted; if (mutation === "reorder") { await page.getByRole("button", { name: "Item B omhoog" }).click(); } else { await page.getByRole("button", { name: "Automatische volgorde herstellen" }).click(); } await mutationStarted; await expect(page.getByText("Opslaan…", { exact: true })).toBeVisible(); releaseMutation(); await expectQueueOrder(page, mutation === "reset" ? "Item A" : "Item B"); releaseStaleGet(); await expectQueueOrder(page, mutation === "reset" ? "Item A" : "Item B"); }; test("attention inbox exposes every visible item as an actionable destination", async ({ page }) => { await page.goto(`/projects/${projectId}/attention`); await expect(page.getByRole("heading", { name: "Jouw aandacht", exact: true })).toBeVisible(); await expect(page.getByText(/item\(s\) vragen jouw aandacht|Niets wacht op jou/)).toBeVisible(); const reviewAction = page.getByRole("button", { name: /Beoordelen/ }).first(); if (await reviewAction.isVisible()) { await reviewAction.click(); await expect(page).toHaveURL(/\/review-queue\?reviewItemId=/); } const accessibility = await new AxeBuilder({ page }).analyze(); expect(accessibility.violations.filter((violation) => ["serious", "critical"].includes(violation.impact ?? ""))).toEqual([]); }); test("Auto Mode distinguishes queued work from real execution and protects roadmap progress", async ({ page }) => { let state = autoModeState(); let roadmapImports = 0; await page.route(`**/api/projects/${projectId}/autonomy**`, async (route) => { const request = route.request(); const url = new URL(request.url()); if (request.method() === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope(state)) }); return; } if (request.method() === "POST" && url.pathname.endsWith("/roadmap")) { roadmapImports += 1; await route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify(envelope(state)) }); return; } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope(state)) }); }); await page.goto(`/projects/${projectId}/auto`); await expect(page.getByRole("heading", { name: "Auto Mode", exact: true })).toBeVisible(); await expect(page.getByText("Klaar voor uitvoering", { exact: true }).first()).toBeVisible(); await expect(page.getByText("Werk staat veilig in de wachtrij")).toBeVisible(); await expect(page.getByText("ProjectBrain werkt", { exact: true })).toHaveCount(0); await page.getByText("Autonomiebeleid en geavanceerde instellingen", { exact: true }).click(); await page.getByText("Geavanceerde grenzen", { exact: true }).click(); await expect(page.getByLabel("Werkitems per run")).toHaveValue("8"); await expect(page.getByLabel("Looptijd per run")).toHaveValue("45"); await expect(page.getByText("Roadmap handmatig importeren", { exact: true })).toBeVisible(); await page.getByLabel("Roadmap").fill("# Nieuwe roadmap\n\n## Nieuwe fase\n- [ ] Nieuw werk"); await page.getByRole("button", { name: "Roadmap bijwerken" }).click(); await expect(page.getByText("Bestaande roadmap en voortgang vervangen?")).toBeVisible(); expect(roadmapImports).toBe(0); await page.getByRole("button", { name: "Annuleren" }).click(); state = { ...state, activeLease: { token: "00000000-0000-4000-8000-000000000001", kind: "implement", actor: "chatgpt-agent", roadmapId: "roadmap-e2e", phaseId: "phase-e2e", itemId: "item-e2e", createdAt: new Date(Date.now() - 60_000).toISOString(), expiresAt: new Date(Date.now() + 30 * 60_000).toISOString(), }, }; await page.getByRole("button", { name: "Vernieuwen" }).click(); await expect(page.getByText("ProjectBrain werkt", { exact: true })).toBeVisible(); await expect(page.getByText("Exact werkitem", { exact: true }).first()).toBeVisible(); const accessibility = await new AxeBuilder({ page }).analyze(); expect(accessibility.violations.filter((violation) => ["serious", "critical"].includes(violation.impact ?? ""))).toEqual([]); }); test("Auto Mode ignores an older read after saving a reordered queue", async ({ page }) => { await exerciseStaleQueueRead(page, "reorder"); }); test("Auto Mode ignores an older read after resetting queue order", async ({ page }) => { await exerciseStaleQueueRead(page, "reset"); });