mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-07 16:11:46 +02:00
test(runner): gate installed package staging regressions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user