@zhushanwen/subagent-core 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chunk-KXVBIVOU.js +16 -0
- package/dist/chunk-T2SYBW3I.js +24 -0
- package/dist/chunk-VURUAGMM.js +2305 -0
- package/dist/{types-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
- package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -668
- package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
- package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
- package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
- package/dist/execution/engine/engine-discovery-scan.js +20 -0
- package/dist/execution/engine/paths.cjs +6 -23
- package/dist/execution/engine/paths.d.cts +1 -8
- package/dist/execution/engine/paths.d.ts +1 -8
- package/dist/execution/engine/paths.js +1 -1
- package/dist/execution/relay-env.cjs +12 -28
- package/dist/execution/relay-env.d.cts +1 -33
- package/dist/execution/relay-env.d.ts +1 -33
- package/dist/execution/relay-env.js +5 -3
- package/dist/index.cjs +14045 -16059
- package/dist/index.d.cts +1035 -1579
- package/dist/index.d.ts +1035 -1579
- package/dist/index.js +9838 -13800
- package/dist.bundle/index.cjs +28875 -30323
- package/package.json +20 -26
- package/src/__tests__/record-store-last-line.test.ts +16 -7
- package/src/core/error-message.ts +7 -6
- package/src/core/host-services.ts +8 -2
- package/src/core/notify-ports.ts +17 -2
- package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
- package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
- package/src/execution/__tests__/collect-coordinator-service.test.ts +36 -47
- package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
- package/src/execution/__tests__/delivery-methods.test.ts +179 -237
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -13
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +20 -33
- package/src/execution/__tests__/gc-timer.test.ts +7 -22
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
- package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
- package/src/execution/__tests__/idle-gc.test.ts +200 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
- package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
- package/src/execution/__tests__/record-store.test.ts +60 -6
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
- package/src/execution/__tests__/round-supervisor.test.ts +293 -0
- package/src/execution/__tests__/session-pending.test.ts +119 -17
- package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -11
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
- package/src/execution/__tests__/subagent-service.test.ts +2 -2
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
- package/src/execution/__tests__/sync-collect-recovery.test.ts +21 -36
- package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
- package/src/execution/agent-registry.ts +6 -2
- package/src/execution/dialog-queue.ts +6 -73
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
- package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
- package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
- package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
- package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +81 -54
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
- package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
- package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +7 -2
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
- package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
- package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
- package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
- package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
- package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
- package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
- package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
- package/src/execution/engine/__tests__/registry.test.ts +206 -1
- package/src/execution/engine/__tests__/routing.test.ts +97 -0
- package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
- package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
- package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
- package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
- package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
- package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
- package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
- package/src/execution/engine/client/client-options.ts +103 -0
- package/src/execution/engine/client/engine-client.ts +687 -0
- package/src/execution/engine/client/mirror.ts +167 -0
- package/src/execution/engine/client/pid-file.ts +392 -0
- package/src/execution/engine/client/reaper.ts +95 -0
- package/src/execution/engine/client/remote-engine.ts +441 -0
- package/src/execution/engine/client/reverse-router.ts +255 -0
- package/src/execution/engine/common/capability-gate.ts +109 -22
- package/src/execution/engine/common/errors.ts +9 -25
- package/src/execution/engine/common/event-journal.ts +4 -50
- package/src/execution/engine/common/journal-replay.ts +3 -1
- package/src/execution/engine/common/kill-chain.ts +18 -251
- package/src/execution/engine/common/nesting-guard.ts +18 -114
- package/src/execution/engine/common/pool-manager.ts +4 -0
- package/src/execution/engine/common/session-view-service.ts +56 -84
- package/src/execution/engine/config.ts +151 -0
- package/src/execution/engine/d8-compat.ts +346 -0
- package/src/execution/engine/engine-discovery-roots.ts +107 -0
- package/src/execution/engine/engine-discovery-scan.ts +269 -0
- package/src/execution/engine/engine-discovery.ts +30 -7
- package/src/execution/engine/engine-inspect-package.ts +282 -0
- package/src/execution/engine/engine-manifest.ts +244 -0
- package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
- package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
- package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
- package/src/execution/engine/host/host-bridge.ts +150 -0
- package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
- package/src/execution/engine/host/pi-host-binding.ts +91 -0
- package/src/execution/engine/host/spawned-children.ts +139 -0
- package/src/execution/engine/host-task-spec.ts +29 -2
- package/src/execution/engine/model-validation.ts +55 -2
- package/src/execution/engine/paths.ts +16 -41
- package/src/execution/engine/port.ts +48 -2
- package/src/execution/engine/registry.ts +153 -34
- package/src/execution/engine/routing.ts +225 -52
- package/src/execution/engine/types.ts +25 -200
- package/src/execution/finalize-record.ts +25 -0
- package/src/execution/idle-gc.ts +89 -14
- package/src/execution/lifecycle-manager.ts +10 -7
- package/src/execution/lifecycle-predicates.ts +10 -8
- package/src/execution/record-store.ts +29 -2
- package/src/execution/relay-env.ts +19 -36
- package/src/execution/round-supervisor/domain.ts +89 -0
- package/src/execution/round-supervisor/index.ts +33 -0
- package/src/execution/round-supervisor/notify-accounting.ts +87 -0
- package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
- package/src/execution/round-supervisor/service-binding.test.ts +661 -0
- package/src/execution/round-supervisor/service-binding.ts +273 -0
- package/src/execution/round-supervisor/supervisor.ts +426 -0
- package/src/execution/session-pending.ts +68 -74
- package/src/execution/settled-watchdog.ts +39 -0
- package/src/execution/subagent-service.ts +746 -400
- package/src/execution/subprocess-agent-runner.ts +16 -8
- package/src/execution/types.ts +41 -117
- package/src/execution/ui-channels.ts +13 -214
- package/src/execution/ui-request-handler-factory.ts +7 -4
- package/src/execution/ui-request-observability.ts +2 -2
- package/src/execution/workflow-state-root.ts +67 -0
- package/src/execution/worktree-git-ops.ts +5 -0
- package/src/execution/worktree-manager.ts +11 -1
- package/src/index.ts +55 -26
- package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
- package/src/orchestration/execute-agent-call.ts +1 -1
- package/src/orchestration/file-run-store.ts +144 -59
- package/src/orchestration/models/types.ts +43 -42
- package/src/shared/zcode-model-ref.ts +31 -0
- package/dist/chunk-43ONBFZX.js +0 -240
- package/dist/chunk-A4IVZWVX.js +0 -31
- package/dist/chunk-APZY4IME.js +0 -27
- package/dist/chunk-V3VHZ2VX.js +0 -59
- package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
- package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
- package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
- package/dist/execution/engine/engines/zcode/constants.js +0 -54
- package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
- package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
- package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
- package/dist/execution/engine/engines/zcode/reader.js +0 -9
- package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
- package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
- package/src/__tests__/session-runner.test.ts +0 -69
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
- package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
- package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
- package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
- package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
- package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
- package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
- package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
- package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
- package/src/execution/__tests__/output-collector.test.ts +0 -469
- package/src/execution/__tests__/pi-invocation.test.ts +0 -166
- package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
- package/src/execution/__tests__/rpc-mode.test.ts +0 -89
- package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
- package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
- package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
- package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
- package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
- package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
- package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
- package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
- package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
- package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
- package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
- package/src/execution/__tests__/spawn-args.test.ts +0 -446
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
- package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
- package/src/execution/__tests__/spawned-children.test.ts +0 -236
- package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
- package/src/execution/__tests__/stdin-writer.test.ts +0 -464
- package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
- package/src/execution/__tests__/temp-prompt.test.ts +0 -53
- package/src/execution/__tests__/timeout-integration.test.ts +0 -616
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
- package/src/execution/__tests__/turn-limiter.test.ts +0 -65
- package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
- package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
- package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
- package/src/execution/engine/__tests__/paths.test.ts +0 -39
- package/src/execution/engine/common/schema-emulation.ts +0 -189
- package/src/execution/engine/common/session-view-projection.ts +0 -51
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
- package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
- package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
- package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
- package/src/execution/engine/engines/pi/output-collector.ts +0 -317
- package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
- package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
- package/src/execution/engine/engines/pi/reader.ts +0 -59
- package/src/execution/engine/engines/pi/registration.ts +0 -37
- package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
- package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
- package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
- package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
- package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
- package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
- package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
- package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
- package/src/execution/engine/engines/zcode/connection.ts +0 -598
- package/src/execution/engine/engines/zcode/constants.ts +0 -176
- package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
- package/src/execution/engine/engines/zcode/parser.ts +0 -102
- package/src/execution/engine/engines/zcode/preparer.ts +0 -235
- package/src/execution/engine/engines/zcode/reader.ts +0 -381
- package/src/execution/engine/engines/zcode/registration.ts +0 -37
- package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
- package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
- package/src/execution/round-settlement.ts +0 -93
- package/src/execution/ui-request-queue.ts +0 -215
- package/src/shared/schema-env.ts +0 -44
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
// src/execution/__tests__/session-runner-dispatch.test.ts
|
|
2
|
-
//
|
|
3
|
-
// [S-17a / runSpawn 阶段拆分] 重构提取辅助函数的可观察行为锁定(stdout 分派域)。
|
|
4
|
-
//
|
|
5
|
-
// 背景:session-runner.ts 按执行阶段拆分(文件顶部「runSpawn 的阶段拆分」注释块)后,
|
|
6
|
-
// 新提取的辅助函数(dispatchSdkEventByType / accumulateMessageEnd / handleHeaderLine /
|
|
7
|
-
// setupFreshChild / buildChildEnv / buildSpawnInvocation / startGetStateHandshake /
|
|
8
|
-
// writeAliveMarkerBestEffort)残留分支无断言锁定。本文件经 runSpawn 全链路(FakeChild
|
|
9
|
-
// stdout 行喂入)锁定可观察行为:
|
|
10
|
-
// - stdout 分派:message_end usage 累计 / stopReason=error 错误收口 / compaction_start
|
|
11
|
-
// 事件外发 / 空行不打 invalid 计数 / 末尾残留 invalid 行 close 时聚合留痕;
|
|
12
|
-
// - header 行:worktree pid 补全(onWorktreePid 收到推导 sessionFile)、回调抛错
|
|
13
|
-
// best-effort 不中断解析、alive marker 写失败不中断;
|
|
14
|
-
// - setupFreshChild:worktree pid 注册失败(防御 catch)warn 留痕 spawn 继续;
|
|
15
|
-
// - buildChildEnv:relay 激活时归属 env 注入子进程;
|
|
16
|
-
// - buildSpawnInvocation:resume model 无 / 时 provider 兜底 unknown;
|
|
17
|
-
// - startGetStateHandshake:[F2] get_state 写 stdin 同步 EPIPE → error 留痕 +
|
|
18
|
-
// 握手立即放弃,close 收尾不被阻塞。
|
|
19
|
-
//
|
|
20
|
-
// mock 布局与 session-runner-epipe.test.ts 一致(FakeChild + fs/alive-store/
|
|
21
|
-
// session-pending/temp-prompt mock + 共享 logger mock)。
|
|
22
|
-
|
|
23
|
-
import { spawn } from "node:child_process";
|
|
24
|
-
import type { ChildProcess } from "node:child_process";
|
|
25
|
-
|
|
26
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
27
|
-
|
|
28
|
-
const { loggerMock } = vi.hoisted(() => ({
|
|
29
|
-
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
30
|
-
}));
|
|
31
|
-
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
32
|
-
|
|
33
|
-
vi.mock("node:child_process", async () => {
|
|
34
|
-
const { FakeChild } = await import("./helpers/spawn-mock.ts");
|
|
35
|
-
return {
|
|
36
|
-
spawn: vi.fn(() => new FakeChild()),
|
|
37
|
-
// buildEnvBlock 的 git branch 调用(execFile 异步):默认 err-first 兜底 → branch=""
|
|
38
|
-
execFile: vi.fn(
|
|
39
|
-
(
|
|
40
|
-
_cmd: string,
|
|
41
|
-
_args: readonly string[],
|
|
42
|
-
_opts: unknown,
|
|
43
|
-
cb: (err: Error | null, stdout?: string, stderr?: string) => void,
|
|
44
|
-
) => cb(new Error("execFile not configured in this test")),
|
|
45
|
-
),
|
|
46
|
-
};
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
vi.mock("node:fs", async () => {
|
|
50
|
-
const actual = await import("node:fs");
|
|
51
|
-
return {
|
|
52
|
-
default: {
|
|
53
|
-
...actual,
|
|
54
|
-
mkdirSync: vi.fn(),
|
|
55
|
-
existsSync: vi.fn(() => false),
|
|
56
|
-
appendFileSync: vi.fn(),
|
|
57
|
-
writeFileSync: vi.fn(),
|
|
58
|
-
readdirSync: vi.fn(() => []),
|
|
59
|
-
},
|
|
60
|
-
mkdirSync: vi.fn(),
|
|
61
|
-
existsSync: vi.fn(() => false),
|
|
62
|
-
appendFileSync: vi.fn(),
|
|
63
|
-
writeFileSync: vi.fn(),
|
|
64
|
-
readdirSync: vi.fn(() => []),
|
|
65
|
-
promises: actual.promises,
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
vi.mock("../alive-store.ts", () => ({
|
|
70
|
-
writeAliveMarker: vi.fn(),
|
|
71
|
-
readAliveMarker: vi.fn(() => undefined),
|
|
72
|
-
isProcessAlive: vi.fn(() => false),
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
vi.mock("../session-pending.ts", () => ({
|
|
76
|
-
readActivePendingFromSessionFile: vi.fn(() => ({ count: 0 })),
|
|
77
|
-
prunePendingCursor: vi.fn(),
|
|
78
|
-
listActivePendingFromSessionFile: vi.fn(() => ({ items: [] })),
|
|
79
|
-
}));
|
|
80
|
-
|
|
81
|
-
vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
|
|
82
|
-
writePromptToTempFile: vi.fn(async (agent: string) => {
|
|
83
|
-
const safeName = agent.replace(/[^\w.-]+/g, "_");
|
|
84
|
-
return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
|
|
85
|
-
}),
|
|
86
|
-
cleanupTempPrompt: vi.fn(async () => {}),
|
|
87
|
-
}));
|
|
88
|
-
|
|
89
|
-
import { writeAliveMarker } from "../alive-store.ts";
|
|
90
|
-
import { runSpawn, type RunOptions } from "../engine/engines/pi/session-runner.ts";
|
|
91
|
-
import {
|
|
92
|
-
RELAY_ENV_NODE,
|
|
93
|
-
RELAY_ENV_RECORD_ID,
|
|
94
|
-
RELAY_ENV_SCRIPT,
|
|
95
|
-
RELAY_ENV_SESSION_ID,
|
|
96
|
-
RELAY_ENV_SOCKET,
|
|
97
|
-
} from "../relay-env.ts";
|
|
98
|
-
import { _resetLifecycleState } from "../lifecycle-manager.ts";
|
|
99
|
-
import {
|
|
100
|
-
FakeChild,
|
|
101
|
-
lastSpawnedChild as lastSpawnedChildOf,
|
|
102
|
-
makeCtx,
|
|
103
|
-
makeOpts,
|
|
104
|
-
makeRecord,
|
|
105
|
-
sessionHeader,
|
|
106
|
-
waitForSpawn as waitForSpawnOf,
|
|
107
|
-
} from "./helpers/spawn-mock.ts";
|
|
108
|
-
import type { SessionRunnerContext } from "../engine/engines/pi/session-runner.ts";
|
|
109
|
-
|
|
110
|
-
const mockSpawn = vi.mocked(spawn);
|
|
111
|
-
const mockWriteAliveMarker = vi.mocked(writeAliveMarker);
|
|
112
|
-
const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
|
|
113
|
-
const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
|
|
114
|
-
|
|
115
|
-
/** FakeChild → spawn mock 实现(结构缺 ChildProcess 私有字段,仅类型层收窄;运行时形状由 runSpawn 消费面界定)。 */
|
|
116
|
-
const fakeSpawnImpl = (): ChildProcess => new FakeChild() as unknown as ChildProcess;
|
|
117
|
-
|
|
118
|
-
/** runSpawn 收尾:end 流 + close(code),await runSpawn 返回 AgentResult。 */
|
|
119
|
-
async function settleRunSpawn(
|
|
120
|
-
child: FakeChild,
|
|
121
|
-
promise: ReturnType<typeof runSpawn>,
|
|
122
|
-
code = 0,
|
|
123
|
-
): Promise<Awaited<ReturnType<typeof runSpawn>>> {
|
|
124
|
-
child.stdout.end();
|
|
125
|
-
child.stderr.end();
|
|
126
|
-
child.emit("close", code);
|
|
127
|
-
return promise;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
describe("[S-17a] stdout 分派辅助函数行为(dispatchSdkEventByType / accumulateMessageEnd)", () => {
|
|
131
|
-
beforeEach(() => {
|
|
132
|
-
vi.clearAllMocks();
|
|
133
|
-
// 显式重置 spawn 实现(防止个别用例的 mockImplementation 泄漏到后续用例)
|
|
134
|
-
mockSpawn.mockImplementation(fakeSpawnImpl);
|
|
135
|
-
_resetLifecycleState();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
afterEach(() => {
|
|
139
|
-
_resetLifecycleState();
|
|
140
|
-
vi.unstubAllEnvs();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("message_end 带 usage → 外发 message_end 事件(cost 从 cost.total 提取)+ totalTokens 累计", async () => {
|
|
144
|
-
const onEvent = vi.fn();
|
|
145
|
-
const record = makeRecord("sa-usage");
|
|
146
|
-
const promise = runSpawn(record, "Task: usage", makeOpts({ onEvent }), makeCtx());
|
|
147
|
-
await waitForSpawn();
|
|
148
|
-
const child = lastSpawnedChild();
|
|
149
|
-
|
|
150
|
-
child.stdout.write(
|
|
151
|
-
`${JSON.stringify({
|
|
152
|
-
type: "message_end",
|
|
153
|
-
message: {
|
|
154
|
-
usage: { input: 10, output: 20, cacheRead: 5, cacheWrite: 0, cost: { total: 0.42 } },
|
|
155
|
-
stopReason: "stop",
|
|
156
|
-
},
|
|
157
|
-
})}\n`,
|
|
158
|
-
);
|
|
159
|
-
await new Promise((r) => setImmediate(r));
|
|
160
|
-
|
|
161
|
-
// 事件外发(AgentEvent 形状):cost 字段拍平自 usage.cost.total
|
|
162
|
-
expect(onEvent).toHaveBeenCalledWith({
|
|
163
|
-
type: "message_end",
|
|
164
|
-
usage: { input: 10, output: 20, cacheRead: 5, cacheWrite: 0, cost: 0.42 },
|
|
165
|
-
});
|
|
166
|
-
// 累计副作用:totalTokens = input+output+cacheRead+cacheWrite
|
|
167
|
-
expect(record.totalTokens).toBe(35);
|
|
168
|
-
|
|
169
|
-
const result = await settleRunSpawn(child, promise);
|
|
170
|
-
expect(result.success).toBe(true);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("message_end stopReason=error → error 事件收口进 record.lastError → runSpawn 结果 failed", async () => {
|
|
174
|
-
const onEvent = vi.fn();
|
|
175
|
-
const record = makeRecord("sa-merr");
|
|
176
|
-
const promise = runSpawn(record, "Task: stop-error", makeOpts({ onEvent }), makeCtx());
|
|
177
|
-
await waitForSpawn();
|
|
178
|
-
const child = lastSpawnedChild();
|
|
179
|
-
|
|
180
|
-
child.stdout.write(
|
|
181
|
-
`${JSON.stringify({
|
|
182
|
-
type: "message_end",
|
|
183
|
-
message: { stopReason: "error", errorMessage: "provider exploded" },
|
|
184
|
-
})}\n`,
|
|
185
|
-
);
|
|
186
|
-
await new Promise((r) => setImmediate(r));
|
|
187
|
-
|
|
188
|
-
// error 事件外发(errorMessage 优先,缺省回退 reason/stopReason)
|
|
189
|
-
expect(onEvent).toHaveBeenCalledWith({ type: "error", message: "provider exploded" });
|
|
190
|
-
expect(record.lastError).toBe("provider exploded");
|
|
191
|
-
|
|
192
|
-
// close 后 outcome 判定:lastError 分支 → success=false + error 原文
|
|
193
|
-
const result = await settleRunSpawn(child, promise, 0);
|
|
194
|
-
expect(result.success).toBe(false);
|
|
195
|
-
expect(result.error).toContain("provider exploded");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("compaction_start → 外发 compaction 事件(record 无数据副作用)", async () => {
|
|
199
|
-
const onEvent = vi.fn();
|
|
200
|
-
const record = makeRecord("sa-compaction");
|
|
201
|
-
const promise = runSpawn(record, "Task: compaction", makeOpts({ onEvent }), makeCtx());
|
|
202
|
-
await waitForSpawn();
|
|
203
|
-
const child = lastSpawnedChild();
|
|
204
|
-
|
|
205
|
-
child.stdout.write(`${JSON.stringify({ type: "compaction_start" })}\n`);
|
|
206
|
-
await new Promise((r) => setImmediate(r));
|
|
207
|
-
|
|
208
|
-
expect(onEvent).toHaveBeenCalledWith({ type: "compaction" });
|
|
209
|
-
expect(record.lastError).toBeUndefined();
|
|
210
|
-
|
|
211
|
-
const result = await settleRunSpawn(child, promise);
|
|
212
|
-
expect(result.success).toBe(true);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("空行不打 invalid 计数;stdout 末尾残留 invalid 行(无换行)close 时聚合 debug 留痕", async () => {
|
|
216
|
-
const record = makeRecord("sa-invalid");
|
|
217
|
-
const promise = runSpawn(record, "Task: invalid-lines", makeOpts(), makeCtx());
|
|
218
|
-
await waitForSpawn();
|
|
219
|
-
const child = lastSpawnedChild();
|
|
220
|
-
|
|
221
|
-
// 空行(parseSpawnLine 返回 null):静默跳过,不算 invalid
|
|
222
|
-
child.stdout.write("\n");
|
|
223
|
-
// 末尾残留半行(无换行,close 前未 flush):非法 JSON → close 路径计入 invalid 统计
|
|
224
|
-
child.stdout.write("this is not json at all");
|
|
225
|
-
await new Promise((r) => setImmediate(r));
|
|
226
|
-
|
|
227
|
-
loggerMock.debug.mockClear();
|
|
228
|
-
const result = await settleRunSpawn(child, promise, 0);
|
|
229
|
-
|
|
230
|
-
expect(result.success).toBe(true);
|
|
231
|
-
// [LC-9/T7②] close 聚合留痕:总数 + 样本(事件行损坏被静默丢弃的排查入口)
|
|
232
|
-
expect(loggerMock.debug).toHaveBeenCalledWith(
|
|
233
|
-
expect.stringContaining("stdout had 1 invalid line(s) dropped in total"),
|
|
234
|
-
);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
describe("[S-17a] handleHeaderLine:header 行分派 + worktree pid 补全", () => {
|
|
239
|
-
const WORKTREE = {
|
|
240
|
-
path: "/tmp/wt-checkout",
|
|
241
|
-
branch: "wt/dispatch-1",
|
|
242
|
-
baseCommit: "abc123",
|
|
243
|
-
mainCwd: "/tmp/test",
|
|
244
|
-
} as const;
|
|
245
|
-
|
|
246
|
-
beforeEach(() => {
|
|
247
|
-
vi.clearAllMocks();
|
|
248
|
-
mockSpawn.mockImplementation(fakeSpawnImpl);
|
|
249
|
-
_resetLifecycleState();
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
afterEach(() => {
|
|
253
|
-
_resetLifecycleState();
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it("header 到达 → sessionFile 推导回填 + onWorktreePid 收到 (branch, pid, 推导路径)", async () => {
|
|
257
|
-
const onWorktreePid = vi.fn();
|
|
258
|
-
const ctx = makeCtx({ onWorktreePid } as Partial<SessionRunnerContext>);
|
|
259
|
-
const opts = makeOpts({ worktree: WORKTREE });
|
|
260
|
-
const record = makeRecord("sa-wt-header");
|
|
261
|
-
const promise = runSpawn(record, "Task: wt-header", opts, ctx);
|
|
262
|
-
await waitForSpawn();
|
|
263
|
-
const child = lastSpawnedChild();
|
|
264
|
-
|
|
265
|
-
child.stdout.write(`${JSON.stringify(sessionHeader("sa-sess-wt"))}\n`);
|
|
266
|
-
await new Promise((r) => setImmediate(r));
|
|
267
|
-
|
|
268
|
-
// header 分支的 worktree pid 补全:带推导出的 sessionFile(registry entry 数据源)
|
|
269
|
-
expect(onWorktreePid).toHaveBeenCalledWith(
|
|
270
|
-
"wt/dispatch-1",
|
|
271
|
-
child.pid,
|
|
272
|
-
expect.stringContaining("sa-sess-wt"),
|
|
273
|
-
);
|
|
274
|
-
expect(record.sessionFile).toContain("sa-sess-wt");
|
|
275
|
-
|
|
276
|
-
await settleRunSpawn(child, promise);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("onWorktreePid 回调同步抛错 → best-effort error 留痕,stdout 解析不中断(后续事件照常分派)", async () => {
|
|
280
|
-
const onEvent = vi.fn();
|
|
281
|
-
// setupFreshChild 内两处注册调用先成功,header 分支的第三处抛错
|
|
282
|
-
const onWorktreePid = vi
|
|
283
|
-
.fn()
|
|
284
|
-
.mockReturnValueOnce(undefined)
|
|
285
|
-
.mockReturnValueOnce(undefined)
|
|
286
|
-
.mockImplementation(() => {
|
|
287
|
-
throw new Error("registry lock contention");
|
|
288
|
-
});
|
|
289
|
-
const ctx = makeCtx({ onWorktreePid } as Partial<SessionRunnerContext>);
|
|
290
|
-
const opts = makeOpts({ worktree: WORKTREE });
|
|
291
|
-
const promise = runSpawn(makeRecord("sa-wt-throw"), "Task: wt-throw", makeOpts({ ...opts, onEvent }), ctx);
|
|
292
|
-
await waitForSpawn();
|
|
293
|
-
const child = lastSpawnedChild();
|
|
294
|
-
|
|
295
|
-
child.stdout.write(`${JSON.stringify(sessionHeader("sa-sess-wt2"))}\n`);
|
|
296
|
-
child.stdout.write(`${JSON.stringify({ type: "compaction_start" })}\n`);
|
|
297
|
-
await new Promise((r) => setImmediate(r));
|
|
298
|
-
|
|
299
|
-
// bestEffort 缺省 debug 级(msg + { detail } 两参):同步段异常不阻断 stdout 解析
|
|
300
|
-
expect(loggerMock.debug).toHaveBeenCalledWith(
|
|
301
|
-
expect.stringContaining("onWorktreePid callback (first header) failed"),
|
|
302
|
-
expect.objectContaining({ detail: "registry lock contention" }),
|
|
303
|
-
);
|
|
304
|
-
// 解析继续:header 之后的 compaction 事件照常外发
|
|
305
|
-
expect(onEvent).toHaveBeenCalledWith({ type: "compaction" });
|
|
306
|
-
|
|
307
|
-
await settleRunSpawn(child, promise);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it("alive marker 写失败(writeAliveMarker 抛错)→ best-effort debug 留痕,header 回填与解析照常", async () => {
|
|
311
|
-
const onEvent = vi.fn();
|
|
312
|
-
mockWriteAliveMarker.mockImplementation(() => {
|
|
313
|
-
throw new Error("disk full");
|
|
314
|
-
});
|
|
315
|
-
const record = makeRecord("sa-marker-fail");
|
|
316
|
-
const promise = runSpawn(record, "Task: marker-fail", makeOpts({ onEvent }), makeCtx());
|
|
317
|
-
await waitForSpawn();
|
|
318
|
-
const child = lastSpawnedChild();
|
|
319
|
-
|
|
320
|
-
child.stdout.write(`${JSON.stringify(sessionHeader("sa-sess-mf"))}\n`);
|
|
321
|
-
child.stdout.write(`${JSON.stringify({ type: "compaction_start" })}\n`);
|
|
322
|
-
await new Promise((r) => setImmediate(r));
|
|
323
|
-
|
|
324
|
-
// [持久化 C] marker 是崩溃恢复的增强信号,缺失只降低可恢复性,不影响执行主流程
|
|
325
|
-
expect(loggerMock.debug).toHaveBeenCalledWith(
|
|
326
|
-
expect.stringContaining("alive marker write failed (best-effort continue)"),
|
|
327
|
-
);
|
|
328
|
-
expect(record.sessionFile).toContain("sa-sess-mf");
|
|
329
|
-
expect(onEvent).toHaveBeenCalledWith({ type: "compaction" });
|
|
330
|
-
|
|
331
|
-
await settleRunSpawn(child, promise);
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
describe("[S-17a] setupFreshChild:worktree pid 注册防御 catch", () => {
|
|
336
|
-
beforeEach(() => {
|
|
337
|
-
vi.clearAllMocks();
|
|
338
|
-
mockSpawn.mockImplementation(fakeSpawnImpl);
|
|
339
|
-
_resetLifecycleState();
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
afterEach(() => {
|
|
343
|
-
_resetLifecycleState();
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it("第二处 onWorktreePid 注册抛错(防御 catch)→ warn 留痕,spawn 主流程不被阻断", async () => {
|
|
347
|
-
// 第一处(无保护)成功,第二处(try/catch 保护)抛错
|
|
348
|
-
const onWorktreePid = vi
|
|
349
|
-
.fn()
|
|
350
|
-
.mockReturnValueOnce(undefined)
|
|
351
|
-
.mockImplementation(() => {
|
|
352
|
-
throw new Error("registry write failed");
|
|
353
|
-
});
|
|
354
|
-
const ctx = makeCtx({ onWorktreePid } as Partial<SessionRunnerContext>);
|
|
355
|
-
const opts = makeOpts({
|
|
356
|
-
worktree: { path: "/tmp/wt-checkout", branch: "wt/defensive", baseCommit: "abc", mainCwd: "/tmp/test" },
|
|
357
|
-
});
|
|
358
|
-
const record = makeRecord("sa-wt-defensive");
|
|
359
|
-
const promise = runSpawn(record, "Task: wt-defensive", opts, ctx);
|
|
360
|
-
await waitForSpawn();
|
|
361
|
-
const child = lastSpawnedChild();
|
|
362
|
-
|
|
363
|
-
// [S1] 注册表写失败最坏后果是条目停留 pid=0(reaper 宽限兜底),不阻断 spawn
|
|
364
|
-
expect(loggerMock.warn).toHaveBeenCalledWith(
|
|
365
|
-
expect.stringContaining("worktree pid registration failed (defensive)"),
|
|
366
|
-
expect.objectContaining({ branch: "wt/defensive" }),
|
|
367
|
-
);
|
|
368
|
-
// prompt 命令已照常写入 stdin(spawn 装配未被中断)
|
|
369
|
-
expect(child.stdin.writable).toBe(true);
|
|
370
|
-
|
|
371
|
-
const result = await settleRunSpawn(child, promise);
|
|
372
|
-
expect(result.success).toBe(true);
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
describe("[S-17a] buildChildEnv / buildSpawnInvocation:env 与参数组装", () => {
|
|
377
|
-
beforeEach(() => {
|
|
378
|
-
vi.clearAllMocks();
|
|
379
|
-
mockSpawn.mockImplementation(fakeSpawnImpl);
|
|
380
|
-
_resetLifecycleState();
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
afterEach(() => {
|
|
384
|
-
_resetLifecycleState();
|
|
385
|
-
vi.unstubAllEnvs();
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it("relay 激活(SOCKET/NODE/SCRIPT 三 env 齐备)→ 子进程 env 注入归属键 SESSION_ID/RECORD_ID", async () => {
|
|
389
|
-
vi.stubEnv(RELAY_ENV_SOCKET, "/tmp/relay.sock");
|
|
390
|
-
vi.stubEnv(RELAY_ENV_NODE, "node-1");
|
|
391
|
-
vi.stubEnv(RELAY_ENV_SCRIPT, "/tmp/relay.mjs");
|
|
392
|
-
const record = makeRecord("sa-relay");
|
|
393
|
-
const promise = runSpawn(record, "Task: relay-env", makeOpts(), makeCtx());
|
|
394
|
-
await waitForSpawn();
|
|
395
|
-
|
|
396
|
-
const call = mockSpawn.mock.calls.at(-1);
|
|
397
|
-
expect(call).toBeDefined();
|
|
398
|
-
const env = call?.[2]?.env as Record<string, string | undefined> | undefined;
|
|
399
|
-
// [E 方案 §5.2-2] tee 帧路由键:SESSION_ID = ctx.sessionRootId、RECORD_ID = record.id
|
|
400
|
-
expect(env?.[RELAY_ENV_SESSION_ID]).toBe("root-session-test");
|
|
401
|
-
expect(env?.[RELAY_ENV_RECORD_ID]).toBe("sa-relay");
|
|
402
|
-
|
|
403
|
-
await settleRunSpawn(lastSpawnedChild(), promise);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it("relay 未激活 → 归属 env 不注入(未激活环境下携带 record 值 env 是误导噪声)", async () => {
|
|
407
|
-
const promise = runSpawn(makeRecord("sa-norelay"), "Task: no-relay", makeOpts(), makeCtx());
|
|
408
|
-
await waitForSpawn();
|
|
409
|
-
|
|
410
|
-
const call = mockSpawn.mock.calls.at(-1);
|
|
411
|
-
const env = call?.[2]?.env as Record<string, string | undefined> | undefined;
|
|
412
|
-
expect(env?.[RELAY_ENV_SESSION_ID]).toBeUndefined();
|
|
413
|
-
expect(env?.[RELAY_ENV_RECORD_ID]).toBeUndefined();
|
|
414
|
-
|
|
415
|
-
await settleRunSpawn(lastSpawnedChild(), promise);
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it("resume model 无 / 分隔(异常形态回显)→ provider 兜底 unknown,--model 仍可执行", async () => {
|
|
419
|
-
const promise = runSpawn(
|
|
420
|
-
makeRecord("sa-resume-noslash"),
|
|
421
|
-
"Task: resume",
|
|
422
|
-
makeOpts() as RunOptions,
|
|
423
|
-
makeCtx(),
|
|
424
|
-
{ sessionFile: "/tmp/prev-session.jsonl", model: "legacy-model-no-slash" },
|
|
425
|
-
);
|
|
426
|
-
await waitForSpawn();
|
|
427
|
-
|
|
428
|
-
const call = mockSpawn.mock.calls.at(-1);
|
|
429
|
-
const args = call?.[1] as string[];
|
|
430
|
-
// [U1 D2 豁免] resume 回显不经 registry 断言,无 / 时按 unknown provider 兜底
|
|
431
|
-
//(与 subagent-service record 回读同构)
|
|
432
|
-
const modelIdx = args.indexOf("--model");
|
|
433
|
-
expect(modelIdx).toBeGreaterThan(-1);
|
|
434
|
-
expect(args[modelIdx + 1]).toBe("unknown/legacy-model-no-slash");
|
|
435
|
-
// resume 锁定 sessionFile:--session 参数追加
|
|
436
|
-
const sessionIdx = args.indexOf("--session");
|
|
437
|
-
expect(args[sessionIdx + 1]).toBe("/tmp/prev-session.jsonl");
|
|
438
|
-
|
|
439
|
-
await settleRunSpawn(lastSpawnedChild(), promise);
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
describe("[S-17a] startGetStateHandshake:[F2] 同步写失败兜底", () => {
|
|
444
|
-
beforeEach(() => {
|
|
445
|
-
vi.clearAllMocks();
|
|
446
|
-
_resetLifecycleState();
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
afterEach(() => {
|
|
450
|
-
_resetLifecycleState();
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it("get_state 写 stdin 同步 EPIPE(stdin 已断)→ error 留痕 + 握手立即放弃,close 收尾不被阻塞", async () => {
|
|
454
|
-
// spawn 即返回 stdin 已断语义的 child:对 get_state 命令的 write 同步抛 EPIPE
|
|
455
|
-
//(prompt 命令写入正常,区分 writeStdinLine 的两条调用链)
|
|
456
|
-
mockSpawn.mockImplementation((): ChildProcess => {
|
|
457
|
-
const c = new FakeChild();
|
|
458
|
-
const realWrite = c.stdin.write.bind(c.stdin);
|
|
459
|
-
c.stdin.write = ((chunk: string | Uint8Array, ...rest: unknown[]) => {
|
|
460
|
-
if (typeof chunk === "string" && chunk.includes('"get_state"')) {
|
|
461
|
-
const err = new Error("write EPIPE") as NodeJS.ErrnoException;
|
|
462
|
-
err.code = "EPIPE";
|
|
463
|
-
throw err;
|
|
464
|
-
}
|
|
465
|
-
return realWrite(chunk as Buffer, ...(rest as []));
|
|
466
|
-
}) as typeof c.stdin.write;
|
|
467
|
-
return c as unknown as ChildProcess;
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
const record = makeRecord("sa-hs-epipe");
|
|
471
|
-
const promise = runSpawn(record, "Task: hs-epipe", makeOpts(), makeCtx());
|
|
472
|
-
await waitForSpawn();
|
|
473
|
-
const child = lastSpawnedChild();
|
|
474
|
-
await new Promise((r) => setImmediate(r));
|
|
475
|
-
|
|
476
|
-
// [F2] unhandledRejection 防线:executor 同步异常转 reject 后被 .catch 接住,
|
|
477
|
-
// 错误消息带 EPIPE 语义与冷路径恢复指引
|
|
478
|
-
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
479
|
-
expect.stringContaining("get_state handshake failed"),
|
|
480
|
-
);
|
|
481
|
-
expect(loggerMock.error).toHaveBeenCalledWith(expect.stringContaining("EPIPE"));
|
|
482
|
-
|
|
483
|
-
// 握手已放弃(abandonHandshake):close 收尾不被阻塞,runSpawn 正常 resolve
|
|
484
|
-
const result = await settleRunSpawn(child, promise, 0);
|
|
485
|
-
expect(result.success).toBe(true);
|
|
486
|
-
// sessionFile 未回填(握手失败形态,收尾由 existsSync + findSessionFileByHeaderId 兜底)
|
|
487
|
-
expect(record.sessionFile).toBeUndefined();
|
|
488
|
-
});
|
|
489
|
-
});
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
// src/execution/__tests__/session-runner-epipe.test.ts
|
|
2
|
-
//
|
|
3
|
-
// [v4 A-1] session-runner 异步 stdin 'error' listener 测试。
|
|
4
|
-
//
|
|
5
|
-
// 背景:writeStdinLine 的 try/catch 只覆盖同步 write 抛错;子进程退出后内核回写 EPIPE
|
|
6
|
-
// 以异步 stream 'error' event 到达,若无 listener 会让 Node 抛 unhandled 'error' 崩主进程(P1)。
|
|
7
|
-
// runSpawn 在 spawn 后、首次 stdin 写入前注册 child.stdin.on('error') handler,合并
|
|
8
|
-
// 同步/异步 EPIPE 计数(共用 stdin-writer 的 recordEpipeFailure)。
|
|
9
|
-
//
|
|
10
|
-
// 验收:① listener 存在 ② error 触发后 spawnedChildren 不含该 recordId
|
|
11
|
-
// ③ recordEpipeFailure 计数累加(同步/异步合并)
|
|
12
|
-
|
|
13
|
-
import { spawn } from "node:child_process";
|
|
14
|
-
|
|
15
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
16
|
-
|
|
17
|
-
vi.mock("node:child_process", async () => {
|
|
18
|
-
const { FakeChild } = await import("./helpers/spawn-mock.ts");
|
|
19
|
-
return {
|
|
20
|
-
spawn: vi.fn(() => new FakeChild()),
|
|
21
|
-
// buildEnvBlock 的 git branch 调用(execFile 异步):默认 err-first 兜底 → catch → branch=""
|
|
22
|
-
execFile: vi.fn(
|
|
23
|
-
(
|
|
24
|
-
_cmd: string,
|
|
25
|
-
_args: readonly string[],
|
|
26
|
-
_opts: unknown,
|
|
27
|
-
cb: (err: Error | null, stdout?: string, stderr?: string) => void,
|
|
28
|
-
) => cb(new Error("execFile not configured in this test")),
|
|
29
|
-
),
|
|
30
|
-
};
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
vi.mock("node:fs", async () => {
|
|
34
|
-
const actual = await import("node:fs");
|
|
35
|
-
return {
|
|
36
|
-
default: {
|
|
37
|
-
...actual,
|
|
38
|
-
mkdirSync: vi.fn(),
|
|
39
|
-
existsSync: vi.fn(() => false),
|
|
40
|
-
appendFileSync: vi.fn(),
|
|
41
|
-
writeFileSync: vi.fn(),
|
|
42
|
-
readdirSync: vi.fn(() => []),
|
|
43
|
-
},
|
|
44
|
-
mkdirSync: vi.fn(),
|
|
45
|
-
existsSync: vi.fn(() => false),
|
|
46
|
-
appendFileSync: vi.fn(),
|
|
47
|
-
writeFileSync: vi.fn(),
|
|
48
|
-
readdirSync: vi.fn(() => []),
|
|
49
|
-
promises: actual.promises,
|
|
50
|
-
};
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
vi.mock("../alive-store.ts", () => ({ writeAliveMarker: vi.fn() }));
|
|
54
|
-
|
|
55
|
-
vi.mock("../session-pending.ts", () => ({
|
|
56
|
-
readActivePendingFromSessionFile: vi.fn(() => ({ count: 0 })),
|
|
57
|
-
prunePendingCursor: vi.fn(),
|
|
58
|
-
}));
|
|
59
|
-
|
|
60
|
-
vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
|
|
61
|
-
writePromptToTempFile: vi.fn(async (agent: string) => {
|
|
62
|
-
const safeName = agent.replace(/[^\w.-]+/g, "_");
|
|
63
|
-
return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
|
|
64
|
-
}),
|
|
65
|
-
cleanupTempPrompt: vi.fn(async () => {}),
|
|
66
|
-
}));
|
|
67
|
-
|
|
68
|
-
import {
|
|
69
|
-
EPIPE_FAILURE_THRESHOLD,
|
|
70
|
-
recordEpipeFailure,
|
|
71
|
-
resetAllEpipeFailures,
|
|
72
|
-
} from "../engine/engines/pi/stdin-writer.ts";
|
|
73
|
-
import { runSpawn, spawnedChildren } from "../engine/engines/pi/session-runner.ts";
|
|
74
|
-
import { _resetLifecycleState } from "../lifecycle-manager.ts";
|
|
75
|
-
import {
|
|
76
|
-
FakeChild,
|
|
77
|
-
lastSpawnedChild as lastSpawnedChildOf,
|
|
78
|
-
makeCtx,
|
|
79
|
-
makeOpts,
|
|
80
|
-
makeRecord,
|
|
81
|
-
waitForSpawn as waitForSpawnOf,
|
|
82
|
-
} from "./helpers/spawn-mock.ts";
|
|
83
|
-
|
|
84
|
-
const mockSpawn = vi.mocked(spawn);
|
|
85
|
-
const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
|
|
86
|
-
const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
|
|
87
|
-
|
|
88
|
-
/** 构造 EPIPE errno 错误(模拟子进程退出后内核回写 EPIPE)。 */
|
|
89
|
-
function epipeError(message = "write after end"): NodeJS.ErrnoException {
|
|
90
|
-
const err = new Error(message) as NodeJS.ErrnoException;
|
|
91
|
-
err.code = "EPIPE";
|
|
92
|
-
return err;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** 手动 emit close 让 runSpawn 的 exitCode promise resolve(非 chatMode 默认 close resolve)。 */
|
|
96
|
-
function settleRunSpawn(child: FakeChild): void {
|
|
97
|
-
child.stdout.end();
|
|
98
|
-
child.stderr.end();
|
|
99
|
-
child.emit("close", 0);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
describe("[v4 A-1] session-runner 异步 stdin 'error' listener", () => {
|
|
103
|
-
beforeEach(() => {
|
|
104
|
-
vi.clearAllMocks();
|
|
105
|
-
_resetLifecycleState();
|
|
106
|
-
resetAllEpipeFailures();
|
|
107
|
-
spawnedChildren.clear();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
afterEach(() => {
|
|
111
|
-
_resetLifecycleState();
|
|
112
|
-
resetAllEpipeFailures();
|
|
113
|
-
spawnedChildren.clear();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("① spawn 后注册 child.stdin 'error' listener", async () => {
|
|
117
|
-
const record = makeRecord("sa-listener");
|
|
118
|
-
const promise = runSpawn(record, "Task: listener-exists", makeOpts(), makeCtx());
|
|
119
|
-
|
|
120
|
-
await waitForSpawn();
|
|
121
|
-
const child = lastSpawnedChild();
|
|
122
|
-
|
|
123
|
-
// listener 已注册——无 listener 时异步 EPIPE 会崩主进程(这是 A-1 修复的 P1)
|
|
124
|
-
expect(child.stdin.listenerCount("error")).toBeGreaterThan(0);
|
|
125
|
-
|
|
126
|
-
settleRunSpawn(child);
|
|
127
|
-
await promise;
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("②③ async stdin 'error' → 移出 spawnedChildren + recordEpipeFailure 计数递增", async () => {
|
|
131
|
-
const record = makeRecord("sa-async-epipe");
|
|
132
|
-
const promise = runSpawn(record, "Task: async-epipe", makeOpts(), makeCtx());
|
|
133
|
-
|
|
134
|
-
await waitForSpawn();
|
|
135
|
-
const child = lastSpawnedChild();
|
|
136
|
-
|
|
137
|
-
// runSpawn 已把 child 注册进 spawnedChildren
|
|
138
|
-
expect(spawnedChildren.has(record.id)).toBe(true);
|
|
139
|
-
|
|
140
|
-
// 发一次异步 stdin 'error'(count 0→1,未达阈值,handler 只 warn 不 throw)
|
|
141
|
-
child.stdin.emit("error", epipeError());
|
|
142
|
-
|
|
143
|
-
// ② spawnedChildren 已移除该 recordId(标记 dead)
|
|
144
|
-
expect(spawnedChildren.has(record.id)).toBe(false);
|
|
145
|
-
|
|
146
|
-
// ③ 计数累加:handler 调 recordEpipeFailure 设为 1;此处再调一次应返回 2
|
|
147
|
-
// (若 handler 未运行,counter 仍为 0,本次调用只会返回 1)
|
|
148
|
-
expect(recordEpipeFailure(record.id)).toBe(2);
|
|
149
|
-
|
|
150
|
-
settleRunSpawn(child);
|
|
151
|
-
await promise;
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("③ 同步预置计数 + 异步 stdin 'error' 合并计数(handler 不 throw,防 listener 内 throw 崩进程)", async () => {
|
|
155
|
-
const record = makeRecord("sa-threshold");
|
|
156
|
-
const promise = runSpawn(record, "Task: threshold", makeOpts(), makeCtx());
|
|
157
|
-
|
|
158
|
-
await waitForSpawn();
|
|
159
|
-
const child = lastSpawnedChild();
|
|
160
|
-
|
|
161
|
-
// 预置 count=1(模拟 deliverMessage 同步路径已计 1 次失败——证明共用同一计数器)
|
|
162
|
-
expect(recordEpipeFailure(record.id)).toBe(1);
|
|
163
|
-
|
|
164
|
-
// [v4 A-1 裁决] async handler 内不 throw(stream 'error' listener 内 throw 会经 emit
|
|
165
|
-
// 传播为 uncaughtException 崩主进程,违背 A-1 防崩)——达阈值 throw 留同步路径。
|
|
166
|
-
// emit 不抛错:
|
|
167
|
-
expect(() => child.stdin.emit("error", epipeError())).not.toThrow();
|
|
168
|
-
|
|
169
|
-
// handler 调 recordEpipeFailure → count=2(达阈值但 async 不 throw);此处再调返回 3
|
|
170
|
-
expect(recordEpipeFailure(record.id)).toBe(3);
|
|
171
|
-
expect(EPIPE_FAILURE_THRESHOLD).toBe(2);
|
|
172
|
-
|
|
173
|
-
// handler 已移出 spawnedChildren(标记 dead,下次 deliverMessage 检测走冷路径)
|
|
174
|
-
expect(spawnedChildren.has(record.id)).toBe(false);
|
|
175
|
-
|
|
176
|
-
settleRunSpawn(child);
|
|
177
|
-
await promise;
|
|
178
|
-
});
|
|
179
|
-
});
|