perf(ci): run qualified unit tests with native Bun (#159988)

* perf(ci): run qualified unit tests with native Bun

* test(ci): keep a V8-only cache warm fixture

* fix(ci): include shared timeout in PR wrapper inventory

* perf(test): skip Bun bytecode cache during test teardown

* test: handle worker fixture retirement notifications

Handle rejected observer returns without delaying retirement, matching the
worker pool contract, and avoid shadowing the lane fixture input helper.

* perf(ci): qualify and tune Bun test workers

Complete SQLite admission before Bun Vitest threads start and retain each
worker generation's canonical cleanup decision in lifecycle fixtures.
Keep proxy-agent fixture values out of Bun's native fetch cache and compare
large socket payloads with full byte equality.

Renew the 292-file native qualification from paired 2,518-case proof.
Apply the existing UI JIT and allocator settings to ordinary unit-fast
Bun/Vitest selections and their cache warmer, preserving Node and native
Bun settings and dual-runtime release coverage.

Proof: 208 routing/warming cases; 61 lifecycle cases on each runtime;
66 ordered proxy/fetch cases plus the canonical 11-case proxy owner on each
runtime; independent P0-P2 review. The changed-file gate was interrupted
by the local disk reserve guard after core typing and 14 core-test type
shards passed. Tuned full-cohort performance remains to be measured;
the completed untuned comparison was 4.60% slower than Node.

* perf(test): expand native Bun coverage and reduce artifact IO

* perf(test): yield between artifact verification batches

* test: qualify native Bun cases after runtime integration

* test: refresh native Bun qualification after main integration
This commit is contained in:
Peter Steinberger 2026-10-01 17:28:11 -07:00 • committed by GitHub
parent 45762faaa8
commit 9137cfcc5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 2248 additions and 667 deletions

View file

@ -133,9 +133,11 @@ Transform keys also include each project's dependency optimizer directory. This
prevents cached UI imports from mixing separate projects' Lit instances when a
focused run and a full run share the persistent cache.
Linux PR tests use Bun for the measured compatible unit lanes and Control UI
Vitest job. Full Release Validation keeps their Node coverage and runs them on Bun
too; see [test runtime selection](/ci/pipeline#test-runtime-selection).
Linux PR tests use Bun for compatible unit lanes and Control UI Vitest selections.
Audited synchronous unit-fast tests can use Bun's native runner; changed test or
setup bytes return to Vitest. Full Release Validation retains complete Node
coverage plus qualified Bun coverage; see
[test runtime selection](/ci/pipeline#test-runtime-selection).
Both runtimes group uncached, non-isolated UI files by environment in batches
to reduce worker restarts while retaining native shard ownership and worker budgets.

View file

@ -127,9 +127,41 @@ future-attempt artifacts remain invalid.
### Test runtime selection
Linux test shards select Bun through `scripts/lib/ci-test-runtime.mts`. The
ordinary and isolated unit-fast lanes partition their existing file inventories: files with known Bun
failures or additional skips stay on Node, and the compatible remainder runs on
Bun. Those Node files still execute; they are not excluded from CI.
ordinary unit-fast lane partitions its existing file inventory: files with known
Bun failures or additional skips stay on Node, and the compatible remainder runs
on Bun. Those Node files still execute; they are not excluded from CI. The
isolated unit-fast lane runs completely on Bun.
Bun test processes disable the Node-compatible bytecode cache because the pinned
fork generates bytecode during Vitest teardown. Bun's transpiler cache and the
Vitest transform cache remain available. Node orchestration, worker compilation,
and Node test selections retain their existing cache settings.
Bun's Vitest parent completes the canonical SQLite native-close admission before
creating test threads. Each worker inherits that decision, allowing capable
runtimes to reuse readers while negative probes retain conservative cleanup.
Audited ordinary unit-fast tests use Bun's native test runner, including
qualified async callbacks and self-contained zero-argument setup hooks. Hooks
that take a Vitest context retain Vitest: Bun interprets a hook parameter as a
completion callback. Tests that register a pending promise
assertion before settling it retain Vitest: native Bun waits inside that matcher.
The same runtime owner intersects their qualification data with the
canonical inventory and each existing stripe's include patterns. The remaining
compatible files keep Vitest on Bun. Native admission checks test, setup, and
fixture-helper bytes; changed or unreadable inputs return the affected tests to
Vitest without dropping coverage. Production sources remain free to change and
are still exercised. Extra Vitest arguments, including cache-warming collection,
retain the existing Vitest path.
Native Bun receives explicit file paths, the existing hermetic environment setup,
the repository tsconfig, and the shared test deadline. It disables automatic env
file loading and runs one test process inside the existing plan and worker budget.
The shared process owner retains output watchdogs, interruption handling, and
joined descendant cleanup. Release validation runs the complete Node selection
before the compatible Bun/Vitest and native Bun portions in that same slot;
every result remains required. Native execution adds no CI jobs or worker fanout.
The process lane runs `terminal-pty-bun.test.ts` on Bun and retains its other
files on Node. The pinned fork supports `Bun.Terminal.pause()` and `resume()`,
so its native real-PTY cases run on Linux. macOS and Linux select the native PTY
@ -141,16 +173,15 @@ The qualified TypeScript compiler analysis files and `src/library.test.ts` run
on Bun. The pinned fork exposes the child-process pipe handles and stream
reference controls used by TypeScript's synchronous native API. Compiler
assertions in mixed runtime suites remain enabled.
The Node Code Mode executor suite also stays on Node: its warm-worker cleanup
requires diagnostics-channel delivery to preserve sibling subscribers when a
callback unsubscribes during publication. Bun can skip the next subscriber.
The plugin runtime retention proof (`src/plugins/runtime.retention.test.ts`) also
stays on Node: JavaScriptCore can keep a released successor registry alive past
forced collections without any retaining path, so its WeakRef checks are
nondeterministic on Bun.
The complete fake-timer lane also supports Bun. Control UI keeps its GC-sensitive
retention proofs (`chat-pane-retained-presentation.test.ts`, `chat-thread.test.ts`,
and `usage-page-details.test.ts`) on Node and runs the remaining files on Bun.
The Code Mode executor runs with Vitest on Bun using the fork's
copy-on-write diagnostics-channel subscriber handling. Markdown render-aware
chunking stays on Node because the pinned WebKit lacks the `Intl.Segmenter`
surrogate-boundary fix needed by that suite.
The complete fake-timer lane, plugin and proxy retention tests, and Control UI
support Bun. UI retains its six GC assertions in `chat-pane-retention.test.ts`,
`chat-thread-retention.test.ts`, and `usage-page-retention.test.ts`; they use
runtime-neutral collection and WeakRef checks. V8-specific heap and worker-limit
assertions and the remaining qualified Node-only selections still run on Node.
The missing-Docker test also runs on Bun, using an empty executable directory
instead of an empty `PATH`, which Bun resolves through its default search path.
Other families retain Node until they pass on the pinned fork within their
@ -176,7 +207,8 @@ the same module identity on both runtimes.
The complete memory plugin config (`memory-lancedb` and `memory-wiki`) also
supports Bun, with its existing isolated workers and database-worker exclusions.
A paired Linux Testbox comparison with two workers passed the same 53 files,
A paired Linux Testbox comparison on the preceding Bun `ddfce5d01` / WebKit
`4429d113` build with two workers passed the same 53 files,
474 tests, and one platform skip on each runtime. Bun reduced complete test-command
wall time from 58.72s to 52.79s cold and from 43.51s to 38.68s with warm caches
and reversed runtime order: 10–11% faster, with warm aggregate RSS near 3.94 GiB
@ -199,12 +231,10 @@ Current-runner targets use three native shards and three workers per row,
including exact-target Full Release Validation dispatches. Historical
compatibility targets retain their unsharded package command.
The UI runtime partition is applied after Vitest selects each native shard, so
files keep their original shard ownership. Compatible PR selections run Bun
first and record Vitest's original shard inventory. After successful, joined
completion, a shard with no Node-only files omits that Node process. Missing or
invalid inventory evidence retains the Node run. Dual validation runs
the complete UI selection on Node, then excludes only the usage detail file from Bun;
Its assertions remain required on Node, with no added skips.
files keep their original shard ownership. Compatible PR selections run the
complete shard on Bun. Dual validation runs the complete UI selection on Node
and then on Bun, including the six retention assertions. Partial runtime
partitions still require the original shard inventory before omitting Node work.
Partitions without browser files retain browser discovery for native sharding
but omit Chromium version probing and Playwright's speculative browser startup.
@ -248,15 +278,20 @@ function ownership, shared-environment deletion, and a module-key crash.
The shared provider-catalog retention test is qualified on this build and runs
on Bun; tests that assert V8 heap behavior continue to run on Node.
The fork keeps the lifecycle-script `node` shim in a per-user directory, with a
private fallback when that directory is unusable. This lets several accounts on
one host run Bun installs without Node.
private fallback when that directory is unusable. `NODE` and `npm_node_execpath`
point to the executable in either location. This lets several accounts on one
host run Bun installs without Node. The same build retains the tsconfig,
diagnostics-channel, and inspector/GC fixes. The render-aware chunking exception
above preserves coverage for the missing WebKit segmentation fix.
Its prerelease tag includes both source revisions because `Bun.revision` alone
does not distinguish builds linked against different WebKit revisions.
Node continues to own orchestration, builds, compiler preparation, and cleanup;
Vitest and its workers use the selected runtime. Bun and Node have separate
transform-cache directories and timing identities. Either runtime failing fails
the job. This adds no matrix rows or runner registrations.
Node continues to own orchestration, builds, compiler preparation, and cleanup.
Vitest and its workers use the selected runtime; qualified native selections use
`bun test` and skip Vitest compilation and transform caching. Bun/Vitest, native
Bun, and Node have separate timing identities, and the two Vitest runtimes keep
separate transform-cache directories. Any test engine failing fails the job.
This adds no matrix rows or runner registrations.
`NODE_OPTIONS`, where configured, limits Node heaps; the UI lane retains Node's
default heap limit. Bun does not use that V8 limit. Compare observed memory use alongside elapsed time before admitting more

View file

@ -99,9 +99,9 @@ For targets with [test runtime selection](/ci/pipeline#test-runtime-selection),
Bun-compatible selection on Bun. Both results are required; they share existing
jobs and execute sequentially within each worker slot. Older targets without this
capability retain Node-only testing.
This includes the Control UI config when the target's runtime owner admits it;
its Bun pass excludes two GC-sensitive files retained in the full Node pass.
An older unit-only runtime owner retains the UI's Node pass.
This includes the complete Control UI selection on both runtimes, including the
retention assertions, when the target's runtime owner admits it. Older targets
retain the UI's Node pass and only the Bun selections their runtime owner admits.
Package Acceptance separately retains expanded published-upgrade scenarios:
current unpublished candidates include native operator state, and stable/full

View file

@ -107,8 +107,7 @@ actual Vitest process and workers while retaining Node for orchestration and
compiler preparation. It does not use Bun's native test runner. `bun run` alone
does not select Bun for tests. Node remains the local default.
For the CI Control UI comparison, run the full Node selection followed by its
compatible Bun partition:
For the CI Control UI comparison, run the full selection on Node followed by Bun:
```sh
OPENCLAW_NODE_TEST_CONFIGS_JSON='["ui/vitest.config.ts"]' \
@ -117,9 +116,9 @@ OPENCLAW_CI_TEST_RUNTIME_POLICY=dual \
node --import tsx scripts/ci-run-node-test-shard.mts
```
The Bun partition deliberately excludes two whole GC-sensitive files, which
remain covered by Node. Running the complete UI config directly with
`OPENCLAW_VITEST_RUNTIME=bun` also runs those currently incompatible assertions.
Both passes include the retention assertions, which use runtime-neutral garbage
collection and WeakRef checks. Run the complete UI selection only on Bun with
`OPENCLAW_VITEST_RUNTIME=bun`.
Test processes and their CLI fixtures keep Sparkplug baseline compilation enabled
but run it synchronously. This avoids a Node 24 shutdown deadlock where a

View file

@ -366,6 +366,17 @@ export function resolveShardChildCommand(
testProjectsEntrypoint = resolveTestProjectsEntrypoint(),
workerRun?: VitestWorkerRun,
) {
if (args[0] === "--native-bun") {
return {
command: nodeExecPath,
args: [
"--import",
"tsx",
fileURLToPath(new URL("./run-vitest.mts", import.meta.url)),
...args,
],
};
}
const loaderArgs = testProjectsEntrypoint.endsWith(".mts") ? ["--import", "tsx"] : [];
return {
command: nodeExecPath,
@ -441,6 +452,10 @@ async function runChild(
timingKey: string,
context?: Awaited<ReturnType<typeof createWorkerContext>>,
) {
const spawnEnv =
args[0] === "--native-bun" && context
? { ...childEnv, OPENCLAW_NATIVE_BUN_PARENT_IPC: "1" }
: childEnv;
// Use Node directly. `pnpm exec node` may reconcile the workspace before
// tests, which destroys the sticky dependency fast path.
const childCommand = resolveShardChildCommand(
@ -457,7 +472,7 @@ async function runChild(
command: childCommand.command,
args: childCommand.args,
options: {
env: childEnv,
env: spawnEnv,
stdio: ["ignore", "pipe", "pipe", "ipc"],
},
homeMode: "tooling",
@ -481,7 +496,7 @@ async function runChild(
);
} else {
child = spawn(childCommand.command, childCommand.args, {
env: childEnv,
env: spawnEnv,
stdio: ["ignore", "pipe", "pipe"],
});
completion = new Promise<number>((resolve) => {
@ -810,6 +825,7 @@ export async function runShardPlans(plans: ShardPlan[], options: RunShardOptions
return;
}
const runtime = selection.runtime;
const nativeBun = selection.engine === "bun-test";
const nativeFiles = nativeShardFiles;
if (
runtime === "node" &&
@ -835,13 +851,23 @@ export async function runShardPlans(plans: ShardPlan[], options: RunShardOptions
: entry;
const selectedArgs =
selectedEntry.kind === "target" ? [selectedEntry.target] : selectedEntry.plan.configs;
const args =
vitestExtraArgs.length > 0 ? [...selectedArgs, "--", ...vitestExtraArgs] : selectedArgs;
const childEnv = buildChildEnv(selectedEntry, baseEnv, scratchDir, index, {
serial: concurrency === 1,
cacheSlot,
runtime,
});
const args = nativeBun
? ["--native-bun", ...selection.files.map((file) => `./${file}`)]
: vitestExtraArgs.length > 0
? [...selectedArgs, "--", ...vitestExtraArgs]
: selectedArgs;
const childEnv = nativeBun
? prepareChildEnv(entry, { ...baseEnv, OPENCLAW_VITEST_RUNTIME: "bun" })
: buildChildEnv(selectedEntry, baseEnv, scratchDir, index, {
serial: concurrency === 1,
cacheSlot,
runtime,
});
if (nativeBun) {
delete childEnv[FS_MODULE_CACHE_ROOT_ENV_KEY];
delete childEnv[FS_MODULE_CACHE_PATH_ENV_KEY];
delete childEnv.OPENCLAW_VITEST_INCLUDE_FILE;
}
if (selection.includeAfterShard) {
childEnv.OPENCLAW_VITEST_POST_SHARD_INCLUDE_FILE =
childEnv.OPENCLAW_VITEST_INCLUDE_FILE;
@ -863,8 +889,9 @@ export async function runShardPlans(plans: ShardPlan[], options: RunShardOptions
childEnv.OPENCLAW_VITEST_NATIVE_SHARD_REQUEST_ID = uiReceipt.requestId;
}
const timingKey = entry.kind === "group" ? (entry.timingKey ?? entry.name) : entry.name;
const timingPrefix =
runtime === "bun"
const timingPrefix = nativeBun
? "bun-native:"
: runtime === "bun"
? "bun:"
: selection.configs || selection.includePatterns
? "node-subset:"

View file

@ -4501,6 +4501,7 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
"ui/src/pages/chat/chat-pane-placement.test.ts",
"ui/src/pages/chat/chat-pane-progress-history.test.ts",
"ui/src/pages/chat/chat-pane-retained-presentation.test.ts",
"ui/src/pages/chat/chat-pane-retention.test.ts",
"ui/src/pages/chat/chat-pane-run-activity.test.ts",
"ui/src/pages/chat/chat-pane-session-access.test.ts",
"ui/src/pages/chat/chat-pane-session-controls.test.ts",
@ -4527,6 +4528,7 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
"ui/src/pages/chat/chat-submit-handoff.test.ts",
"ui/src/pages/chat/chat-talk-reconciliation.browser.test.ts",
"ui/src/pages/chat/chat-thread-live-final.test.ts",
"ui/src/pages/chat/chat-thread-retention.test.ts",
"ui/src/pages/chat/chat-thread.test.ts",
"ui/src/pages/chat/chat-view.attachment-paste.test.ts",
"ui/src/pages/chat/chat-view.test.ts",
@ -4605,6 +4607,7 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
"ui/src/pages/sessions/sessions-page.typing.test.ts",
"ui/src/pages/sessions/view.test.ts",
"ui/src/pages/usage/metrics.node.test.ts",
"ui/src/pages/usage/usage-page-retention.test.ts",
"ui/src/pages/workboard/workboard.e2e.test.ts",
"ui/src/plugins/control-ui-runtime.test.ts",
"ui/src/styles/base-theme-tokens.node.test.ts",

View file

@ -0,0 +1,461 @@
{
"setup": {
"src/infra/supervisor-markers.ts": "bbdc4260a2bd637b8fac2e13e76d1ecd7747b7bcd5ed90410249d09ea4b665b3",
"src/test-utils/bun-process.ts": "77220fb93b0d204b4d645065d699dd289d13e3b18deb54f9f942c7a801c790bb",
"src/test-utils/env.ts": "8e9010865a1119c0e13c4d7ac7df72dc71329e5f5ce6a9ee7363bfedc37e9531",
"test/setup.env.ts": "aa96fede1449f3ffb524ecebef5d0eb16d89f4db3f1a17e7f2c63aa0b432041e",
"test/test-env.ts": "aa52b5d73162d321512bb264e0673a440703f28f43a5627859fb11f434ee10ef",
"test/test-home-context.mts": "ab02ed0c3e96d2adbaae760779ae6d930e0d8325b125203c943cc647cafae329",
"test/test-home-policy.mts": "09d6a515889850eb8c6589c14f8b3d37c117370dd376809fea7cbae1b9a456ce",
"test/vitest/vitest.shared.config.ts": "27a05026dc0fd71d3815c95c0f3135443b450a2ac6d63f8e74d507961c0232d4",
"test/vitest/vitest.timeouts.ts": "82465558c77ce00d1e892a4ba06be6df5d4e449eecffb5383d1c45ce8ad7d073",
"test/vitest/vitest.unit-fast.config.ts": "c375573bd6c01fc691c8890933356e37fd20a45561dbe772c140ed86c76dc4f6",
"tsconfig.json": "37ff1b9ebd37523754e0060f132e078f79ed07aaaf1ac201eff5e8696f7dcc50"
},
"tests": {
"packages/markdown-core/src/chunk-text.test.ts": "07c00c11cfbfe30e1a9920571bfafabf1a7ea3f48ae37fd3e116aebd923c982b",
"packages/markdown-core/src/construct-fallbacks.test.ts": "e552111a938587812885504df9560f2eea1a598d4d63a2e8a77b5e8aa41b38f8",
"packages/markdown-core/src/fences.test.ts": "d582c290def5234f56568f95ac70c99f95d5f6580b327825e5a78375040a8cf8",
"packages/markdown-core/src/html-tags.test.ts": "340ef2c90def17a50577f9cecb4248f906b8762bffb44ca226160f1ffa14a678",
"packages/markdown-core/src/image-spans.test.ts": "63b1f56b6ed5d63897672153e052cd2f21574a741bafdfd19b9bd5fa40275a71",
"packages/markdown-core/src/ir.block-metadata.test.ts": "d9ec13ce8d6793a3eb0e6bb961c7bfe266c8268801e8ecebf9f520afa82b94df",
"packages/markdown-core/src/ir.blockquote-spacing.test.ts": "f7d12c2b1f32de68fb2bf2defaa15c0c02d4fc3f3a580804b50504fc3ae259fc",
"packages/markdown-core/src/ir.chunking.test.ts": "956ceee10ca08f204ab625d5511cf2dab28c93e1075f423443c6db8a4f875a38",
"packages/markdown-core/src/ir.dunder-identifiers.test.ts": "b11517befb989f52f6e3e4f558916672441f1306358d45b9a23ef021fc11d751",
"packages/markdown-core/src/ir.emphasis-cjk.test.ts": "887326af4e9cd34cf89c865fcd80dfc8d089382293b4624cf111100479a01310",
"packages/markdown-core/src/ir.fenced-content.test.ts": "9940d6edd6ec219a385641329cfa7940b93dd98e613e0283bd4ad74c96bd60bc",
"packages/markdown-core/src/ir.hr-spacing.test.ts": "655847d741d9659ed83d2e8f21bb6c342e30a25e0f4cf621d98f81b661d20229",
"packages/markdown-core/src/ir.links.test.ts": "25f622a010ee3e4fa9675de1808672f20ffe4ba10979d1ac2f0ddf66f90aeb8a",
"packages/markdown-core/src/ir.nested-lists.test.ts": "513d3dcd710c5f313083a8e4cf06d19e7e92ea3ebfc01a00a2db5f7d5dc84cc1",
"packages/markdown-core/src/ir.raw-html.test.ts": "b38947fe2682c8780d129f42e34f65470ea3602f8f87725000499cc502d8c77f",
"packages/markdown-core/src/ir.table-block.test.ts": "81a19462327441a9d125794283126e46c251fbc52c119714ecbd79a33aea3df1",
"packages/markdown-core/src/ir.test.ts": "12379e51f55984d4dd0059036f69d1e83ca62d75533d679aed3ceddd70a787b4",
"packages/markdown-core/src/render.annotations.test.ts": "0648c358209131bee225405da96963488d209176e2fc341d68a517fc6cdddea3",
"packages/markdown-core/src/render.crossing.test.ts": "2111d04da3cf0eb9dc6f3c0dc72dc11d507275c95a63658c96a5afb88d2237c8",
"packages/media-core/src/content-length.test.ts": "2e2df6447fd11e0a8e85741773ebf06b4f81ee1c194dc971a85d05f4d59ac23c",
"packages/media-core/src/file-name.test.ts": "0908c0a54e8ab077f77d9023842990eb10d57433708468bbb013a2c30124fd14",
"packages/media-core/src/inbound-path-policy.test.ts": "713676ac0164d0aebdab22130f28ce30c8889141d38e9b1843ba5b36a2d84492",
"packages/media-core/src/inline-image-data-url.test.ts": "ce40173f9720218edeadd6290860145c5c8e72483e1bc531e220940824ce6b7c",
"packages/memory-host-sdk/src/host/batch-status.test.ts": "5dacc6d632e6bdfa007d59a7b96f5e9ca540c429de28417aacb92be3f5b099f3",
"packages/memory-host-sdk/src/host/curated-annotations.test.ts": "8c1e39d2a8c957250268b148af5326f4b8ade967462d975a16e6b664f6ad51b8",
"packages/memory-host-sdk/src/host/embedding-provider-adapter-utils.test.ts": "5eec297ca446b2c5b42fd8a54b243dad14380a4ef608629655075abe13ccc793",
"packages/memory-host-sdk/src/host/embedding-vector.test.ts": "8cad9de369be65deaede293e2410a2e764ff2b6a15962d9b60b442bec3786f45",
"packages/memory-host-sdk/src/host/embeddings-model-normalize.test.ts": "d32840cfd884e34aedb002eed8fe0e9de9d5778ea2c9864d52269512dbe24a4d",
"packages/memory-host-sdk/src/host/query-expansion.test.ts": "a9409fd36eea133c8ceb3a7a8d1b58c6754894f4796471c77a33da424ca44f27",
"packages/memory-host-sdk/src/host/read-file-shared.test.ts": "8439df64d3005fad90bfa36a6034613ec323fe3c30925d2d4eebeadb8df59c26",
"packages/memory-host-sdk/src/host/session-reset-recall.test.ts": "f6029d20d60a79326c40c75cb1a69edadf6fe43e4d88c606e9aa1df074e0d188",
"packages/memory-host-sdk/src/host/types.test.ts": "16d0a755584d62dfc98499850a6781613a7c56aad3ca408c298e0b170e268506",
"packages/plugin-package-contract/src/index.test.ts": "bc9707584c2f07c882913e2860407607be47ee81033519ab731b5f2acd4668c7",
"packages/terminal-core/src/decorative-emoji.test.ts": "1761aaa38f07bab889993e4e021acb79d2283d1bc668bf49c076786c2976877b",
"packages/terminal-core/src/osc-progress.test.ts": "e68866eec5b2f560158c12c38f0f0cd630cbd94a3377c89e1e716006eb3a35d2",
"packages/terminal-core/src/safe-text.test.ts": "9be7d056fe4202bdea7b45bcd681511d37f09d26daeff9de739577b4565ac6b3",
"src/acp/approval-classifier.test.ts": "79e8e3ab8b0673bb35b6492399331dff6d08a96cf4c186ce70dcdeca39fd03e9",
"src/acp/commands.test.ts": "a67a51e22c666aa7eadde4ef188a29caefafc171be5630e848be8f32cced4c6c",
"src/acp/event-mapper.test.ts": "8f499f9126b2206063546704c05fb85292f75fbd2187cf109136db9eb76946fe",
"src/acp/session-new-ordering.test.ts": "683b59bbec66b29bf67a99bcc31a2e2cae6075ba5ca1c1806c981c6e5a8213c9",
"src/acp/translator.replay.test.ts": "89d37c13cf4413b96266339badbd2757c65dd19715d468b7d3ccab4ba3461934",
"src/acp/translator.session-list.test.ts": "f23294d11be0ee2c86fa3aac70e7d75e36f13b647f786e913102d1cb63d1c356",
"src/agents/agent-bundle-mcp-names.test.ts": "6ba8029617e48656b5dd59aa9473d5041c9cb2dfea3e688995cf79559018e5cd",
"src/agents/agent-command-admission-facts.test.ts": "81dbeaeaff7d55df63ad9619a3482e1534fa21a205f21f2a3e1890f6e2a7e94a",
"src/agents/agent-hooks/compaction-instructions.test.ts": "b8956e4ec58097f82a8dcb75c96d517ec3c67dd9d354de1912fb4b86477db565",
"src/agents/agent-hooks/compaction-safeguard-quality.test.ts": "79f953eeecd4f464cf6b88a0c32a1be2c525a9a2a1e6c7d54272c83974255cad",
"src/agents/agent-lifecycle-parent-state.test.ts": "4a46dbedc803c7d2be075094f60e8def72f547f1b9aeddfdb8166f6b6a1fd490",
"src/agents/auth-profiles.getsoonestcooldownexpiry.test.ts": "41fac6505bf3f3785665959c824d51ed5728f82a736bd7e2fcea33f845606f9b",
"src/agents/auth-profiles/credential-state.test.ts": "35bb1fa98bfaca3714353a13c849608a992b664e39fb67e6cc8f7b8b8cfd96dd",
"src/agents/auth-profiles/display.test.ts": "b563da5ec81a731e2f98198a4a26ac34d2f25e8326c1bc236b68f4536c5dddf4",
"src/agents/auth-profiles/failure-copy.test.ts": "0e95bba185b2f5f570b4d77e09093e576aa27279e1c11d4241f84f0253350ef3",
"src/agents/auth-profiles/oauth-refresh-timeout.test.ts": "4dbbaec4215a33b765ac86bb50bc1004eeee7c2326b37661dab4ec209b40f435",
"src/agents/auth-profiles/portability.test.ts": "2c24312f5882ac0ae393763046ff2511cd2dd26a3e71a7df922e726e48c558e2",
"src/agents/bash-process-scope.test.ts": "a07d7db5716eedbd4a9a1610ab5df62ee0796c4dbf6598323ab5e3417a8dd9f8",
"src/agents/bootstrap-mode.test.ts": "ba7d8399d3d12e20f371f63f605da629441dc917952902f69aa3ad325aa2ce2f",
"src/agents/channel-tool-metadata.test.ts": "619c690d72703836fc4b4392645ec2c99cb5f06b1163b90db0d38d26618b3f49",
"src/agents/cli-runner/execute-output-buffer.test.ts": "efec535746f1b0ee47bb2472ee087ff20e34dcfbc3891751a4199de6d866e60e",
"src/agents/cli-runner/reseed-envelope.test.ts": "0961018082d60eb414b38bb243ca6fedb5655f0ea02e800302e2e8016cec00e5",
"src/agents/cli-runner/tool-policy.test.ts": "d2266fb2ee4d12aee5b27e0b661f4b4461a7a19c1f2abc66b83300b64ed26f83",
"src/agents/code-mode-node-progress.test.ts": "2938f786c785d00a56a6d424e4ffa5efa732f098265892d83d06fb799d6aa5e8",
"src/agents/command-poll-backoff.test.ts": "8164c270ae43e3788ca9ade9e5c9946c92cfa734bd2ffd428c05c8edbb7b6f78",
"src/agents/command/attempt-callbacks.test.ts": "251caabb0060ffef7cef64ef70e71e91927230145a2d5d42befedbdf5e5c7907",
"src/agents/configured-provider-fallback.test.ts": "9f36557b74ea720e084890fbcf0c55c606ad4db40e024c96f81b7d00d0c42201",
"src/agents/console-sanitize.test.ts": "f233d57f86e3cc88797a6fac23633d797323a95922d5528bab73bc6f19b089a3",
"src/agents/content-blocks.test.ts": "2190553e82c6e0ad264c4f5e3b60270171f4f32d1e3060291cd49ea9a71caf89",
"src/agents/credential-safety-prompt.test.ts": "ffd19b53e0cfc02b70b16b35fa909f8e6d6d76d606abbd4f12352d073c651215",
"src/agents/deepseek-text-filter.test.ts": "95eb4dba67e36228ffce1480c7028d1c635a362c73c30d5034df368d1bb68df9",
"src/agents/delivery-evidence-values.test.ts": "a010b7ca4fed805cd5a9b6bba8d5741e098c138f23ac02e3a554025d27a8ddc7",
"src/agents/embedded-agent-helpers/image-errors.test.ts": "3ab19c95b72b0127f9f3d381f61d0df7f88d8c7e008e5177b906cf76a68aca94",
"src/agents/embedded-agent-message-delivery.test.ts": "ff271f79517dfb8cfe2e2ae862395facc256a78e39f9b1f7fea001a1c082662f",
"src/agents/embedded-agent-runner/compaction-duplicate-user-messages.test.ts": "08a6a0eb92f9ee24d1a736d9424f1e7f2bae225f321597c3daca13e9ad1216bc",
"src/agents/embedded-agent-runner/failure-signal.test.ts": "7ad56835769b133807e258b1d96267e1fe894c854ca4ed6c35364a5d303eadb3",
"src/agents/embedded-agent-runner/prompt-cache-observability.test.ts": "88ccd7da80b0907fa9d7d9abeadadae045e1f50e4a33f84504841a18f3b4fe65",
"src/agents/embedded-agent-runner/run.lane-timeout.test.ts": "6ce78843bea5c61f1a658c295084d061b6749234ffd06006b9ce8667c710c5af",
"src/agents/embedded-agent-runner/run/attempt.context-engine-helpers.test.ts": "376a1fbb57be5872c3bc8f2564bbe2e7c82d507ae7d845a06375eda5e76594f0",
"src/agents/embedded-agent-runner/run/attempt.user-transcript-context-registry.test.ts": "80f92c1c7596d5c529626310917ec3f75eba9e0f2fdf7e89eb7b4c523a05cc85",
"src/agents/embedded-agent-runner/run/auth-profile-failure-policy.test.ts": "e6101a6e7754b27ea2c4e78bfd41e6b196b7d8bb557a9d9494a2dcb31393d3c8",
"src/agents/embedded-agent-runner/run/auth-store.test.ts": "af3fba29515a4dd32c798ab6a86a22db1c2f035c496d4e113dd89f987ec9c119",
"src/agents/embedded-agent-runner/run/compaction-live-model-selection.test.ts": "5f0ae687153c910f1fbd761546cab1fbe0a11ba8862955b440b6bc4bafc749e2",
"src/agents/embedded-agent-runner/run/compaction-timeout.test.ts": "8ae082ba60420a052b78c096eb4b3e440eeaddc8b01c922c4b0295ad175af59c",
"src/agents/embedded-agent-runner/run/failure-suspension.test.ts": "54d69e8c07eb8afcbb33e63b274967d794eeccc636e20bc79df156106706fc68",
"src/agents/embedded-agent-runner/run/idle-timeout-breaker.test.ts": "c6201856e015333da3e8498c043267ccbb7803fb4cd936a14e6bf3f43b8c6f09",
"src/agents/embedded-agent-runner/run/prepared-runtime-context.test.ts": "00d829450db4e225e1edf3b0aeb09227aa43a7ae27f2149cbc03af92f1f3bb36",
"src/agents/embedded-agent-runner/run/retry-budget.test.ts": "5b1c5a929f1368fd2460f695e11eb60680135ca71a8142370cc963a35a932c14",
"src/agents/embedded-agent-runner/run/turn-taint-state.test.ts": "952d1551a74f4961edfdede9a8a8e600961814b5329010781ed6f2cb3099c91b",
"src/agents/embedded-agent-runner/tool-call-argument-decoding.test.ts": "8f0de1e0c3957986153db9f1c452eceffa75602cc222f1ff77bb9ecfa8aaee45",
"src/agents/embedded-agent-runner/tool-result-text-budget.test.ts": "6c8b69ceaee9a62be05eff2379290ea90c7273fcd04c6457ee36f76abe2af21b",
"src/agents/embedded-agent-runner/usage-accumulator.test.ts": "4bda042c29435dee0ad85cb26b33ff1cf2bc6b27eb048de6e0e206e2688ba395",
"src/agents/exec-approval-result.test.ts": "6a9ee26f0249bc705f2cb3f5f4b506df8759278ebf127b124ce4661be3d06a1b",
"src/agents/failover-policy.test.ts": "5e695a09f9aa53e494537cccee78bca1819d64c68ed44983a7c7b89ea243928a",
"src/agents/failover/signal.test.ts": "cdf4814fbed612c46bf6bff8c4bc3bb6149b6d56e5b377175f57f7f4c5034590",
"src/agents/harness/tool-surface.test.ts": "2f9fd64b9bb04960268e34dfa8181e2dc642dd46866600d0a3a62d9a197af745",
"src/agents/harness/transcript-visibility.test.ts": "59a81d1f55f61efa1244c77a3a786333247da70c1348b27cf0bf3b6f94adf0ac",
"src/agents/harness/workspace-instructions.test.ts": "38814f6b9acda9635d0454e2f766d929c086b782e7c92b69e9bf802b5ef28b11",
"src/agents/hook-system-context-boundary.test.ts": "681cc5c2c34778156dcd03394b505efdb8c1f71768ca540460bb42e4ae3188a7",
"src/agents/identity.human-delay.test.ts": "45a6137d9dfda55f239eb67ef038cd040643560f83635975df4be7a479ebdbd9",
"src/agents/image-sanitization.test.ts": "e52c9508bb900a8e029f0a1d7f28bd1db764c261afb7a95e985c9971878104ee",
"src/agents/lanes.test.ts": "8f3e77b4ff4f5dc3608242c32c19b39de73159cd5833a82cc1bacb4771848726",
"src/agents/live-model-errors.test.ts": "85f6efe77c8524fef8f05beb372d956c4dbf8478038402d4cfcaaf4beb642624",
"src/agents/live-model-session-id.test.ts": "ef2c9691df15b474bc7c79e268e07b0f0f5ac416027b31e40f056ba4e57b47dd",
"src/agents/live-model-turn-probes.test.ts": "b7e434209ba2aeff9c7c6659dc341a23475f363de5848f64d5310ac001486878",
"src/agents/mcp-app-model-context.test.ts": "742e9c2c4309dddb5e8fcbd9fa0df171b4a9e7171fb4f742cb2098071ac61076",
"src/agents/mcp-codex-tool-approval.test.ts": "23fc891a9a5b5d9999e230a8b9e05108670624bc14783709e8fd31f156d40390",
"src/agents/mcp-stdio-frame-decoder.test.ts": "7adcaa25b669604055fe05854dab920ebb866e0521bc9ce1cc23213f62a91178",
"src/agents/mcp-tool-filter.test.ts": "ed79a366a256243ad0734d6e526a609291d61f568eec3f8a57c74856c4ebfda9",
"src/agents/model-compat-catalog.test.ts": "6d838f151d918a8fc42b935ee3928587c62dafab9b5f129b704a4e640ef86233",
"src/agents/model-ref-profile.test.ts": "652f4164ac4f5ed10d891e37be8e384b0119768b071b2cbbde96a11b83e7fc3a",
"src/agents/model-selection-display.test.ts": "eb0bc39f2a5785424ea2fa536fa53bd4b65b58902b34af8d56ad280ace2a5760",
"src/agents/model-tool-support.test.ts": "fb010a8ce91e1970f66fc3c257b265f19d524dbda4038ec3de1646a790b6eccd",
"src/agents/owner-display.test.ts": "e93e0de1b5e065b3e1e7a481dbc661d79f25466db3a1842c38681e7303e438d5",
"src/agents/prompt-surface.test.ts": "373408ae8121ccff88cf86b2630d9bb35cbe1bd919830cd68c936a540275f31a",
"src/agents/provider-tool-policy.test.ts": "556bfe4194fe0a53c9e6edc58665f0511c490d3d4ed935460c99c17b97ef5f00",
"src/agents/pty-keys.test.ts": "8e31a2eb84126ed4c358569a2c1259641ac0d1ca46df7d602cb7bbd6efb96ab4",
"src/agents/responses-image-payload-sanitizer.test.ts": "7128780318aa4e9599c866dbdea18b57308dfcf9b770104c592d4a03abfb0d70",
"src/agents/runtime-auth-refresh.test.ts": "7d46003c01685bf335b7043bd6ca49f3f1afdf4ded14d29c00b716c9c36ca157",
"src/agents/sandbox/bind-spec.test.ts": "550d18dd06011f61e4527e0411d7154af47be3ee67de9dbcb4dcc9bb920a5518",
"src/agents/scheduled-tool-policy.test.ts": "1bb55206b22560f23dd00a66ba0a348a6c0a612d650fc103d0f682948f216dd7",
"src/agents/schema/clean-for-xai.test.ts": "9d2d1a52434231a99e3fb535d9cfaf3e1eba86e45e5840a782e8e808dade47a7",
"src/agents/session-permission-exec-mode.test.ts": "df1e5cd6258f8830d3245428e3ba4ba513fdfc644949ecd175d6e1cef77b3290",
"src/agents/sessions/http-dispatcher.test.ts": "8a9c63855bc6965314f5c43614e088db7cdf75a54a08bfe5e045c0d8b540bba6",
"src/agents/sessions/model-registry-schema.test.ts": "11fe8a030de954f836f64aa6336ba7e9f360210886a57cd5984338dbf8a6c648",
"src/agents/sessions/session-file-parser.test.ts": "9447e7217afae2bc5b0a786785dd78b2b604f9e59e9829ca2e367b7a9d682b0f",
"src/agents/sessions/tools/limits.test.ts": "4f503349cbb77d00f524f94016bb7e687cb82f1d29020be1b9addbc77abadadb",
"src/agents/sessions/tools/truncate.test.ts": "279acda7bedd7cc20172c3455a6456aa685c1b4a7deea0759ad6697f8b736b0c",
"src/agents/stream-message-shared.test.ts": "e6573a15ea050a6a1a6194cc9b2dde09788f0011fe72c5115b4f50e26b31918b",
"src/agents/subagents/announce/subagent-announce-handoff.test.ts": "af7c473ed0fc16cdfd837e56dd95a1b3423500cd06616aceb4c582fcfb6942a4",
"src/agents/subagents/completion/subagent-completion-result.test.ts": "48e16e7c5b4d6a4bf8b170151872482445af78966e279c3427a59bcd80b42520",
"src/agents/subagents/registry/subagent-run-timeout.test.ts": "edfc673bb3f952b429f85a825ce0db3649e12afe2a13406df333a02fda1c939a",
"src/agents/subagents/spawn/subagent-target-policy.test.ts": "bceafbc5b05cf2684ccd1c3fd91eb6c510fe6586b33ee147fe66f80d0e1f20fb",
"src/agents/subagents/subagent-terminal-outcome.test.ts": "50c95003e5f59c52fb91e241b310e2b1fedfe94c5b27c3492739021422aaa28a",
"src/agents/test-helpers/agent-tools-sandbox-context.test.ts": "7e0612c00bba5c9b31354654c529f6e40760e95c6bd176691e91021146f9cbdc",
"src/agents/tool-allowlist-guard.test.ts": "e4f8367a8e7cdf7c308126b6bf2788e0d774c17ef02af511411d19e468e1cb8b",
"src/agents/tool-description-presets.test.ts": "1a20246114aa8749623cadf1c8b1fd4763f5ba5ac820e91a40019b345513890b",
"src/agents/tool-description-summary.test.ts": "95ed9f8bd9af4dcb629a482b818e92e05927766d43e5bacb24ca514f184b9eef",
"src/agents/tool-error-state.test.ts": "a46155c3008a32fe22dc166de8a79faf0a280f914c590520d99dd188c402a714",
"src/agents/tool-error-summary.test.ts": "c336cb27fe9f3c6d62e711aff0cd8f0e5e5f83eab7f4df946566a3e64e93b6e9",
"src/agents/tool-mutation.test.ts": "d573bb7c0202cd7ff8eb05b6a50395a9388f9f67cf597fef74534c47864baccd",
"src/agents/tool-policy.plugin-only-allowlist.test.ts": "93ee0bbb41bfed827198043de4b9a8b8aa3b50167ac084a6351194ee743ed806",
"src/agents/tool-replay-safety.test.ts": "1a94db4f61f86c6ff120073a1040f007edef7f0269fb485193c4e03e47354f8b",
"src/agents/tool-schema-hints.test.ts": "37548cd1cfaff9e1ffd59e680a718bb831d9b07e3108c6192cbf15b4dbbc4753",
"src/agents/tool-search-config.test.ts": "9bbb762c45b5fc7b7cfc65777bb0d26b4562985e73bba7cb6176a875b138f634",
"src/agents/tools/assistant-phase-text.test.ts": "ed2587b690b6d1b888b927450b6c9636b02efca42f1cf4431dae858b2bd7fa15",
"src/agents/tools/computer-tool-guidance.test.ts": "9f5f2697494a657d0a72bf92ae0d528593a8ebe16c634bcfb0fea9754b493db7",
"src/agents/usage.normalization.test.ts": "f625ee715988bbb3ed9d5f4d8c07ca06496f3c2db7a13ccd5b8c5852eb0ec589",
"src/agents/usage.test.ts": "48edbdd1118661abc70e32757993f80acc0d118fdcd6cc44d2024748f6d0c361",
"src/agents/utils/git.test.ts": "6373644a37457e3198f779f180cc471fff7f66196c22650bed18b6a7bb8559da",
"src/agents/utils/syntax-highlight.test.ts": "f3f9eb813f63d2a877a9bb3ecedd5d7e55c7b0419949782514a17ea4179e1914",
"src/agents/worktrees/name.test.ts": "7ec4aecc37fe09bbe2e177c1f1fc925dc5bd80d64beca751c91c81170424991d",
"src/auto-reply/commands-args.test.ts": "5f53f3864b171c7843cf569384606c155a45ccfe36b12d0dbff9edde7022019c",
"src/auto-reply/handoff-summarizer.test.ts": "8858063ca77fc9bf3b06357315cac21de336a68327f4599f9fddd812dcfaafce",
"src/auto-reply/model.test.ts": "e02b8e938c33b73d790da8c263a32ce10351336c33ce66ece39f0d2926308284",
"src/auto-reply/reply/commands-parse.test.ts": "8a3a0b814ce22c1905d8804450f44f4d648a0a671b3b422d17784694653b5556",
"src/auto-reply/reply/commands-reset-mode.test.ts": "c193932671acce98a728ce60ca6036ad223e1792366126a6a69df836b78171a9",
"src/auto-reply/reply/commands-setunset.test.ts": "f078c6e8e86643f280d4e2bd06740dd86271dcfaebb7dd7c02acc14ef28ebb7c",
"src/auto-reply/reply/commands-slash-parse.test.ts": "45b285c93f8eba3ab8595b235af10f398ed77206eba5a9a6f3462c226c984f9e",
"src/auto-reply/reply/completion-delivery-policy.test.ts": "866908e984e3e24d6d1e7bea71818c4726650955c473e23b6025da1364e292d9",
"src/auto-reply/reply/get-reply-exec-overrides.test.ts": "1547be36d887a47469dd8b461811a4a95a8bf51988557ee22811f4a2654277e7",
"src/auto-reply/reply/inbound-media.test.ts": "002dd8c22d4a6afa85d4c6b03ea222d6c9fc01173c26868d4a6f0c4ae9ae12da",
"src/auto-reply/reply/queue-policy.test.ts": "9219101c001dbd2f675f239095fadbe66f702f9d95f45f0639a81a83f8e0c4e6",
"src/auto-reply/reply/reply-inline.test.ts": "d638a2edd033144241c9b2f9668def3ee827f1d9e6e73c094ee5b9cabf71e90b",
"src/auto-reply/reply/routing-policy.test.ts": "261904edb451ded0df4c56a46e28d0a67864164a78b7818ba1aeb733fd37079c",
"src/auto-reply/reply/typing-policy.test.ts": "8e6da06555642aa177b44cb6e2640f81cbde79d9992f41c23d3369544758f136",
"src/auto-reply/tokens.test.ts": "0bc6a7ca567c5d425d65d96a91fefb7de9e712c835ece48743f5c0e877b3356c",
"src/channels/allow-from.test.ts": "a1c26835b9c2508e6e36dbc91ef01c2e1c78111f0d9bcc67dbb080f186d83e7f",
"src/channels/allowlist-match.test.ts": "2cd71e2c6bf3bc8b7570900320c1c2547fccc40dd3233081d17ff47782b16264",
"src/channels/channels-misc.test.ts": "4caef25275c1b2cd483e405b9ead4e6471f2afc4fe10bb4e6749fdd5068e812f",
"src/channels/command-gating.test.ts": "d0800f6b92d2e2809d4db3d86594377a032aa84132d924e3b3b77572b568e876",
"src/channels/conversation-label.test.ts": "e21a32ce35537cac08de839d363ae92c55424c226b910fa5133ee7a4acc8bf82",
"src/channels/inbound-event/classification.test.ts": "2c8a38fd69ae7f2a397685eb5da570fc82ea84a16a4490145f5ab8d49a210d9b",
"src/channels/join-intro/join-intro-prompt.test.ts": "ef940c4c50ca0b925bb3afd4fa7fe97b9fee986f48c77e84f3555c16f0da5f00",
"src/channels/location.test.ts": "b18da68da3fd085edc88161e4736d2c2a0a5862d2c4961579af80071317c1160",
"src/channels/mention-gating.test.ts": "7ba08afe8946684ba3ca78a2b2057d3069f0843fa5abd9db083a04c990ca2f12",
"src/channels/message-access/runtime-identity.test.ts": "efea8aa9c66ef044e20ae3cbef1d03794b48f8c15ded92e0b9cd825c3d79f734",
"src/channels/message/capabilities.test.ts": "b084bea0aea8befa2219e313ccbee5eb0c034aac43cbb8b2c83adb713bf7550b",
"src/channels/message/ingress-drain-lifecycle.test.ts": "5ae8e7dbba6d6b55de4b1f60908f6070ea57cef6ed5cf27c4cc3d81799e3ed4b",
"src/channels/message/receipt.test.ts": "2b88afdec358368a736840f0ded7260bfeda160d67ee22bf0f7a128549401cb7",
"src/channels/message/rendered-batch.test.ts": "6088a82ec82726e790b4e12af237ba5f928bd50a8b15c03b593aa89e4c6c2b04",
"src/channels/native-command-session-targets.test.ts": "d0ef2361bd7abe9ead0974098e38f63368a489bb2d256cb7ce39a4a9b93f35bd",
"src/channels/plugins/account-action-gate.test.ts": "47a88b1da7166ed9e39e1e66c6b2a0b3c64025e2e404dd68fd60b63a27060411",
"src/channels/plugins/chat-target-prefixes.test.ts": "711733c1e2e68c4a105f70dca64ad17ea68dfd8d7275be54ed54e865d8555743",
"src/channels/plugins/conversation-read-origin.test.ts": "0842e4bc132bbf3850bb3675b491879f3ee4bb32dd02f4a40c61de120a5c64a9",
"src/channels/plugins/directory-adapters.test.ts": "0eaa6e0c6e806dfc79964dd009e6295d9cd88359b29d462c0eec885a6e8fba7c",
"src/channels/plugins/directory-config-helpers.test.ts": "6ec97e53d748d6bcd371cda79b5ddfec49d9d1825ac491a5f6ddd36d9438e011",
"src/channels/plugins/target-resolvers.test.ts": "965f3044208950b983a0d4a8cb0c7dccb86f29f15b25d086f7a10c8a248d7ac8",
"src/channels/plugins/threading-helpers.test.ts": "fec00209febd7f074548061ae7005693893570fdc2fbdb6298c431ddbdb1d326",
"src/channels/sender-label.test.ts": "9012cb6f59ba7aa4d74850099cc509b82bcb30c3c89cac667788f389a229ccb2",
"src/channels/status/read-model.test.ts": "cba19d6f760d32398dc3a5ba3b54ea6b51d6301b49f31626b3a806ca25740379",
"src/channels/targets.test.ts": "8dfd946bc71eb2cbf3ef1c14ce38a41c1809fb6ac8fb04dfdd74fc40b05f6940",
"src/channels/thread-binding-id.test.ts": "141bc303f229dc6bb4a906b70da7b5a692544b7c5617043dc1838e8e867b18d6",
"src/channels/thread-bindings-messages.test.ts": "25cf5d517fa785e1b0b196abb9d6026ce430762c021a43e45d9649fe48ff9215",
"src/channels/turn/agent-run-terminal-outcome.test.ts": "c2ee2babe480edc2eb2b8fd70519e32a3d307e23757136b76f11db22e6bf0198",
"src/channels/turn/history-window.test.ts": "d93e9626ad53501381a7423a55f60f06e4887f496ebc435ce1abde8fbfc51c83",
"src/cli/command-path-matches.test.ts": "fa85d58f4f233bbd2b63761e29f2cf0cd895ac5eec527202c8f42aa1d0255250",
"src/cli/config-set-mode.test.ts": "e3502443846d7bb97ce44dc52f42f5dc0031b6d6e7712968696c9393fab46e09",
"src/cli/gateway-port-option.test.ts": "83a2d130f3125c0a2bbeddce28e77c54a131699b4c23ff51095c773471af4e5f",
"src/cli/log-level-option.test.ts": "1974c26220e9bbb09d21e4b51d39f11ee2dcb25b6493dab4d8ee36fbd1c5a829",
"src/cli/nodes-media-utils.test.ts": "6ec3b6dcb6a457c48a28d3f1541c69fb163429a6360600c99f92add2772805f9",
"src/cli/parse-timeout.test.ts": "861cf40e85d56d34898606a8600ca5f9b74e623375f81896fc985652141f061d",
"src/cli/program/command-descriptor-utils.test.ts": "3d5d4bb049c94903ae6e53e9058c5dc865e2717061f298811fa1d483cead95cd",
"src/cli/program/helpers.test.ts": "034accfb58e8940107d2547e674ac4c67fda4b586bdde5e090c52af58f236084",
"src/cli/quote-cli-arg.test.ts": "b13508c5b0e21a274e3e27bc3134e318454c2c057ca42004dd25f45f323dfd84",
"src/cli/root-option-value.test.ts": "88a550cd633ea6e7312db192b9ec271d056b76e0494fc5aa3baee20dbe93cfff",
"src/cli/update-cli/update-command-legacy-service-stop.test.ts": "f23d80ffae28f5e90e77001072777a927f80f76805c79923809a2941c826405b",
"src/commands/daemon-runtime.test.ts": "4aa49bca4461212ac6d2a9fb5f34982a4847375ee2d16fad803aa9c77cd768e4",
"src/commands/doctor/shared/legacy-models-add-metadata.test.ts": "f489d32449a8d10d204b974b5415388faba0d6a0829f2b416d2c50358e7da66c",
"src/commands/gateway-presence.test.ts": "924fbce142b4e822a6f91d6d4909852fec5fb0fc41319e0165e3cf0725e2befb",
"src/commands/migrate/item-selection.test.ts": "900579739d2c99a6fb7729c359c254f903716f7dc09bc18466d152f9ba65ebb4",
"src/commands/onboard-hooks.test.ts": "40c398090bb91e55a2dca62565846b093090242e533cc58be7f68135825a2f86",
"src/commands/status-all/channels-table.test.ts": "050eb224b541fcb4b967ddc6c52d7ab2f832bc67be07cfc90988573071ef0253",
"src/compat/legacy-names.test.ts": "3fb70ed33b30a09195a2217cc56c690d0faa3f499605825671c6160f28a70d7e",
"src/daemon/cmd-argv.test.ts": "0a5ae720d6df04ba46b9a868be65a66112871f09dd41bf3973f45dbdbe3879de",
"src/daemon/constants.test.ts": "5530e8fa310abed10b75816de28e688c5286230fee9b5e46dfdbac37c50eba68",
"src/daemon/launchd-label.test.ts": "7246592b488149ec103cde0bf26b4337822e6f1255f9cd1bb4f078e28a6aba7f",
"src/daemon/paths.test.ts": "549745d86d4c5652c6adf0f8dba2344a11da669ba7cf3af0d9f99a4fe29e8fd9",
"src/daemon/runtime-binary.test.ts": "fc68cc21e2b5b68f357a5dee00bce25ab12287504f4a4f04621b03176d86f93e",
"src/daemon/runtime-format.test.ts": "c2818ea8d2596f21b0c7bba0bc4b28da483c174bcb268dfaf4f963aa5d10c8f5",
"src/daemon/systemd-busctl-legacy.test.ts": "6fc0581d95e5046732589147f6b84b184be619770918ea53ad426723d9377bd2",
"src/daemon/systemd-time-span.test.ts": "ed99c5ae67c9f5723cb943f5db295df435cbd4a410ce9a8487f2a103feb07545",
"src/hooks/bundled/session-memory/transcript.test.ts": "e4fd8b0dafbc779a1e5afc79c3efcedd08cd0f29dd4e0744bd42710a0670500c",
"src/hooks/internal-hook-types.test.ts": "980ec7f15f9045d131f3a2b00b4b3ff8f5b0888e3d7fd3baa6adcf5d59b902e8",
"src/hooks/policy.test.ts": "603894cefcaa2074dbe3094ec31f7b31b4e060804f1e53f6d433bc531fae5e44",
"src/image-generation/capabilities.test.ts": "26d630fce16f68f0414ceec0e428fa2857574dde9a90b67699c908bd8e2b04b8",
"src/image-generation/live-test-helpers.test.ts": "4eb1175eb81d4521dc28030700e425e90ed5d543e44d54f2473d1f74f26e27be",
"src/image-generation/runtime.test.ts": "9d738b542f71f35ce77408146b58f29056c860ddff7f4caa2dd6e613f8e60b8c",
"src/infra/approval-display-paths.test.ts": "600186dbe178dd1fd05220aa6f7e22c47446ae5e6643311c484d9682d5d5b68f",
"src/infra/approval-errors.test.ts": "4799f4986b6d306fdf53a1c4aca792c882e2bae467ce0853c0a9dee6ec4d61a0",
"src/infra/approval-native-delivery.test.ts": "6f6436144744ff4bee65f11958282596348b7c1de8eab01109eb80ba383d3e9b",
"src/infra/approval-native-route-notice.test.ts": "ef0d242afc8fe12750c2490048824487258f6038c0e78ca5c1927b99f610bee4",
"src/infra/approval-native-target-key.test.ts": "767216d20484db7bbeb96f635d1406094d36bc025ea07705a777a83533b5c72b",
"src/infra/approval-resolution-ref.test.ts": "d97bf9140dab5d910a36b329baf7793bd9f9d14d277d62dfbef509a407ca1586",
"src/infra/clawhub-spec.test.ts": "5c9965aa57ab5cc20570f3fa576b820730e314079acde19eaeeef929afa0ddc1",
"src/infra/cli-root-options.test.ts": "bbe076f12f10d083ed7e4dd81a05f8bcb138baf86526372cc8863964acc5ae26",
"src/infra/command-analysis/risks.test.ts": "5caf83ac53004c579209f1fe18c82c90551adf5e1b8ea29c6e06ba4b0bb3a8a3",
"src/infra/deep-merge.test.ts": "69908bcf5be403d8882631424944b7aa31c1d629347a6d082d1a06e960a32ad5",
"src/infra/diagnostic-error-metadata.test.ts": "f5026d8668b7d63868871e5e3af06c45307e7749648a0807d46d7e35c18f2818",
"src/infra/diagnostic-flags.test.ts": "aa335bfa1ea19156df602b5d79dbc40a7896fc98ffde992c0928df1969600551",
"src/infra/diagnostic-llm-content.test.ts": "305ccc22218a0e8dba15266764f755acfdcef7d677cbb24d7bb7ae247d3c574e",
"src/infra/ed25519-signature.test.ts": "348f6025673b2d91130274d6ea5dab3e48aa72b320edb32ceb865f7e2b561706",
"src/infra/exec-policy.test.ts": "e888906c6d9c5b47bdcbb7635be5c0cfc66921d886bd7f1875fdc4268fea9e3a",
"src/infra/exec-safe-bin-semantics.test.ts": "bed11472fb2ce0a954a63c7aec7a379cc54d5958cf3f8509e6c1a354f755b9af",
"src/infra/exec-safety.test.ts": "6b90ea3b3fd71ea6f26a5a6cfba3a525dc9aaf59f248ab383808f62c60b74da1",
"src/infra/fatal-error-hooks.test.ts": "e219b1143beca42c6cea5f3a453158a0dfbb29efd8e72bd96a7fe3fd83508112",
"src/infra/fetch-headers.test.ts": "a1a380c18b723eae387fac1b36ca41c533a56d5d85ef89b26a15ddec7f9fb39e",
"src/infra/fixed-window-rate-limit.test.ts": "39675e52a1af76ba8f6b56ddf5b9202db4c8223f18d0b87971b07b7ad256fb96",
"src/infra/format-time/parse-offsetless-zoned-datetime.test.ts": "a8e9ff882da87af5884d73b6f6f544f4a1ed6ba0f047d264a545087814553c88",
"src/infra/fs-observation-mode.test.ts": "acffb4d8765733e98a2b005eacd087a7f7bec5ddbda6c4261a0bf0b8bcffa221",
"src/infra/gemini-auth.test.ts": "1b942fe00074e818c769152a08f274bd17470fed1783c323151d13b1d2853b97",
"src/infra/google-api-base-url.test.ts": "817ebb7c592b157ff9d535a3927b4ebcaf3382f1ac96ddff7b79177062671a16",
"src/infra/heartbeat-cooldown.test.ts": "2a9e6686735b2d0869b79e54854d0972f81e777b0d71238a3cc0eda6765ad90f",
"src/infra/heartbeat-reason.test.ts": "0b89f6a8de9535655b70eacacd74563a24c6c5787827c4815152f0e62b809d1f",
"src/infra/inline-option-token.test.ts": "839fafbb610d7d03e8131d8174878d53181fe2dac754791de4382373719edfdb",
"src/infra/install-mode-options.test.ts": "86caf74b9c3b59b0f143f9e626b0edef8dba83fd25474dcbd9feb472b4cdc5df",
"src/infra/lru-cache.test.ts": "da7318d8ed0e2d677d2118010099598c547b2b906100c54a239f803562ca05c4",
"src/infra/map-size.test.ts": "62e9bbfc0268e43bcfadd5c140ae93ddef72aab8f5318b961ecf09f8e560ed6d",
"src/infra/net/form-data.test.ts": "6460a0310fb1d845bb7746f054ee5a2ffeb7df26bce4290e27b1364c2b0b52af",
"src/infra/net/hostname.test.ts": "535d1c086c392f0a8211299c4603ec71753f759851440f5b9625c3c233f921e7",
"src/infra/network-interfaces.test.ts": "f28a653d4fb6b86dfb47ae4294c959f6c3c5138f7c3fd5f4c45c74afdd80e02e",
"src/infra/node-options.test.ts": "e1ab9b9f3c91b86d98418f5789445c3e6ab72a1cb5273f79462348a94317db6c",
"src/infra/node-pairing-authz.test.ts": "4448af1e65e0135750ff73c99c1c8c7914ec708c5e1d2ba789988895da2998e0",
"src/infra/node-shell.test.ts": "91ff4312a46a7acf5d71b211519a6f442398269e7a20be867e0a2055723a0a88",
"src/infra/outbound/conversation-id.test.ts": "0c76c8697dc24b3f56ed7943fbbe77fc91dcbf3f3654d771cf76919528b5c957",
"src/infra/outbound/target-errors.test.ts": "cfca36c8bf93afea1540c6b48f4e98c5785b0b429e53cd2bd45821bd8a7e726b",
"src/infra/outbound/thread-id.test.ts": "768e2c2faef4b6b79c4f845bc56bc9890e1a8eea333b6f86a94ea14094326230",
"src/infra/package-tag.test.ts": "da52ec02e850dd67719316e23320cd2b769626c5329740ac235f8df6c12f165d",
"src/infra/prototype-keys.test.ts": "1e1911fbdb49917b5306b207768fe0263925768cf0124c66053ac606891ec0e2",
"src/infra/runtime-status.test.ts": "07587cede720df40d49a925be6ef009029f3b0c516fb845eafb0a206fde59370",
"src/infra/scp-host.test.ts": "168e0b7d410173311bfc59b40cd4ff58bbf76eac9625cc65c64f68a327d7d007",
"src/infra/shell-inline-command.test.ts": "6f5d22311d24d1ad388a86a7380abb0bde9bbac1b67489c713e7b26396b9b3ef",
"src/infra/system-message.test.ts": "adc22e6bac7bed1ae7e9f518a7887661a3b546e8e54a38fb5bf3ad86833b3bd4",
"src/infra/system-run-normalize.test.ts": "d719f4a4168a55aff53a2645e21c9cda58dd5b5e41daf1a2a9ac12bb3a362987",
"src/infra/tcp-port.test.ts": "be7ee81d3fcb5da3965bbd4a2958d0b99b0243c046df0eab6dfd2a63f7d37ee3",
"src/infra/update-dev-target.test.ts": "e9859f6ded2b4461bac4c010f57e3aad0dfa3a390ad3ad758dd9212cab1f03ac",
"src/infra/update-run-record.test.ts": "4ced76cee88be8dcf67f4d9b75d2af0836578b402b23b9a5c1f7294e8408cd06",
"src/infra/ws.test.ts": "83ef26ec93efa98726ace7774ebad803ab47f958dbda3374934e5136d5e2b656",
"src/interactive/payload.fallback.test.ts": "12aa42466eee52509766c2896b95c35957b4c38869087321a9f16407949fc97e",
"src/interactive/payload.test.ts": "2c14416f0fe1e92cd9c547d2f1f31bbda48709f8e63a6a8c4aeea1ac5b5de6e9",
"src/logging/diagnostic-session-attention.test.ts": "608a9261c484c06667b0393a10ed2fddcdc260aff972b8e1f9cb6dce5b8a1ee3",
"src/logging/levels.test.ts": "56b2964eea7fe05d1487db5ecfd60fb626da35f54472156ad1b149f4a2aa718a",
"src/logging/redact-patterns.test.ts": "3ee196bead789de7e3105f986ea2c4788e62a585728208197f34087b7055f299",
"src/media-generation/live-test-helpers.test.ts": "82b7ef595c6791d0362be96035a67278fdde350857a3723b3aa82dc227f5e056",
"src/media-understanding/file-attachment-outcomes.test.ts": "6b2d7e3d58fecb073801b378d5a218f7508556c60d60cab751296eb806af32b4",
"src/media/audio.test.ts": "a1adfb8d5540c7b40d5825591aba325e3f25548110c19464891e39ea7998d03a",
"src/media/media-facts.persisted.test.ts": "717fff484d89b6ee6c16dc9c2973129011145f42ec4d672d1d87d6b33926c707",
"src/music-generation/capabilities.test.ts": "f7fe974b0f5b4a28938ab9fdc336f509a97b4b0b3972c8cc307c6a2b727a23ff",
"src/node-host/invoke-payload.test.ts": "9c482ba5f6456a633d66703829427e19bc2347d99d27416eb7b4dbf98d571aa1",
"src/plugin-sdk/access-groups.test.ts": "21e32411912dea47f704950893def1a0c8a4bd98a4c2236785b90d17e64aacf2",
"src/plugin-sdk/approval-auth-helpers.test.ts": "3aba619d0caeb2d98e83c5d6fd03ffa1b1bce5438cc4c295a859655e4e106172",
"src/plugin-sdk/boolean-param.test.ts": "47b45e211ad621d87ac9c3f3c6a576a0a68e05ee20cb20e3cc47ab6afee41eb6",
"src/plugin-sdk/channel-config-ui-hints.test.ts": "5cb2058de234fc98ac97c572ba90159b54aa02f564d52e8129979a459042c32c",
"src/plugin-sdk/channel-route.test.ts": "498e773d0618d941086f196e576e2a3765b8b956c10add8a68c548cf4fe5f8f0",
"src/plugin-sdk/channel-send-result.test.ts": "a55cf5a4079b74238419f4f9c86317fe019b39ecefe575566a7ffd5da235bf72",
"src/plugin-sdk/cli-backend-compat.test.ts": "846c3f1d9c1b99a2e0d9eb8b9779c005ba14b170af3d492f9c0937f5f3974097",
"src/plugin-sdk/html-entity-runtime.test.ts": "1e43e8484c5ba8a10bcbe4a27569ea4db0a3a7c009def559e0ec2f0bf89cfc5f",
"src/plugin-sdk/inbound-event-delivery.test.ts": "381b21f2fe9d7f18d916d138ff407f25531ee20f774ff0c64a322b67e7a84cde",
"src/plugin-sdk/memory-host-markdown.test.ts": "2250d58130867caba2c4c0e021af25e58179b6bdc5b5baf1cb53789d674ceb08",
"src/plugin-sdk/migration-sensitive-redaction.test.ts": "8853a011b82737663d5f0bc824e67473706c5a71b09cfe00a097e8a0e736f3af",
"src/plugin-sdk/number-runtime.test.ts": "5c2d8fb95a2124a94dfc6e08db150a44cf78a6a464e62d10072b25162ee1195a",
"src/plugin-sdk/pair-loop-guard-runtime.test.ts": "9d1736eb17d6f0fce9b198282d76ceeaf862934b7f72b93018972e939eca7057",
"src/plugin-sdk/provider-catalog-live-normalize.gate.test.ts": "409f7be7641bed08a1a4112d1414fbe895ef27632add676feebfa17e3bd443f8",
"src/plugin-sdk/provider-catalog-live-normalize.primitives.test.ts": "661d3bbf8dcda2adc19a4529981e694dd6cad5392613e7f5130ab0af356d9405",
"src/plugin-sdk/provider-catalog-live-normalize.tokens.test.ts": "51e82a23f9bbe3db00474439cd313139c9fd0228caee8e56669f5e0c527b4f27",
"src/plugin-sdk/qa-channel-protocol.test.ts": "5286ae1ba0b984668fd18f4f75338148fad50fe7124f9157b6619639da48561b",
"src/plugin-sdk/request-url.test.ts": "df500a3e6736e6fa9328de9a60524398b3d81e11ef1d3514ee88142dbfa45b6a",
"src/plugin-sdk/tool-payload.test.ts": "b77ef67d4e3cdc8eb45bed49846e8b218c34f683d98fa5ded1be2399b1af74bf",
"src/plugin-sdk/webhook-memory-guards.test.ts": "be4bf2f1ed7bc889681b98e9d92321128a274fa2284d9a7fb39b9883d2f4ec42",
"src/plugin-sdk/widget-html.test.ts": "1dcb3685d65988559800417cb569489fcfcec9f5d5081f1c53a0d62e0121f13c",
"src/plugins/config-policy.test.ts": "6183663752f4bfbc16c3219edee8d59dbc14c0f690a095a0f3fb4a63fb34dada",
"src/plugins/hook-agent-context.test.ts": "d53aae81be65a00c98bb1291dcafdfa85781d3812490a0c1d1e867614060219d",
"src/plugins/hook-decision-types.test.ts": "e3af2eefec59997ea01d73e2fe2ce1e9308ecea4825cb4868a92ca9cfb68c0e9",
"src/plugins/loader-cache-state.test.ts": "a4c1007df6bf0a54fdfe7d8b563ef63d21fcf9790de103a8d91ecc15302d3264",
"src/plugins/manifest-command-aliases.test.ts": "53e9ba745bbd08ce17441fbb7a9f62720732a5d00275e0319af48ecaa96926d8",
"src/plugins/min-host-version.test.ts": "2badebac5971c52a658a4fe54a16eb393a75230ff8d73153452ff92a854773a0",
"src/plugins/package-compat.test.ts": "e58d0349ed513201dd538cb535a3180f2115d0019134c567feb1d7c0ddde3a8a",
"src/plugins/plugin-entry-order.test.ts": "31052e21ee2c99cdf0c94ac5c37ecca7158578fe4cb065444aa2967dc866b6ae",
"src/plugins/plugin-scope.test.ts": "49381c3edcdfa7411244f3cd548eaca9baa1937858e717058b921c630d7ba2b1",
"src/plugins/provider-model-primary.test.ts": "2c463d3dfcce9ad68c2977ac53b2ae6f7bfecc1fe87d181431fd955fa663cd55",
"src/plugins/provider-registry-shared.test.ts": "41fc1ef21160480dd35663d63b167ada4286f6122ebf09a96bd38271cd857f49",
"src/plugins/provider-validation.test.ts": "9651823b534df7513fdd18a2f73266573caee65587af2a1bc84fdf56fff2169a",
"src/plugins/runtime/runtime-llm-error.test.ts": "752529ad749ec0e37c008ac2293673e43bea2bb3aee3a54a9121f33142255043",
"src/plugins/slots.test.ts": "f1964a87c9dc7225dd93c340c143313d8424155711da65b8163bd55fc3742dab",
"src/plugins/startup-trace-segment.test.ts": "787a3097785015b6aa8c96375ec62dd76e86c6e08716b54453606bba33800a74",
"src/plugins/tool-hook-matcher.test.ts": "f1586d8f8e1374474ededa66afa6901a6100f17090281d7907dc75f591e5b153",
"src/poll-params.test.ts": "c99af3160dccf9abbb8a6548ec878fdd9396c0d9d6c7ce19b2c9f63d0df225d2",
"src/polls.test.ts": "707560d109f1cc9cf365692024ae0265b51f64872b2941c77c0a553e66981407",
"src/process/bounded-line-framer.test.ts": "63a874f0b41fce113b286205002adb5eecf93eed4740cdef415eb49b24074ef6",
"src/routing/account-id.test.ts": "c725fbb0b732e2a368d8a12ae6268a53f0bdff26748044bab1245a7364da3c13",
"src/sessions/input-provenance.test.ts": "797d47be47d849d6a24ea5dedfad393a00dd615d786da91afe73edcf185dc8b5",
"src/sessions/model-overrides.test.ts": "2970cae89573cac32daeb784f67fd38f31070814cff6c325b6366b6b911a4576",
"src/sessions/session-id.test.ts": "cf84405b617b2a150c3d2cf16aff7b6908fe16a295d5ba5bee18202680eadf05",
"src/sessions/system-turn-prompt.test.ts": "36f60f8afcb399f1e1c03dde289c33ceb78a4969ebfa51dd4859aa0e5f0e1a97",
"src/sessions/user-turn-media.test.ts": "6bc108c46e2c5c2145290c585218937bf53db88ddae0c5ccd7741d3c273db4d2",
"src/shared/account-enabled.test.ts": "66b8d6b00b63e011b12e08876287d827be01654437ada29ec4fb730a617512df",
"src/shared/agent-liveness.test.ts": "981ce89dc657574ff0e904fab038e585760a9e3f405bc960f013d8d3a74013b7",
"src/shared/agent-run-status.test.ts": "72de865238c6cac42c0899152d305b34d090b069e96af535f469fb7bc72a5cb5",
"src/shared/assistant-error-format.html.test.ts": "ac28001f81cba38724f00ac484150bbb83dd64c031540334bf56fbc9042f12f6",
"src/shared/assistant-error-format.test.ts": "a5007d689ad0fed08302b07bcc5bae5b860eb7097efb9d9e39bab259afb2858c",
"src/shared/chat-content.test.ts": "f12b2204b073bf5cfa1e7dfbb4da698a3dcd07bb6f49da667da48596a6eafc9e",
"src/shared/chat-envelope.test.ts": "4f3dae7e65f29f29061a00e6c5b046e123d51631cbc14d16f6e60f173270a429",
"src/shared/chat-send-ack-status.test.ts": "d34e431d12fe0481d70a55485a1f8dfc9e051dff948c57154b08b7eedd6f6049",
"src/shared/clawhub-recommendations.test.ts": "a96b8c956a03c5131598948ccc46bbf5a45eec99333bfd1c04a9c3819d529cc0",
"src/shared/device-auth.test.ts": "936b9cf2909590a4c456590526428e74f2cc879edc94379138de493723f92b11",
"src/shared/device-bootstrap-profile.test.ts": "d87c86cb03e05abc27d39f7ac62037e341de6f216ba5b4d1860ca7647e321993",
"src/shared/device-pairing-access.test.ts": "dc96b4272e726bd838c1ace4b734a818de07f86e77a959c2cdc6cb967ccb15d0",
"src/shared/frontmatter.test.ts": "4201834b0a3dbafa4a8f678251b96bdccb021634f6428c454842840b67779817",
"src/shared/gateway-client-platform.test.ts": "116e715f7a4ace13035a50ea3a483106b72f6210cf177a7b5a1630475bbf6559",
"src/shared/html-entities.test.ts": "c73c13299255979e7958bf0a3e1dde3486f23471fa647ba745e621c433d801ce",
"src/shared/http-error-response.test.ts": "8c42982d9bdbb8a6ff6a30f2c75b8bd9bcb81bf81828f78d35933cbc7f79805c",
"src/shared/human-list.test.ts": "b0370ee449b8c235b598d2d01e92dff5dbec2f92b14bd56382fa69aeaa62ddcf",
"src/shared/incognito-session-key.test.ts": "75d083988042139871810f3bb220f7130b61ccc07605b6f08443b5e79d9c1eb9",
"src/shared/iso-time.test.ts": "8b651db1d5c3a832abb646d903689ee8bfec51c7353f2d154cde20ed0ed8e97f",
"src/shared/levenshtein-distance.test.ts": "581c55099c4183cfb4193bd118e6c73003fc2b7a0b316fc000a15eac5c037614",
"src/shared/markdown-code.test.ts": "823dbc9a8c5ff0b02359afed44587a25e04f4f98f70a2f4a0a9a2a47754cec5f",
"src/shared/model-key.test.ts": "1fb29120c480886717bdbe62ab542dd2391b53cc570315025ed6246d6592159a",
"src/shared/model-param-b.test.ts": "733d88147fb2fbb7585b46d99fd00997f0996fd1d1cec081bfcb74e8444d8923",
"src/shared/node-list-parse.test.ts": "4b793aa7c9857c1b98c5c7cc18400a065cb24dceb219ecc02380620e1a288455",
"src/shared/node-presence.test.ts": "4749cce6d722effed5ce82cbdf0dcc85a03cf30538e2a1687331565b8aca9104",
"src/shared/operator-scope-compat.test.ts": "e0003ff949e13ad1ac7613bc08e79bc2dff3270df48ef4540fc76dda63df25fa",
"src/shared/path-array-index.test.ts": "583340ccb38f01d405a684c1bf83093fc5ed585eac3f9b13d728c8ab6408ca31",
"src/shared/requirements.test.ts": "2fe0160d9a1343b5f0cd6cdaa8acc3c1640a8a1e53f58a7ee93d7c35b1489415",
"src/shared/scoped-expiring-id-cache.test.ts": "75a1ca2ddb8590fabfaf89ec05d63d46b833847f1069eedd18e1aeaf1cf81567",
"src/shared/session-list-visibility.test.ts": "9b76729ac9c4c3ff7c4956a6d3701e798620805c28524198220cd415bbaf4825",
"src/shared/silent-reply-policy.test.ts": "2e9d64c1c2a8bcceea51873257028816183d7164a494b76ede8d2675d0cc3cf0",
"src/shared/sort-and-limit.test.ts": "7bc01b05fa0df254c5d3a5321ed03a90960f21acaf039b6f557c06991fea7083",
"src/shared/string-sample.test.ts": "7a2059b9878a4fdbff6ce01cbbf028bd053e79030cc74ff5cd8363b95098e4e8",
"src/shared/subagents-format.test.ts": "1ad829ebbd630b23b4041d60dcf39fb968967deeff69fb8bc96aac1ba1f44a88",
"src/shared/text-chunking.test.ts": "2e74d61f5f23a0ae69a7e66fe79adf15248b19ce6a5118cc59617514ed1f7c85",
"src/shared/text-truncate.test.ts": "1dd5db0600355ddb5b7cf53a23348a36be872f77b8795d01ce75da6865694c15",
"src/shared/text/citation-control-markers.test.ts": "abf5d5443e18ae9ddb70f895ace165c542d41ce868b01a096d29720980c47ec9",
"src/shared/text/final-tags.test.ts": "f685252337f378a2826993939aaa1df5200ae1700e235053b7e481e8192ebe5d",
"src/shared/text/formatted-reasoning-message.test.ts": "5abb1908a60a452ea2b6b43a323cb72f84be9f4da3933130fef8288c4afeecdb",
"src/shared/text/join-segments.test.ts": "ba846b34c346268964ae89bb8251a049d5bf8024dbadb89705b6a2ae2aca5771",
"src/shared/text/message-id-hints.test.ts": "5e7401a5a3963ef241d0af4cee3d1b6be71b75a552828de2d7a6309d65c184d0",
"src/shared/text/strip-markdown.test.ts": "3f7f2532c35c8ae0faf62c7d126cf8bbcc466fd20118646d7920d55847fb26ef",
"src/shared/text/tool-call-shaped-text.test.ts": "0ce85e801647c5ec06516a4c4b3adb3beb52dca26b0928cd8234f8cb83099bf4",
"src/shared/thread-binding-lifecycle.test.ts": "b5ceaa3e93809a3f239ccf000f4f9fd02d688c144ec3466d730ffc263075945e",
"src/shared/usage-aggregates.test.ts": "5ad71e8322ab818f262c918c2d0bbc3a615a484fecee9da4a06c2a4cf315dfa7",
"src/skills/discovery/skill-index.test.ts": "c01880709fa4d9c76fdce80cae5cd868606c3498b5451015a76824d0cb06d465",
"src/skills/lifecycle/gh-config-discovery.test.ts": "889782c1738b129d442e2d49901a8cc9171840e392421b937ad70907e2e789d3",
"src/skills/lifecycle/install-output.test.ts": "2a55cfedc31aa2470f3c7d2a743def176b37bc2f17ba705c35be740e35409775",
"src/skills/workshop/config.test.ts": "7ff46a2786f1928f5c53c80cd9951377d0e7373d2f53d73e45d8c54c4f2ad254",
"src/skills/workshop/learn-prompt.test.ts": "16f6bb26dba951b0d479c1499ef0a9a3cb60b3e6239a870a6523677966755e27",
"src/utils/chunk-items.test.ts": "4b165806a3c07eb758b3b78da6ad7d77427db9781fb7d4c9d2cf725a192f9a88",
"src/utils/normalize-secret-input.test.ts": "2b474185847ecfca939b16f82f380007fc276c640bc7b5d5f8d300e4c7e4499a",
"src/utils/reaction-level.test.ts": "19e5367b157e9875742dc851f4c69adc73dec6fbcc38b920ffc711073d8f2236",
"src/utils/safe-json.test.ts": "aecf090cf62c17dac56cf2bf1e5694109d3e34867d1136227cc743d8c51ade50",
"src/utils/string-readers.test.ts": "6a23a8ed2cb5e2976e7a35f84cee13f9b701e7038cecfb0ca707b49f1b757e17",
"src/utils/token-format.test.ts": "d7a3bf34360e5bb147e1191a9931970da79ee561b0b371e6cbedfa3dfa48dea4",
"src/utils/transcript-tools.test.ts": "8dba462027982f23719694df3df401a3ac8e2a707227c8136bbe4078c83e2012",
"src/utils/utf8-truncate.test.ts": "e08f1403e7076c970a05912a7a2b8c9fbd74a45dfaa4e2a25d2d4e04f63650b0",
"src/video-generation/capabilities.test.ts": "7d3c063fb55c19776fef89df95d67655187687566654d97df098cb748e07dd61",
"src/video-generation/live-test-helpers.test.ts": "cca2a5f82822089fea4212a972743004b60407d3545476f9d2cd371463bb039e",
"test/e2e/qa-lab/runtime/gateway-stability-runtime.test.ts": "8154e55414caae4edebe1d4c3f55c03c3c0f28694a56def6c5246d497a98885e",
"test/e2e/qa-lab/runtime/quota-reset-diagnostics.test.ts": "b46a8a32b372ec5633136e86cd6114e51a08259cd4db7ae71a53a14335745070",
"test/helpers/bounded-child-output.test.ts": "82301a8b3a0fbe1001fb29e2fea5ef5fc4a13c053b11b60e943c79ee3dc870b3",
"test/helpers/live-image-probe.test.ts": "1e6c30018e4b875b8909dffdda08ca10c6f8f63754ee9ccaafa632b8a29d72f3",
"test/scripts/arg-utils.test.ts": "9de73e9b531d4bab7e89725ee7329537d4657a30e61b87cdbcd01caa05db1700",
"test/scripts/bounded-output-tail.test.ts": "37bffdbd77f3e58bdcb5945ac961d296fe9cfa027aad9d607eb27bf0e7a4530d",
"test/scripts/canonical-json.test.ts": "63929fda6e25bf317699d988d54485bf7f4cfbae59d5cf6112c352c417007427",
"test/scripts/changed-path-facts.test.ts": "7f1fbda6997d4b7c13b8c62f28b85cb8befad0d120be08c78580221ec5105533",
"test/scripts/check-docs-i18n-glossary.test.ts": "0736fd4f0357b6ae1b52a4b9fdac1e47211105e1585ad910788f8ffd34aac2e4",
"test/scripts/check-opengrep-rule-metadata.test.ts": "c5bcf68146fafb1c0b933ff226a1e7e843d45b9808c9790e542fd7bf81ec673b",
"test/scripts/ci-check-family-scope.test.ts": "b06ceef933cf2fe63a37db8e60f9ce3624b2b616ab99c5445027228a58649871",
"test/scripts/ci-docker-seed-plan.test.ts": "a6cf2686a06a5ffea3107a771b14c7375e55b8d44d8c843e350a9015b831d55c",
"test/scripts/control-ui-mock-channels.test.ts": "5047a73ca815562eaf82e54833579bc67bcafdc0b8466815ab2579563325986b",
"test/scripts/control-ui-proof-args.test.ts": "752c73e93d91b6b283a05d16b1aacb0c4aeac3065e768cce611702055ccc622e",
"test/scripts/crabbox-routing-policy.test.ts": "927f0ca8931b58f0dd67882afca0585b6b81e7d9a3940a292e92746e9c454d94",
"test/scripts/dependency-spec-policy.test.ts": "ccbc882e2e37e2385b3f5ba4d5d4c6596c914ae075a7fca089219aaf0804865c",
"test/scripts/docker-release-policy.test.ts": "cb7ff37a30c58dd8887a99ace636c5a89a09cc4d78331c667fb47b78a8bd446e",
"test/scripts/extension-source-classifier.test.ts": "83021f2b50efe97f1eeeeba784d5340cd32ed29f3e1dbba30514bb01263f215e",
"test/scripts/format-duration.test.ts": "9598d5cf428971e6e983b0ab17820e42a88c70b5d2b19096136ddefdc91f98c2",
"test/scripts/gateway-frame-payload.test.ts": "83d41c6642ecf52d8be36ac7fd2f860176cb47498970b765a27eb80c494942d5",
"test/scripts/mantis-telegram-storage.test.ts": "472b8357c237c048aa73135307bf8db1d328237c93c7a6d258997a81201de3e7",
"test/scripts/mcp-channel-limits.test.ts": "a12ccf5dd7c3e75dd6bd0661f72f2ba44e86c5317cf2f32eb86e3db237aac64d",
"test/scripts/mcp-channels-attachment-contract.test.ts": "bca3676aaa055d77b5d482e403e0792eae419a7f1b3dda0d74685b6529ebc040",
"test/scripts/merge-head-diff-base.test.ts": "93e26fbfac3f2af63d27f474ec4d2bec52469b0726208b5a884f67dce6475c9f",
"test/scripts/mock-openai-config.test.ts": "7817722032382649003338c7aa42b30d62cfe04ad548ae8a1b2fd5e7d928c370",
"test/scripts/npm-registry-signatures.test.ts": "2c23de20ac9c254581c86cac55698573d2a2a9e4c360f4bc617369cbdfa7f6f8",
"test/scripts/numeric-options.test.ts": "25c6c6c2e6cdab961000f3a414c64832847c8d7922f77002df3162d3b1287162",
"test/scripts/plugin-sdk-declaration-budget.test.ts": "7d6feefdd4825e9743c50cbca10423c53460d571965fa86c1c75ec1c9cb9474a",
"test/scripts/pnpm-lockfile-documents.test.ts": "e7a73f2d2201bc9fd8b4dd32e6c326e79e14c149d9ccf04c947eed1fcff05396",
"test/scripts/proof-video-render.test.ts": "42a8cfa6a3da522b1530e484814371a2bd58eff1126e70650ab0bfb8e28d8353",
"test/scripts/release-validation-intent.test.ts": "03cc98fee67c6a916d8072b23630194f988d5e3459c5cd5832cc4ee57ac662c9",
"test/scripts/report-cli-helpers.test.ts": "e98a862c853af2df85a5a845ffa4c57f57a1f6b570bfc3216ef59496a9878e5d",
"test/scripts/weighted-test-shards.test.ts": "da83b09464718f64ea06a0c144c094425d98ae86f8a92749676258575cce6a83",
"test/vitest-credential-redaction.test.ts": "18c696cb29d624f76d39630ee3781d2fd9658940819f67b65db13d1cf67a752d"
},
"helpers": {
"src/agents/auth-profiles/portability.test.ts": {
"src/agents/auth-profiles/credential-fixtures.test-support.ts": "cd9f191d109d725f5998321c9ad63b52b1d968423a832b9e5a45a3c4b7b2a73e"
},
"src/agents/embedded-agent-runner/run/compaction-timeout.test.ts": {
"src/agents/test-helpers/agent-message-fixtures.ts": "305704c1b71ff28f4c3ee989f61ccf68e14c75b24f459efbe33a38ba2d941fe7",
"src/agents/test-helpers/usage-fixtures.ts": "4cc4cdf5c18ab69b7e16d48e98ce7baf869df5245f708af4fbbf64752763eb61",
"test/helpers/text-tool-result.ts": "433d6e7378a0a3e84634753f346ffef1ad43b544400fd558cbe98f71982a9811"
},
"src/agents/live-model-turn-probes.test.ts": {
"src/agents/test-helpers/live-model-turn-probes.ts": "f8b7984f0a58bb521ae7713fc85c563127753d6c8c5e2e9dfe4c7a3efc69bfe7"
},
"src/infra/network-interfaces.test.ts": {
"src/test-helpers/network-interfaces.ts": "17b2f01822b380d9f2621621d9e9774ded654f91bb9c529b8d591f3aa0720c9c"
},
"src/skills/discovery/skill-index.test.ts": {
"src/skills/test-support/test-helpers.ts": "57584225352106f80e218a053f2bf30c044ba74002a36ce0bfb7fa3039f62045"
}
}
}

View file

@ -1,4 +1,6 @@
import { globSync } from "node:fs";
import { createHash } from "node:crypto";
import { globSync, readFileSync } from "node:fs";
import path from "node:path";
import { agentVitestProjectOwners } from "../../test/vitest/vitest.agents-paths.mjs";
import { databaseWorkerCoreTestFiles } from "../../test/vitest/vitest.database-worker-core-paths.mjs";
import {
@ -20,6 +22,7 @@ import {
unitTestIncludePatterns,
} from "../../test/vitest/vitest.unit-paths.mjs";
import { buildVitestRunPlans } from "../test-projects.test-support.mts";
import nativeBunQualification from "./ci-test-native-bun-qualification.json" with { type: "json" };
import { vitestOptionConsumesNextArg } from "./vitest-cli-mode.mts";
export type CiTestRuntimePolicy = "node" | "bun-compatible" | "dual";
@ -32,13 +35,25 @@ type TestSelection = {
vitestArgs?: readonly string[];
};
type TestShard = TestSelection & { groups?: readonly TestSelection[] };
export type CiTestRuntimeSelection = {
type VitestRuntimeSelection = {
runtime: TestRuntime;
engine?: "vitest";
configs?: string[];
includePatterns?: string[];
includeAfterShard?: true;
env?: Readonly<Record<string, string>>;
};
export type CiTestRuntimeSelection =
| VitestRuntimeSelection
| {
runtime: "bun";
engine: "bun-test";
files: string[];
configs?: never;
includePatterns?: never;
includeAfterShard?: never;
env?: never;
};
// Short-lived UI workers spend less time compiling their top JIT tier when it
// starts later. Keep every tier enabled and share the producer/consumer policy.
@ -51,6 +66,11 @@ export const BUN_UI_TEST_ENV = {
const gatewayCoreConfig = "test/vitest/vitest.gateway-core.config.ts";
const gatewayClientConfig = "test/vitest/vitest.gateway-client.config.ts";
const unitFastConfig = "test/vitest/vitest.unit-fast.config.ts";
const exactTestFilePattern = /^[\w./-]+\.test\.[cm]?[jt]sx?$/u;
const nativeBunTestHashes: Readonly<Record<string, string>> = nativeBunQualification.tests;
const nativeBunHelperHashes: Readonly<Record<string, Readonly<Record<string, string>>>> =
nativeBunQualification.helpers;
const bunCompatibleConfigs = new Set([
"test/vitest/vitest.unit-fast-fake-timers.config.ts",
"test/vitest/vitest.unit-fast-isolated.config.ts",
@ -66,7 +86,7 @@ const embeddedRunOwner = agentVitestProjectOwners.embeddedRun;
const runtimePartitions = new Map<
string,
{
files: (cwd: string) => string[];
files: (cwd: string, includePatterns?: string[]) => string[];
nodeRequired: ReadonlySet<string> | ((file: string) => boolean);
includeAfterShard?: true;
}
@ -83,13 +103,12 @@ const runtimePartitions = new Map<
},
],
[
"test/vitest/vitest.unit-fast.config.ts",
unitFastConfig,
{
files: unitFastFiles,
files: (_cwd, includePatterns) => unitFastFiles(includePatterns),
nodeRequired: new Set([
// The pinned WebKit still misidentifies UTF-16 surrogate-pair segment boundaries.
"packages/markdown-core/src/render-aware-chunking.test.ts",
// Bun skips a sibling diagnostics subscriber when warm-worker cleanup unsubscribes.
"src/agents/code-mode-node.test.ts",
"src/cli/cli-process-diagnostics.test.ts",
// Asserts V8 used_heap_size deltas, cachedDataVersionTag stability, explicit GC,
// and Worker resourceLimits.maxOldGenerationSizeMb propagation.
@ -169,9 +188,46 @@ function partitionRequiresNode(
: partition.nodeRequired.has(file);
}
function unitFastFiles(): string[] {
const otherOwners = new Set([...getUnitFastTimerTestFiles(), ...getUnitFastIsolatedTestFiles()]);
return getUnitFastTestFiles().filter((file) => !otherOwners.has(file));
function unitFastFiles(includePatterns?: string[]): string[] {
const otherOwners = new Set([
...getUnitFastTimerTestFiles(includePatterns),
...getUnitFastIsolatedTestFiles(includePatterns),
]);
return getUnitFastTestFiles(includePatterns).filter((file) => !otherOwners.has(file));
}
function matchesNativeBunSource(file: string, sha256: string, cwd: string): boolean {
try {
return (
createHash("sha256")
.update(readFileSync(path.join(cwd, file)))
.digest("hex") === sha256
);
} catch {
// Missing or unreadable qualification inputs retain the ordinary Vitest run.
return false;
}
}
function qualifiedNativeBunFiles(files: readonly string[], cwd: string): string[] {
const candidates = files.filter((file) => nativeBunTestHashes[file]);
if (
!candidates.length ||
!Object.entries(nativeBunQualification.setup).every(([file, sha256]) =>
matchesNativeBunSource(file, sha256, cwd),
)
) {
return [];
}
// Native table argument semantics are qualified against test bytes, not the
// production code they exercise. Changed tests/helpers keep Vitest coverage.
return candidates.filter(
(file) =>
matchesNativeBunSource(file, nativeBunTestHashes[file]!, cwd) &&
Object.entries(nativeBunHelperHashes[file] ?? {}).every(([helper, sha256]) =>
matchesNativeBunSource(helper, sha256, cwd),
),
);
}
function unitFiles(cwd: string): string[] {
@ -284,7 +340,7 @@ export function resolveCiTestRuntimeSelections(
if (selection.targets?.length) {
// Preserve exact target argv and its native owner; broad targets can carry
// multiple process/filter contracts and stay on Node.
if (selection.targets.some((target) => !/^[\w./-]+\.test\.[cm]?[jt]sx?$/u.test(target))) {
if (selection.targets.some((target) => !exactTestFilePattern.test(target))) {
return node;
}
const plans = selection.targets.flatMap((target) => buildVitestRunPlans([target], cwd));
@ -309,12 +365,25 @@ export function resolveCiTestRuntimeSelections(
) {
return node;
}
const files = new Set(partition.files(cwd));
return selection.targets.every(
(target) => files.has(target) && !partitionRequiresNode(partition, target),
)
? completeBun()
: node;
const files = new Set(partition.files(cwd, [...selection.targets]));
if (
!selection.targets.every(
(target) => files.has(target) && !partitionRequiresNode(partition, target),
)
) {
return node;
}
if (
config === unitFastConfig &&
args.length === 0 &&
qualifiedNativeBunFiles(selection.targets, cwd).length === selection.targets.length
) {
return [
...(policy === "dual" ? node : []),
{ runtime: "bun", engine: "bun-test", files: [...selection.targets] },
];
}
return completeBun();
}
if (
selection.configs?.length === 2 &&
@ -363,7 +432,14 @@ export function resolveCiTestRuntimeSelections(
if (!partition || (partition.includeAfterShard && !uiPartition)) {
return node;
}
const inventory = partition.files(cwd);
// Reuse canonical scoped analysis only for exact files; glob envelopes keep
// their full inventory and existing matcher semantics.
const exactSelection = selection.includePatterns?.every((pattern) =>
exactTestFilePattern.test(pattern),
)
? [...selection.includePatterns]
: undefined;
const inventory = partition.files(cwd, exactSelection);
const requested = new Set(selection.includePatterns ?? []);
// Canonical file inventories should not reparse every file pair as a glob.
const exactFiles = selection.includePatterns?.every(
@ -381,6 +457,12 @@ export function resolveCiTestRuntimeSelections(
return node;
}
const nodeFiles = files.filter((file) => partitionRequiresNode(partition, file));
// Ordinary CI passes no extra Vitest argv. Collection, filters and overrides
// keep Vitest's interpretation rather than silently changing native semantics.
const nativeFiles =
config === unitFastConfig && args.length === 0 ? qualifiedNativeBunFiles(bunFiles, cwd) : [];
const nativeSet = new Set(nativeFiles);
const vitestFiles = bunFiles.filter((file) => !nativeSet.has(file));
return [
...(policy === "dual"
? node
@ -393,12 +475,19 @@ export function resolveCiTestRuntimeSelections(
},
]
: []),
{
runtime: "bun",
includePatterns: bunFiles,
...(partition.includeAfterShard ? { includeAfterShard: true } : {}),
...(config === "ui/vitest.config.ts" ? { env: BUN_UI_TEST_ENV } : {}),
},
...(vitestFiles.length
? [
{
runtime: "bun" as const,
includePatterns: vitestFiles,
...(partition.includeAfterShard ? { includeAfterShard: true as const } : {}),
...(config === "ui/vitest.config.ts" ? { env: BUN_UI_TEST_ENV } : {}),
},
]
: []),
...(nativeFiles.length
? [{ runtime: "bun" as const, engine: "bun-test" as const, files: nativeFiles }]
: []),
];
}

View file

@ -79,7 +79,7 @@ export async function runVitestBatch(params: VitestBatchRunParams): Promise<numb
...testCommand,
options: {
cwd: repoRoot,
env,
env: testCommand.envOverrides ? { ...env, ...testCommand.envOverrides } : env,
stdio: workers ? ["inherit", "inherit", "inherit", "ipc"] : "inherit",
},
});

View file

@ -1,10 +1,44 @@
import path from "node:path";
import { resolveTestBunSourceArgs } from "../../src/test-utils/bun-process.ts";
import { DEFAULT_VITEST_TEST_TIMEOUT_MS } from "../../test/vitest/vitest.timeouts.ts";
import { resolveRepoRoot } from "./repo-root.mjs";
import { resolveVitestNodeArgs } from "./vitest-process-env.mts";
type VitestTestCommand = {
command: string;
args: string[];
envOverrides?: Readonly<Record<string, string>>;
};
// The pinned Bun fork generates bytecode during Vitest's cache flush at teardown.
// Its separate transpiler cache still reuses module transforms.
const bunTestEnvOverrides = { NODE_DISABLE_COMPILE_CACHE: "1" } as const;
export function resolveNativeBunTestCommand(
files: string[],
repoRoot = resolveRepoRoot(import.meta.url),
): VitestTestCommand {
return {
command: "bun",
envOverrides: bunTestEnvOverrides,
args: [
"test",
"--no-env-file",
...resolveTestBunSourceArgs(repoRoot),
"--preload",
path.join(repoRoot, "test/setup.env.ts"),
"--timeout",
String(DEFAULT_VITEST_TEST_TIMEOUT_MS),
...files,
],
};
}
/** Select only the Vitest process; orchestration and preparation retain Node. */
export function resolveVitestTestCommand(args: string[], env: NodeJS.ProcessEnv = process.env) {
export function resolveVitestTestCommand(
args: string[],
env: NodeJS.ProcessEnv = process.env,
): VitestTestCommand {
const runtime = env.OPENCLAW_VITEST_RUNTIME?.trim() || "node";
if (runtime === "node") {
return { command: process.execPath, args };
@ -19,6 +53,7 @@ export function resolveVitestTestCommand(args: string[], env: NodeJS.ProcessEnv
const nodeFlags = new Set(resolveVitestNodeArgs({}));
return {
command: "bun",
envOverrides: bunTestEnvOverrides,
// Strip V8 flags only before the CLI; test names and filters stay byte-for-byte.
args: [
// Workers inherit this resolver without adding aliases to child-process environments.

View file

@ -1,6 +1,7 @@
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { setImmediate as nextTurn } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import { vitestWorkerDeclarationEntries } from "./vitest-worker-declarations.mts";
@ -26,6 +27,19 @@ const declarations = new Map(
export const VITEST_WORKER_PREPARE_REQUEST = "openclaw:prepare-test-subprocesses";
export const VITEST_WORKER_PREPARE_REPLY = "openclaw:test-subprocesses-prepared";
function readArtifact(filename: string): Promise<Buffer> {
// Callback reads reduce overhead across the generation's many small files.
return new Promise((resolve, reject) => {
fs.readFile(filename, (error, bytes) => {
if (error) {
reject(error);
} else {
resolve(bytes);
}
});
});
}
export async function verifyVitestWorkerArtifacts(
directory: string,
manifest?: VitestWorkerManifest,
@ -47,15 +61,21 @@ export async function verifyVitestWorkerArtifacts(
},
];
const batchSize = 32;
let firstBatch = true;
for (const { files, root: baseDir, changed } of groups) {
const entries = Object.entries(files);
for (let offset = 0; offset < entries.length; offset += batchSize) {
// Ready reads can drain as microtasks without giving signals an event-loop turn.
if (!firstBatch) {
await nextTurn();
}
firstBatch = false;
// Native batches keep pre-install planning dependency-free and signals responsive.
// Drain every started read before rejection: the owner may delete files next.
const settled = await Promise.allSettled(
entries.slice(offset, offset + batchSize).map(async ([name, expected]) => {
const filename = baseDir ? path.join(baseDir, name) : name;
if (hashVitestWorkerArtifact(await fs.promises.readFile(filename)) !== expected) {
if (hashVitestWorkerArtifact(await readArtifact(filename)) !== expected) {
throw new Error(`${changed}: ${name}`);
}
if (

View file

@ -1493,6 +1493,7 @@ test/vitest/vitest.performance-config.ts
test/vitest/vitest.plugin-sdk-paths.mjs
test/vitest/vitest.startup-corpus-paths.mjs
test/vitest/vitest.test-shards.mjs
test/vitest/vitest.timeouts.ts
test/vitest/vitest.tooling-isolated-paths.mjs
test/vitest/vitest.ui-isolated-paths.mjs
test/vitest/vitest.ui-paths.mjs

View file

@ -51,7 +51,10 @@ import {
resolveVitestRuntimeCliSelections,
shouldPrepareVitestCoreWorkers,
} from "./lib/vitest-runtime-selection.mts";
import { resolveVitestTestCommand } from "./lib/vitest-test-runtime.mts";
import {
resolveNativeBunTestCommand,
resolveVitestTestCommand,
} from "./lib/vitest-test-runtime.mts";
import {
createVitestUnhandledErrorDetector,
stripVitestAnsi,
@ -264,6 +267,28 @@ function isExplicitTestFileArg(arg: string): boolean {
return EXPLICIT_TEST_FILE_RE.test(arg) && isExplicitFileTargetArg(arg);
}
function validateNativeBunTestFiles(files: string[], cwd: string): string[] {
if (
files.length === 0 ||
files.some((file) => {
const relative = path.relative(cwd, path.resolve(cwd, file)).replaceAll(path.sep, "/");
return !isExplicitTestFileArg(file) || file !== `./${relative}` || relative.startsWith("../");
})
) {
throw new Error("Native Bun tests require explicit ./relative.test.ts file operands");
}
const missing = resolveMissingExplicitTestFiles(files, cwd);
if (missing.length > 0) {
throw new Error(`Native Bun test files not found: ${missing.join(", ")}`);
}
for (const file of files) {
if (!fs.statSync(path.resolve(cwd, file)).isFile()) {
throw new Error(`Native Bun test operand is not a file: ${file}`);
}
}
return files;
}
function isDelegableBroadProjectRouterTarget(arg: string, cwd: string): boolean {
const relative = toRepoRelativeArg(arg, cwd).replace(/\/+$/u, "");
return (
@ -799,26 +824,45 @@ function forwardVitestOutput(
* Joins watched Vitest processes and keeps expired deadlines failed after cooperative exits.
*/
export function spawnWatchedVitestProcess({
pnpmArgs,
pnpmArgs = [],
nativeBunFiles,
spawnParams,
env,
onNoOutputTimeout,
workerRun,
homeMode = resolveVitestHomeSelection(pnpmArgs, { cwd: spawnParams.cwd, env }),
homeMode = nativeBunFiles
? "hermetic"
: resolveVitestHomeSelection(pnpmArgs, { cwd: spawnParams.cwd, env }),
}: {
pnpmArgs: string[];
spawnParams: PnpmRunnerParams;
env: NodeJS.ProcessEnv;
onNoOutputTimeout?: () => void;
workerRun?: VitestWorkerRun;
homeMode?: Parameters<typeof spawnOwnedVitestProcess>[0]["homeMode"];
}) {
} & (
| {
pnpmArgs: string[];
nativeBunFiles?: never;
workerRun?: VitestWorkerRun;
homeMode?: Parameters<typeof spawnOwnedVitestProcess>[0]["homeMode"];
}
| {
nativeBunFiles: string[];
pnpmArgs?: never;
workerRun?: never;
homeMode?: "hermetic";
}
)) {
if (homeMode !== "tooling") {
assertTestHomeSelection(env, homeMode);
}
let timeoutCompletion: Promise<boolean> | null = null;
const directNodeArgs = resolveDirectNodeVitestArgs(pnpmArgs);
const testCommand = directNodeArgs ? resolveVitestTestCommand(directNodeArgs, env) : undefined;
const testCommand = nativeBunFiles
? resolveNativeBunTestCommand(
validateNativeBunTestFiles(nativeBunFiles, spawnParams.cwd ?? process.cwd()),
)
: directNodeArgs
? resolveVitestTestCommand(directNodeArgs, env)
: undefined;
if (workerRun && testCommand) {
// Give either runtime the same owned compiled-subprocess generation.
const cliIndex = testCommand.args.findIndex((arg) => path.basename(arg) === "vitest.mjs");
@ -849,7 +893,18 @@ export function spawnWatchedVitestProcess({
: spawnParams;
const { child, completion: childCompletion } = spawnOwnedVitestProcess({
...(testCommand
? { ...testCommand, options: childSpawnParams }
? {
...testCommand,
options: testCommand.envOverrides
? {
...childSpawnParams,
env: {
...(childSpawnParams.env ?? process.env),
...testCommand.envOverrides,
},
}
: childSpawnParams,
}
: createPnpmRunnerSpawnSpec({ pnpmArgs, ...childSpawnParams })),
homeMode,
});
@ -925,6 +980,46 @@ export async function runVitest(
argv: string[] = process.argv.slice(2),
env: NodeJS.ProcessEnv = process.env,
): Promise<void> {
if (argv[0] === "--native-bun") {
if (env.OPENCLAW_NATIVE_BUN_PARENT_IPC === "1" && !process.connected) {
throw new Error("Native Bun test parent IPC disconnected before admission");
}
const spawnEnv = resolveRunVitestSpawnEnv(env, ["run"]);
let watched: ReturnType<typeof spawnWatchedVitestProcess> | undefined;
let interrupted: NodeJS.Signals | undefined;
const onSignal = (signal: NodeJS.Signals) => {
interrupted ??= signal;
};
const onParentDisconnect = () => {
interrupted ??= "SIGTERM";
process.kill(process.pid, "SIGTERM");
};
process.on("SIGINT", onSignal);
process.on("SIGTERM", onSignal);
if (process.connected) {
process.once("disconnect", onParentDisconnect);
process.channel?.unref();
}
try {
watched = spawnWatchedVitestProcess({
nativeBunFiles: argv.slice(1),
spawnParams: resolveVitestSpawnParams(spawnEnv),
env: spawnEnv,
});
const result = await watched.completion;
interrupted ??= result.signal ?? undefined;
process.exitCode = result.code ?? 1;
} finally {
const signal = interrupted ?? watched?.getForwardedSignal();
process.off("disconnect", onParentDisconnect);
process.off("SIGINT", onSignal);
process.off("SIGTERM", onSignal);
if (signal) {
await exitBySignal(signal);
}
}
return;
}
if (argv.some((arg) => arg === "--isolated-image" || arg.startsWith("--isolated-image="))) {
const { parseIsolatedVitestArgs, runIsolatedVitest } =
await import("./lib/vitest-isolated.mts");

View file

@ -16,17 +16,21 @@ it("checks the captured registry after cron data cleanup", async () => {
const rotate = pool.rotate.bind(pool);
const closeResources = pool.closeResources.bind(pool);
let cleanupCalled = false;
const cleanup = process.versions.bun
? vi.spyOn(pool, "rotate").mockImplementation(async () => {
await rotate();
cleanupCalled = true;
invalidateRegisteredAgentDatabasesMemo({ env });
})
: vi.spyOn(pool, "closeResources").mockImplementation(async (key) => {
await closeResources(key);
cleanupCalled = true;
invalidateRegisteredAgentDatabasesMemo({ env });
});
const invalidateAfterCleanup = () => {
if (cleanupCalled) {
return;
}
cleanupCalled = true;
invalidateRegisteredAgentDatabasesMemo({ env });
};
const rotateCleanup = vi.spyOn(pool, "rotate").mockImplementation(async () => {
await rotate();
invalidateAfterCleanup();
});
const resourceCleanup = vi.spyOn(pool, "closeResources").mockImplementation(async (key) => {
await closeResources(key);
invalidateAfterCleanup();
});
try {
const pending = readExpiredCronRunEntriesInWorker({
agentId: "main",
@ -37,7 +41,8 @@ it("checks the captured registry after cron data cleanup", async () => {
await expect(pending).rejects.toThrow("registry changed");
expect(cleanupCalled).toBe(true);
} finally {
cleanup.mockRestore();
rotateCleanup.mockRestore();
resourceCleanup.mockRestore();
}
});
});

View file

@ -285,17 +285,21 @@ it("checks the captured registry after logical data cleanup", async () => {
const rotate = pool.rotate.bind(pool);
const closeResources = pool.closeResources.bind(pool);
let cleanupCalled = false;
const cleanup = process.versions.bun
? vi.spyOn(pool, "rotate").mockImplementation(async () => {
await rotate();
cleanupCalled = true;
invalidateRegisteredAgentDatabasesMemo({ env });
})
: vi.spyOn(pool, "closeResources").mockImplementation(async (key) => {
await closeResources(key);
cleanupCalled = true;
invalidateRegisteredAgentDatabasesMemo({ env });
});
const invalidateAfterCleanup = () => {
if (cleanupCalled) {
return;
}
cleanupCalled = true;
invalidateRegisteredAgentDatabasesMemo({ env });
};
const rotateCleanup = vi.spyOn(pool, "rotate").mockImplementation(async () => {
await rotate();
invalidateAfterCleanup();
});
const resourceCleanup = vi.spyOn(pool, "closeResources").mockImplementation(async (key) => {
await closeResources(key);
invalidateAfterCleanup();
});
let consumed = false;
try {
const pending = withSessionEntryReadOnlyInWorker(
@ -313,7 +317,8 @@ it("checks the captured registry after logical data cleanup", async () => {
expect(cleanupCalled).toBe(true);
expect(consumed).toBe(true);
} finally {
cleanup.mockRestore();
rotateCleanup.mockRestore();
resourceCleanup.mockRestore();
}
});
});

View file

@ -1,5 +1,8 @@
import { vi } from "vitest";
import type { WorkerTaskOptions } from "../../infra/worker-task-pool.types.js";
import type {
WorkerTaskOptions,
WorkerTaskPoolOptions,
} from "../../infra/worker-task-pool.types.js";
import type { SessionTranscriptDisplayDeltaResult } from "./session-accessor.sqlite-history-query.js";
import { SessionTranscriptColdError } from "./session-cold-storage-state.js";
import { SessionTranscriptProjectionUnavailableError } from "./session-transcript-projection-error.js";
@ -64,16 +67,33 @@ vi.mock("../../infra/worker-task-pool.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../infra/worker-task-pool.js")>();
return {
...actual,
createOwnedWorkerTaskPool: () => ({
run(prepare: () => unknown, options: WorkerTaskOptions<unknown>) {
if (observed.deferredRun) {
return observed.deferredRun(prepare, options);
}
return observed.run(prepare(), options);
},
rotate: observed.rotate,
closeResources: observed.closeResources,
}),
createOwnedWorkerTaskPool: (poolOptions: WorkerTaskPoolOptions<unknown>) => {
let worker: ReturnType<NonNullable<typeof poolOptions.prepareWorker>> | undefined;
return {
async run(prepare: () => unknown, options: WorkerTaskOptions<unknown>) {
const prepareInput = () => {
const input = prepare();
worker ??= poolOptions.prepareWorker?.();
return input;
};
return await (observed.deferredRun
? observed.deferredRun(prepareInput, options)
: observed.run(prepareInput(), options));
},
async rotate() {
const previous = worker;
worker = undefined;
try {
await observed.rotate();
await previous?.releaseResources?.();
} catch (error) {
void Promise.resolve(poolOptions.onRetirementFailure?.(error)).catch(() => undefined);
throw error;
}
},
closeResources: observed.closeResources,
};
},
WorkerTaskPool: class {
run(prepare: () => unknown, options: WorkerTaskOptions<unknown>) {
return observed.run(prepare(), options);

View file

@ -14,12 +14,19 @@ import * as sqliteScope from "./session-accessor.sqlite-scope.js";
import { canonicalSessionKeyMigrationRequiredError } from "./session-canonical-row.js";
import { readSessionHistoryPageInWorker } from "./session-history-worker-runtime.js";
import { prepareSessionTranscriptHydration } from "./session-transcript-hydration.js";
import { withSessionHistoryWorkerReadCandidates } from "./session-transcript-worker-resources.js";
import {
historyLane,
rotateDatabaseWorkers,
withSessionHistoryWorkerReadCandidates,
} from "./session-transcript-worker-resources.js";
import { withSessionHistoryWorkerDatabase } from "./session-transcript-worker-runtime.js";
const closeCapabilities = vi.hoisted(() => ({ explicitSqliteCloseReleasesNativeResources: true }));
vi.mock("../../infra/bun-sqlite-library.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../infra/bun-sqlite-library.js")>()),
ensureSqliteLibrarySelected: () => ({ source: "runtime" }),
captureSqliteWorkerClosePolicy: () =>
closeCapabilities.explicitSqliteCloseReleasesNativeResources,
getSqliteRuntimeCapabilities: () => ({ ...closeCapabilities, reason: "test policy" }),
}));
@ -116,6 +123,7 @@ beforeEach(() => {
afterEach(async () => {
observed.rotate.mockResolvedValue(undefined);
await Promise.all(observed.resources.splice(0).map((resource) => resource.close()));
await rotateDatabaseWorkers(historyLane);
vi.restoreAllMocks();
expect(observed.nativeWorker).not.toHaveBeenCalled();
});

View file

@ -2,7 +2,10 @@ import assert from "node:assert/strict";
import { channel } from "node:diagnostics_channel";
import { afterAll, afterEach, beforeEach, expect, it, vi } from "vitest";
import { SQLITE_IDLE_HANDLE_TTL_MS } from "../../infra/sqlite-handle-lifecycle.js";
import type { WorkerTaskOptions } from "../../infra/worker-task-pool.types.js";
import type {
WorkerTaskOptions,
WorkerTaskPoolOptions,
} from "../../infra/worker-task-pool.types.js";
import { createDeferredCore } from "../../shared/deferred.js";
import {
historyLane,
@ -30,10 +33,13 @@ const observed = vi.hoisted(() => ({
closeResources: vi.fn<(key?: string) => Promise<void>>().mockResolvedValue(undefined),
unregister: vi.fn<() => void>(),
resources: [] as Resource[],
replaceWorkers: [] as Array<() => () => Promise<void>>,
}));
vi.mock("../../infra/bun-sqlite-library.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../infra/bun-sqlite-library.js")>()),
ensureSqliteLibrarySelected: () => ({ source: "runtime" }),
captureSqliteWorkerClosePolicy: () => observed.explicitSqliteCloseReleasesNativeResources,
getSqliteRuntimeCapabilities: () => ({
explicitSqliteCloseReleasesNativeResources: observed.explicitSqliteCloseReleasesNativeResources,
reason: "test policy",
@ -52,12 +58,33 @@ vi.mock("node:diagnostics_channel", async (importOriginal) => {
vi.mock("../../infra/worker-task-pool.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../infra/worker-task-pool.js")>()),
createOwnedWorkerTaskPool: () => ({
run: (prepare: () => unknown, options: WorkerTaskOptions<unknown>) =>
observed.run(prepare(), options),
rotate: observed.rotate,
closeResources: observed.closeResources,
}),
createOwnedWorkerTaskPool: (poolOptions: WorkerTaskPoolOptions<unknown>) => {
let worker: ReturnType<NonNullable<typeof poolOptions.prepareWorker>> | undefined;
observed.replaceWorkers.push(() => {
const previous = worker;
worker = poolOptions.prepareWorker?.();
return async () => previous?.releaseResources?.();
});
return {
async run(prepare: () => unknown, options: WorkerTaskOptions<unknown>) {
const preparedInput = prepare();
worker ??= poolOptions.prepareWorker?.();
return await observed.run(preparedInput, options);
},
async rotate() {
const previous = worker;
worker = undefined;
try {
await observed.rotate();
await previous?.releaseResources?.();
} catch (error) {
void Promise.resolve(poolOptions.onRetirementFailure?.(error)).catch(() => undefined);
throw error;
}
},
closeResources: observed.closeResources,
};
},
}));
vi.mock("../../state/openclaw-agent-db-resources.js", () => ({
matchesAgentDatabaseReadCandidatePath: (candidate: { path: string }, targetPath: string) =>
@ -107,6 +134,9 @@ afterEach(async () => {
observed.rotate.mockResolvedValue(undefined);
observed.closeResources.mockResolvedValue(undefined);
await Promise.all(observed.resources.splice(0).map((resource) => resource.close()));
await Promise.all(
[historyLane, projectionLane, maintenanceLane].map((lane) => rotateDatabaseWorkers(lane)),
);
});
afterAll(() => {
vi.useRealTimers();
@ -265,6 +295,41 @@ it.each([
},
);
it("binds native-close policy to each worker generation before replies", async () => {
const read = async (lane: typeof historyLane) => {
const request = input();
observed.run.mockResolvedValueOnce({ ok: true, value: false });
await withSessionHistoryWorkerDatabase(
request.database,
(owner) => owner.readEntryPresence(request.scope),
lane,
);
const resource = observed.resources.at(-1);
assert(resource?.agentId);
return resource;
};
observed.explicitSqliteCloseReleasesNativeResources = false;
const early = await read(historyLane);
observed.explicitSqliteCloseReleasesNativeResources = true;
const otherLane = await read(maintenanceLane);
// A completed host decision cannot upgrade a worker born before admission.
await early.close();
expect(observed.rotate).toHaveBeenCalledOnce();
await otherLane.close();
expect(observed.closeResources).toHaveBeenCalledOnce();
const predecessor = await read(historyLane);
const releasePredecessor = observed.replaceWorkers[0]!();
// Replacement captures its own policy without waiting for a task result.
await predecessor.close();
expect(observed.closeResources).toHaveBeenCalledTimes(2);
const successor = await read(historyLane);
await releasePredecessor();
await successor.close();
expect(observed.closeResources).toHaveBeenCalledTimes(3);
expect(observed.rotate).toHaveBeenCalledOnce();
});
it.each([false, true])("retains reads dispatched after cleanup (capable=%s)", async (capable) => {
observed.explicitSqliteCloseReleasesNativeResources = capable;
const request = input();

View file

@ -1,8 +1,8 @@
import { channel } from "node:diagnostics_channel";
import { err, ok, type Result } from "@openclaw/normalization-core/result";
import {
captureSqliteWorkerClosePolicy,
ensureSqliteLibrarySelected,
getSqliteRuntimeCapabilities,
} from "../../infra/bun-sqlite-library.js";
import { runtimeProcessEntrypoints } from "../../infra/runtime-process-entrypoints.js";
import { resolveRuntimeWorkerUrl } from "../../infra/runtime-worker-url.js";
@ -53,7 +53,8 @@ import { captureSessionTranscriptStorageEnvironment } from "./transcript-target-
const workerUrl = resolveRuntimeWorkerUrl(runtimeProcessEntrypoints.sessionTranscript);
function createHistoryPool() {
return createOwnedWorkerTaskPool<
let generation: { canCloseNativeResources: boolean } | undefined;
const pool = createOwnedWorkerTaskPool<
SessionHistoryWorkerInput,
SessionTranscriptWorkerReply<SessionHistoryWorkerInput["kind"]>
>({
@ -63,9 +64,30 @@ function createHistoryPool() {
idleTimeoutMs: 0,
prepareWorker: () => {
ensureSqliteLibrarySelected();
return { options: {} };
// The worker inherits this same fact at creation; later admission cannot upgrade it.
const current = { canCloseNativeResources: captureSqliteWorkerClosePolicy() };
generation = current;
return {
options: {},
async releaseResources() {
if (generation === current) {
generation = undefined;
}
},
};
},
onRetirementFailure() {
generation = undefined;
},
});
return {
...pool,
canCloseNativeResources: () => generation?.canCloseNativeResources === true,
rotate() {
generation = undefined;
return pool.rotate();
},
};
}
function createUsageCostPool(kind: "read" | "refresh") {
@ -285,12 +307,9 @@ async function closeDatabaseWorkerResource(
idle: boolean,
): Promise<void> {
const pool = historyWorkerLanes.find((candidate) => candidate === lane)?.pool;
// Active reads retain native-exit custody even when ordinary close releases handles.
if (
!idle ||
!getSqliteRuntimeCapabilities().explicitSqliteCloseReleasesNativeResources ||
!pool
) {
// Active or unqualified readers keep native-exit custody. Proven idle readers
// release the exact database while retaining this worker's loaded code.
if (!idle || !pool?.canCloseNativeResources()) {
await rotateDatabaseWorkers(lane);
return;
}
@ -480,11 +499,8 @@ export async function withSessionHistoryWorkerReadCandidates<T>(
return closing;
};
const settleCandidates = async () => {
// Failed discovery can retain native handles outside candidate custody.
if (
discoveryFailed ||
!getSqliteRuntimeCapabilities().explicitSqliteCloseReleasesNativeResources
) {
// Failed discovery can retain handles outside candidate custody even on a proven worker.
if (discoveryFailed || !lane.pool.canCloseNativeResources()) {
await retire();
return;
}

View file

@ -1,3 +1,5 @@
import { replaceSessionEntry } from "../config/sessions/session-accessor.js";
import type { SessionEntry } from "../config/sessions/types.js";
import "./session-reaper.js";
type CronSessionReaperTestApi = {
@ -13,3 +15,12 @@ function getTestApi(): CronSessionReaperTestApi {
export function resetReaperThrottle(): void {
getTestApi().resetReaperThrottle();
}
export async function seedSessionEntries(
storePath: string,
entries: Record<string, SessionEntry>,
): Promise<void> {
for (const [sessionKey, entry] of Object.entries(entries)) {
await replaceSessionEntry({ agentId: "main", storePath, sessionKey }, entry);
}
}

View file

@ -20,6 +20,7 @@ import {
} from "../config/sessions/targets.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.js";
import { initializeSqliteRuntimeCapabilities } from "../infra/bun-sqlite-library.js";
import { isCronRunSessionKey } from "../sessions/session-key-utils.js";
import { beginSessionWorkAdmission } from "../sessions/session-lifecycle-admission.js";
import {
@ -35,9 +36,11 @@ import { createOpenClawTestState } from "../test-utils/openclaw-test-state.js";
import { drainSessionStateForTest } from "../test-utils/session-state-cleanup.js";
import type { Logger } from "./service/state.js";
import { sweepCronRunSessions as sweepCronRunSessionsImpl } from "./session-reaper.js";
import { resetReaperThrottle } from "./session-reaper.test-support.js";
import { resetReaperThrottle, seedSessionEntries } from "./session-reaper.test-support.js";
const { listSessionEntriesCore, patchSessionEntryCore, replaceSessionEntry } = sessionAccessor;
const { explicitSqliteCloseReleasesNativeResources: keepsMaintenanceWorker } =
await initializeSqliteRuntimeCapabilities();
function sweepCronRunSessions(
params: Omit<Parameters<typeof sweepCronRunSessionsImpl>[0], "agentId">,
@ -45,15 +48,6 @@ function sweepCronRunSessions(
return sweepCronRunSessionsImpl({ ...params, agentId: "main" });
}
async function seedSessionEntries(
storePath: string,
entries: Record<string, SessionEntry>,
): Promise<void> {
for (const [sessionKey, entry] of Object.entries(entries)) {
await replaceSessionEntry({ agentId: "main", storePath, sessionKey }, entry);
}
}
function readSessionEntries(storePath: string): Record<string, SessionEntry> {
return Object.fromEntries(
listSessionEntriesCore({ agentId: "main", storePath }).map(({ sessionKey, entry }) => [
@ -426,7 +420,7 @@ describe("sweepCronRunSessions", () => {
let foregroundRead:
| ReturnType<typeof sessionEntryReader.readSessionEntriesFromStoreInWorker>
| undefined;
if (!process.versions.bun) {
if (keepsMaintenanceWorker) {
const closeResources = maintenanceLane.pool.closeResources.bind(maintenanceLane.pool);
vi.spyOn(maintenanceLane.pool, "closeResources").mockImplementationOnce((key) => {
const closing = closeResources(key);
@ -446,7 +440,7 @@ describe("sweepCronRunSessions", () => {
});
expect(result).toEqual({ swept: true, pruned: 1 });
if (!process.versions.bun) {
if (keepsMaintenanceWorker) {
expect(foregroundRead).toBeDefined();
expect(await foregroundRead).toMatchObject({
entries: [

View file

@ -28,6 +28,7 @@ import {
prewarmSessionHistoryWorker,
withSessionHistoryWorkerDatabase,
} from "../config/sessions/session-transcript-worker-runtime.js";
import { getSqliteRuntimeCapabilities } from "../infra/bun-sqlite-library.js";
import { DEFAULT_WORKER_PENDING_BYTES } from "../infra/worker-task-capacity.js";
import { KeyedAsyncQueue } from "../plugin-sdk/keyed-async-queue.js";
import { createDeferredCore } from "../shared/deferred.js";
@ -60,6 +61,7 @@ import { readChatHistoryMessageId } from "./session-history-tail.js";
const observed = vi.hoisted(() => ({
timers: vi.spyOn(globalThis, "setTimeout"),
workers: [] as Worker[],
idleCloseKeepsWorker: new WeakMap<Worker, boolean>(),
dispatch: undefined as ((message: unknown) => void) | undefined,
restoration: undefined as
| { sessionId: string; entered: () => void; wait: Promise<void> }
@ -70,6 +72,13 @@ vi.mock("node:worker_threads", async (importOriginal) => {
return {
...actual,
Worker: class extends actual.Worker {
constructor(...args: ConstructorParameters<typeof actual.Worker>) {
const { explicitSqliteCloseReleasesNativeResources } = getSqliteRuntimeCapabilities();
super(...args);
// Match the decision this generation inherits, before any later admission.
observed.idleCloseKeepsWorker.set(this, explicitSqliteCloseReleasesNativeResources);
}
override postMessage(...args: Parameters<Worker["postMessage"]>): void {
const kind = asOptionalRecord(asOptionalRecord(args[0])?.input)?.kind;
if (
@ -235,7 +244,7 @@ it("keeps fresh fixture roots isolated while reusing idle reader execution", asy
const worker = observed.workers.at(-1)!;
expect(worker).toBe(prewarmedWorker);
if (previousWorker) {
if (process.versions.bun) {
if (observed.idleCloseKeepsWorker.get(previousWorker) !== true) {
expect(previousWorker.threadId).toBe(-1);
expect(worker).not.toBe(previousWorker);
} else {
@ -345,12 +354,13 @@ it("settles cancelled message reads before reuse and closes their database handl
"cancel-message-read-message",
]);
expect(observed.workers.at(-1)).toBe(worker);
const keepsWorker = observed.idleCloseKeepsWorker.get(worker) === true;
await closeOpenClawAgentDatabaseByPathAsync(fixture.path, "main");
expect(worker.threadId).toBe(process.versions.bun ? -1 : threadId);
expect(worker.threadId).toBe(keepsWorker ? threadId : -1);
expect((await fixture.read()).messages.map(readChatHistoryMessageId)).toEqual([
"cancel-message-read-message",
]);
if (process.versions.bun) {
if (!keepsWorker) {
expect(observed.workers.at(-1)).not.toBe(worker);
} else {
expect(observed.workers.at(-1)).toBe(worker);
@ -536,6 +546,7 @@ it("closes idle A while active and queued B pages survive, then reads replaced A
await b.read();
const oldWorker = observed.workers.at(-1)!;
const threadId = oldWorker.threadId;
const keepsWorker = observed.idleCloseKeepsWorker.get(oldWorker) === true;
let closing: Promise<boolean> | undefined;
observed.dispatch = (message) => {
const input = asOptionalRecord(asOptionalRecord(message)?.input);
@ -552,7 +563,7 @@ it("closes idle A while active and queued B pages survive, then reads replaced A
["active-b-message"],
["queued-b-message"],
]);
expect(oldWorker.threadId).toBe(process.versions.bun ? -1 : threadId);
expect(oldWorker.threadId).toBe(keepsWorker ? threadId : -1);
// This also exercises Windows replacement while the unrelated agent remains usable.
fs.copyFileSync(a.path, `${a.path}.replacement`);
fs.renameSync(a.path, `${a.path}.previous`);
@ -571,7 +582,7 @@ it("closes idle A while active and queued B pages survive, then reads replaced A
"replacement-a-message",
]);
expect((await b.read()).messages.map(readChatHistoryMessageId)).toEqual(["active-b-message"]);
if (!process.versions.bun) {
if (keepsWorker) {
expect(observed.workers.at(-1)).toBe(oldWorker);
}
});
@ -600,6 +611,7 @@ it("evicts the least recently used of 64 retained targets without charging missi
expect(fs.existsSync(target.storePath)).toBe(false);
}
await targets[64]!.read();
const keepsWorker = observed.idleCloseKeepsWorker.get(worker) === true;
const closeResources = vi.spyOn(historyLane.pool, "closeResources");
try {
// The evicted target has no retained native custody; the hot target still does.
@ -607,8 +619,8 @@ it("evicts the least recently used of 64 retained targets without charging missi
expect(worker.threadId).toBe(threadId);
expect(closeResources).not.toHaveBeenCalled();
await closeOpenClawAgentDatabaseByPathAsync(targets[0]!.path, "retained-0");
expect(worker.threadId).toBe(process.versions.bun ? -1 : threadId);
if (!process.versions.bun) {
expect(worker.threadId).toBe(keepsWorker ? threadId : -1);
if (keepsWorker) {
expect(closeResources).toHaveBeenCalledWith(JSON.stringify([{ path: targets[0]!.path }]));
}
} finally {

View file

@ -3,7 +3,6 @@ import { request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
import { inspect } from "node:util";
import { describe, expect, it } from "vitest";
import { withEnv } from "../../test-utils/env.js";
import { createNodeProxyAgent, resolveEnvNodeProxyUrlForTarget } from "./node-proxy-agent.js";
const PROXY_ENV_KEYS = [
@ -21,11 +20,23 @@ function withProxyEnv<T>(
env: Partial<Record<(typeof PROXY_ENV_KEYS)[number], string | undefined>>,
fn: () => T,
): T {
const clearedEnv = Object.fromEntries(PROXY_ENV_KEYS.map((key) => [key, undefined])) as Record<
(typeof PROXY_ENV_KEYS)[number],
undefined
>;
return withEnv({ ...clearedEnv, ...env }, fn);
const previousEnv = process.env;
const scopedEnv = { ...previousEnv };
for (const key of PROXY_ENV_KEYS) {
const value = env[key];
if (value === undefined) {
delete scopedEnv[key];
} else {
scopedEnv[key] = value;
}
}
// These agents consume JS env values; keep their fixtures out of Bun's native fetch proxy cache.
process.env = scopedEnv;
try {
return fn();
} finally {
process.env = previousEnv;
}
}
describe("resolveEnvNodeProxyUrlForTarget", () => {

View file

@ -80,6 +80,34 @@ describe("diagnostic memory", () => {
resetLogger();
});
it("defers the default heap probe until a sample needs it and then reuses it", async () => {
vi.resetModules();
const getHeapStatistics = vi.fn(() => ({ heap_size_limit: 4 * 1024 ** 3 }));
vi.doMock("node:v8", async (importOriginal) => ({
...(await importOriginal<typeof import("node:v8")>()),
getHeapStatistics,
}));
try {
const { emitDiagnosticMemorySample: sample } = await import("./diagnostic-memory.js");
expect(getHeapStatistics).not.toHaveBeenCalled();
const options = { memoryUsage: memoryUsage({}), emitSample: false };
for (const heapSizeLimitBytes of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
sample({ ...options, heapSizeLimitBytes });
}
expect(getHeapStatistics).not.toHaveBeenCalled();
sample(options);
expect(getHeapStatistics).toHaveBeenCalledTimes(1);
sample({ ...options, heapSizeLimitBytes: 8 * 1024 ** 3 });
sample(options);
expect(getHeapStatistics).toHaveBeenCalledTimes(1);
} finally {
vi.doUnmock("node:v8");
vi.resetModules();
}
});
it("emits pressure when RSS crosses a threshold", () => {
const events: DiagnosticEventPayload[] = [];
const stop = onDiagnosticEvent((event) => events.push(event));

View file

@ -27,7 +27,8 @@ const DEFAULT_GROWTH_WINDOW_MS = 10 * 60 * 1000;
const DEFAULT_PRESSURE_REPEAT_MS = 5 * 60 * 1000;
const BYTE_UNITS = ["B", "KiB", "MiB", "GiB", "TiB"] as const;
const DEFAULT_HEAP_SIZE_LIMIT_BYTES = getHeapStatistics().heap_size_limit;
// Bun's compatibility probe walks the heap; importing diagnostics should not trigger it.
let defaultHeapSizeLimitBytes: number | undefined;
const DEFAULT_PROCESS_MEMORY_LIMIT_BYTES = process.constrainedMemory();
const DEFAULT_PHYSICAL_MEMORY_BYTES = totalmem();
const DEFAULT_IS_BUN_RUNTIME = typeof process.versions.bun === "string";
@ -403,7 +404,8 @@ export function emitDiagnosticMemorySample(options?: {
const current = { ts: now, memory };
const thresholds = resolveThresholds(
options?.thresholds,
options?.heapSizeLimitBytes ?? DEFAULT_HEAP_SIZE_LIMIT_BYTES,
options?.heapSizeLimitBytes ??
(defaultHeapSizeLimitBytes ??= getHeapStatistics().heap_size_limit),
options?.processMemoryLimitBytes ?? DEFAULT_PROCESS_MEMORY_LIMIT_BYTES,
options?.physicalMemoryBytes ?? DEFAULT_PHYSICAL_MEMORY_BYTES,
options?.isBunRuntime ?? DEFAULT_IS_BUN_RUNTIME,

View file

@ -76,7 +76,13 @@ describe("spawn broker resource socket", () => {
const large = { bytes: Buffer.alloc(2 * 1024 * 1024 + 17, 73), nested: { count: 42n } };
await Promise.all([first.send(large), first.send(undefined), first.send("after")]);
await completed.promise;
expect(messages).toEqual([large, undefined, "after"]);
expect(messages).toEqual([{ ...large, bytes: expect.any(Buffer) }, undefined, "after"]);
const received = messages[0];
if (!received || typeof received !== "object" || !("bytes" in received)) {
throw new Error("Missing echoed broker payload");
}
// Compare every byte without treating the multi-megabyte Buffer as object properties.
expect(Buffer.isBuffer(received.bytes) && received.bytes.equals(large.bytes)).toBe(true);
});
it("accepts split headers and coalesced records, then reports actual peer EOF once", async () => {

View file

@ -7,7 +7,10 @@ describe("generated extension asset lint planning", () => {
const generatedAsset = "extensions/canvas/src/host/a2ui/a2ui.bundle.js";
const extensionTest = "extensions/canvas/scripts/bundle-a2ui.test.ts";
const result = detectChangedLanes([generatedAsset, extensionTest]);
const plan = createChangedCheckPlan(result, { env: { PATH: "/usr/bin" } });
const plan = createChangedCheckPlan(result, {
lintOnly: true,
env: { PATH: "/usr/bin" },
});
expect(result.lanes.extensionTests).toBe(true);
expect(plan.commands).toContainEqual(

View file

@ -27,6 +27,7 @@ import {
runShardPlans,
} from "../../scripts/ci-run-node-test-shard.mts";
import { encodeNodeTestGroups } from "../../scripts/lib/ci-node-test-groups-codec.mts";
import nativeBunQualification from "../../scripts/lib/ci-test-native-bun-qualification.json" with { type: "json" };
import {
ciTestShardRequiresBun,
resolveCiTestRuntimeSelections,
@ -38,6 +39,29 @@ import * as workerOwner from "../../scripts/lib/vitest-worker-run.mts";
import * as groupOwner from "../../scripts/vitest-process-group.mts";
import { createDeferred, withTestTimeout } from "../helpers/promise.js";
const nativeQualificationFixtures = vi.hoisted(() => ({
first: "packages/markdown-core/src/chunk-text.test.ts",
second: "src/utils/chunk-items.test.ts",
helperTest: "src/agents/embedded-agent-runner/run/compaction-timeout.test.ts",
helper: "src/agents/test-helpers/agent-message-fixtures.ts",
setup: "test/setup.env.ts",
}));
vi.mock("../../scripts/lib/ci-test-native-bun-qualification.json", async () => {
const { createHash } = await import("node:crypto");
const { readFileSync: readQualificationSource } = await import("node:fs");
const hash = (file: string) =>
createHash("sha256").update(readQualificationSource(file)).digest("hex");
const { first, second, helperTest, helper, setup } = nativeQualificationFixtures;
return {
default: {
setup: { [setup]: hash(setup) },
tests: Object.fromEntries([first, second, helperTest].map((file) => [file, hash(file)])),
helpers: { [helperTest]: { [helper]: hash(helper) } },
},
};
});
vi.mock("node:child_process", async (importOriginal) => ({
...(await importOriginal<typeof import("node:child_process")>()),
}));
@ -50,7 +74,9 @@ const bunConfig = "test/vitest/vitest.unit-fast.config.ts";
const unitConfig = "test/vitest/vitest.unit.config.ts";
const unitSrcConfig = "test/vitest/vitest.unit-src.config.ts";
const libraryTarget = "src/library.test.ts";
const bunTarget = "packages/markdown-core/src/chunk-text.test.ts";
const bunTarget = nativeQualificationFixtures.first;
const vitestBunTarget = "src/agents/sandbox/docker.execDockerRaw.enoent.test.ts";
const nativeBunTarget = nativeQualificationFixtures.second;
const nodeTarget = "test/scripts/update-restart-module-outcome.test.ts";
const agentsSupportConfig = "test/vitest/vitest.agents-support.config.ts";
const worktreeRecoveryTarget = "src/agents/worktrees/service.removal-recovery.test.ts";
@ -246,9 +272,9 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
vi.spyOn(groupOwner, "shouldUseDetachedVitestProcessGroup").mockReturnValue(true);
const includePatterns = [
"ui/src/pages/chat/chat-pane-history.test.ts",
"ui/src/pages/chat/chat-pane-retained-presentation.test.ts",
"ui/src/pages/chat/chat-thread.test.ts",
"ui/src/pages/usage/usage-page-details.test.ts",
"ui/src/pages/chat/chat-pane-retention.test.ts",
"ui/src/pages/chat/chat-thread-retention.test.ts",
"ui/src/pages/usage/usage-page-retention.test.ts",
];
const seen: Array<{ runtime: string | undefined; membership?: string[] }> = [];
await expect(
@ -553,18 +579,27 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
});
it.each([
{ node: 7, bun: 0, expected: 7 },
{ node: 0, bun: 9, expected: 9 },
{ node: 7, bun: 9, expected: 7 },
])("finishes both runtimes and retains the first failure (node=$node, bun=$bun)", async (row) => {
const runChild = vi.fn(async (_args: string[], env: NodeJS.ProcessEnv) =>
env.OPENCLAW_VITEST_RUNTIME === "bun" ? row.bun : row.node,
{ node: 7, bun: 0, native: 0, expected: 7 },
{ node: 0, bun: 9, native: 0, expected: 9 },
{ node: 7, bun: 9, native: 11, expected: 7 },
{ node: 0, bun: 0, native: 11, expected: 11 },
])("finishes every release engine and retains its first failure: %s", async (row) => {
const runChild = vi.fn(async (args: string[], env: NodeJS.ProcessEnv) =>
args[0] === "--native-bun"
? row.native
: env.OPENCLAW_VITEST_RUNTIME === "bun"
? row.bun
: row.node,
);
await expect(
runShardPlans(
resolveShardPlans({
OPENCLAW_NODE_TEST_GROUPS_JSON: JSON.stringify([
{ configs: [bunConfig], shard_name: "eligible" },
{
configs: [bunConfig],
shard_name: "eligible",
includePatterns: [nodeTarget, vitestBunTarget, nativeBunTarget],
},
{ configs: ["later.config.ts"], shard_name: "later" },
]),
}),
@ -579,10 +614,15 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
expect(runChild.mock.calls.map(([, env]) => env.OPENCLAW_VITEST_RUNTIME)).toEqual([
"node",
"bun",
"bun",
]);
expect(
new Set(runChild.mock.calls.map(([, env]) => env.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT)).size,
new Set(
runChild.mock.calls.slice(0, 2).map(([, env]) => env.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT),
).size,
).toBe(2);
expect(runChild.mock.calls[2]?.[0]).toEqual(["--native-bun", `./${nativeBunTarget}`]);
expect(runChild.mock.calls[2]?.[1].OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT).toBeUndefined();
});
it("rejects an invalid runtime policy before scheduling any child", async () => {
@ -606,30 +646,34 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
async ({ policy, vitestArgs }) => {
const skippedOnBun = "src/process/spawn-broker/cleanup.test.ts";
const v8HeapTest = "src/infra/worker-task-pool.memory.test.ts";
const pluginRetentionTest = "src/plugins/runtime.retention.test.ts";
const nodeHistoryBenchmark = "test/scripts/bench-session-history.test.ts";
const nativeCompilerTest = "test/scripts/native-typescript.test.ts";
const compilerGraphTest = "test/scripts/ts-topology.test.ts";
const mixedCompilerTest = "src/plugin-sdk/provider-tools.test.ts";
const missingDockerTest = "src/agents/sandbox/docker.execDockerRaw.enoent.test.ts";
const nodeFiles = [
skippedOnBun,
v8HeapTest,
const bunVitestFiles = [
"src/agents/code-mode-node.test.ts",
nodeHistoryBenchmark,
];
const bunFiles = [
bunTarget,
missingDockerTest,
vitestBunTarget,
nativeCompilerTest,
compilerGraphTest,
mixedCompilerTest,
"src/plugins/runtime.retention.test.ts",
pluginRetentionTest,
"src/process/spawn-broker/proxy-retention.test.ts",
"src/auto-reply/reply/get-reply.imports.test.ts",
"src/plugin-sdk/provider-catalog-shared.cancellation.test.ts",
"src/plugin-sdk/provider-catalog-shared.retention.test.ts",
].toSorted();
const includePatterns = [...bunFiles, ...nodeFiles];
const nodeFiles = [
skippedOnBun,
v8HeapTest,
nodeHistoryBenchmark,
"packages/markdown-core/src/render-aware-chunking.test.ts",
];
const nativeFiles = vitestArgs.length ? [] : [bunTarget, nativeBunTarget];
const bunFiles = nativeFiles.length
? bunVitestFiles
: [bunTarget, ...bunVitestFiles, nativeBunTarget].toSorted();
const includePatterns = [bunTarget, ...bunVitestFiles, nativeBunTarget, ...nodeFiles];
const shard = {
configs: [bunConfig],
includePatterns,
@ -653,13 +697,17 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
env: { OPENCLAW_CI_TEST_RUNTIME_POLICY: policy },
scratchDir: makeScratchDir(),
runChild: async (args, env, label, timing) => {
expect(args).toEqual([
bunConfig,
...(vitestArgs.length ? ["--", ...vitestArgs] : []),
]);
const native = label.startsWith("bun-native:");
expect(args).toEqual(
native
? ["--native-bun", ...nativeFiles.map((file) => `./${file}`)]
: [bunConfig, ...(vitestArgs.length ? ["--", ...vitestArgs] : [])],
);
seen.push({
runtime: env.OPENCLAW_VITEST_RUNTIME,
includes: JSON.parse(readFileSync(env.OPENCLAW_VITEST_INCLUDE_FILE!, "utf8")),
includes: native
? args.slice(1).map((file) => file.slice(2))
: JSON.parse(readFileSync(env.OPENCLAW_VITEST_INCLUDE_FILE!, "utf8")),
label,
timing,
});
@ -677,6 +725,16 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
timing: `${nodePrefix}partition`,
},
{ runtime: "bun", includes: bunFiles, label: "bun:partition", timing: "bun:partition" },
...(nativeFiles.length
? [
{
runtime: "bun",
includes: nativeFiles,
label: "bun-native:partition",
timing: "bun-native:partition",
},
]
: []),
]);
expect(new Set(seen.flatMap(({ includes }) => includes))).toEqual(new Set(includePatterns));
},
@ -739,7 +797,7 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
runtime: "node",
includePatterns: ["packages/markdown-core/src/render-aware-chunking.test.ts"],
},
{ runtime: "bun", includePatterns: [bunTarget] },
{ runtime: "bun", engine: "bun-test", files: [bunTarget] },
]);
expect(
resolveCiTestRuntimeSelections(
@ -747,6 +805,91 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
"bun-compatible",
),
).toEqual([{ runtime: "node" }]);
expect(
resolveCiTestRuntimeSelections(
{
configs: [bunConfig],
includePatterns: ["src/utils/{chunk-items,not-a-real-test}.test.ts"],
},
"bun-compatible",
),
).toEqual([{ runtime: "bun", engine: "bun-test", files: [nativeBunTarget] }]);
});
it("keeps exact native admission and Vitest-only arguments separate", () => {
const selection = { targets: [nativeBunTarget] };
expect(resolveCiTestRuntimeSelections(selection, "node")).toEqual([{ runtime: "node" }]);
expect(resolveCiTestRuntimeSelections(selection, "dual")).toEqual([
{ runtime: "node" },
{ runtime: "bun", engine: "bun-test", files: [nativeBunTarget] },
]);
expect(ciTestShardRequiresBun(selection, "bun-compatible")).toBe(true);
for (const vitestArgs of [
["--testNamePattern=(?!)"],
["--maxWorkers=1"],
["--testTimeout=120000"],
["--hookTimeout=180000"],
[`--exclude=${nativeBunTarget}`],
]) {
expect(
resolveCiTestRuntimeSelections({ ...selection, vitestArgs }, "bun-compatible"),
).toEqual([{ runtime: "bun" }]);
}
expect(
resolveCiTestRuntimeSelections(
{
targets: [nativeBunTarget, vitestBunTarget],
},
"bun-compatible",
),
).toEqual([{ runtime: "bun" }]);
expect(
resolveCiTestRuntimeSelections(
{
configs: [bunConfig],
includePatterns: ["src/infra/plain-object.test.ts"],
},
"bun-compatible",
),
).toEqual([{ runtime: "bun", includePatterns: ["src/infra/plain-object.test.ts"] }]);
});
it("keeps changed qualification inputs on Vitest without dropping their coverage", () => {
const cwd = makeScratchDir();
const { helperTest, helper, setup } = nativeQualificationFixtures;
const files = [bunTarget, helperTest, nativeBunTarget];
const copyInput = (file: string) => {
mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true });
writeFileSync(path.join(cwd, file), readFileSync(file));
};
for (const file of [...Object.keys(nativeBunQualification.setup), helper, ...files]) {
copyInput(file);
}
const selection = { configs: [bunConfig], includePatterns: files };
expect(resolveCiTestRuntimeSelections(selection, "bun-compatible", cwd)).toEqual([
{ runtime: "bun", engine: "bun-test", files },
]);
writeFileSync(path.join(cwd, "src/utils/chunk-items.ts"), "export const changed = true;\n");
expect(resolveCiTestRuntimeSelections(selection, "bun-compatible", cwd)).toEqual([
{ runtime: "bun", engine: "bun-test", files },
]);
writeFileSync(path.join(cwd, nativeBunTarget), `${readFileSync(nativeBunTarget, "utf8")}\n`);
expect(resolveCiTestRuntimeSelections(selection, "dual", cwd)).toEqual([
{ runtime: "node" },
{ runtime: "bun", includePatterns: [nativeBunTarget] },
{ runtime: "bun", engine: "bun-test", files: [bunTarget, helperTest] },
]);
writeFileSync(path.join(cwd, helper), "// changed fixture helper\n");
expect(resolveCiTestRuntimeSelections(selection, "dual", cwd)).toEqual([
{ runtime: "node" },
{ runtime: "bun", includePatterns: [helperTest, nativeBunTarget] },
{ runtime: "bun", engine: "bun-test", files: [bunTarget] },
]);
writeFileSync(path.join(cwd, setup), "// changed setup\n");
expect(resolveCiTestRuntimeSelections(selection, "dual", cwd)).toEqual([
{ runtime: "node" },
{ runtime: "bun", includePatterns: files },
]);
});
it.each(["bun-compatible", "dual"] as const)(
@ -1904,7 +2047,7 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
},
);
it("keeps Bun timings separate from Node membership timing spans", async () => {
it("keeps native Bun, Bun Vitest and Node membership timing spans separate", async () => {
const timingKey =
"agentic-agents-support#selector-2-aaaa#generation-bbbb#part-1-of-2#include-1-cccc";
const lines: string[] = [];
@ -1924,9 +2067,12 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
runChild: async (_args, _childEnv, label, spanKey) => {
lines.push(`2026-08-27T23:00:00Z [shard:${spanKey}] begin`);
lines.push(`2026-08-27T23:00:01Z [shard:${label}] child output`);
lines.push(
`2026-08-27T23:00:${spanKey.startsWith("bun:") ? "03" : "10"}Z [shard:${spanKey}] end (exit 0)`,
);
const seconds = spanKey.startsWith("bun-native:")
? "02"
: spanKey.startsWith("bun:")
? "03"
: "10";
lines.push(`2026-08-27T23:00:${seconds}Z [shard:${spanKey}] end (exit 0)`);
return 0;
},
scratchDir: makeScratchDir(),
@ -1945,6 +2091,9 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
expect(refitTestTimings(runs).timings.compactGroupSeconds.blacksmith[`bun:${timingKey}`]).toBe(
3,
);
expect(
refitTestTimings(runs).timings.compactGroupSeconds.blacksmith[`bun-native:${timingKey}`],
).toBe(2);
});
it.each([

View file

@ -18,14 +18,14 @@ vi.mock(import("../../scripts/lib/ci-node-test-plan.mts"), async (importOriginal
...actual,
createVitestCacheWarmGroups: vi.fn((profile: "full" | "hybrid-hosted" = "full") => {
const hosted = actual.createVitestCacheWarmGroups("hybrid-hosted");
// Keep one compatible file beside a Node-only file and a tooling file.
// Keep one compatible file beside a V8-only file and a tooling file.
// The planner suite owns the production inventory; this proves partitioning.
const tooling = {
...hosted[0]!,
configs: ["test/vitest/vitest.unit-fast.config.ts", "test/vitest/vitest.tooling.config.ts"],
includePatterns: [
"packages/media-core/src/mime.test.ts",
"packages/markdown-core/src/render-aware-chunking.test.ts",
"src/infra/worker-task-pool.memory.test.ts",
"test/scripts/ci-workflow-guards.test.ts",
],
};

View file

@ -5313,9 +5313,9 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
expect(includeFile).toBeTruthy();
const included = JSON.parse(readFileSync(includeFile!, "utf8"));
const retentionFiles = [
"ui/src/pages/chat/chat-pane-retained-presentation.test.ts",
"ui/src/pages/chat/chat-thread.test.ts",
"ui/src/pages/usage/usage-page-details.test.ts",
"ui/src/pages/chat/chat-pane-retention.test.ts",
"ui/src/pages/chat/chat-thread-retention.test.ts",
"ui/src/pages/usage/usage-page-retention.test.ts",
];
expect(included.length).toBeGreaterThan(1000);
expect(included).toEqual(expect.arrayContaining(retentionFiles));

View file

@ -305,16 +305,15 @@ posixSerialDescribe("compiled subprocess preparation progress", { concurrent: fa
await releaseCompiler.promise;
return runManaged({ ...options, args: controlled.args(generation) });
});
const readFile = fs.promises.readFile.bind(fs.promises);
const verificationRead = vi
.spyOn(fs.promises, "readFile")
.mockImplementation(async (...args) => {
if (args[0] === heldOutput) {
verificationStarted.resolve();
await releaseVerification.promise;
}
return readFile(...args);
});
const readFile = fs.readFile.bind(fs);
const verificationRead = vi.spyOn(fs, "readFile").mockImplementation((...args) => {
if (args[0] === heldOutput) {
verificationStarted.resolve();
void releaseVerification.promise.then(() => readFile(...args));
return;
}
return readFile(...args);
});
const borrower = path.join(directory, "vitest.mjs");
fs.writeFileSync(
borrower,

View file

@ -1,7 +1,9 @@
import { spawnSync } from "node:child_process";
import { spawn, spawnSync } from "node:child_process";
import { EventEmitter } from "node:events";
import fs from "node:fs";
import nodePath from "node:path";
import { createInterface } from "node:readline";
import { finished } from "node:stream/promises";
import { fileURLToPath } from "node:url";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { parseCLI } from "vitest/node";
@ -13,7 +15,7 @@ import {
resolveVitestNodeArgs,
resolveVitestNoOutputTimeoutMs,
} from "../../scripts/lib/vitest-process-env.mts";
import { resolveVitestTestCommand } from "../../scripts/lib/vitest-test-runtime.mts";
import * as testRuntime from "../../scripts/lib/vitest-test-runtime.mts";
import {
createVitestUnhandledErrorDetector,
writeVitestUnhandledErrorSummary,
@ -26,17 +28,21 @@ import {
resolveMissingExplicitTestFiles,
resolveTestProjectsDelegationArgs,
resolveVitestSpawnParams,
runVitest,
spawnWatchedVitestProcess,
shouldSuppressVitestStderrLine,
} from "../../scripts/run-vitest.mts";
import { parseTestProjectsArgs } from "../../scripts/test-projects.test-support.mts";
import { forceKillVitestProcessGroup } from "../../scripts/vitest-process-group.mts";
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
import { listGitTrackedFiles } from "../../src/test-utils/repo-files.js";
import { isProcessAlive } from "../helpers/process-wait.js";
import { awaitGateBeforeSettlement, withinTest } from "../helpers/promise.js";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
import { isGatewayServerTestFile } from "../vitest/vitest.gateway-server-paths.mjs";
const posixIt = process.platform === "win32" ? it.skip : it;
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const file = "test/scripts/run-vitest.test.ts";
const toolingConfig = "test/vitest/vitest.tooling.config.ts";
const e2eConfig = "test/vitest/vitest.e2e.config.ts";
@ -76,9 +82,20 @@ describe("scripts/run-vitest", () => {
);
});
it.each([undefined, "bun"])(
it.each([undefined, "node", "bun"] as const)(
"selects the %s test runtime without changing the compiled bootstrap or test operands",
(runtime) => {
async (runtime) => {
const directory = tempDirs.make("oc-test-runtime-cache-");
const env = Object.freeze({
OPENCLAW_VITEST_RUNTIME: runtime,
NODE_COMPILE_CACHE: nodePath.join(directory, "node-cache"),
NODE_COMPILE_CACHE_PORTABLE: "1",
NODE_DISABLE_COMPILE_CACHE: "0",
TMPDIR: directory,
TMP: directory,
TEMP: directory,
});
const parentCacheDisabled = process.env.NODE_DISABLE_COMPILE_CACHE;
const operands = [
"scripts/lib/vitest-worker-bootstrap.mts",
"/compiled/generation",
@ -88,12 +105,9 @@ describe("scripts/run-vitest", () => {
"--no-maglev",
];
const flags = ["--no-maglev", "--no-concurrent-sparkplug"];
expect(
resolveVitestTestCommand([...flags, ...operands], {
OPENCLAW_VITEST_RUNTIME: runtime,
}),
).toEqual({
expect(testRuntime.resolveVitestTestCommand([...flags, ...operands], env)).toEqual({
command: runtime === "bun" ? "bun" : process.execPath,
...(runtime === "bun" ? { envOverrides: { NODE_DISABLE_COMPILE_CACHE: "1" } } : {}),
args:
runtime === "bun"
? [
@ -103,6 +117,40 @@ describe("scripts/run-vitest", () => {
]
: [...flags, ...operands],
});
if (runtime === undefined) {
return;
}
const resolveCommand = testRuntime.resolveVitestTestCommand;
// Keep the real runtime policy while an inert child observes the delivered environment.
const command = vi
.spyOn(testRuntime, "resolveVitestTestCommand")
.mockImplementation((...args) => ({
...resolveCommand(...args),
command: resolveTestNodeExecPath(),
args: [
"-e",
[
'const assert = require("node:assert/strict");',
`assert.equal(process.env.NODE_DISABLE_COMPILE_CACHE, ${JSON.stringify(runtime === "bun" ? "1" : "0")});`,
`assert.equal(process.env.NODE_COMPILE_CACHE, ${JSON.stringify(env.NODE_COMPILE_CACHE)});`,
'assert.equal(process.env.NODE_COMPILE_CACHE_PORTABLE, "1");',
"process.exitCode = 7;",
].join("\n"),
],
}));
try {
const watched = spawnWatchedVitestProcess({
pnpmArgs: ["exec", "node", ...flags, ...operands],
spawnParams: { ...resolveVitestSpawnParams(env), stdio: ["ignore", "pipe", "pipe"] },
env,
homeMode: "hermetic",
});
expect((await watched.completion).code).toBe(7);
expect(env.NODE_DISABLE_COMPILE_CACHE).toBe("0");
expect(process.env.NODE_DISABLE_COMPILE_CACHE).toBe(parentCacheDisabled);
} finally {
command.mockRestore();
}
},
);
@ -118,12 +166,217 @@ describe("scripts/run-vitest", () => {
it("keeps native preparation tools on Node when tests select Bun", () => {
const args = ["--import", "tsx", "scripts/ensure-playwright-chromium.mts"];
expect(resolveVitestTestCommand(args, { OPENCLAW_VITEST_RUNTIME: "bun" })).toEqual({
expect(testRuntime.resolveVitestTestCommand(args, { OPENCLAW_VITEST_RUNTIME: "bun" })).toEqual({
command: process.execPath,
args,
});
});
it.each(
[
[],
["test/scripts/run-vitest.test.ts"],
["./test/scripts"],
["./test/scripts/*.test.ts"],
["./test/scripts/run-vitest.test.ts", "--timeout=1"],
["./test/scripts/run-vitest.test.ts:1"],
["./../outside.test.ts"],
["./test/scripts/missing-native-bun-file.test.ts"],
].map((files) => ({ files })),
)("rejects unbounded native Bun operands $files before launching", async ({ files }) => {
const command = vi.spyOn(testRuntime, "resolveNativeBunTestCommand");
try {
await expect(runVitest(vi.fn(), ["--native-bun", ...files], {})).rejects.toThrow(
/Native Bun test/u,
);
expect(command).not.toHaveBeenCalled();
} finally {
command.mockRestore();
}
});
it("refuses native Bun admission after its expected parent disconnected", async () => {
const connected = Object.getOwnPropertyDescriptor(process, "connected");
const command = vi.spyOn(testRuntime, "resolveNativeBunTestCommand");
Object.defineProperty(process, "connected", { configurable: true, value: false });
try {
await expect(
runVitest(vi.fn(), ["--native-bun", "./test/scripts/run-vitest.test.ts"], {
OPENCLAW_NATIVE_BUN_PARENT_IPC: "1",
}),
).rejects.toThrow("Native Bun test parent IPC disconnected before admission");
expect(command).not.toHaveBeenCalled();
} finally {
if (connected) {
Object.defineProperty(process, "connected", connected);
} else {
Reflect.deleteProperty(process, "connected");
}
command.mockRestore();
}
});
posixIt(
"runs the native Bun entrypoint with bounded arguments and preserves failure",
async () => {
const files = ["./test/scripts/run-vitest.test.ts"];
const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
const directory = tempDirs.make("oc-native-bun-cache-");
const env = Object.freeze({
NODE_COMPILE_CACHE: nodePath.join(directory, "node-cache"),
NODE_COMPILE_CACHE_PORTABLE: "1",
NODE_DISABLE_COMPILE_CACHE: "0",
TMPDIR: directory,
TMP: directory,
TEMP: directory,
});
const parentCacheDisabled = process.env.NODE_DISABLE_COMPILE_CACHE;
const expectedCommand = testRuntime.resolveNativeBunTestCommand(files);
expect(expectedCommand).toEqual({
command: "bun",
envOverrides: { NODE_DISABLE_COMPILE_CACHE: "1" },
args: [
"test",
"--no-env-file",
"--tsconfig-override",
nodePath.join(repoRoot, "tsconfig.json"),
"--preload",
nodePath.join(repoRoot, "test/setup.env.ts"),
"--timeout",
"120000",
...files,
],
});
const command = vi.spyOn(testRuntime, "resolveNativeBunTestCommand").mockReturnValue({
...expectedCommand,
command: resolveTestNodeExecPath(),
args: [
"-e",
[
'const assert = require("node:assert/strict");',
'assert.equal(process.env.NODE_DISABLE_COMPILE_CACHE, "1");',
`assert.equal(process.env.NODE_COMPILE_CACHE, ${JSON.stringify(env.NODE_COMPILE_CACHE)});`,
'assert.equal(process.env.NODE_COMPILE_CACHE_PORTABLE, "1");',
"process.exitCode = 7;",
].join("\n"),
],
});
const exitCode = process.exitCode;
try {
await runVitest(vi.fn(), ["--native-bun", ...files], env);
expect(command).toHaveBeenCalledWith(files);
expect(process.exitCode).toBe(7);
expect(env.NODE_DISABLE_COMPILE_CACHE).toBe("0");
expect(process.env.NODE_DISABLE_COMPILE_CACHE).toBe(parentCacheDisabled);
} finally {
process.exitCode = exitCode;
command.mockRestore();
}
},
);
posixIt("joins native Bun descendants when its CI parent disconnects", async ({ signal }) => {
const fixture = [
'const { spawn } = require("node:child_process");',
'const descendant = spawn(process.execPath, ["-e",',
' "setInterval(() => {}, 1000); process.send(process.pid);",',
'], { stdio: ["ignore", "ignore", "ignore", "ipc"] });',
'descendant.once("message", (pid) => {',
" descendant.disconnect();",
' process.stdout.write(JSON.stringify({ child: process.pid, descendant: pid }) + "\\n");',
"});",
"descendant.unref();",
"setInterval(() => {}, 1000);",
].join("\n");
const runtimeUrl = new URL("../../scripts/lib/vitest-test-runtime.mts", import.meta.url).href;
const replacement = `data:text/javascript,${encodeURIComponent(
`export function resolveNativeBunTestCommand() {
return { command: process.execPath, args: ["-e", ${JSON.stringify(fixture)}] };
}
export function resolveVitestTestCommand() { throw new Error("unexpected Vitest launch"); }`,
)}`;
const preload = `data:text/javascript,${encodeURIComponent(
`import { registerHooks } from "node:module";
registerHooks({ resolve(specifier, context, nextResolve) {
const resolved = nextResolve(specifier, context);
return resolved.url === ${JSON.stringify(runtimeUrl)}
? { url: ${JSON.stringify(replacement)}, shortCircuit: true }
: resolved;
} });`,
)}`;
const child = spawn(
process.versions.bun ? "node" : process.execPath,
[
"--import",
preload,
nodePath.resolve("scripts/run-vitest.mts"),
"--native-bun",
"./test/scripts/run-vitest.test.ts",
],
{
detached: true,
stdio: ["ignore", "pipe", "pipe", "ipc"],
env: { ...process.env, OPENCLAW_NATIVE_BUN_PARENT_IPC: "1" },
},
);
let stderr = "";
child.stderr!.setEncoding("utf8");
child.stderr!.on("data", (chunk: string) => {
stderr = (stderr + chunk).slice(-16_384);
});
// Explicit IPC disconnect can omit Node's aggregate close notification.
// Observe termination and drain both pipes independently instead.
const exited = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
(resolve, reject) => {
child.once("exit", (code, exitSignal) => resolve({ code, signal: exitSignal }));
child.once("error", reject);
},
);
const joined = Promise.all([exited, finished(child.stdout!), finished(child.stderr!)]).then(
([result]) => result,
);
const lines = createInterface({ input: child.stdout! });
let nativePid = 0;
let descendantPid = 0;
const ready = new Promise<void>((resolve, reject) => {
lines.once("line", (line) => {
try {
const pids = JSON.parse(line);
nativePid = pids.child;
descendantPid = pids.descendant;
expect(isProcessAlive(nativePid)).toBe(true);
expect(isProcessAlive(descendantPid)).toBe(true);
child.disconnect();
resolve();
} catch (error) {
reject(new Error("Failed to verify native Bun child readiness", { cause: error }));
}
});
lines.once("close", () => reject(new Error("native child closed before readiness")));
});
try {
const [, result] = await withinTest(
Promise.all([
awaitGateBeforeSettlement(ready, joined, "native child closed before readiness"),
joined,
]),
signal,
);
expect(result, stderr).toEqual({ code: null, signal: "SIGTERM" });
expect(child.stdout!.readableEnded).toBe(true);
expect(child.stderr!.readableEnded).toBe(true);
expect(isProcessGroupStopped(child.pid!)).toBe(true);
expect(isProcessGroupStopped(nativePid)).toBe(true);
} finally {
lines.close();
forceKillVitestProcessGroup(child);
if (nativePid) {
forceKillVitestProcessGroup({ pid: nativePid });
}
await joined;
}
});
it.each(["mjs", "mts"])(
"resolves dependencies before native parsing at the %s entrypoint",
(extension) => {
@ -532,134 +785,148 @@ registerHooks({resolve(specifier, context, nextResolve) {
});
posixIt.for([
{ timeout: false, exitCode: 0, expectedCode: 0 },
{ timeout: true, exitCode: 0, expectedCode: 1 },
{ timeout: true, exitCode: 7, expectedCode: 7 },
{ timeout: true, exitCode: null, expectedCode: null },
])("settles descendants (timeout=$timeout, child=$exitCode)", async (row, context) => {
const watchedEnv = {
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "5000",
};
let noOutputTimedOut = false;
// Only watchdog timers are fake; child I/O, diagnostics, and group joins stay real.
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
const { clock } = setTimeout as typeof setTimeout & { clock: { tick(ms: number): void } };
let watched: ReturnType<typeof spawnWatchedVitestProcess>;
try {
watched = spawnWatchedVitestProcess({
pnpmArgs: [
"exec",
"node",
"-e",
[
'const { spawn } = require("node:child_process");',
row.exitCode === null
? ""
: `process.once("SIGTERM", () => process.exit(${row.exitCode}));`,
'const descendant = spawn(process.execPath, ["-e",',
' "setInterval(() => {}, 1000); process.send(process.pid);",',
'], { stdio: ["ignore", "ignore", "ignore", "ipc"] });',
'descendant.once("message", (pid) => {',
" descendant.disconnect();",
" process.stdout.write(`${pid}\\n`);",
"});",
"descendant.unref();",
"setInterval(() => {}, 1000);",
].join("\n"),
],
spawnParams: {
detached: true,
{ timeout: false, exitCode: 0, expectedCode: 0, nativeBun: false },
{ timeout: true, exitCode: 0, expectedCode: 1, nativeBun: false },
{ timeout: true, exitCode: 7, expectedCode: 7, nativeBun: false },
{ timeout: true, exitCode: null, expectedCode: null, nativeBun: false },
{ timeout: true, exitCode: 0, expectedCode: 1, nativeBun: true },
])(
"settles descendants (timeout=$timeout, child=$exitCode, native=$nativeBun)",
async (row, context) => {
const watchedEnv = {
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "5000",
...(row.nativeBun ? { OPENCLAW_LIVE_TEST: "1", OPENCLAW_LIVE_USE_REAL_HOME: "1" } : {}),
};
let noOutputTimedOut = false;
// Only watchdog timers are fake; child I/O, diagnostics, and group joins stay real.
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
const { clock } = setTimeout as typeof setTimeout & { clock: { tick(ms: number): void } };
let watched: ReturnType<typeof spawnWatchedVitestProcess>;
const fixtureArgs = [
"-e",
[
'const { spawn } = require("node:child_process");',
row.exitCode === null
? ""
: `process.once("SIGTERM", () => process.exit(${row.exitCode}));`,
'const descendant = spawn(process.execPath, ["-e",',
' "setInterval(() => {}, 1000); process.send(process.pid);",',
'], { stdio: ["ignore", "ignore", "ignore", "ipc"] });',
'descendant.once("message", (pid) => {',
" descendant.disconnect();",
row.nativeBun
? ' process.stdout.write(JSON.stringify({ pid, home: process.env.HOME, tmp: process.env.TMPDIR, live: process.env.OPENCLAW_LIVE_TEST, realHome: process.env.OPENCLAW_LIVE_USE_REAL_HOME }) + "\\n");'
: " process.stdout.write(`${pid}\\n`);",
"});",
"descendant.unref();",
"setInterval(() => {}, 1000);",
].join("\n"),
];
const nativeCommand = row.nativeBun
? vi.spyOn(testRuntime, "resolveNativeBunTestCommand").mockReturnValue({
command: process.execPath,
args: fixtureArgs,
})
: undefined;
try {
watched = spawnWatchedVitestProcess({
...(row.nativeBun
? { nativeBunFiles: ["./test/scripts/run-vitest.test.ts"] }
: { pnpmArgs: ["exec", "node", ...fixtureArgs] }),
spawnParams: {
detached: true,
env: watchedEnv,
stdio: ["ignore", "pipe", "pipe"],
},
env: watchedEnv,
stdio: ["ignore", "pipe", "pipe"],
},
env: watchedEnv,
onNoOutputTimeout: () => {
noOutputTimedOut = true;
},
});
} finally {
vi.useRealTimers();
}
const rawExit = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
(resolve, reject) => {
watched.child.once("close", (code, closeSignal) => resolve({ code, signal: closeSignal }));
watched.child.once("error", reject);
},
);
let descendantPid = 0;
const lines = createInterface({ input: watched.child.stdout! });
const ready = new Promise<void>((resolve, reject) => {
lines.once("line", (line) => {
try {
// The descendant acknowledges its running loop on the watched pipe. Check
// and signal in this notification, before a delayed poll can outlive it.
descendantPid = Number(line);
expect(Number.isInteger(descendantPid) && descendantPid > 0).toBe(true);
expect(isProcessAlive(descendantPid)).toBe(true);
if (row.timeout) {
clock.tick(Number(watchedEnv.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS));
} else {
process.kill(watched.child.pid!, "SIGTERM");
}
resolve();
} catch (error) {
reject(error instanceof Error ? error : new Error(String(error)));
}
});
lines.once("close", () => reject(new Error("fixture closed before reporting readiness")));
});
try {
const snapshot = await withinTest(
Promise.all([
awaitGateBeforeSettlement(
ready,
watched.completion,
"fixture closed before reporting readiness",
),
rawExit,
watched.completion,
]).then(([, raw, result]) => {
const psArgs =
process.platform === "linux" ? ["-eL", "-o", "pgid=,state="] : ["-axo", "pgid=,state="];
const stateResult = spawnSync("ps", psArgs, {
encoding: "utf8",
});
const rows = stateResult.stdout
.split(/\r?\n/)
.filter(Boolean)
.map((line) => /^\s*(\d+)\s+(\S+)\s*$/.exec(line));
const groupStopped =
!stateResult.error &&
stateResult.signal === null &&
stateResult.stderr.trim() === "" &&
stateResult.status === 0 &&
rows.every(Boolean) &&
rows
.filter((processRow) => Number(processRow?.[1]) === watched.child.pid)
.every((processRow) => /^[ZX]/.test(processRow?.[2] ?? ""));
return { groupStopped, noOutputTimedOut, raw, result };
}),
context.signal,
);
const signal = row.exitCode === null ? "SIGTERM" : null;
expect(snapshot).toEqual({
groupStopped: true,
noOutputTimedOut: row.timeout,
raw: { code: row.exitCode, signal },
result: { code: row.expectedCode, signal, groupJoined: true },
});
} finally {
lines.close();
watched.teardown();
forceKillVitestProcessGroup(watched.child);
if (descendantPid && isProcessAlive(descendantPid)) {
process.kill(descendantPid, "SIGKILL");
onNoOutputTimeout: () => {
noOutputTimedOut = true;
},
});
} finally {
vi.useRealTimers();
nativeCommand?.mockRestore();
}
await watched.completion;
}
});
const rawExit = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
(resolve, reject) => {
watched.child.once("close", (code, closeSignal) =>
resolve({ code, signal: closeSignal }),
);
watched.child.once("error", reject);
},
);
let descendantPid = 0;
let nativeTempRoot: string | undefined;
const lines = createInterface({ input: watched.child.stdout! });
const ready = new Promise<void>((resolve, reject) => {
lines.once("line", (line) => {
try {
// The descendant acknowledges its running loop on the watched pipe. Check
// and signal in this notification, before a delayed poll can outlive it.
if (row.nativeBun) {
const state = JSON.parse(line);
descendantPid = state.pid;
nativeTempRoot = state.tmp;
expect(state.home).toBe(nodePath.join(state.tmp, "home"));
expect(fs.existsSync(state.home)).toBe(true);
expect(state.live).toBeUndefined();
expect(state.realHome).toBeUndefined();
} else {
descendantPid = Number(line);
}
expect(Number.isInteger(descendantPid) && descendantPid > 0).toBe(true);
expect(isProcessAlive(descendantPid)).toBe(true);
if (row.timeout) {
clock.tick(Number(watchedEnv.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS));
} else {
process.kill(watched.child.pid!, "SIGTERM");
}
resolve();
} catch (error) {
reject(error instanceof Error ? error : new Error(String(error)));
}
});
lines.once("close", () => reject(new Error("fixture closed before reporting readiness")));
});
try {
const snapshot = await withinTest(
Promise.all([
awaitGateBeforeSettlement(
ready,
watched.completion,
"fixture closed before reporting readiness",
),
rawExit,
watched.completion,
]).then(([, raw, result]) => {
const groupStopped = isProcessGroupStopped(watched.child.pid!);
return { groupStopped, noOutputTimedOut, raw, result };
}),
context.signal,
);
const signal = row.exitCode === null ? "SIGTERM" : null;
expect(snapshot).toEqual({
groupStopped: true,
noOutputTimedOut: row.timeout,
raw: { code: row.exitCode, signal },
result: { code: row.expectedCode, signal, groupJoined: true },
});
if (nativeTempRoot) {
expect(fs.existsSync(nativeTempRoot)).toBe(false);
}
} finally {
lines.close();
watched.teardown();
forceKillVitestProcessGroup(watched.child);
if (descendantPid && isProcessAlive(descendantPid)) {
process.kill(descendantPid, "SIGKILL");
}
await watched.completion;
}
},
);
it.each<{ env: NodeJS.ProcessEnv; expected: NodeJS.ProcessEnv }>([
{ env: { OPENCLAW_LOCAL_CHECK: "0" }, expected: { OPENCLAW_LOCAL_CHECK: "1" } },
@ -805,3 +1072,21 @@ registerHooks({resolve(specifier, context, nextResolve) {
});
});
});
function isProcessGroupStopped(pid: number) {
const psArgs =
process.platform === "linux" ? ["-eL", "-o", "pgid=,state="] : ["-axo", "pgid=,state="];
const stateResult = spawnSync("ps", psArgs, { encoding: "utf8" });
const rows = stateResult.stdout
.split(/\r?\n/u)
.filter(Boolean)
.map((line) => /^\s*(\d+)\s+(\S+)\s*$/u.exec(line));
return (
!stateResult.error &&
stateResult.signal === null &&
stateResult.stderr.trim() === "" &&
stateResult.status === 0 &&
rows.every(Boolean) &&
rows.filter((row) => Number(row?.[1]) === pid).every((row) => /^[ZX]/u.test(row?.[2] ?? ""))
);
}

View file

@ -158,8 +158,8 @@ it.for([
},
);
it.runIf(process.platform !== "win32").for(
["direct", "projects", "contracts-direct", "contracts-projects"].flatMap((route) =>
it.runIf(process.platform !== "win32").for([
...["direct", "projects", "contracts-direct", "contracts-projects"].flatMap((route) =>
(route.startsWith("contracts-")
? ["ready", "excluded"]
: [
@ -178,10 +178,12 @@ it.runIf(process.platform !== "win32").for(
).map((mode) => ({
route,
mode,
phase: "pre-spawn",
})),
),
)(
"$route runner owns pre-spawn worker preparation through $mode",
{ route: "batch", mode: "ready", phase: "lazy" },
])(
"$route runner owns $phase worker preparation through $mode",
({ route, mode }, { workerArtifacts }) =>
workerArtifacts.fixtureLifetime.run(async () => {
const selectedFile = route.startsWith("contracts-")
@ -203,6 +205,9 @@ it.runIf(process.platform !== "win32").for(
const compiled = path.join(directory, "compiled.jsonl");
const launched = path.join(directory, "launched.json");
const compilerReceipt = path.join(directory, "compiler.json");
const childCacheReceipt = path.join(directory, "child-cache.json");
const nodeCompileCache = path.join(directory, "node-compile-cache");
const expectedCache = { path: nodeCompileCache, portable: "1", disabled: null };
const canceled = path.join(directory, "canceled");
const input = writeFixture(directory, "input.mjs", "export const fixture = true;");
const compiler = writeFixture(
@ -212,7 +217,17 @@ it.runIf(process.platform !== "win32").for(
import fs from 'node:fs';
import {runWorkerFixtureCompiler} from ${JSON.stringify(new URL("./fixtures/vitest-worker-compiler.mjs", import.meta.url).href)};
const generation=process.argv[2];
fs.writeFileSync(${JSON.stringify(compilerReceipt)},JSON.stringify({pid:process.pid,generation}));
fs.writeFileSync(${JSON.stringify(compilerReceipt)},JSON.stringify({
pid:process.pid,generation,
...(${JSON.stringify(route === "batch")} ? {
runtime: process.versions.bun ? 'bun' : 'node',
cache: {
path: process.env.NODE_COMPILE_CACHE,
portable: process.env.NODE_COMPILE_CACHE_PORTABLE,
disabled: process.env.NODE_DISABLE_COMPILE_CACHE ?? null,
},
} : {}),
}));
if (${JSON.stringify(mode)}==='failure') process.exit(7);
if (${JSON.stringify(mode)}==='cancel') {
const watcher=fs.watch(${JSON.stringify(directory)},()=>{});
@ -246,6 +261,15 @@ import cp from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import {syncFixtureBuiltinExports} from ${JSON.stringify(new URL("./fixtures/ci-fixture-runtime.cjs", import.meta.url).href)};
const probeCache=${JSON.stringify(route === "batch")};
const cacheEnvironment=()=>({
path: process.env.NODE_COMPILE_CACHE,
portable: process.env.NODE_COMPILE_CACHE_PORTABLE,
disabled: process.env.NODE_DISABLE_COMPILE_CACHE ?? null,
});
if(probeCache && process.argv[1]===${JSON.stringify(leaf)}) {
fs.writeFileSync(${JSON.stringify(childCacheReceipt)},JSON.stringify(cacheEnvironment()));
}
const spawn=cp.spawn;
cp.spawn=(bin,args,options)=>{
if(args[0]===${JSON.stringify(path.join(root, "scripts/lib/vitest-worker-compiler.mts"))}) {
@ -256,8 +280,14 @@ cp.spawn=(bin,args,options)=>{
const generation=bootstrap<0?undefined:args[bootstrap+1];
fs.writeFileSync(${JSON.stringify(launched)},JSON.stringify({
prepared: Boolean(generation && fs.existsSync(path.join(generation,'manifest.json'))),
...(probeCache ? {
command: bin,
orchestratorRuntime: process.versions.bun ? 'bun' : 'node',
orchestratorCache: cacheEnvironment(),
} : {}),
}));
return spawn(bin,[${JSON.stringify(leaf)}],options);
// Keep the selected environment; this fixture proves delivery without requiring Bun.
return spawn(probeCache ? process.execPath : bin,[${JSON.stringify(leaf)}],options);
}
return spawn(bin,args,options);
};
@ -281,22 +311,33 @@ syncFixtureBuiltinExports();
fs.writeFileSync(leaf, "if(process.connected) process.disconnect();\n");
}
const args =
route === "direct" || route === "contracts-direct"
route === "batch"
? [
"scripts/run-vitest.mjs",
"run",
"--config",
...(mode === "channels" ? [channelsConfig] : [selectedConfig, selectedFile]),
...controls,
]
: [
"--import",
"./scripts/tsx.mjs",
"scripts/test-projects.mts",
selectedFile,
"--",
...controls,
];
writeFixture(
directory,
"batch.mts",
`import {runVitestBatch} from ${JSON.stringify(path.join(root, "scripts/lib/vitest-batch-runner.mts"))};
process.exitCode = await runVitestBatch({config:${JSON.stringify(infraConfig)},args:[${JSON.stringify(coreWorker)}],targets:[],env:process.env});`,
),
]
: route === "direct" || route === "contracts-direct"
? [
"scripts/run-vitest.mjs",
"run",
"--config",
...(mode === "channels" ? [channelsConfig] : [selectedConfig, selectedFile]),
...controls,
]
: [
"--import",
"./scripts/tsx.mjs",
"scripts/test-projects.mts",
selectedFile,
"--",
...controls,
];
const includeFile = mode.startsWith("include-")
? writeFixture(
directory,
@ -310,6 +351,14 @@ syncFixtureBuiltinExports();
OPENCLAW_VITEST_INCLUDE_FILE: includeFile,
OPENCLAW_E2E_USE_PREBUILT_DIST: "1",
...fixturePreloadEnv(preload, "node"),
...(route === "batch"
? {
OPENCLAW_VITEST_RUNTIME: "bun",
NODE_COMPILE_CACHE: nodeCompileCache,
NODE_COMPILE_CACHE_PORTABLE: "1",
NODE_DISABLE_COMPILE_CACHE: undefined,
}
: {}),
});
expect(result.code, result.stdout + result.stderr).toBe(
mode === "cancel" ? 143 : mode === "failure" ? 1 : 0,
@ -326,13 +375,30 @@ syncFixtureBuiltinExports();
expect(fs.existsSync(launched)).toBe(false);
} else {
expect(JSON.parse(fs.readFileSync(launched, "utf8"))).toEqual({
prepared: ready,
prepared: ready && route !== "batch",
...(route === "batch"
? {
command: "bun",
orchestratorRuntime: "node",
orchestratorCache: expectedCache,
}
: {}),
});
}
if (route === "batch") {
expect(JSON.parse(fs.readFileSync(childCacheReceipt, "utf8"))).toEqual({
...expectedCache,
disabled: "1",
});
}
if (prepared) {
const receipt = JSON.parse(fs.readFileSync(compilerReceipt, "utf8"));
expect(isProcessAlive(receipt.pid)).toBe(false);
expect(fs.existsSync(receipt.generation)).toBe(false);
if (route === "batch") {
expect(receipt.runtime).toBe("node");
expect(receipt.cache).toEqual(expectedCache);
}
if (ready) {
expect(fs.readFileSync(compiled, "utf8").trim().split("\n")).toHaveLength(1);
}

View file

@ -13,73 +13,104 @@ import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
it("keeps the runner event loop responsive while verifying a completed generation", async ({
signal,
}) => {
const directory = tempDirs.make("vitest-worker-verification-");
fs.mkdirSync(path.join(directory, "dist"));
const manifest: VitestWorkerManifest = {
identity: "verification-fixture",
inputs: {},
outputs: {},
durationMs: 0,
};
const source = "export const value = 1;\n";
const hash = hashVitestWorkerArtifact(source);
for (let index = 0; index < 64; index++) {
const input = path.join(directory, `input-${index}.ts`);
const output = `output-${index}.js`;
fs.writeFileSync(input, source);
fs.writeFileSync(path.join(directory, "dist", output), source);
manifest.inputs[input] = hash;
manifest.outputs[output] = hash;
}
const held = path.join(directory, "input-0.ts");
const started = createDeferred();
const release = createDeferred();
const readFile = fs.promises.readFile.bind(fs.promises);
const reader = vi.spyOn(fs.promises, "readFile").mockImplementation(async (...args) => {
if (args[0] === held) {
started.resolve();
await release.promise;
it.for(["filesystem", "microtask"] as const)(
"keeps the runner event loop responsive while verifying a completed generation (%s reads)",
async (completion, { signal }) => {
const directory = tempDirs.make("vitest-worker-verification-");
fs.mkdirSync(path.join(directory, "dist"));
const manifest: VitestWorkerManifest = {
identity: "verification-fixture",
inputs: {},
outputs: {},
durationMs: 0,
};
const source = "export const value = 1;\n";
const hash = hashVitestWorkerArtifact(source);
const files = new Set<string>();
for (let index = 0; index < 64; index++) {
const input = path.join(directory, `input-${index}.ts`);
const output = `output-${index}.js`;
const outputPath = path.join(directory, "dist", output);
fs.writeFileSync(input, source);
fs.writeFileSync(outputPath, source);
manifest.inputs[input] = hash;
manifest.outputs[output] = hash;
files.add(input);
files.add(outputPath);
}
return readFile(...args);
});
let completed = false;
// Supply the manifest so an asynchronous manifest read alone cannot satisfy
// the assertion: the source/artifact traversal itself must yield to I/O.
let verification: Promise<void> | undefined;
try {
verification = Promise.resolve(verifyVitestWorkerArtifacts(directory, manifest)).then(() => {
completed = true;
const held = path.join(directory, "input-0.ts");
const started = createDeferred();
const release = createDeferred();
const observedReads: string[] = [];
const readFile = fs.readFile.bind(fs);
const reader = vi.spyOn(fs, "readFile").mockImplementation((...args) => {
const [filename, callback] = args;
if (completion === "filesystem") {
if (filename === held) {
started.resolve();
void release.promise.then(() => readFile(...args));
return;
}
return readFile(...args);
}
if (typeof filename !== "string" || !files.has(filename)) {
return readFile(...args);
}
observedReads.push(filename);
queueMicrotask(() => callback(null, Buffer.from(source)));
});
await withinTest(
awaitGateBeforeSettlement(started.promise, verification, "verification bypassed async reads"),
signal,
);
await nextTurn();
expect(completed, "verification blocked the runner until every file was hashed").toBe(false);
} finally {
release.resolve();
let completed = false;
// Supply the manifest so an asynchronous manifest read alone cannot satisfy
// the assertion: the source/artifact traversal itself must yield to I/O.
let verification: Promise<void> | undefined;
try {
await verification;
verification = Promise.resolve(verifyVitestWorkerArtifacts(directory, manifest)).then(() => {
completed = true;
});
if (completion === "filesystem") {
await withinTest(
awaitGateBeforeSettlement(
started.promise,
verification,
"verification bypassed async reads",
),
signal,
);
}
await nextTurn();
expect(completed, "verification blocked the runner until every file was hashed").toBe(false);
} finally {
reader.mockRestore();
release.resolve();
try {
await verification;
} finally {
reader.mockRestore();
}
}
}
});
if (completion === "microtask") {
expect(observedReads.toSorted()).toEqual([...files].toSorted());
}
},
);
it.for(["inputs", "outputs"] as const)(
"drains active %s reads before failed verification releases the generation",
async (group, { signal }) => {
it.for([
{ group: "inputs", damage: "changed" },
{ group: "outputs", damage: "changed" },
{ group: "inputs", damage: "missing" },
{ group: "outputs", damage: "missing" },
] as const)(
"drains active $group reads before $damage verification releases the generation",
async ({ group, damage }, { signal }) => {
const owner = createVitestWorkerRun();
const directory = owner.descriptor.directory;
const files = group === "inputs" ? directory : path.join(directory, "dist");
fs.mkdirSync(files, { recursive: true });
const bad = path.join(files, "changed.js");
const held = path.join(files, "held.js");
fs.writeFileSync(bad, "changed");
if (damage === "changed") {
fs.writeFileSync(bad, "changed");
}
fs.writeFileSync(held, "expected");
const hash = hashVitestWorkerArtifact("expected");
const manifest: VitestWorkerManifest = {
@ -98,17 +129,21 @@ it.for(["inputs", "outputs"] as const)(
const started = createDeferred();
const failedRead = createDeferred();
const release = createDeferred();
const readFile = fs.promises.readFile.bind(fs.promises);
const reader = vi.spyOn(fs.promises, "readFile").mockImplementation(async (...args) => {
if (args[0] === held) {
const readFile = fs.readFile.bind(fs);
const reader = vi.spyOn(fs, "readFile").mockImplementation((...args) => {
const [filename, callback] = args;
if (filename === held) {
started.resolve();
await release.promise;
void release.promise.then(() => readFile(...args));
return;
}
const bytes = await readFile(...args);
if (args[0] === bad) {
if (filename !== bad) {
return readFile(...args);
}
readFile(filename, (error, bytes) => {
callback(error, bytes);
failedRead.resolve();
}
return bytes;
});
});
let completed = false;
let failure: unknown;
@ -141,7 +176,11 @@ it.for(["inputs", "outputs"] as const)(
group === "inputs"
? "Source changed during compiled subprocess invocation"
: "Compiled subprocess artifact changed";
expect(failure).toMatchObject({ message: expect.stringContaining(diagnostic) });
if (damage === "missing") {
expect(failure).toMatchObject({ code: "ENOENT" });
} else {
expect(failure).toMatchObject({ message: expect.stringContaining(diagnostic) });
}
expect(fs.existsSync(directory)).toBe(false);
},
);

View file

@ -269,15 +269,20 @@ describe("compiled worker content cache", () => {
await f.seed();
const probe = cachedProbe(f.root, f.directory);
f.nextInvocation();
const read = fs.promises.readFile.bind(fs.promises);
const read = fs.readFile.bind(fs);
let changed = false;
const reader = vi.spyOn(fs.promises, "readFile").mockImplementation(async (...args) => {
const bytes = await read(...args);
if (args[0] === probe && !changed) {
changed = true;
f.write("src/package.json", '{"type":"commonjs"}');
const reader = vi.spyOn(fs, "readFile").mockImplementation((...args) => {
if (args[0] !== probe) {
return read(...args);
}
return bytes;
const [filename, callback] = args;
read(filename, (error, bytes) => {
if (!error && !changed) {
changed = true;
f.write("src/package.json", '{"type":"commonjs"}');
}
callback(error, bytes);
});
});
try {
expect(await f.restore()).toBeUndefined();

View file

@ -245,12 +245,13 @@ const waitForRelease=()=>new Promise(resolve=>{
const poll=setInterval(check,50);
check();
});
const readFile=fsp.readFile;
fsp.readFile=async(filename,...args)=>{
const readFile=fs.readFile;
fs.readFile=(filename,...args)=>{
if(filename===input && !held && (!ciRoot || phase!=='admission') && (phase==='admission' || (phase==='disposal' && borrowerClosed))) {
held=true;
publish('verification-ready',{owner:process.pid});
await waitForRelease();
void waitForRelease().then(()=>readFile(filename,...args));
return;
}
return readFile(filename,...args);
};
@ -514,9 +515,9 @@ it("rejects a live borrower when its owner closes during verification", ({
const manifestFile = path.join(directory, "manifest.json");
const started = createDeferred();
const release = createDeferred();
const readFile = fs.promises.readFile.bind(fs.promises);
const readFile = fs.readFile.bind(fs);
let held = false;
const reader = vi.spyOn(fs.promises, "readFile").mockImplementation(async (...args) => {
const reader = vi.spyOn(fs, "readFile").mockImplementation((...args) => {
const filename = args[0];
if (
!held &&
@ -528,7 +529,8 @@ it("rejects a live borrower when its owner closes during verification", ({
if (Object.hasOwn(manifest.inputs, filename)) {
held = true;
started.resolve();
await release.promise;
void release.promise.then(() => readFile(...args));
return;
}
}
return readFile(...args);

View file

@ -145,6 +145,15 @@ describe("unit-fast vitest lane", () => {
paths.isUnitFastTimerTestFile(file),
]);
console.log("UNIT_FAST_MEMBERSHIP_PROBE", JSON.stringify({ membership, unselectedFileReads }));
const { resolveCiTestRuntimeSelections } = await import("./scripts/lib/ci-test-runtime.mts");
const runtimeSelections = ["unit-fast", "unit-fast-isolated"].map((name, index) => ({
target: resolveCiTestRuntimeSelections({ targets: [selectedTests[index]] }, "bun-compatible"),
group: resolveCiTestRuntimeSelections({
configs: ["test/vitest/vitest." + name + ".config.ts"],
includePatterns: selectedTests,
}, "bun-compatible"),
}));
console.log("UNIT_FAST_RUNTIME_PROBE", JSON.stringify({ runtimeSelections, unselectedFileReads }));
hookFileReads = 0;
outsideFileReads = 0;
unselectedFileReads = 0;
@ -227,6 +236,26 @@ describe("unit-fast vitest lane", () => {
],
unselectedFileReads: 0,
});
const runtime = configProbeResult.stdout.match(/UNIT_FAST_RUNTIME_PROBE (.+)/u);
expect(runtime, configProbeResult.stdout).not.toBeNull();
expect(JSON.parse(runtime?.[1] ?? "null")).toEqual({
runtimeSelections: [
{
target: [{ runtime: "bun" }],
group: [
{
runtime: "bun",
includePatterns: ["src/agents/agent-tools.deferred-followup-guidance.test.ts"],
},
],
},
{
target: [{ runtime: "bun" }],
group: [{ runtime: "bun" }],
},
],
unselectedFileReads: 0,
});
const probeMatch = configProbeResult.stdout.match(
/UNIT_FAST_IO_PROBE (\d+) (\d+) (\d+) (\d+) (\d+)/u,
);

View file

@ -29,9 +29,10 @@ import { DEFAULT_VITEST_TEST_TIMEOUT_MS } from "./vitest.timeouts.ts";
import { compiledSubprocessesPlugin } from "./vitest.worker-artifacts.ts";
if (process.versions.bun) {
// Removal: delete this Vitest bootstrap after oven-sh/bun#42349 ships in supported Bun.
const { ensureSqliteLibrarySelected } = await import("../../src/infra/bun-sqlite-library.ts");
ensureSqliteLibrarySelected();
// Threads capture this decision when created; late admission leaves their SQLite pools conservative.
const { initializeSqliteRuntimeCapabilities } =
await import("../../src/infra/bun-sqlite-library.ts");
await initializeSqliteRuntimeCapabilities();
}
export type { LocalVitestScheduling };

View file

@ -0,0 +1,8 @@
import { isMainThread } from "node:worker_threads";
import { expect, it } from "vitest";
import { getSqliteRuntimeCapabilities } from "../../src/infra/bun-sqlite-library.js";
it.runIf(!isMainThread)("inherits settled SQLite close admission before test execution", () => {
// A negative probe is valid; missing parent admission would permanently disable worker reuse.
expect(getSqliteRuntimeCapabilities().decided).toBe(true);
});

View file

@ -27,6 +27,7 @@ export const uiIsolatedTestFiles = [
"ui/src/pages/chat/chat-pane-lifecycle.test.ts",
"ui/src/pages/chat/chat-pane-pull-requests.test.ts",
"ui/src/pages/chat/chat-pane-retained-presentation.test.ts",
"ui/src/pages/chat/chat-pane-retention.test.ts",
"ui/src/pages/chat/chat-pane-swarm-startup.test.ts",
"ui/src/pages/chat/chat-pane.read-marker.test.ts",
"ui/src/pages/chat/chat-pane.session-discussion.test.ts",

View file

@ -10,7 +10,6 @@ import { loadSettings, patchSettings } from "../../app/settings.ts";
import type { SessionCapability } from "../../lib/sessions/index.ts";
import { createApplicationContextProvider } from "../../test-helpers/application-context.ts";
import { createMockBoardProvider } from "../../test-helpers/board-provider.ts";
import { collectGarbageForTest } from "../../test-helpers/garbage-collection.ts";
import { installDialogPolyfill } from "../../test-helpers/modal-dialog.ts";
import { createStorageMock } from "../../test-helpers/storage.ts";
import { renderComposerFixture } from "./chat-composer.test-support.ts";
@ -55,53 +54,6 @@ describe("chat pane retained presentation lifecycle", () => {
vi.unstubAllGlobals();
});
it.each(["connection", "pane"] as const)(
"releases reply preview objects at the %s retirement boundary",
async (boundary) => {
class ReplyPreviewMessage {
role = "assistant";
content = "Previous connection's answer";
}
let preview: WeakRef<ReplyPreviewMessage> | undefined;
let requests = 0;
const client = createGatewayBrowserClientFixture({
request: async (method) => {
if (method !== "chat.message.get") {
return {};
}
requests += 1;
const message = new ReplyPreviewMessage();
preview = new WeakRef(message);
return { ok: true, message };
},
});
const { pane } = createTestChatPane({ client });
pane.requestReplyMessage("source-message");
await vi.waitFor(() => expect(pane.readReplyMessage("source-message")).toBeDefined());
pane.resetOlderMessagesViewport();
pane.presented = false;
pane.presented = true;
const retainedControl = new WeakRef({ unowned: true });
await collectGarbageForTest();
expect(retainedControl.deref()).toBeUndefined();
expect(preview!.deref()).toBeDefined();
pane.requestReplyMessage("source-message");
expect(requests).toBe(1);
if (boundary === "connection") {
pane.applyGatewaySnapshot({ ...pane.context.gateway.snapshot, phase: "stopped" });
} else {
pane.disconnectedCallback();
}
const retiredControl = new WeakRef({ unowned: true });
await collectGarbageForTest();
expect(retiredControl.deref()).toBeUndefined();
expect(preview!.deref()).toBeUndefined();
expect(pane.readReplyMessage("source-message")).toBeUndefined();
},
);
it.each([false, true])(
"restores dormant sidebar tabs for compact=%s without replacing saved task preferences",
(compact) => {

View file

@ -0,0 +1,61 @@
/* @vitest-environment jsdom */
/* @vitest-environment-options {"url":"http://chat-pane-retained.test/"} */
import { afterEach, describe, expect, it, vi } from "vitest";
import { collectGarbageForTest } from "../../test-helpers/garbage-collection.ts";
import { createGatewayBrowserClientFixture, createTestChatPane } from "./chat-pane.test-support.ts";
import { resetChatComposerState } from "./components/chat-composer.ts";
describe("chat pane retained presentation lifecycle", () => {
afterEach(() => {
resetChatComposerState();
vi.unstubAllGlobals();
});
it.each(["connection", "pane"] as const)(
"releases reply preview objects at the %s retirement boundary",
async (boundary) => {
class ReplyPreviewMessage {
role = "assistant";
content = "Previous connection's answer";
}
let preview: WeakRef<ReplyPreviewMessage> | undefined;
let requests = 0;
const client = createGatewayBrowserClientFixture({
request: async (method) => {
if (method !== "chat.message.get") {
return {};
}
requests += 1;
const message = new ReplyPreviewMessage();
preview = new WeakRef(message);
return { ok: true, message };
},
});
const { pane } = createTestChatPane({ client });
pane.requestReplyMessage("source-message");
await vi.waitFor(() => expect(pane.readReplyMessage("source-message")).toBeDefined());
pane.resetOlderMessagesViewport();
pane.presented = false;
pane.presented = true;
const retainedControl = new WeakRef({ unowned: true });
await collectGarbageForTest();
expect(retainedControl.deref()).toBeUndefined();
expect(preview!.deref()).toBeDefined();
pane.requestReplyMessage("source-message");
expect(requests).toBe(1);
if (boundary === "connection") {
pane.applyGatewaySnapshot({ ...pane.context.gateway.snapshot, phase: "stopped" });
} else {
pane.disconnectedCallback();
}
const retiredControl = new WeakRef({ unowned: true });
await collectGarbageForTest();
expect(retiredControl.deref()).toBeUndefined();
expect(preview!.deref()).toBeUndefined();
expect(pane.readReplyMessage("source-message")).toBeUndefined();
},
);
});

View file

@ -0,0 +1,43 @@
// @vitest-environment node
import { describe, expect, it } from "vitest";
import { collectGarbageForTest } from "../../test-helpers/garbage-collection.ts";
import { buildItems } from "./chat-thread.test-support.ts";
import {
getExpandedToolCards,
resetChatThreadState,
syncToolCardExpansionState,
} from "./chat-thread.ts";
describe("tool expansion state", () => {
it("releases a closed pane's messages while retaining its disclosure choices", async () => {
resetChatThreadState();
class TranscriptMessage {
role = "assistant";
content = [{ type: "toolcall", id: "released-call", name: "read" }];
}
const paneId = "released-pane";
const sessionKey = "released-session";
const populatePane = () => {
const message = new TranscriptMessage();
const items = buildItems({ paneId, sessionKey, messages: [message] });
syncToolCardExpansionState(sessionKey, items, true);
return {
messageReference: new WeakRef(message),
collectionControl: new WeakRef({ unowned: true }),
};
};
try {
const { messageReference, collectionControl } = populatePane();
await collectGarbageForTest();
expect(collectionControl.deref()).toBeUndefined();
expect(messageReference.deref() !== undefined).toBe(true);
resetChatThreadState(paneId);
await collectGarbageForTest();
expect(messageReference.deref()).toBeUndefined();
expect([...getExpandedToolCards(sessionKey).values()]).toEqual([true]);
} finally {
resetChatThreadState();
}
});
});

View file

@ -0,0 +1,22 @@
import { buildCachedChatItems } from "./chat-thread.ts";
export type CachedChatItemsProps = Parameters<typeof buildCachedChatItems>[0];
export function createProps(overrides: Partial<CachedChatItemsProps> = {}): CachedChatItemsProps {
return {
paneId: "pane-a",
sessionKey: "main",
runId: null,
messages: [],
toolMessages: [],
streamSegments: [],
stream: null,
streamStartedAt: null,
showToolCalls: true,
...overrides,
};
}
export function buildItems(overrides: Partial<CachedChatItemsProps> = {}) {
return buildCachedChatItems(createProps(overrides));
}

View file

@ -7,7 +7,6 @@ import type { MessageGroup } from "../../lib/chat/chat-types.ts";
import { prependUniqueNativeMessages } from "../../lib/chat/history-message-identity.ts";
import { normalizeMessage } from "../../lib/chat/message-normalizer.ts";
import * as toolCards from "../../lib/chat/tool-cards.ts";
import { collectGarbageForTest } from "../../test-helpers/garbage-collection.ts";
import { coalesceAgentRunFrames } from "./chat-agent-run-grouping.ts";
import { buildChatItems } from "./chat-thread-build.ts";
import {
@ -15,6 +14,7 @@ import {
type WorkGroupRenderItem as WorkGroupItem,
} from "./chat-thread-grouping.ts";
import * as threadItems from "./chat-thread-items.ts";
import { buildItems, createProps, type CachedChatItemsProps } from "./chat-thread.test-support.ts";
import {
buildCachedChatItems,
coalesceStreamRuns,
@ -38,31 +38,11 @@ function preparedGroup(key: string, message: unknown): MessageGroup {
return requireGroup(groupMessages([{ kind: "message", key, message }])[0]);
}
type CachedChatItemsProps = Parameters<typeof buildCachedChatItems>[0];
type ChatQueueItem = NonNullable<CachedChatItemsProps["queue"]>[number];
// Display fixtures need provenance markers for inbound-metadata stripping.
const SENDER_METADATA_BLOCK = `${markInboundContextLabel("Sender:")}\n\`\`\`json\n{"label":"openclaw-control-ui","id":"openclaw-control-ui"}\n\`\`\``;
function createProps(overrides: Partial<CachedChatItemsProps> = {}): CachedChatItemsProps {
return {
paneId: "pane-a",
sessionKey: "main",
runId: null,
messages: [],
toolMessages: [],
streamSegments: [],
stream: null,
streamStartedAt: null,
showToolCalls: true,
...overrides,
};
}
function buildItems(overrides: Partial<CachedChatItemsProps> = {}) {
return buildCachedChatItems(createProps(overrides));
}
function chatMessage(
role: string,
content: unknown,
@ -1936,38 +1916,6 @@ describe("buildCachedChatItems", () => {
});
describe("tool expansion state", () => {
it("releases a closed pane's messages while retaining its disclosure choices", async () => {
resetChatThreadState();
class TranscriptMessage {
role = "assistant";
content = [{ type: "toolcall", id: "released-call", name: "read" }];
}
const paneId = "released-pane";
const sessionKey = "released-session";
const populatePane = () => {
const message = new TranscriptMessage();
const items = buildItems({ paneId, sessionKey, messages: [message] });
syncToolCardExpansionState(sessionKey, items, true);
return {
messageReference: new WeakRef(message),
collectionControl: new WeakRef({ unowned: true }),
};
};
try {
const { messageReference, collectionControl } = populatePane();
await collectGarbageForTest();
expect(collectionControl.deref()).toBeUndefined();
expect(messageReference.deref() !== undefined).toBe(true);
resetChatThreadState(paneId);
await collectGarbageForTest();
expect(messageReference.deref()).toBeUndefined();
expect([...getExpandedToolCards(sessionKey).values()]).toEqual([true]);
} finally {
resetChatThreadState();
}
});
it("skips the tool-card walk when the item array identity is unchanged", () => {
resetChatThreadState();
const group = preparedGroup("assistant-stable", {

View file

@ -7,7 +7,6 @@ import { GatewayRequestError, type GatewayBrowserClient } from "../../api/gatewa
import type { SessionsUsageResult } from "../../api/types.ts";
import * as downloads from "../../lib/download.ts";
import * as toast from "../../lib/toast.ts";
import { collectGarbageForTest } from "../../test-helpers/garbage-collection.ts";
import { waitForFast } from "../../test-helpers/wait-for.ts";
import type { UsageSessionEntry } from "./types.ts";
import {
@ -72,117 +71,6 @@ describe("UsagePage detail requests", () => {
},
);
it("releases hydrated export reports after download while the page stays mounted", async () => {
class ExportReport {
name = "exported-context";
blockChars = 10;
}
let report: WeakRef<ExportReport> | undefined;
const snapshot = cacheSnapshot("fresh");
const session = {
key: "agent:main:export-lifetime",
label: "Export lifetime",
agentId: "main",
hasContextWeight: true,
usage: snapshot.result.totals,
};
const request = async (method: string, params?: Record<string, unknown>) => {
if (method === "sessions.usage") {
const weight = params?.includeContextWeight
? {
...contextWeight("exported-context"),
skills: { promptChars: 10, entries: [new ExportReport()] },
}
: undefined;
if (weight) {
report = new WeakRef(weight.skills.entries[0]!);
}
return {
...snapshot.result,
sessions: [{ ...session, ...(weight ? { contextWeight: weight } : {}) }],
};
}
return { providers: [] };
};
const download = vi.spyOn(downloads, "downloadTextFile").mockImplementation(() => {});
const page = await createPage({ request } as unknown as GatewayBrowserClient, true);
await preloadUsage(page);
page
.querySelector(".usage-export-menu")!
.dispatchEvent(new CustomEvent("wa-select", { detail: { item: { value: "json" } } }));
await waitForFast(() => expect(download).toHaveBeenCalledOnce());
expect(download.mock.calls[0]![1]).toContain("exported-context");
const collectionControl = new WeakRef({ unowned: true });
await collectGarbageForTest();
expect(collectionControl.deref()).toBeUndefined();
expect(report).toBeDefined();
expect(report!.deref()).toBeUndefined();
expect(page.isConnected).toBe(true);
});
it("keeps cancelled details displayed and releases them on explicit clear", async () => {
class DetailPayload {
timestamp = 1;
totalTokens = 10;
role = "user";
content = "Selected session";
}
const payloads: WeakRef<DetailPayload>[] = [];
const request = async (method: string) => {
const payload = new DetailPayload();
payloads.push(new WeakRef(payload));
return method === "sessions.usage.logs" ? { logs: [payload] } : { points: [payload] };
};
const page = await createPage({ request } as unknown as GatewayBrowserClient);
await page.details.timeSeries.load("agent:main:detail-lifetime");
await page.details.sessionLogs.load("agent:main:detail-lifetime");
page.details.cancel();
const collectionControl = new WeakRef({ unowned: true });
await collectGarbageForTest();
expect(collectionControl.deref()).toBeUndefined();
expect(payloads).toHaveLength(2);
expect(payloads.every((payload) => payload.deref() !== undefined)).toBe(true);
expect(page.details.timeSeries.data).not.toBeNull();
expect(page.details.sessionLogs.data).not.toBeNull();
page.details.clear();
await collectGarbageForTest();
expect(payloads.every((payload) => payload.deref() === undefined)).toBe(true);
expect(page.details.timeSeries.data).toBeNull();
expect(page.details.sessionLogs.data).toBeNull();
expect(page.isConnected).toBe(true);
});
it("releases a loaded overview when its Gateway identity is replaced", async () => {
class OverviewPayload {
key = "agent:main:overview-lifetime";
usage = null;
}
let payload: WeakRef<OverviewPayload> | undefined;
const snapshot = cacheSnapshot("fresh");
const request = async (method: string) => {
if (method === "sessions.usage") {
const report = new OverviewPayload();
payload = new WeakRef(report);
return { ...snapshot.result, sessions: [report] };
}
return { providers: [] };
};
const page = await createPage({ request } as unknown as GatewayBrowserClient);
await page.loadUsage();
expect(payload).toBeDefined();
page.context = contextWithClient({
request: async () => ({}),
} as unknown as GatewayBrowserClient);
page.requestUpdate();
await page.updateComplete;
const collectionControl = new WeakRef({ unowned: true });
await collectGarbageForTest();
expect(collectionControl.deref()).toBeUndefined();
expect(payload!.deref()).toBeUndefined();
expect(page.isConnected).toBe(true);
});
it("keeps unavailable timeline and conversation details pending and refreshes them when admission reopens", async () => {
const snapshot = cacheSnapshot("fresh");
const session = {

View file

@ -0,0 +1,130 @@
/* @vitest-environment jsdom */
import { afterEach, describe, expect, it, vi } from "vitest";
import type { GatewayBrowserClient } from "../../api/gateway.ts";
import * as downloads from "../../lib/download.ts";
import { collectGarbageForTest } from "../../test-helpers/garbage-collection.ts";
import { waitForFast } from "../../test-helpers/wait-for.ts";
import {
cacheSnapshot,
cleanupUsagePageTest,
contextWithClient,
contextWeight,
createPage,
preloadUsage,
} from "./usage-page.test-support.ts";
afterEach(cleanupUsagePageTest);
describe("UsagePage detail requests", () => {
it("releases hydrated export reports after download while the page stays mounted", async () => {
class ExportReport {
name = "exported-context";
blockChars = 10;
}
let report: WeakRef<ExportReport> | undefined;
const snapshot = cacheSnapshot("fresh");
const session = {
key: "agent:main:export-lifetime",
label: "Export lifetime",
agentId: "main",
hasContextWeight: true,
usage: snapshot.result.totals,
};
const request = async (method: string, params?: Record<string, unknown>) => {
if (method === "sessions.usage") {
const weight = params?.includeContextWeight
? {
...contextWeight("exported-context"),
skills: { promptChars: 10, entries: [new ExportReport()] },
}
: undefined;
if (weight) {
report = new WeakRef(weight.skills.entries[0]!);
}
return {
...snapshot.result,
sessions: [{ ...session, ...(weight ? { contextWeight: weight } : {}) }],
};
}
return { providers: [] };
};
const download = vi.spyOn(downloads, "downloadTextFile").mockImplementation(() => {});
const page = await createPage({ request } as unknown as GatewayBrowserClient, true);
await preloadUsage(page);
page
.querySelector(".usage-export-menu")!
.dispatchEvent(new CustomEvent("wa-select", { detail: { item: { value: "json" } } }));
await waitForFast(() => expect(download).toHaveBeenCalledOnce());
expect(download.mock.calls[0]![1]).toContain("exported-context");
const collectionControl = new WeakRef({ unowned: true });
await collectGarbageForTest();
expect(collectionControl.deref()).toBeUndefined();
expect(report).toBeDefined();
expect(report!.deref()).toBeUndefined();
expect(page.isConnected).toBe(true);
});
it("keeps cancelled details displayed and releases them on explicit clear", async () => {
class DetailPayload {
timestamp = 1;
totalTokens = 10;
role = "user";
content = "Selected session";
}
const payloads: WeakRef<DetailPayload>[] = [];
const request = async (method: string) => {
const payload = new DetailPayload();
payloads.push(new WeakRef(payload));
return method === "sessions.usage.logs" ? { logs: [payload] } : { points: [payload] };
};
const page = await createPage({ request } as unknown as GatewayBrowserClient);
await page.details.timeSeries.load("agent:main:detail-lifetime");
await page.details.sessionLogs.load("agent:main:detail-lifetime");
page.details.cancel();
const collectionControl = new WeakRef({ unowned: true });
await collectGarbageForTest();
expect(collectionControl.deref()).toBeUndefined();
expect(payloads).toHaveLength(2);
expect(payloads.every((payload) => payload.deref() !== undefined)).toBe(true);
expect(page.details.timeSeries.data).not.toBeNull();
expect(page.details.sessionLogs.data).not.toBeNull();
page.details.clear();
await collectGarbageForTest();
expect(payloads.every((payload) => payload.deref() === undefined)).toBe(true);
expect(page.details.timeSeries.data).toBeNull();
expect(page.details.sessionLogs.data).toBeNull();
expect(page.isConnected).toBe(true);
});
it("releases a loaded overview when its Gateway identity is replaced", async () => {
class OverviewPayload {
key = "agent:main:overview-lifetime";
usage = null;
}
let payload: WeakRef<OverviewPayload> | undefined;
const snapshot = cacheSnapshot("fresh");
const request = async (method: string) => {
if (method === "sessions.usage") {
const report = new OverviewPayload();
payload = new WeakRef(report);
return { ...snapshot.result, sessions: [report] };
}
return { providers: [] };
};
const page = await createPage({ request } as unknown as GatewayBrowserClient);
await page.loadUsage();
expect(payload).toBeDefined();
page.context = contextWithClient({
request: async () => ({}),
} as unknown as GatewayBrowserClient);
page.requestUpdate();
await page.updateComplete;
const collectionControl = new WeakRef({ unowned: true });
await collectGarbageForTest();
expect(collectionControl.deref()).toBeUndefined();
expect(payload!.deref()).toBeUndefined();
expect(page.isConnected).toBe(true);
});
});