Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

scripts/build/build.test.ts

Raw
import { spawnSync } from "node:child_process";
import {
	cpSync,
	existsSync,
	lstatSync,
	mkdirSync,
	mkdtempSync,
	readdirSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join } from "node:path";
import { pathToFileURL } from "node:url";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { buildExtension, ROOT, rootVersion } from "./build.ts";
import { registrationConflicts } from "./conflicts.ts";
import { BUILD_MANIFEST, listFiles, publishPackage } from "./publish.ts";
import { readBuildSpec } from "./spec.ts";
import {
	forbiddenImports,
	moduleImports,
	verifyCollection,
	verifyPackage,
} from "./verify.ts";

const PI_PACKAGE_ROOT = join(
	ROOT,
	"node_modules",
	"@earendil-works",
	"pi-coding-agent",
);

function temp(prefix: string): string {
	return mkdtempSync(join(tmpdir(), prefix));
}

it("installs declared externals using npm with a spaced working directory", async () => {
	const root = temp("pi-ext-externals with spaces-");
	try {
		const dependency = join(root, "external");
		const extension = join(root, "extensions", "demo");
		mkdirSync(dependency);
		mkdirSync(extension, { recursive: true });
		writeFileSync(
			join(root, "package.json"),
			JSON.stringify({ version: "1.0.0" }),
		);
		writeFileSync(
			join(dependency, "package.json"),
			JSON.stringify({
				name: "pi-ext-build-fixture",
				version: "1.2.3",
				type: "module",
				main: "index.js",
			}),
		);
		writeFileSync(
			join(dependency, "index.js"),
			"export const evidence = 'external';\n",
		);
		writeFileSync(
			join(extension, "package.json"),
			JSON.stringify({
				name: "@bugabinga/pi-ext-demo",
				type: "module",
				main: "index.ts",
				dependencies: {
					"pi-ext-build-fixture": `file:${dependency.replaceAll("\\", "/")}`,
				},
				"pi-ext": {
					externals: {
						"pi-ext-build-fixture": "Local installer regression fixture",
					},
				},
			}),
		);
		writeFileSync(
			join(extension, "index.ts"),
			"import { evidence } from 'pi-ext-build-fixture'; export default function () { return evidence; }\n",
		);
		const built = await buildExtension("demo", { root });
		const manifest = JSON.parse(
			readFileSync(
				join(built.dir, "node_modules", "pi-ext-build-fixture", "package.json"),
				"utf8",
			),
		);
		expect(manifest.version).toBe("1.2.3");
		expect(
			lstatSync(
				join(built.dir, "node_modules", "pi-ext-build-fixture"),
			).isSymbolicLink(),
		).toBe(false);
		rmSync(dependency, { recursive: true, force: true });
		const consumer = spawnSync(
			process.execPath,
			[
				"--input-type=module",
				"-e",
				`const extension = await import(${JSON.stringify(pathToFileURL(join(built.dir, "index.ts")).href)}); console.log(extension.default());`,
			],
			{ encoding: "utf8", timeout: 10_000 },
		);
		expect(consumer.error).toBeUndefined();
		expect(consumer.status, consumer.stderr).toBe(0);
		expect(consumer.stdout.trim()).toBe("external");
		expect(existsSync(join(root, "dist", ".staging"))).toBe(false);
	} finally {
		rmSync(root, { recursive: true, force: true });
	}
}, 60_000);

