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 {
	existsSync,
	mkdirSync,
	mkdtempSync,
	readdirSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
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));
}

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.js");
		expect(files).toContain("dynamic-validator-worker.js");
		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]+\.js$/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.js",
			pi: { extensions: ["index.js"] },
		});
		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.js"), "utf8");
		expect(entry).not.toContain("unknown sub-agent extension");
		expect(moduleImports("index.js", entry)).toEqual(
			expect.arrayContaining([
				expect.stringMatching(/^\.\/implementation-[a-z0-9]+\.js$/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;
		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).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, "ultra");
			spawnSync("cp", ["-r", pkg, relocated]);
			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),
			);
			expect(report.diagnostics).toEqual([]);
			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-");
		pkg = (await buildExtension("the-system", { distRoot })).dir;
	}, 60_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");
		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]+\.js$/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).toEqual([
			expect.stringMatching(/\/skills\/vertical-slices\/SKILL\.md$/u),
		]);
		expect(report.skills[0]).not.toContain(ROOT);
	}, 120_000);
});

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) => e.path.split("/").at(-2))).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("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.js"] } });

			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" });
		} finally {
			rmSync(root, { recursive: true, force: true });
		}
	});
});