@zhushanwen/subagent-core 0.4.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/dist/{chunk-4KN4TTG7.js → chunk-43ONBFZX.js} +1 -1
- package/dist/{chunk-X7SZ5HLQ.js → chunk-V3VHZ2VX.js} +20 -27
- package/dist/execution/engine/engines/zcode/constants.cjs +29 -41
- package/dist/execution/engine/engines/zcode/constants.d.cts +65 -53
- package/dist/execution/engine/engines/zcode/constants.d.ts +65 -53
- package/dist/execution/engine/engines/zcode/constants.js +17 -27
- 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 +2 -2
- package/dist/index.cjs +16418 -14070
- package/dist/index.d.cts +5136 -4016
- package/dist/index.d.ts +5136 -4016
- package/dist/index.js +16343 -14048
- package/dist/{types-DpUO16pj.d.cts → types-Dv4QhSJ_.d.cts} +215 -112
- package/dist/{types-DpUO16pj.d.ts → types-Dv4QhSJ_.d.ts} +215 -112
- package/package.json +3 -3
- 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 +4 -2
- package/src/__tests__/review-fix-loop-utils.test.ts +10 -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/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 +4 -4
- 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 +2 -2
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +6 -6
- package/src/execution/__tests__/chat-engine-routing.test.ts +68 -33
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +4 -4
- package/src/execution/__tests__/cold-resurrect.test.ts +283 -0
- package/src/execution/__tests__/delivery-methods.test.ts +75 -62
- package/src/execution/__tests__/descendant-sweep-guards.test.ts +220 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +6 -6
- package/src/execution/__tests__/dialog-queue.test.ts +2 -2
- package/src/execution/__tests__/engine-model-validation.test.ts +741 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +29 -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 +5 -5
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +3 -3
- package/src/execution/__tests__/finalize-record.test.ts +173 -17
- package/src/execution/__tests__/gc-timer.test.ts +55 -2
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +12 -12
- package/src/execution/__tests__/get-state-handshake.test.ts +1 -1
- 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 +50 -148
- package/src/execution/__tests__/kill-all-escalation.test.ts +2 -2
- package/src/execution/__tests__/lifecycle-predicates.test.ts +1 -2
- package/src/execution/__tests__/manifest-parentid.test.ts +4 -4
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +1 -1
- 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 +973 -18
- 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.test.ts +10 -9
- 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 +15 -9
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +4 -4
- package/src/execution/__tests__/run-spawn-edges.test.ts +17 -15
- package/src/execution/__tests__/run-spawn-integration.test.ts +2 -2
- 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 +4 -4
- package/src/execution/__tests__/service-kill-escalation.test.ts +7 -5
- 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 +257 -14
- package/src/execution/__tests__/session-reconstructor.test.ts +366 -15
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +1 -1
- package/src/execution/__tests__/session-runner-close-prune.test.ts +2 -2
- package/src/execution/__tests__/session-runner-dispatch.test.ts +489 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +3 -3
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +5 -4
- 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 +282 -47
- 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 +2 -1
- 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 +42 -22
- package/src/execution/__tests__/subagent-service-message-close.test.ts +29 -26
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +8 -8
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -22
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +30 -29
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +45 -20
- 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 +6 -5
- 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 +12 -6
- package/src/execution/__tests__/ui-request-handler.test.ts +4 -3
- package/src/execution/__tests__/ui-request-queue.test.ts +142 -2
- 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/agent-registry.ts +74 -30
- package/src/execution/agent-result-mapper.ts +3 -0
- package/src/execution/cold-resurrect.ts +199 -0
- 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 +22 -112
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +15 -28
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +12 -12
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +21 -82
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +4 -7
- 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 +4 -2
- 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/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} +1 -1
- 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/{session-runner.ts → engine/engines/pi/session-runner.ts} +617 -412
- 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 +54 -27
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +321 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +13 -12
- 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 +20 -8
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +77 -110
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +25 -319
- 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 +48 -148
- package/src/execution/engine/engines/zcode/appserver-launcher.ts +167 -0
- package/src/execution/engine/engines/zcode/connection.ts +24 -11
- package/src/execution/engine/engines/zcode/constants.ts +75 -71
- package/src/execution/engine/engines/zcode/golden-sample.ts +8 -46
- package/src/execution/engine/engines/zcode/parser.ts +12 -346
- package/src/execution/engine/engines/zcode/preparer.ts +12 -143
- package/src/execution/engine/engines/zcode/session-channel.ts +351 -39
- package/src/execution/engine/engines/zcode/zcode-engine.ts +665 -760
- 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 +24 -6
- 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 +131 -93
- package/src/execution/idle-gc.ts +28 -0
- package/src/execution/lifecycle-predicates.ts +1 -1
- package/src/execution/notify-host.ts +212 -0
- package/src/execution/record-store.ts +119 -73
- package/src/execution/round-settlement.ts +93 -0
- package/src/execution/session-file-gc.ts +96 -62
- package/src/execution/session-reconstructor.ts +256 -143
- package/src/execution/sessions-index.ts +117 -47
- package/src/execution/settled-watchdog.ts +264 -52
- package/src/execution/subagent-actions-core.ts +22 -22
- package/src/execution/subagent-service.ts +815 -963
- package/src/execution/subprocess-agent-runner.ts +144 -97
- package/src/execution/types.ts +9 -0
- package/src/execution/ui-request-handler-factory.ts +102 -74
- package/src/execution/ui-request-queue.ts +50 -19
- package/src/index.ts +442 -316
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +1 -1
- package/src/orchestration/__tests__/agent-call-stream.test.ts +2 -2
- package/src/orchestration/__tests__/args-validator.test.ts +14 -0
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +1 -1
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +11 -9
- package/src/orchestration/__tests__/execute-agent-call.test.ts +151 -11
- package/src/orchestration/__tests__/file-run-store.test.ts +7 -6
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +2 -2
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +1 -1
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +3 -3
- package/src/orchestration/__tests__/lifecycle.test.ts +7 -5
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +1 -1
- package/src/orchestration/__tests__/run-snapshot.test.ts +8 -5
- 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} +27 -19
- 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 +1 -1
- package/src/orchestration/__tests__/worker-script-builder.test.ts +1 -1
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +1 -1
- package/src/orchestration/agent-opts-resolver.ts +1 -1
- package/src/orchestration/args-validator.ts +53 -31
- package/src/orchestration/execute-agent-call.ts +19 -73
- package/src/orchestration/launcher.ts +128 -81
- package/src/orchestration/lifecycle.ts +43 -45
- package/src/orchestration/models/ports.ts +4 -4
- package/src/orchestration/models/run-runtime.ts +2 -2
- package/src/orchestration/models/trace.ts +1 -1
- package/src/orchestration/models/types.ts +111 -22
- package/src/orchestration/models/workflow-run.ts +1 -1
- package/src/orchestration/script-lint.ts +91 -58
- package/src/orchestration/{error-recovery.ts → worker-message-pump.ts} +157 -150
- package/src/shared/__tests__/meta-parser.test.ts +151 -0
- package/src/shared/__tests__/resource-discovery.test.ts +258 -215
- package/src/shared/atomic-write.ts +12 -9
- package/src/shared/meta-parser.ts +158 -84
- package/src/shared/model-ref.ts +15 -2
- package/src/shared/resource-discovery.ts +9 -183
- package/src/shared/schema-jsonify.ts +1 -1
- package/workflows/review-fix-loop-utils.cjs +151 -89
- package/workflows/review-fix-loop-utils.d.cts +287 -0
- 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__/preparer-appserver.test.ts +0 -387
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -710
- package/src/execution/engine/engines/zcode/appserver-home.ts +0 -442
- package/src/execution/engine/engines/zcode/appserver-probe.ts +0 -141
- package/src/execution/engine/engines/zcode/launcher.ts +0 -161
- package/src/execution/execute-options-mapper.ts +0 -115
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -288
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// src/execution/__tests__/delivery-methods.test.ts
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// 冷路径续轮 + chatMode 统一投递单元测试(M2-B1 投递基础设施;D2 单轨后口径)。
|
|
4
4
|
//
|
|
5
5
|
// mock session-runner(runSpawn 受控 + killAllSpawnedChildren 空实现 + getChildByRecord/spawnedChildren
|
|
6
|
-
// 真实 Map 语义),走 SubagentService
|
|
7
|
-
// -
|
|
8
|
-
//
|
|
9
|
-
// -
|
|
6
|
+
// 真实 Map 语义),走 SubagentService.deliverChatMessage → PiEngine.deliverPrompt 真实逻辑:
|
|
7
|
+
// - 冷路径(进程死):续轮 resume spawn(runSpawn 收到 resume 参数);chatMode+done 回 running/round+1;
|
|
8
|
+
// 终态 / 无 sessionFile / 无 controller throw 行动语言
|
|
9
|
+
// - 热路径(进程活):prompt + streamingBehavior(interrupt → steer/followUp)
|
|
10
10
|
//
|
|
11
|
-
// stdin-writer 不 mock(端到端验证
|
|
11
|
+
// stdin-writer 不 mock(端到端验证 PiEngine.deliverPrompt→sendPromptCommand→child.stdin 字节)。
|
|
12
12
|
// [review 修复] 已删除 deliverToRunning describe(busy follow_up/steer 投递)——随
|
|
13
13
|
// deliverToRunning 方法一并移除(无生产调用方,pendingMessages 消费确认制死机制)。
|
|
14
14
|
|
|
@@ -29,7 +29,7 @@ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
|
29
29
|
|
|
30
30
|
// mock session-runner:runSpawn 受控 + killAllSpawnedChildren 空实现;
|
|
31
31
|
// getChildByRecord/spawnedChildren 提供真实 Map 语义(deliverToRunning 注册 mock child 用)。
|
|
32
|
-
vi.mock("../session-runner.ts", () => {
|
|
32
|
+
vi.mock("../engine/engines/pi/session-runner.ts", () => {
|
|
33
33
|
const spawnedChildren = new Map<string, unknown>();
|
|
34
34
|
return {
|
|
35
35
|
runSpawn: vi.fn(),
|
|
@@ -41,7 +41,7 @@ vi.mock("../session-runner.ts", () => {
|
|
|
41
41
|
};
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
import { runSpawn, spawnedChildren, type SessionRunnerContext } from "../session-runner.ts";
|
|
44
|
+
import { runSpawn, spawnedChildren, type SessionRunnerContext } from "../engine/engines/pi/session-runner.ts";
|
|
45
45
|
import * as lifecycle from "../lifecycle-manager.ts";
|
|
46
46
|
import { createRecord } from "../execution-record.ts";
|
|
47
47
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
@@ -59,11 +59,15 @@ function makeTmpAgentDir(): string {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function makePi(): PiLike & {
|
|
62
|
-
appendEntry: ReturnType<typeof vi.fn
|
|
63
|
-
events: { emit: ReturnType<typeof vi.fn
|
|
64
|
-
sendMessage: ReturnType<typeof vi.fn
|
|
62
|
+
appendEntry: ReturnType<typeof vi.fn<(customType: string, data?: unknown) => void>>;
|
|
63
|
+
events: { emit: ReturnType<typeof vi.fn<(channel: string, data: unknown) => void>> };
|
|
64
|
+
sendMessage: ReturnType<typeof vi.fn<(message: Parameters<PiLike["sendMessage"]>[0], options?: Parameters<PiLike["sendMessage"]>[1]) => void>>;
|
|
65
65
|
} {
|
|
66
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
appendEntry: vi.fn((customType: string, data?: unknown) => {}),
|
|
68
|
+
events: { emit: vi.fn((channel: string, data: unknown) => {}) },
|
|
69
|
+
sendMessage: vi.fn(() => {}),
|
|
70
|
+
};
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
function makeResult(success: boolean): AgentResult {
|
|
@@ -100,7 +104,7 @@ function makeIdleRecord(id = "sa-chat"): ExecutionRecord {
|
|
|
100
104
|
|
|
101
105
|
/** PassThrough child(可读出 stdin 字节验证 deliverToRunning 写入)。 */
|
|
102
106
|
function makeStreamChild(): ChildProcess {
|
|
103
|
-
// exitCode/signalCode:真 ChildProcess 未退出时均为 null
|
|
107
|
+
// exitCode/signalCode:真 ChildProcess 未退出时均为 null(热路径投递 [race-F5]
|
|
104
108
|
// 写后死进程检测读这两个字段,缺省 undefined 会被误判为已死触发 warn)。
|
|
105
109
|
return { stdin: new PassThrough(), exitCode: null, signalCode: null } as unknown as ChildProcess;
|
|
106
110
|
}
|
|
@@ -108,7 +112,7 @@ function makeStreamChild(): ChildProcess {
|
|
|
108
112
|
function readStdinLines(child: ChildProcess): unknown[] {
|
|
109
113
|
const stream = child.stdin as unknown as PassThrough;
|
|
110
114
|
stream.pause();
|
|
111
|
-
const text = stream.read()?.toString() ?? "";
|
|
115
|
+
const text: string = stream.read()?.toString() ?? "";
|
|
112
116
|
return text
|
|
113
117
|
.split("\n")
|
|
114
118
|
.map((l) => l.trim())
|
|
@@ -116,14 +120,14 @@ function readStdinLines(child: ChildProcess): unknown[] {
|
|
|
116
120
|
.map((l) => JSON.parse(l));
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
describe("
|
|
123
|
+
describe("冷路径续轮(M2-B1 idle 投递;D2 后经 deliverChatMessage 无活进程到达)", () => {
|
|
120
124
|
let agentDir: string;
|
|
121
125
|
let service: SubagentService;
|
|
122
126
|
let record: ExecutionRecord;
|
|
123
127
|
|
|
124
128
|
beforeEach(() => {
|
|
125
129
|
agentDir = makeTmpAgentDir();
|
|
126
|
-
const modelService = new ModelConfigService({ agentDir });
|
|
130
|
+
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
127
131
|
service = new SubagentService({ cwd: agentDir, modelService });
|
|
128
132
|
service.initSession({ pi: makePi(), sessionId: "root-session" });
|
|
129
133
|
record = makeIdleRecord();
|
|
@@ -134,16 +138,16 @@ describe("resumeRound (M2-B1 idle 投递)", () => {
|
|
|
134
138
|
|
|
135
139
|
afterEach(() => {
|
|
136
140
|
service.dispose();
|
|
137
|
-
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
141
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
138
142
|
});
|
|
139
143
|
|
|
140
|
-
it("
|
|
144
|
+
it("冷路径续轮(running) → kickOff runSpawn 收到 resume 参数;chatMode+done 回 running/round+1", async () => {
|
|
141
145
|
mockRunSpawn.mockResolvedValueOnce(makeResult(true));
|
|
142
146
|
const beforeRound = record.round;
|
|
143
147
|
|
|
144
|
-
service.
|
|
148
|
+
await service.chatActions.deliverChatMessage(record, "next round msg", false);
|
|
145
149
|
|
|
146
|
-
//
|
|
150
|
+
// 冷路径守卫通过后:status 已手动设回 running(M2-A 边界,绕过 tryTransition)
|
|
147
151
|
expect(record.status).toBe("running");
|
|
148
152
|
|
|
149
153
|
// detached:等 runSpawn 被调
|
|
@@ -163,42 +167,42 @@ describe("resumeRound (M2-B1 idle 投递)", () => {
|
|
|
163
167
|
expect(record.round).toBe(beforeRound! + 1);
|
|
164
168
|
});
|
|
165
169
|
|
|
166
|
-
it("终态 closed record → throw 行动语言(MF-4,仅 running 可续聊),不触发 kickOff", () => {
|
|
170
|
+
it("终态 closed record → throw 行动语言(MF-4,仅 running 可续聊),不触发 kickOff", async () => {
|
|
167
171
|
record.status = "closed";
|
|
168
172
|
// MF-4:行动语言(spec §3.1),不暴露 resume/controller 内部词汇
|
|
169
|
-
expect(
|
|
173
|
+
await expect(service.chatActions.deliverChatMessage(record, "msg", false)).rejects.toThrow(/not ready for a new message/);
|
|
170
174
|
expect(mockRunSpawn).not.toHaveBeenCalled();
|
|
171
175
|
});
|
|
172
176
|
|
|
173
|
-
it("record 无 sessionFile → throw 行动语言(MF-4 canonical session unavailable),不触发 kickOff", () => {
|
|
177
|
+
it("record 无 sessionFile → throw 行动语言(MF-4 canonical session unavailable),不触发 kickOff", async () => {
|
|
174
178
|
record.sessionFile = undefined;
|
|
175
|
-
expect(
|
|
179
|
+
await expect(service.chatActions.deliverChatMessage(record, "msg", false)).rejects.toThrow(/session unavailable/);
|
|
176
180
|
expect(mockRunSpawn).not.toHaveBeenCalled();
|
|
177
181
|
});
|
|
178
182
|
|
|
179
|
-
it("record 无 controller → throw 行动语言(MF-4),不触发 kickOff", () => {
|
|
183
|
+
it("record 无 controller → throw 行动语言(MF-4),不触发 kickOff", async () => {
|
|
180
184
|
record.controller = undefined;
|
|
181
|
-
expect(
|
|
185
|
+
await expect(service.chatActions.deliverChatMessage(record, "msg", false)).rejects.toThrow(/not ready for a new message/);
|
|
182
186
|
expect(mockRunSpawn).not.toHaveBeenCalled();
|
|
183
187
|
});
|
|
184
188
|
});
|
|
185
189
|
|
|
186
190
|
// ============================================================
|
|
187
|
-
//
|
|
191
|
+
// deliverChatMessage(V2 决策 3 chatMode 统一投递:按进程死活分流;D2 后经 engine.interactRecord)
|
|
188
192
|
// ============================================================
|
|
189
193
|
|
|
190
|
-
describe("
|
|
194
|
+
describe("deliverChatMessage (V2 决策 3 chatMode 统一投递)", () => {
|
|
191
195
|
let agentDir: string;
|
|
192
196
|
let service: SubagentService;
|
|
193
197
|
let record: ExecutionRecord;
|
|
194
198
|
|
|
195
199
|
beforeEach(() => {
|
|
196
200
|
agentDir = makeTmpAgentDir();
|
|
197
|
-
const modelService = new ModelConfigService({ agentDir });
|
|
201
|
+
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
198
202
|
service = new SubagentService({ cwd: agentDir, modelService });
|
|
199
203
|
service.initSession({ pi: makePi(), sessionId: "root-session" });
|
|
200
204
|
record = makeIdleRecord(); // chatMode:true, idle, round=1
|
|
201
|
-
// sessionFile
|
|
205
|
+
// sessionFile:冷路径续轮需要(热路径不用,设了无害)
|
|
202
206
|
record.sessionFile = path.join(agentDir, "fake-session.jsonl");
|
|
203
207
|
spawnedChildren.clear();
|
|
204
208
|
mockRunSpawn.mockReset();
|
|
@@ -209,15 +213,15 @@ describe("deliverMessage (V2 决策 3 chatMode 统一投递)", () => {
|
|
|
209
213
|
service.dispose();
|
|
210
214
|
spawnedChildren.clear();
|
|
211
215
|
lifecycle._resetLifecycleState();
|
|
212
|
-
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
216
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
213
217
|
});
|
|
214
218
|
|
|
215
|
-
it("热路径 interrupt=false:进程活 → prompt streamingBehavior:followUp + status=running + pid 记录", () => {
|
|
219
|
+
it("热路径 interrupt=false:进程活 → prompt streamingBehavior:followUp + status=running + pid 记录", async () => {
|
|
216
220
|
const child = makeStreamChild();
|
|
217
221
|
Object.assign(child, { pid: 12345 });
|
|
218
222
|
spawnedChildren.set(record.id, child);
|
|
219
223
|
|
|
220
|
-
service.
|
|
224
|
+
await service.chatActions.deliverChatMessage(record, "after you finish", false);
|
|
221
225
|
|
|
222
226
|
const lines = readStdinLines(child);
|
|
223
227
|
expect(lines).toHaveLength(1);
|
|
@@ -227,11 +231,11 @@ describe("deliverMessage (V2 决策 3 chatMode 统一投递)", () => {
|
|
|
227
231
|
expect(record.pid).toBe(12345);
|
|
228
232
|
});
|
|
229
233
|
|
|
230
|
-
it("热路径 interrupt=true:进程活 → prompt streamingBehavior:steer", () => {
|
|
234
|
+
it("热路径 interrupt=true:进程活 → prompt streamingBehavior:steer", async () => {
|
|
231
235
|
const child = makeStreamChild();
|
|
232
236
|
spawnedChildren.set(record.id, child);
|
|
233
237
|
|
|
234
|
-
service.
|
|
238
|
+
await service.chatActions.deliverChatMessage(record, "stop now", true);
|
|
235
239
|
|
|
236
240
|
const lines = readStdinLines(child);
|
|
237
241
|
expect(lines[0]).toMatchObject({ type: "prompt", message: "stop now", streamingBehavior: "steer" });
|
|
@@ -241,13 +245,13 @@ describe("deliverMessage (V2 决策 3 chatMode 统一投递)", () => {
|
|
|
241
245
|
// [race-F5] 写后死进程检测:热路径 write 同步成功(数据进内核缓冲)但子进程在读取前
|
|
242
246
|
// 已死(gate/idle kill 竞速)→ 消息将随缓冲静默丢弃。修复:写后检查 exitCode/signalCode,
|
|
243
247
|
// 已死则 warn 留证(含 runId 与消息类型),不抛错不重试(终态已由 kill 路径保证)。
|
|
244
|
-
it("[race-F5] 热路径写后子进程已死 → logger.warn 被记录(含 runId 与消息类型),不抛错", () => {
|
|
248
|
+
it("[race-F5] 热路径写后子进程已死 → logger.warn 被记录(含 runId 与消息类型),不抛错", async () => {
|
|
245
249
|
const child = makeStreamChild();
|
|
246
250
|
// 模拟 gate/idle kill 竞速:写 stdin 成功但子进程已死(SIGTERM 终止形态)
|
|
247
251
|
Object.assign(child, { signalCode: "SIGTERM" });
|
|
248
252
|
spawnedChildren.set(record.id, child);
|
|
249
253
|
|
|
250
|
-
expect(
|
|
254
|
+
await expect(service.chatActions.deliverChatMessage(record, "lost msg", true)).resolves.toBeUndefined();
|
|
251
255
|
|
|
252
256
|
// 写入照常发生(热路径语义不变,不做二次分发)
|
|
253
257
|
const lines = readStdinLines(child);
|
|
@@ -259,13 +263,13 @@ describe("deliverMessage (V2 决策 3 chatMode 统一投递)", () => {
|
|
|
259
263
|
);
|
|
260
264
|
});
|
|
261
265
|
|
|
262
|
-
it("[race-F5] 活进程正常投递不触发死进程 warn(守卫不误报)", () => {
|
|
266
|
+
it("[race-F5] 活进程正常投递不触发死进程 warn(守卫不误报)", async () => {
|
|
263
267
|
// loggerMock 是模块级共享 mock:清历史调用后再断言(防前一用例的 warn 干扰)
|
|
264
268
|
loggerMock.warn.mockClear();
|
|
265
269
|
const child = makeStreamChild();
|
|
266
270
|
spawnedChildren.set(record.id, child);
|
|
267
271
|
|
|
268
|
-
service.
|
|
272
|
+
await service.chatActions.deliverChatMessage(record, "normal", false);
|
|
269
273
|
|
|
270
274
|
expect(loggerMock.warn).not.toHaveBeenCalledWith(
|
|
271
275
|
expect.stringContaining("died around stdin write"),
|
|
@@ -273,24 +277,24 @@ describe("deliverMessage (V2 决策 3 chatMode 统一投递)", () => {
|
|
|
273
277
|
);
|
|
274
278
|
});
|
|
275
279
|
|
|
276
|
-
it("热路径 disarm idle timer:arm
|
|
280
|
+
it("热路径 disarm idle timer:arm 后投递 → timer 清除(防 turn 期间误杀)", async () => {
|
|
277
281
|
const child = makeStreamChild();
|
|
278
282
|
spawnedChildren.set(record.id, child);
|
|
279
283
|
// 先 arm idle timer(模拟 Step 4a agent_settled 后 armed)
|
|
280
284
|
lifecycle.armIdleTimer(record.id, () => {}, 10000);
|
|
281
285
|
expect(lifecycle.hasIdleTimer(record.id)).toBe(true);
|
|
282
286
|
|
|
283
|
-
service.
|
|
287
|
+
await service.chatActions.deliverChatMessage(record, "msg", false);
|
|
284
288
|
|
|
285
289
|
// disarmIdleTimer 被调 → timer 清除(新 turn 不被 idle timer 误杀)
|
|
286
290
|
expect(lifecycle.hasIdleTimer(record.id)).toBe(false);
|
|
287
291
|
});
|
|
288
292
|
|
|
289
|
-
it("冷路径:进程死(无 child)→
|
|
293
|
+
it("冷路径:进程死(无 child)→ 续轮 resume spawn(runSpawn 收到 resume 参数)", async () => {
|
|
290
294
|
mockRunSpawn.mockResolvedValueOnce(makeResult(true));
|
|
291
295
|
// spawnedChildren 无该 record → getChildByRecord 返回 undefined → 冷路径
|
|
292
296
|
|
|
293
|
-
service.
|
|
297
|
+
await service.chatActions.deliverChatMessage(record, "resume msg", false);
|
|
294
298
|
|
|
295
299
|
await vi.waitFor(() => expect(mockRunSpawn).toHaveBeenCalledTimes(1));
|
|
296
300
|
const call = mockRunSpawn.mock.calls[0]!;
|
|
@@ -308,7 +312,7 @@ describe("deliverMessage (V2 决策 3 chatMode 统一投递)", () => {
|
|
|
308
312
|
Object.assign(child, { killed: true }); // 进程已 kill
|
|
309
313
|
spawnedChildren.set(record.id, child);
|
|
310
314
|
|
|
311
|
-
service.
|
|
315
|
+
await service.chatActions.deliverChatMessage(record, "after kill", false);
|
|
312
316
|
|
|
313
317
|
await vi.waitFor(() => expect(mockRunSpawn).toHaveBeenCalledTimes(1));
|
|
314
318
|
});
|
|
@@ -316,7 +320,7 @@ describe("deliverMessage (V2 决策 3 chatMode 统一投递)", () => {
|
|
|
316
320
|
it("续聊后 agent_settled → onRoundSettled 设 running + round+1(v4 B-1 idle 折入 running,复用 Step 4a 链路)", () => {
|
|
317
321
|
// buildSessionRunnerContext 是 private,类型断言访问(测试专用)
|
|
318
322
|
const ctx = (service as unknown as { buildSessionRunnerContext(): SessionRunnerContext }).buildSessionRunnerContext();
|
|
319
|
-
record.status = "running"; //
|
|
323
|
+
record.status = "running"; // 模拟热路径投递设的新 turn 状态
|
|
320
324
|
const beforeRound = record.round;
|
|
321
325
|
|
|
322
326
|
// 模拟 session-runner 在 agent_settled 时调本回调(Step 4a 接入点)
|
|
@@ -331,21 +335,21 @@ describe("deliverMessage (V2 决策 3 chatMode 统一投递)", () => {
|
|
|
331
335
|
// 冷路径并发守卫(review round2 MF1:同 turn 批量两条 message 双冷路径双 spawn)
|
|
332
336
|
// ============================================================
|
|
333
337
|
// 复现链(reviewer 探针实证):pi 对同一条 assistant message 的 tool calls 顺序执行
|
|
334
|
-
// (subagent tool sequential),tool1
|
|
338
|
+
// (subagent tool sequential),tool1 的投递在冷路径续轮返回即
|
|
335
339
|
// resolve——早于 runSpawn 完成 spawn 注册(session-runner spawnedChildren.set 前有
|
|
336
340
|
// pool.acquire await / writePromptToTempFile 等多个异步点);tool2 立即执行 →
|
|
337
|
-
// getChildByRecord 仍 undefined → 再次冷路径。v4
|
|
341
|
+
// getChildByRecord 仍 undefined → 再次冷路径。v4 两态收敛后续轮的
|
|
338
342
|
// `status !== "running"` 守卫对 idle-resumable record 恒放行(idle 本来就是 running)、
|
|
339
343
|
// `status = "running"` 是幂等写 → 两次 kickOff → runSpawn 被调 2 次 → 两个 pi 子进程
|
|
340
344
|
// 以 --session 同一 JSONL 双写 + 第一个进程脱离 kill 记账成孤儿。
|
|
341
|
-
describe("
|
|
345
|
+
describe("deliverChatMessage 冷路径并发守卫(review round2 MF1)", () => {
|
|
342
346
|
let agentDir: string;
|
|
343
347
|
let service: SubagentService;
|
|
344
348
|
let record: ExecutionRecord;
|
|
345
349
|
|
|
346
350
|
beforeEach(() => {
|
|
347
351
|
agentDir = makeTmpAgentDir();
|
|
348
|
-
const modelService = new ModelConfigService({ agentDir });
|
|
352
|
+
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
349
353
|
service = new SubagentService({ cwd: agentDir, modelService });
|
|
350
354
|
service.initSession({ pi: makePi(), sessionId: "root-session" });
|
|
351
355
|
record = makeIdleRecord();
|
|
@@ -359,7 +363,7 @@ describe("deliverMessage 冷路径并发守卫(review round2 MF1)", () => {
|
|
|
359
363
|
service.dispose();
|
|
360
364
|
spawnedChildren.clear();
|
|
361
365
|
lifecycle._resetLifecycleState();
|
|
362
|
-
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
366
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
363
367
|
});
|
|
364
368
|
|
|
365
369
|
it("同 record 连续两条 message 冷路径 → 第二条 throw 行动语言,runSpawn 仅 1 次", async () => {
|
|
@@ -371,13 +375,13 @@ describe("deliverMessage 冷路径并发守卫(review round2 MF1)", () => {
|
|
|
371
375
|
);
|
|
372
376
|
|
|
373
377
|
// 第一条:正常冷路径 resume
|
|
374
|
-
await expect(service.
|
|
378
|
+
await expect(service.chatActions.deliverChatMessage(record, "first msg", false)).resolves.toBeUndefined();
|
|
375
379
|
await vi.waitFor(() => expect(mockRunSpawn).toHaveBeenCalledTimes(1));
|
|
376
380
|
|
|
377
381
|
// 第二条:spawn 仍在途(getChildByRecord undefined)→ 再走冷路径。
|
|
378
|
-
//
|
|
382
|
+
// 修复前:续轮守卫恒放行 → 第二次 kickOff → runSpawn 2 次(双 spawn 双写 session)。
|
|
379
383
|
// 修复后:in-flight 守卫 throw 行动语言(MF-4)。
|
|
380
|
-
await expect(service.
|
|
384
|
+
await expect(service.chatActions.deliverChatMessage(record, "second msg", false)).rejects.toThrow(
|
|
381
385
|
/already starting a new round/,
|
|
382
386
|
);
|
|
383
387
|
expect(mockRunSpawn).toHaveBeenCalledTimes(1);
|
|
@@ -388,7 +392,7 @@ describe("deliverMessage 冷路径并发守卫(review round2 MF1)", () => {
|
|
|
388
392
|
expect((service as unknown as { resumesInFlight: Set<string> }).resumesInFlight.has(record.id)).toBe(false),
|
|
389
393
|
);
|
|
390
394
|
mockRunSpawn.mockResolvedValueOnce(makeResult(true));
|
|
391
|
-
await expect(service.
|
|
395
|
+
await expect(service.chatActions.deliverChatMessage(record, "third msg", false)).resolves.toBeUndefined();
|
|
392
396
|
await vi.waitFor(() => expect(mockRunSpawn).toHaveBeenCalledTimes(2));
|
|
393
397
|
expect(mockRunSpawn.mock.calls[1]![1]).toBe("third msg");
|
|
394
398
|
});
|
|
@@ -401,25 +405,34 @@ describe("deliverMessage 冷路径并发守卫(review round2 MF1)", () => {
|
|
|
401
405
|
() => new Promise<AgentResult>((res) => { releaseA = res; }),
|
|
402
406
|
);
|
|
403
407
|
|
|
404
|
-
await service.
|
|
408
|
+
await service.chatActions.deliverChatMessage(record, "A msg", false);
|
|
405
409
|
await vi.waitFor(() => expect(mockRunSpawn).toHaveBeenCalledTimes(1));
|
|
406
410
|
|
|
407
411
|
// B 的冷路径不受 A 在途影响
|
|
408
|
-
await expect(service.
|
|
412
|
+
await expect(service.chatActions.deliverChatMessage(recordB, "B msg", false)).resolves.toBeUndefined();
|
|
409
413
|
await vi.waitFor(() => expect(mockRunSpawn).toHaveBeenCalledTimes(2));
|
|
410
414
|
expect(mockRunSpawn.mock.calls[1]![0]).toBe(recordB);
|
|
411
415
|
|
|
412
416
|
releaseA(makeResult(true));
|
|
413
417
|
});
|
|
414
418
|
|
|
415
|
-
it("
|
|
416
|
-
//
|
|
419
|
+
it("冷路径续轮在途时 EPIPE 兜底重入同样被守卫拦截(不持锁路径,review round2 MF1)", async () => {
|
|
420
|
+
// D2 后续轮本体是编排层私有,不持锁的重入形态只剩 EPIPE 兜底(PiEngine.deliverPrompt
|
|
421
|
+
// catch 分支直调续轮)——此处经热路径 EPIPE 复现:活 child 的 stdin 已销毁 → 写入
|
|
422
|
+
// EPIPE → 兜底转冷路径。首条消息已占在途守卫时,第二条的 EPIPE 兜底被拒。
|
|
417
423
|
let release!: (r: AgentResult) => void;
|
|
418
|
-
|
|
419
|
-
|
|
424
|
+
mockRunSpawn.mockImplementationOnce(() => new Promise<AgentResult>((res) => { release = res; }));
|
|
425
|
+
// 第一条:冷路径(无 child)→ 续轮在途
|
|
426
|
+
await expect(service.chatActions.deliverChatMessage(record, "first", false)).resolves.toBeUndefined();
|
|
427
|
+
await vi.waitFor(() => expect(mockRunSpawn).toHaveBeenCalledTimes(1));
|
|
420
428
|
|
|
421
|
-
|
|
422
|
-
|
|
429
|
+
// 第二条:EPIPE 兜底(child 活但 stdin 断)→ 兜底转冷路径 → 在途守卫 throw 行动语言
|
|
430
|
+
const epipe = new Error("write EPIPE") as NodeJS.ErrnoException;
|
|
431
|
+
epipe.code = "EPIPE"; // writeStdinLine 的 R3 判据:err.code === 'EPIPE' 才转 throw
|
|
432
|
+
const child = { stdin: { destroyed: false, write: () => { throw epipe; } }, exitCode: null, signalCode: null, killed: false } as unknown as ChildProcess;
|
|
433
|
+
spawnedChildren.set(record.id, child);
|
|
434
|
+
await expect(service.chatActions.deliverChatMessage(record, "second", false)).rejects.toThrow(/already starting a new round/);
|
|
435
|
+
expect(mockRunSpawn).toHaveBeenCalledTimes(1);
|
|
423
436
|
|
|
424
437
|
release(makeResult(true));
|
|
425
438
|
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// src/execution/__tests__/descendant-sweep-guards.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [S-17c / T2-②] sweepDescendantsOfSession 守卫分支补充(与 descendant-sweep.test.ts
|
|
4
|
+
// 互补,不改动其既有用例)。
|
|
5
|
+
//
|
|
6
|
+
// 覆盖该文件未锁定的分支:
|
|
7
|
+
// - register entry 缺 sessionId(TODO T5 marker fallback 锚点)→ skipped 留痕不杀;
|
|
8
|
+
// - readProcessCmdline 两条失败边:ps 非零退出(status≠0/error)与 spawnSync 同步
|
|
9
|
+
// 抛错 → cmdline undefined → "cmdline probe failed" 保守跳过;
|
|
10
|
+
// - killPidWithEscalation 的 SIGTERM 发送失败(EPERM 等权限/垂死窗口)→ debug
|
|
11
|
+
// 留痕不武装升级(30s 后无 SIGKILL);
|
|
12
|
+
// - SIGKILL 升级窗口内目标自行退出(ESRCH)→ 吞掉不崩(目标已达成)。
|
|
13
|
+
//
|
|
14
|
+
// 真实 fs(临时目录)+ mock spawnSync / process.kill / logger——同 descendant-sweep
|
|
15
|
+
// 布局,新增 logger mock 用于 SIGTERM 失败留痕断言。
|
|
16
|
+
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
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
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
25
|
+
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
26
|
+
}));
|
|
27
|
+
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
28
|
+
|
|
29
|
+
// 只 mock spawnSync(ps 探测受控);spawn 等其余实现保留 actual(本文件不经 runSpawn)。
|
|
30
|
+
vi.mock("node:child_process", async () => {
|
|
31
|
+
const actual = await import("node:child_process");
|
|
32
|
+
return {
|
|
33
|
+
...actual,
|
|
34
|
+
spawnSync: vi.fn(),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
import { sweepDescendantsOfSession } from "../engine/engines/pi/session-runner.ts";
|
|
39
|
+
import { clearPendingCursors } from "../session-pending.ts";
|
|
40
|
+
import { writeAliveMarker } from "../alive-store.ts";
|
|
41
|
+
|
|
42
|
+
const mockSpawnSync = vi.mocked(spawnSync);
|
|
43
|
+
|
|
44
|
+
const ALIVE_PID = 424_242;
|
|
45
|
+
|
|
46
|
+
/** process.kill 受控:signal=0 探活与真实信号分别处置,信号调用记录到 kills。 */
|
|
47
|
+
const kills: Array<{ pid: number; signal: string | number }> = [];
|
|
48
|
+
|
|
49
|
+
function entryLine(customType: string, data: Record<string, unknown>): string {
|
|
50
|
+
return `${JSON.stringify({
|
|
51
|
+
type: "custom",
|
|
52
|
+
customType,
|
|
53
|
+
data,
|
|
54
|
+
timestamp: "2026-09-01T00:00:00.000Z",
|
|
55
|
+
id: `e-${Math.random().toString(36).slice(2)}`,
|
|
56
|
+
})}\n`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function registerData(fields: Record<string, unknown>): Record<string, unknown> {
|
|
60
|
+
return { id: "bg-1", type: "session", name: "desc", status: "active", registeredAt: 1, ...fields };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** ps -p <pid> -o command= 的完整 SpawnSyncReturns 形态(字段补齐省 as 链)。 */
|
|
64
|
+
function psResult(stdout: string, status = 0): ReturnType<typeof spawnSync> {
|
|
65
|
+
return {
|
|
66
|
+
pid: 0,
|
|
67
|
+
output: [null, stdout, ""],
|
|
68
|
+
stdout,
|
|
69
|
+
stderr: "",
|
|
70
|
+
status,
|
|
71
|
+
signal: null,
|
|
72
|
+
} as ReturnType<typeof spawnSync>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let sessionDir = "";
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
vi.clearAllMocks();
|
|
79
|
+
clearPendingCursors();
|
|
80
|
+
kills.length = 0;
|
|
81
|
+
sessionDir = fs.mkdtempSync(path.join(os.tmpdir(), "desc-sweep-guard-"));
|
|
82
|
+
vi.spyOn(process, "kill").mockImplementation(((pid: number, signal?: string | number) => {
|
|
83
|
+
if (signal === undefined || signal === 0) return true; // 探活恒真(目标在窗口内存活)
|
|
84
|
+
kills.push({ pid, signal });
|
|
85
|
+
return true;
|
|
86
|
+
}) as typeof process.kill);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
vi.restoreAllMocks();
|
|
91
|
+
clearPendingCursors();
|
|
92
|
+
fs.rmSync(sessionDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/** 层主 + 一个带 .alive marker 的直接后代(标准单层树)。 */
|
|
96
|
+
function writeSingleLevelTree(): { rootFile: string; childFile: string } {
|
|
97
|
+
const rootFile = path.join(sessionDir, "20260901T000000-000_root-sess.jsonl");
|
|
98
|
+
const childFile = path.join(sessionDir, "20260901T000001-000_child-sess.jsonl");
|
|
99
|
+
fs.writeFileSync(rootFile, entryLine("pending:register", registerData({ sessionId: "child-sess" })));
|
|
100
|
+
fs.writeFileSync(childFile, "other entry line\n");
|
|
101
|
+
writeAliveMarker(childFile, { pid: ALIVE_PID, id: "child-sess", startedAt: Date.now() });
|
|
102
|
+
return { rootFile, childFile };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** ps 探测默认桩:存活 pid 返回 pi rpc 形态。 */
|
|
106
|
+
function stubCmdlineByPid(): void {
|
|
107
|
+
mockSpawnSync.mockImplementation((_cmd: string, args?: readonly string[]) => {
|
|
108
|
+
const pid = Number(args?.[1]);
|
|
109
|
+
return psResult(`node /usr/local/bin/pi --mode rpc --session-dir /tmp/sess-${pid}\n`);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
describe("[S-17c] sweep 守卫:pending 项缺 sessionId(T5 marker fallback 锚点)", () => {
|
|
114
|
+
it("register entry 无 sessionId → skipped 留痕(reason 指向 T5 兜底),不杀任何进程", () => {
|
|
115
|
+
stubCmdlineByPid();
|
|
116
|
+
const rootFile = path.join(sessionDir, "20260901T000000-000_root2.jsonl");
|
|
117
|
+
fs.writeFileSync(rootFile, entryLine("pending:register", registerData({ sessionId: undefined })));
|
|
118
|
+
|
|
119
|
+
const r = sweepDescendantsOfSession(rootFile, sessionDir, "test-nosessid");
|
|
120
|
+
|
|
121
|
+
expect(r.killed).toHaveLength(0);
|
|
122
|
+
expect(r.skipped).toHaveLength(1);
|
|
123
|
+
// sessionId 字段回退为 pending 操作 id(诊断线索),reason 说明 marker 兜底锚点
|
|
124
|
+
expect(r.skipped[0]).toMatchObject({
|
|
125
|
+
sessionId: "bg-1",
|
|
126
|
+
reason: expect.stringContaining("no sessionId (marker-based fallback pending T5)"),
|
|
127
|
+
});
|
|
128
|
+
expect(kills).toHaveLength(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("[S-17c] sweep 守卫:readProcessCmdline 失败边(保守跳过不动手)", () => {
|
|
133
|
+
it.each([
|
|
134
|
+
[
|
|
135
|
+
"ps 非零退出(进程刚死,ps -p 无此进程)",
|
|
136
|
+
() =>
|
|
137
|
+
mockSpawnSync.mockImplementation(() => psResult("", 1)),
|
|
138
|
+
],
|
|
139
|
+
[
|
|
140
|
+
"spawnSync 同步抛错(ps 不可用)",
|
|
141
|
+
() =>
|
|
142
|
+
mockSpawnSync.mockImplementation(() => {
|
|
143
|
+
throw new Error("ps binary missing");
|
|
144
|
+
}),
|
|
145
|
+
],
|
|
146
|
+
])("%s → cmdline probe failed,跳过不杀", (_label, setupProbe) => {
|
|
147
|
+
const { rootFile } = writeSingleLevelTree();
|
|
148
|
+
setupProbe();
|
|
149
|
+
|
|
150
|
+
const r = sweepDescendantsOfSession(rootFile, sessionDir, "test-ps-fail");
|
|
151
|
+
|
|
152
|
+
// [防误杀] 探测失败 = 无法证明是 pi rpc 形态 → 一律不动手
|
|
153
|
+
expect(r.killed).toHaveLength(0);
|
|
154
|
+
expect(r.skipped).toHaveLength(1);
|
|
155
|
+
expect(r.skipped[0]).toMatchObject({
|
|
156
|
+
pid: ALIVE_PID,
|
|
157
|
+
reason: expect.stringContaining("cmdline probe failed (ps unavailable)"),
|
|
158
|
+
});
|
|
159
|
+
expect(kills).toHaveLength(0);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("[S-17c] killPidWithEscalation:SIGTERM/ SIGKILL 失败边", () => {
|
|
164
|
+
it("SIGTERM 发送即失败(权限/垂死窗口 EPERM)→ debug 留痕不武装升级,30s 后无 SIGKILL", async () => {
|
|
165
|
+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
166
|
+
stubCmdlineByPid();
|
|
167
|
+
vi.spyOn(process, "kill").mockImplementation(((pid: number, signal?: string | number) => {
|
|
168
|
+
if (signal === undefined || signal === 0) return true;
|
|
169
|
+
if (signal === "SIGTERM") {
|
|
170
|
+
const err = new Error("operation not permitted") as NodeJS.ErrnoException;
|
|
171
|
+
err.code = "EPERM";
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
kills.push({ pid, signal });
|
|
175
|
+
return true;
|
|
176
|
+
}) as typeof process.kill);
|
|
177
|
+
|
|
178
|
+
const { rootFile } = writeSingleLevelTree();
|
|
179
|
+
const r = sweepDescendantsOfSession(rootFile, sessionDir, "test-sigterm-fail");
|
|
180
|
+
|
|
181
|
+
// killed 列表记录的是处置意图;实际 SIGTERM 未送达(失败留痕,best-effort continue)
|
|
182
|
+
expect(r.killed).toEqual([ALIVE_PID]);
|
|
183
|
+
expect(loggerMock.debug).toHaveBeenCalledWith(
|
|
184
|
+
expect.stringContaining(`SIGTERM to pid ${ALIVE_PID} failed (best-effort continue)`),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// 无升级 timer:SIGTERM 失败后直接 return,越过 30s 窗口也不补发 SIGKILL
|
|
188
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
189
|
+
expect(kills).toHaveLength(0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("SIGKILL 升级窗口内目标自行退出(ESRCH)→ 吞掉不崩,SIGTERM 已达即目标达成", async () => {
|
|
193
|
+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
194
|
+
stubCmdlineByPid();
|
|
195
|
+
// 探活恒真(fire 时窗口内仍活);SIGTERM 送达成功;SIGKILL 时进程已退出(ESRCH)
|
|
196
|
+
vi.spyOn(process, "kill").mockImplementation(((pid: number, signal?: string | number) => {
|
|
197
|
+
if (signal === undefined || signal === 0) return true;
|
|
198
|
+
if (signal === "SIGKILL") {
|
|
199
|
+
const err = new Error("No such process") as NodeJS.ErrnoException;
|
|
200
|
+
err.code = "ESRCH";
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
kills.push({ pid, signal });
|
|
204
|
+
return true;
|
|
205
|
+
}) as typeof process.kill);
|
|
206
|
+
|
|
207
|
+
const { rootFile } = writeSingleLevelTree();
|
|
208
|
+
const r = sweepDescendantsOfSession(rootFile, sessionDir, "test-sigkill-esrch");
|
|
209
|
+
|
|
210
|
+
expect(r.killed).toEqual([ALIVE_PID]);
|
|
211
|
+
expect(kills).toEqual([{ pid: ALIVE_PID, signal: "SIGTERM" }]);
|
|
212
|
+
|
|
213
|
+
// 升级 fire:ESRCH 是预期终态(进程在窗口内自行退出),吞掉不崩不重试
|
|
214
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
215
|
+
expect(kills).toEqual([{ pid: ALIVE_PID, signal: "SIGTERM" }]);
|
|
216
|
+
expect(loggerMock.debug).not.toHaveBeenCalledWith(
|
|
217
|
+
expect.stringContaining("SIGTERM to pid"),
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -34,7 +34,7 @@ vi.mock("node:child_process", async () => {
|
|
|
34
34
|
import {
|
|
35
35
|
looksLikePiRpcProcess,
|
|
36
36
|
sweepDescendantsOfSession,
|
|
37
|
-
} from "../session-runner.ts";
|
|
37
|
+
} from "../engine/engines/pi/session-runner.ts";
|
|
38
38
|
import {
|
|
39
39
|
clearPendingCursors,
|
|
40
40
|
listActivePendingFromSessionFile,
|
|
@@ -89,7 +89,7 @@ beforeEach(() => {
|
|
|
89
89
|
afterEach(() => {
|
|
90
90
|
vi.restoreAllMocks();
|
|
91
91
|
clearPendingCursors();
|
|
92
|
-
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
92
|
+
fs.rmSync(sessionDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
/** 标准三层树:root(层主)→ child-sess → grand-sess(叶)。 */
|
|
@@ -123,8 +123,8 @@ function psResult(stdout: string): ReturnType<typeof spawnSync> {
|
|
|
123
123
|
|
|
124
124
|
/** ps 探测默认桩:全部存活 pid 返回 pi rpc 形态。 */
|
|
125
125
|
function stubCmdlineByPid(): void {
|
|
126
|
-
mockSpawnSync.mockImplementation((_cmd: string, args
|
|
127
|
-
const pid = Number(args[1]);
|
|
126
|
+
mockSpawnSync.mockImplementation((_cmd: string, args?: readonly string[]) => {
|
|
127
|
+
const pid = Number(args?.[1]);
|
|
128
128
|
return psResult(`node /usr/local/bin/pi --mode rpc --no-extensions --session-dir /tmp/sess-${pid}\n`);
|
|
129
129
|
});
|
|
130
130
|
}
|
|
@@ -189,8 +189,8 @@ describe("[T2-②] sweepDescendantsOfSession(迭代补杀)", () => {
|
|
|
189
189
|
it("cmdline 非 pi --mode rpc → pid 复用守卫拦截,不杀(但迭代继续展开其后代)", () => {
|
|
190
190
|
const { rootFile, childFile, grandFile } = writeThreeLevelTree();
|
|
191
191
|
// 伪造无关进程占住 child 的 pid;grand 正常
|
|
192
|
-
mockSpawnSync.mockImplementation((_cmd: string, args
|
|
193
|
-
const pid = Number(args[1]);
|
|
192
|
+
mockSpawnSync.mockImplementation((_cmd: string, args?: readonly string[]) => {
|
|
193
|
+
const pid = Number(args?.[1]);
|
|
194
194
|
return psResult(
|
|
195
195
|
pid === ALIVE_PID_CHILD ? "vim notes.txt\n" : "pi --mode rpc\n",
|
|
196
196
|
);
|
|
@@ -343,7 +343,7 @@ describe("DialogGlobalQueue — dialog 超时上界(LC-3/T2⑦)", () => {
|
|
|
343
343
|
|
|
344
344
|
// 完整错误消息承载在父进程日志:含已等待时长 + 「重新发起提问」恢复指引
|
|
345
345
|
expect(loggerMock.warn).toHaveBeenCalledTimes(1);
|
|
346
|
-
const msg = loggerMock.warn.mock.calls[0][0] as string;
|
|
346
|
+
const msg = loggerMock.warn.mock.calls[0]![0] as string;
|
|
347
347
|
expect(msg).toContain("1000ms");
|
|
348
348
|
expect(msg).toContain("重新发起提问");
|
|
349
349
|
// 队列状态已释放
|
|
@@ -364,7 +364,7 @@ describe("DialogGlobalQueue — dialog 超时上界(LC-3/T2⑦)", () => {
|
|
|
364
364
|
await vi.advanceTimersByTimeAsync(1);
|
|
365
365
|
await expect(p).resolves.toEqual({ cancelled: true });
|
|
366
366
|
expect(loggerMock.warn).toHaveBeenCalledTimes(1);
|
|
367
|
-
const msg = loggerMock.warn.mock.calls[0][0] as string;
|
|
367
|
+
const msg = loggerMock.warn.mock.calls[0]![0] as string;
|
|
368
368
|
expect(msg).toContain(`${DEFAULT_DIALOG_TIMEOUT_MS}ms`);
|
|
369
369
|
expect(msg).toContain("重新发起提问");
|
|
370
370
|
});
|