pi-mono/scripts/coding-agent-smoke.test.mjs
Christian Klotz 23cf2b948d
feat: unify package artifact validation (#10197)
* feat: unify package artifact validation

* fix: restrict local artifact output paths

* fix: clarify package artifact module names

* feat: support pnpm package artifact consumers

* fix: preserve local release package installs

Keep npm and Bun consumer installs as runnable release artifacts while retaining isolated validation for other packages. Simplify smoke fixtures and avoid redundant release work.
2026-10-06 13:30:02 +02:00

103 lines
4.3 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import test from "node:test";
import { installConsumer } from "./local-package-install.mjs";
import { produceArtifactSet } from "./package-artifacts.mjs";
import { codingAgentName, smokeTestCodingAgent } from "./coding-agent-smoke.mjs";
const devPackages = ["pi-client", "pi-protocol", "pi-server"].map((name) => `@earendil-works/${name}`);
function writePackage(directory, manifest, files) {
mkdirSync(directory, { recursive: true });
writeFileSync(join(directory, "package.json"), `${JSON.stringify(manifest, null, "\t")}\n`);
for (const [path, content] of Object.entries(files)) {
mkdirSync(dirname(join(directory, path)), { recursive: true });
writeFileSync(join(directory, path), content);
}
}
function createFixture(t, { importServer = false, declareServer = false } = {}) {
const temporaryRoot = mkdtempSync(join(tmpdir(), "pi-coding-agent-smoke-test-"));
t.after(() => rmSync(temporaryRoot, { recursive: true, force: true }));
const root = join(temporaryRoot, "fixture with spaces");
const repoRoot = join(root, "repo");
mkdirSync(repoRoot, { recursive: true });
writeFileSync(join(repoRoot, "package.json"), '{"name":"fixture","private":true}\n');
const packageNames = [codingAgentName, ...devPackages];
for (const name of packageNames) {
const isAgent = name === codingAgentName;
writePackage(
join(repoRoot, "packages", name.split("/")[1]),
{
name,
version: "1.0.0",
type: "module",
main: "./dist/index.js",
types: "./dist/index.d.ts",
exports: isAgent
? {
".": { types: "./dist/index.d.ts", import: "./dist/index.js" },
"./client": { source: "./src/client/index.ts" },
"./experimental/plugin": { source: "./src/experimental/plugin.ts" },
}
: { ".": { types: "./dist/index.d.ts", import: "./dist/index.js" } },
files: ["dist"],
...(isAgent
? {
bin: { pi: "dist/bundle/cli.js" },
...(declareServer ? { dependencies: { "@earendil-works/pi-server": "1.0.0" } } : {}),
devDependencies: Object.fromEntries(devPackages.map((packageName) => [packageName, "1.0.0"])),
}
: {}),
},
{
"dist/index.js": isAgent
? `${importServer ? 'import "@earendil-works/pi-server";' : ""}
export function createAgentSession() {}
export class SessionManager { static inMemory() {} }
export class ModelRuntime { static create() {} }
`
: "export {};\n",
"dist/index.d.ts": "export {};\n",
...(isAgent
? {
"dist/cli.js": 'console.log("1.0.0");\n',
"dist/bundle/cli.js": 'console.log("1.0.0");\n',
}
: {}),
},
);
}
const artifactSet = produceArtifactSet({ build: false, outDir: join(root, "artifacts"), repoRoot, source: null });
const directory = join(root, "consumer");
installConsumer({ artifactSet, directory, packageNames: [codingAgentName] });
return directory;
}
test("accepts a valid coding-agent package and rejects development-only packages and files", (t) => {
const directory = createFixture(t);
smokeTestCodingAgent(directory);
const nested = join(directory, "node_modules", codingAgentName, "node_modules/@earendil-works/pi-server");
mkdirSync(nested, { recursive: true });
writeFileSync(join(nested, "package.json"), JSON.stringify({ name: "@earendil-works/pi-server", version: "1.0.0" }));
assert.throws(() => smokeTestCodingAgent(directory), /pi-server must not be installed/);
rmSync(nested, { recursive: true });
const experimental = join(directory, "node_modules", codingAgentName, "dist/experimental");
mkdirSync(experimental);
assert.throws(() => smokeTestCodingAgent(directory), /contains development-only code/);
});
// #9132: smoke-test the public SDK, not just a bundled CLI that hides missing imports.
test("fails when the SDK imports an undeclared server despite a working CLI", (t) => {
const directory = createFixture(t, { importServer: true });
assert.throws(() => smokeTestCodingAgent(directory), /Cannot find package '@earendil-works\/pi-server'/);
});
test("fails if a development-only dependency is added back to the published dependency tree", (t) => {
const directory = createFixture(t, { declareServer: true });
assert.throws(() => smokeTestCodingAgent(directory), /pi-server must not be installed/);
});