mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
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:
parent
45762faaa8
commit
9137cfcc5f
47 changed files with 2248 additions and 667 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
461
scripts/lib/ci-test-native-bun-qualification.json
Normal file
461
scripts/lib/ci-test-native-bun-qualification.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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] ?? ""))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
8
test/vitest/vitest.sqlite-admission.test.ts
Normal file
8
test/vitest/vitest.sqlite-admission.test.ts
Normal 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);
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
61
ui/src/pages/chat/chat-pane-retention.test.ts
Normal file
61
ui/src/pages/chat/chat-pane-retention.test.ts
Normal 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();
|
||||
},
|
||||
);
|
||||
});
|
||||
43
ui/src/pages/chat/chat-thread-retention.test.ts
Normal file
43
ui/src/pages/chat/chat-thread-retention.test.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
22
ui/src/pages/chat/chat-thread.test-support.ts
Normal file
22
ui/src/pages/chat/chat-thread.test-support.ts
Normal 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));
|
||||
}
|
||||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
130
ui/src/pages/usage/usage-page-retention.test.ts
Normal file
130
ui/src/pages/usage/usage-page-retention.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue