Files
PaperClipAI/scripts/__tests__/run-vitest-stable-shard.test.mjs
T
Nicky LeachandPaperclip 2506bcdaf4 Admit exact stopped Runner sources under the current run lease (#19134)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native execution keeps provider state and a durable controller
journal.
> - A completed process can stop before the next run attaches.
> - The current run must own recovery of that exact source.
> - An old controller must lose permission to change the consumed source
journal.
> - This pull request adds explicit source admission and protected task
dispatch.
> - The benefit is a narrow recovery boundary that preserves the
original successful result.

## Linked Issues or Issue Description

Native prerequisites #19132 and #19153 are merged into master. This PR
targets master and updates the TypeScript Runner driver contract
alongside Core and SDK admission. This change adds the SDK and Core
admission boundary. Executor selection and fresh source preparation are
a separate change. Related #17916 supplies the workspace export holds
preserved here.

## What Changed

- Plugins advertise `codexSourceRecoveryVersion: 1` for Codex App Server
and `acpxSourceRecoveryVersion: 1` for ACPX. Hosts can qualify either,
both, or neither. Core selects the matching field, because a
stopped-source check for one provider cannot establish the other
provider’s recovery guarantees. ACPX orchestration is tracked separately
in #19196. Use the existing exact source identity and `resumeFrom` under
the authenticated current run. No public recovery mode is added. Require
the claimed source, current account and prepared launch at protected
dispatch.
- Keep the first source binding pinned on the consumer lease, including
after uncertain dispatch. A newly acquired coordinator can refresh only
the same source.
- Claim one stopped successful source under the current run's acquired
native lease. Store the token-free consumer reference in existing lease
metadata. Preserve the source status and result.
- Check the exact six source identity fields, encrypted journal header,
company, task, agent, environment, driver, workspace, account selection,
and current coordinator generation. Keep workspace export and legacy
settlement holds.
- Serialize claims and checkpoint writers in the same coordinator lock
order. Reject stale source writes after consumption. Check the current
consumer before recovered source writes.
- Check source admission before and after protected worker dispatch.
Keep the request and lease owned by the current run while the native
identity remains the source.
- Allow the same owner to prepare B after a fresh source submission,
using the exact consumed-source proof instead of a generic warm-reuse
flag. Recheck that proof after dispatch.
- Preserve ordinary cached adoption and repeated preparation after
launch. The current authenticated owner can differ from the adopted PRP
identity without requesting a new source launch. Keep ordinary suspended
recovery and live task attachment checks. Extract the shared source
resolver to avoid a module cycle, and make ordinary selection honor the
same durable source consumer.

## Verification

- Current head `cac9cf188a` includes merged #19300 and the symmetric
capabilities introduced in `b7e25a64b4`. The task-owner and warm-rebind
fixtures now identify their physical task. All 29 SDK schema cases, 95
Core ownership and source-admission cases, 25 native Codex and ACPX
recovery cases, and the binary metadata check passed on this head. The
SDK and Runner builds and server typecheck passed. Both native providers
reject the other provider’s capability. GitHub CI is green and [standard
Greptile scored this commit
5/5](https://github.com/paperclipai/paperclip/pull/19134#issuecomment-6645574114)
with no open threads.

- Previous head `bda49271a4ae362a15baf3fe6d6d297e25fd2000` is directly
based on master after #19132 and #19153 landed. [GitHub workflow
42072192023](https://github.com/paperclipai/paperclip/actions/runs/42072192023)
passed on its first attempt. All check runs completed successfully or as
intentional workflow skips; Snyk passed. Standard Greptile scored this
exact head 5/5. That review had no unresolved findings.
- Runner and SDK TypeScript builds and server typecheck passed. The
focused SDK, task-owner, task-runtime, PostgreSQL source and authority
suites passed all 161 cases. The production transport-factory getter
regression and 14 native-lane selection checks also passed.
- The Linux native lane executed the new ordinary warm A→B process case.
It kept one Runner/provider process and completed two turns on the same
thread. The eager-getter mutation fails with
`native_runner_source_attachment_pending`; preserving the live getter
passes. The production factory test changes the current owner after
transport creation and checks that same options object.
- PostgreSQL cases cover both concurrent source-writer/claim orders,
exact source submission under the current caller, source-switch denial
after uncertain dispatch, same-source coordinator refresh, stale
same-boot generation, corrupted journal headers, competing consumers,
account changes and export holds.
- The protected dispatch sequence submits A, prepares B and retries B
preparation. It rejects undispatched sources, changed accounts and
foreign target bindings. Source status and accepted result remain
unchanged.
- Ordinary adopted A preparation under current B, repeated ordinary
preparation after launch and ordinary suspended continuation retain
their existing path. Absent or false claims preserve ordinary identity
denial; malformed claims fail closed.
- Earlier development attempts exposed ordinary preparation regressions
and the live-getter omission; the final regressions cover those fixes.
The first embedded PostgreSQL attempt needed missing library aliases
repaired in ignored dependencies. Those attempts are not passing
evidence.
- Historical, superseded CI heads had unchanged UI fake-timer and
Playwright health-response-lifetime failures. Their isolated cases and
scoped retries passed. The response-lifetime cause remains unproven. No
UI/test timeout was changed. The final rebased-head workflow above
passed without a retry.
- Whitespace and added-line secret/PII scans passed. Full workspace
checks ran on GitHub. No live vendor probe or deployment was performed.

## Risks

- Any driver that used the earlier Codex TypeScript option must use
`codexSourceRecoveryVersion`. The new plugin description rejects the
generic spelling. The Launcher’s existing Codex-specific HTTP header can
map to this property when the plugin adopts the new SDK. The change
requires no journal migration or ROM change.

- This is an inactive admission foundation. Executor orchestration is
separate in #19157. Native restore integrity and provider
stopped-source/account validation must be present before executor
activation. The native prerequisite uses ordinary recovery followed by
attachment. The composed executor proofs are in #19157.
- The source claim is durable. A later controller must recover the same
consumer or keep the hold. It cannot select another source after an
ambiguous launch.
- Admission adds bounded database checks around protected startup calls.
Busy issue locks fail closed; other lock waits have a one-second bound.
- No migration, snapshot format, proactive receipt, recurring worker, or
deployment is included. Full workspace checks and platform coverage run
in CI. These tests do not prove vendor history durability or a hosted
rollout.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning, through Codex with
repository tools and code execution. The runtime does not expose a
context-window size.

## 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 on the current head
- [x] Standard Greptile is 5/5 with no unresolved findings on the
current head
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 12:55:06 -08:00

774 lines
41 KiB
JavaScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { chmodSync, existsSync, 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 ingressRetrySuitePath = "server/src/services/native-runtime/native-ingress-bootstrap-retry.integration.test.ts";
const usageDeliverySuitePath = "server/src/services/native-runtime/native-usage-delivery.integration.test.ts";
const warmCheckpointOwnershipSuitePath = "server/src/services/native-runtime/native-warm-checkpoint-ownership.integration.test.ts";
const pluginWarmOwnerSuitePath = "server/src/services/native-runtime/plugin-runner-task-warm.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";
const restartWaitSuites = ["lease-renewal", "preparation", "checkpoint-cost", "diskless"]
.map(name => `server/src/services/native-runtime/native-runner-restart-${name}.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(ingressRetrySuitePath));
assert.ok(!files.includes(dotRunnerSuitePath));
assert.ok(!files.includes(restartRecoverySuitePath));
assert.ok(!files.includes(usageDeliverySuitePath));
assert.ok(!files.includes(warmCheckpointOwnershipSuitePath));
assert.ok(!files.includes(pluginWarmOwnerSuitePath));
assert.deepEqual([...files, chatSuitePath, nativeRunnerSuitePath, ingressRetrySuitePath, dotRunnerSuitePath, restartRecoverySuitePath, ...restartWaitSuites, usageDeliverySuitePath, warmCheckpointOwnershipSuitePath, pluginWarmOwnerSuitePath, "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.ok(files.includes(warmCheckpointOwnershipSuitePath));
assert.ok(files.includes(pluginWarmOwnerSuitePath));
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",
ingressRetrySuitePath,
dotRunnerSuitePath,
restartRecoverySuitePath,
...restartWaitSuites,
usageDeliverySuitePath,
warmCheckpointOwnershipSuitePath,
pluginWarmOwnerSuitePath,
]);
});
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 lanes separate complete package and native workloads without changing local shards", () => {
for (const [args, envOverrides, packageArgs, native] of [
[["--shard=1/2"], { GITHUB_WORKFLOW: "PR" }, [], false],
[["--shard=2/2"], { GITHUB_WORKFLOW: "PR" }, null, true],
[[], { GITHUB_WORKFLOW: "PR" }, [], true],
[["--shard=2/3"], { GITHUB_WORKFLOW: "PR" }, ["--shard=2/3"], false],
[["--shard=3/3"], { GITHUB_WORKFLOW: "PR" }, ["--shard=3/3"], true],
[["--shard=2/2"], { GITHUB_WORKFLOW: "Release" }, ["--shard=2/2"], false],
[["--shard=2/2"], {}, ["--shard=2/2"], false],
[[], {}, [], false],
]) {
const plan = wrapperPlan(args, envOverrides);
assert.equal(plan.runNativeRunnerGroup, native);
assert.equal(plan.runPackageTests, packageArgs !== null);
const vitest = plan.plannedCommands.find(command => command.args[0] === "exec");
assert.deepEqual(vitest?.args ?? null, packageArgs === null ? null : ["exec", "vitest", "run", ...packageArgs]);
assert.equal(plan.plannedCommands.some(command => command.args.includes("general-server-native-runner")), native);
assert.ok(plan.plannedCommands.some(command => command.args.includes("build:rust")), "both workloads require debug fixtures");
}
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("default chat isolation covers six partitions without dropping native suites", () => {
for (const args of [[], ["--mode", "general"]]) {
for (const workflow of ["PR", "Release", ""]) {
const env = { GITHUB_WORKFLOW: workflow, PAPERCLIP_TEST_DATABASE_URL: "" };
const plan = dryRunJson(args, env);
const full = dryRunJson(["--mode", "general", "--group", "general-server",
"--shard-index", "0", "--shard-count", "1"], env);
assert.deepEqual(plan.chatVitestShards, Array.from({ length: 6 }, (_, index) => ({ index, count: 6 })));
assert.ok(!plan.selectedGeneralServerSuites.includes(chatSuitePath));
assert.equal(new Set(plan.selectedGeneralServerSuites).size, plan.selectedGeneralServerSuites.length);
assert.deepEqual([...plan.selectedGeneralServerSuites, chatSuitePath].sort(), full.selectedGeneralServerSuites.sort());
}
}
});
test("default chat isolation leaves explicit groups and shared databases unchanged", () => {
for (const args of [[], ["--mode", "general"]]) {
const shared = dryRunJson(args, { PAPERCLIP_TEST_DATABASE_URL: "postgresql://shared/test", GITHUB_WORKFLOW: "PR" });
assert.equal(shared.chatVitestShards, null);
assert.equal(shared.selectedGeneralServerSuites, null);
}
const full = dryRunJson(["--mode", "general", "--group", "general-server",
"--shard-index", "0", "--shard-count", "1"], { PAPERCLIP_TEST_DATABASE_URL: "", GITHUB_WORKFLOW: "PR" });
assert.equal(full.chatVitestShards, null);
assert.ok(full.selectedGeneralServerSuites.includes(chatSuitePath));
assert.ok(full.selectedGeneralServerSuites.includes(nativeRunnerSuitePath));
assert.deepEqual(dryRunJson(["--mode", "general", "--group", "general-chat"],
{ PAPERCLIP_TEST_DATABASE_URL: "" }).chatVitestShards, [{ index: 0, count: 1 }]);
assert.equal(dryRunJson(["--mode", "serialized"], { PAPERCLIP_TEST_DATABASE_URL: "" }).chatVitestShards, null);
});
function defaultLauncher({ shared = false, failureIndex = null } = {}) {
const directory = mkdtempSync(path.join(os.tmpdir(), "default-chat-launch-"));
const file = path.join(repoRoot, chatSuitePath);
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 cases = lines.map(line => ({ file, projectName: "@paperclipai/server",
name: `default launcher fixture ${line}`, location: { line } }));
const receipt = path.join(directory, "events.ndjson");
const active = path.join(directory, "active");
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(cases)};
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 {
const entry = { args, selected, home: process.env.PAPERCLIP_HOME,
config: process.env.PAPERCLIP_CONFIG, instance: process.env.PAPERCLIP_INSTANCE_ID,
tmpdir: process.env.TMPDIR };
const record = edge => fs.appendFileSync(${JSON.stringify(receipt)}, JSON.stringify({ ...entry, edge }) + '\\n');
record('begin');
if (fs.existsSync(${JSON.stringify(active)})) process.exit(91);
fs.writeFileSync(${JSON.stringify(active)}, 'running');
setTimeout(() => {
record('end');
fs.unlinkSync(${JSON.stringify(active)});
process.exit(selected.includes(${JSON.stringify(failureIndex === null ? null : lines[failureIndex])}) ? 7 : 0);
}, selected.length ? 200 : 0);
}
`);
chmodSync(fakePnpm, 0o755);
const result = spawnSync(process.execPath, [script, "--mode", "general"], {
cwd: repoRoot, encoding: "utf8", timeout: 30_000,
env: workflowEnv({ GITHUB_WORKFLOW: "PR", PATH: `${directory}${path.delimiter}${process.env.PATH}`,
PAPERCLIP_TEST_DATABASE_URL: shared ? "postgresql://shared/test" : "", PAPERCLIP_CI_PROFILE: "false" }),
});
const events = existsSync(receipt) ? readFileSync(receipt, "utf8").trim().split("\n").map(line => JSON.parse(line)) : [];
return { result, events, lines };
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
test("default chat isolation executes every partition sequentially before other groups", { skip: process.platform === "win32" }, () => {
const { result, events, lines } = defaultLauncher();
assert.equal(result.status, 0, result.stdout + result.stderr);
const chat = events.filter(event => event.selected.length > 0);
assert.deepEqual(chat.map(event => event.edge), Array.from({ length: 6 }, () => ["begin", "end"]).flat());
const starts = chat.filter(event => event.edge === "begin");
assert.deepEqual(starts.flatMap(event => event.selected).sort((a, b) => a - b), lines);
for (const event of starts) {
assert.ok(event.args.includes("--no-file-parallelism"));
assert.deepEqual(event.args.filter(arg => arg.startsWith("--maxWorkers")), ["--maxWorkers=1"]);
}
for (const field of ["home", "config", "instance", "tmpdir"]) {
assert.equal(new Set(starts.map(event => event[field])).size, 6, `${field} must be isolated per child`);
}
assert.equal(events.findIndex(event => event.selected.length === 0), 12);
const server = events[12];
assert.ok(server.args.includes("@paperclipai/server"));
const excluded = server.args.flatMap((arg, index) => arg === "--exclude" ? [server.args[index + 1]] : []);
assert.equal(excluded.filter(file => file === chatSuitePath.replace(/^server\//, "")).length, 1);
for (const file of [nativeRunnerSuitePath, ingressRetrySuitePath, usageDeliverySuitePath, dotRunnerSuitePath, restartRecoverySuitePath,
"server/src/services/native-runtime/plugin-runner-probe.test.ts"]) {
assert.ok(!excluded.includes(file.replace(/^server\//, "")), `default must retain ${file}`);
}
});
test("default chat isolation propagates failure without starting another partition or group", { skip: process.platform === "win32" }, () => {
const { result, events } = defaultLauncher({ failureIndex: 2 });
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stderr, /failed \(7\)/);
assert.equal(events.length, 6);
assert.ok(events.every(event => event.selected.length === 1));
assert.deepEqual(events.map(event => event.edge), ["begin", "end", "begin", "end", "begin", "end"]);
});
test("default chat isolation keeps shared-database execution monolithic and serial", { skip: process.platform === "win32" }, () => {
const { result, events } = defaultLauncher({ shared: true });
assert.equal(result.status, 0, result.stdout + result.stderr);
assert.ok(events.every(event => event.selected.length === 0));
assert.ok(events[0].args.includes("@paperclipai/server"));
assert.ok(events[0].args.includes("--no-file-parallelism"));
assert.deepEqual(events[0].args.filter(arg => arg.startsWith("--maxWorkers")), ["--maxWorkers=1"]);
const excluded = events[0].args.flatMap((arg, index) => arg === "--exclude" ? [events[0].args[index + 1]] : []);
assert.ok(!excluded.includes(chatSuitePath.replace(/^server\//, "")));
assert.ok(!excluded.includes(nativeRunnerSuitePath.replace(/^server\//, "")));
assert.deepEqual(events.map(event => event.edge), Array.from({ length: events.length / 2 }, () => ["begin", "end"]).flat());
});
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 });
}
});
for (const [group, parallelWorkers, shardArgs] of [
["general-server-native-runner", 4, []],
["general-server-without-chat-or-native-runner", 3, ["--shard-index", "0", "--shard-count", "12"]],
]) test(`${group} files overlap only when their database fixtures are isolated`, { skip: process.platform === "win32" }, () => {
const directory = mkdtempSync(path.join(os.tmpdir(), "native-file-workers-"));
const receipt = path.join(directory, "args.json");
const fakePnpm = path.join(directory, "pnpm");
writeFileSync(fakePnpm, `#!/usr/bin/env node\nrequire('node:fs').writeFileSync(${JSON.stringify(receipt)}, JSON.stringify(process.argv.slice(2)));\n`);
chmodSync(fakePnpm, 0o755);
try {
for (const [databaseEnv, workers] of [
[{}, parallelWorkers],
[{ PAPERCLIP_TEST_DATABASE_URL: "postgresql://shared/test" }, 1],
[{ PAPERCLIP_ISSUE_RECOVERY_TEST_DATABASE_URL: "postgresql://shared/recovery_test" }, 1],
[{ PAPERCLIP_TEST_POSTGRES_ADMIN_URL: "postgresql://owned/postgres" }, parallelWorkers],
]) {
const env = workflowEnv({ PATH: `${directory}${path.delimiter}${process.env.PATH}`, PAPERCLIP_CI_PROFILE: "false" });
for (const key of Object.keys(env)) if (key.endsWith("_TEST_DATABASE_URL")) delete env[key];
Object.assign(env, databaseEnv);
const result = spawnSync(process.execPath, [script, "--mode", "general", "--group", group, ...shardArgs], {
cwd: repoRoot, encoding: "utf8", env,
});
assert.equal(result.status, 0, result.stdout + result.stderr);
const args = JSON.parse(readFileSync(receipt, "utf8"));
assert.ok(args.includes(`--maxWorkers=${workers}`));
assert.ok(args.includes(workers > 1 ? "--fileParallelism" : "--no-file-parallelism"));
if (workers > 1) assert.ok(args.includes("--pool=forks") && args.includes("--isolate"));
if (group === "general-server-native-runner") {
for (const file of [restartRecoverySuitePath, ...restartWaitSuites]) assert.ok(args.includes(file));
}
}
} finally { rmSync(directory, { recursive: true, force: true }); }
});