paperclip/server/scripts/verify-runner-vendor-depend...

99 lines
3.8 KiB
JavaScript

import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
findMissingVendorDependencies,
findRunnerExternalPackages,
} from "./verify-runner-vendor-dependencies.mjs";
describe("findMissingVendorDependencies", () => {
it("returns nothing when every runner dependency is already declared on server", () => {
const missing = findMissingVendorDependencies(
new Set(["acpx", "ajv", "smol-toml"]),
new Set(["acpx", "ajv", "smol-toml", "express"]),
);
expect(missing).toEqual([]);
});
it("flags a runner dependency that isn't mirrored into server/package.json", () => {
// This is the exact shape of the incident this check exists to catch:
// packages/paperclip-runner/package.json grew a new runtime dependency
// (smol-toml) that never got mirrored into server/package.json, so the
// vendored `cp -R` copy failed to resolve it at runtime (#13110, #13116).
const missing = findMissingVendorDependencies(
new Set(["acpx", "ajv", "smol-toml"]),
new Set(["acpx", "ajv"]),
);
expect(missing).toEqual(["smol-toml"]);
});
it("sorts multiple missing dependencies for a stable error message", () => {
const missing = findMissingVendorDependencies(
new Set(["smol-toml", "ajv-formats", "acpx"]),
new Set(),
);
expect(missing).toEqual(["acpx", "ajv-formats", "smol-toml"]);
});
});
describe("findRunnerExternalPackages", () => {
// Fixture-level coverage for the actual esbuild scan, not just the diff
// function: a real dist/index.js + dist/testing.js on disk, structurally
// matching packages/paperclip-runner's shape (testing.js re-exports
// index.js, which imports another local module that imports a bare npm
// specifier), plus package.json dependency noise that should be ignored
// because nothing reachable from these entry points imports it.
let fixtureDir;
afterEach(() => {
if (fixtureDir) rmSync(fixtureDir, { recursive: true, force: true });
fixtureDir = undefined;
});
function writeFixture() {
fixtureDir = mkdtempSync(join(tmpdir(), "paperclip-runner-vendor-fixture-"));
writeFileSync(
join(fixtureDir, "internal.js"),
'import { parse } from "smol-toml";\n' +
"export function parseSomething(text) { return parse(text); }\n",
);
writeFileSync(
join(fixtureDir, "index.js"),
'export * from "./internal.js";\nexport const marker = "index";\n',
);
writeFileSync(
join(fixtureDir, "testing.js"),
'export * from "./index.js";\nexport const testingMarker = "testing";\n',
);
return [join(fixtureDir, "index.js"), join(fixtureDir, "testing.js")];
}
it("reports only the npm packages actually reachable from the entry points", async () => {
const entryPoints = writeFixture();
const externalPackageNames = await findRunnerExternalPackages(entryPoints);
// smol-toml is reachable through internal.js -> index.js -> testing.js
// and must be reported. Nothing else was imported anywhere in the
// fixture, so this also proves the scan doesn't fall back to "every
// dependency the package declares" (which is what made the check
// over-broad before -- see the module header).
expect(externalPackageNames).toEqual(new Set(["smol-toml"]));
});
it("throws a clear, actionable error when an entry point is missing", async () => {
fixtureDir = mkdtempSync(join(tmpdir(), "paperclip-runner-vendor-fixture-"));
const missingEntryPoint = join(fixtureDir, "index.js");
await expect(findRunnerExternalPackages([missingEntryPoint])).rejects.toThrow(
/expected build output at .*index\.js.*Run "pnpm --filter @paperclipai\/paperclip-runner build" first/s,
);
});
});