Files
PaperClipAI/scripts/__tests__/runner-release-crate-cache.test.mjs
T
DottaandPaperclip 54cc522e86 feat(runner): add gated native Antigravity ACP execution (#18188)
## Thinking Path

> - Paperclip manages agents and their work.
> - Native Runner must bind each provider turn to the selected task,
account, model, and instructions.
> - Antigravity needs native ACP admission and physical process
ownership across continuation.
> - A saved conversation must not reuse an old process owner or silently
switch its effective model.
> - This pull request adds the native candidate behind exact operator
admission.
> - The default release gate remains closed while qualification is
pending.

## Linked Issues or Issue Description

Builds on merged #18187. Product account setup and the candidate
producer are in dependent #18189. The repeatable browser journey is in
#18190. Refs: #12503, #14938; those proposals use legacy CLI adapters.

**Agent or provider**

Google Antigravity ACP 1.4.3.

**Why this adapter is useful**

Users need to set up an Antigravity agent and run real work through
their managed Google subscription.

**How the agent is invoked**

Native Paperclip Runner uses the pinned Antigravity ACP runtime. Exact
operator admission is required while qualification remains pending.

## What Changed

- Add native Antigravity ACP model and instruction admission,
selected-account homes, and fenced refresh ownership.
- Carry exact candidate authority from the control plane through Runner,
Rust, the sidecar, and each current run. Clamp daemon lifetime to the
absolute permission expiry, including replacement owners.
- Preserve a rotated OAuth refresh token before later identity
verification can fail, including failed or cancelled startup admission.
Make private lease-close retries safe.
- Bound native probe retirement and retry the exact original owner; do
not delete credential homes before physical proof.
- Preserve Gemini API-key recovery when shared Google subscription
capability is added.
- Resume the same tagged native conversation with fresh per-turn process
ownership and physical native retirement.
- Include native manifests in isolated Rust image stages, release cache
keys, and dev rebuild detection. The workflow changes only extend
existing cache input lists.
- Keep current master provider behavior and append Grok 13, Pi 35, and
Copilot 84 input identities. Preserve prior identities, including saved
Copilot 82 and 83 runs.

## Verification

- Runner TypeScript main checks, profile generators, Rust formatting,
and offline workspace checks pass. The native backend integration target
passes 28 tests.
- The complete transport and cleanup group passes 387 tests without
unhandled rejections. These checks use local scripted children and
actual process and port ownership.
- Saved-profile decoding, native-execution parsing, installation, and
selected-credential controls pass 208 focused tests. All 109
pre-existing profile identity files remain byte-for-byte unchanged.
Copilot, Grok, and shared TypeScript/Rust generation checks pass.
- The final recovery fixture passes all seven cases. After the master
rebase, the combined recovery and owned-Node-runtime group passes all 10
cases. Each expiry case sets one 45-second absolute grant before
startup. It includes the deliberate six-second cold delay, four existing
eight-second command budgets, and active-turn headroom. The test proves
an active turn before expiry and then checks the daemon, owned process
group, child processes, and all three native port fences. The 60-second
test deadline does not renew provider authority.
- The consolidated model-discovery and pending-release group passes 22
tests. Google credential compatibility and auth recovery pass 25 tests,
including existing Gemini API-key recovery. The full product settings
test group remains intact in #18189.
- Earlier failures remain in their original CI runs and local records.
They exposed the Copilot 82 decoder gap, an expired five-second fixture
bootstrap ticket, and a fixture grant consumed before activation on
loaded CI. Each repair has a separate result. Production ticket limits
and admission behavior are unchanged by the final test commits.
- An unrelated decision-expiry check failed in [the earlier CI
run](https://github.com/paperclipai/paperclip/actions/runs/40941986698/job/123688668997).
Its owning code is unchanged from the base. The unchanged suite passes
20 tests on isolated PostgreSQL. A controlled database rollback
reproduces the earlier wake: clean startup schedules the 120-second
decision expiry, while rollback reconciliation leaves a 30-second
continuation-coalescing wake. The decision stays open, with no wake
delivery or error. The fresh final-head server shard passes; the failed
result is preserved.
- Final native commit `ec6bc7e4d77f5ae0cc4946dbba6c662e95a87f5b` passed
`ci / verify` and `ci / e2e`. Fresh Greptile and Paperclip reviews both
score 5/5, with no open findings. Antigravity retains the recorded
native `.5` closure and exact low-model profile. This PR does not claim
global or cross-platform qualification.

## Risks

- The new transport carries private managed credentials. Exact
ownership, account generation, model, instructions, and native
retirement are fail-closed.
- Only local macOS ARM64 has a verified complete Antigravity runtime.
Default Antigravity task execution remains disabled without current
trusted operator admission. The Google server sign-in lane and
production candidate producer ship in #18189; this native slice does not
expose the subscription catalog choice.
- Shared launch inputs change provider identities. New identities are
append-only; existing recorded identities remain available.

## Model Used

OpenAI Codex (GPT-6). The host did not expose the exact model revision
or context limit. The model used tools for code edits, tests, and
review.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-29 16:36:12 -05:00

606 lines
32 KiB
JavaScript

import assert from "node:assert/strict";
import { execFileSync, spawnSync } from "node:child_process";
import { chmodSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { CACHED_PATHS, HASHED_INPUTS, REQUIRED, RUNNER_PACKAGE, computeScope, markCurrent } from "../runner-release-crate-cache.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const script = path.join(repoRoot, "scripts/runner-release-crate-cache.mjs");
const readWorkflow = (name) => readFileSync(path.join(repoRoot, ".github/workflows", name), "utf8");
const prWorkflow = readWorkflow("pr-trusted.yml");
const releaseWorkflow = readWorkflow("release-verify.yml");
// Slice one job out of a workflow, from its two-space-indented key to the next.
function job(workflow, name) {
const start = workflow.indexOf(`\n ${name}:\n`);
assert.ok(start >= 0, `workflow is missing the ${name} job`);
const body = workflow.slice(start + 1);
const header = ` ${name}:\n`.length;
const next = body.slice(header).search(/\n [a-z0-9_-]+:\n/);
return next === -1 ? body : body.slice(0, header + next + 1);
}
// Slice one step out of a job by its name, up to the next step.
function step(body, name) {
const marker = ` - name: ${name}\n`;
const start = body.indexOf(marker);
assert.ok(start >= 0, `missing step: ${name}`);
const rest = body.slice(start + marker.length);
const next = rest.search(/\n - name: /);
return marker + (next === -1 ? rest : rest.slice(0, next + 1));
}
const RELEASE_DIR = `${RUNNER_PACKAGE}/runner/target/release`;
const KEY_STEP = "Compute the Runner release crate cache key";
const RESTORE_STEP = "Restore the Runner release crate (read only)";
const MARK_STEP = "Mark the restored Runner release crate current";
const LOOKUP_STEP = "Check for this push's Runner release crate";
const BUILD_STEP = "Build the Runner release crate for its cache";
const SAVE_STEP = "Save the Runner release crate";
const RUST_RESTORE_STEP = "Restore Runner Rust dependencies (read only)";
// Every PR job that builds the Runner release binary, and the command each
// one runs cargo through. The crate must be restored and marked before it.
const READERS = [
["typecheck_release_registry", "run: pnpm run typecheck:build-gaps"],
["verify_paperclip_runner", "run: pnpm --filter @paperclipai/paperclip-runner ${{ matrix.command }}"],
["build", "run: pnpm build"],
["canary_dry_run", "- name: Release canary dry run via release.sh internal build"],
];
const WRITER_GATE = "matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha";
const expectedPathBlock = ` path: |\n${CACHED_PATHS.map((entry) => ` ${entry}\n`).join("")}`;
const expectedKey = `key: runner-release-crate-v1-\${{ steps.runner_release_crate_key.outputs.scope }}-\${{ hashFiles(${HASHED_INPUTS.map((entry) => `'${entry}'`).join(", ")}) }}`;
function assertCacheStep(body, name, { lookupOnly = false } = {}) {
assert.match(body, /uses: actions\/cache\/(restore|save)@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6\.1\.0/, name);
assert.ok(body.includes(expectedPathBlock), `${name}: the cached path list must be exactly the script's CACHED_PATHS`);
assert.ok(body.includes(expectedKey), `${name}: the key must be built from the script's scope and HASHED_INPUTS`);
// An exact match or nothing: a prefix match would restore another
// revision's artifacts and the mark step would make cargo keep them.
assert.doesNotMatch(body, /restore-keys:/, name);
assert.equal(/lookup-only: true/.test(body), lookupOnly, name);
assert.doesNotMatch(body, /enableCrossOsArchive|fail-on-cache-miss: true/, name);
}
test("the script's path list and key inputs cover what the release build of the crate reads and writes", () => {
assert.deepEqual(CACHED_PATHS, [
`${RELEASE_DIR}/.fingerprint/paperclip-runner-core-*`,
`${RELEASE_DIR}/deps/libpaperclip_runner_core-*`,
`${RELEASE_DIR}/deps/paperclip_runner_core-*.d`,
`${RELEASE_DIR}/deps/paperclip_runnerd-*`,
`${RELEASE_DIR}/paperclip-runnerd`,
`${RELEASE_DIR}/paperclip-runnerd.d`,
]);
assert.deepEqual(HASHED_INPUTS, [
`${RUNNER_PACKAGE}/rust-toolchain.toml`,
`${RUNNER_PACKAGE}/acpx-profiles.json`,
`${RUNNER_PACKAGE}/antigravity-distributions.json`,
`${RUNNER_PACKAGE}/runner/Cargo.toml`,
`${RUNNER_PACKAGE}/runner/Cargo.lock`,
`${RUNNER_PACKAGE}/runner/crates/**`,
`${RUNNER_PACKAGE}/protocol/**`,
`${RUNNER_PACKAGE}/test-fixtures/**`,
`${RUNNER_PACKAGE}/test/fixtures/**`,
"tests/runner-e2e/fixtures/**",
]);
for (const input of HASHED_INPUTS) {
const fixed = input.replace(/\/\*\*$/, "");
assert.ok(statSync(path.join(repoRoot, fixed)), `${input} must exist in the repository`);
}
// The stage step copies the uplifted binary; it has to be in the set.
const stage = readFileSync(path.join(repoRoot, RUNNER_PACKAGE, "scripts/stage-runner-binary.mjs"), "utf8");
assert.match(stage, /"runner", "target", "release", executable/);
assert.ok(CACHED_PATHS.includes(`${RELEASE_DIR}/paperclip-runnerd`));
});
// Every `include_str!` / `include_bytes!` in one Rust source, with the path
// each one names. Three argument forms are resolved: a string literal
// (relative to the source file), and `concat!(...)` of string literals,
// `env!("CARGO_MANIFEST_DIR")` (the directory of the nearest Cargo.toml) and
// macro metavariables (`$name`, which make the resolved path a directory
// prefix rather than a file). Anything else is reported as unsupported, so
// a new include form cannot slip past the guard by not matching a regex.
function crateIncludes(source, text) {
const includes = [];
for (const match of text.matchAll(/include_(?:str|bytes)!\s*\(/g)) {
const start = match.index + match[0].length;
let depth = 1;
let inString = false;
let end = start;
for (; end < text.length && depth > 0; end += 1) {
const ch = text[end];
if (inString) {
if (ch === "\\") end += 1;
else if (ch === '"') inString = false;
} else if (ch === '"') inString = true;
else if (ch === "(") depth += 1;
else if (ch === ")") depth -= 1;
}
assert.equal(depth, 0, `${source}: unbalanced include at offset ${match.index}`);
const argument = text.slice(start, end - 1).trim();
includes.push({ argument, ...resolveIncludeArgument(source, argument) });
}
return includes;
}
function manifestDir(source) {
let dir = path.dirname(source);
while (!statSync(path.join(dir, "Cargo.toml"), { throwIfNoEntry: false })?.isFile()) {
const parent = path.dirname(dir);
assert.notEqual(parent, dir, `${source}: no Cargo.toml above the source`);
dir = parent;
}
return dir;
}
function resolveIncludeArgument(source, argument) {
const literal = argument.match(/^"((?:[^"\\]|\\.)*)"$/);
if (literal) return { form: "literal", resolved: path.resolve(path.dirname(source), literal[1]), exact: true };
const concat = argument.match(/^concat!\s*\(([\s\S]*)\)$/);
assert.ok(concat, `${source}: unsupported include form: ${argument}`);
const parts = splitTopLevel(concat[1]);
// A relative path is joined to the source's directory; one that starts
// with the manifest directory is one string, since its next literal
// starts with the "/" that separates the two.
let base = `${path.dirname(source)}/`;
let text = "";
let exact = true;
let form = "concat";
for (const [index, part] of parts.entries()) {
const string = part.match(/^"((?:[^"\\]|\\.)*)"$/);
if (string) {
assert.ok(exact || !/(^|\/)\.\.(\/|$)/.test(string[1]), `${source}: a path segment after a metavariable must not climb out: ${argument}`);
if (exact) text += string[1];
continue;
}
if (part === 'env!("CARGO_MANIFEST_DIR")') {
assert.equal(index, 0, `${source}: env!("CARGO_MANIFEST_DIR") must be the first part of concat!: ${argument}`);
base = manifestDir(source);
form = "concat-manifest-dir";
continue;
}
if (/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(part)) {
// The fixed prefix before the first metavariable is the directory the
// macro's callers can reach; the suffix cannot climb out of it.
exact = false;
form = form === "concat-manifest-dir" ? form : "concat-metavariable";
continue;
}
assert.fail(`${source}: unsupported concat! part in include: ${part}`);
}
const joined = base + text;
const resolved = path.resolve(exact || text.endsWith("/") ? joined : path.dirname(joined));
return { form, resolved, exact };
}
// Split concat!'s arguments on the commas outside string literals.
function splitTopLevel(text) {
const parts = [];
let current = "";
let inString = false;
for (let index = 0; index < text.length; index += 1) {
const ch = text[index];
if (inString) {
current += ch;
if (ch === "\\") current += text[++index] ?? "";
else if (ch === '"') inString = false;
} else if (ch === '"') {
inString = true;
current += ch;
} else if (ch === ",") {
parts.push(current);
current = "";
} else current += ch;
}
parts.push(current);
return parts.map((part) => part.trim()).filter(Boolean);
}
const underHashedInputs = (resolved) =>
HASHED_INPUTS.map((input) => path.join(repoRoot, input.replace(/\/\*\*$/, ""))).some((root) => resolved === root || resolved.startsWith(`${root}${path.sep}`));
test("the include parser resolves literals, concat! with the manifest directory and with metavariables, and rejects other forms", () => {
const root = mkdtempSync(path.join(os.tmpdir(), "runner-release-crate-includes-"));
try {
const crate = path.join(root, "crates/demo");
mkdirSync(path.join(crate, "src/nested"), { recursive: true });
mkdirSync(path.join(root, "protocol/fixtures"), { recursive: true });
writeFileSync(path.join(crate, "Cargo.toml"), '[package]\nname = "demo"\n');
writeFileSync(path.join(root, "protocol/schema.json"), "{}");
const source = path.join(crate, "src/nested/mod.rs");
const text = [
'let a = include_str!("../../../../protocol/schema.json");',
'let b = include_bytes!(\n "../../../../protocol/schema.json"\n);',
'let c = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../protocol/", "schema.json"));',
'let d = include_str!(concat!("../../../../protocol/fixtures/", $fixture, ".json"));',
'let e = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../protocol/fixtures/", $golden, ".summary.json"));',
'let f = include_str!("a (paren) in the name.json");',
].join("\n");
const includes = crateIncludes(source, text);
assert.deepEqual(
includes.map(({ form, resolved, exact }) => [form, path.relative(root, resolved), exact]),
[
["literal", "protocol/schema.json", true],
["literal", "protocol/schema.json", true],
["concat-manifest-dir", "protocol/schema.json", true],
["concat-metavariable", "protocol/fixtures", false],
["concat-manifest-dir", "protocol/fixtures", false],
["literal", "crates/demo/src/nested/a (paren) in the name.json", true],
],
);
const rejected = [
'include_str!(concat!("../protocol/", $x, "/../../../outside.json"))',
'include_str!(concat!("../protocol/", env!("CARGO_MANIFEST_DIR")))',
'include_str!(concat!(env!("OTHER_DIR"), "/schema.json"))',
'include_str!(r"../protocol/schema.json")',
'include_str!(path_from_a_function())',
'include_str!(concat!("../protocol/", some_macro!("x")))',
];
for (const argument of rejected) {
assert.throws(() => crateIncludes(source, argument), /unsupported|must not climb out|must be the first part/, argument);
}
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("every file the crate includes by path sits under a hashed input, so an edit to it changes the key", () => {
const crateRoot = path.join(repoRoot, RUNNER_PACKAGE, "runner/crates");
const sources = [];
(function walk(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else if (entry.name.endsWith(".rs")) sources.push(full);
}
})(crateRoot);
assert.ok(sources.length > 10, "the crate sources must be found");
const forms = new Set();
let includes = 0;
for (const source of sources) {
for (const { argument, form, resolved, exact } of crateIncludes(source, readFileSync(source, "utf8"))) {
includes += 1;
forms.add(form);
const where = `${path.relative(repoRoot, source)} includes ${argument.replace(/\s+/g, " ")}`;
assert.ok(underHashedInputs(resolved), `${where}, which resolves to ${path.relative(repoRoot, resolved)}, outside every HASHED_INPUTS pattern`);
const info = statSync(resolved, { throwIfNoEntry: false });
assert.ok(exact ? info?.isFile() : info?.isDirectory(), `${where}: ${path.relative(repoRoot, resolved)} must exist`);
}
}
assert.ok(includes > 0, "the crate includes protocol files by path; the guard must see them");
// The crate uses every supported form today, so none of the parser's
// branches is exercised only by the synthetic test above.
assert.deepEqual([...forms].sort(), ["concat-manifest-dir", "concat-metavariable", "literal"]);
});
test("isolated Docker Rust stages preserve the layout of embedded schema and identity inputs", () => {
const crateRoot = path.join(repoRoot, RUNNER_PACKAGE, "runner/crates");
const sources = [];
(function walk(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else if (entry.name.endsWith(".rs")) sources.push(full);
}
})(crateRoot);
for (const [dockerfile, stage] of [["Dockerfile", "runner-build"], ["docker/daytona-runner/Dockerfile", "runnerd-build"]]) {
const docker = readFileSync(path.join(repoRoot, dockerfile), "utf8");
const body = docker.split(/^FROM .+ AS /m).find(part => part.startsWith(`${stage}\n`));
assert.ok(body, `${dockerfile}: missing ${stage}`);
const workdir = /^WORKDIR (.+)$/m.exec(body)?.[1];
assert.ok(workdir?.startsWith("/"), `${dockerfile}: an absolute stage working directory is required`);
const copies = [...body.matchAll(/^COPY (?!-)(.+)$/gm)].flatMap(([, instruction]) => {
const args = instruction.trim().split(/\s+/);
const destination = path.posix.resolve(workdir, args.pop());
return args.map(source => {
const absolute = path.join(repoRoot, source);
return { source: absolute, destination: statSync(absolute).isDirectory() ? destination : path.posix.join(destination, path.basename(source)) };
});
});
const copiedPath = source => {
const copy = copies.find(copy => source === copy.source || source.startsWith(`${copy.source}${path.sep}`));
return copy ? path.posix.join(copy.destination, path.relative(copy.source, source)) : null;
};
let checked = 0;
for (const source of sources) {
for (const { resolved } of crateIncludes(source, readFileSync(source, "utf8"))) {
// Cargo test-only fixture trees are intentionally absent from production
// Docker stages. Their cache coverage is checked by the preceding test.
if (/\/(?:test-fixtures|test\/fixtures|tests\/runner-e2e\/fixtures)(?:\/|$)/.test(resolved)) continue;
const stagedSource = copiedPath(source);
assert.ok(stagedSource, `${dockerfile}: crate source was not copied: ${path.relative(repoRoot, source)}`);
const expected = path.posix.resolve(path.posix.dirname(stagedSource), path.relative(path.dirname(source), resolved));
assert.equal(copiedPath(resolved), expected, `${dockerfile}: include input ${path.relative(repoRoot, resolved)} must retain its crate-relative path`);
checked++;
}
}
assert.ok(checked > 0, `${dockerfile}: the guard must see real Rust include paths`);
}
});
test("every PR reader restores the crate after the dependency cache and marks it before building", () => {
for (const [name, buildCommand] of READERS) {
const body = job(prWorkflow, name);
const positions = [RUST_RESTORE_STEP, KEY_STEP, RESTORE_STEP, MARK_STEP].map((stepName) => body.indexOf(` - name: ${stepName}\n`));
assert.ok(positions.every((position) => position >= 0), `${name}: a crate cache step is missing`);
assert.deepEqual([...positions].sort((a, b) => a - b), positions, `${name}: the crate steps must follow the dependency restore in order`);
const build = body.indexOf(buildCommand);
assert.ok(build > positions.at(-1), `${name}: the crate must be marked before ${buildCommand}`);
const key = step(body, KEY_STEP);
assert.match(key, /id: runner_release_crate_key\n/, name);
assert.match(key, /run: echo "scope=\$\(node scripts\/runner-release-crate-cache\.mjs scope\)" >> "\$GITHUB_OUTPUT"/, name);
const restore = step(body, RESTORE_STEP);
assert.match(restore, /id: runner_release_crate\n/, name);
assertCacheStep(restore, `${name} restore`);
assert.doesNotMatch(restore, /actions\/cache\/save/, name);
const mark = step(body, MARK_STEP);
assert.match(mark, /if: \$\{\{ steps\.runner_release_crate\.outputs\.cache-hit == 'true' \}\}/, name);
assert.match(mark, /run: node scripts\/runner-release-crate-cache\.mjs mark-current/, name);
}
// No other job restores or saves the crate.
const total = prWorkflow.match(/- name: Restore the Runner release crate \(read only\)\n/g)?.length ?? 0;
assert.equal(total, READERS.length);
assert.doesNotMatch(prWorkflow, /actions\/cache\/save@/);
});
test("the Verify Paperclip Runner job restores the crate only in the lanes that build it", () => {
const body = job(prWorkflow, "verify_paperclip_runner");
const lanes = [...body.matchAll(/- lane_label: (.+)\n\s+command: (.+)\n(\s+release_crate: (true)\n)?/g)].map(([, label, , , flag]) => [label, flag === "true"]);
assert.deepEqual(lanes, [
["static checks", false],
["rust", true],
["vitest 1/2", false],
["vitest 2/2", true],
]);
for (const stepName of [KEY_STEP, RESTORE_STEP]) {
assert.match(step(body, stepName), /if: \$\{\{ matrix\.release_crate == true \}\}/, stepName);
}
// A lane that skipped the restore has no cache-hit output, so the mark
// step's own condition is enough there.
assert.match(step(body, MARK_STEP), /if: \$\{\{ steps\.runner_release_crate\.outputs\.cache-hit == 'true' \}\}/);
for (const [name] of READERS.filter(([name]) => name !== "verify_paperclip_runner")) {
for (const stepName of [KEY_STEP, RESTORE_STEP]) {
assert.doesNotMatch(step(job(prWorkflow, name), stepName), /^\s+if:/m, `${name}: ${stepName} is unconditional`);
}
}
});
test("only the master Rust lane writes the crate, after a lookup, with the readers' exact path list and key", () => {
const body = job(releaseWorkflow, "verify_paperclip_runner");
const positions = ["Verify Paperclip Runner", "Warm debug dependencies for the shared Runner cache", KEY_STEP, LOOKUP_STEP, BUILD_STEP, SAVE_STEP].map((stepName) => body.indexOf(` - name: ${stepName}\n`));
assert.ok(positions.every((position) => position >= 0), "a writer step is missing");
assert.deepEqual([...positions].sort((a, b) => a - b), positions, "the writer steps must follow the checks and the debug warm-up, in order");
for (const stepName of [KEY_STEP, LOOKUP_STEP]) {
assert.match(step(body, stepName), new RegExp(`if: \\$\\{\\{ ${WRITER_GATE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} \\}\\}`), stepName);
}
for (const stepName of [BUILD_STEP, SAVE_STEP]) {
assert.match(
step(body, stepName),
new RegExp(`if: \\$\\{\\{ ${WRITER_GATE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} && steps\\.runner_release_crate\\.outputs\\.cache-hit != 'true' \\}\\}`),
stepName,
);
}
assert.match(step(body, KEY_STEP), /run: echo "scope=\$\(node scripts\/runner-release-crate-cache\.mjs scope\)" >> "\$GITHUB_OUTPUT"/);
const lookup = step(body, LOOKUP_STEP);
assert.match(lookup, /id: runner_release_crate\n/);
assert.match(lookup, /uses: actions\/cache\/restore@/);
assertCacheStep(lookup, "writer lookup", { lookupOnly: true });
// The saved artifacts come from the readers' exact command, on a crate
// cleaned of the test-profile variants the lane's checks built.
const build = step(body, BUILD_STEP);
assert.match(build, /working-directory: packages\/paperclip-runner\n/);
assert.match(build, /set -euo pipefail\n\s+cargo clean --release --manifest-path runner\/Cargo\.toml -p paperclip-runner-core\n\s+pnpm build:binary\n/);
const runnerScripts = JSON.parse(readFileSync(path.join(repoRoot, RUNNER_PACKAGE, "package.json"), "utf8")).scripts;
assert.match(runnerScripts["build:binary"], /^cargo build --release --manifest-path runner\/Cargo\.toml --locked -p paperclip-runner-core --bin paperclip-runnerd && /);
const save = step(body, SAVE_STEP);
assert.match(save, /uses: actions\/cache\/save@/);
assertCacheStep(save, "writer save");
// The dependency cache's own writer gate is the same expression.
assert.match(body, new RegExp(`save-if: \\$\\{\\{ ${WRITER_GATE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} \\}\\}`));
assert.equal((releaseWorkflow.match(/actions\/cache\/save@/g) ?? []).length, 1);
});
test("the scope names the platform, the compiler and the checkout root, and survives a missing rustc", () => {
const a = computeScope({ workspace: "/home/runner/_work/paperclip/paperclip", rustcVersion: "1.97.1-abcdef123-x86_64-unknown-linux-gnu" });
const b = computeScope({ workspace: "/home/runner/work/paperclip/paperclip", rustcVersion: "1.97.1-abcdef123-x86_64-unknown-linux-gnu" });
const c = computeScope({ workspace: "/home/runner/_work/paperclip/paperclip", rustcVersion: "1.97.2-0123456ab-x86_64-unknown-linux-gnu" });
assert.notEqual(a, b, "a different checkout root must select a different entry");
assert.notEqual(a, c, "a different compiler must select a different entry");
assert.equal(a, computeScope({ workspace: "/home/runner/_work/paperclip/paperclip/", rustcVersion: "1.97.1-abcdef123-x86_64-unknown-linux-gnu" }));
assert.match(a, new RegExp(`^${os.platform()}-${os.arch()}-1\\.97\\.1-abcdef123-x86_64-unknown-linux-gnu-[0-9a-f]{12}$`));
assert.doesNotMatch(computeScope({ rustcVersion: "weird version/with spaces" }), /[^A-Za-z0-9._-]/);
// The CLI reads rustc from PATH; a stand-in proves which fields it reads,
// and an empty PATH proves a missing compiler still yields a usable key.
const bin = mkdtempSync(path.join(os.tmpdir(), "runner-release-crate-rustc-"));
try {
const stub = path.join(bin, "rustc");
writeFileSync(stub, '#!/bin/sh\nprintf "rustc 1.97.1 (8bab26f4f 2026-06-30)\\nbinary: rustc\\ncommit-hash: 8bab26f4f0123456789abcdef\\ncommit-date: 2026-06-30\\nhost: x86_64-unknown-linux-gnu\\nrelease: 1.97.1\\nLLVM version: 21.1.2\\n"\n');
chmodSync(stub, 0o755);
const withStub = execFileSync(process.execPath, [script, "scope"], { encoding: "utf8", env: { ...process.env, PATH: bin, GITHUB_WORKSPACE: "/home/runner/_work/paperclip/paperclip" } }).trim();
assert.equal(withStub, computeScope({ workspace: "/home/runner/_work/paperclip/paperclip", rustcVersion: "1.97.1-8bab26f4f-x86_64-unknown-linux-gnu" }));
const without = execFileSync(process.execPath, [script, "scope"], { encoding: "utf8", env: { ...process.env, PATH: mkdtempSync(path.join(os.tmpdir(), "empty-path-")), GITHUB_WORKSPACE: "/home/runner/_work/paperclip/paperclip" } }).trim();
assert.match(without, /-no-rustc-/);
} finally {
rmSync(bin, { recursive: true, force: true });
}
});
// The tree cargo leaves behind for the crate's release lib and bin units,
// with the mtimes GitHub's cache restores: older than a fresh checkout.
function layRestoredCrate(root) {
const release = path.join(root, RELEASE_DIR);
const lib = path.join(release, ".fingerprint/paperclip-runner-core-0b6c4654c2710743");
const bin = path.join(release, ".fingerprint/paperclip-runner-core-46cce78dbc4840c2");
mkdirSync(path.join(release, "deps"), { recursive: true });
mkdirSync(lib, { recursive: true });
mkdirSync(bin, { recursive: true });
const files = {
[path.join(lib, "lib-paperclip_runner_core")]: "0123456789abcdef",
[path.join(lib, "lib-paperclip_runner_core.json")]: "{}",
[path.join(lib, "dep-lib-paperclip_runner_core")]: "dep",
[path.join(lib, "invoked.timestamp")]: "t",
[path.join(bin, "bin-paperclip-runnerd")]: "fedcba9876543210",
[path.join(bin, "bin-paperclip-runnerd.json")]: "{}",
[path.join(bin, "dep-bin-paperclip-runnerd")]: "dep",
[path.join(release, "deps/libpaperclip_runner_core-0b6c4654c2710743.rlib")]: "rlib",
[path.join(release, "deps/libpaperclip_runner_core-0b6c4654c2710743.rmeta")]: "rmeta",
[path.join(release, "deps/paperclip_runner_core-0b6c4654c2710743.d")]: "d",
[path.join(release, "deps/paperclip_runnerd-46cce78dbc4840c2")]: "ELF",
[path.join(release, "deps/paperclip_runnerd-46cce78dbc4840c2.d")]: "d",
[path.join(release, "paperclip-runnerd")]: "ELF",
[path.join(release, "paperclip-runnerd.d")]: "d",
// A dependency's artifacts sit beside the crate's and must not be touched.
[path.join(release, "deps/libserde-1234567890abcdef.rlib")]: "dep rlib",
};
const old = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
for (const [file, content] of Object.entries(files)) {
writeFileSync(file, content);
utimesSync(file, old, old);
}
for (const dir of [lib, bin, path.join(release, "deps"), path.join(release, ".fingerprint"), release]) utimesSync(dir, old, old);
return { release, files: Object.keys(files), old };
}
test("mark-current makes every restored artifact newer than the checkout and leaves dependencies alone", () => {
const root = mkdtempSync(path.join(os.tmpdir(), "runner-release-crate-"));
try {
const { release, files, old } = layRestoredCrate(root);
const checkout = path.join(root, RUNNER_PACKAGE, "runner/crates/runner-core/src/lib.rs");
mkdirSync(path.dirname(checkout), { recursive: true });
writeFileSync(checkout, "// checked out after the artifacts were built\n");
const checkedOutAt = statSync(checkout).mtimeMs;
const result = markCurrent({ root, now: new Date(checkedOutAt + 5000) });
assert.equal(result.action, "marked");
assert.equal(result.files, 14);
assert.equal(result.bytes, files.filter((file) => !file.includes("libserde")).reduce((sum, file) => sum + statSync(file).size, 0));
for (const file of files) {
const mtime = statSync(file).mtimeMs;
if (file.includes("libserde")) assert.equal(Math.floor(mtime / 1000), Math.floor(old.getTime() / 1000), "a dependency artifact must keep its mtime");
else assert.ok(mtime > checkedOutAt, `${path.relative(release, file)} must be newer than the checkout`);
}
for (const dir of [".fingerprint/paperclip-runner-core-0b6c4654c2710743", ".fingerprint/paperclip-runner-core-46cce78dbc4840c2"]) {
assert.ok(statSync(path.join(release, dir)).mtimeMs > checkedOutAt, `${dir} must be newer than the checkout`);
}
assert.ok(statSync(checkout).mtimeMs === checkedOutAt, "sources are never touched");
// The CLI prints the same result as one line of JSON for the job log.
const printed = JSON.parse(execFileSync(process.execPath, [script, "mark-current"], { encoding: "utf8", env: { ...process.env, GITHUB_WORKSPACE: root } }));
assert.equal(printed.schema, "paperclip.runner-release-crate.v1");
assert.equal(printed.action, "marked");
assert.equal(printed.files, 14);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
// One file per REQUIRED entry, as cargo 1.97.1 lays them out for the crate's
// release lib and bin units. Measured: without any one of these cargo
// recompiles the unit (the whole crate for a lib file, a relink for a bin
// file); without any of the files in NOT_NEEDED it reports the crate fresh
// and recreates the uplifted binary from the one under deps.
const REQUIRED_FILES = {
"the lib fingerprint hash": ".fingerprint/paperclip-runner-core-0b6c4654c2710743/lib-paperclip_runner_core",
"the lib dep-info": ".fingerprint/paperclip-runner-core-0b6c4654c2710743/dep-lib-paperclip_runner_core",
"the lib rlib": "deps/libpaperclip_runner_core-0b6c4654c2710743.rlib",
"the lib rmeta": "deps/libpaperclip_runner_core-0b6c4654c2710743.rmeta",
"the bin fingerprint hash": ".fingerprint/paperclip-runner-core-46cce78dbc4840c2/bin-paperclip-runnerd",
"the bin dep-info": ".fingerprint/paperclip-runner-core-46cce78dbc4840c2/dep-bin-paperclip-runnerd",
"the bin under deps": "deps/paperclip_runnerd-46cce78dbc4840c2",
};
const NOT_NEEDED = [
".fingerprint/paperclip-runner-core-0b6c4654c2710743/lib-paperclip_runner_core.json",
".fingerprint/paperclip-runner-core-0b6c4654c2710743/invoked.timestamp",
".fingerprint/paperclip-runner-core-46cce78dbc4840c2/bin-paperclip-runnerd.json",
"deps/paperclip_runner_core-0b6c4654c2710743.d",
"deps/paperclip_runnerd-46cce78dbc4840c2.d",
"paperclip-runnerd",
"paperclip-runnerd.d",
];
test("mark-current discards a restore that lacks any file cargo needs for either unit, naming the file, so cargo sees a plain miss", () => {
assert.deepEqual(REQUIRED.map(({ label }) => label), Object.keys(REQUIRED_FILES), "every REQUIRED entry has a missing-file case");
for (const [label, file] of Object.entries(REQUIRED_FILES)) {
const root = mkdtempSync(path.join(os.tmpdir(), "runner-release-crate-"));
try {
const { release } = layRestoredCrate(root);
rmSync(path.join(release, file));
const result = markCurrent({ root });
assert.equal(result.action, "discarded", `${file} missing must discard the restore`);
assert.equal(result.reason, `incomplete: ${label} is missing`);
assert.ok(result.removed > 0, label);
assert.deepEqual(readdirSync(path.join(release, ".fingerprint")), [], label);
assert.deepEqual(readdirSync(path.join(release, "deps")), ["libserde-1234567890abcdef.rlib"], `${label}: only the crate's own artifacts are removed`);
assert.deepEqual(readdirSync(release).sort(), [".fingerprint", "deps"], label);
} finally {
rmSync(root, { recursive: true, force: true });
}
}
// The bin's dep-info alone does not stand in for the bin: the glob that
// finds the binary under deps must not be satisfied by the `.d` beside it.
const root = mkdtempSync(path.join(os.tmpdir(), "runner-release-crate-"));
try {
const { release } = layRestoredCrate(root);
rmSync(path.join(release, "deps/paperclip_runnerd-46cce78dbc4840c2"));
assert.ok(statSync(path.join(release, "deps/paperclip_runnerd-46cce78dbc4840c2.d")).isFile());
assert.equal(markCurrent({ root }).reason, "incomplete: the bin under deps is missing");
} finally {
rmSync(root, { recursive: true, force: true });
}
// A file cargo neither reads nor checks for may be absent without costing
// a rebuild, so its absence must not cost one here either.
for (const file of NOT_NEEDED) {
const intact = mkdtempSync(path.join(os.tmpdir(), "runner-release-crate-"));
try {
const { release } = layRestoredCrate(intact);
rmSync(path.join(release, file));
const result = markCurrent({ root: intact });
assert.equal(result.action, "marked", `${file} missing must not discard the restore`);
assert.equal(result.files, 13, file);
} finally {
rmSync(intact, { recursive: true, force: true });
}
}
// Nothing restored at all is reported the same way and exits cleanly.
const empty = mkdtempSync(path.join(os.tmpdir(), "runner-release-crate-empty-"));
try {
const printed = JSON.parse(execFileSync(process.execPath, [script, "mark-current"], { encoding: "utf8", env: { ...process.env, GITHUB_WORKSPACE: empty } }));
assert.equal(printed.action, "discarded");
assert.equal(printed.reason, "incomplete: the lib fingerprint hash is missing");
assert.equal(printed.removed, 0);
} finally {
rmSync(empty, { recursive: true, force: true });
}
});
test("the CLI prints the lists the workflows embed", () => {
const paths = execFileSync(process.execPath, [script, "paths"], { encoding: "utf8" }).trim().split("\n");
assert.deepEqual(paths, CACHED_PATHS);
const inputs = execFileSync(process.execPath, [script, "hashed-inputs"], { encoding: "utf8" }).trim().split("\n");
assert.deepEqual(inputs, HASHED_INPUTS);
const usage = spawnSync(process.execPath, [script, "nonsense"], { encoding: "utf8" });
assert.equal(usage.status, 2);
assert.match(usage.stderr, /usage: runner-release-crate-cache\.mjs/);
});