220 lines
9.0 KiB
TypeScript
220 lines
9.0 KiB
TypeScript
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<void>((resolve) => { releaseStaleGet = resolve; });
|
|
const staleGetStarted = new Promise<void>((resolve) => { markStaleGetStarted = resolve; });
|
|
const mutationGate = new Promise<void>((resolve) => { releaseMutation = resolve; });
|
|
const mutationStarted = new Promise<void>((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");
|
|
});
|