@zhushanwen/subagent-core 0.2.0 → 0.5.1
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 +45 -2
- package/agents/analyst.md +60 -0
- package/agents/coder.md +69 -0
- package/agents/debugger.md +66 -0
- package/agents/doc-reviewer.md +49 -0
- package/agents/explorer.md +63 -0
- package/agents/general-purpose.md +32 -0
- package/agents/orchestrator.md +61 -0
- package/agents/planner.md +53 -0
- package/agents/researcher.md +67 -0
- package/agents/reviewer.md +73 -0
- package/dist/chunk-43ONBFZX.js +240 -0
- package/dist/chunk-APZY4IME.js +27 -0
- package/dist/chunk-V3VHZ2VX.js +59 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +62 -9
- package/dist/execution/engine/engines/zcode/constants.d.cts +116 -12
- package/dist/execution/engine/engines/zcode/constants.d.ts +116 -12
- package/dist/execution/engine/engines/zcode/constants.js +39 -7
- package/dist/execution/engine/engines/zcode/reader.d.cts +2 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +2 -1
- package/dist/execution/engine/engines/zcode/reader.js +4 -234
- package/dist/execution/engine/paths.js +7 -19
- package/dist/index.cjs +18234 -1277
- package/dist/index.d.cts +5646 -673
- package/dist/index.d.ts +5646 -673
- package/dist/index.js +17828 -1277
- package/dist/{types-BxyAidGf.d.cts → types-Dv4QhSJ_.d.cts} +678 -112
- package/dist/{types-BxyAidGf.d.ts → types-Dv4QhSJ_.d.ts} +678 -112
- package/package.json +7 -4
- package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +0 -1
- package/src/__tests__/append-system-prompt-assembly.test.ts +8 -7
- package/src/__tests__/fr4-get-state-handshake.test.ts +2 -2
- package/src/__tests__/m2-append-content-probe.test.ts +0 -1
- package/src/__tests__/manifest-store.test.ts +10 -9
- package/src/__tests__/record-store-cache.test.ts +0 -2
- package/src/__tests__/record-store-index.test.ts +1 -2
- package/src/__tests__/review-fix-loop-script.test.ts +93 -3
- package/src/__tests__/review-fix-loop-utils.test.ts +31 -10
- package/src/__tests__/robustness-low-batch1.test.ts +6 -5
- package/src/__tests__/robustness-medium-batch1.test.ts +2 -2
- package/src/__tests__/robustness-medium-batch2.test.ts +17 -6
- package/src/__tests__/robustness-medium-batch4.test.ts +2 -2
- package/src/__tests__/session-runner.test.ts +1 -1
- package/src/__tests__/smoke.test.ts +9 -2
- package/src/core/__tests__/host-services.test.ts +2 -2
- package/src/core/__tests__/logger.test.ts +1 -1
- package/src/core/error-message.ts +9 -0
- package/src/core/host-services.ts +21 -5
- package/src/core/notify-ports.ts +18 -5
- package/src/execution/__tests__/agent-profile.test.ts +232 -0
- package/src/execution/__tests__/agent-registry.test.ts +370 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +18 -2
- package/src/execution/__tests__/agents-assembly.test.ts +219 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +6 -6
- package/src/execution/__tests__/chat-engine-routing.test.ts +136 -30
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +5 -4
- package/src/execution/__tests__/cold-resurrect.test.ts +283 -0
- package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
- package/src/execution/__tests__/delivery-methods.test.ts +77 -62
- package/src/execution/__tests__/descendant-sweep-guards.test.ts +220 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
- package/src/execution/__tests__/dialog-queue.test.ts +199 -1
- package/src/execution/__tests__/engine-model-validation.test.ts +741 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +31 -24
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +21 -91
- package/src/execution/__tests__/execute-nesting.test.ts +34 -188
- package/src/execution/__tests__/execution-record.test.ts +217 -60
- package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +3 -3
- package/src/execution/__tests__/finalize-record.test.ts +278 -16
- package/src/execution/__tests__/gc-timer.test.ts +57 -2
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +14 -12
- package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +1 -1
- package/src/execution/__tests__/helpers/session-runner-mocks.ts +113 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +150 -3
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +137 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +280 -0
- package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
- package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +1 -2
- package/src/execution/__tests__/manifest-parentid.test.ts +4 -4
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
- package/src/execution/__tests__/model-resolver.test.ts +2 -2
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +23 -121
- package/src/execution/__tests__/nested-visibility.test.ts +2 -0
- package/src/execution/__tests__/notify-ledger.test.ts +1249 -0
- package/src/execution/__tests__/output-collector.test.ts +117 -6
- package/src/execution/__tests__/pi-invocation.test.ts +1 -1
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
- package/src/execution/__tests__/record-store.test.ts +89 -5
- package/src/execution/__tests__/records-cwd-isolation.test.ts +2 -2
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +29 -132
- package/src/execution/__tests__/recursive-visibility-env.test.ts +55 -179
- package/src/execution/__tests__/rpc-mode.test.ts +2 -2
- package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +387 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +19 -9
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +5 -4
- package/src/execution/__tests__/run-spawn-edges.test.ts +418 -26
- package/src/execution/__tests__/run-spawn-integration.test.ts +3 -3
- package/src/execution/__tests__/run-spawn-resume.test.ts +2 -2
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +2 -2
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +16 -8
- package/src/execution/__tests__/service-kill-escalation.test.ts +91 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +0 -2
- package/src/execution/__tests__/session-file-gc.test.ts +35 -0
- package/src/execution/__tests__/session-pending.test.ts +471 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +366 -15
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
- package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
- package/src/execution/__tests__/session-runner-dispatch.test.ts +489 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +4 -3
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +227 -0
- package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +290 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +7 -5
- package/src/execution/__tests__/settled-watchdog.test.ts +524 -0
- package/src/execution/__tests__/spawn-args.test.ts +11 -10
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +26 -23
- package/src/execution/__tests__/spawn-event-adapter.test.ts +1 -1
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +5 -4
- package/src/execution/__tests__/spawned-children.test.ts +155 -11
- package/src/execution/__tests__/start-sync-model-guard.test.ts +4 -4
- package/src/execution/__tests__/stdin-writer.test.ts +6 -4
- package/src/execution/__tests__/stream-sink-retirement.test.ts +6 -6
- package/src/execution/__tests__/subagent-actions-core.test.ts +1017 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +99 -35
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +271 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +33 -29
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +341 -0
- package/src/execution/__tests__/subagent-service.test.ts +37 -30
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +88 -12
- package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +75 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +22 -20
- package/src/execution/__tests__/temp-prompt.test.ts +1 -1
- package/src/execution/__tests__/timeout-integration.test.ts +13 -12
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +1 -1
- package/src/execution/__tests__/turn-limiter.test.ts +1 -1
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +107 -7
- package/src/execution/__tests__/ui-request-handler.test.ts +4 -3
- package/src/execution/__tests__/ui-request-queue.test.ts +167 -2
- package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
- package/src/execution/__tests__/worktree-manager.test.ts +14 -5
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +4 -3
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
- package/src/execution/agent-registry.ts +298 -30
- package/src/execution/agent-result-mapper.ts +3 -0
- package/src/execution/agents-assembly.ts +88 -0
- package/src/execution/cold-resurrect.ts +199 -0
- package/src/execution/concurrency-pool.ts +71 -9
- package/src/execution/dialog-queue.ts +101 -3
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +91 -0
- package/src/execution/engine/__tests__/common/data-dir.test.ts +1 -1
- package/src/execution/engine/__tests__/common/journal-wiring.test.ts +84 -0
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +34 -7
- package/src/execution/engine/__tests__/common/persona-router.test.ts +4 -5
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +211 -51
- package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +149 -0
- package/src/execution/engine/__tests__/common/session-view-service.test.ts +499 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +56 -99
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +88 -15
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +33 -7
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +75 -54
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +128 -0
- package/src/execution/engine/__tests__/engine-discovery.test.ts +15 -3
- package/src/execution/engine/__tests__/model-prompt.test.ts +15 -3
- package/src/execution/engine/__tests__/registry.test.ts +94 -3
- package/src/execution/engine/__tests__/routing.test.ts +115 -6
- package/src/execution/engine/common/capability-gate.ts +92 -0
- package/src/execution/engine/common/journal-wiring.ts +97 -0
- package/src/execution/engine/common/kill-chain.ts +42 -10
- package/src/execution/engine/common/nesting-guard.ts +66 -0
- package/src/execution/engine/common/persona-router.ts +18 -8
- package/src/execution/engine/common/pool-manager.ts +332 -78
- package/src/execution/engine/common/session-view-service.ts +506 -0
- package/src/execution/engine/common/session-view-types.ts +125 -0
- package/src/execution/engine/engine-discovery.ts +12 -16
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +174 -28
- package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +257 -0
- package/src/execution/{argv-mirror.ts → engine/engines/pi/argv-mirror.ts} +43 -21
- package/src/execution/{get-state-handshake.ts → engine/engines/pi/get-state-handshake.ts} +86 -11
- package/src/execution/{output-collector.ts → engine/engines/pi/output-collector.ts} +108 -19
- package/src/execution/engine/engines/pi/pi-engine.ts +434 -38
- package/src/execution/{pi-invocation.ts → engine/engines/pi/pi-invocation.ts} +2 -2
- package/src/execution/engine/engines/pi/reader.ts +11 -0
- package/src/execution/engine/engines/pi/registration.ts +4 -2
- package/src/execution/engine/engines/pi/session-runner.ts +2840 -0
- package/src/execution/{spawn-event-adapter.ts → engine/engines/pi/spawn-event-adapter.ts} +75 -34
- package/src/execution/{stdin-writer.ts → engine/engines/pi/stdin-writer.ts} +10 -9
- package/src/execution/{temp-prompt.ts → engine/engines/pi/temp-prompt.ts} +2 -2
- package/src/execution/{turn-limiter.ts → engine/engines/pi/turn-limiter.ts} +1 -1
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +301 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +321 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +473 -0
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +7 -210
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +6 -110
- package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +247 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +451 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +792 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +782 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +168 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +210 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +353 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +282 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +372 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +65 -47
- package/src/execution/engine/engines/zcode/appserver-launcher.ts +167 -0
- package/src/execution/engine/engines/zcode/connection.ts +598 -0
- package/src/execution/engine/engines/zcode/constants.ts +147 -14
- package/src/execution/engine/engines/zcode/golden-sample.ts +42 -34
- package/src/execution/engine/engines/zcode/parser.ts +12 -346
- package/src/execution/engine/engines/zcode/preparer.ts +22 -150
- package/src/execution/engine/engines/zcode/session-channel.ts +968 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +1023 -250
- package/src/execution/engine/host-task-spec.ts +32 -36
- package/src/execution/engine/model-validation.ts +177 -0
- package/src/execution/engine/port.ts +65 -7
- package/src/execution/engine/registry.ts +53 -0
- package/src/execution/engine/routing.ts +75 -1
- package/src/execution/engine/types.ts +30 -103
- package/src/execution/execution-record.ts +222 -144
- package/src/execution/finalize-record.ts +181 -85
- package/src/execution/idle-gc.ts +28 -0
- package/src/execution/lifecycle-manager.ts +27 -3
- package/src/execution/lifecycle-predicates.ts +1 -1
- package/src/execution/manifest-store.ts +53 -62
- package/src/execution/notifier.ts +10 -10
- package/src/execution/notify-host.ts +212 -0
- package/src/execution/notify-ledger.ts +117 -16
- package/src/execution/record-entry.ts +8 -2
- package/src/execution/record-store.ts +148 -88
- package/src/execution/round-settlement.ts +93 -0
- package/src/execution/session-file-gc.ts +96 -62
- package/src/execution/session-pending.ts +213 -62
- package/src/execution/session-reconstructor.ts +256 -143
- package/src/execution/sessions-index.ts +127 -102
- package/src/execution/settled-watchdog.ts +311 -0
- package/src/execution/subagent-actions-core.ts +686 -0
- package/src/execution/subagent-service.ts +1106 -907
- package/src/execution/subprocess-agent-runner.ts +144 -97
- package/src/execution/types.ts +9 -0
- package/src/execution/ui-request-handler-factory.ts +126 -75
- package/src/execution/ui-request-queue.ts +50 -19
- package/src/execution/worktree-git-ops.ts +397 -0
- package/src/execution/worktree-manager.ts +72 -10
- package/src/execution/worktree-registry.ts +6 -12
- package/src/index.ts +542 -9
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +3 -5
- package/src/orchestration/__tests__/agent-call-stream.test.ts +2 -2
- package/src/orchestration/__tests__/args-meta.test.ts +358 -0
- package/src/orchestration/__tests__/args-validator.test.ts +14 -0
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +384 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +151 -11
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store.test.ts +390 -0
- package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
- package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +3 -3
- package/src/orchestration/__tests__/lifecycle.test.ts +84 -5
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +1 -1
- package/src/orchestration/__tests__/run-snapshot.test.ts +356 -0
- package/src/orchestration/__tests__/script-generate.test.ts +314 -0
- package/src/orchestration/__tests__/script-lint.test.ts +17 -0
- package/src/orchestration/__tests__/test-mocks.ts +2 -0
- package/src/orchestration/__tests__/worker-exit-without-result.test.ts +7 -6
- package/src/orchestration/__tests__/worker-message-pump-finalize-run.test.ts +229 -0
- package/src/orchestration/__tests__/{error-recovery-handlers.test.ts → worker-message-pump-handlers.test.ts} +28 -27
- package/src/orchestration/__tests__/{error-recovery-postmessage-defense.test.ts → worker-message-pump-postmessage-defense.test.ts} +8 -8
- package/src/orchestration/__tests__/{error-recovery-serialize-failed-result.test.ts → worker-message-pump-serialize-failed-result.test.ts} +2 -2
- package/src/orchestration/__tests__/{error-recovery-workflow-call.test.ts → worker-message-pump-workflow-call.test.ts} +1 -1
- package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
- package/src/orchestration/__tests__/worker-script-builder.test.ts +1 -1
- package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +1 -1
- package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
- package/src/orchestration/agent-opts-resolver.ts +8 -8
- package/src/orchestration/args-meta.ts +198 -0
- package/src/orchestration/args-validator.ts +53 -31
- package/src/orchestration/execute-agent-call.ts +25 -79
- package/src/orchestration/file-run-store.ts +327 -0
- package/src/orchestration/launcher.ts +160 -104
- package/src/orchestration/lifecycle.ts +371 -97
- package/src/orchestration/models/agent-call.ts +7 -7
- package/src/orchestration/models/budget.ts +5 -5
- package/src/orchestration/models/ports.ts +4 -4
- package/src/orchestration/models/run-runtime.ts +15 -15
- package/src/orchestration/models/trace.ts +9 -9
- package/src/orchestration/models/types.ts +111 -22
- package/src/orchestration/models/workflow-run.ts +28 -28
- package/src/orchestration/models/workflow-script.ts +4 -4
- package/src/orchestration/run-snapshot.ts +266 -0
- package/src/orchestration/script-generate.ts +154 -0
- package/src/orchestration/script-lint.ts +123 -90
- package/src/orchestration/worker-handle.ts +10 -10
- package/src/orchestration/worker-host.ts +10 -10
- package/src/orchestration/{error-recovery.ts → worker-message-pump.ts} +498 -221
- package/src/orchestration/worker-script-builder.ts +401 -346
- package/src/orchestration/workflow-files.ts +37 -11
- package/src/orchestration/workflow-run-summary.ts +69 -0
- package/src/orchestration/workflow-script-registry-impl.ts +31 -9
- package/src/shared/__tests__/agent-ref.test.ts +209 -2
- package/src/shared/__tests__/atomic-write.test.ts +267 -0
- package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
- package/src/shared/__tests__/injection-render.test.ts +518 -0
- package/src/shared/__tests__/meta-parser.test.ts +151 -0
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
- package/src/shared/__tests__/resource-discovery.test.ts +261 -217
- package/src/shared/agent-ref.ts +143 -2
- package/src/shared/atomic-write.ts +323 -0
- package/src/shared/bounded-serialize.ts +154 -0
- package/src/shared/injection-render.ts +279 -0
- package/src/shared/meta-parser.ts +182 -68
- package/src/shared/model-ref.ts +15 -2
- package/src/shared/resource-discovery.ts +97 -204
- package/src/shared/resource-meta.ts +14 -0
- package/src/shared/schema-jsonify.ts +1 -1
- package/src/shared/xml-injection.ts +9 -9
- package/workflows/README.md +9 -9
- package/workflows/chain.js +4 -2
- package/workflows/map-reduce.js +5 -3
- package/workflows/parallel.js +5 -3
- package/workflows/review-fix-loop-utils.cjs +165 -92
- package/workflows/review-fix-loop-utils.d.cts +287 -0
- package/workflows/review-fix-loop.js +18 -7
- package/workflows/scatter-gather.js +4 -2
- package/dist/chunk-3VOERJPJ.js +0 -22
- package/src/execution/__tests__/execute-options-mapper.test.ts +0 -200
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -580
- package/src/execution/engine/engines/zcode/launcher.ts +0 -161
- package/src/execution/execute-options-mapper.ts +0 -115
- package/src/execution/session-runner.ts +0 -1789
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -288
|
@@ -26,14 +26,25 @@ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
|
26
26
|
|
|
27
27
|
// mock session-runner:runSpawn/killAllSpawnedChildren 占位(import 链需要),
|
|
28
28
|
// getChildByRecord 默认返回 undefined(无活进程 → isResumable=true),用例内按需覆盖。
|
|
29
|
-
vi.mock("../session-runner.ts", () => ({
|
|
29
|
+
vi.mock("../engine/engines/pi/session-runner.ts", () => ({
|
|
30
30
|
runSpawn: vi.fn(),
|
|
31
31
|
killAllSpawnedChildren: vi.fn(),
|
|
32
32
|
getChildByRecord: vi.fn(() => undefined),
|
|
33
|
+
registerSpawnedChildForRecord: vi.fn(),
|
|
34
|
+
killRecordChildWithEscalation: vi.fn(),
|
|
33
35
|
spawnedChildren: new Map(),
|
|
34
36
|
}));
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
// [D8] idle-gc 归档时释放引擎池引用(releasePoolRef 经 getEngineDataDir 解析
|
|
39
|
+
// dataDir)——测试钉到模块级 holder(vi.mock factory 闭包只能引用模块级变量),
|
|
40
|
+
// beforeEach 刷新为当前 tmp agentDir,防触碰真实数据目录。
|
|
41
|
+
let gcDataDirHolder = "";
|
|
42
|
+
vi.mock("../engine/common/data-dir.ts", () => ({
|
|
43
|
+
getEngineDataDir: () => gcDataDirHolder,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
import { acquirePool } from "../engine/common/pool-manager.ts";
|
|
47
|
+
import { getChildByRecord } from "../engine/engines/pi/session-runner.ts";
|
|
37
48
|
import { createRecord } from "../execution-record.ts";
|
|
38
49
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
39
50
|
import { RecordStore } from "../record-store.ts";
|
|
@@ -95,6 +106,7 @@ describe("[M8] idle record GC 定时器(startGcTimer)", () => {
|
|
|
95
106
|
beforeEach(() => {
|
|
96
107
|
vi.useFakeTimers();
|
|
97
108
|
agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "gc-timer-"));
|
|
109
|
+
gcDataDirHolder = agentDir;
|
|
98
110
|
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
99
111
|
service = new SubagentService({ cwd: agentDir, modelService });
|
|
100
112
|
service.initSession({ pi: makePi(), sessionId: "root-session" });
|
|
@@ -181,4 +193,47 @@ describe("[M8] idle record GC 定时器(startGcTimer)", () => {
|
|
|
181
193
|
|
|
182
194
|
expect(store.getMutable("sa-after-dispose")).toBeDefined();
|
|
183
195
|
});
|
|
196
|
+
|
|
197
|
+
it("[D8] 归档超 TTL record 时释放引擎池引用:journal 跟随删除、refs 移除、归零删池原生状态", () => {
|
|
198
|
+
// 建池(engine 缺省投影 'pi' + poolKey 'shared'——与 runEngineTask 回填形态一致)
|
|
199
|
+
const poolDir = acquirePool(agentDir, "pi", "shared", "sa-pool-1");
|
|
200
|
+
fs.mkdirSync(path.join(poolDir, "native-state"), { recursive: true });
|
|
201
|
+
fs.writeFileSync(path.join(poolDir, "native-state", "db.sqlite"), "x");
|
|
202
|
+
fs.writeFileSync(path.join(poolDir, "journal-sa-pool-1.jsonl"), "{}\n");
|
|
203
|
+
fs.writeFileSync(path.join(poolDir, "journal-sa-pool-2.jsonl"), "{}\n"); // 他人 journal(refs 无条目)
|
|
204
|
+
|
|
205
|
+
const fireAt = Date.now() + GC_INTERVAL_MS;
|
|
206
|
+
const record = makeIdleRecord("sa-pool-1", fireAt - IDLE_TTL_MS - 1);
|
|
207
|
+
record.engineHandle = { sessionRef: {}, poolKey: "shared" };
|
|
208
|
+
store.register(record);
|
|
209
|
+
|
|
210
|
+
service.startGcTimer();
|
|
211
|
+
vi.advanceTimersByTime(GC_INTERVAL_MS);
|
|
212
|
+
|
|
213
|
+
expect(store.getMutable("sa-pool-1")).toBeUndefined(); // 已归档
|
|
214
|
+
// release 语义:自己的 journal 删、他人 journal 保留、归零删原生状态、目录保留(剩 journal)
|
|
215
|
+
expect(fs.existsSync(path.join(poolDir, "journal-sa-pool-1.jsonl"))).toBe(false);
|
|
216
|
+
expect(fs.existsSync(path.join(poolDir, "journal-sa-pool-2.jsonl"))).toBe(true);
|
|
217
|
+
expect(fs.existsSync(path.join(poolDir, "native-state"))).toBe(false);
|
|
218
|
+
expect(fs.existsSync(poolDir)).toBe(true);
|
|
219
|
+
// refs 归零后随池清理(无残留幻影引用)
|
|
220
|
+
const refsPath = path.join(poolDir, "refs.json");
|
|
221
|
+
if (fs.existsSync(refsPath)) {
|
|
222
|
+
expect(Object.keys((JSON.parse(fs.readFileSync(refsPath, "utf8")) as { refs: object }).refs)).toEqual([]);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("[D8] 无 engineHandle 的 record 归档不触碰引擎池(存量 record 零影响)", () => {
|
|
227
|
+
const poolDir = acquirePool(agentDir, "pi", "shared", "sa-plain");
|
|
228
|
+
fs.writeFileSync(path.join(poolDir, "journal-sa-plain.jsonl"), "{}\n");
|
|
229
|
+
|
|
230
|
+
const fireAt = Date.now() + GC_INTERVAL_MS;
|
|
231
|
+
store.register(makeIdleRecord("sa-plain", fireAt - IDLE_TTL_MS - 1)); // 无 engineHandle
|
|
232
|
+
|
|
233
|
+
service.startGcTimer();
|
|
234
|
+
vi.advanceTimersByTime(GC_INTERVAL_MS);
|
|
235
|
+
|
|
236
|
+
expect(store.getMutable("sa-plain")).toBeUndefined(); // 正常归档
|
|
237
|
+
expect(fs.existsSync(path.join(poolDir, "journal-sa-plain.jsonl"))).toBe(true); // 池/journal 不动
|
|
238
|
+
});
|
|
184
239
|
});
|
|
@@ -27,10 +27,12 @@ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
|
27
27
|
|
|
28
28
|
// mock session-runner:import 链需要(getRecordForAction 本身不调 spawn,仅守卫读
|
|
29
29
|
// hasLiveProcessHandle——默认无活进程,与跨重启场景一致)。
|
|
30
|
-
vi.mock("../session-runner.ts", () => ({
|
|
30
|
+
vi.mock("../engine/engines/pi/session-runner.ts", () => ({
|
|
31
31
|
runSpawn: vi.fn(),
|
|
32
32
|
killAllSpawnedChildren: vi.fn(),
|
|
33
33
|
getChildByRecord: vi.fn(() => undefined),
|
|
34
|
+
registerSpawnedChildForRecord: vi.fn(),
|
|
35
|
+
killRecordChildWithEscalation: vi.fn(),
|
|
34
36
|
spawnedChildren: new Map(),
|
|
35
37
|
}));
|
|
36
38
|
|
|
@@ -167,7 +169,7 @@ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)",
|
|
|
167
169
|
const file = writeSessionJsonl(sessionsDir, { id: "sa-restart-1", rootSessionId: "root-session" });
|
|
168
170
|
expect(store.getMutable("sa-restart-1")).toBeUndefined(); // 前置:内存确无
|
|
169
171
|
|
|
170
|
-
const record = service.getRecordForAction("sa-restart-1");
|
|
172
|
+
const record = service.chatActions.getRecordForAction("sa-restart-1");
|
|
171
173
|
|
|
172
174
|
// [v4 A-3] 无条件 chatMode=true(跨重启恢复入口)
|
|
173
175
|
expect(record.chatMode).toBe(true);
|
|
@@ -184,7 +186,7 @@ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)",
|
|
|
184
186
|
expect(record.status).toBe("running");
|
|
185
187
|
// register 生效:进内存,二次调用走内存命中(同一引用)
|
|
186
188
|
expect(store.getMutable("sa-restart-1")).toBe(record);
|
|
187
|
-
expect(service.getRecordForAction("sa-restart-1")).toBe(record);
|
|
189
|
+
expect(service.chatActions.getRecordForAction("sa-restart-1")).toBe(record);
|
|
188
190
|
});
|
|
189
191
|
|
|
190
192
|
it("rootSessionId 校验仍生效:异树 record(other-session)→ throw not found or not owned", () => {
|
|
@@ -192,7 +194,7 @@ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)",
|
|
|
192
194
|
// 但后置校验 record.rootSessionId !== this.sessionRootId 必须拦截
|
|
193
195
|
writeSessionJsonl(sessionsDir, { id: "sa-foreign", rootSessionId: "other-session" });
|
|
194
196
|
|
|
195
|
-
expect(() => service.getRecordForAction("sa-foreign")).toThrow(/not found or not owned/);
|
|
197
|
+
expect(() => service.chatActions.getRecordForAction("sa-foreign")).toThrow(/not found or not owned/);
|
|
196
198
|
});
|
|
197
199
|
|
|
198
200
|
it("直接父校验仍生效:孙级 record(parentRecordId=sa-parent)→ 主进程 throw direct parent", () => {
|
|
@@ -204,14 +206,14 @@ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)",
|
|
|
204
206
|
});
|
|
205
207
|
|
|
206
208
|
// 重建后 parentRecordId=sa-parent ≠ 主进程 baseline(undefined) → cross-layer 守卫拦截
|
|
207
|
-
expect(() => service.getRecordForAction("sa-grand")).toThrow(/direct parent/);
|
|
209
|
+
expect(() => service.chatActions.getRecordForAction("sa-grand")).toThrow(/direct parent/);
|
|
208
210
|
});
|
|
209
211
|
|
|
210
212
|
it(".finalized sidecar(closed 终态)不重建 → throw not found or not owned", () => {
|
|
211
213
|
const file = writeSessionJsonl(sessionsDir, { id: "sa-fin", rootSessionId: "root-session" });
|
|
212
214
|
writeFinalized(file); // sidecar 矩阵分支 2 → status=closed → find(status==="running") miss
|
|
213
215
|
|
|
214
|
-
expect(() => service.getRecordForAction("sa-fin")).toThrow(/not found or not owned/);
|
|
216
|
+
expect(() => service.chatActions.getRecordForAction("sa-fin")).toThrow(/not found or not owned/);
|
|
215
217
|
expect(store.getMutable("sa-fin")).toBeUndefined(); // 未重建注册
|
|
216
218
|
});
|
|
217
219
|
|
|
@@ -228,14 +230,14 @@ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)",
|
|
|
228
230
|
worktree: true,
|
|
229
231
|
});
|
|
230
232
|
|
|
231
|
-
const record = service.getRecordForAction("sa-wt");
|
|
233
|
+
const record = service.chatActions.getRecordForAction("sa-wt");
|
|
232
234
|
// hadWorktree 从磁盘 identity entry 的 worktree 标志恢复
|
|
233
235
|
expect(record.hadWorktree).toBe(true);
|
|
234
236
|
// handle 无法恢复(不可序列化)
|
|
235
237
|
expect(record.worktreeHandle).toBeUndefined();
|
|
236
238
|
|
|
237
|
-
// 冷路径续聊(无活进程)→
|
|
238
|
-
await expect(service.
|
|
239
|
+
// 冷路径续聊(无活进程)→ 续轮守卫拒绝,不 spawn 回落主 repo
|
|
240
|
+
await expect(service.chatActions.deliverChatMessage(record, "resume after restart", false)).rejects.toThrow(
|
|
239
241
|
/worktree isolation.*lost when the parent process restarted/,
|
|
240
242
|
);
|
|
241
243
|
});
|
|
@@ -244,11 +246,11 @@ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)",
|
|
|
244
246
|
// identity entry 无 worktree 字段(旧文件)→ found.worktree undefined → hadWorktree false
|
|
245
247
|
writeSessionJsonl(sessionsDir, { id: "sa-nowt", rootSessionId: "root-session" });
|
|
246
248
|
|
|
247
|
-
const record = service.getRecordForAction("sa-nowt");
|
|
249
|
+
const record = service.chatActions.getRecordForAction("sa-nowt");
|
|
248
250
|
expect(record.hadWorktree).toBe(false);
|
|
249
251
|
|
|
250
|
-
//
|
|
252
|
+
// 冷路径续聊不被 worktree 守卫拦截(resume 正常发起;后续 spawn 编排
|
|
251
253
|
// 超出本用例关注点——runSpawn 在本文件是 no-op mock)
|
|
252
|
-
await expect(service.
|
|
254
|
+
await expect(service.chatActions.deliverChatMessage(record, "resume normal", false)).resolves.toBeUndefined();
|
|
253
255
|
});
|
|
254
256
|
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/execution/__tests__/get-state-handshake.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [T1/RC-1] requestGetStateOnce 单测:agent_end 决策点惰性回补用的单次 get_state 请求辅助。
|
|
4
|
+
//
|
|
5
|
+
// 契约锚点(与 performGetStateHandshake 共用消息构造 sendGetStateCommand + 字段提取
|
|
6
|
+
// extractGetStateFields,但不做重试循环):
|
|
7
|
+
// 1. response 到达 → resolve 提取后的 GetStateResult(无条件 finish,单次语义无重试);
|
|
8
|
+
// 2. 超时 → resolve 空对象(调用方走保守分支);
|
|
9
|
+
// 3. stdin 同步写失败(EPIPE 形态)→ resolve 空对象、永不 reject(fire-and-forget 契约);
|
|
10
|
+
// 4. 自清理:finish 后从监听表注销本请求 resolver(消费注册器返回的注销函数);
|
|
11
|
+
// 注册器不返回注销函数时退化为 no-op(与握手同形态)。
|
|
12
|
+
//
|
|
13
|
+
// performGetStateHandshake 既有行为由 run-spawn-rpc-mode.test.ts 集成覆盖,此处不重复。
|
|
14
|
+
|
|
15
|
+
import { describe, expect, it, vi } from "vitest";
|
|
16
|
+
|
|
17
|
+
import { requestGetStateOnce } from "../engine/engines/pi/get-state-handshake.ts";
|
|
18
|
+
import type { ChildProcess } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
/** 最小 FakeChild:只需 stdin.write 行为(成功 / 可注入同步 throw)。 */
|
|
21
|
+
function makeFakeStdin(behavior?: { throwOnWrite?: Error }): { child: ChildProcess; writes: string[] } {
|
|
22
|
+
const writes: string[] = [];
|
|
23
|
+
const child = {
|
|
24
|
+
stdin: {
|
|
25
|
+
write(line: string): boolean {
|
|
26
|
+
if (behavior?.throwOnWrite) throw behavior.throwOnWrite;
|
|
27
|
+
writes.push(line);
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
} as unknown as ChildProcess;
|
|
32
|
+
return { child, writes };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 可控的监听表:模拟 stdout pump 的 get_stateListeners(注册返回注销函数)。 */
|
|
36
|
+
function makeListenerRegistry() {
|
|
37
|
+
const resolvers = new Map<string, (data: unknown) => void>();
|
|
38
|
+
const removed: string[] = [];
|
|
39
|
+
const add = (id: string, resolver: (data: unknown) => void) => {
|
|
40
|
+
resolvers.set(id, resolver);
|
|
41
|
+
return () => {
|
|
42
|
+
if (resolvers.get(id) === resolver) resolvers.delete(id);
|
|
43
|
+
removed.push(id);
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
return { resolvers, removed, add };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("requestGetStateOnce([T1/RC-1] 惰性回补单次请求)", () => {
|
|
50
|
+
it("response 到达 → resolve 提取的 sessionFile/sessionId,并从监听表注销", async () => {
|
|
51
|
+
const { child, writes } = makeFakeStdin();
|
|
52
|
+
const reg = makeListenerRegistry();
|
|
53
|
+
|
|
54
|
+
const promise = requestGetStateOnce(child, reg.add, 1000);
|
|
55
|
+
// 消息构造:单行 JSON {id, type:"get_state"}(与 performGetStateHandshake 同一 sendGetStateCommand)
|
|
56
|
+
expect(writes).toHaveLength(1);
|
|
57
|
+
const sent = JSON.parse(writes[0]!) as { id: string; type: string };
|
|
58
|
+
expect(sent.type).toBe("get_state");
|
|
59
|
+
expect(reg.resolvers.has(sent.id)).toBe(true);
|
|
60
|
+
|
|
61
|
+
// response 到达(含空串 sessionFile 应被过滤的字段形态覆盖提取规则)
|
|
62
|
+
reg.resolvers.get(sent.id)?.({
|
|
63
|
+
sessionFile: "/tmp/sessions/abc.jsonl",
|
|
64
|
+
sessionId: "sess-1",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await expect(promise).resolves.toEqual({
|
|
68
|
+
sessionFile: "/tmp/sessions/abc.jsonl",
|
|
69
|
+
sessionId: "sess-1",
|
|
70
|
+
});
|
|
71
|
+
// 自清理:finish 后从监听表移除本请求 resolver
|
|
72
|
+
expect(reg.resolvers.has(sent.id)).toBe(false);
|
|
73
|
+
expect(reg.removed).toEqual([sent.id]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("response 只含部分字段(仅 sessionId)→ resolve 已提取部分(单次语义:到达即 finish,不等重试)", async () => {
|
|
77
|
+
const { child } = makeFakeStdin();
|
|
78
|
+
const reg = makeListenerRegistry();
|
|
79
|
+
|
|
80
|
+
const promise = requestGetStateOnce(child, reg.add, 1000);
|
|
81
|
+
const id = reg.resolvers.keys().next().value as string;
|
|
82
|
+
reg.resolvers.get(id)?.({ sessionId: "only-id" });
|
|
83
|
+
|
|
84
|
+
await expect(promise).resolves.toEqual({ sessionId: "only-id" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("超时无 response → resolve 空对象(不 reject,不调注销前的 resolver)", async () => {
|
|
88
|
+
vi.useFakeTimers();
|
|
89
|
+
try {
|
|
90
|
+
const { child } = makeFakeStdin();
|
|
91
|
+
const reg = makeListenerRegistry();
|
|
92
|
+
|
|
93
|
+
const promise = requestGetStateOnce(child, reg.add, 1000);
|
|
94
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
95
|
+
|
|
96
|
+
await expect(promise).resolves.toEqual({});
|
|
97
|
+
} finally {
|
|
98
|
+
vi.useRealTimers();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("stdin 同步写失败(EPIPE code,writeStdinLine rethrow 路径)→ 立即 resolve 空对象、永不 reject", async () => {
|
|
103
|
+
// writeStdinLine 只对 code 为 EPIPE / ERR_STREAM_DESTROYED 的错误 rethrow([R3]),
|
|
104
|
+
// requestGetStateOnce 的 catch 捕获后按「回补失败」resolve 空对象(同超时语义)。
|
|
105
|
+
const epipeErr = Object.assign(new Error("write after end"), { code: "EPIPE" });
|
|
106
|
+
const { child } = makeFakeStdin({ throwOnWrite: epipeErr });
|
|
107
|
+
const reg = makeListenerRegistry();
|
|
108
|
+
|
|
109
|
+
await expect(requestGetStateOnce(child, reg.add, 1000)).resolves.toEqual({});
|
|
110
|
+
// 写失败路径不注册监听(未发出请求)
|
|
111
|
+
expect(reg.resolvers.size).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("注册器不返回注销函数(void 形态,对齐旧握手调用方)→ 正常 resolve 不抛", async () => {
|
|
115
|
+
const { child } = makeFakeStdin();
|
|
116
|
+
const resolvers = new Map<string, (data: unknown) => void>();
|
|
117
|
+
const addVoid = (id: string, resolver: (data: unknown) => void): void => {
|
|
118
|
+
resolvers.set(id, resolver);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const promise = requestGetStateOnce(child, addVoid, 1000);
|
|
122
|
+
const id = resolvers.keys().next().value as string;
|
|
123
|
+
resolvers.get(id)?.({ sessionFile: "/tmp/x.jsonl" });
|
|
124
|
+
|
|
125
|
+
await expect(promise).resolves.toEqual({ sessionFile: "/tmp/x.jsonl" });
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -21,7 +21,7 @@ export function mockExtensionApi(
|
|
|
21
21
|
const noop = (): void => { /* test mock: method not invoked by this test */ };
|
|
22
22
|
// Proxy<T> 泛型参数决定返回类型——直接声明为 ExtensionAPI,
|
|
23
23
|
// TS 接受(Proxy handler 对 target 的类型不约束 T)。
|
|
24
|
-
return new Proxy<ExtensionAPI>(overrides as ExtensionAPI, {
|
|
24
|
+
return new Proxy<ExtensionAPI>(overrides as unknown as ExtensionAPI, {
|
|
25
25
|
get(target, prop: string | symbol): unknown {
|
|
26
26
|
if (prop in target) return target[prop as keyof ExtensionAPI];
|
|
27
27
|
return noop;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/execution/__tests__/helpers/session-runner-mocks.ts
|
|
2
|
+
//
|
|
3
|
+
// session-runner 系测试(keep-alive-no-progress / recursive-visibility-env 等)的
|
|
4
|
+
// 测试文件侧装配:vi.mocked 取回 + spawn/keep-alive 前奏 + 子进程收尾。
|
|
5
|
+
//
|
|
6
|
+
// 与 spawn-mock.ts 的分工(依赖约束,勿混淆):
|
|
7
|
+
// - spawn-mock.ts 会被各 vi.mock 工厂 `await import()`(求值时机 = 被 mock 模块首次
|
|
8
|
+
// 请求),故它禁止值依赖 session-runner / alive-store / session-pending——否则形成
|
|
9
|
+
// 「mock 工厂 → helper → session-runner → node:child_process(mock 求值中)」循环。
|
|
10
|
+
// mock 模块工厂 + FakeChild + 纯 fixture 都在 spawn-mock.ts。
|
|
11
|
+
// - 本文件只被测试文件顶层静态 import(vi.mock 已 hoist 注册完毕、mock 工厂已可安全
|
|
12
|
+
// 执行),故可值依赖 session-runner / alive-store / session-pending 做 vi.mocked 取回
|
|
13
|
+
// 与 runSpawn 前奏封装。
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
|
|
18
|
+
import { afterEach, beforeEach, vi } from "vitest";
|
|
19
|
+
|
|
20
|
+
import { runSpawn, SPAWN_WATCHDOG_ENV } from "../../engine/engines/pi/session-runner.ts";
|
|
21
|
+
import {
|
|
22
|
+
listActivePendingFromSessionFile,
|
|
23
|
+
readActivePendingFromSessionFile,
|
|
24
|
+
} from "../../session-pending.ts";
|
|
25
|
+
import { isProcessAlive, readAliveMarker, writeAliveMarker } from "../../alive-store.ts";
|
|
26
|
+
import {
|
|
27
|
+
emitStdoutLine,
|
|
28
|
+
lastSpawnedChild,
|
|
29
|
+
makeCtx,
|
|
30
|
+
makeOpts,
|
|
31
|
+
makeRecord,
|
|
32
|
+
waitForSpawn,
|
|
33
|
+
type FakeChild,
|
|
34
|
+
} from "./spawn-mock.ts";
|
|
35
|
+
|
|
36
|
+
/** keep-alive 用例的 session header 行(stdout 首行)。 */
|
|
37
|
+
function sessionHeaderLine(): Record<string, unknown> {
|
|
38
|
+
return { type: "session", id: "sess-ka", timestamp: "2026-09-01T00:00:00.000Z", cwd: "/tmp/test" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** vi.mocked 取回超集(各测试文件按需解构;与各 vi.mock 工厂返回同一 mock 实例)。 */
|
|
42
|
+
export function takeSessionRunnerMocks() {
|
|
43
|
+
return {
|
|
44
|
+
mockSpawn: vi.mocked(spawn),
|
|
45
|
+
mockExistsSync: vi.mocked(fs.existsSync),
|
|
46
|
+
mockReaddirSync: vi.mocked(fs.readdirSync as (path: fs.PathLike) => string[]),
|
|
47
|
+
mockPending: vi.mocked(readActivePendingFromSessionFile),
|
|
48
|
+
mockListPending: vi.mocked(listActivePendingFromSessionFile),
|
|
49
|
+
mockReadAliveMarker: vi.mocked(readAliveMarker),
|
|
50
|
+
mockWriteAliveMarker: vi.mocked(writeAliveMarker),
|
|
51
|
+
mockIsProcessAlive: vi.mocked(isProcessAlive),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 非 chatMode 层主 + fake timers + agent_end keep-alive 落位的公共前奏。
|
|
57
|
+
*
|
|
58
|
+
* 返回 promise(runSpawn 原始 promise,供手动 exit/close 收尾的用例)与 finish
|
|
59
|
+
* (标准 close 收尾)两种取用形态。
|
|
60
|
+
*/
|
|
61
|
+
export async function spawnAndReachKeepAlive(
|
|
62
|
+
opts = makeOpts(),
|
|
63
|
+
task = "Task: keep-alive no-progress",
|
|
64
|
+
): Promise<{
|
|
65
|
+
child: FakeChild;
|
|
66
|
+
promise: Promise<Awaited<ReturnType<typeof runSpawn>>>;
|
|
67
|
+
finish: (code?: number) => Promise<Awaited<ReturnType<typeof runSpawn>>>;
|
|
68
|
+
}> {
|
|
69
|
+
const record = makeRecord();
|
|
70
|
+
const promise = runSpawn(record, task, opts, makeCtx());
|
|
71
|
+
await waitForSpawn(vi.mocked(spawn));
|
|
72
|
+
const child = lastSpawnedChild(vi.mocked(spawn));
|
|
73
|
+
// fake timers 必须在 emit agent_end 之前启用(keep-alive timer 新建于 agent_end
|
|
74
|
+
// 处理器内;不 fake setImmediate——stream flush 靠真实事件循环交付,见 MF-3 先例)。
|
|
75
|
+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
|
|
76
|
+
emitStdoutLine(child, sessionHeaderLine());
|
|
77
|
+
emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
|
|
78
|
+
await new Promise((r) => setImmediate(r));
|
|
79
|
+
return {
|
|
80
|
+
child,
|
|
81
|
+
promise,
|
|
82
|
+
finish: (code = 143) => {
|
|
83
|
+
child.stdout.end();
|
|
84
|
+
child.stderr.end();
|
|
85
|
+
child.emit("close", code);
|
|
86
|
+
return promise as Promise<Awaited<ReturnType<typeof runSpawn>>>;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 让最近 spawn 的 FakeChild 发 close(0) 并等 runSpawn 收尾(env 注入用例的标准收尾装配)。 */
|
|
92
|
+
export async function closeLastChildAndAwait<T>(promise: Promise<T>): Promise<T> {
|
|
93
|
+
const child = lastSpawnedChild(vi.mocked(spawn));
|
|
94
|
+
child.emit("close", 0);
|
|
95
|
+
return promise;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** keep-alive 系测试的统一 hooks:清 mock + stub 空 watchdog env + 统一恢复(在 describe 内调用)。 */
|
|
99
|
+
export function keepAliveTestHooks(
|
|
100
|
+
mockPending: ReturnType<typeof takeSessionRunnerMocks>["mockPending"],
|
|
101
|
+
): void {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
vi.clearAllMocks();
|
|
104
|
+
vi.stubEnv(SPAWN_WATCHDOG_ENV, "");
|
|
105
|
+
mockPending.mockReturnValue({ count: 1, recentUnregister: false });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
vi.restoreAllMocks();
|
|
110
|
+
vi.unstubAllEnvs();
|
|
111
|
+
vi.useRealTimers();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
// };
|
|
48
48
|
// });
|
|
49
49
|
// vi.mock("../alive-store.ts", () => ({ writeAliveMarker: vi.fn() }));
|
|
50
|
-
// vi.mock("../temp-prompt.ts", () => ({
|
|
50
|
+
// vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
|
|
51
51
|
// writePromptToTempFile: vi.fn(async (agent: string) => {
|
|
52
52
|
// const safeName = agent.replace(/[^\w.-]+/g, "_");
|
|
53
53
|
// return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
|
|
@@ -60,8 +60,10 @@ import type { ChildProcess } from "node:child_process";
|
|
|
60
60
|
import { EventEmitter } from "node:events";
|
|
61
61
|
import { PassThrough } from "node:stream";
|
|
62
62
|
|
|
63
|
+
import { vi } from "vitest";
|
|
64
|
+
|
|
63
65
|
import { createRecord } from "../../execution-record.ts";
|
|
64
|
-
import type { RunOptions, SessionRunnerContext } from "../../session-runner.ts";
|
|
66
|
+
import type { RunOptions, SessionRunnerContext } from "../../engine/engines/pi/session-runner.ts";
|
|
65
67
|
|
|
66
68
|
/** FakeChild 的假 pid(满足 ChildProcess.pid 形状,无真实进程语义)。 */
|
|
67
69
|
const FAKE_PID = 12345;
|
|
@@ -194,8 +196,9 @@ export function makeRecord(id = "run-1") {
|
|
|
194
196
|
return createRecord(id, {
|
|
195
197
|
agent: "general-purpose",
|
|
196
198
|
model: "test-model",
|
|
197
|
-
mode: "
|
|
199
|
+
mode: "background",
|
|
198
200
|
task: "do something",
|
|
201
|
+
slug: "spawn-mock",
|
|
199
202
|
startedAt: 1_000_000,
|
|
200
203
|
rootSessionId: "root-session",
|
|
201
204
|
parentRecordId: undefined,
|
|
@@ -240,3 +243,147 @@ export function makeCtx(overrides: Partial<SessionRunnerContext> = {}): SessionR
|
|
|
240
243
|
...overrides,
|
|
241
244
|
};
|
|
242
245
|
}
|
|
246
|
+
|
|
247
|
+
// ── mock 模块工厂(session-runner 系测试共享,从 keep-alive-no-progress /
|
|
248
|
+
// recursive-visibility-env 的逐字重复 mock 块收敛为单源)──
|
|
249
|
+
//
|
|
250
|
+
// 使用模式(vi.mock 工厂内 await import 本文件,免疫 hoist 时序):
|
|
251
|
+
// vi.mock("node:child_process", async () =>
|
|
252
|
+
// (await import("./helpers/spawn-mock.ts")).childProcessModule());
|
|
253
|
+
//
|
|
254
|
+
// 本段保持「无 session-runner / alive-store / session-pending 值依赖」——本文件会被
|
|
255
|
+
// 各 vi.mock 工厂 await import(求值时机 = 被 mock 模块首次请求),若再值依赖
|
|
256
|
+
// session-runner 会构成「mock 工厂 → 本文件 → session-runner → node:child_process
|
|
257
|
+
// (mock 求值中)」循环。需要值依赖这些模块的测试文件侧装配放
|
|
258
|
+
// session-runner-mocks.ts(只被测试文件顶层静态 import)。
|
|
259
|
+
|
|
260
|
+
/** 共享 logger mock 单例(复核失败 warn 留痕断言用;vi.clearAllMocks 统一重置)。 */
|
|
261
|
+
export const loggerMock = { debug: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
262
|
+
|
|
263
|
+
/** "../../core/logger.ts" mock 工厂。 */
|
|
264
|
+
export function coreLoggerModule() {
|
|
265
|
+
return { getLogger: () => loggerMock };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** "node:child_process" mock 工厂(spawn 返回 FakeChild + execFile err-first 兜底)。 */
|
|
269
|
+
export function childProcessModule() {
|
|
270
|
+
return {
|
|
271
|
+
spawn: vi.fn(() => new FakeChild()),
|
|
272
|
+
// buildEnvBlock 用 execFile 异步取 git branch:默认 err-first 兜底(catch → branch=""),
|
|
273
|
+
// 形态同 worktree-manager.test.ts 的 setupExecFile
|
|
274
|
+
execFile: vi.fn(
|
|
275
|
+
(
|
|
276
|
+
_cmd: string,
|
|
277
|
+
_args: readonly string[],
|
|
278
|
+
_opts: unknown,
|
|
279
|
+
cb: (err: Error | null, stdout?: string, stderr?: string) => void,
|
|
280
|
+
) => cb(new Error("execFile not configured in this test")),
|
|
281
|
+
),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** "node:fs" mock 工厂(fs 侧写方法 vi.fn 化,promises 保留 actual)。 */
|
|
286
|
+
export async function fsModule(): Promise<Record<string, unknown>> {
|
|
287
|
+
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
288
|
+
return {
|
|
289
|
+
default: {
|
|
290
|
+
...actual,
|
|
291
|
+
mkdirSync: vi.fn(),
|
|
292
|
+
existsSync: vi.fn(() => false),
|
|
293
|
+
appendFileSync: vi.fn(),
|
|
294
|
+
writeFileSync: vi.fn(),
|
|
295
|
+
readdirSync: vi.fn(() => []),
|
|
296
|
+
},
|
|
297
|
+
mkdirSync: vi.fn(),
|
|
298
|
+
existsSync: vi.fn(() => false),
|
|
299
|
+
appendFileSync: vi.fn(),
|
|
300
|
+
writeFileSync: vi.fn(),
|
|
301
|
+
readdirSync: vi.fn(() => []),
|
|
302
|
+
promises: actual.promises,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** "../alive-store.ts" mock 工厂(keep-alive 系超集:marker 读写 + pid 存活判定兜底)。 */
|
|
307
|
+
export function aliveStoreModule() {
|
|
308
|
+
return {
|
|
309
|
+
writeAliveMarker: vi.fn(),
|
|
310
|
+
// [T5②] keep-alive 心跳刷新读取现有 marker id(缺失兜底 record.id)
|
|
311
|
+
readAliveMarker: vi.fn(() => undefined),
|
|
312
|
+
isProcessAlive: vi.fn(() => false),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** "../session-pending.ts" mock 工厂(keep-alive 判定统一 count>0:有活跃后代 → keep-alive 分支)。 */
|
|
317
|
+
export function sessionPendingModule() {
|
|
318
|
+
return {
|
|
319
|
+
readActivePendingFromSessionFile: vi.fn(() => ({ count: 1, recentUnregister: false })),
|
|
320
|
+
prunePendingCursor: vi.fn(),
|
|
321
|
+
listActivePendingFromSessionFile: vi.fn(() => ({ items: [] })),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** "../engine/engines/pi/temp-prompt.ts" mock 工厂(prompt 落 /tmp/fake-<agent>,收尾 no-op)。 */
|
|
326
|
+
export function tempPromptModule() {
|
|
327
|
+
return {
|
|
328
|
+
writePromptToTempFile: vi.fn(async (agent: string) => {
|
|
329
|
+
const safeName = agent.replace(/[^\w.-]+/g, "_");
|
|
330
|
+
return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
|
|
331
|
+
}),
|
|
332
|
+
cleanupTempPrompt: vi.fn(async () => {}),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── recursive-visibility / schema-env 族 fixture(env 断言族专用,与上方 makeRecord 系
|
|
337
|
+
// 默认值不同:/fake/* 路径 + test/model 形态参与 PI_SUBAGENT_* 断言,逐字保留原值)──
|
|
338
|
+
|
|
339
|
+
/** 构造 env 注入断言族的 ExecutionRecord(可 override id/depth)。 */
|
|
340
|
+
export function makeVisibilityRecord(overrides: { id?: string; depth?: number } = {}) {
|
|
341
|
+
return createRecord(overrides.id ?? "sa-test-record", {
|
|
342
|
+
agent: "general-purpose",
|
|
343
|
+
model: "test/model",
|
|
344
|
+
mode: "background",
|
|
345
|
+
slug: "t",
|
|
346
|
+
task: "test task",
|
|
347
|
+
startedAt: Date.now(),
|
|
348
|
+
rootSessionId: "should-be-overridden-by-sessionRootId-source",
|
|
349
|
+
parentRecordId: undefined,
|
|
350
|
+
depth: overrides.depth ?? 0,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** 构造 env 注入断言族的 RunOptions(可 override 关键字段)。 */
|
|
355
|
+
export function makeRunOpts(overrides: Partial<RunOptions> = {}): RunOptions {
|
|
356
|
+
return {
|
|
357
|
+
resolved: { model: { provider: "test", id: "model", name: "Model", reasoning: false }, thinkingLevel: undefined },
|
|
358
|
+
agentConfig: undefined,
|
|
359
|
+
appendSystemPrompt: undefined,
|
|
360
|
+
skillPath: undefined,
|
|
361
|
+
schema: undefined,
|
|
362
|
+
maxTurns: undefined,
|
|
363
|
+
graceTurns: undefined,
|
|
364
|
+
signal: undefined,
|
|
365
|
+
onEvent: undefined,
|
|
366
|
+
...overrides,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** 构造 env 注入断言族的 SessionRunnerContext(可 override 关键字段)。 */
|
|
371
|
+
export function makeVisibilityCtx(overrides: Partial<SessionRunnerContext> = {}): SessionRunnerContext {
|
|
372
|
+
return {
|
|
373
|
+
cwd: "/fake/cwd",
|
|
374
|
+
agentDir: "/fake/agent",
|
|
375
|
+
skillDirs: [],
|
|
376
|
+
mainCwd: "/fake/cwd",
|
|
377
|
+
sessionRootId: "root-main-session",
|
|
378
|
+
rootCwd: "/fake/cwd",
|
|
379
|
+
...overrides,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** 取最近一次 spawn 调用传入的 env(跨进程身份注入断言用)。 */
|
|
384
|
+
export function getLastSpawnEnv<T extends { mock: { calls: unknown[][] } }>(
|
|
385
|
+
mockSpawn: T,
|
|
386
|
+
): Record<string, string | undefined> {
|
|
387
|
+
const opts = mockSpawn.mock.calls.at(-1)?.[2] as { env?: Record<string, string | undefined> } | undefined;
|
|
388
|
+
return opts?.env ?? {};
|
|
389
|
+
}
|