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); });