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