Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/the-system/__e2e__/renderer.spec.ts

Raw
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: <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];
	});
}