describe("ultra package", () => {
	let distRoot: string;
	let pkg: string;

	beforeAll(async () => {
		distRoot = temp("pi-ext-dist-");
		pkg = (await buildExtension("ultra", { distRoot })).dir;
	}, 60_000);

	afterAll(() => {
		rmSync(distRoot, { recursive: true, force: true });
	});

	it("emits a complete self-contained package with declared resources", () => {
		const files = listFiles(pkg);
		expect(files).toContain("index.ts");
		expect(files).toContain("dynamic-validator-worker.js");
		expect(files).not.toContain("dynamic-validator-worker.ts");
		expect(files).toContain("package.json");
		expect(files).toContain("prompts/ultra-authoring/SKILL.md");
		expect(files).toContain("workflows/feature.json");
		expect(files.some((f) => /^implementation-[a-z0-9]+\.ts$/u.test(f))).toBe(
			true,
		);
		expect(files.some((f) => f.includes("/node_modules/"))).toBe(false);
		expect(existsSync(join(distRoot, ".staging"))).toBe(false);

		const manifest = JSON.parse(
			readFileSync(join(pkg, "package.json"), "utf8"),
		);
		expect(manifest).toMatchObject({
			name: "@bugabinga/pi-ext-ultra",
			version: rootVersion(),
			private: true,
			type: "module",
			main: "index.ts",
			pi: { extensions: ["index.ts"] },
		});
		expect(manifest.scripts).toBeUndefined();
		expect(manifest.dependencies).toBeUndefined();
		expect(Object.keys(manifest.peerDependencies)).toEqual(
			expect.arrayContaining(["@earendil-works/pi-coding-agent", "typebox"]),
		);
	});

	it("keeps the lazy runtime out of the entry chunk", () => {
		const entry = readFileSync(join(pkg, "index.ts"), "utf8");
		expect(entry).not.toContain("unknown sub-agent extension");
		expect(moduleImports("index.ts", entry)).toEqual(
			expect.arrayContaining([
				expect.stringMatching(/^\.\/implementation-[a-z0-9]+\.ts$/u),
			]),
		);
	});

	it("imports only Node built-ins, Pi modules, and package-private chunks", () => {
		expect(forbiddenImports(pkg)).toEqual([]);
	});

	it("loads relocated outside the checkout, activates the lazy runtime, and exposes the authoring skill", async () => {
		const report = await verifyPackage(pkg, {
			userSettings: { ultra: { autoEnable: true } },
		});
		expect(report.errors).toEqual([]);
		expect(report.ok).toBe(true);
		expect(report.extensions).toHaveLength(1);
		const ultra = report.extensions[0]!;
		expect(ultra.path).not.toContain(ROOT);
		expect(ultra.commands).toContain("ultra");
		expect(ultra.tools).toContain("run_workflow");
		expect(ultra.activeTools).toContain("run_workflow");
		expect(report.skills.map((path) => path.replaceAll("\\", "/"))).toEqual([
			expect.stringMatching(/\/prompts\/ultra-authoring\/SKILL\.md$/u),
		]);
		expect(report.skills[0]).not.toContain(ROOT);
	}, 120_000);

	it("runs the validator worker from a relocated package against the running Pi", () => {
		const root = temp("pi-ext-worker-");
		try {
			const relocated = join(root, "node_modules", "@test", "ultra");
			mkdirSync(dirname(relocated), { recursive: true });
			cpSync(pkg, relocated, { recursive: true });
			const draft = join(root, "draft");
			mkdirSync(draft);
			writeFileSync(
				join(draft, "manifest.json"),
				JSON.stringify({ name: "hello-tool", description: "says hello" }),
			);
			writeFileSync(
				join(draft, "index.ts"),
				`export default function (pi) {
	pi.registerTool({
		name: "hello_tool",
		label: "Hello",
		description: "hello",
		parameters: { type: "object", properties: {} },
		async execute() {
			return { content: [{ type: "text", text: "hi" }], details: {} };
		},
	});
}
`,
			);
			const result = spawnSync(
				process.execPath,
				[
					"--no-warnings",
					join(relocated, "dynamic-validator-worker.js"),
					draft,
				],
				{
					encoding: "utf8",
					env: {
						...process.env,
						ULTRA_PI_PACKAGE_ROOT: PI_PACKAGE_ROOT,
						PI_CODING_AGENT_DIR: join(root, "agent"),
					},
					timeout: 60_000,
				},
			);
			const line = result.stdout
				.split("\n")
				.find((candidate) => candidate.startsWith("ULTRA_VALIDATION_RESULT="));
			expect(line, result.stdout + result.stderr).toBeDefined();
			const report = JSON.parse(
				(line ?? "").slice("ULTRA_VALIDATION_RESULT=".length),
			);
			for (const diagnostic of report.diagnostics) {
				expect(diagnostic).toMatchObject({
					code: "validator_warning_target",
					status: "warning",
				});
				expect(diagnostic.elapsedMs).toBeGreaterThan(diagnostic.limitMs);
			}
			expect(report.ok).toBe(true);
		} finally {
			rmSync(root, { recursive: true, force: true });
		}
	}, 90_000);
});

