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