repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
scripts/build/packages.test.ts
Rawimport { 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);
});