describe("the-system package", () => {
	let distRoot: string;
	let pkg: string;

	beforeAll(async () => {
		distRoot = temp("pi-ext-dist-");
		// Release variant: the shipped artifact, free of browser-E2E test entries.
		pkg = (await buildExtension("the-system", { distRoot, release: true })).dir;
	}, 120_000);

	afterAll(() => {
		rmSync(distRoot, { recursive: true, force: true });
	});

	it("bundles every third-party dependency and ships skills plus browser assets", () => {
		const files = listFiles(pkg);
		expect(files).toContain("skills/vertical-slices/SKILL.md");
		expect(files).toContain("skills/the-system-mermaid/SKILL.md");
		expect(files).toContain("assets/mermaid.min.js");
		expect(files).toContain("assets/minisearch.js");
		// Math renders to MathML via Temml (bundled), so nothing ships external.
		expect(files.some((f) => f.startsWith("node_modules/"))).toBe(false);
		expect(forbiddenImports(pkg)).toEqual([]);
		const manifest = JSON.parse(
			readFileSync(join(pkg, "package.json"), "utf8"),
		);
		expect(manifest.dependencies).toBeUndefined();
		const renderer = files.find((f) =>
			/^system-renderer-[a-z0-9]+\.ts$/u.test(f),
		);
		expect(renderer).toBeDefined();
		expect(readFileSync(join(pkg, renderer ?? ""), "utf8")).toContain(
			"./assets/",
		);
	});

	it("loads relocated with its skills served from the package", async () => {
		const report = await verifyPackage(pkg);
		expect(report.errors).toEqual([]);
		expect(report.ok).toBe(true);
		expect(report.extensions[0]?.commands).toContain("system");
		expect(report.skills.map((path) => path.replaceAll("\\", "/"))).toEqual([
			expect.stringMatching(/\/skills\/vertical-slices\/SKILL\.md$/u),
		]);
		expect(report.skills[0]).not.toContain(ROOT);
	}, 120_000);
});

describe("release build parity", () => {
	let devRoot: string;
	let releaseRoot: string;
	let dev: { dir: string; entries: string[] };
	let release: { dir: string; entries: string[] };

	beforeAll(async () => {
		devRoot = temp("pi-ext-dev-");
		releaseRoot = temp("pi-ext-rel-");
		dev = await buildExtension("ultra", { distRoot: devRoot });
		release = await buildExtension("ultra", {
			distRoot: releaseRoot,
			release: true,
		});
	}, 180_000);

	afterAll(() => {
		rmSync(devRoot, { recursive: true, force: true });
		rmSync(releaseRoot, { recursive: true, force: true });
	});

	const isModule = (file: string) => /\.[cm]?[jt]s$/u.test(file);
	const resourceFiles = (dir: string) =>
		listFiles(dir).filter(
			(file) =>
				!file.endsWith(".map") &&
				!isModule(file) &&
				file !== "package.json" &&
				file !== BUILD_MANIFEST,
		);

	it("emits identical entries, chunk boundaries, externals, manifest, and resources", () => {
		// Entry chunk names are hash-free, so they are stable across variants.
		expect([...release.entries].sort()).toEqual([...dev.entries].sort());

		// Package-private chunks are content-hashed; only their count and the
		// entry import fan-out define the chunk boundaries, not their hashes.
		const chunkCount = (built: { dir: string; entries: string[] }) =>
			listFiles(built.dir).filter(
				(file) => isModule(file) && !built.entries.includes(file),
			).length;
		expect(chunkCount(release)).toBe(chunkCount(dev));
		expect(chunkCount(dev)).toBeGreaterThan(0);

		const entryImports = (built: { dir: string; entries: string[] }) =>
			built.entries.map(
				(entry) =>
					moduleImports(
						entry,
						readFileSync(join(built.dir, entry), "utf8"),
					).filter((specifier) => specifier.startsWith("./")).length,
			);
		expect(entryImports(release)).toEqual(entryImports(dev));

		// Externals and manifest shape are authored once and copied verbatim.
		expect(readFileSync(join(release.dir, "package.json"), "utf8")).toBe(
			readFileSync(join(dev.dir, "package.json"), "utf8"),
		);

		// Resources ship byte-for-byte regardless of variant.
		const resources = resourceFiles(dev.dir);
		expect(resourceFiles(release.dir)).toEqual(resources);
		expect(resources.length).toBeGreaterThan(0);
		for (const file of resources)
			expect(readFileSync(join(release.dir, file))).toEqual(
				readFileSync(join(dev.dir, file)),
			);
	});

	it("differs only by sourcemaps and minification", () => {
		// Sourcemaps are the debugging-information difference.
		expect(listFiles(dev.dir).some((file) => file.endsWith(".map"))).toBe(true);
		expect(listFiles(release.dir).some((file) => file.endsWith(".map"))).toBe(
			false,
		);

		// Minification is the behavior-neutral optimization: every shipped chunk
		// shrinks, and the total release JavaScript is smaller.
		const jsBytes = (dir: string) =>
			listFiles(dir)
				.filter(isModule)
				.reduce((sum, file) => sum + readFileSync(join(dir, file)).length, 0);
		expect(jsBytes(release.dir)).toBeLessThan(jsBytes(dev.dir));
	});
});

