import { createHash } from "node:crypto"; import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { expect, test } from "@playwright/test"; import { createBoardRuntime } from "../board-runtime.ts"; import { createSystemCommand } from "../system-command.ts"; const roots: string[] = []; test.afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); test("/system view and book render rich System docs without source mutation", async ({ page, }) => { const root = systemFixture(); const before = hashTree(path.join(root, ".system")); const command = createSystemCommand({ sendUserMessage() {} } as never); const notices: string[] = []; const previousNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = "test"; try { await command.handler("view PX-SPEC-SPEC0001", ctx(root, notices)); await command.handler("view PX-PHASE-PHASE002", ctx(root, notices)); await command.handler("book", ctx(root, notices)); } finally { process.env.NODE_ENV = previousNodeEnv; } const view = pathFromNotice(notices[0] ?? ""); const phaseView = pathFromNotice(notices[1] ?? ""); const book = pathFromNotice(notices[2] ?? ""); await page.goto(pathToFileURL(view).href); await expect( page.getByRole("heading", { name: "Contract" }).first(), ).toBeVisible(); await expect(page.getByText("Metadata")).toBeVisible(); await expect(page.locator("mjx-container")).toBeVisible(); await expect(page.locator(".mermaid")).toBeVisible(); await expect(page.locator("iframe")).toHaveAttribute( "src", /file:\/\/.*mockup-card\.html/, ); await expect(page.getByText("raw html works")).toBeVisible(); await page.goto(pathToFileURL(phaseView).href); await expect(page.locator(".phase-current")).toHaveText("Current phase"); await expect( page.getByRole("heading", { name: "Pre-v1" }).first(), ).toBeVisible(); await page.goto(pathToFileURL(book).href); await expect( page.getByRole("heading", { name: /the-system-render-e2e-.* Dossier/ }), ).toBeVisible(); await expect(page.getByRole("link", { name: "CORE-SYSTEM" })).toBeVisible(); await expect(page.locator("#PX-PHASE-PHASE002")).toBeVisible(); await expect(page.locator(".book-top .active-phase")).toHaveText( "Current phase pre-v1", ); await expect(page.locator('.book-nav a[aria-current="true"]')).toContainText( "PX-PHASE-PHASE002", ); await expect(page.locator(".book-toc")).toHaveCount(0); await expect(page.getByText("On this page")).toHaveCount(0); await expect(page.locator(".book-nav .nav-utils")).toBeVisible(); await expect(page.locator(".mobile-nav > details")).not.toHaveAttribute( "open", "", ); await expect(page.locator("#PX-SPEC-SPEC0001 .doc-related")).toHaveCount(1); await expect( page.locator("#PX-SPEC-SPEC0001 .doc-related"), ).not.toHaveAttribute("open", ""); await expect( page.locator("#PX-SPEC-SPEC0001 article > :last-child"), ).toHaveClass(/doc-related/); await expect(page.locator("#PX-SPEC-SPEC0001 .doc-related")).toContainText( "Build", ); await expect(page.locator("#PX-SPEC-SPEC0001 .doc-related")).toContainText( "Bug", ); await expect( page.locator('a[href^="file:"][href*="PX-"][href$="index.html"]'), ).toHaveCount(0); await expect( page.locator('#PX-SPEC-SPEC0001 .ledger a[href="#PX-RESEARCH-RSCH0001"]'), ).toHaveCount(1); const orderedSections = await page .locator(".book-section") .evaluateAll((sections) => sections.map((section) => section.id)); expect(orderedSections.indexOf("CORE-MISSION")).toBeLessThan( orderedSections.indexOf("PX-PHASE-PHASE002"), ); expect(orderedSections.indexOf("PX-PHASE-PHASE002")).toBeLessThan( orderedSections.indexOf("CORE-RULES"), ); expect(orderedSections.indexOf("CORE-RULES")).toBeLessThan( orderedSections.indexOf("PX-PHASE-PHASE001"), ); expect(orderedSections.indexOf("PX-PHASE-PHASE001")).toBeLessThan( orderedSections.indexOf("PX-SPEC-SPEC0001"), ); expect(orderedSections.indexOf("PX-SPEC-SPEC0001")).toBeLessThan( orderedSections.indexOf("PX-PLAN-PLAN0001"), ); expect(orderedSections.indexOf("PX-PLAN-PLAN0001")).toBeLessThan( orderedSections.indexOf("PX-PLAN-PLAN0002"), ); expect(orderedSections.indexOf("PX-PLAN-PLAN0002")).toBeLessThan( orderedSections.indexOf("PX-ISSUE-ISSUE001"), ); expect(orderedSections.indexOf("PX-ISSUE-ISSUE001")).toBeLessThan( orderedSections.indexOf("PX-RESEARCH-RSCH0001"), ); await assertSidebarConnected(page); const graph = page.locator("[data-graph-svg]"); await expect(graph).toBeVisible(); await expect(graph.locator("a")).toHaveCount(5); await expect(graph.locator('a[href="#PX-PHASE-PHASE002"]')).toHaveCount(0); const initialViewBox = await graph.getAttribute("viewBox"); await graph.hover(); await page.mouse.wheel(0, -240); await expect(graph).not.toHaveAttribute("viewBox", initialViewBox ?? ""); await page.locator('[data-graph-zoom="reset"]').click(); await expect(graph).toHaveAttribute("viewBox", initialViewBox ?? ""); await page.locator('.book-nav a[href="#PX-PHASE-PHASE002"]').click(); await expect(page.locator("#PX-PHASE-PHASE002")).toBeInViewport(); await expect(page).toHaveURL(/#PX-PHASE-PHASE002$/); await expect.poll(() => page.evaluate(() => scrollX)).toBe(0); await assertSidebarConnected(page); await page.locator('.book-nav a[href="#PX-SPEC-SPEC0001"]').click(); await expect(page.locator("#PX-SPEC-SPEC0001")).toBeInViewport(); await expect.poll(() => page.evaluate(() => scrollX)).toBe(0); const ledger = page.locator("#PX-SPEC-SPEC0001 .ledger"); await expect(ledger).not.toHaveAttribute("open", ""); await ledger .locator("summary") .evaluate((element) => (element as HTMLElement).click()); await expect(ledger).toHaveAttribute("open", ""); const researchLink = ledger.locator('a[href="#PX-RESEARCH-RSCH0001"]'); await researchLink.evaluate((element) => element.scrollIntoView({ behavior: "instant", block: "center" }), ); await expect(researchLink).toBeVisible(); await researchLink.click(); await expect(page.locator("#PX-RESEARCH-RSCH0001")).toBeInViewport(); await expect.poll(() => page.evaluate(() => scrollX)).toBe(0); await page.mouse.wheel(0, 1200); await assertSidebarConnected(page); await page.goto(pathToFileURL(book).href); await page.setViewportSize({ width: 390, height: 844 }); await expect(page.locator(".book-nav")).toBeHidden(); await expect(page.locator(".mobile-nav")).toBeVisible(); await expect(page.locator(".mobile-nav > details")).not.toHaveAttribute( "open", "", ); await expect(page.locator(".graph-frame")).toBeVisible(); await page.locator('[data-graph-zoom="in"]').click(); await expect(graph).not.toHaveAttribute("viewBox", initialViewBox ?? ""); await expect.poll(() => page.evaluate(() => scrollX)).toBe(0); await page.locator("#search").fill("body-only-term"); await expect(page.locator("#results")).toContainText("PX-SPEC-SPEC0001"); await expect(page.locator("#results")).toContainText("body-only-term"); expect(existsSync(path.join(path.dirname(view), "mockup-card.html"))).toBe( true, ); expect(hashTree(path.join(root, ".system"))).toBe(before); }); test("/system board persists independent spec, plan, and issue assessments", async ({ page, }) => { const root = systemFixture(); const runtime = createBoardRuntime(); try { await runtime.beginProposal(root); await runtime.tool.execute( "assessment", { items: [ { id: "PX-ISSUE-ISSUE001", state: "done", rationale: "The reproduction no longer fails.", }, ], }, new AbortController().signal, undefined, { cwd: root } as never, ); runtime.endProposal(); const notice = await runtime.open(root); expect(notice).toContain("Browser: Browser opener disabled"); const url = notice.match(/http:\/\/127\.0\.0\.1:\d+\/\?k=[^\s]+/)?.[0]; expect(url).toBeTruthy(); await page.goto(url ?? ""); const spec = page.locator('[data-board-item][data-id="PX-SPEC-SPEC0001"]'); const planA = page.locator('[data-board-item][data-id="PX-PLAN-PLAN0001"]'); const planB = page.locator('[data-board-item][data-id="PX-PLAN-PLAN0002"]'); const issue = page.locator( '[data-board-item][data-id="PX-ISSUE-ISSUE001"]', ); await expect(spec).toBeVisible(); await expect(planA).toBeVisible(); await expect(planB).toBeVisible(); await expect(issue.locator("[data-proposal]")).toContainText( "The reproduction no longer fails.", ); await issue.locator("[data-apply-proposal]").click(); await expect(issue).toHaveClass(/is-done/); await expect(issue.locator("[data-assessment]")).toContainText("assess"); await spec.locator(":scope > .board-row > .board-bundle > summary").click(); const specDetails = spec.locator( ":scope > .board-row > .board-bundle > .board-details", ); await expect(specDetails).toContainText("body-only-term"); await expect(specDetails.locator(".ledger")).toContainText("Metadata"); await expect( spec.locator(":scope > .board-row > .board-state"), ).toBeDisabled(); await planA.locator(".board-state").click(); await expect(planA).toHaveClass(/is-done/); await planB.locator(".board-state").click(); await expect(planB).toHaveClass(/is-done/); await expect( spec.locator(":scope > .board-row > .board-state"), ).toBeEnabled(); await spec.locator(":scope > .board-row > .board-state").click(); await expect(spec).toHaveClass(/is-done/); await spec.locator(":scope > .board-row > .board-state").click(); await expect(spec).not.toHaveClass(/is-done/); await expect(planA).toHaveClass(/is-done/); await expect(planB).toHaveClass(/is-done/); await spec.locator(":scope > .board-row > .board-state").click(); await planA.locator(".board-state").click(); await expect(spec).not.toHaveClass(/is-done/); await expect(planA).not.toHaveClass(/is-done/); await expect(planB).toHaveClass(/is-done/); const board = JSON.parse( readFileSync(path.join(root, ".system/board.json"), "utf8"), ); expect(board.items["PX-ISSUE-ISSUE001"].source).toBe("assess"); expect(board.items["PX-SPEC-SPEC0001"].state).toBe("todo"); expect(board.items["PX-PLAN-PLAN0001"].state).toBe("todo"); expect(board.items["PX-PLAN-PLAN0002"].state).toBe("done"); await page.setViewportSize({ width: 390, height: 844 }); const positions = await page.evaluate(() => { const specs = document .querySelector(".board-spec-list") ?.getBoundingClientRect(); const issues = document .querySelector(".board-issue-list") ?.getBoundingClientRect(); return { specsBottom: specs?.bottom ?? 0, issuesTop: issues?.top ?? 0 }; }); expect(positions.issuesTop).toBeGreaterThanOrEqual(positions.specsBottom); await expect.poll(() => page.evaluate(() => scrollX)).toBe(0); } finally { runtime.shutdown(); } }); test("/system board survives refresh, follows disk changes, and toggles metadata", async ({ page, }) => { const root = systemFixture(); const runtime = createBoardRuntime(); try { const notice = await runtime.open(root); const url = notice.match(/http:\/\/127\.0\.0\.1:\d+\/\?k=[^\s]+/)?.[0]; await page.goto(url ?? ""); const spec = page.locator('[data-board-item][data-id="PX-SPEC-SPEC0001"]'); const planA = page.locator('[data-board-item][data-id="PX-PLAN-PLAN0001"]'); const planB = page.locator('[data-board-item][data-id="PX-PLAN-PLAN0002"]'); await planA.locator(".board-state").click(); await expect(planA).toHaveClass(/is-done/); await page.reload(); await expect(planA).toHaveClass(/is-done/); await expect(planB).not.toHaveClass(/is-done/); write( root, ".system/issues/PX-ISSUE-ISSUE002-later/index.md", "---\nid: PX-ISSUE-ISSUE002\ntype: issue\ntitle: Later\nspecs: [PX-SPEC-SPEC0001]\n---\n\n# Later\n", ); const late = page.locator('[data-board-item][data-id="PX-ISSUE-ISSUE002"]'); await expect(late).toBeVisible({ timeout: 15000 }); const planLedger = planB.locator(":scope > .board-row .ledger"); await planB .locator(":scope > .board-row > .board-bundle > summary") .click(); await planLedger.locator("summary").click(); await planLedger.locator("[data-status-toggle]").click(); await expect( planB.locator(":scope > .board-row > .board-bundle > summary"), ).toContainText("approved", { timeout: 15000 }); expect( readFileSync( path.join(root, ".system/plans/PX-PLAN-PLAN0002-test/index.md"), "utf8", ), ).toContain("status: approved"); await planB.locator(".board-state").click(); await expect(planB).toHaveClass(/is-done/); await spec.locator(":scope > .board-row > .board-bundle > summary").click(); const specLedger = spec.locator(":scope > .board-row .ledger"); await specLedger.locator("summary").click(); const boardToggle = specLedger.locator("[data-board-toggle]"); await expect(boardToggle).toHaveText("todo"); await boardToggle.click(); await expect(boardToggle).toHaveText("done"); await expect(spec).toHaveClass(/is-done/); const board = JSON.parse( readFileSync(path.join(root, ".system/board.json"), "utf8"), ); expect(board.items["PX-SPEC-SPEC0001"].state).toBe("done"); } finally { runtime.shutdown(); } }); async function assertSidebarConnected(page: { evaluate: (fn: () => T) => Promise; }) { const layout = await page.evaluate(() => { const top = document.querySelector(".book-top")?.getBoundingClientRect(); const nav = document.querySelector(".book-nav")?.getBoundingClientRect(); const main = document.querySelector(".book-main")?.getBoundingClientRect(); return { scrollX, top: top ? { bottom: top.bottom, left: top.left, right: top.right } : null, nav: nav ? { top: nav.top, left: nav.left, right: nav.right } : null, main: main ? { left: main.left } : null, }; }); expect(layout.scrollX).toBe(0); expect(layout.top?.left).toBe(0); expect(layout.nav?.top).toBe(layout.top?.bottom); expect(layout.nav?.left).toBeGreaterThanOrEqual(0); expect(layout.nav?.right ?? 0).toBeLessThan(layout.main?.left ?? 0); } function ctx(root: string, notices: string[]) { return { cwd: root, waitForIdle: async () => {}, ui: { notify: (message: string) => notices.push(message) }, } as never; } function systemFixture(): string { const root = mkdtempSync(path.join(tmpdir(), "the-system-render-e2e-")); roots.push(root); for (const directory of ["specs", "phases", "plans", "issues", "research"]) mkdirSync(path.join(root, ".system", directory), { recursive: true }); write( root, ".system/config.json", '{"prefix":"PX","schemaVersion":0,"currentPhase":"PX-PHASE-PHASE002"}', ); write(root, ".system/board.json", '{"schemaVersion":0,"items":{}}'); write(root, ".system/SYSTEM.md", "# SYSTEM\n"); write(root, ".system/MISSION.md", "# MISSION\n"); write(root, ".system/RULES.md", "# RULES\n"); write( root, ".system/research/PX-RESEARCH-RSCH0001-notes/index.md", "---\nid: PX-RESEARCH-RSCH0001\ntype: research\ntitle: Notes\n---\n\n# Notes\n", ); write( root, ".system/specs/PX-SPEC-SPEC0001-contract/index.md", [ "---", "id: PX-SPEC-SPEC0001", "type: spec", "title: Contract", "research: [PX-RESEARCH-RSCH0001]", "---", "", "# Contract", "", "body-only-term", "", "```mermaid", "flowchart TD; A-->B;", "```", "", "$x+1$", "", "[Mockup](./mockup-card.html)", "", "

raw html works

", ].join("\n"), ); write( root, ".system/specs/PX-SPEC-SPEC0001-contract/mockup-card.html", "
Mockup card
", ); write( root, ".system/phases/PX-PHASE-PHASE001-delivery/index.md", "---\nid: PX-PHASE-PHASE001\ntype: phase\ntitle: Prototype\nlabel: prototype\n---\n\n# Prototype\n", ); write( root, ".system/phases/PX-PHASE-PHASE002-after-delivery/index.md", "---\nid: PX-PHASE-PHASE002\ntype: phase\ntitle: Pre-v1\nlabel: pre-v1\n---\n\n# Pre-v1\n", ); write( root, ".system/plans/PX-PLAN-PLAN0001-build/index.md", "---\nid: PX-PLAN-PLAN0001\ntype: plan\ntitle: Build\nspec: PX-SPEC-SPEC0001\nstatus: draft\n---\n\n# Build\n", ); write( root, ".system/plans/PX-PLAN-PLAN0002-test/index.md", "---\nid: PX-PLAN-PLAN0002\ntype: plan\ntitle: Test\nspec: PX-SPEC-SPEC0001\nstatus: draft\ndepends_on: [PX-PLAN-PLAN0001]\n---\n\n# Test\n", ); write( root, ".system/issues/PX-ISSUE-ISSUE001-bug/index.md", "---\nid: PX-ISSUE-ISSUE001\ntype: issue\ntitle: Bug\nspecs: [PX-SPEC-SPEC0001]\n---\n\n# Bug\n", ); return root; } function write(root: string, relative: string, content: string) { const target = path.join(root, relative); mkdirSync(path.dirname(target), { recursive: true }); writeFileSync(target, content, "utf8"); } function pathFromNotice(notice: string): string { const match = notice.match(/written: (.*)$/m); expect(match?.[1]).toBeTruthy(); return match?.[1] ?? ""; } function hashTree(root: string): string { const hash = createHash("sha256"); for (const file of files(root).sort()) { hash.update(path.relative(root, file)); hash.update(readFileSync(file)); } return hash.digest("hex"); } function files(root: string): string[] { return readdirSync(root).flatMap((name) => { const file = path.join(root, name); return statSync(file).isDirectory() ? files(file) : [file]; }); }