@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
|
@@ -65,10 +65,10 @@ import { spawn } from "node:child_process";
|
|
|
65
65
|
import type { EnginePort, RunContext } from "../engine/port.ts";
|
|
66
66
|
import { resolveJournalPath } from "../engine/paths.ts";
|
|
67
67
|
import { clearEngines, registerEngine } from "../engine/registry.ts";
|
|
68
|
+
import type { AgentCallOpts } from "../../orchestration/models/types.ts";
|
|
68
69
|
import type {
|
|
69
70
|
AgentEvent,
|
|
70
71
|
AgentOutcome,
|
|
71
|
-
AgentTaskSpec,
|
|
72
72
|
EngineCapabilities,
|
|
73
73
|
EngineHandle,
|
|
74
74
|
ProbeReport,
|
|
@@ -76,7 +76,7 @@ import type {
|
|
|
76
76
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
77
77
|
import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
|
|
78
78
|
import { toSubagentRecordEntry } from "../record-entry.ts";
|
|
79
|
-
import { getChildByRecord } from "../session-runner.ts";
|
|
79
|
+
import { getChildByRecord } from "../engine/engines/pi/session-runner.ts";
|
|
80
80
|
import { SubagentService } from "../subagent-service.ts";
|
|
81
81
|
import type { ExecuteOptions } from "../types.ts";
|
|
82
82
|
|
|
@@ -86,7 +86,7 @@ const mockSpawn = vi.mocked(spawn);
|
|
|
86
86
|
// 假引擎(EnginePort 最小实现;run 行为由每个用例注入)
|
|
87
87
|
// ============================================================
|
|
88
88
|
|
|
89
|
-
/** zcode 形态 capabilities(conversation/steer unsupported、sandbox none)。 */
|
|
89
|
+
/** zcode 形态 capabilities(conversation/steer unsupported、sandbox none、maxTurns false)。 */
|
|
90
90
|
const ZCODE_LIKE_CAPS: EngineCapabilities = {
|
|
91
91
|
schemaEnforcement: "emulated",
|
|
92
92
|
steer: "unsupported",
|
|
@@ -98,10 +98,11 @@ const ZCODE_LIKE_CAPS: EngineCapabilities = {
|
|
|
98
98
|
resume: "cold",
|
|
99
99
|
interrupt: "kill-only",
|
|
100
100
|
permissionMode: "native",
|
|
101
|
+
maxTurns: false,
|
|
101
102
|
};
|
|
102
103
|
|
|
103
104
|
interface CapturedRun {
|
|
104
|
-
task:
|
|
105
|
+
task: AgentCallOpts;
|
|
105
106
|
ctx: RunContext;
|
|
106
107
|
}
|
|
107
108
|
|
|
@@ -111,7 +112,7 @@ class FakeEngine implements EnginePort {
|
|
|
111
112
|
/** U2:probe 结果注入(路由期 routeEngine 消费;缺省 ok——probe 通过的常态)。 */
|
|
112
113
|
probeFailed = false;
|
|
113
114
|
/** run 实现注入(缺省:挂起不 resolve——record 保持 running 便于内存断言)。 */
|
|
114
|
-
runImpl: (task:
|
|
115
|
+
runImpl: (task: AgentCallOpts, ctx: RunContext) => Promise<{ handle: EngineHandle; outcome: AgentOutcome }>;
|
|
115
116
|
|
|
116
117
|
constructor(id: string) {
|
|
117
118
|
this.id = id;
|
|
@@ -132,7 +133,7 @@ class FakeEngine implements EnginePort {
|
|
|
132
133
|
}
|
|
133
134
|
: { ok: true, engineVersion: "fake", checks: [{ name: "bin", ok: true }] };
|
|
134
135
|
}
|
|
135
|
-
async run(task:
|
|
136
|
+
async run(task: AgentCallOpts, ctx: RunContext): Promise<{ handle: EngineHandle; outcome: AgentOutcome }> {
|
|
136
137
|
this.runs.push({ task, ctx });
|
|
137
138
|
return this.runImpl(task, ctx);
|
|
138
139
|
}
|
|
@@ -222,7 +223,7 @@ function baseOpts(agentDir: string, extra: Partial<ExecuteOptions> = {}): Execut
|
|
|
222
223
|
|
|
223
224
|
/** 挂起 run 的引擎路径下取内存 running record 的 engine 盖章(collectRecords 内存源投影)。 */
|
|
224
225
|
function runningEngineTag(service: SubagentService, id: string): string | undefined {
|
|
225
|
-
const rec = service.collectRecords(10, "running").find((r) => r.id === id);
|
|
226
|
+
const rec = service.queries.collectRecords(10, "running").find((r) => r.id === id);
|
|
226
227
|
return rec?.engine;
|
|
227
228
|
}
|
|
228
229
|
|
|
@@ -242,7 +243,7 @@ describe("chat 工具域引擎路由分叉(U0:D4/D5/D10)", () => {
|
|
|
242
243
|
// registry 是 globalThis 进程单例——必须清空,防假引擎泄漏进其他测试文件
|
|
243
244
|
clearEngines();
|
|
244
245
|
resetCoreForTests();
|
|
245
|
-
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
246
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
246
247
|
vi.clearAllMocks();
|
|
247
248
|
});
|
|
248
249
|
|
|
@@ -308,7 +309,7 @@ describe("chat 工具域引擎路由分叉(U0:D4/D5/D10)", () => {
|
|
|
308
309
|
const handle = await service.execute(baseOpts(agentDir));
|
|
309
310
|
await vi.waitFor(() => expect(mockSpawn).toHaveBeenCalled());
|
|
310
311
|
|
|
311
|
-
const rec = service.collectRecords(10, "running").find((r) => r.id === handle.subagentId);
|
|
312
|
+
const rec = service.queries.collectRecords(10, "running").find((r) => r.id === handle.subagentId);
|
|
312
313
|
expect(rec).toBeDefined();
|
|
313
314
|
expect(rec?.engine).toBeUndefined();
|
|
314
315
|
expect(JSON.stringify(toSubagentRecordEntry(rec!))).not.toContain("engine");
|
|
@@ -320,10 +321,10 @@ describe("chat 工具域引擎路由分叉(U0:D4/D5/D10)", () => {
|
|
|
320
321
|
await vi.waitFor(() => expect(mockSpawn).toHaveBeenCalled());
|
|
321
322
|
|
|
322
323
|
expect(zcode.runs.length).toBe(0);
|
|
323
|
-
// piEngine 只是注册表占位——pi
|
|
324
|
-
|
|
324
|
+
// piEngine 只是注册表占位——D2 单轨后 pi chat 走 Service 内部 DI 的 PiEngine 实例
|
|
325
|
+
//(chatPiEngine,经 EnginePort 交接轮次),registry 占位实例仍未被 chat 域消费
|
|
325
326
|
expect(piEngine.runs.length).toBe(0);
|
|
326
|
-
const rec = service.collectRecords(10, "running").find((r) => r.id === handle.subagentId);
|
|
327
|
+
const rec = service.queries.collectRecords(10, "running").find((r) => r.id === handle.subagentId);
|
|
327
328
|
expect(rec?.engine).toBeUndefined();
|
|
328
329
|
});
|
|
329
330
|
|
|
@@ -342,7 +343,7 @@ describe("chat 工具域引擎路由分叉(U0:D4/D5/D10)", () => {
|
|
|
342
343
|
expect((err as { recovery?: string }).recovery).toContain("engine: pi");
|
|
343
344
|
expect((err as { recovery?: string }).recovery).toContain("不传该参数");
|
|
344
345
|
expect(zcode.runs.length).toBe(0);
|
|
345
|
-
expect(service.collectRecords(10, "all")).toHaveLength(0);
|
|
346
|
+
expect(service.queries.collectRecords(10, "all")).toHaveLength(0);
|
|
346
347
|
});
|
|
347
348
|
|
|
348
349
|
it("[预检] fork:true / worktree:true 同步拒绝", async () => {
|
|
@@ -354,7 +355,39 @@ describe("chat 工具域引擎路由分叉(U0:D4/D5/D10)", () => {
|
|
|
354
355
|
/engine_capability_unsupported/,
|
|
355
356
|
);
|
|
356
357
|
expect(zcode.runs.length).toBe(0);
|
|
357
|
-
expect(service.collectRecords(10, "all")).toHaveLength(0);
|
|
358
|
+
expect(service.queries.collectRecords(10, "all")).toHaveLength(0);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("[D3-④] zcode + maxTurns → 同步拒绝(record 创建前,不产生孤儿 record)——旧形态的 engine.run 内异步拒绝废弃", async () => {
|
|
362
|
+
// 旧形态:Service 层预检不查 maxTurns → record 创建 + kickOffEngineRun →
|
|
363
|
+
// zcode run 内硬编码 shape 检查 throw → failed record(异步化)。
|
|
364
|
+
// D3-④ 检查点钉死后:capabilities.maxTurns 位驱动,record 创建前同步 throw。
|
|
365
|
+
const { service, zcode } = setup(agentDir);
|
|
366
|
+
const err = await service
|
|
367
|
+
.execute(baseOpts(agentDir, { engine: "zcode", maxTurns: 5 }))
|
|
368
|
+
.catch((e: unknown) => e);
|
|
369
|
+
expect(err).toBeInstanceOf(Error);
|
|
370
|
+
expect((err as Error).message).toContain("engine_capability_unsupported");
|
|
371
|
+
expect((err as Error).message).toContain("maxTurns");
|
|
372
|
+
expect((err as { recovery?: string }).recovery).toContain("engine: pi");
|
|
373
|
+
// 「不产生孤儿 record」断言落点(V4④):store 无新增条目、引擎未被触达
|
|
374
|
+
expect(zcode.runs.length).toBe(0);
|
|
375
|
+
expect(service.queries.collectRecords(10, "all")).toHaveLength(0);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("[V4⑤ 反向] pi + maxTurns/fork/conversation/worktree → 零拦截(pi 既有合法能力无回归)", async () => {
|
|
379
|
+
const { service, zcode } = setup(agentDir);
|
|
380
|
+
// maxTurns=3(V4⑤ 场景原样):正常进入 pi 轮次(spawn 被调),无同步拒绝
|
|
381
|
+
const handle = await service.execute(baseOpts(agentDir, { maxTurns: 3 }));
|
|
382
|
+
await vi.waitFor(() => expect(mockSpawn).toHaveBeenCalled());
|
|
383
|
+
expect(zcode.runs.length).toBe(0);
|
|
384
|
+
expect(service.queries.collectRecords(10, "running").find((r) => r.id === handle.subagentId)).toBeDefined();
|
|
385
|
+
// fork / conversation 组合同样直通(第二次 spawn 被调,无 engine_capability_unsupported)
|
|
386
|
+
await service.execute(baseOpts(agentDir, { fork: true, conversation: true }));
|
|
387
|
+
await vi.waitFor(() => expect(mockSpawn.mock.calls.length).toBeGreaterThanOrEqual(2));
|
|
388
|
+
// pi + worktree:预检放行(caps.sandbox='emulated')——execute 不因能力被拒而
|
|
389
|
+
// reject(worktree 创建成败是另一维度,与拦截无关)
|
|
390
|
+
await expect(service.execute(baseOpts(agentDir, { worktree: true }))).resolves.toBeTypeOf("object");
|
|
358
391
|
});
|
|
359
392
|
|
|
360
393
|
// ============================================================
|
|
@@ -367,14 +400,14 @@ describe("chat 工具域引擎路由分叉(U0:D4/D5/D10)", () => {
|
|
|
367
400
|
expect(err).toBeInstanceOf(Error);
|
|
368
401
|
expect((err as Error).message).toContain("engine_not_found");
|
|
369
402
|
expect((err as Error).message).toContain("zcode");
|
|
370
|
-
expect(service.collectRecords(10, "all")).toHaveLength(0);
|
|
403
|
+
expect(service.queries.collectRecords(10, "all")).toHaveLength(0);
|
|
371
404
|
});
|
|
372
405
|
|
|
373
406
|
// ============================================================
|
|
374
407
|
// 5. 引擎分支骨架(D10 终止链 + 终态迁移)
|
|
375
408
|
// ============================================================
|
|
376
409
|
|
|
377
|
-
it("[骨架]
|
|
410
|
+
it("[骨架] 合流任务形状字段正确(prompt/cwd/model/thinkingLevel/schema/skillPath/appendSystemPrompt——D6 直传)", async () => {
|
|
378
411
|
const { service, zcode } = setup(agentDir);
|
|
379
412
|
await service.execute(
|
|
380
413
|
baseOpts(agentDir, {
|
|
@@ -388,12 +421,14 @@ describe("chat 工具域引擎路由分叉(U0:D4/D5/D10)", () => {
|
|
|
388
421
|
);
|
|
389
422
|
await vi.waitFor(() => expect(zcode.runs.length).toBe(1));
|
|
390
423
|
const { task, ctx } = zcode.runs[0];
|
|
391
|
-
|
|
392
|
-
expect(task.
|
|
424
|
+
// [D6 合流] task = AgentCallOpts(host-task-spec 从 ExecuteOptions 直译)
|
|
425
|
+
expect(task.prompt).toBe("do work");
|
|
426
|
+
expect(task.description).toBe("routing-test");
|
|
393
427
|
expect(task.model).toBe("zcode/glm");
|
|
394
|
-
expect(task.
|
|
428
|
+
expect(task.thinkingLevel).toBe("high");
|
|
395
429
|
expect(task.schema).toEqual({ type: "object" });
|
|
396
|
-
expect(task.
|
|
430
|
+
expect(task.skillPath).toBe("/tmp/skill.md");
|
|
431
|
+
expect(task.appendSystemPrompt).toEqual(["extra"]);
|
|
397
432
|
expect(ctx.poolKey).toBe("shared");
|
|
398
433
|
});
|
|
399
434
|
|
|
@@ -403,7 +438,7 @@ describe("chat 工具域引擎路由分叉(U0:D4/D5/D10)", () => {
|
|
|
403
438
|
const handle = await service.execute(baseOpts(agentDir, { engine: "zcode" }));
|
|
404
439
|
|
|
405
440
|
// record 终态化即 archive(无 sessionFile 不可磁盘重建)→ 内存查询落空
|
|
406
|
-
await vi.waitFor(() => expect(service.findRecord(handle.subagentId)).toBeUndefined());
|
|
441
|
+
await vi.waitFor(() => expect(service.queries.findRecord(handle.subagentId)).toBeUndefined());
|
|
407
442
|
// 完成通知(chat 域宿主职责):notifier 立即 flush(无其他 running)→ result 进 sendMessage 正文
|
|
408
443
|
await vi.waitFor(() => {
|
|
409
444
|
const sent = pi.sendMessage.mock.calls.some((c) => String(c[0]?.content).includes("hello result"));
|
|
@@ -417,7 +452,7 @@ describe("chat 工具域引擎路由分叉(U0:D4/D5/D10)", () => {
|
|
|
417
452
|
Promise.resolve({ handle: fakeHandle(), outcome: { ...doneOutcome(""), error: "engine_run_failed: boom", engineId: "zcode" } });
|
|
418
453
|
const handle = await service.execute(baseOpts(agentDir, { engine: "zcode" }));
|
|
419
454
|
|
|
420
|
-
await vi.waitFor(() => expect(service.findRecord(handle.subagentId)).toBeUndefined());
|
|
455
|
+
await vi.waitFor(() => expect(service.queries.findRecord(handle.subagentId)).toBeUndefined());
|
|
421
456
|
await vi.waitFor(() => {
|
|
422
457
|
const sent = pi.sendMessage.mock.calls.some((c) => String(c[0]?.content).includes("boom"));
|
|
423
458
|
expect(sent).toBe(true);
|
|
@@ -468,7 +503,7 @@ describe("chat 引擎分支 U2:probe 兜底 / journal / engineHandle", () => {
|
|
|
468
503
|
afterEach(() => {
|
|
469
504
|
clearEngines();
|
|
470
505
|
delete process.env.XYZ_AGENT_DATA_DIR;
|
|
471
|
-
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
506
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
472
507
|
vi.clearAllMocks();
|
|
473
508
|
});
|
|
474
509
|
|
|
@@ -482,7 +517,7 @@ describe("chat 引擎分支 U2:probe 兜底 / journal / engineHandle", () => {
|
|
|
482
517
|
await vi.waitFor(() => expect(mockSpawn).toHaveBeenCalled());
|
|
483
518
|
expect(piEngine.runs.length).toBe(0);
|
|
484
519
|
|
|
485
|
-
const rec = service.collectRecords(10, "running").find((r) => r.id === handle.subagentId);
|
|
520
|
+
const rec = service.queries.collectRecords(10, "running").find((r) => r.id === handle.subagentId);
|
|
486
521
|
expect(rec?.engine).toBe("pi");
|
|
487
522
|
expect(rec?.engineFallback).toEqual({ from: "zcode", reason: "engine_probe_failed" });
|
|
488
523
|
// entry(register 写点)含 engineFallback——兜底路径允许新增键(D5 只约束纯缺省路径)
|
|
@@ -498,7 +533,7 @@ describe("chat 引擎分支 U2:probe 兜底 / journal / engineHandle", () => {
|
|
|
498
533
|
);
|
|
499
534
|
expect(mockSpawn).not.toHaveBeenCalled();
|
|
500
535
|
expect(zcode.runs.length).toBe(0);
|
|
501
|
-
expect(service.collectRecords(10, "all")).toHaveLength(0);
|
|
536
|
+
expect(service.queries.collectRecords(10, "all")).toHaveLength(0);
|
|
502
537
|
});
|
|
503
538
|
|
|
504
539
|
it("[journal] taskId=record.id + onPoolResolved retarget 落 engines/zcode/<poolKey>/,journalPath 同源回填", async () => {
|
|
@@ -522,7 +557,7 @@ describe("chat 引擎分支 U2:probe 兜底 / journal / engineHandle", () => {
|
|
|
522
557
|
});
|
|
523
558
|
};
|
|
524
559
|
const handle = await service.execute(baseOpts(agentDir, { engine: "zcode" }));
|
|
525
|
-
await vi.waitFor(() => expect(service.findRecord(handle.subagentId)).toBeUndefined());
|
|
560
|
+
await vi.waitFor(() => expect(service.queries.findRecord(handle.subagentId)).toBeUndefined());
|
|
526
561
|
|
|
527
562
|
// journal 文件名 = journal-<record.id>.jsonl,落在 engines/zcode/<poolKey>/ 下
|
|
528
563
|
const journalPath = resolveJournalPath(agentDir, "zcode", POOL, handle.subagentId);
|
|
@@ -553,20 +588,91 @@ describe("chat 引擎分支 U2:probe 兜底 / journal / engineHandle", () => {
|
|
|
553
588
|
});
|
|
554
589
|
};
|
|
555
590
|
const handle = await service.execute(baseOpts(agentDir, { engine: "zcode" }));
|
|
556
|
-
await vi.waitFor(() => expect(service.findRecord(handle.subagentId)).toBeUndefined());
|
|
591
|
+
await vi.waitFor(() => expect(service.queries.findRecord(handle.subagentId)).toBeUndefined());
|
|
557
592
|
|
|
558
|
-
const h = lastRecordEntry(pi)?.engineHandle;
|
|
593
|
+
const h = lastRecordEntry(pi)?.engineHandle as Record<string, unknown> | undefined;
|
|
559
594
|
expect(h?.sessionRef).toEqual({ dbPath: "sessions.db" });
|
|
560
595
|
expect(h?.poolKey).toBe(POOL);
|
|
561
596
|
expect(h?.journalPath).toBe(resolveJournalPath(agentDir, "zcode", POOL, handle.subagentId));
|
|
562
597
|
});
|
|
563
598
|
|
|
599
|
+
// ============================================================
|
|
600
|
+
// [R4 §3.4 不变量 3] onHandleReady 运行中回填:create 应答后立即落 entry——
|
|
601
|
+
// 运行中 GUI 经 entry 重建 record 即得 ①②级读取钥匙(不等 run resolve)
|
|
602
|
+
// ============================================================
|
|
603
|
+
|
|
604
|
+
it("[onHandleReady] create 应答后回调 → record.engineHandle 立即回填 + reportRecordTransition 落 entry(record 仍 running)", async () => {
|
|
605
|
+
process.env.XYZ_AGENT_DATA_DIR = agentDir;
|
|
606
|
+
const { service, zcode, pi } = setup(agentDir);
|
|
607
|
+
const POOL = "zcode-appserver-home";
|
|
608
|
+
let releaseRun!: (v: { handle: EngineHandle; outcome: AgentOutcome }) => void;
|
|
609
|
+
zcode.runImpl = (task, ctx) => {
|
|
610
|
+
ctx.onPoolResolved?.(POOL);
|
|
611
|
+
// create 应答后的回调时点(app-server 引擎在 session/create 应答后触发)
|
|
612
|
+
ctx.onHandleReady?.({
|
|
613
|
+
sessionRef: { dbPath: ".zcode/cli/db/db.sqlite", sessionId: "sess-live-1" },
|
|
614
|
+
poolKey: POOL,
|
|
615
|
+
});
|
|
616
|
+
return new Promise((resolve) => {
|
|
617
|
+
releaseRun = resolve; // 挂起 run——模拟运行中任务
|
|
618
|
+
});
|
|
619
|
+
};
|
|
620
|
+
const handle = await service.execute(baseOpts(agentDir, { engine: "zcode" }));
|
|
621
|
+
await vi.waitFor(() => expect(zcode.runs.length).toBe(1));
|
|
622
|
+
|
|
623
|
+
// run 尚未 resolve(record 仍 running):engineHandle 已回填且 entry 已落盘
|
|
624
|
+
await vi.waitFor(() => {
|
|
625
|
+
const entries = pi.appendEntry.mock.calls.filter((c) => c[0] === "subagent-record");
|
|
626
|
+
const withHandle = entries.filter(
|
|
627
|
+
(c) => (c[1] as Record<string, unknown>).engineHandle !== undefined,
|
|
628
|
+
);
|
|
629
|
+
expect(withHandle.length).toBeGreaterThan(0);
|
|
630
|
+
});
|
|
631
|
+
const running = service["collectRecords"](10, "running").find((r) => r.id === handle.subagentId);
|
|
632
|
+
expect(running?.engineHandle).toEqual({
|
|
633
|
+
sessionRef: { dbPath: ".zcode/cli/db/db.sqlite", sessionId: "sess-live-1" },
|
|
634
|
+
poolKey: POOL,
|
|
635
|
+
journalPath: resolveJournalPath(agentDir, "zcode", POOL, handle.subagentId),
|
|
636
|
+
});
|
|
637
|
+
// 回填 entry 的 journalPath 与 onPoolResolved retarget 后的实际落盘路径一致(同源)
|
|
638
|
+
expect(fs.existsSync(resolveJournalPath(agentDir, "zcode", POOL, handle.subagentId))).toBe(false);
|
|
639
|
+
|
|
640
|
+
// 终态收口(防 dangling)
|
|
641
|
+
releaseRun({ handle: fakeHandle(), outcome: doneOutcome("ok") });
|
|
642
|
+
await vi.waitFor(() => expect(service["findRecord"](handle.subagentId)).toBeUndefined());
|
|
643
|
+
}, 10_000);
|
|
644
|
+
|
|
645
|
+
it("[onHandleReady] 引擎不回调(spawn 形态)时零回填——终态回填仍兜底(行为不变)", async () => {
|
|
646
|
+
process.env.XYZ_AGENT_DATA_DIR = agentDir;
|
|
647
|
+
const { service, zcode, pi } = setup(agentDir);
|
|
648
|
+
const POOL = "zcode-p-glm";
|
|
649
|
+
zcode.runImpl = (task, ctx) => {
|
|
650
|
+
ctx.onPoolResolved?.(POOL); // 只 onPoolResolved——spawn 引擎形态(无 onHandleReady)
|
|
651
|
+
return Promise.resolve({
|
|
652
|
+
handle: {
|
|
653
|
+
data: { v: 1, engineId: "zcode", sessionRef: { dbPath: "sessions.db", sessionId: "sess-1" }, poolKey: POOL, adapterVersion: "test" },
|
|
654
|
+
},
|
|
655
|
+
outcome: doneOutcome("ok"),
|
|
656
|
+
});
|
|
657
|
+
};
|
|
658
|
+
const handle = await service.execute(baseOpts(agentDir, { engine: "zcode" }));
|
|
659
|
+
await vi.waitFor(() => expect(service["findRecord"](handle.subagentId)).toBeUndefined());
|
|
660
|
+
const entries = pi.appendEntry.mock.calls.filter((c) => c[0] === "subagent-record");
|
|
661
|
+
// 运行中回填 entry 不存在(register 写点无 engineHandle;archive 终态侧才有)
|
|
662
|
+
const runningEntries = entries.slice(0, -1);
|
|
663
|
+
for (const c of runningEntries) {
|
|
664
|
+
expect((c[1] as Record<string, unknown>).engineHandle).toBeUndefined();
|
|
665
|
+
}
|
|
666
|
+
const final = entries[entries.length - 1][1] as Record<string, unknown>;
|
|
667
|
+
expect(final.engineHandle).toMatchObject({ poolKey: POOL });
|
|
668
|
+
});
|
|
669
|
+
|
|
564
670
|
it("[D5 回归] pi 纯缺省路径 entry 不含 engine/engineFallback/engineHandle 键", async () => {
|
|
565
671
|
const { service, pi } = setup(agentDir);
|
|
566
672
|
const handle = await service.execute(baseOpts(agentDir));
|
|
567
673
|
await vi.waitFor(() => expect(mockSpawn).toHaveBeenCalled());
|
|
568
674
|
|
|
569
|
-
const rec = service.collectRecords(10, "running").find((r) => r.id === handle.subagentId);
|
|
675
|
+
const rec = service.queries.collectRecords(10, "running").find((r) => r.id === handle.subagentId);
|
|
570
676
|
const entryJson = JSON.stringify(toSubagentRecordEntry(rec!));
|
|
571
677
|
expect(entryJson).not.toContain("engineFallback");
|
|
572
678
|
expect(entryJson).not.toContain("engineHandle");
|
|
@@ -589,7 +695,7 @@ describe("chat 引擎分支 U2:probe 兜底 / journal / engineHandle", () => {
|
|
|
589
695
|
const h = await service.execute(baseOpts(agentDir, { engine: "zcode" }));
|
|
590
696
|
// 等本条终态化(record 离开内存)再发下一条——泄漏形态下第 7 条 acquire 永久
|
|
591
697
|
// 排队、record 永卡 running,本 waitFor 即超时失败
|
|
592
|
-
await vi.waitFor(() => expect(service.findRecord(h.subagentId)).toBeUndefined());
|
|
698
|
+
await vi.waitFor(() => expect(service.queries.findRecord(h.subagentId)).toBeUndefined());
|
|
593
699
|
}
|
|
594
700
|
// 7 次引擎 run 全部真实执行(第 7 次未被泄漏槽阻塞)
|
|
595
701
|
expect(zcode.runs.length).toBe(7);
|
|
@@ -58,9 +58,10 @@ vi.mock("../alive-store.ts", () => ({ writeAliveMarker: vi.fn() }));
|
|
|
58
58
|
// chatMode agent_end 早返回(continue),不读 session-pending;统一 count=0 防干扰。
|
|
59
59
|
vi.mock("../session-pending.ts", () => ({
|
|
60
60
|
readActivePendingFromSessionFile: vi.fn(() => ({ count: 0 })),
|
|
61
|
+
prunePendingCursor: vi.fn(),
|
|
61
62
|
}));
|
|
62
63
|
|
|
63
|
-
vi.mock("../temp-prompt.ts", () => ({
|
|
64
|
+
vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
|
|
64
65
|
writePromptToTempFile: vi.fn(async (agent: string) => {
|
|
65
66
|
const safeName = agent.replace(/[^\w.-]+/g, "_");
|
|
66
67
|
return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
|
|
@@ -68,8 +69,8 @@ vi.mock("../temp-prompt.ts", () => ({
|
|
|
68
69
|
cleanupTempPrompt: vi.fn(async () => {}),
|
|
69
70
|
}));
|
|
70
71
|
|
|
71
|
-
import { runSpawn } from "../session-runner.ts";
|
|
72
|
-
import type { SessionRunnerContext } from "../session-runner.ts";
|
|
72
|
+
import { runSpawn } from "../engine/engines/pi/session-runner.ts";
|
|
73
|
+
import type { SessionRunnerContext } from "../engine/engines/pi/session-runner.ts";
|
|
73
74
|
import { _resetLifecycleState } from "../lifecycle-manager.ts";
|
|
74
75
|
import { createRecord } from "../execution-record.ts";
|
|
75
76
|
import type { ExecutionRecord } from "../types.ts";
|
|
@@ -94,7 +95,7 @@ function makeChatModeRecord(id = "sa-chat"): ExecutionRecord {
|
|
|
94
95
|
return createRecord(id, {
|
|
95
96
|
agent: "general-purpose",
|
|
96
97
|
model: "test-model",
|
|
97
|
-
mode: "
|
|
98
|
+
mode: "background",
|
|
98
99
|
task: "chat task",
|
|
99
100
|
slug: "chat",
|
|
100
101
|
startedAt: 1_000_000,
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// src/execution/__tests__/cold-resurrect.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [D4-③] coldLookupForAction 单元测试(依赖注入直测)。
|
|
4
|
+
//
|
|
5
|
+
// 背景:cold-resurrect.ts 自 SubagentService 搬移后按 ColdResurrectDeps 依赖注入
|
|
6
|
+
// 设计,此前仅有经 subagent-service 集成路径的 running 重建覆盖——closed 可重连
|
|
7
|
+
// 候选的「恢复失败守卫 / 部分恢复回边」(assertReconnectAllowed / resurrectColdRecord
|
|
8
|
+
// 的 wasClosed 分支)零直测。本文件锁定这些路径的可观察行为:
|
|
9
|
+
// - 恢复失败:worktree 绑定丢失 / 异进程活实例(closed 与 running 候选)→
|
|
10
|
+
// ResurrectDeniedError 且内存无残留(register 不被调用);
|
|
11
|
+
// - 部分恢复:closed 可重连记录 → resurrectClosed 回边翻回 running + 磁盘终态位
|
|
12
|
+
// 同步翻转(.finalized 删除 + .alive 刷新当前进程)+ register/transition 上报;
|
|
13
|
+
// - 恢复源缺失:sessionFile 缺失 / sidecar 翻转失败(目录不存在)均不阻断重生
|
|
14
|
+
// (best-effort 语义)。
|
|
15
|
+
//
|
|
16
|
+
// fixture 一律 mkdtempSync 自建自删(tmpdir),不触碰真实数据目录。
|
|
17
|
+
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as os from "node:os";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
|
|
22
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
23
|
+
|
|
24
|
+
import { readAliveMarker, writeAliveMarker } from "../alive-store.ts";
|
|
25
|
+
import { COLD_LOOKUP_SCAN_LIMIT, coldLookupForAction, type ColdResurrectDeps } from "../cold-resurrect.ts";
|
|
26
|
+
import type { SubagentRecord } from "../types.ts";
|
|
27
|
+
import { ResurrectDeniedError } from "../types.ts";
|
|
28
|
+
|
|
29
|
+
/** 磁盘/索引侧候选记录(SubagentRecord 最小合法形状 + closed 可重连缺省)。 */
|
|
30
|
+
function makeFound(overrides: Partial<SubagentRecord> = {}): SubagentRecord {
|
|
31
|
+
return {
|
|
32
|
+
id: "sa-cold-1",
|
|
33
|
+
agent: "general-purpose",
|
|
34
|
+
task: "cold recovery task",
|
|
35
|
+
slug: "cold-test",
|
|
36
|
+
status: "closed",
|
|
37
|
+
closedReason: "parent-shutdown",
|
|
38
|
+
mode: "background",
|
|
39
|
+
startedAt: 1_700_000_000_000,
|
|
40
|
+
rootSessionId: "root-session",
|
|
41
|
+
parentRecordId: undefined,
|
|
42
|
+
depth: 0,
|
|
43
|
+
endedAt: 1_700_000_100_000,
|
|
44
|
+
turns: 0,
|
|
45
|
+
totalTokens: 0,
|
|
46
|
+
model: "test/model-a",
|
|
47
|
+
thinkingLevel: undefined,
|
|
48
|
+
eventLog: [],
|
|
49
|
+
displayItems: [],
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface DepOverrides {
|
|
55
|
+
/** findLightById(idToFile 索引直查)返回值。 */
|
|
56
|
+
direct?: SubagentRecord;
|
|
57
|
+
/** collectRecords(磁盘全扫)返回清单。 */
|
|
58
|
+
disk?: SubagentRecord[];
|
|
59
|
+
/** getSessionRootId 返回值(归属校验)。 */
|
|
60
|
+
rootId?: string;
|
|
61
|
+
/** getBaselineRecordId 返回值(直接父校验)。 */
|
|
62
|
+
baseline?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 构造依赖注入桩(register / reportRecordTransition 可断言副作用)。 */
|
|
66
|
+
function makeDeps(o: DepOverrides = {}): ColdResurrectDeps {
|
|
67
|
+
return {
|
|
68
|
+
findLightById: vi.fn(() => o.direct),
|
|
69
|
+
collectRecords: vi.fn(() => o.disk ?? []),
|
|
70
|
+
register: vi.fn(),
|
|
71
|
+
reportRecordTransition: vi.fn(),
|
|
72
|
+
getSessionRootId: vi.fn(() => o.rootId ?? "root-session"),
|
|
73
|
+
getBaselineRecordId: vi.fn(() => o.baseline),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("[D4-③] coldLookupForAction 冷查/复活链", () => {
|
|
78
|
+
let dir: string;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), "cold-resurrect-"));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** tmpdir 下建 session 文件 + 可选 .finalized sidecar,返回 sessionFile 路径。 */
|
|
89
|
+
function writeSessionFixture(opts: { finalized?: string; aliveMarker?: string } = {}): string {
|
|
90
|
+
const sessionFile = path.join(dir, "20260901T000000-000_sa-cold-1.jsonl");
|
|
91
|
+
fs.writeFileSync(sessionFile, "{}\n", "utf-8");
|
|
92
|
+
if (opts.finalized !== undefined) {
|
|
93
|
+
fs.writeFileSync(`${sessionFile}.finalized`, opts.finalized, "utf-8");
|
|
94
|
+
}
|
|
95
|
+
if (opts.aliveMarker !== undefined) {
|
|
96
|
+
fs.writeFileSync(`${sessionFile}.alive`, opts.aliveMarker, "utf-8");
|
|
97
|
+
}
|
|
98
|
+
return sessionFile;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── 部分恢复(closed 可重连 → 透明重生回边)──
|
|
102
|
+
|
|
103
|
+
it("closed 可重连记录(parent-shutdown)+ allowReconnect → 重生翻回 running:终态位清除、sidecar 翻转、register + transition 上报", () => {
|
|
104
|
+
const sessionFile = writeSessionFixture({ finalized: JSON.stringify({ reason: "parent-shutdown" }) });
|
|
105
|
+
const found = makeFound({ sessionFile, round: 2 });
|
|
106
|
+
const deps = makeDeps({ disk: [found] });
|
|
107
|
+
|
|
108
|
+
const record = coldLookupForAction(deps, "sa-cold-1", true)!;
|
|
109
|
+
|
|
110
|
+
// 重生回边:closed → running,终态语义位清除
|
|
111
|
+
expect(record.status).toBe("running");
|
|
112
|
+
expect(record.closedReason).toBeUndefined();
|
|
113
|
+
expect(record.endedAt).toBeUndefined();
|
|
114
|
+
// [v4 A-3] 跨重启恢复入口:chatMode 无条件 true
|
|
115
|
+
expect(record.chatMode).toBe(true);
|
|
116
|
+
// 身份/续聊字段从磁盘候选回填
|
|
117
|
+
expect(record.sessionFile).toBe(sessionFile);
|
|
118
|
+
expect(record.round).toBe(2);
|
|
119
|
+
expect(record.model).toBe("test/model-a");
|
|
120
|
+
expect(record.hadWorktree).toBe(false);
|
|
121
|
+
// register + [v8.5 D] 重生后立刻上报 transition entry(live ≡ reload)
|
|
122
|
+
expect(vi.mocked(deps.register)).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(vi.mocked(deps.register)).toHaveBeenCalledWith(record);
|
|
124
|
+
expect(vi.mocked(deps.reportRecordTransition)).toHaveBeenCalledTimes(1);
|
|
125
|
+
expect(vi.mocked(deps.reportRecordTransition)).toHaveBeenCalledWith(record);
|
|
126
|
+
// [review MF-8] 磁盘终态位同步翻转:.finalized 删除 + .alive 刷新为当前进程
|
|
127
|
+
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
|
|
128
|
+
expect(readAliveMarker(sessionFile)).toMatchObject({ pid: process.pid, id: "sa-cold-1" });
|
|
129
|
+
// 冷查扫描契约:全目录兜底按 COLD_LOOKUP_SCAN_LIMIT 上限扫全量(无 root 过滤)
|
|
130
|
+
expect(vi.mocked(deps.collectRecords)).toHaveBeenCalledWith(COLD_LOOKUP_SCAN_LIMIT, "all", undefined);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("idToFile 索引直查返回非 running 态 → 回退磁盘全扫兜底定位(closed 候选仍可重连)", () => {
|
|
134
|
+
const sessionFile = writeSessionFixture();
|
|
135
|
+
const found = makeFound({ sessionFile });
|
|
136
|
+
// direct 命中但 status=closed → 不直接采用,落到 collectRecords 兜底
|
|
137
|
+
const deps = makeDeps({ direct: found, disk: [found] });
|
|
138
|
+
|
|
139
|
+
const record = coldLookupForAction(deps, "sa-cold-1", true)!;
|
|
140
|
+
|
|
141
|
+
expect(record.status).toBe("running");
|
|
142
|
+
expect(vi.mocked(deps.collectRecords)).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(vi.mocked(deps.register)).toHaveBeenCalledWith(record);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("closed disconnected(.finalized 空内容兜底死因)同样落在可重连集内", () => {
|
|
147
|
+
const sessionFile = writeSessionFixture({ finalized: "" });
|
|
148
|
+
const deps = makeDeps({ disk: [makeFound({ sessionFile, closedReason: "disconnected" })] });
|
|
149
|
+
|
|
150
|
+
const record = coldLookupForAction(deps, "sa-cold-1", true)!;
|
|
151
|
+
|
|
152
|
+
expect(record.status).toBe("running");
|
|
153
|
+
expect(vi.mocked(deps.register)).toHaveBeenCalledTimes(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ── 恢复失败守卫(ResurrectDeniedError + 内存无残留)──
|
|
157
|
+
|
|
158
|
+
it("worktree 绑定丢失(跨重启 WorktreeHandle 不可序列化)→ ResurrectDeniedError 拒绝,register 不残留", () => {
|
|
159
|
+
const sessionFile = writeSessionFixture();
|
|
160
|
+
const deps = makeDeps({ disk: [makeFound({ sessionFile, worktree: true })] });
|
|
161
|
+
|
|
162
|
+
expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(ResurrectDeniedError);
|
|
163
|
+
expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(/worktree isolation/);
|
|
164
|
+
expect(vi.mocked(deps.register)).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("closed 候选仍有异进程活实例(.alive 指向活 pid)→ 拒绝双写,register 不残留", () => {
|
|
168
|
+
// 用本测试进程 pid 模拟「另一进程的活跃实例」——isProcessAlive 判活为真
|
|
169
|
+
const sessionFile = writeSessionFixture();
|
|
170
|
+
writeAliveMarker(sessionFile, { pid: process.pid, id: "sa-cold-1", startedAt: Date.now() });
|
|
171
|
+
const deps = makeDeps({ disk: [makeFound({ sessionFile })] });
|
|
172
|
+
|
|
173
|
+
expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(ResurrectDeniedError);
|
|
174
|
+
expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(
|
|
175
|
+
/still finishing in another process/,
|
|
176
|
+
);
|
|
177
|
+
expect(vi.mocked(deps.register)).not.toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("running 候选仍有异进程活实例(父进程重启后旧子进程尚存窗口)→ 拒绝并给恢复指引", () => {
|
|
181
|
+
const sessionFile = writeSessionFixture();
|
|
182
|
+
writeAliveMarker(sessionFile, { pid: process.pid, id: "sa-cold-1", startedAt: Date.now() });
|
|
183
|
+
const deps = makeDeps({ disk: [makeFound({ sessionFile, status: "running" })] });
|
|
184
|
+
|
|
185
|
+
expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(ResurrectDeniedError);
|
|
186
|
+
expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(
|
|
187
|
+
/currently running in another process instance/,
|
|
188
|
+
);
|
|
189
|
+
expect(vi.mocked(deps.register)).not.toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("损坏 .alive marker(无法解析)→ 视为确死放行重生,marker 被当前进程覆盖写", () => {
|
|
193
|
+
const sessionFile = writeSessionFixture({
|
|
194
|
+
finalized: JSON.stringify({ reason: "parent-shutdown" }),
|
|
195
|
+
aliveMarker: "{not valid json",
|
|
196
|
+
});
|
|
197
|
+
const deps = makeDeps({ disk: [makeFound({ sessionFile })] });
|
|
198
|
+
|
|
199
|
+
const record = coldLookupForAction(deps, "sa-cold-1", true)!;
|
|
200
|
+
|
|
201
|
+
expect(record.status).toBe("running");
|
|
202
|
+
expect(readAliveMarker(sessionFile)).toMatchObject({ pid: process.pid });
|
|
203
|
+
expect(vi.mocked(deps.register)).toHaveBeenCalledTimes(1);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── 候选过滤 / 归属与父层校验 ──
|
|
207
|
+
|
|
208
|
+
it.each([
|
|
209
|
+
["自然完成死因 gc", makeFound({ closedReason: "gc" })],
|
|
210
|
+
["用户主动 close", makeFound({ closedReason: "user-close" })],
|
|
211
|
+
])("不可重连死因(%s)→ 磁盘也无候选,返回 undefined", (_label, found) => {
|
|
212
|
+
const deps = makeDeps({ disk: [found] });
|
|
213
|
+
|
|
214
|
+
expect(coldLookupForAction(deps, "sa-cold-1", true)).toBeUndefined();
|
|
215
|
+
expect(vi.mocked(deps.register)).not.toHaveBeenCalled();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("allowReconnect=false:closed 候选一律不可见(running-only 查询语义)", () => {
|
|
219
|
+
const sessionFile = writeSessionFixture();
|
|
220
|
+
const deps = makeDeps({ disk: [makeFound({ sessionFile })] });
|
|
221
|
+
|
|
222
|
+
expect(coldLookupForAction(deps, "sa-cold-1", false)).toBeUndefined();
|
|
223
|
+
expect(vi.mocked(deps.register)).not.toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("rootSessionId 不匹配 → 返回 undefined(不区分失败形态,防跨 session 探测)", () => {
|
|
227
|
+
const deps = makeDeps({ disk: [makeFound({ rootSessionId: "other-root" })], rootId: "root-session" });
|
|
228
|
+
|
|
229
|
+
expect(coldLookupForAction(deps, "sa-cold-1", true)).toBeUndefined();
|
|
230
|
+
expect(vi.mocked(deps.register)).not.toHaveBeenCalled();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("parentRecordId 跨层不匹配(候选有直接父)→ 原样抛 direct parent 错误(跨层导航指引)", () => {
|
|
234
|
+
const deps = makeDeps({
|
|
235
|
+
disk: [makeFound({ status: "running", parentRecordId: "sa-parent" })],
|
|
236
|
+
baseline: undefined,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(/is owned by its direct parent/);
|
|
240
|
+
expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(/parent=sa-parent/);
|
|
241
|
+
expect(vi.mocked(deps.register)).not.toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("顶层候选(parentRecordId 缺失)遇嵌套进程 baseline → direct parent 错误带 (root layer) 回显", () => {
|
|
245
|
+
const deps = makeDeps({
|
|
246
|
+
disk: [makeFound({ status: "running", parentRecordId: undefined })],
|
|
247
|
+
baseline: "rec-baseline-child",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(
|
|
251
|
+
/parent=\(root layer\)/,
|
|
252
|
+
);
|
|
253
|
+
expect(vi.mocked(deps.register)).not.toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ── 恢复源缺失(best-effort 不阻断重生主流程)──
|
|
257
|
+
|
|
258
|
+
it("sessionFile 缺失的 closed 候选 → 跳过 sidecar 翻转,重生照常完成(register + transition 上报)", () => {
|
|
259
|
+
const deps = makeDeps({ disk: [makeFound({ sessionFile: undefined })] });
|
|
260
|
+
|
|
261
|
+
const record = coldLookupForAction(deps, "sa-cold-1", true)!;
|
|
262
|
+
|
|
263
|
+
expect(record.status).toBe("running");
|
|
264
|
+
expect(record.sessionFile).toBeUndefined();
|
|
265
|
+
expect(vi.mocked(deps.register)).toHaveBeenCalledTimes(1);
|
|
266
|
+
expect(vi.mocked(deps.reportRecordTransition)).toHaveBeenCalledTimes(1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("sidecar 翻转失败(sessionFile 所在目录不存在,writeAliveMarker ENOENT)→ best-effort 吞错,重生不中断", () => {
|
|
270
|
+
// 恢复源缺失形态:sessionFile 指向已消失的目录(推导路径过期 / 目录被清理)
|
|
271
|
+
const sessionFile = path.join(dir, "vanished-dir", "20260901T000000-000_sa-cold-1.jsonl");
|
|
272
|
+
const deps = makeDeps({ disk: [makeFound({ sessionFile })] });
|
|
273
|
+
|
|
274
|
+
const record = coldLookupForAction(deps, "sa-cold-1", true)!;
|
|
275
|
+
|
|
276
|
+
// best-effort:sidecar 翻转失败不阻断重生主流程(状态回边与注册照常)
|
|
277
|
+
expect(record.status).toBe("running");
|
|
278
|
+
expect(vi.mocked(deps.register)).toHaveBeenCalledTimes(1);
|
|
279
|
+
expect(vi.mocked(deps.reportRecordTransition)).toHaveBeenCalledTimes(1);
|
|
280
|
+
// marker 写入确实失败(目录不存在,未落盘)
|
|
281
|
+
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
});
|