Files
PaperClipAI/scripts/__tests__/run-vitest-stable-shard.test.mjs
T
DottaandPaperclip 9f524ec1a7 fix: persist native Runner usage before transport acknowledgement (#18354)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent execution must preserve usage so Paperclip can price work and
enforce budgets.
> - Remote Runner execution can outlive its web receiver.
> - The receiver previously acknowledged Runner events before its later
usage recorder saved the accounting receipt.
> - This pull request saves required native usage receipts before that
transport acknowledgement.
> - A replacement receiver can accept the same retained report and
create one ledger charge.

## Linked Issues or Issue Description

**What happened?**

Native remote Runner usage could leave its durable outbox before the web
receiver saved its accounting receipt. The later recorder required a
local receipt directory. A replacement web receiver could lose the only
remaining report when the database write failed.

**Expected behavior**

The Runner retains unaccepted structured usage. Postgres owns accepted
receipts. Repeated delivery preserves one receipt for each source report
and one charge for the run. Missing usage keeps accounting pending.

**Steps to reproduce**

1. Start a supported built-in native remote Runner.
2. Make the receipt database insert fail after the provider reports
usage.
3. Replace the web receiver and its local directory.
4. Reconnect the same Runner and inspect the saved receipt and ledger.

**Paperclip version or commit**

The initial investigation used
`ebd05e9f4c88c66af7473db86bbaacfd1544aa0a`.

**Deployment mode**

A web server with a remote native Runner and shared Postgres.

Related work: #18327 owns output logs and provider traces. This change
has no dependency on its S3 export path. #18031 covers interrupted Codex
receipt recovery. #18005 covers legacy spool reads. #17516 covers Codex
stop-boundary retention.

## What Changed

- Add native producer receipt delivery through the existing Runner
callback before transport acknowledgement.
- Reuse `run_usage_receipts`, stable source sequences, and deterministic
report identities. Reject conflicting replay and retry a lost commit
response with the same receipt.
- Restore per-turn usage from structured receipt facts. Preserve
provider turns that share one PRP envelope turn.
- Bind retained warm transport callbacks to the current run authority
epoch, allowing old attachment control acknowledgements while preserving
accounting ownership.
- Persist Codex session baselines and cumulative totals in the existing
provider checkpoint. Mark qualified whole-run deltas, preserve them
across resume/restart/attachment, and leave old unmarked reports
incomplete and unpriced.
- Normalize the bounded final usage tail before the terminal event.
Retain later increases for explicit reconciliation, block further
attachment, and fail an affected successor without changing its
baseline.
- Complete runs from the accepted immutable receipt and its exact saved
rate-card amount; preserve existing live-budget enforcement.
- Skip the local receipt recorder for supported built-in native remote
Runner execution. Keep local, legacy, and external Dot/Muse behavior
unchanged.
- Keep pricing, reservations, ledger settlement, source fencing, and
budget holds with their existing owners.
- Put the actual Runner integration test in the existing Rust-cached
native test lane. Build before opening Postgres and give cold
compilation its own bounded budget.

## Verification

- Fresh Greptile review is 5/5 on
`e7376a50c8162770da1d581ecf25c8cffac64520`, with no unresolved findings.
Parent and independent peer review approved the final changes.
- Real PostgreSQL 18.6 and a real Rust Runner exercise failed receipt
insertion, receiver-directory loss, replay from the same producer, and
exactly one 25-cent ledger charge. The actual accounting spool path
stays absent. Optional logs/traces are disabled. The provider is
controlled; no paid request is made and the fixture never pre-saves the
expected receipt.
- Producer checks passed 32 usage/provider tests,37 terminal controls,
corrected completion, all 3 previously failing exit cases, and both full
transport goal-continuation cases. Rejected active-goal completion
retains its 120-token tail before failure. Baseline 100→successor150→33
duplicate old snapshots→old120 retains explicit incomplete
reconciliation evidence and positive budget exposure.
- Heartbeat executor checks passed 982/982. PostgreSQL tests preserve
exact 1.002 USD pricing, the 100.2-cent ledger amount, budget holds,
source takeover, partial/final receipts, cost-only corrections, equal
rate-card replay, and stale-completion interleaving. Newer incomplete
usage remains pending rather than settling an older snapshot.
- Runtime replay and strict usage-schema checks passed 2/2; generated
contracts and Rust formatting pass. Test-partition contracts passed
32/32.
- [Final combined-head
evidence](https://github.com/paperclipai/paperclip/pull/18354#issuecomment-6531526158)
records full workspace typecheck/build and 41/41 accounting tests
against all five cloud PR heads, including this final accounting commit.
It includes commands and evidence limits.
- Exact-head CI is green: 53successful checks, 6intentional skips, and
the legacy Snyk status passed. Typecheck, build, Runner, server,
browser, and aggregate gates pass. Runner Vitest1/2 initially failed an
unchanged mocked Antigravity login fixture; its focused case passed
locally, then one unchanged job retry passed 131 files/1995 tests,
including all 11 Antigravity tests. The initial failed attempt remains
recorded in CI.

Initial local attempts exposed the host SysV semaphore limit and
corrected harness path, IPC readiness, and queue-counter defects. A
temporary test loader selected an existing POSIX-semaphore PostgreSQL
18.6 build without changing repository dependencies. An optional
expanded debug provider run was interrupted at its unrelated 8192-turn
rollover stress case; no full-file local pass is claimed. Required
failure cases were run directly, and the full Runner CI remains
required.

## Risks

- The guarantee covers built-in native remote Runner execution. Local
native execution, legacy adapters, external Dot/Muse execution, and
server-originated model calls retain their existing contracts.
- This does not provide traffic switching or general web-server
failover. The existing controller identity, checkpoint, and generation
gates still apply.
- The receipt JSON gains optional structured producer facts. No schema
migration or accounting S3 spool is added. Text output and raw trace
payloads are excluded.
- Codex accounting authority requires a Runner artifact built from this
provider baseline implementation and qualified by digest. Update
receivers and strict usage-schema readers before releasing that Runner;
PRP does not negotiate this payload field. Existing unmarked Codex
reports remain pending/unpriced. No deployed Runner artifact is claimed
fixed.
- Usage arriving beyond the bounded terminal tail may require manual
reconciliation or a fresh session. Its evidence remains in the existing
producer checkpoint/outbox; no adjustment ledger or recurring scan is
added.

## Model Used

OpenAI `gpt-6.1-sol` assisted with the implementation, tests, and
documentation through code execution and repository tools. The context
window size is unavailable.

## 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-30 15:20:42 -05:00

597 lines
30 KiB
JavaScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
import {
defaultSuiteWeight,
loadShardDurations,
partitionGeneralServerSuites,
} from "../general-server-shard.mjs";
import { assertSelectedTests, partitionTestLines } from "../test-line-shard.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const script = path.join(repoRoot, "scripts", "run-vitest-stable.mjs");
const durationsManifest = path.join(repoRoot, "scripts", "general-server-shard-durations.json");
const serializedDurationsManifest = path.join(
repoRoot,
"scripts",
"serialized-shard-durations.json",
);
// Membership of general-server-without-chat depends on GITHUB_WORKFLOW (see
// prWorkflowName in run-vitest-stable.mjs), so strip the ambient value and
// make every test pin the caller it mirrors explicitly — these tests
// themselves run inside a workflow on CI.
function workflowEnv(envOverrides = {}) {
const env = { ...process.env, ...envOverrides };
if (!("GITHUB_WORKFLOW" in envOverrides)) {
delete env.GITHUB_WORKFLOW;
}
return env;
}
function dryRun(args, envOverrides = {}) {
const result = spawnSync(process.execPath, [script, ...args, "--dry-run"], {
cwd: repoRoot,
encoding: "utf8",
env: workflowEnv(envOverrides),
});
return result;
}
function dryRunJson(args, envOverrides = {}) {
const result = dryRun(args, envOverrides);
assert.equal(result.status, 0, `expected success for ${args.join(" ")}: ${result.stderr}`);
return JSON.parse(result.stdout);
}
const SHARD_COUNT = 12;
const SERIALIZED_SHARD_COUNT = 9;
test("the general-server shards form a complete, non-overlapping partition", () => {
const shards = Array.from({ length: SHARD_COUNT }, (_, index) =>
dryRunJson(["--mode", "general", "--group", "general-server", "--shard-index", String(index), "--shard-count", String(SHARD_COUNT)]),
);
const total = shards[0].generalServerSuiteCount;
assert.ok(total > 0, "expected a non-empty general-server suite set");
const seen = new Set();
let selectedTotal = 0;
for (const shard of shards) {
assert.equal(shard.generalServerSuiteCount, total, "suite count must be stable across shards");
for (const file of shard.selectedGeneralServerSuites) {
assert.ok(
!(file.startsWith("server/src/__tests__/") && /[^/]*(?:route|routes|authz)[^/]*\.test\.ts$/.test(file)),
`route/authz suite must stay in the serialized lane, not general-server: ${file}`,
);
assert.ok(!seen.has(file), `suite assigned to more than one shard: ${file}`);
seen.add(file);
selectedTotal += 1;
}
}
// Every suite runs exactly once: union covers the whole set with no overlap.
assert.equal(selectedTotal, total, "every suite must be selected exactly once");
assert.equal(seen.size, total, "union of shards must cover the whole suite set");
});
test("general shards retain scripts and route-named suites outside the serialized directory", () => {
const general = dryRunJson(["--mode", "general", "--group", "general-server", "--shard-index", "0", "--shard-count", "1"]);
for (const file of [
"server/scripts/verify-runner-vendor-dependencies.test.mjs",
"server/src/services/openrouter-models.test.ts",
"server/src/routes/setup-token-route.test.ts",
]) assert.ok(general.selectedGeneralServerSuites.includes(file), `missing configured server suite: ${file}`);
});
for (const group of ["general-workspaces-b", "general-workspaces-c"]) {
test(`shard flags are rejected for the ${group} group`, () => {
const result = dryRun(["--mode", "general", "--group", group, "--shard-index", "0", "--shard-count", "3"]);
assert.notEqual(result.status, 0, `${group} must not accept shard flags`);
});
}
test("workspace lanes cover every non-server project in the root Vitest configuration", () => {
const config = readFileSync(path.join(repoRoot, "vitest.config.ts"), "utf8");
const roots = [...config.matchAll(/^\s+"([^"]+)",?\s*$/gm)].map(match => match[1]);
assert.ok(roots.includes("server"), "expected the explicit root Vitest project list");
const expected = roots.filter(root => root !== "server")
.map(root => JSON.parse(readFileSync(path.join(repoRoot, root, "package.json"), "utf8")).name).sort();
const actual = ["general-workspaces-a", "general-workspaces-b", "general-workspaces-c"]
.flatMap(group => dryRunJson(["--mode", "general", "--group", group]).workspaceProjects).sort();
assert.deepEqual(actual, expected, "no configured project may be silently omitted or run twice");
});
// Collects every `- group:` matrix entry in a workflow with the keys it sets.
// Comment and blank lines are dropped first, and an entry ends only when the
// indentation returns to the item level, so a comment between two keys cannot
// hide a shard setting from the assertions below.
function matrixEntries(text) {
const entries = [];
let current = null;
for (const line of text.split("\n")) {
if (/^\s*(#|$)/.test(line)) continue;
const indent = line.match(/^ */)[0].length;
const item = line.match(/^ *- group: (\S+)/);
if (item) {
current = { group: item[1], indent, keys: ["group"] };
entries.push(current);
continue;
}
if (!current) continue;
if (indent <= current.indent) { current = null; continue; }
const key = line.match(/^ *([A-Za-z_]+):/);
if (key) current.keys.push(key[1]);
}
return entries;
}
test("matrixEntries sees shard keys that sit behind comments or later in the entry", () => {
const entries = matrixEntries([
" include:",
" - group: general-workspaces-b",
" group_label: workspaces-b",
" # a comment between keys must not hide the shard setting",
" shard_index: 0",
"",
" shard_count: 3",
" - group: general-workspaces-c",
" group_label: workspaces-c",
" steps:",
" - name: unrelated",
" shard_index: 9",
].join("\n"));
assert.deepEqual(entries.map(entry => entry.group), ["general-workspaces-b", "general-workspaces-c"]);
assert.deepEqual(entries[0].keys, ["group", "group_label", "shard_index", "shard_count"]);
assert.deepEqual(entries[1].keys, ["group", "group_label"], "keys after the matrix must not leak into the last entry");
});
test("workspaces-c runs the db project alone and workspaces-b no longer carries it", () => {
// The db project was 168-250s of the 436-513s workspaces-b vitest time
// (PR runs 38084171761, 38083084276 and 38083020834, 2026-10-10), so it
// runs in its own lane. The PR and release matrices both carry that lane.
const workspacesC = dryRunJson(["--mode", "general", "--group", "general-workspaces-c"]);
assert.deepEqual(workspacesC.workspaceProjects, ["@paperclipai/db"]);
assert.equal(workspacesC.workspacesVitestShard, null);
const workspacesB = dryRunJson(["--mode", "general", "--group", "general-workspaces-b"]);
assert.ok(!workspacesB.workspaceProjects.includes("@paperclipai/db"), "db must not run twice");
assert.ok(workspacesB.workspaceProjects.length > 0, "workspaces-b must still run its other projects");
for (const workflow of ["pr-trusted.yml", "release-verify.yml"]) {
const entries = workflow === "pr-trusted.yml"
? JSON.parse(readFileSync(path.join(repoRoot, ".github/ci-task-matrix.json"), "utf8")).general_tests
.map((entry) => ({ ...entry, keys: Object.keys(entry) }))
: matrixEntries(readFileSync(path.join(repoRoot, ".github", "workflows", workflow), "utf8"));
// The parser must demonstrably see shard keys in this workflow, otherwise
// the unsharded assertions below could pass for the wrong reason.
const sharded = entries.filter(entry => entry.group === "general-workspaces-a");
assert.ok(sharded.length > 0 && sharded.every(entry => entry.keys.includes("shard_index") && entry.keys.includes("shard_count")),
`${workflow} must carry the sharded workspaces-a entries`);
for (const group of ["general-workspaces-b", "general-workspaces-c"]) {
const matching = entries.filter(entry => entry.group === group);
assert.equal(matching.length, 1, `${workflow} must run ${group} exactly once`);
assert.ok(matching[0].keys.includes("group_label"), `${workflow} must label the ${group} job`);
assert.deepEqual(matching[0].keys.filter(key => key.startsWith("shard_")), [], `${workflow} must run ${group} unsharded`);
}
}
});
test("workspaces-a shards map to Vitest native --shard slices over a stable project list", () => {
const shards = [0, 1].map((index) =>
dryRunJson([
"--mode", "general", "--group", "general-workspaces-a",
"--shard-index", String(index), "--shard-count", "2",
]),
);
assert.deepEqual(
shards.map((shard) => shard.workspacesVitestShard),
["1/2", "2/2"],
"each matrix job must pass its own --shard slice to vitest",
);
// Vitest's --shard partitions each project's file list deterministically, so
// an identical project list across jobs is what guarantees complete,
// non-overlapping coverage of the lane.
assert.deepEqual(shards[0].workspaceProjects, shards[1].workspaceProjects);
assert.ok(shards[0].workspaceProjects.length > 0, "workspaces-a must run at least one project");
const unsharded = dryRunJson(["--mode", "general", "--group", "general-workspaces-a"]);
assert.deepEqual(
unsharded.workspaceProjects,
shards[0].workspaceProjects,
"sharding must not change which projects the lane covers",
);
assert.equal(unsharded.workspacesVitestShard, null);
});
test("duration-aware partition balances skewed weights better than round-robin", () => {
// Round-robin puts all three heavy suites on shard 0 (indexes 0, 3, 6).
const files = ["a", "b", "c", "d", "e", "f", "g", "h", "i"];
const durations = { a: 30000, d: 30000, g: 30000, b: 100, c: 100, e: 100, f: 100, h: 100, i: 100 };
const shards = partitionGeneralServerSuites(files, 3, durations);
const totals = shards.map((shard) => shard.totalWeight);
const maxTotal = Math.max(...totals);
const minTotal = Math.min(...totals);
assert.ok(
maxTotal - minTotal <= 200,
`expected near-even shard weights, got ${totals.join(", ")}`,
);
assert.equal(
shards.flatMap((shard) => shard.files).sort().join(","),
files.join(","),
"partition must cover every file exactly once",
);
});
test("the partition is deterministic for identical inputs", () => {
const files = Array.from({ length: 50 }, (_, index) => `suite-${index}.test.ts`);
const durations = Object.fromEntries(files.map((file, index) => [file, (index * 37) % 5000]));
const first = partitionGeneralServerSuites(files, 3, durations);
const second = partitionGeneralServerSuites(files, 3, durations);
assert.deepEqual(first, second, "same inputs must always produce the same partition");
});
test("suites missing from the manifest get the median weight", () => {
assert.equal(defaultSuiteWeight({ a: 100, b: 300, c: 900 }), 300);
assert.equal(defaultSuiteWeight({ a: 100, b: 300, c: 500, d: 900 }), 400);
assert.equal(defaultSuiteWeight({}), 1000, "empty manifest falls back to a fixed weight");
});
test("a missing or malformed manifest degrades to uniform weights", () => {
assert.deepEqual(loadShardDurations(path.join(repoRoot, "scripts", "no-such-manifest.json")), {});
const files = ["a", "b", "c", "d"];
const shards = partitionGeneralServerSuites(files, 2, {});
assert.equal(shards[0].files.length + shards[1].files.length, files.length);
assert.equal(Math.abs(shards[0].files.length - shards[1].files.length), 0);
});
test("the checked-in manifest loads and covers most of the current suite set", () => {
const durations = loadShardDurations(durationsManifest);
assert.ok(Object.keys(durations).length > 0, "manifest must parse to a non-empty duration map");
const shard = dryRunJson(["--mode", "general", "--group", "general-server", "--shard-index", "0", "--shard-count", "1"]);
const currentFiles = shard.selectedGeneralServerSuites;
const known = currentFiles.filter((file) => durations[file] !== undefined).length;
assert.ok(
known / currentFiles.length >= 0.5,
`manifest is stale: only ${known} of ${currentFiles.length} suites have recorded durations — regenerate it from a recent PR run (see the manifest's $comment)`,
);
});
test("the chat integration suite keeps a measured duration for duration-aware fallbacks", () => {
// The PR and release matrices both run the chat suite in dedicated
// line-sharded lanes, but the plain general-server group (local full runs)
// still weighs it into the LPT partition; a median-fallback weight there
// would silently overload whichever shard receives it.
const chatSuite = "server/src/__tests__/chat-channels.integration.test.ts";
const durations = loadShardDurations(durationsManifest);
assert.ok(
Number.isFinite(durations[chatSuite]),
"the full chat cohort must have a measured duration, not the median fallback",
);
});
test("the checked-in serialized manifest loads and covers most of the current suite set", () => {
const durations = loadShardDurations(serializedDurationsManifest);
assert.ok(Object.keys(durations).length > 0, "manifest must parse to a non-empty duration map");
const shard = dryRunJson(["--mode", "serialized", "--shard-index", "0", "--shard-count", "1"]);
const currentFiles = shard.selectedSerializedSuites;
const known = currentFiles.filter((file) => durations[file] !== undefined).length;
assert.ok(
known / currentFiles.length >= 0.5,
`manifest is stale: only ${known} of ${currentFiles.length} suites have recorded durations — regenerate it from a recent PR run (see the manifest's $comment)`,
);
});
test("the real serialized shard partition is complete, non-overlapping and duration-balanced", () => {
const durations = loadShardDurations(serializedDurationsManifest);
const fallback = defaultSuiteWeight(durations);
const shards = Array.from({ length: SERIALIZED_SHARD_COUNT }, (_, index) =>
dryRunJson(["--mode", "serialized", "--shard-index", String(index), "--shard-count", String(SERIALIZED_SHARD_COUNT)]),
);
const total = shards[0].serializedSuiteCount;
const selected = shards.flatMap((shard) => shard.selectedSerializedSuites);
assert.equal(selected.length, total, "every serialized suite must be selected exactly once");
assert.equal(new Set(selected).size, total, "serialized shards must not overlap");
const totals = shards.map((shard) =>
shard.selectedSerializedSuites.reduce((sum, file) => sum + (durations[file] ?? fallback), 0),
);
const maxTotal = Math.max(...totals);
const minTotal = Math.min(...totals);
// LPT keeps the spread within the heaviest single suite; use that as the bound.
const heaviest = Math.max(...Object.values(durations));
assert.ok(
maxTotal - minTotal <= heaviest,
`serialized shard weight spread ${maxTotal - minTotal}ms exceeds heaviest suite ${heaviest}ms: ${totals.join(", ")}`,
);
});
const chatSuitePath = "server/src/__tests__/chat-channels.integration.test.ts";
const nativeRunnerSuitePath =
"server/src/services/native-runtime/native-codex-runner.integration.test.ts";
const usageDeliverySuitePath = "server/src/services/native-runtime/native-usage-delivery.integration.test.ts";
const dotRunnerSuitePath = "server/src/__tests__/dot-runner.test.ts";
const restartRecoverySuitePath =
"server/src/services/native-runtime/native-runner-restart-recovery.integration.test.ts";
// Mirrors pr-trusted.yml (12 shards, called by pr.yml so GITHUB_WORKFLOW is
// "PR"): the chat suite runs in its dedicated lanes and the cargo-dependent
// native-runner suite in the Rust-cached final Verify Paperclip Runner vitest
// shard, so together the three cover the full server group exactly.
test("12 PR without-chat shards plus the dedicated chat and native-runner lanes cover the original server group exactly", () => {
const durations = loadShardDurations(durationsManifest);
const fallback = defaultSuiteWeight(durations);
const prEnv = { GITHUB_WORKFLOW: "PR" };
const full = dryRunJson(["--mode", "general", "--group", "general-server", "--shard-index", "0", "--shard-count", "1"], prEnv);
const shards = Array.from({ length: 12 }, (_, index) => dryRunJson([
"--mode", "general", "--group", "general-server-without-chat",
"--shard-index", String(index), "--shard-count", "12",
], prEnv));
const totals = shards.map((shard) =>
shard.selectedGeneralServerSuites.reduce((sum, file) => sum + (durations[file] ?? fallback), 0),
);
const maxTotal = Math.max(...totals);
const minTotal = Math.min(...totals);
// LPT keeps the spread within the heaviest single suite; use that as the
// bound. The chat and native-runner suites run in their own lanes, so
// exclude them here.
const chat = "server/src/__tests__/chat-channels.integration.test.ts";
const nativeRunner =
"server/src/services/native-runtime/native-codex-runner.integration.test.ts";
const heaviest = Math.max(
...Object.entries(durations)
.filter(([file]) => file !== chat && file !== nativeRunner)
.map(([, ms]) => ms),
);
assert.ok(
maxTotal - minTotal <= heaviest,
`shard weight spread ${maxTotal - minTotal}ms exceeds heaviest suite ${heaviest}ms: ${totals.join(", ")}`,
);
const files = shards.flatMap((shard) => shard.selectedGeneralServerSuites);
assert.ok(!files.includes(chatSuitePath));
assert.ok(!files.includes(nativeRunnerSuitePath));
assert.ok(!files.includes(dotRunnerSuitePath));
assert.ok(!files.includes(restartRecoverySuitePath));
assert.ok(!files.includes(usageDeliverySuitePath));
assert.deepEqual([...files, chatSuitePath, nativeRunnerSuitePath, dotRunnerSuitePath, restartRecoverySuitePath, usageDeliverySuitePath, "server/src/services/native-runtime/plugin-runner-probe.test.ts"].sort(), full.selectedGeneralServerSuites.sort());
assert.equal(new Set(files).size, files.length);
const defaultRun = dryRunJson([], prEnv);
assert.ok(defaultRun.generalServerSuiteCount === full.generalServerSuiteCount);
});
// The legacy group and local default must remain complete when a caller has
// not selected a separate native lane.
for (const [caller, envOverrides] of [["Release", { GITHUB_WORKFLOW: "Release" }], ["no ambient workflow", {}]]) {
test(`10 without-chat shards under ${caller} keep the native-runner suite and cover the server group with chat alone`, () => {
const full = dryRunJson(["--mode", "general", "--group", "general-server", "--shard-index", "0", "--shard-count", "1"], envOverrides);
const shards = Array.from({ length: 10 }, (_, index) => dryRunJson([
"--mode", "general", "--group", "general-server-without-chat",
"--shard-index", String(index), "--shard-count", "10",
], envOverrides));
const files = shards.flatMap((shard) => shard.selectedGeneralServerSuites);
assert.ok(!files.includes(chatSuitePath));
assert.ok(files.includes(nativeRunnerSuitePath));
assert.ok(files.includes(dotRunnerSuitePath));
assert.ok(files.includes(restartRecoverySuitePath));
assert.deepEqual([...files, chatSuitePath].sort(), full.selectedGeneralServerSuites.sort());
assert.equal(new Set(files).size, files.length);
});
}
// Release verification selects its dedicated lane explicitly. Do not infer it
// from the caller name: previews and future workflow callers need the same cover.
for (const caller of ["Release", "Cloud readiness", "another caller"]) {
test(`release server, chat, and native lanes cover every server suite once under ${caller}`, () => {
const env = { GITHUB_WORKFLOW: caller };
const full = dryRunJson(["--mode", "general", "--group", "general-server", "--shard-index", "0", "--shard-count", "1"], env);
const files = Array.from({ length: 10 }, (_, index) => dryRunJson([
"--mode", "general", "--group", "general-server-without-chat-or-native-runner",
"--shard-index", String(index), "--shard-count", "10",
], env)).flatMap((shard) => shard.selectedGeneralServerSuites);
const native = dryRunJson(["--mode", "general", "--group", "general-server-native-runner"], env);
const combined = [...files, chatSuitePath, ...native.selectedGeneralServerSuites];
assert.equal(new Set(combined).size, combined.length, "lanes must not overlap");
assert.deepEqual(combined.sort(), full.selectedGeneralServerSuites.sort());
});
}
test("the native-runner lane runs exactly the cargo-dependent suites", () => {
const lane = dryRunJson(["--mode", "general", "--group", "general-server-native-runner"]);
assert.deepEqual(lane.selectedGeneralServerSuites, [
"server/src/services/native-runtime/plugin-runner-probe.test.ts",
"server/src/services/native-runtime/native-codex-runner.integration.test.ts",
dotRunnerSuitePath,
restartRecoverySuitePath,
usageDeliverySuitePath,
]);
});
test("shard flags are rejected for the native-runner group", () => {
const result = dryRun(["--mode", "general", "--group", "general-server-native-runner", "--shard-index", "0", "--shard-count", "2"]);
assert.notEqual(result.status, 0, "the native-runner lane is a single suite and must not accept shard flags");
});
// The PR-side exclusion above is safe only while the wiring it assumes holds:
// pr.yml (the caller whose name reusable pr-trusted.yml jobs see as
// GITHUB_WORKFLOW) is named PR, the sharded vitest lanes partition cleanly
// with exactly one final shard, and that lane's package script routes through
// the wrapper that runs the native-runner group.
test("the PR workflow wiring for the native-runner lane holds", () => {
const prWorkflow = readFileSync(path.join(repoRoot, ".github/workflows/pr.yml"), "utf8");
assert.match(prWorkflow, /^name: PR$/m,
"renaming pr.yml silently moves the native-runner suite back into the uncached server shards");
const matrix = JSON.parse(readFileSync(path.join(repoRoot, ".github/ci-task-matrix.json"), "utf8"));
const lanes = matrix.verify_paperclip_runner
.map((entry) => /test:typescript:vitest --shard=(\d+)\/(\d+)/.exec(entry.command))
.filter(Boolean).map((match) => [Number(match[1]), Number(match[2])]);
assert.ok(lanes.length > 0, "expected sharded test:typescript:vitest lanes in pr-trusted.yml");
assert.equal(new Set(lanes.map(([, count]) => count)).size, 1, "vitest lanes must agree on the shard count");
const shardCount = lanes[0][1];
assert.deepEqual(
lanes.map(([index]) => index).sort((left, right) => left - right),
Array.from({ length: shardCount }, (_, index) => index + 1),
"vitest lanes must cover every shard exactly once",
);
assert.equal(lanes.filter(([index, count]) => index === count).length, 1,
"exactly one final vitest shard carries the native-runner group");
const runnerPackage = JSON.parse(
readFileSync(path.join(repoRoot, "packages/paperclip-runner/package.json"), "utf8"),
);
assert.equal(runnerPackage.scripts["test:typescript:vitest"], "node ./scripts/run-pr-vitest-lane.mjs");
});
const laneWrapper = path.join(repoRoot, "packages/paperclip-runner/scripts/run-pr-vitest-lane.mjs");
function wrapperPlan(args, envOverrides = {}) {
const result = spawnSync(process.execPath, [laneWrapper, ...args, "--dry-run"], {
cwd: repoRoot,
encoding: "utf8",
env: workflowEnv(envOverrides),
});
assert.equal(result.status, 0, `expected wrapper dry run to succeed for ${args.join(" ")}: ${result.stderr}`);
return JSON.parse(result.stdout);
}
test("the PR vitest lane wrapper runs the native-runner group exactly on the final PR shard", () => {
for (const [args, envOverrides, expected] of [
[["--shard=1/2"], { GITHUB_WORKFLOW: "PR" }, false],
[["--shard=2/2"], { GITHUB_WORKFLOW: "PR" }, true],
[[], { GITHUB_WORKFLOW: "PR" }, true],
[["--shard=2/2"], { GITHUB_WORKFLOW: "Release" }, false],
[["--shard=2/2"], {}, false],
[[], {}, false],
]) {
const plan = wrapperPlan(args, envOverrides);
assert.equal(plan.runNativeRunnerGroup, expected,
`args ${JSON.stringify(args)} env ${JSON.stringify(envOverrides)}`);
const commands = plan.plannedCommands.map((planned) => planned.args.join(" "));
assert.ok(commands.some((command) => command.startsWith(`exec vitest run${args.length ? ` ${args.join(" ")}` : ""}`)),
"the wrapper must pass shard flags through to vitest");
assert.equal(
commands.some((command) => command.includes("--group general-server-native-runner")),
expected,
);
}
const malformed = spawnSync(process.execPath, [laneWrapper, "--shard=nonsense", "--dry-run"], {
cwd: repoRoot,
encoding: "utf8",
env: workflowEnv({ GITHUB_WORKFLOW: "PR" }),
});
assert.notEqual(malformed.status, 0, "a malformed shard flag must fail rather than guess a lane");
});
const lineShardFile = path.join(repoRoot, "server/src/__tests__/chat-channels.integration.test.ts");
const caseAt = (line, name) => ({ name, file: lineShardFile, projectName: "@paperclipai/server", location: { line, column: 3 } });
test("test-line shards cover nested and parameterized cases exactly once without splitting a source line", () => {
const cases = [caseAt(10, "suite > nested > first"), caseAt(10, "suite > nested > second"),
caseAt(20, "same name"), caseAt(30, "same name"), caseAt(40, "last"), caseAt(50, "new case")];
const shards = partitionTestLines(cases, 3, lineShardFile);
assert.deepEqual(shards.map((shard) => shard.tests.length), [2, 2, 2]);
assert.equal(shards.filter((shard) => shard.lines.includes(10)).length, 1);
assert.equal(shards.find((shard) => shard.lines.includes(10)).tests.length, 2);
assert.equal(shards.flatMap((shard) => shard.lines).length, 5);
assert.deepEqual(shards.flatMap((shard) => shard.tests).sort((a, b) => a.location.line - b.location.line), cases);
assert.deepEqual(partitionTestLines([...cases].reverse(), 3, lineShardFile).map((shard) => shard.lines), shards.map((shard) => shard.lines));
});
test("line-shard collection rejects empty, foreign, or unlocated tests and invalid shard counts", () => {
const good = caseAt(10, "valid");
for (const input of [[], null, [{ ...good, file: "/another.test.ts" }], [{ ...good, projectName: "wrong" }],
[{ ...good, location: undefined }], [{ ...good, location: { line: 0 } }], [{ ...good, name: "" }]]) {
assert.throws(() => partitionTestLines(input, 1, lineShardFile));
}
for (const count of [0, -1, 1.5, Infinity, 2]) assert.throws(() => partitionTestLines([good], count, lineShardFile));
});
test("filtered collection must match the exact assigned case identities, including duplicates", () => {
const expected = [caseAt(10, "same"), caseAt(10, "same"), caseAt(20, "nested > case")];
assertSelectedTests(expected, [...expected].reverse(), lineShardFile);
for (const actual of [expected.slice(1), [...expected, caseAt(30, "extra")],
[expected[0], expected[1], caseAt(20, "renamed")],
[expected[0], expected[1], caseAt(21, "nested > case")]]) {
assert.throws(() => assertSelectedTests(expected, actual, lineShardFile));
}
});
test("parallel chat jobs form a complete disjoint partition", () => {
for (const count of [2, 3, 4]) {
const shards = Array.from({ length: count }, (_, index) =>
dryRunJson(["--mode", "general", "--group", "general-chat", "--shard-index", String(index), "--shard-count", String(count)],
{ PAPERCLIP_TEST_DATABASE_URL: "" }).chatVitestShards,
);
assert.ok(shards.every((workers) => workers.length === 2));
assert.deepEqual(shards.flat().map(({ index }) => index).sort((a, b) => a - b),
Array.from({ length: count * 2 }, (_, index) => index));
assert.ok(shards.flat().every((shard) => shard.count === count * 2));
}
});
test("shared-database and unsharded chat runs retain one serial partition", () => {
for (let index = 0; index < 3; index++) {
const result = dryRunJson(["--mode", "general", "--group", "general-chat", "--shard-index", String(index), "--shard-count", "3"],
{ PAPERCLIP_TEST_DATABASE_URL: "postgresql://shared/test" });
assert.deepEqual(result.chatVitestShards, [{ index, count: 3 }]);
}
assert.deepEqual(dryRunJson(["--mode", "general", "--group", "general-chat"],
{ PAPERCLIP_TEST_DATABASE_URL: "" }).chatVitestShards, [{ index: 0, count: 1 }]);
});
test("a failed chat child fails the job after its sibling finishes", { skip: process.platform === "win32" }, () => {
const directory = mkdtempSync(path.join(os.tmpdir(), "chat-launch-proof-"));
const file = path.join(repoRoot, "server/src/__tests__/chat-channels.integration.test.ts");
const lines = readFileSync(file, "utf8").split("\n")
.flatMap((line, index) => /^\s*it\(/.test(line) ? [index + 1] : []).slice(0, 6);
assert.equal(lines.length, 6);
const tests = lines.map((line) => ({ file, projectName: "@paperclipai/server",
name: `launcher fixture ${line}`, location: { line } }));
const receipt = path.join(directory, "sibling-finished");
const fakePnpm = path.join(directory, "pnpm");
try {
writeFileSync(fakePnpm, `#!${process.execPath}
const fs = require('node:fs');
const args = process.argv.slice(2);
const tests = ${JSON.stringify(tests)};
const selected = args.flatMap(arg => {
const match = arg.match(/chat-channels\\.integration\\.test\\.ts:(\\d+)$/);
return match ? [Number(match[1])] : [];
});
if (args.includes('list')) {
const output = args.find(arg => arg.startsWith('--json=')).slice(7);
fs.writeFileSync(output, JSON.stringify(selected.length
? tests.filter(test => selected.includes(test.location.line)) : tests));
} else if (selected.includes(${lines[0]})) process.exit(7);
else setTimeout(() => fs.writeFileSync(${JSON.stringify(receipt)}, 'finished'), 200);
`);
chmodSync(fakePnpm, 0o755);
const result = spawnSync(process.execPath, [script, "--mode", "general", "--group", "general-chat",
"--shard-index", "0", "--shard-count", "3"], {
cwd: repoRoot, encoding: "utf8", timeout: 30_000,
env: { ...process.env, PATH: `${directory}${path.delimiter}${process.env.PATH}`,
PAPERCLIP_TEST_DATABASE_URL: "", PAPERCLIP_CI_PROFILE: "false" },
});
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stderr, /failed \(7\)/);
assert.equal(readFileSync(receipt, "utf8"), "finished", "a failed worker must not abandon its sibling");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});