test(runner): gate installed package staging regressions

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-30 19:59:19 -05:00
1 parent 9661dd6473
commit fa8642d1eb
3 files changed
+47 -4

No files matched your search

@@ -113,6 +113,21 @@ separately staged runnerd artifact digest. The consumer uses no workspace
protocol, source-relative import, or deep package path. This is the packaging
gate; workspace tests alone are not proof.
For installed dependencies without bundled dependencies, the gate packs a private
copy outside pnpm's dependency tree, excluding only the package-root
`node_modules` directory. Manifest bytes, file modes, symlinks, and npm file
selection rules remain unchanged. Packages declaring bundled dependencies keep
the original pack path so their dependency payload is preserved. Temporary pack
inputs are removed after success or failure; installed sources are not modified.
Run the focused staging regression from the repository root:
```sh
node --test packages/paperclip-runner/scripts/installed-package-pack.test.mjs
```
This regression also runs in the Runner's `test:typescript:prep` gate.
## Consequences
- Existing tests importing mock/conformance values from the package root must
+1 -1
View File
@@ -74,7 +74,7 @@
"typecheck:browser": "tsc -p tsconfig.browser.json --noEmit",
"test": "pnpm run test:typescript && pnpm run test:rust",
"test:typescript": "pnpm run test:typescript:prep && vitest run",
"test:typescript:prep": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node --test test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs test/acpx-message-boundaries-package-contract.test.mjs test/acpx-rich-extensions-package-contract.test.mjs test/acpx-pi-receipt-package-contract.test.mjs test/acpx-response-delivery-package-contract.test.mjs scripts/candidate-provider-pack.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs",
"test:typescript:prep": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node --test test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs test/acpx-message-boundaries-package-contract.test.mjs test/acpx-rich-extensions-package-contract.test.mjs test/acpx-pi-receipt-package-contract.test.mjs test/acpx-response-delivery-package-contract.test.mjs scripts/candidate-provider-pack.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs scripts/installed-package-pack.test.mjs",
"test:typescript:vitest": "node ./scripts/run-pr-vitest-lane.mjs",
"test:rust": "cargo test --release --manifest-path runner/Cargo.toml --locked --workspace",
"test:codex": "cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --test codex_provider",
@@ -15,7 +15,28 @@ async function fixture(t, extra = {}) {
return { root, source };
}
for (const extra of [{}, { bundleDependencies: false }, { bundleDependencies: [] }]) {
// Capture bytes, modes, and link text without following package symlinks.
async function inventory(root) {
const entries = [];
async function visit(relative) {
const path = join(root, relative);
const stat = await lstat(path);
const entry = { path: relative, mode: stat.mode & 0o7777 };
if (stat.isSymbolicLink()) entries.push({ ...entry, type: "link", target: await readlink(path) });
else if (stat.isDirectory()) {
entries.push({ ...entry, type: "directory" });
for (const name of (await readdir(path)).sort()) await visit(join(relative, name));
} else {
assert.ok(stat.isFile());
entries.push({ ...entry, type: "file", bytes: await readFile(path) });
}
}
await visit("");
return entries;
}
for (const extra of [{}, { bundleDependencies: false }, { bundleDependencies: [] },
{ bundledDependencies: false }, { bundledDependencies: [] }]) {
test(`stages unchanged non-bundled package ${JSON.stringify(extra)}`, async t => {
const { root, source } = await fixture(t, extra);
await mkdir(join(source, "node_modules"));
@@ -24,6 +45,7 @@ for (const extra of [{}, { bundleDependencies: false }, { bundleDependencies: []
await chmod(join(source, "bin.js"), 0o755);
await writeFile(join(source, ".npmignore"), "excluded.txt\n");
await symlink("bin.js", join(source, "link"));
const sourceBefore = await inventory(source);
let staged;
const result = await withInstalledPackagePackInput(source, root, async (input, external) => {
staged = input;
@@ -37,31 +59,35 @@ for (const extra of [{}, { bundleDependencies: false }, { bundleDependencies: []
return "tarball";
});
assert.equal(result, "tarball");
assert.deepEqual(await inventory(source), sourceBefore);
await assert.rejects(lstat(staged), { code: "ENOENT" });
assert.equal(await readFile(join(source, "node_modules/foreign"), "utf8"), "must not copy");
});
}
for (const extra of [{ bundleDependencies: true }, { bundleDependencies: ["dependency"] }, { bundledDependencies: ["dependency"] }]) {
for (const extra of [{ bundleDependencies: true }, { bundleDependencies: ["dependency"] }, { bundledDependencies: true }, { bundledDependencies: ["dependency"] }]) {
test(`preserves original bundled pack path ${JSON.stringify(extra)}`, async t => {
const { root, source } = await fixture(t, extra);
const before = await readdir(root);
const sourceBefore = await inventory(source);
assert.equal(await withInstalledPackagePackInput(source, root, async (input, external) => {
assert.equal(input, source); assert.equal(external, false); return "unchanged";
}), "unchanged");
assert.deepEqual(await readdir(root), before);
assert.deepEqual(await inventory(source), sourceBefore);
});
}
test("removes only its staging input when packing fails", async t => {
const { root, source } = await fixture(t);
const sourceBefore = await inventory(source);
let staged;
const error = new Error("pack failed");
await assert.rejects(withInstalledPackagePackInput(source, root, async input => {
staged = input; throw error;
}), value => value === error);
await assert.rejects(lstat(staged), { code: "ENOENT" });
assert.ok((await lstat(join(source, "package.json"))).isFile());
assert.deepEqual(await inventory(source), sourceBefore);
});
for (const useFiles of [true, false]) {
@@ -93,11 +119,13 @@ for (const useFiles of [true, false]) {
};
// Check actual archive bytes against the installed input; the failing npm
// directory traversal itself is reproduced separately on Linux CI.
const sourceBefore = await inventory(source);
let input;
const staged = await withInstalledPackagePackInput(source, root, async path => {
input = path;
return pack(path, "first");
});
assert.deepEqual(await inventory(source), sourceBefore);
for (const file of staged.files) {
const bytes = execFileSync("tar", ["-xzOf", "-", `package/${file.path}`],
{ input: staged.archive, timeout: 5_000, maxBuffer: 1024 * 1024 });