perf(build): compile the Runner binary while its TypeScript surface builds

The Runner package build ran seven TypeScript steps (17s on the CI Build
job), then cargo build (36s), then two checks that read the staged binary,
as one && chain. The TypeScript steps do not read the binary and cargo
reads nothing they write, so the build now starts cargo first, runs the
steps while it compiles, stages the binary once both are done, and keeps
the binary readers after staging. About 13s off every pnpm build.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
devinfoleyandClaude Fable 5.1 committed 2026-10-11 19:22:08 +00:00
1 parent 1ac48aff8d
commit 4a5fbd3437
4 files changed
+505 -11

No files matched your search

+2 -2
View File
@@ -63,7 +63,7 @@
},
"sideEffects": false,
"scripts": {
"build": "pnpm run check:protocol-manifest && pnpm run build:typescript && pnpm run check:capability-contract && pnpm run check:capability-inventory && pnpm run check:protocol-coverage && node scripts/generate-semantic-contracts.mjs --check && node scripts/check-runner-workflow-traceability.mjs && pnpm run build:binary && node scripts/generate-replay-goldens.mjs --check && node scripts/generate-semantic-action-catalog.mjs --check",
"build": "node scripts/build-runner-package.mjs",
"build:typescript": "pnpm run check:acpx-profiles && pnpm run ensure:eval-build-deps && pnpm run check:protocol-types && node ./node_modules/typescript/bin/tsc --version && node ./node_modules/typescript/bin/tsc -p tsconfig.json && node ./node_modules/typescript/bin/tsc -p tsconfig.surfaces.json && node scripts/build-verified-provider-entrypoints.mjs",
"build:rust": "cargo build --manifest-path runner/Cargo.toml --locked --workspace --bins",
"build:binary": "cargo build --release --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --bin paperclip-runnerd && node scripts/stage-runner-binary.mjs",
@@ -81,7 +81,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-model-selection-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/cursor-provisioner-layout.test.mjs scripts/release-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 scripts/runner-package-executable.test.mjs scripts/stage-runner-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-model-selection-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/cursor-provisioner-layout.test.mjs scripts/release-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 scripts/runner-package-executable.test.mjs scripts/stage-runner-binary.test.mjs scripts/build-runner-package.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",
@@ -0,0 +1,227 @@
#!/usr/bin/env node
// Build the Runner package with the Rust binary compiling while the
// TypeScript surface builds and its checks run.
//
// `pnpm run build` used to be one `&&` chain: seven TypeScript steps (about
// 17s on the CI Build job), then `cargo build` (about 36s with the dependency
// cache restored), then two checks that read the staged binary. Nothing in
// the first seven steps reads the binary or runner/target, and cargo reads
// nothing those steps write: its only non-Rust inputs are the committed
// fixtures and schemas under protocol/, which the steps check but never
// regenerate. So cargo starts first, the TypeScript steps run while it
// compiles, the binary is staged once both are done, and the last two steps
// keep their place after staging.
//
// This build sits on the critical path of `pnpm -r build`: every adapter,
// plugin package and the server wait for it.
//
// PAPERCLIP_RUNNER_BUILD_SERIAL=1 runs cargo after the TypeScript steps
// instead, for a machine that cannot afford both at once.
import { spawn } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
export const CARGO_BUILD_ARGS = [
"build",
"--release",
"--manifest-path",
"runner/Cargo.toml",
"--locked",
"-p",
"paperclip-runner-core",
"--bin",
"paperclip-runnerd",
];
// Steps that run while cargo compiles, in this order. A `run` entry names a
// script in package.json. A `node` entry runs a script directly because the
// matching `check:*` wrapper rebuilds the TypeScript surface first for
// standalone use, and this build compiles it exactly once.
export const STEPS_DURING_CARGO = Object.freeze([
{ run: "check:protocol-manifest" },
{ run: "build:typescript" },
{ run: "check:capability-contract" },
{ run: "check:capability-inventory" },
{ run: "check:protocol-coverage" },
{ node: ["scripts/generate-semantic-contracts.mjs", "--check"] },
{ node: ["scripts/check-runner-workflow-traceability.mjs"] },
]);
// Runs after cargo has exited successfully and before anything reads the binary.
export const STAGE_BINARY_STEP = Object.freeze({ node: ["scripts/stage-runner-binary.mjs"] });
// Steps that read the staged binary, in this order.
export const STEPS_AFTER_BINARY = Object.freeze([
{ node: ["scripts/generate-replay-goldens.mjs", "--check"] },
{ node: ["scripts/generate-semantic-action-catalog.mjs", "--check"] },
]);
export const TIMING_LINE_PREFIX = "paperclip.runner-build.timing.v1";
const STOP_GRACE_MS = 5_000;
export function describeStep(step) {
return step.run ? `pnpm run ${step.run}` : `node ${step.node.join(" ")}`;
}
export class RunnerBuildError extends Error {
constructor(name, result) {
const reason = result.error
? `could not start (${result.error.message})`
: result.signal
? `was stopped by ${result.signal}`
: `exited with code ${result.code}`;
super(`${name} ${reason}`);
this.name = "RunnerBuildError";
this.step = name;
this.result = result;
}
}
function pnpmCommand(env) {
// pnpm sets npm_execpath to its own entry point while it runs this script,
// so a nested `pnpm run` uses the same pnpm. Outside pnpm, use the one on
// PATH; Windows needs a shell to resolve the .cmd shim.
const execPath = env.npm_execpath;
if (execPath && /pnpm/.test(path.basename(execPath))) {
return { command: process.execPath, args: [execPath] };
}
return process.platform === "win32"
? { command: "pnpm", args: [], shell: true }
: { command: "pnpm", args: [] };
}
export function defaultCommandFor(step, env = process.env) {
if (step.node) {
return { command: process.execPath, args: [...step.node] };
}
const pnpm = pnpmCommand(env);
return { command: pnpm.command, args: [...pnpm.args, "run", step.run], shell: pnpm.shell };
}
export function defaultCargoCommand() {
return { command: "cargo", args: [...CARGO_BUILD_ARGS] };
}
function launch({ command, args, shell }, { cwd, env }) {
const child = spawn(command, args, { cwd, env, stdio: "inherit", shell: shell === true });
let settled = null;
const exit = new Promise((resolve) => {
const settle = (result) => {
if (!settled) {
settled = result;
resolve(result);
}
};
child.once("error", (error) => settle({ error }));
child.once("exit", (code, signal) => settle({ code, signal }));
});
return { child, exit, result: () => settled };
}
const succeeded = (result) => !result.error && result.code === 0 && !result.signal;
async function stop(running) {
if (running.result()) return running.result();
try {
running.child.kill("SIGTERM");
} catch {
// Already gone.
}
const timer = new Promise((resolve) => setTimeout(resolve, STOP_GRACE_MS, "timeout"));
if ((await Promise.race([running.exit, timer])) === "timeout" && !running.result()) {
try {
running.child.kill("SIGKILL");
} catch {
// Already gone.
}
}
return running.exit;
}
/**
* Run the Runner build. `commandFor` and `cargoCommand` exist so tests can
* stand in for pnpm and cargo with local Node processes.
*/
export async function runRunnerBuild({
cwd = packageRoot,
env = process.env,
serial = env.PAPERCLIP_RUNNER_BUILD_SERIAL === "1",
commandFor = (step) => defaultCommandFor(step, env),
cargoCommand = defaultCargoCommand,
log = (line) => process.stderr.write(`${line}\n`),
} = {}) {
const startedAt = Date.now();
const timing = { cargoStartedAt: null, cargoEndedAt: null, stepsEndedAt: null };
let cargo = null;
const startCargo = () => {
const spec = cargoCommand();
log(`[runner build] ${spec.command} ${spec.args.join(" ")}`);
timing.cargoStartedAt = Date.now();
cargo = launch(spec, { cwd, env });
void cargo.exit.then(() => {
timing.cargoEndedAt = Date.now();
});
};
const runStep = async (step) => {
const name = describeStep(step);
log(`[runner build] ${name}`);
const result = await launch(commandFor(step), { cwd, env }).exit;
if (!succeeded(result)) throw new RunnerBuildError(name, result);
};
const cargoFailure = () => {
const result = cargo?.result();
return result && !succeeded(result) ? new RunnerBuildError("cargo build", result) : null;
};
if (!serial) startCargo();
try {
for (const step of STEPS_DURING_CARGO) {
// A cargo failure that has already surfaced ends the build before the
// next step, instead of after the whole TypeScript chain.
const failure = cargoFailure();
if (failure) throw failure;
await runStep(step);
}
} catch (error) {
if (cargo) await stop(cargo);
throw error;
}
timing.stepsEndedAt = Date.now();
if (serial) startCargo();
const cargoResult = await cargo.exit;
if (!succeeded(cargoResult)) throw new RunnerBuildError("cargo build", cargoResult);
await runStep(STAGE_BINARY_STEP);
for (const step of STEPS_AFTER_BINARY) await runStep(step);
const endedAt = Date.now();
const summary = {
mode: serial ? "serial" : "overlap",
cargo_ms: timing.cargoEndedAt - timing.cargoStartedAt,
steps_ms: timing.stepsEndedAt - startedAt,
// How long the build waited on cargo alone after the TypeScript steps.
cargo_wait_ms: Math.max(0, timing.cargoEndedAt - timing.stepsEndedAt),
total_ms: endedAt - startedAt,
};
log(
`${TIMING_LINE_PREFIX},${Object.entries(summary)
.map(([key, value]) => `${key}:${value}`)
.join(",")}`,
);
return summary;
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
runRunnerBuild().catch((error) => {
process.stderr.write(`[runner build] ${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
});
}
@@ -0,0 +1,245 @@
import assert from "node:assert/strict";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { test } from "node:test";
import {
CARGO_BUILD_ARGS,
RunnerBuildError,
STAGE_BINARY_STEP,
STEPS_AFTER_BINARY,
STEPS_DURING_CARGO,
TIMING_LINE_PREFIX,
defaultCargoCommand,
defaultCommandFor,
describeStep,
runRunnerBuild,
} from "./build-runner-package.mjs";
// Every stand-in appends "<name> start" and "<name> end" to a shared event
// log, so a test can assert what ran before what. Its arguments:
// log, name, exit code, marker to write at start, file to wait for before
// ending, marker to write at the end. The cargo stand-in also records its
// pid so a test can check that the build stopped it.
const STAND_IN = `
const fs = require("node:fs");
const [log, name, exitCode, startMarker, waitFor, endMarker] = process.argv.slice(1);
const append = (line) => fs.appendFileSync(log, line + "\\n");
if (name === "cargo") fs.writeFileSync(log + ".cargo.pid", String(process.pid));
append(name + " start");
if (startMarker) fs.writeFileSync(startMarker, "");
const deadline = Date.now() + 15000;
while (waitFor && !fs.existsSync(waitFor)) {
if (Date.now() > deadline) { append(name + " timeout"); process.exit(3); }
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
}
if (endMarker) fs.writeFileSync(endMarker, "");
append(name + " end");
process.exit(Number(exitCode));
`;
const FIRST_STEP = describeStep(STEPS_DURING_CARGO[0]);
const RELEASE_STEP = describeStep(STEPS_DURING_CARGO[STEPS_DURING_CARGO.length - 1]);
function harness({ exitCodes = {}, cargoExit = 0, cargoWaitsForRelease = true } = {}) {
const root = mkdtempSync(path.join(tmpdir(), "runner-build-"));
const log = path.join(root, "events.log");
const cargoStarted = path.join(root, "cargo.started");
const release = path.join(root, "release");
const standIn = (name, exitCode, startMarker = "", waitFor = "", endMarker = "") => ({
command: process.execPath,
args: ["-e", STAND_IN, log, name, String(exitCode), startMarker, waitFor, endMarker],
});
return {
root,
log,
events: () => (existsSync(log) ? readFileSync(log, "utf8").trim().split("\n") : []),
cargoPid: () => Number(readFileSync(`${log}.cargo.pid`, "utf8")),
options: {
cwd: root,
env: { ...process.env, PAPERCLIP_RUNNER_BUILD_SERIAL: "" },
log: () => {},
cargoCommand: () => standIn("cargo", cargoExit, cargoStarted, cargoWaitsForRelease ? release : ""),
commandFor: (step) => {
const name = describeStep(step);
// The first step waits for cargo's start marker so the event order
// between two processes started back to back is not left to chance.
// The last step during cargo releases the cargo stand-in.
return standIn(
name,
exitCodes[name] ?? 0,
"",
name === FIRST_STEP && cargoWaitsForRelease ? cargoStarted : "",
name === RELEASE_STEP ? release : "",
);
},
},
cleanup: () => rmSync(root, { recursive: true, force: true }),
};
}
const alive = (pid) => {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error.code === "EPERM";
}
};
async function waitUntilGone(pid) {
const deadline = Date.now() + 10_000;
while (alive(pid) && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 25));
}
return !alive(pid);
}
test("cargo starts before the first TypeScript step and is joined before the binary is staged", async () => {
const h = harness();
try {
const summary = await runRunnerBuild(h.options);
const events = h.events();
const at = (line) => {
const index = events.indexOf(line);
assert.notEqual(index, -1, `missing event ${line} in ${events.join(" | ")}`);
return index;
};
assert.ok(at("cargo start") < at(`${FIRST_STEP} start`), "cargo started after the first step");
for (let i = 1; i < STEPS_DURING_CARGO.length; i += 1) {
assert.ok(
at(`${describeStep(STEPS_DURING_CARGO[i - 1])} end`) < at(`${describeStep(STEPS_DURING_CARGO[i])} start`),
"TypeScript steps ran out of order",
);
}
// cargo was still running while the steps ran: it only ends once the last step releases it.
assert.ok(at(`${RELEASE_STEP} start`) < at("cargo end"), "cargo ended before the steps finished");
assert.ok(at("cargo end") < at(`${describeStep(STAGE_BINARY_STEP)} start`), "binary staged before cargo ended");
const afterNames = STEPS_AFTER_BINARY.map(describeStep);
assert.ok(at(`${describeStep(STAGE_BINARY_STEP)} end`) < at(`${afterNames[0]} start`));
assert.ok(at(`${afterNames[0]} end`) < at(`${afterNames[1]} start`));
assert.equal(summary.mode, "overlap");
for (const key of ["cargo_ms", "steps_ms", "cargo_wait_ms", "total_ms"]) {
assert.equal(typeof summary[key], "number", key);
assert.ok(summary[key] >= 0, key);
}
} finally {
h.cleanup();
}
});
test("a failing TypeScript step stops cargo and names the step", async () => {
const failing = describeStep(STEPS_DURING_CARGO[1]);
const h = harness({ exitCodes: { [failing]: 7 } });
try {
await assert.rejects(runRunnerBuild(h.options), (error) => {
assert.ok(error instanceof RunnerBuildError);
assert.equal(error.step, failing);
assert.match(error.message, /exited with code 7/);
return true;
});
assert.ok(await waitUntilGone(h.cargoPid()), "cargo stand-in outlived the failed build");
const events = h.events();
assert.ok(!events.includes("cargo end"), "cargo ran to completion after the failure");
assert.ok(!events.some((line) => line.startsWith(describeStep(STEPS_DURING_CARGO[2]))), "a later step ran after the failure");
assert.ok(!events.some((line) => line.startsWith(describeStep(STAGE_BINARY_STEP))), "the binary was staged after the failure");
} finally {
h.cleanup();
}
});
test("a failing cargo build fails the build before the binary is staged", async () => {
const h = harness({ cargoExit: 2, cargoWaitsForRelease: false });
try {
await assert.rejects(runRunnerBuild(h.options), (error) => {
assert.ok(error instanceof RunnerBuildError);
assert.equal(error.step, "cargo build");
assert.match(error.message, /exited with code 2/);
return true;
});
const events = h.events();
assert.ok(events.includes("cargo end"));
assert.ok(!events.some((line) => line.startsWith(describeStep(STAGE_BINARY_STEP))), "the binary was staged after cargo failed");
assert.ok(!events.some((line) => line.startsWith(describeStep(STEPS_AFTER_BINARY[0]))), "a post-binary step ran after cargo failed");
} finally {
h.cleanup();
}
});
test("a cargo failure that surfaces early ends the TypeScript chain at the next step", async () => {
const h = harness({ cargoExit: 2, cargoWaitsForRelease: false });
// The cargo stand-in fails at once and leaves a marker; the first step
// holds until that marker exists, so cargo's failure is known before the
// second step would start.
const cargoDone = path.join(h.root, "cargo.done");
const innerCargo = h.options.cargoCommand;
h.options.cargoCommand = () => {
const spec = innerCargo();
spec.args[spec.args.length - 1] = cargoDone;
return spec;
};
const innerStep = h.options.commandFor;
h.options.commandFor = (step) => {
const spec = innerStep(step);
if (describeStep(step) === FIRST_STEP) spec.args[spec.args.length - 2] = cargoDone;
return spec;
};
try {
await assert.rejects(runRunnerBuild(h.options), (error) => {
assert.equal(error.step, "cargo build");
return true;
});
const events = h.events();
assert.ok(events.includes(`${FIRST_STEP} end`), `first step should have run to its end: ${events.join(" | ")}`);
assert.ok(!events.some((line) => line.startsWith(describeStep(STEPS_DURING_CARGO[1]))), "the second step ran after cargo had failed");
} finally {
h.cleanup();
}
});
test("PAPERCLIP_RUNNER_BUILD_SERIAL=1 runs cargo after the TypeScript steps", async () => {
const h = harness({ cargoWaitsForRelease: false });
h.options.env = { ...process.env, PAPERCLIP_RUNNER_BUILD_SERIAL: "1" };
try {
const summary = await runRunnerBuild(h.options);
const events = h.events();
assert.ok(events.indexOf(`${RELEASE_STEP} end`) < events.indexOf("cargo start"));
assert.ok(events.indexOf("cargo end") < events.indexOf(`${describeStep(STAGE_BINARY_STEP)} start`));
assert.equal(summary.mode, "serial");
} finally {
h.cleanup();
}
});
test("the plan compiles the TypeScript surface once and keeps the binary readers after staging", () => {
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
assert.equal(packageJson.scripts.build, "node scripts/build-runner-package.mjs");
const wrapped = STEPS_DURING_CARGO.filter((step) => step.run).map((step) => step.run);
for (const name of wrapped) assert.ok(packageJson.scripts[name], `missing script ${name}`);
const compiles = (name) => name === "build:typescript" || packageJson.scripts[name].includes("build:typescript");
assert.deepEqual(wrapped.filter(compiles), ["build:typescript"]);
for (const step of [...STEPS_DURING_CARGO, STAGE_BINARY_STEP, ...STEPS_AFTER_BINARY]) {
assert.ok(!step.run || step.run !== "build:binary", "the plan must not run build:binary; the build runs cargo itself");
if (step.node) assert.ok(existsSync(new URL(`../${step.node[0]}`, import.meta.url)), `missing ${step.node[0]}`);
}
// build:binary stays the standalone way to compile and stage the binary,
// and the build runs the same cargo invocation.
const standalone = packageJson.scripts["build:binary"];
assert.equal(standalone, `cargo ${CARGO_BUILD_ARGS.join(" ")} && node ${STAGE_BINARY_STEP.node.join(" ")}`);
assert.deepEqual(defaultCargoCommand(), { command: "cargo", args: CARGO_BUILD_ARGS });
assert.deepEqual(STEPS_AFTER_BINARY.map(describeStep), [
"node scripts/generate-replay-goldens.mjs --check",
"node scripts/generate-semantic-action-catalog.mjs --check",
]);
assert.equal(typeof TIMING_LINE_PREFIX, "string");
});
test("nested pnpm runs use the pnpm that started the build", () => {
const viaPnpm = defaultCommandFor({ run: "build:typescript" }, { npm_execpath: "/opt/pnpm/bin/pnpm.cjs" });
assert.deepEqual(viaPnpm, { command: process.execPath, args: ["/opt/pnpm/bin/pnpm.cjs", "run", "build:typescript"], shell: undefined });
const viaPath = defaultCommandFor({ run: "build:typescript" }, {});
assert.equal(viaPath.command, "pnpm");
assert.deepEqual(viaPath.args, ["run", "build:typescript"]);
const direct = defaultCommandFor({ node: ["scripts/stage-runner-binary.mjs"] }, {});
assert.deepEqual(direct, { command: process.execPath, args: ["scripts/stage-runner-binary.mjs"] });
});
@@ -103,21 +103,33 @@ describe("server package build script", () => {
);
});
it("compiles the vendored Runner's TypeScript surface once per build", () => {
it("compiles the vendored Runner's TypeScript surface once per build", async () => {
// prepare:runner-vendor runs the Runner's full build, and `pnpm -r build`
// has already run it once by the time the server builds. Each extra
// build:typescript inside that chain (two tsc projects plus the eval-kernel
// build:typescript inside that build (two tsc projects plus the eval-kernel
// rebuild) costs about 7s on the CI Build job's critical path, twice.
// Checks that read dist/ must call their script directly, the way
// generate-replay-goldens.mjs already does, not through a `check:*`
// wrapper that rebuilds first for standalone use.
const runner = JSON.parse(readFileSync(new URL(
"../../../packages/paperclip-runner/package.json", import.meta.url,
), "utf8")) as { scripts: Record<string, string> };
const steps = runner.scripts.build.split(" && ");
const wrapped = steps
.filter((step) => step.startsWith("pnpm run "))
.map((step) => step.slice("pnpm run ".length));
//
// The Runner's build is scripts/build-runner-package.mjs, which runs the
// TypeScript steps while cargo compiles and exports the step plan, so the
// plan is read from the module rather than parsed out of a shell chain.
const runnerRoot = new URL("../../../packages/paperclip-runner/", import.meta.url);
const runner = JSON.parse(readFileSync(
new URL("package.json", runnerRoot), "utf8",
)) as { scripts: Record<string, string> };
expect(runner.scripts.build).toBe("node scripts/build-runner-package.mjs");
const plan = await import(new URL("scripts/build-runner-package.mjs", runnerRoot).href) as {
STEPS_DURING_CARGO: ReadonlyArray<{ run?: string; node?: string[] }>;
STAGE_BINARY_STEP: { node: string[] };
STEPS_AFTER_BINARY: ReadonlyArray<{ run?: string; node?: string[] }>;
describeStep: (step: { run?: string; node?: string[] }) => string;
};
const wrapped = plan.STEPS_DURING_CARGO
.filter((step) => step.run)
.map((step) => step.run as string);
for (const name of wrapped) {
expect(runner.scripts[name], `build step ${name}`).toBeDefined();
}
@@ -125,12 +137,22 @@ describe("server package build script", () => {
step === "build:typescript" || (runner.scripts[step] ?? "").includes("build:typescript");
expect(wrapped.filter(compiles)).toEqual(["build:typescript"]);
const steps = plan.STEPS_DURING_CARGO.map(plan.describeStep);
expect(steps).toContain("node scripts/generate-semantic-contracts.mjs --check");
expect(steps).toContain("node scripts/check-runner-workflow-traceability.mjs");
expect(steps.indexOf("node scripts/generate-semantic-contracts.mjs --check"))
.toBeGreaterThan(steps.indexOf("pnpm run build:typescript"));
expect(steps.indexOf("node scripts/check-runner-workflow-traceability.mjs"))
.toBeGreaterThan(steps.indexOf("pnpm run build:typescript"));
// The binary the server vendors is staged by the build itself, after
// cargo, and the steps that read it stay behind the staging step.
expect(plan.describeStep(plan.STAGE_BINARY_STEP)).toBe("node scripts/stage-runner-binary.mjs");
expect(plan.STEPS_AFTER_BINARY.map(plan.describeStep)).toEqual([
"node scripts/generate-replay-goldens.mjs --check",
"node scripts/generate-semantic-action-catalog.mjs --check",
]);
expect([...plan.STEPS_DURING_CARGO, ...plan.STEPS_AFTER_BINARY].map(plan.describeStep))
.not.toContain("pnpm run build:binary");
});
it("ships the pinned OpenCode dependency resolved by the vendored Runner", () => {