Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

scripts/build/packages.test.ts

Raw
import { spawnSync } from "node:child_process";
import {
	copyFileSync,
	existsSync,
	mkdtempSync,
	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 { allExtensionNames, DIST_ROOT, ROOT } from "./build.ts";
import { readBuildSpec } from "./spec.ts";
import { verifyCollection, verifyPackage } from "./verify.ts";

const built = allExtensionNames().filter((name) =>
	existsSync(join(DIST_ROOT, name, "package.json")),
);

// Explicit package verification must never silently skip an empty checkout.
const active = process.env["PI_EXT_VERIFY_PACKAGES"] === "1";
const minimumPi = (
	JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")) as {
		"pi-ext": { minimumPi: string };
	}
)["pi-ext"].minimumPi;
const version = process.env["PI_EXT_MATRIX_PI"] ?? minimumPi;
const sdkPackages = ["pi-ai", "pi-agent-core", "pi-coding-agent", "pi-tui"].map(
	(name) => `@earendil-works/${name}`,
);
let host: string | undefined;
let probePath: string;

describe.skipIf(!active)("generated packages", () => {
	beforeAll(() => {
		if (!/^\d+\.\d+\.\d+$/u.test(version))
			throw new Error(`Invalid matrix Pi version: ${version}`);
		const missing = allExtensionNames().filter((name) => !built.includes(name));
		if (missing.length)
			throw new Error(
				`Package verification requires all release builds; missing: ${missing.join(", ")}`,
			);
		host = mkdtempSync(join(tmpdir(), "pi-ext-matrix-host-"));
		writeFileSync(
			join(host, "package.json"),
			JSON.stringify({
				private: true,
				type: "module",
				dependencies: Object.fromEntries(
					sdkPackages.map((name) => [name, version]),
				),
			}),
		);
		const args = [
			"install",
			"--ignore-scripts",
			"--no-audit",
			"--no-fund",
			"--loglevel=error",
		];
		const windows = process.platform === "win32";
		const install = spawnSync(
			windows ? `npm.cmd ${args.join(" ")}` : "npm",
			windows ? [] : args,
			{ cwd: host, encoding: "utf8", shell: windows, timeout: 120_000 },
		);
		if (install.error || install.status !== 0)
			throw new Error(
				`Matrix SDK install failed: ${install.error?.message ?? install.stderr}`,
			);
		for (const name of sdkPackages) {
			const actual = JSON.parse(
				readFileSync(join(host, "node_modules", name, "package.json"), "utf8"),
			) as { version: string };
			expect(actual.version, `${name} resolved in ${host}`).toBe(version);
		}
		const codemodeVersion = (
			JSON.parse(
				readFileSync(
					join(
						host,
						"node_modules",
						"@earendil-works",
						"pi-codemode",
						"package.json",
					),
					"utf8",
				),
			) as { version: string }
		).version;
		const bootstrap = spawnSync(
			process.execPath,
			[
				"--input-type=module",
				"--eval",
				"await import('@earendil-works/pi-coding-agent')",
			],
			{ cwd: host, encoding: "utf8", timeout: 20_000 },
		);
		if (bootstrap.error || bootstrap.status !== 0)
			throw new Error(
				`Matrix host Pi ${version} cannot start without extensions (pi-codemode ${codemodeVersion}): ${bootstrap.error?.message ?? bootstrap.stderr}`,
			);
		// Bare SDK imports resolve from this copied probe, never the checkout.
		probePath = join(host, "load-probe.ts");
		for (const file of ["load-probe.ts", "conflicts.ts"])
			copyFileSync(join(ROOT, "scripts", "build", file), join(host, file));
		console.log(
			`OK compatibility host Pi ${version}: ${host}; ${built.length} release packages`,
		);
	}, 130_000);
	afterAll(() => {
		if (host) rmSync(host, { recursive: true, force: true });
	});

	it("contains every extension package", () => {
		expect(built).toEqual(allExtensionNames());
	});

	it.each(built)(
		"%s loads relocated outside the checkout",
		async (name) => {
			const report = await verifyPackage(join(DIST_ROOT, name), {
				externals: readBuildSpec(join(ROOT, "extensions", name)).externals,
				probePath,
			});
			expect(report.errors).toEqual([]);
			expect(report.piVersion).toBe(version);
			expect(report.ok).toBe(true);
		},
		120_000,
	);

	it("the complete collection loads without identity, tool, command, flag, or skill conflicts", async () => {
		const report = await verifyCollection(
			built.map((name) => join(DIST_ROOT, name)),
			{ probePath },
		);
		expect(report.errors).toEqual([]);
		expect(report.piVersion).toBe(version);
		expect(report.conflicts).toEqual([]);
		expect(report.ok).toBe(true);
	}, 600_000);
});