repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/the-system/__e2e__/renderer.spec.ts
Rawimport { 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: <T>(fn: () => T) => Promise<T>;
}) {
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)",
"",
"<p>raw html works</p>",
].join("\n"),
);
write(
root,
".system/specs/PX-SPEC-SPEC0001-contract/mockup-card.html",
"<section>Mockup card</section>",
);
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];
});
}