describe("collection", () => {
	it("reports identity, tool, command, flag, and skill conflicts", () => {
		expect(
			registrationConflicts(
				[
					{ path: "/a/index.js", tools: ["x"], commands: ["go"], flags: [] },
					{ path: "/b/index.js", tools: ["x"], commands: [], flags: ["f"] },
					{ path: "/c/index.js", tools: [], commands: ["go"], flags: ["f"] },
				],
				["@p/one", "@p/two", "@p/one"],
				["/a/skills/s/SKILL.md", "/b/skills/s/SKILL.md"],
			),
		).toEqual([
			"command go registered by /a/index.js, /c/index.js",
			"flag f registered by /b/index.js, /c/index.js",
			"package @p/one registered by @p/one, @p/one",
			"skill s registered by /a/skills/s/SKILL.md, /b/skills/s/SKILL.md",
			"tool x registered by /a/index.js, /b/index.js",
		]);
		expect(
			registrationConflicts(
				[{ path: "/a/index.js", tools: ["x"], commands: ["go"], flags: [] }],
				["@p/one"],
				["/a/skills/s/SKILL.md"],
			),
		).toEqual([]);
	});

	it("loads several generated packages together without conflicts", async () => {
		const distRoot = temp("pi-ext-dist-");
		try {
			const dirs: string[] = [];
			for (const name of ["pfui", "footer", "ultra", "nushell"])
				dirs.push((await buildExtension(name, { distRoot })).dir);
			const report = await verifyCollection(dirs);
			expect(report.errors).toEqual([]);
			expect(report.conflicts).toEqual([]);
			expect(report.ok).toBe(true);
			expect(report.extensions.map((e) => basename(dirname(e.path)))).toEqual([
				"pfui",
				"footer",
				"ultra",
				"nushell",
			]);
		} finally {
			rmSync(distRoot, { recursive: true, force: true });
		}
	}, 180_000);
});

describe("publishPackage", () => {
	function stage(root: string, files: Record<string, string>): string {
		const staging = join(
			root,
			`staging-${Math.random().toString(36).slice(2)}`,
		);
		for (const [name, content] of Object.entries(files)) {
			mkdirSync(join(staging, name, ".."), { recursive: true });
			writeFileSync(join(staging, name), content);
		}
		return staging;
	}

	it("keeps previous chunks for one generation and switches entries last", () => {
		const root = temp("pi-ext-publish-");
		try {
			const target = join(root, "dist", "demo");
			publishPackage(
				stage(root, {
					"index.js": "import './chunk-a.js'",
					"chunk-a.js": "a",
					"prompts/one.md": "one",
					"package.json": "{}",
				}),
				target,
				["index.js"],
			);
			expect(listFiles(target)).toEqual([
				BUILD_MANIFEST,
				"chunk-a.js",
				"index.js",
				"package.json",
				"prompts/one.md",
			]);

			publishPackage(
				stage(root, {
					"index.js": "import './chunk-b.js'",
					"chunk-b.js": "b",
					"package.json": "{}",
				}),
				target,
				["index.js"],
			);
			expect(listFiles(target)).toEqual([
				BUILD_MANIFEST,
				"chunk-a.js",
				"chunk-b.js",
				"index.js",
				"package.json",
				"prompts/one.md",
			]);
			expect(readFileSync(join(target, "index.js"), "utf8")).toBe(
				"import './chunk-b.js'",
			);

			publishPackage(
				stage(root, {
					"index.js": "import './chunk-c.js'",
					"chunk-c.js": "c",
					"package.json": "{}",
				}),
				target,
				["index.js"],
			);
			expect(listFiles(target)).toEqual([
				BUILD_MANIFEST,
				"chunk-b.js",
				"chunk-c.js",
				"index.js",
				"package.json",
			]);
			expect(readdirSync(root).filter((n) => n.startsWith("staging-"))).toEqual(
				[],
			);
		} finally {
			rmSync(root, { recursive: true, force: true });
		}
	});
});

