OpenClaw Test Performance
Use evidence first. The goal is real pnpm test, plugin-suite, and
plugin-inspector speed/RSS improvement with coverage intact, not runner tuning by
guesswork.
Workflow
- Read the relevant local
AGENTS.md files before editing:
src/agents/AGENTS.md for agent/import hotspots.
src/channels/AGENTS.md and src/plugins/AGENTS.md for plugin/channel
laziness.
src/gateway/AGENTS.md for server lifecycle tests.
test/helpers/AGENTS.md and test/helpers/channels/AGENTS.md for shared
contract helpers.
src/infra/outbound/AGENTS.md for outbound/media/action tests.
- Establish a baseline before changing code:
- Prefer
pnpm test:perf:groups --full-suite --allow-failures --output <file>
for full-suite ranking.
- For bundled plugin breadth, run the smallest relevant
pnpm test:extensions:batch <plugin[,plugin...]> or plugin-inspector command
before jumping to the full extension sweep.
- For a scoped hotspot use:
/usr/bin/time -l pnpm test <file-or-files> --maxWorkers=1 --reporter=verbose
- For import-heavy suspicion add:
OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1.
- Separate wall/runner noise from real file cost:
- Compare Vitest duration, test body timing, import breakdown, wall time, and
max RSS.
- Re-run single files when grouped/full-suite numbers look stale or noisy.
- If a full-suite grouped run reports a lane failure but JSON says tests
passed, capture that as harness/noise and verify the suspect file directly.
- Pick the next attack by return and risk:
- High return: one file/test dominates seconds or RSS and has a clear root.
- High leverage: one plugin or SDK barrel causes every plugin-inspector or
extension-batch run to load broad runtime.
- Lower risk: static descriptors, target parsing, routing, auth bypass,
setup hints, registry fixtures, or test server lifecycle.
- Higher risk: real memory/runtime behavior, live providers, protocol
contracts, or broad production refactors.
- Fix the root cause, not the symptom:
- Move static metadata/parsing into narrow helpers or lightweight artifacts
reused by full runtime and fast paths.
- Prefer dependency injection, loaded-plugin-only lookup, explicit fixtures,
and pure helpers over broad mocks.
- Reuse suite-level servers/clients when a fresh handshake is irrelevant.
- Keep schedulers/background loops off unless the test proves scheduling.
- In plugin paths, move static metadata into manifest/lightweight artifacts
and keep runtime plugin loads behind explicit execution boundaries.
- Preserve coverage shape:
- Do not delete a slow integration proof unless the exact production
composition is extracted into a named helper and tested.
- Keep one cheap integration smoke when cross-component wiring matters.
- State explicitly what incidental coverage was removed, if any.
- Re-benchmark the same command after the change and compute seconds plus
percent gain.
- Update the running report when requested or when this thread is tracking one.
Include before/after commands, artifacts, coverage notes, verification, and
next attack order.
- Commit with
scripts/committer "<message>" <paths...> and push when the
user asked for commits/pushes. Stage only files touched for this attack.
Plugin-Suite Workflow
Use this section when perf work involves bundled plugins, plugin-inspector, SDK
barrels, package-boundary tests, or extension suites.
- Map the suite shape first:
- source tests:
pnpm test extensions/<id> or pnpm test:extensions:batch <id>
- package boundaries:
pnpm run test:extensions:package-boundary:canary and
pnpm run test:extensions:package-boundary:compile
- all bundled source tests:
pnpm test:extensions
- plugin import memory:
pnpm test:extensions:memory -- --json .artifacts/test-perf/extensions-memory.json
- plugin-inspector/report work: keep report primitives in
plugin-inspector;
keep wrappers thin and collect peak RSS when the command supports it.
- Start narrow, then widen:
- one plugin changed: run that plugin's tests and plugin-inspector slice.
- SDK/public barrel changed: add representative provider, channel, memory,
and feature plugins.
- loader/runtime mirror changed: add package-boundary checks and build/package
proof as needed.
- unknown shared plugin behavior: run
test:extensions:batch groups before
pnpm test:extensions.
- Treat plugin-inspector failures as product signals:
- JSON must parse.
- warnings/errors must be classified, not hidden.
- runtime capture should be quiet and config-tolerant.
- command output should include wall time, exit code, and peak RSS when
available.
- For broad or package-heavy plugin proof, use Blacksmith Testbox by default on
maintainer machines. Warm once and reuse the same box:
blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90
blacksmith testbox run --id <ID> "OPENCLAW_TESTBOX=1 pnpm test:extensions:batch <ids>"
- stop the box when done.
- If plugin performance is package-artifact sensitive, switch to
openclaw-pre-release-plugin-testing and Package Acceptance rather than
trusting source-only timing.
Metric Collection
Collect at least one stable metric before and after. Prefer the same machine and
same command. For Testbox comparisons, use the same tbx_... id when possible.
| Metric |
Use for |
Preferred source |
| wall time |
user-visible suite cost |
/usr/bin/time -l, test wrapper duration, Testbox run time |
| Vitest duration |
test body/import cost |
Vitest output per file/shard |
| import duration |
broad barrel/runtime loads |
OPENCLAW_VITEST_IMPORT_DURATIONS=1 |
| max RSS |
memory pressure and OOM risk |
/usr/bin/time -l, pnpm test:extensions:memory, wrapper memory summaries |
| CPU/user/sys |
CPU-bound vs wait-bound split |
/usr/bin/time -l locally, Testbox job timing when local CPU is noisy |
| heap snapshots |
real leak vs retained module graph |
openclaw-test-heap-leaks workflow |
Local scoped command with CPU/RSS:
timeout 240 /usr/bin/time -l pnpm test <file> --maxWorkers=1 --reporter=verbose
Plugin import memory profile:
pnpm build
pnpm test:extensions:memory -- --top 20 --json .artifacts/test-perf/extensions-memory.json
Targeted plugin import memory:
pnpm test:extensions:memory -- --extension discord --extension telegram --skip-combined
Heap/RSS escalation:
OPENCLAW_TEST_MEMORY_TRACE=1 \
OPENCLAW_TEST_HEAPSNAPSHOT_INTERVAL_MS=60000 \
OPENCLAW_TEST_HEAPSNAPSHOT_DIR=.tmp/heapsnap \
OPENCLAW_TEST_WORKERS=2 \
OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144 \
pnpm test
Use openclaw-test-heap-leaks when RSS keeps growing across intervals, workers
OOM, or the suspect command has app-object retention. Do not call RSS growth a
leak until snapshots or retainers support it.
Common Root Causes
- Full bundled channel/plugin runtime loaded for static data.
getChannelPlugin() fallback used when an already-loaded fixture or pure
parser would suffice.
- Broad
api.ts, runtime-api.ts, test-api.ts, or plugin-sdk barrels pulled
into hot tests.
- SDK root aliases or package barrels pulling focused subpaths back into a broad
plugin graph.
- Plugin-inspector loading runtime code just to render metadata, reports, or CI
policy scores.
- Bundled plugin capture reusing real config/home state instead of synthetic,
redacted, isolated state.
- Partial-real mocks using
importActual() around broad modules.
vi.resetModules() plus fresh imports in per-test loops.
- Test plugin registry seeded in
beforeAll while runtime state resets in
afterEach.
- Per-test gateway/server/client startup when state reset would suffice.
- Runtime/default model/auth selection paid by idle snapshots or fixtures.
- Plugin-owned media/action discovery triggered before checking whether args
contain plugin-owned fields.
- Timings missing from
test/fixtures/test-timings.unit.json, causing hotspot
files to stay in shared workers.
- Parallel Vitest runs sharing
node_modules/.experimental-vitest-cache without
distinct OPENCLAW_VITEST_FS_MODULE_CACHE_PATH values.
Benchmark Commands
Scoped file:
timeout 240 /usr/bin/time -l pnpm test <file> --maxWorkers=1 --reporter=verbose
Scoped file with import breakdown:
timeout 240 /usr/bin/time -l env \
OPENCLAW_VITEST_IMPORT_DURATIONS=1 \
OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 \
pnpm test <file> --maxWorkers=1 --reporter=verbose
Grouped suite:
pnpm test:perf:groups --full-suite --allow-failures \
--output .artifacts/test-perf/<name>.json
Extension batch:
pnpm test:extensions:batch <plugin[,plugin...]> -- --reporter=verbose
All extension tests:
pnpm test:extensions
Package-boundary plugin checks:
pnpm run test:extensions:package-boundary:canary
pnpm run test:extensions:package-boundary:compile
Reuse an existing Vitest JSON report:
pnpm test:perf:groups --report <vitest-json> \
--output .artifacts/test-perf/<name>.json
Verification
- Always run the targeted test surface that proves the change.
- For source changes, run
pnpm check:changed before push; in maintainer
Testbox mode run it in the warmed Testbox.
- For test-only changes, run
pnpm test:changed or the exact edited tests.
- Run
pnpm build when touching lazy-loading, bundled artifacts, package
boundaries, dynamic imports, build output, or public surfaces.
- For plugin SDK/barrel/runtime changes, add
pnpm plugin-sdk:api:check or
pnpm plugin-sdk:api:gen when the API surface may drift.
- For plugin-suite perf fixes, verify at least one representative plugin batch
plus the changed gate; use Package Acceptance if the bug only exists in a
packed artifact.
- If deps are missing/stale, run
pnpm install and retry the exact failed
command once.
- Use the report format:
| Metric | Before | After | Gain |
| -------------- | -----: | -----: | ------------: |
| File wall time | `Xs` | `Ys` | `-Zs` (`P%`) |
| Max RSS | `XMB` | `YMB` | `-ZMB` (`P%`) |
| CPU user/sys | `X/Ys` | `A/Bs` | explain |
Handoff
Keep the final concise:
- Root cause.
- Suite/plugin scope.
- Files changed.
- Before/after wall, Vitest/import, CPU, and RSS numbers where available.
- Leak classification if memory was involved: real leak, retained module graph,
or inconclusive.
- Coverage retained.
- Verification commands.
- Testbox ID or workflow URL for remote proof.
- Commit hash and push status.
1---2name: openclaw-test-performance3description: Benchmark, diagnose, and optimize OpenClaw test and plugin-suite runtime, import hotspots, CPU/RSS, heap growth, and slow coverage paths.4---56# OpenClaw Test Performance78Use evidence first. The goal is real `pnpm test`, plugin-suite, and9plugin-inspector speed/RSS improvement with coverage intact, not runner tuning by10guesswork.1112## Workflow13141. Read the relevant local `AGENTS.md` files before editing:15 - `src/agents/AGENTS.md` for agent/import hotspots.16 - `src/channels/AGENTS.md` and `src/plugins/AGENTS.md` for plugin/channel17 laziness.18 - `src/gateway/AGENTS.md` for server lifecycle tests.19 - `test/helpers/AGENTS.md` and `test/helpers/channels/AGENTS.md` for shared20 contract helpers.21 - `src/infra/outbound/AGENTS.md` for outbound/media/action tests.222. Establish a baseline before changing code:23 - Prefer `pnpm test:perf:groups --full-suite --allow-failures --output <file>`24 for full-suite ranking.25 - For bundled plugin breadth, run the smallest relevant `pnpm26test:extensions:batch <plugin[,plugin...]>` or plugin-inspector command27 before jumping to the full extension sweep.28 - For a scoped hotspot use:29 `/usr/bin/time -l pnpm test <file-or-files> --maxWorkers=1 --reporter=verbose`30 - For import-heavy suspicion add:31 `OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1`.323. Separate wall/runner noise from real file cost:33 - Compare Vitest duration, test body timing, import breakdown, wall time, and34 max RSS.35 - Re-run single files when grouped/full-suite numbers look stale or noisy.36 - If a full-suite grouped run reports a lane failure but JSON says tests37 passed, capture that as harness/noise and verify the suspect file directly.384. Pick the next attack by return and risk:39 - High return: one file/test dominates seconds or RSS and has a clear root.40 - High leverage: one plugin or SDK barrel causes every plugin-inspector or41 extension-batch run to load broad runtime.42 - Lower risk: static descriptors, target parsing, routing, auth bypass,43 setup hints, registry fixtures, or test server lifecycle.44 - Higher risk: real memory/runtime behavior, live providers, protocol45 contracts, or broad production refactors.465. Fix the root cause, not the symptom:47 - Move static metadata/parsing into narrow helpers or lightweight artifacts48 reused by full runtime and fast paths.49 - Prefer dependency injection, loaded-plugin-only lookup, explicit fixtures,50 and pure helpers over broad mocks.51 - Reuse suite-level servers/clients when a fresh handshake is irrelevant.52 - Keep schedulers/background loops off unless the test proves scheduling.53 - In plugin paths, move static metadata into manifest/lightweight artifacts54 and keep runtime plugin loads behind explicit execution boundaries.556. Preserve coverage shape:56 - Do not delete a slow integration proof unless the exact production57 composition is extracted into a named helper and tested.58 - Keep one cheap integration smoke when cross-component wiring matters.59 - State explicitly what incidental coverage was removed, if any.607. Re-benchmark the same command after the change and compute seconds plus61 percent gain.628. Update the running report when requested or when this thread is tracking one.63 Include before/after commands, artifacts, coverage notes, verification, and64 next attack order.659. Commit with `scripts/committer "<message>" <paths...>` and push when the66 user asked for commits/pushes. Stage only files touched for this attack.6768## Plugin-Suite Workflow6970Use this section when perf work involves bundled plugins, plugin-inspector, SDK71barrels, package-boundary tests, or extension suites.72731. Map the suite shape first:74 - source tests: `pnpm test extensions/<id>` or `pnpm test:extensions:batch <id>`75 - package boundaries: `pnpm run test:extensions:package-boundary:canary` and76 `pnpm run test:extensions:package-boundary:compile`77 - all bundled source tests: `pnpm test:extensions`78 - plugin import memory: `pnpm test:extensions:memory -- --json .artifacts/test-perf/extensions-memory.json`79 - plugin-inspector/report work: keep report primitives in `plugin-inspector`;80 keep wrappers thin and collect peak RSS when the command supports it.812. Start narrow, then widen:82 - one plugin changed: run that plugin's tests and plugin-inspector slice.83 - SDK/public barrel changed: add representative provider, channel, memory,84 and feature plugins.85 - loader/runtime mirror changed: add package-boundary checks and build/package86 proof as needed.87 - unknown shared plugin behavior: run `test:extensions:batch` groups before88 `pnpm test:extensions`.893. Treat plugin-inspector failures as product signals:90 - JSON must parse.91 - warnings/errors must be classified, not hidden.92 - runtime capture should be quiet and config-tolerant.93 - command output should include wall time, exit code, and peak RSS when94 available.954. For broad or package-heavy plugin proof, use Blacksmith Testbox by default on96 maintainer machines. Warm once and reuse the same box:97 - `blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90`98 - `blacksmith testbox run --id <ID> "OPENCLAW_TESTBOX=1 pnpm test:extensions:batch <ids>"`99 - stop the box when done.1005. If plugin performance is package-artifact sensitive, switch to101 `openclaw-pre-release-plugin-testing` and Package Acceptance rather than102 trusting source-only timing.103104## Metric Collection105106Collect at least one stable metric before and after. Prefer the same machine and107same command. For Testbox comparisons, use the same `tbx_...` id when possible.108109| Metric | Use for | Preferred source |110| --------------- | ---------------------------------- | --------------------------------------------------------------------------- |111| wall time | user-visible suite cost | `/usr/bin/time -l`, test wrapper duration, Testbox run time |112| Vitest duration | test body/import cost | Vitest output per file/shard |113| import duration | broad barrel/runtime loads | `OPENCLAW_VITEST_IMPORT_DURATIONS=1` |114| max RSS | memory pressure and OOM risk | `/usr/bin/time -l`, `pnpm test:extensions:memory`, wrapper memory summaries |115| CPU/user/sys | CPU-bound vs wait-bound split | `/usr/bin/time -l` locally, Testbox job timing when local CPU is noisy |116| heap snapshots | real leak vs retained module graph | `openclaw-test-heap-leaks` workflow |117118Local scoped command with CPU/RSS:119120```bash121timeout 240 /usr/bin/time -l pnpm test <file> --maxWorkers=1 --reporter=verbose122```123124Plugin import memory profile:125126```bash127pnpm build128pnpm test:extensions:memory -- --top 20 --json .artifacts/test-perf/extensions-memory.json129```130131Targeted plugin import memory:132133```bash134pnpm test:extensions:memory -- --extension discord --extension telegram --skip-combined135```136137Heap/RSS escalation:138139```bash140OPENCLAW_TEST_MEMORY_TRACE=1 \141OPENCLAW_TEST_HEAPSNAPSHOT_INTERVAL_MS=60000 \142OPENCLAW_TEST_HEAPSNAPSHOT_DIR=.tmp/heapsnap \143OPENCLAW_TEST_WORKERS=2 \144OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144 \145pnpm test146```147148Use `openclaw-test-heap-leaks` when RSS keeps growing across intervals, workers149OOM, or the suspect command has app-object retention. Do not call RSS growth a150leak until snapshots or retainers support it.151152## Common Root Causes153154- Full bundled channel/plugin runtime loaded for static data.155- `getChannelPlugin()` fallback used when an already-loaded fixture or pure156 parser would suffice.157- Broad `api.ts`, `runtime-api.ts`, `test-api.ts`, or plugin-sdk barrels pulled158 into hot tests.159- SDK root aliases or package barrels pulling focused subpaths back into a broad160 plugin graph.161- Plugin-inspector loading runtime code just to render metadata, reports, or CI162 policy scores.163- Bundled plugin capture reusing real config/home state instead of synthetic,164 redacted, isolated state.165- Partial-real mocks using `importActual()` around broad modules.166- `vi.resetModules()` plus fresh imports in per-test loops.167- Test plugin registry seeded in `beforeAll` while runtime state resets in168 `afterEach`.169- Per-test gateway/server/client startup when state reset would suffice.170- Runtime/default model/auth selection paid by idle snapshots or fixtures.171- Plugin-owned media/action discovery triggered before checking whether args172 contain plugin-owned fields.173- Timings missing from `test/fixtures/test-timings.unit.json`, causing hotspot174 files to stay in shared workers.175- Parallel Vitest runs sharing `node_modules/.experimental-vitest-cache` without176 distinct `OPENCLAW_VITEST_FS_MODULE_CACHE_PATH` values.177178## Benchmark Commands179180Scoped file:181182```bash183timeout 240 /usr/bin/time -l pnpm test <file> --maxWorkers=1 --reporter=verbose184```185186Scoped file with import breakdown:187188```bash189timeout 240 /usr/bin/time -l env \190 OPENCLAW_VITEST_IMPORT_DURATIONS=1 \191 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 \192 pnpm test <file> --maxWorkers=1 --reporter=verbose193```194195Grouped suite:196197```bash198pnpm test:perf:groups --full-suite --allow-failures \199 --output .artifacts/test-perf/<name>.json200```201202Extension batch:203204```bash205pnpm test:extensions:batch <plugin[,plugin...]> -- --reporter=verbose206```207208All extension tests:209210```bash211pnpm test:extensions212```213214Package-boundary plugin checks:215216```bash217pnpm run test:extensions:package-boundary:canary218pnpm run test:extensions:package-boundary:compile219```220221Reuse an existing Vitest JSON report:222223```bash224pnpm test:perf:groups --report <vitest-json> \225 --output .artifacts/test-perf/<name>.json226```227228## Verification229230- Always run the targeted test surface that proves the change.231- For source changes, run `pnpm check:changed` before push; in maintainer232 Testbox mode run it in the warmed Testbox.233- For test-only changes, run `pnpm test:changed` or the exact edited tests.234- Run `pnpm build` when touching lazy-loading, bundled artifacts, package235 boundaries, dynamic imports, build output, or public surfaces.236- For plugin SDK/barrel/runtime changes, add `pnpm plugin-sdk:api:check` or237 `pnpm plugin-sdk:api:gen` when the API surface may drift.238- For plugin-suite perf fixes, verify at least one representative plugin batch239 plus the changed gate; use Package Acceptance if the bug only exists in a240 packed artifact.241- If deps are missing/stale, run `pnpm install` and retry the exact failed242 command once.243- Use the report format:244245```markdown246| Metric | Before | After | Gain |247| -------------- | -----: | -----: | ------------: |248| File wall time | `Xs` | `Ys` | `-Zs` (`P%`) |249| Max RSS | `XMB` | `YMB` | `-ZMB` (`P%`) |250| CPU user/sys | `X/Ys` | `A/Bs` | explain |251```252253## Handoff254255Keep the final concise:256257- Root cause.258- Suite/plugin scope.259- Files changed.260- Before/after wall, Vitest/import, CPU, and RSS numbers where available.261- Leak classification if memory was involved: real leak, retained module graph,262 or inconclusive.263- Coverage retained.264- Verification commands.265- Testbox ID or workflow URL for remote proof.266- Commit hash and push status.