@zhushanwen/subagent-core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -0
- package/dist/chunk-3VOERJPJ.js +22 -0
- package/dist/chunk-A4IVZWVX.js +31 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
- package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
- package/dist/execution/engine/engines/zcode/constants.js +22 -0
- package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
- package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
- package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
- package/dist/execution/engine/engines/zcode/reader.js +239 -0
- package/dist/execution/engine/paths.cjs +55 -0
- package/dist/execution/engine/paths.d.cts +8 -0
- package/dist/execution/engine/paths.d.ts +8 -0
- package/dist/execution/engine/paths.js +26 -0
- package/dist/execution/relay-env.cjs +62 -0
- package/dist/execution/relay-env.d.cts +33 -0
- package/dist/execution/relay-env.d.ts +33 -0
- package/dist/execution/relay-env.js +20 -0
- package/dist/index.cjs +1852 -0
- package/dist/index.d.cts +1184 -0
- package/dist/index.d.ts +1184 -0
- package/dist/index.js +1810 -0
- package/dist/types-BxyAidGf.d.cts +875 -0
- package/dist/types-BxyAidGf.d.ts +875 -0
- package/package.json +99 -0
- package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
- package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
- package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
- package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
- package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
- package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
- package/src/__tests__/m2-append-content-probe.test.ts +73 -0
- package/src/__tests__/manifest-store.test.ts +368 -0
- package/src/__tests__/record-store-cache.test.ts +335 -0
- package/src/__tests__/record-store-index.test.ts +560 -0
- package/src/__tests__/record-store-last-line.test.ts +129 -0
- package/src/__tests__/review-fix-loop-script.test.ts +288 -0
- package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
- package/src/__tests__/robustness-low-batch1.test.ts +286 -0
- package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
- package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
- package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
- package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
- package/src/__tests__/session-runner.test.ts +69 -0
- package/src/__tests__/smoke.test.ts +9 -0
- package/src/__tests__/stream-sink.test.ts +203 -0
- package/src/core/__tests__/host-services.test.ts +144 -0
- package/src/core/__tests__/logger.test.ts +78 -0
- package/src/core/__tests__/notify-ports.test.ts +104 -0
- package/src/core/host-services.ts +97 -0
- package/src/core/logger.ts +47 -0
- package/src/core/notify-ports.ts +126 -0
- package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
- package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
- package/src/execution/__tests__/config.test.ts +302 -0
- package/src/execution/__tests__/delivery-methods.test.ts +424 -0
- package/src/execution/__tests__/dialog-queue.test.ts +299 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
- package/src/execution/__tests__/execute-nesting.test.ts +323 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
- package/src/execution/__tests__/execution-record.test.ts +1394 -0
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
- package/src/execution/__tests__/finalize-record.test.ts +367 -0
- package/src/execution/__tests__/finalized-marker.test.ts +104 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
- package/src/execution/__tests__/host-mode.test.ts +87 -0
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
- package/src/execution/__tests__/model-resolver.test.ts +465 -0
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/output-collector.test.ts +358 -0
- package/src/execution/__tests__/path-encoding.test.ts +104 -0
- package/src/execution/__tests__/pi-invocation.test.ts +134 -0
- package/src/execution/__tests__/record-store.test.ts +1057 -0
- package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
- package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
- package/src/execution/__tests__/relay-env.test.ts +42 -0
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/rpc-mode.test.ts +89 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +293 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
- package/src/execution/__tests__/spawn-args.test.ts +445 -0
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
- package/src/execution/__tests__/stdin-writer.test.ts +462 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
- package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +788 -0
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +615 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/ui-channels.test.ts +187 -0
- package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
- package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
- package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
- package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
- package/src/execution/__tests__/worktree-manager.test.ts +633 -0
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
- package/src/execution/__tests__/worktree-registry.test.ts +199 -0
- package/src/execution/agent-registry.ts +199 -0
- package/src/execution/agent-result-mapper.ts +90 -0
- package/src/execution/alive-store.ts +119 -0
- package/src/execution/argv-mirror.ts +113 -0
- package/src/execution/best-effort.ts +38 -0
- package/src/execution/channel-registry-access.ts +144 -0
- package/src/execution/concurrency-pool.ts +116 -0
- package/src/execution/config.ts +165 -0
- package/src/execution/dialog-queue.ts +330 -0
- package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
- package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
- package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
- package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
- package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
- package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
- package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
- package/src/execution/engine/__tests__/paths.test.ts +39 -0
- package/src/execution/engine/__tests__/registry.test.ts +120 -0
- package/src/execution/engine/__tests__/routing.test.ts +231 -0
- package/src/execution/engine/common/data-dir.ts +66 -0
- package/src/execution/engine/common/errors.ts +183 -0
- package/src/execution/engine/common/event-journal.ts +254 -0
- package/src/execution/engine/common/journal-replay.ts +62 -0
- package/src/execution/engine/common/kill-chain.ts +221 -0
- package/src/execution/engine/common/nesting-guard.ts +50 -0
- package/src/execution/engine/common/persona-router.ts +108 -0
- package/src/execution/engine/common/pool-manager.ts +226 -0
- package/src/execution/engine/common/schema-emulation.ts +189 -0
- package/src/execution/engine/common/session-view-projection.ts +51 -0
- package/src/execution/engine/engine-discovery.ts +65 -0
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
- package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
- package/src/execution/engine/engines/pi/reader.ts +48 -0
- package/src/execution/engine/engines/pi/registration.ts +35 -0
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
- package/src/execution/engine/engines/zcode/constants.ts +43 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
- package/src/execution/engine/engines/zcode/launcher.ts +161 -0
- package/src/execution/engine/engines/zcode/parser.ts +436 -0
- package/src/execution/engine/engines/zcode/preparer.ts +363 -0
- package/src/execution/engine/engines/zcode/reader.ts +381 -0
- package/src/execution/engine/engines/zcode/registration.ts +37 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
- package/src/execution/engine/host-task-spec.ts +47 -0
- package/src/execution/engine/model-prompt.ts +158 -0
- package/src/execution/engine/paths.ts +42 -0
- package/src/execution/engine/port.ts +153 -0
- package/src/execution/engine/registry.ts +133 -0
- package/src/execution/engine/routing.ts +218 -0
- package/src/execution/engine/types.ts +309 -0
- package/src/execution/execute-options-mapper.ts +115 -0
- package/src/execution/execution-record.ts +984 -0
- package/src/execution/finalize-record.ts +254 -0
- package/src/execution/finalized-marker.ts +70 -0
- package/src/execution/get-state-handshake.ts +104 -0
- package/src/execution/host-mode.ts +56 -0
- package/src/execution/idle-gc.ts +47 -0
- package/src/execution/lifecycle-manager.ts +513 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +256 -0
- package/src/execution/model-config-service.ts +250 -0
- package/src/execution/model-resolver.ts +248 -0
- package/src/execution/notifier.ts +372 -0
- package/src/execution/notify-ledger.ts +580 -0
- package/src/execution/output-collector.ts +228 -0
- package/src/execution/path-encoding.ts +60 -0
- package/src/execution/pi-invocation.ts +120 -0
- package/src/execution/record-entry.ts +132 -0
- package/src/execution/record-store.ts +1265 -0
- package/src/execution/relay-env.ts +37 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +120 -0
- package/src/execution/session-pending.ts +170 -0
- package/src/execution/session-reconstructor.ts +678 -0
- package/src/execution/session-runner.ts +1789 -0
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/spawn-event-adapter.ts +363 -0
- package/src/execution/stdin-writer.ts +198 -0
- package/src/execution/stream-sink.ts +126 -0
- package/src/execution/subagent-service.ts +2268 -0
- package/src/execution/subprocess-agent-runner.ts +317 -0
- package/src/execution/temp-prompt.ts +62 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +102 -0
- package/src/execution/types.ts +1023 -0
- package/src/execution/ui-channels.ts +216 -0
- package/src/execution/ui-interaction-model.ts +48 -0
- package/src/execution/ui-request-handler-factory.ts +243 -0
- package/src/execution/ui-request-observability.ts +81 -0
- package/src/execution/ui-request-queue.ts +184 -0
- package/src/execution/worktree-manager.ts +688 -0
- package/src/execution/worktree-registry.ts +279 -0
- package/src/index.ts +87 -0
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
- package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
- package/src/orchestration/__tests__/args-validator.test.ts +143 -0
- package/src/orchestration/__tests__/config-loader.test.ts +503 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
- package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
- package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
- package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
- package/src/orchestration/__tests__/script-lint.test.ts +831 -0
- package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
- package/src/orchestration/__tests__/test-mocks.ts +197 -0
- package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
- package/src/orchestration/__tests__/worker-host.test.ts +120 -0
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/agent-opts-resolver.ts +174 -0
- package/src/orchestration/args-validator.ts +127 -0
- package/src/orchestration/config-loader.ts +316 -0
- package/src/orchestration/error-recovery.ts +1018 -0
- package/src/orchestration/execute-agent-call.ts +256 -0
- package/src/orchestration/launcher.ts +455 -0
- package/src/orchestration/lifecycle.ts +399 -0
- package/src/orchestration/models/__tests__/budget.test.ts +307 -0
- package/src/orchestration/models/agent-call.ts +83 -0
- package/src/orchestration/models/budget.ts +118 -0
- package/src/orchestration/models/ports.ts +164 -0
- package/src/orchestration/models/run-runtime.ts +104 -0
- package/src/orchestration/models/run-spec.ts +83 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +183 -0
- package/src/orchestration/models/types.ts +301 -0
- package/src/orchestration/models/workflow-run.ts +254 -0
- package/src/orchestration/models/workflow-script-registry.ts +35 -0
- package/src/orchestration/models/workflow-script.ts +117 -0
- package/src/orchestration/script-lint.ts +766 -0
- package/src/orchestration/skill-discovery.ts +135 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +98 -0
- package/src/orchestration/worker-script-builder.ts +421 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +133 -0
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/meta-parser.test.ts +304 -0
- package/src/shared/__tests__/model-ref.test.ts +306 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
- package/src/shared/__tests__/resource-discovery.test.ts +749 -0
- package/src/shared/__tests__/resource-meta.test.ts +51 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/__tests__/timer-delay.test.ts +61 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/agent-ref.ts +57 -0
- package/src/shared/meta-parser.ts +261 -0
- package/src/shared/model-ref.ts +286 -0
- package/src/shared/resource-discovery.ts +835 -0
- package/src/shared/resource-meta.ts +65 -0
- package/src/shared/schema-env.ts +44 -0
- package/src/shared/schema-jsonify.ts +58 -0
- package/src/shared/timer-delay.ts +54 -0
- package/src/shared/xml-injection.ts +35 -0
- package/workflows/README.md +81 -0
- package/workflows/_shared/agent-refs.cjs +40 -0
- package/workflows/chain.js +137 -0
- package/workflows/map-reduce.js +180 -0
- package/workflows/parallel.js +154 -0
- package/workflows/review-fix-loop-utils.cjs +1542 -0
- package/workflows/review-fix-loop.js +1605 -0
- package/workflows/scatter-gather.js +184 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
// src/__tests__/timeout-integration.test.ts
|
|
2
|
+
//
|
|
3
|
+
// timeoutMs / signal abort → child.kill 端到端路径测试。
|
|
4
|
+
//
|
|
5
|
+
// 背景:session-runner.ts 无独立 timeoutMs 字段——超时机制是 watchdog(基于
|
|
6
|
+
// computeWatchdogMs(opts.maxTurns) 动态计算的下限 30min timer,兜底 SIGTERM);
|
|
7
|
+
// 外部取消通过 opts.signal (AbortSignal) 传播:onAbort → child.kill("SIGTERM")。
|
|
8
|
+
// 故「timeout 端到端路径」实际是 watchdog timer + signal abort → child.kill 两条链路。
|
|
9
|
+
//
|
|
10
|
+
// 本文件聚焦三条终止语义路径(与 run-spawn-integration.test.ts §12 watchdog 测试互补,
|
|
11
|
+
// 该文件关注 timer 边界值,本文件关注端到端 kill 语义 + 外部 signal 场景):
|
|
12
|
+
// 1. watchdog 到期 → child.kill(maxTurns 驱动的整体超时兜底)
|
|
13
|
+
// 2. 正常完成先于 watchdog 到期 → clearTimeout 生效,不 kill
|
|
14
|
+
// 3. 外部 signal abort(运行中 abort / spawn 前已 aborted)→ child.kill
|
|
15
|
+
//
|
|
16
|
+
// mock 策略(与 run-spawn-integration.test.ts / run-spawn-edges.test.ts 一致):
|
|
17
|
+
// - node:child_process.spawn → 返回 FakeChild(EventEmitter + PassThrough)。
|
|
18
|
+
// - node:child_process.execFile → err-first callback 默认兜底(buildEnvBlock git branch
|
|
19
|
+
// 失败 → catch → branch="")。
|
|
20
|
+
// - node:fs 同步方法 → mock(避免触碰真实文件系统),promises 保留真实实现。
|
|
21
|
+
// - temp-prompt → mock(返回固定路径,消除 fake-timers flaky)。
|
|
22
|
+
// - alive-store.writeAliveMarker → mock。
|
|
23
|
+
|
|
24
|
+
import type { PassThrough } from "node:stream";
|
|
25
|
+
|
|
26
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
27
|
+
|
|
28
|
+
// ── mock modules ──
|
|
29
|
+
// vitest 把 vi.mock 提升到文件顶部,工厂内部引用模块用 await import。
|
|
30
|
+
|
|
31
|
+
vi.mock("node:child_process", async () => {
|
|
32
|
+
const { EventEmitter } = await import("node:events");
|
|
33
|
+
const { PassThrough } = await import("node:stream");
|
|
34
|
+
|
|
35
|
+
// FakeChild:模拟 ChildProcess(EventEmitter + PassThrough streams)。
|
|
36
|
+
class FakeChild extends EventEmitter {
|
|
37
|
+
pid = 12345;
|
|
38
|
+
stdout = new PassThrough();
|
|
39
|
+
stdin = new PassThrough(); // runSpawn 注册 stdin EPIPE handler(session-runner),FakeChild 必须提供
|
|
40
|
+
stderr = new PassThrough();
|
|
41
|
+
killed = false;
|
|
42
|
+
killSignal: string | undefined;
|
|
43
|
+
// [race-F4] SIGKILL 升级判定读 exitCode/signalCode(真 ChildProcess 未退出时均 null),
|
|
44
|
+
// FakeChild 同形提供才能驱动升级路径测试(undefined === null 为 false 会误判已退出)。
|
|
45
|
+
exitCode: number | null = null;
|
|
46
|
+
signalCode: string | null = null;
|
|
47
|
+
kill(sig?: string): boolean {
|
|
48
|
+
this.killed = true;
|
|
49
|
+
this.killSignal = sig;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
spawn: vi.fn(() => new FakeChild()),
|
|
56
|
+
// buildEnvBlock 用 execFile 异步取 git branch:默认 err-first 兜底(catch → branch="")
|
|
57
|
+
execFile: vi.fn(
|
|
58
|
+
(
|
|
59
|
+
_cmd: string,
|
|
60
|
+
_args: readonly string[],
|
|
61
|
+
_opts: unknown,
|
|
62
|
+
cb: (err: Error | null, stdout?: string, stderr?: string) => void,
|
|
63
|
+
) => cb(new Error("execFile not configured in this test")),
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
vi.mock("node:fs", async () => {
|
|
69
|
+
const actual = await import("node:fs");
|
|
70
|
+
return {
|
|
71
|
+
default: {
|
|
72
|
+
...actual,
|
|
73
|
+
mkdirSync: vi.fn(),
|
|
74
|
+
existsSync: vi.fn(() => false),
|
|
75
|
+
appendFileSync: vi.fn(),
|
|
76
|
+
writeFileSync: vi.fn(),
|
|
77
|
+
readdirSync: vi.fn(() => []),
|
|
78
|
+
},
|
|
79
|
+
mkdirSync: vi.fn(),
|
|
80
|
+
existsSync: vi.fn(() => false),
|
|
81
|
+
appendFileSync: vi.fn(),
|
|
82
|
+
writeFileSync: vi.fn(),
|
|
83
|
+
readdirSync: vi.fn(() => []),
|
|
84
|
+
promises: actual.promises,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
vi.mock("../alive-store.ts", () => ({
|
|
89
|
+
writeAliveMarker: vi.fn(),
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
vi.mock("../temp-prompt.ts", () => ({
|
|
93
|
+
writePromptToTempFile: vi.fn(async (agent: string) => {
|
|
94
|
+
const safeName = agent.replace(/[^\w.-]+/g, "_");
|
|
95
|
+
return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
|
|
96
|
+
}),
|
|
97
|
+
cleanupTempPrompt: vi.fn(async () => {}),
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
import { spawn } from "node:child_process";
|
|
101
|
+
import * as fs from "node:fs";
|
|
102
|
+
|
|
103
|
+
import { createRecord } from "../execution-record.ts";
|
|
104
|
+
import {
|
|
105
|
+
computeWatchdogMs,
|
|
106
|
+
type RunOptions,
|
|
107
|
+
resolveSpawnWatchdogMs,
|
|
108
|
+
runSpawn,
|
|
109
|
+
SPAWN_WATCHDOG_ENV,
|
|
110
|
+
type SessionRunnerContext,
|
|
111
|
+
} from "../session-runner.ts";
|
|
112
|
+
|
|
113
|
+
const mockSpawn = vi.mocked(spawn);
|
|
114
|
+
const mockExistsSync = vi.mocked(fs.existsSync);
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* spawn mock 返回的 fake child 类型(结构子集)。
|
|
118
|
+
* FakeChild 定义在 vi.mock 工厂内部(作用域隔离),测试代码通过此类型访问成员。
|
|
119
|
+
*/
|
|
120
|
+
interface FakeChild {
|
|
121
|
+
pid: number;
|
|
122
|
+
stdout: PassThrough;
|
|
123
|
+
stderr: PassThrough;
|
|
124
|
+
killed: boolean;
|
|
125
|
+
killSignal: string | undefined;
|
|
126
|
+
kill(sig?: string): boolean;
|
|
127
|
+
emit(event: string, ...args: unknown[]): boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 从最近一次 spawn 调用取回返回的 FakeChild。 */
|
|
131
|
+
function lastSpawnedChild(): FakeChild {
|
|
132
|
+
const result = mockSpawn.mock.results.at(-1);
|
|
133
|
+
if (!result) throw new Error("spawn was not called yet");
|
|
134
|
+
return result.value as FakeChild;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================
|
|
138
|
+
// 辅助:向 stdout 写一行(自动补换行)
|
|
139
|
+
// ============================================================
|
|
140
|
+
|
|
141
|
+
function emitStdoutLine(child: FakeChild, obj: Record<string, unknown>): void {
|
|
142
|
+
child.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 构造 session header 行(stdout 首行)。 */
|
|
146
|
+
function sessionHeader(id = "sess-abc"): Record<string, unknown> {
|
|
147
|
+
return {
|
|
148
|
+
type: "session",
|
|
149
|
+
id,
|
|
150
|
+
timestamp: "2026-07-03T12-00-00-000Z",
|
|
151
|
+
cwd: "/tmp/test",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============================================================
|
|
156
|
+
// 辅助:构造最小合法的 record / opts / ctx
|
|
157
|
+
// ============================================================
|
|
158
|
+
|
|
159
|
+
function makeRecord() {
|
|
160
|
+
return createRecord("run-1", {
|
|
161
|
+
agent: "general-purpose",
|
|
162
|
+
model: "test-model",
|
|
163
|
+
mode: "sync",
|
|
164
|
+
task: "do something",
|
|
165
|
+
startedAt: 1_000_000,
|
|
166
|
+
rootSessionId: "root-session",
|
|
167
|
+
parentRecordId: undefined,
|
|
168
|
+
depth: 0,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function makeOpts(overrides: Partial<RunOptions> = {}): RunOptions {
|
|
173
|
+
return {
|
|
174
|
+
resolved: {
|
|
175
|
+
model: {
|
|
176
|
+
id: "test-model",
|
|
177
|
+
name: "Test Model",
|
|
178
|
+
provider: "test",
|
|
179
|
+
reasoning: false,
|
|
180
|
+
},
|
|
181
|
+
thinkingLevel: undefined,
|
|
182
|
+
},
|
|
183
|
+
agentConfig: undefined,
|
|
184
|
+
appendSystemPrompt: undefined,
|
|
185
|
+
skillPath: undefined,
|
|
186
|
+
schema: undefined,
|
|
187
|
+
maxTurns: undefined,
|
|
188
|
+
graceTurns: undefined,
|
|
189
|
+
signal: undefined,
|
|
190
|
+
onEvent: undefined,
|
|
191
|
+
...overrides,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function makeCtx(overrides: Partial<SessionRunnerContext> = {}): SessionRunnerContext {
|
|
196
|
+
return {
|
|
197
|
+
cwd: "/tmp/test",
|
|
198
|
+
agentDir: "/tmp/test/agents",
|
|
199
|
+
skillDirs: [],
|
|
200
|
+
mainCwd: "/tmp/test",
|
|
201
|
+
mainSessionFile: undefined,
|
|
202
|
+
sessionRootId: "root-session-test",
|
|
203
|
+
rootCwd: "/tmp/test",
|
|
204
|
+
...overrides,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* fake timers 下推进时间直到 spawn 被调用(返回 child 控制器,与共享 helper 的 void 返回
|
|
210
|
+
* 不同,故就地维护)。
|
|
211
|
+
*
|
|
212
|
+
* [快照语义] 同 helpers/spawn-mock.ts 的 waitForSpawn:记调用时 baseline,等待其后的
|
|
213
|
+
* **新** spawn——支持同一测试/文件内多次 runSpawn(旧 `length === 0` 只对首次有效)。
|
|
214
|
+
*
|
|
215
|
+
* runSpawn 在 mkdirSync + writePromptToTempFile(mock 的 async I/O)之后才调 spawn。
|
|
216
|
+
* 每次推进 10ms 让轮询 setTimeout 触发,advanceTimersByTimeAsync 同时 flush 已 resolve
|
|
217
|
+
* 的 I/O promise,使 runSpawn 继续走到 spawn。
|
|
218
|
+
*/
|
|
219
|
+
async function waitForSpawnFake(timeoutSteps = 200): Promise<FakeChild> {
|
|
220
|
+
const baseline = mockSpawn.mock.results.length;
|
|
221
|
+
for (let i = 0; i < timeoutSteps; i++) {
|
|
222
|
+
if (mockSpawn.mock.results.length > baseline) break;
|
|
223
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
224
|
+
}
|
|
225
|
+
if (mockSpawn.mock.results.length <= baseline) {
|
|
226
|
+
throw new Error("spawn was not called (fake timers did not progress to spawn)");
|
|
227
|
+
}
|
|
228
|
+
return lastSpawnedChild();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 真实 timers 下轮询直到 spawn 被调用(返回 child 控制器;用于 signal abort 测试——
|
|
233
|
+
* 不需要推进 watchdog,用 queueMicrotask 触发 abort,真实 timers 下 mock I/O 正常 resolve)。
|
|
234
|
+
*
|
|
235
|
+
* [快照语义] 同 helpers/spawn-mock.ts 的 waitForSpawn:记 baseline 等**新** spawn,
|
|
236
|
+
* 支持同文件多次 runSpawn。
|
|
237
|
+
*/
|
|
238
|
+
async function waitForSpawnReal(timeoutMs = 1000): Promise<FakeChild> {
|
|
239
|
+
const start = Date.now();
|
|
240
|
+
const baseline = mockSpawn.mock.results.length;
|
|
241
|
+
while (mockSpawn.mock.results.length <= baseline) {
|
|
242
|
+
if (Date.now() - start > timeoutMs) {
|
|
243
|
+
throw new Error(`spawn was not called within ${timeoutMs}ms`);
|
|
244
|
+
}
|
|
245
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
246
|
+
}
|
|
247
|
+
return lastSpawnedChild();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ============================================================
|
|
251
|
+
// 测试
|
|
252
|
+
// ============================================================
|
|
253
|
+
|
|
254
|
+
describe("timeoutMs / signal abort → child.kill 端到端路径", () => {
|
|
255
|
+
beforeEach(() => {
|
|
256
|
+
vi.clearAllMocks();
|
|
257
|
+
mockExistsSync.mockReturnValue(false);
|
|
258
|
+
// [F-4 假红源修复] env 隔离:多数用例默认 maxTurns=undefined →
|
|
259
|
+
// resolveSpawnWatchdogMs 走 env 兑底分支,宿主 export SPAWN_WATCHDOG 即假红
|
|
260
|
+
//(极端值 3e9 还会在 assertSafeTimerDelay fail-fast 直接炸掉 runSpawn)。
|
|
261
|
+
// 用例内显式 stubEnv 的值照常叠加生效(inner describe 的 afterEach
|
|
262
|
+
// unstubAllEnvs 与本处兼容)。
|
|
263
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
afterEach(() => {
|
|
267
|
+
vi.restoreAllMocks();
|
|
268
|
+
vi.unstubAllEnvs();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ── 1. watchdog 到期 → child.kill ──
|
|
272
|
+
//
|
|
273
|
+
// [R1] watchdog = setTimeout(() => child.kill("SIGTERM"), computeWatchdogMs(maxTurns))。
|
|
274
|
+
// computeWatchdogMs 下限 30min(SPAWN_WATCHDOG_FLOOR_MS),maxTurns=6 → max(30min, 30min)=30min。
|
|
275
|
+
// 子进程卡死(turn_end 永不触发)时 limiter 失效,watchdog 兜底 kill 防资源泄漏。
|
|
276
|
+
describe("watchdog 到期 → signal abort → child.kill", () => {
|
|
277
|
+
beforeEach(() => {
|
|
278
|
+
vi.useFakeTimers();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
afterEach(() => {
|
|
282
|
+
vi.useRealTimers();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("watchdog 到期(超过 computeWatchdogMs 阈值)→ child.kill(SIGTERM) 被调用", async () => {
|
|
286
|
+
const record = makeRecord();
|
|
287
|
+
// maxTurns=6 → computeWatchdogMs = max(30min, 6*5min) = 30min
|
|
288
|
+
// 不 await:runSpawn 内部 await 子进程 close,watchdog kill 后还需 emit close 才 resolve
|
|
289
|
+
const promise = runSpawn(record, "Task: hang", makeOpts({ maxTurns: 6 }), makeCtx());
|
|
290
|
+
|
|
291
|
+
const child = await waitForSpawnFake();
|
|
292
|
+
|
|
293
|
+
// spawn 后尚未触发 kill
|
|
294
|
+
expect(child.killed).toBe(false);
|
|
295
|
+
|
|
296
|
+
// 推进时间越过 watchdog 阈值(30 * 60 * 1000 + 100ms 余量)
|
|
297
|
+
await vi.advanceTimersByTimeAsync(30 * 60 * 1000 + 100);
|
|
298
|
+
|
|
299
|
+
// watchdog 触发 child.kill("SIGTERM")
|
|
300
|
+
expect(child.killed).toBe(true);
|
|
301
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
302
|
+
|
|
303
|
+
// 收尾:emit close 让 runSpawn resolve(避免悬挂)
|
|
304
|
+
emitStdoutLine(child, sessionHeader());
|
|
305
|
+
child.stdout.end();
|
|
306
|
+
child.emit("close", 143); // SIGTERM = 128+15
|
|
307
|
+
|
|
308
|
+
const result = await promise;
|
|
309
|
+
// 信号终止(>=128)视为正常完成
|
|
310
|
+
expect(result.success).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// [race-F4] SIGKILL 升级:watchdog SIGTERM 后子进程无视信号(exitCode/signalCode
|
|
314
|
+
// 保持 null = 未退出)→ 30s 后升级 SIGKILL。挂住子进程永不回收的兑底防线。
|
|
315
|
+
it("[race-F4] watchdog SIGTERM 后 30s 未 exit → 升级 SIGKILL", async () => {
|
|
316
|
+
const record = makeRecord();
|
|
317
|
+
const promise = runSpawn(record, "Task: hang ignores SIGTERM", makeOpts({ maxTurns: 6 }), makeCtx());
|
|
318
|
+
|
|
319
|
+
const child = await waitForSpawnFake();
|
|
320
|
+
|
|
321
|
+
// watchdog 到期 → SIGTERM(升级路径入口)
|
|
322
|
+
await vi.advanceTimersByTimeAsync(30 * 60 * 1000 + 100);
|
|
323
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
324
|
+
|
|
325
|
+
// 子进程无视 SIGTERM:不 emit exit/close,exitCode/signalCode 保持 null。
|
|
326
|
+
// 推进升级窗口:非贴边断言(waitForSpawnFake 每步 10ms 推进,升级 timer 挂载
|
|
327
|
+
// 时刻有 ≤100ms 抖动)——先推 29s(余量 > 抖动)断言未升级,再推 2s 越过窗口。
|
|
328
|
+
await vi.advanceTimersByTimeAsync(29 * 1000);
|
|
329
|
+
expect(child.killSignal).toBe("SIGTERM"); // 尚未升级
|
|
330
|
+
await vi.advanceTimersByTimeAsync(2 * 1000);
|
|
331
|
+
expect(child.killSignal).toBe("SIGKILL"); // 已升级
|
|
332
|
+
|
|
333
|
+
// 收尾:SIGKILL 后子进程退出 → runSpawn resolve(信号终止视为完成)
|
|
334
|
+
emitStdoutLine(child, sessionHeader());
|
|
335
|
+
child.stdout.end();
|
|
336
|
+
child.emit("close", 137); // SIGKILL = 128+9
|
|
337
|
+
const result = await promise;
|
|
338
|
+
expect(result.success).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// [race-F4] 升级 timer 的 exit 自动 clear:SIGTERM 后子进程正常退出(exit 事件到
|
|
342
|
+
// 达),升级 timer 被清,不误发 SIGKILL(killSignal 停留在 SIGTERM)。
|
|
343
|
+
it("[race-F4] SIGTERM 后子进程 exit → 升级 timer 被 clear,不误发 SIGKILL", async () => {
|
|
344
|
+
const record = makeRecord();
|
|
345
|
+
const promise = runSpawn(record, "Task: dies on SIGTERM", makeOpts({ maxTurns: 6 }), makeCtx());
|
|
346
|
+
|
|
347
|
+
const child = await waitForSpawnFake();
|
|
348
|
+
|
|
349
|
+
// watchdog 到期 → SIGTERM
|
|
350
|
+
await vi.advanceTimersByTimeAsync(30 * 60 * 1000 + 100);
|
|
351
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
352
|
+
|
|
353
|
+
// 子进程响应 SIGTERM 退出:exitCode 143(SIGTERM 终止形态)+ exit + close。
|
|
354
|
+
// exit 事件触发升级 timer clear(killChildWithEscalation 内 once("exit") 挂钩)。
|
|
355
|
+
child.exitCode = 143;
|
|
356
|
+
child.emit("exit", 143);
|
|
357
|
+
child.stdout.end();
|
|
358
|
+
child.emit("close", 143);
|
|
359
|
+
const result = await promise;
|
|
360
|
+
|
|
361
|
+
// 越过升级窗口:无 SIGKILL(timer 已被 exit clear),killSignal 停留在 SIGTERM
|
|
362
|
+
await vi.advanceTimersByTimeAsync(60 * 1000);
|
|
363
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
364
|
+
expect(result.success).toBe(true);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ── 2. watchdog 默认不挂载 + env 兑底(预算语义对齐 2026-08)──
|
|
369
|
+
//
|
|
370
|
+
// 语义:maxTurns 未传 → env 兑底;显式 0/负 → 压过 env 不挂(U5,SP-6 参数 > env,
|
|
371
|
+
// 旧实现 maxTurns:0 落到 env 兑底,参数显式关不掉 watchdog,已废);
|
|
372
|
+
// env XYZ_SUBAGENT_SPAWN_WATCHDOG_MS 设置时按绝对时限兑底挂载(hang 泄漏防线
|
|
373
|
+
// opt-in)。resolveSpawnWatchdogMs 是挂载判定唯一入口。
|
|
374
|
+
describe("watchdog 默认不挂载 + env 兑底(预算语义对齐)", () => {
|
|
375
|
+
beforeEach(() => {
|
|
376
|
+
vi.useFakeTimers();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
afterEach(() => {
|
|
380
|
+
vi.useRealTimers();
|
|
381
|
+
vi.unstubAllEnvs();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("resolveSpawnWatchdogMs:maxTurns 未传/0/负数且 env 未设 → undefined(不挂)", () => {
|
|
385
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, ""); // 空串 = 未设(raw falsy)
|
|
386
|
+
expect(resolveSpawnWatchdogMs(undefined)).toBeUndefined();
|
|
387
|
+
expect(resolveSpawnWatchdogMs(null)).toBeUndefined();
|
|
388
|
+
expect(resolveSpawnWatchdogMs(0)).toBeUndefined();
|
|
389
|
+
expect(resolveSpawnWatchdogMs(-5)).toBeUndefined();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("resolveSpawnWatchdogMs:env 设置 → 绝对时限;非法值 → undefined;maxTurns 有效优先", () => {
|
|
393
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "60000");
|
|
394
|
+
expect(resolveSpawnWatchdogMs(undefined)).toBe(60_000);
|
|
395
|
+
// [U5] SP-6 参数 > env:显式 0 压过 env 兑底 → 不挂(旧实现落成 60000)
|
|
396
|
+
expect(resolveSpawnWatchdogMs(0)).toBeUndefined();
|
|
397
|
+
expect(resolveSpawnWatchdogMs(-5)).toBeUndefined();
|
|
398
|
+
// maxTurns 有效(>0)→ turns 估算优先于 env 兑底
|
|
399
|
+
expect(resolveSpawnWatchdogMs(6)).toBe(computeWatchdogMs(6));
|
|
400
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "abc");
|
|
401
|
+
expect(resolveSpawnWatchdogMs(undefined)).toBeUndefined();
|
|
402
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "-1");
|
|
403
|
+
expect(resolveSpawnWatchdogMs(undefined)).toBeUndefined();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// [U1] setTimeout 2^31-1 溢出 fail-fast:溢出 delay 被 Node 置 1ms 立即触发
|
|
407
|
+
//(watchdog 变「启动即杀」),两路径(env 兑底 / turns 估算)都必须在入口拦截。
|
|
408
|
+
it("resolveSpawnWatchdogMs:env 值溢出(>2^31-1)→ fail-fast throw(不静默 clamp/不挂)", () => {
|
|
409
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "3000000000");
|
|
410
|
+
expect(() => resolveSpawnWatchdogMs(undefined)).toThrowError(/2147483647/);
|
|
411
|
+
expect(() => resolveSpawnWatchdogMs(undefined)).toThrowError(/Recovery/);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("resolveSpawnWatchdogMs:maxTurns 估算值溢出 → fail-fast throw", () => {
|
|
415
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "");
|
|
416
|
+
// maxTurns=1e6 → 估算 5e9 ms > 2^31-1,入口拦截
|
|
417
|
+
expect(() => resolveSpawnWatchdogMs(1_000_000)).toThrowError(/2147483647/);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// [F-R3] maxTurns 垃圾值网格:NaN/""/null/0/负数(含 ±Infinity)。
|
|
421
|
+
// 旧实现 NaN/"" 绕过 Number.isFinite 且压过 env 兑底成「显式不限」。
|
|
422
|
+
it("resolveSpawnWatchdogMs:maxTurns NaN → fail-fast throw(压过 env);\"\"→Number(\"\")=0 显式不限;null/0/负数网格(F-R3)", () => {
|
|
423
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "60000");
|
|
424
|
+
// NaN:非有限数 fail-fast(与 timer-delay U1 同语义),不再压过 env 兑底成「显式不限」
|
|
425
|
+
expect(() => resolveSpawnWatchdogMs(NaN)).toThrowError(/not a finite number/);
|
|
426
|
+
expect(() => resolveSpawnWatchdogMs(NaN)).toThrowError(/Recovery/);
|
|
427
|
+
// ±Infinity:同非有限数 fail-fast
|
|
428
|
+
expect(() => resolveSpawnWatchdogMs(Infinity)).toThrowError(/not a finite number/);
|
|
429
|
+
// "":Number("") === 0 → 与显式 0 同路径(显式不限,压过 env,U5 语义不回归)
|
|
430
|
+
expect(resolveSpawnWatchdogMs("" as unknown as number)).toBeUndefined();
|
|
431
|
+
// null → 未传语义,env 兑底生效
|
|
432
|
+
expect(resolveSpawnWatchdogMs(null)).toBe(60_000);
|
|
433
|
+
// 0 / 负数 → 显式不限,压过 env(既有 U5 网格不回归)
|
|
434
|
+
expect(resolveSpawnWatchdogMs(0)).toBeUndefined();
|
|
435
|
+
expect(resolveSpawnWatchdogMs(-5)).toBeUndefined();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("maxTurns 未传 → 推进 56min 不 kill(旧实现 50min 估算会误杀)", async () => {
|
|
439
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "");
|
|
440
|
+
const record = makeRecord();
|
|
441
|
+
const promise = runSpawn(record, "Task: no-turns", makeOpts({ maxTurns: undefined }), makeCtx());
|
|
442
|
+
|
|
443
|
+
const child = await waitForSpawnFake();
|
|
444
|
+
expect(child.killed).toBe(false);
|
|
445
|
+
|
|
446
|
+
// 旧实现的估算默认(10 turns → 50min)已过,仍不 kill = watchdog 未挂载
|
|
447
|
+
await vi.advanceTimersByTimeAsync(56 * 60 * 1000);
|
|
448
|
+
expect(child.killed).toBe(false);
|
|
449
|
+
|
|
450
|
+
emitStdoutLine(child, sessionHeader());
|
|
451
|
+
child.stdout.end();
|
|
452
|
+
child.emit("close", 0);
|
|
453
|
+
const result = await promise;
|
|
454
|
+
expect(result.success).toBe(true);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("maxTurns: 0(显式关不掉的旧默认已废)→ 同样不挂 watchdog", async () => {
|
|
458
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "");
|
|
459
|
+
const record = makeRecord();
|
|
460
|
+
const promise = runSpawn(record, "Task: zero-turns", makeOpts({ maxTurns: 0 }), makeCtx());
|
|
461
|
+
|
|
462
|
+
const child = await waitForSpawnFake();
|
|
463
|
+
await vi.advanceTimersByTimeAsync(56 * 60 * 1000);
|
|
464
|
+
expect(child.killed).toBe(false);
|
|
465
|
+
|
|
466
|
+
emitStdoutLine(child, sessionHeader());
|
|
467
|
+
child.stdout.end();
|
|
468
|
+
child.emit("close", 0);
|
|
469
|
+
const result = await promise;
|
|
470
|
+
expect(result.success).toBe(true);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// [U5] SP-6 参数 > env:显式 maxTurns:0 压过 SPAWN_WATCHDOG_ENV 兑底——
|
|
474
|
+
// 旧实现落成 env 时限 kill,参数显式关不掉 watchdog。
|
|
475
|
+
it("maxTurns: 0 + env=60000 → 显式 0 压过 env,61s 不 kill(U5)", async () => {
|
|
476
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "60000");
|
|
477
|
+
const record = makeRecord();
|
|
478
|
+
const promise = runSpawn(record, "Task: zero-turns-override", makeOpts({ maxTurns: 0 }), makeCtx());
|
|
479
|
+
|
|
480
|
+
const child = await waitForSpawnFake();
|
|
481
|
+
// 推进越过 env 兑底时限(60s):若 maxTurns:0 未压过 env,此处已被 kill
|
|
482
|
+
await vi.advanceTimersByTimeAsync(61 * 1000);
|
|
483
|
+
expect(child.killed).toBe(false);
|
|
484
|
+
|
|
485
|
+
emitStdoutLine(child, sessionHeader());
|
|
486
|
+
child.stdout.end();
|
|
487
|
+
child.emit("close", 0);
|
|
488
|
+
const result = await promise;
|
|
489
|
+
expect(result.success).toBe(true);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("env SPAWN_WATCHDOG_MS=60000 + maxTurns 未传 → 1min 兑底 kill", async () => {
|
|
493
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "60000");
|
|
494
|
+
const record = makeRecord();
|
|
495
|
+
const promise = runSpawn(record, "Task: env-watchdog", makeOpts(), makeCtx());
|
|
496
|
+
|
|
497
|
+
const child = await waitForSpawnFake();
|
|
498
|
+
// 非贴边断言:waitForSpawnFake 每步 10ms 推进,watchdog 挂载时刻有 ≤10ms 抖动,
|
|
499
|
+
// 用 55s / 61s 两个窗口避开 60s 贴边比较
|
|
500
|
+
await vi.advanceTimersByTimeAsync(55_000);
|
|
501
|
+
expect(child.killed).toBe(false);
|
|
502
|
+
await vi.advanceTimersByTimeAsync(6_000);
|
|
503
|
+
expect(child.killed).toBe(true);
|
|
504
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
505
|
+
|
|
506
|
+
emitStdoutLine(child, sessionHeader());
|
|
507
|
+
child.stdout.end();
|
|
508
|
+
child.emit("close", 143);
|
|
509
|
+
const result = await promise;
|
|
510
|
+
expect(result.success).toBe(true);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// ── 3. watchdog 到期前正常完成 → clearTimeout 生效,不 kill ──
|
|
515
|
+
describe("watchdog 到期前正常完成 → 不 kill", () => {
|
|
516
|
+
beforeEach(() => {
|
|
517
|
+
vi.useFakeTimers();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
afterEach(() => {
|
|
521
|
+
vi.useRealTimers();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it("正常 close(0) 先于 watchdog 到期 → clearTimeout 生效,推进时间后 child 未被 kill", async () => {
|
|
525
|
+
const record = makeRecord();
|
|
526
|
+
// maxTurns=6 → watchdog=30min
|
|
527
|
+
const promise = runSpawn(record, "Task: quick", makeOpts({ maxTurns: 6 }), makeCtx());
|
|
528
|
+
|
|
529
|
+
const child = await waitForSpawnFake();
|
|
530
|
+
|
|
531
|
+
// 正常完成:emit header + close(0)(远早于 30min watchdog)
|
|
532
|
+
emitStdoutLine(child, sessionHeader());
|
|
533
|
+
child.stdout.end();
|
|
534
|
+
child.emit("close", 0);
|
|
535
|
+
|
|
536
|
+
const result = await promise;
|
|
537
|
+
expect(result.success).toBe(true);
|
|
538
|
+
|
|
539
|
+
// close 后 runSpawn 已 clearTimeout(watchdog);推进 30+ 分钟验证 watchdog 未触发 kill
|
|
540
|
+
await vi.advanceTimersByTimeAsync(30 * 60 * 1000 + 100);
|
|
541
|
+
|
|
542
|
+
expect(child.killed).toBe(false);
|
|
543
|
+
expect(child.killSignal).toBeUndefined();
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// ── 3. 外部 signal abort → child.kill ──
|
|
548
|
+
//
|
|
549
|
+
// [d] onAbort = () => child.kill("SIGTERM"),opts.signal.addEventListener("abort", onAbort, {once:true})。
|
|
550
|
+
// 前置检查:if (opts.signal?.aborted) onAbort()——spawn 前已 aborted 时 addEventListener
|
|
551
|
+
// 不会触发,立即 kill 兑现取消语义。
|
|
552
|
+
describe("外部 signal abort → child.kill", () => {
|
|
553
|
+
it("运行中 abort signal → child.kill(SIGTERM) 被调用,success=false(取消语义)", async () => {
|
|
554
|
+
const controller = new AbortController();
|
|
555
|
+
const record = makeRecord();
|
|
556
|
+
const promise = runSpawn(
|
|
557
|
+
record,
|
|
558
|
+
"Task: cancelled",
|
|
559
|
+
makeOpts({ signal: controller.signal }),
|
|
560
|
+
makeCtx(),
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const child = await waitForSpawnReal();
|
|
564
|
+
|
|
565
|
+
// abort 必须在 spawn 之后(addEventListener 已注册)。
|
|
566
|
+
// queueMicrotask 延迟到当前微任务清空后触发,确保 listener 已挂载。
|
|
567
|
+
queueMicrotask(() => controller.abort());
|
|
568
|
+
|
|
569
|
+
// emit header + close(被 kill 后子进程退出,signal 终止 exitCode>=128)
|
|
570
|
+
emitStdoutLine(child, sessionHeader());
|
|
571
|
+
child.stdout.end();
|
|
572
|
+
child.emit("close", 143); // SIGTERM = 128+15
|
|
573
|
+
|
|
574
|
+
const result = await promise;
|
|
575
|
+
|
|
576
|
+
expect(child.killed).toBe(true);
|
|
577
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
578
|
+
// signal.aborted 路径:success=false,但 error 为 undefined(取消不算 error)
|
|
579
|
+
expect(result.success).toBe(false);
|
|
580
|
+
expect(result.error).toBeUndefined();
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("spawn 前已 aborted 的 signal → 前置检查立即 kill,兑现取消语义", async () => {
|
|
584
|
+
// 覆盖 session-runner.ts L570: if (opts.signal?.aborted) onAbort()
|
|
585
|
+
// 已 aborted 的 signal addEventListener("abort") 不会再触发回调,
|
|
586
|
+
// 故 runSpawn 在注册 listener 后立即前置检查,直接 kill 兑现取消。
|
|
587
|
+
const controller = new AbortController();
|
|
588
|
+
controller.abort(); // spawn 前已 abort
|
|
589
|
+
|
|
590
|
+
const record = makeRecord();
|
|
591
|
+
const promise = runSpawn(
|
|
592
|
+
record,
|
|
593
|
+
"Task: pre-aborted",
|
|
594
|
+
makeOpts({ signal: controller.signal }),
|
|
595
|
+
makeCtx(),
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
const child = await waitForSpawnReal();
|
|
599
|
+
|
|
600
|
+
// 前置检查在 spawn 后同步执行 → child 立即被 kill
|
|
601
|
+
expect(child.killed).toBe(true);
|
|
602
|
+
expect(child.killSignal).toBe("SIGTERM");
|
|
603
|
+
|
|
604
|
+
// 收尾:emit close 让 runSpawn resolve
|
|
605
|
+
emitStdoutLine(child, sessionHeader());
|
|
606
|
+
child.stdout.end();
|
|
607
|
+
child.emit("close", 143);
|
|
608
|
+
|
|
609
|
+
const result = await promise;
|
|
610
|
+
// signal.aborted → success=false,error undefined
|
|
611
|
+
expect(result.success).toBe(false);
|
|
612
|
+
expect(result.error).toBeUndefined();
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/__tests__/tombstone-store.test.ts
|
|
2
|
+
//
|
|
3
|
+
// tombstone-store 专属测试。
|
|
4
|
+
// 覆盖:write→read 往返 / 缺 sidecar → undefined / 损坏 sidecar → undefined / 结构校验。
|
|
5
|
+
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
import { readCancelledTombstone, writeCancelledTombstone } from "../tombstone-store.ts";
|
|
13
|
+
|
|
14
|
+
describe("tombstone-store", () => {
|
|
15
|
+
let tmpDir: string;
|
|
16
|
+
let sessionFile: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ts-test-"));
|
|
20
|
+
sessionFile = path.join(tmpDir, "2026-01-01_uuid.jsonl");
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("write → read 往返", () => {
|
|
27
|
+
it("写入后读回完整数据", () => {
|
|
28
|
+
writeCancelledTombstone(sessionFile, {
|
|
29
|
+
id: "bg-1", status: "cancelled", agent: "worker", startedAt: 1000, endedAt: 2000,
|
|
30
|
+
});
|
|
31
|
+
const tomb = readCancelledTombstone(sessionFile);
|
|
32
|
+
expect(tomb).toEqual({
|
|
33
|
+
id: "bg-1", status: "cancelled", agent: "worker", startedAt: 1000, endedAt: 2000,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("sidecar 路径 = sessionFile + '.cancelled'", () => {
|
|
38
|
+
writeCancelledTombstone(sessionFile, {
|
|
39
|
+
id: "bg-1", status: "cancelled", agent: "w", startedAt: 1, endedAt: 2,
|
|
40
|
+
});
|
|
41
|
+
expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("读降级", () => {
|
|
46
|
+
it("无 sidecar → undefined(正常——非 cancelled record)", () => {
|
|
47
|
+
expect(readCancelledTombstone(sessionFile)).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("损坏 JSON → undefined", () => {
|
|
51
|
+
fs.writeFileSync(`${sessionFile}.cancelled`, "NOT JSON\n", "utf-8");
|
|
52
|
+
expect(readCancelledTombstone(sessionFile)).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("status 非 'cancelled' → undefined", () => {
|
|
56
|
+
fs.writeFileSync(
|
|
57
|
+
`${sessionFile}.cancelled`,
|
|
58
|
+
`${JSON.stringify({ id: "x", status: "done", agent: "w", startedAt: 1, endedAt: 2 })}\n`,
|
|
59
|
+
"utf-8",
|
|
60
|
+
);
|
|
61
|
+
expect(readCancelledTombstone(sessionFile)).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("缺必填字段 → undefined", () => {
|
|
65
|
+
fs.writeFileSync(
|
|
66
|
+
`${sessionFile}.cancelled`,
|
|
67
|
+
`${JSON.stringify({ id: "x", status: "cancelled" })}\n`, // 缺 agent/startedAt/endedAt
|
|
68
|
+
"utf-8",
|
|
69
|
+
);
|
|
70
|
+
expect(readCancelledTombstone(sessionFile)).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|