describe("buildExtension", () => {
	function fakeRepo(): { root: string; ext: string } {
		const root = temp("pi-ext-repo-");
		writeFileSync(
			join(root, "package.json"),
			JSON.stringify({ name: "fake", version: "9.9.9" }),
		);
		const ext = join(root, "extensions", "demo");
		mkdirSync(ext, { recursive: true });
		writeFileSync(
			join(ext, "package.json"),
			JSON.stringify({
				name: "@bugabinga/pi-ext-demo",
				description: "demo",
				license: "MIT",
				peerDependencies: { "@earendil-works/pi-coding-agent": "*" },
			}),
		);
		writeFileSync(
			join(ext, "index.ts"),
			'import { join } from "node:path";\nexport default function demo() { return join("a", "b"); }\n',
		);
		return { root, ext };
	}

	it("emits Pi modules as TypeScript with host SDK imports preserved", async () => {
		const { root, ext } = fakeRepo();
		try {
			writeFileSync(
				join(ext, "index.ts"),
				'export default () => import("./lazy.ts");\n',
			);
			writeFileSync(
				join(ext, "lazy.ts"),
				'export { VERSION } from "@earendil-works/pi-coding-agent";\n',
			);
			const built = await buildExtension("demo", { root });
			expect(built.entries).toEqual(["index.ts"]);
			expect(listFiles(built.dir)).toEqual(
				expect.arrayContaining([
					"index.ts",
					expect.stringMatching(/^lazy-[a-z0-9]+\.ts$/u),
				]),
			);
			expect(
				JSON.parse(readFileSync(join(built.dir, "package.json"), "utf8")),
			).toMatchObject({
				main: "index.ts",
				pi: { extensions: ["index.ts"] },
			});
			writeFileSync(
				join(built.dir, "index.d.ts"),
				'export * from "../source.ts";\n',
			);
			expect(forbiddenImports(built.dir)).toEqual([]);
			writeFileSync(join(built.dir, "index.ts"), 'import "undeclared";\n');
			expect(forbiddenImports(built.dir)).toEqual(["undeclared"]);
			writeFileSync(join(built.dir, "index.ts"), 'import "./missing.ts";\n');
			expect(forbiddenImports(built.dir)).toEqual(["./missing.ts"]);
		} finally {
			rmSync(root, { recursive: true, force: true });
		}
	});

	it("leaves the previous package intact when a rebuild fails", async () => {
		const { root, ext } = fakeRepo();
		try {
			const built = await buildExtension("demo", { root });
			const before = listFiles(built.dir).map((f) => [
				f,
				readFileSync(join(built.dir, f), "utf8"),
			]);
			expect(
				JSON.parse(readFileSync(join(built.dir, "package.json"), "utf8")),
			).toMatchObject({ version: "9.9.9", pi: { extensions: ["index.ts"] } });

			writeFileSync(join(ext, "index.ts"), 'import "./missing.ts";\n');
			await expect(buildExtension("demo", { root })).rejects.toThrow();

			const after = listFiles(built.dir).map((f) => [
				f,
				readFileSync(join(built.dir, f), "utf8"),
			]);
			expect(after).toEqual(before);
			expect(existsSync(join(root, "dist", ".staging"))).toBe(false);
		} finally {
			rmSync(root, { recursive: true, force: true });
		}
	});

	it("rejects undeclared or missing build declarations", () => {
		const { root, ext } = fakeRepo();
		try {
			writeFileSync(
				join(ext, "package.json"),
				JSON.stringify({
					name: "@bugabinga/pi-ext-demo",
					"pi-ext": { resources: ["missing"] },
				}),
			);
			expect(() => readBuildSpec(ext)).toThrow(/missing does not exist/u);
			writeFileSync(
				join(ext, "package.json"),
				JSON.stringify({
					name: "@bugabinga/pi-ext-demo",
					"pi-ext": { externals: { left: "" } },
				}),
			);
			expect(() => readBuildSpec(ext)).toThrow(/incompatibility reason/u);
			writeFileSync(
				join(ext, "package.json"),
				JSON.stringify({
					name: "@bugabinga/pi-ext-demo",
					dependencies: { left: "^1" },
					"pi-ext": { externals: { left: "native addon" } },
				}),
			);
			expect(readBuildSpec(ext).externals).toEqual({ left: "native addon" });
			writeFileSync(
				join(ext, "package.json"),
				JSON.stringify({
					name: "@bugabinga/pi-ext-demo",
					"pi-ext": { nodeEntries: ["missing-worker.ts"] },
				}),
			);
			expect(() => readBuildSpec(ext)).toThrow(
				/missing-worker.ts does not exist/u,
			);
		} finally {
			rmSync(root, { recursive: true, force: true });
		}
	});
});