@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.
Files changed (278) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-KXVBIVOU.js +16 -0
  3. package/dist/chunk-T2SYBW3I.js +24 -0
  4. package/dist/chunk-VURUAGMM.js +2305 -0
  5. package/dist/{types-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
  6. package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -668
  7. package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
  8. package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
  9. package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
  10. package/dist/execution/engine/engine-discovery-scan.js +20 -0
  11. package/dist/execution/engine/paths.cjs +6 -23
  12. package/dist/execution/engine/paths.d.cts +1 -8
  13. package/dist/execution/engine/paths.d.ts +1 -8
  14. package/dist/execution/engine/paths.js +1 -1
  15. package/dist/execution/relay-env.cjs +12 -28
  16. package/dist/execution/relay-env.d.cts +1 -33
  17. package/dist/execution/relay-env.d.ts +1 -33
  18. package/dist/execution/relay-env.js +5 -3
  19. package/dist/index.cjs +14045 -16059
  20. package/dist/index.d.cts +1035 -1579
  21. package/dist/index.d.ts +1035 -1579
  22. package/dist/index.js +9838 -13800
  23. package/dist.bundle/index.cjs +28875 -30323
  24. package/package.json +20 -26
  25. package/src/__tests__/record-store-last-line.test.ts +16 -7
  26. package/src/core/error-message.ts +7 -6
  27. package/src/core/host-services.ts +8 -2
  28. package/src/core/notify-ports.ts +17 -2
  29. package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
  30. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
  31. package/src/execution/__tests__/collect-coordinator-service.test.ts +36 -47
  32. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
  33. package/src/execution/__tests__/delivery-methods.test.ts +179 -237
  34. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
  35. package/src/execution/__tests__/execute-nesting.test.ts +20 -13
  36. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +20 -33
  37. package/src/execution/__tests__/gc-timer.test.ts +7 -22
  38. package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
  39. package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
  40. package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
  41. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
  42. package/src/execution/__tests__/idle-gc.test.ts +200 -0
  43. package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
  44. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
  45. package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
  46. package/src/execution/__tests__/record-store.test.ts +60 -6
  47. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
  48. package/src/execution/__tests__/round-supervisor.test.ts +293 -0
  49. package/src/execution/__tests__/session-pending.test.ts +119 -17
  50. package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
  51. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -11
  52. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
  53. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
  54. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
  55. package/src/execution/__tests__/subagent-service.test.ts +2 -2
  56. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
  57. package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
  58. package/src/execution/__tests__/sync-collect-recovery.test.ts +21 -36
  59. package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
  60. package/src/execution/agent-registry.ts +6 -2
  61. package/src/execution/dialog-queue.ts +6 -73
  62. package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
  63. package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
  64. package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
  65. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
  66. package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
  67. package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
  68. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
  69. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
  70. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
  71. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
  72. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
  73. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
  74. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
  75. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +81 -54
  76. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
  77. package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
  78. package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
  79. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +7 -2
  80. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
  81. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
  82. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
  83. package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
  84. package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
  85. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
  86. package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
  87. package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
  88. package/src/execution/engine/__tests__/registry.test.ts +206 -1
  89. package/src/execution/engine/__tests__/routing.test.ts +97 -0
  90. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
  91. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
  92. package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
  93. package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
  94. package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
  95. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
  96. package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
  97. package/src/execution/engine/client/client-options.ts +103 -0
  98. package/src/execution/engine/client/engine-client.ts +687 -0
  99. package/src/execution/engine/client/mirror.ts +167 -0
  100. package/src/execution/engine/client/pid-file.ts +392 -0
  101. package/src/execution/engine/client/reaper.ts +95 -0
  102. package/src/execution/engine/client/remote-engine.ts +441 -0
  103. package/src/execution/engine/client/reverse-router.ts +255 -0
  104. package/src/execution/engine/common/capability-gate.ts +109 -22
  105. package/src/execution/engine/common/errors.ts +9 -25
  106. package/src/execution/engine/common/event-journal.ts +4 -50
  107. package/src/execution/engine/common/journal-replay.ts +3 -1
  108. package/src/execution/engine/common/kill-chain.ts +18 -251
  109. package/src/execution/engine/common/nesting-guard.ts +18 -114
  110. package/src/execution/engine/common/pool-manager.ts +4 -0
  111. package/src/execution/engine/common/session-view-service.ts +56 -84
  112. package/src/execution/engine/config.ts +151 -0
  113. package/src/execution/engine/d8-compat.ts +346 -0
  114. package/src/execution/engine/engine-discovery-roots.ts +107 -0
  115. package/src/execution/engine/engine-discovery-scan.ts +269 -0
  116. package/src/execution/engine/engine-discovery.ts +30 -7
  117. package/src/execution/engine/engine-inspect-package.ts +282 -0
  118. package/src/execution/engine/engine-manifest.ts +244 -0
  119. package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
  120. package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
  121. package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
  122. package/src/execution/engine/host/host-bridge.ts +150 -0
  123. package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
  124. package/src/execution/engine/host/pi-host-binding.ts +91 -0
  125. package/src/execution/engine/host/spawned-children.ts +139 -0
  126. package/src/execution/engine/host-task-spec.ts +29 -2
  127. package/src/execution/engine/model-validation.ts +55 -2
  128. package/src/execution/engine/paths.ts +16 -41
  129. package/src/execution/engine/port.ts +48 -2
  130. package/src/execution/engine/registry.ts +153 -34
  131. package/src/execution/engine/routing.ts +225 -52
  132. package/src/execution/engine/types.ts +25 -200
  133. package/src/execution/finalize-record.ts +25 -0
  134. package/src/execution/idle-gc.ts +89 -14
  135. package/src/execution/lifecycle-manager.ts +10 -7
  136. package/src/execution/lifecycle-predicates.ts +10 -8
  137. package/src/execution/record-store.ts +29 -2
  138. package/src/execution/relay-env.ts +19 -36
  139. package/src/execution/round-supervisor/domain.ts +89 -0
  140. package/src/execution/round-supervisor/index.ts +33 -0
  141. package/src/execution/round-supervisor/notify-accounting.ts +87 -0
  142. package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
  143. package/src/execution/round-supervisor/service-binding.test.ts +661 -0
  144. package/src/execution/round-supervisor/service-binding.ts +273 -0
  145. package/src/execution/round-supervisor/supervisor.ts +426 -0
  146. package/src/execution/session-pending.ts +68 -74
  147. package/src/execution/settled-watchdog.ts +39 -0
  148. package/src/execution/subagent-service.ts +746 -400
  149. package/src/execution/subprocess-agent-runner.ts +16 -8
  150. package/src/execution/types.ts +41 -117
  151. package/src/execution/ui-channels.ts +13 -214
  152. package/src/execution/ui-request-handler-factory.ts +7 -4
  153. package/src/execution/ui-request-observability.ts +2 -2
  154. package/src/execution/workflow-state-root.ts +67 -0
  155. package/src/execution/worktree-git-ops.ts +5 -0
  156. package/src/execution/worktree-manager.ts +11 -1
  157. package/src/index.ts +55 -26
  158. package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
  159. package/src/orchestration/execute-agent-call.ts +1 -1
  160. package/src/orchestration/file-run-store.ts +144 -59
  161. package/src/orchestration/models/types.ts +43 -42
  162. package/src/shared/zcode-model-ref.ts +31 -0
  163. package/dist/chunk-43ONBFZX.js +0 -240
  164. package/dist/chunk-A4IVZWVX.js +0 -31
  165. package/dist/chunk-APZY4IME.js +0 -27
  166. package/dist/chunk-V3VHZ2VX.js +0 -59
  167. package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
  168. package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
  169. package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
  170. package/dist/execution/engine/engines/zcode/constants.js +0 -54
  171. package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
  172. package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
  173. package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
  174. package/dist/execution/engine/engines/zcode/reader.js +0 -9
  175. package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
  176. package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
  177. package/src/__tests__/session-runner.test.ts +0 -69
  178. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  179. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
  180. package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
  181. package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
  182. package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
  183. package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
  184. package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
  185. package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
  186. package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
  187. package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
  188. package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
  189. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
  190. package/src/execution/__tests__/output-collector.test.ts +0 -469
  191. package/src/execution/__tests__/pi-invocation.test.ts +0 -166
  192. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
  193. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  194. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
  195. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
  196. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
  197. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
  198. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  199. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  200. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  201. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
  202. package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
  203. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
  204. package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
  205. package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
  206. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
  207. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
  208. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
  209. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
  210. package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
  211. package/src/execution/__tests__/spawn-args.test.ts +0 -446
  212. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
  213. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  214. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
  215. package/src/execution/__tests__/spawned-children.test.ts +0 -236
  216. package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
  217. package/src/execution/__tests__/stdin-writer.test.ts +0 -464
  218. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
  219. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  220. package/src/execution/__tests__/timeout-integration.test.ts +0 -616
  221. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  222. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  223. package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
  224. package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
  225. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
  226. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  227. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
  228. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  229. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
  230. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  231. package/src/execution/engine/common/schema-emulation.ts +0 -189
  232. package/src/execution/engine/common/session-view-projection.ts +0 -51
  233. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
  234. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  235. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
  236. package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
  237. package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
  238. package/src/execution/engine/engines/pi/output-collector.ts +0 -317
  239. package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
  240. package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
  241. package/src/execution/engine/engines/pi/reader.ts +0 -59
  242. package/src/execution/engine/engines/pi/registration.ts +0 -37
  243. package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
  244. package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
  245. package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
  246. package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
  247. package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
  248. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
  249. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
  250. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
  251. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
  252. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
  253. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
  254. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  255. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
  256. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
  257. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
  258. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
  259. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
  260. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
  261. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
  262. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
  263. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
  264. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
  265. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
  266. package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
  267. package/src/execution/engine/engines/zcode/connection.ts +0 -598
  268. package/src/execution/engine/engines/zcode/constants.ts +0 -176
  269. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
  270. package/src/execution/engine/engines/zcode/parser.ts +0 -102
  271. package/src/execution/engine/engines/zcode/preparer.ts +0 -235
  272. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  273. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  274. package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
  275. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
  276. package/src/execution/round-settlement.ts +0 -93
  277. package/src/execution/ui-request-queue.ts +0 -215
  278. 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
- });