@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,490 +0,0 @@
1
- // src/execution/__tests__/ask-user-transit-e2e.test.ts
2
- //
3
- // #34 跨进程 ask_user transit 完整 e2e 测试。
4
- //
5
- // 验证完整链路:
6
- // FakeChild.stdout emit extension_ui_request
7
- // → runSpawn stdout pump(parseSpawnLine 解析)
8
- // → createUiRequestQueue 入队 + handleUiRequest
9
- // → ctx.uiRequestHandler(由 createUiRequestHandlerForMode 构造)
10
- // → channel registry 命中 'ask_user' → channel handler 返回固定答案
11
- // → respond 回写 child.stdin(extension_ui_response)
12
- //
13
- // 与单元测试的差异:ui-request-handler.test.ts 只测 parseSpawnLine + parseChannel +
14
- // createUiRequestQueue 各层独立;本文件串起**所有层**(adapter → queue → factory →
15
- // channel handler → respond),覆盖跨进程协议透传的真实链路,是 ask_user 功能的
16
- // 端到端契约验证。
17
- //
18
- // 关键被测对象:
19
- // 1. spawn-event-adapter.parseSpawnLine:识别 extension_ui_request 行
20
- // 2. session-runner.runSpawn 的 stdout pump:调 enqueueUiRequest
21
- // 3. ui-request-queue.createUiRequestQueue:FIFO + handleUiRequest
22
- // 4. ui-request-handler-factory.createUiRequestHandlerForMode:channel 路由 + 默认转发
23
- // 5. stdin-writer.respond:回写 extension_ui_response
24
- //
25
- // mock 策略:与 run-spawn-integration.test.ts 一致(共享 helpers/spawn-mock.ts),
26
- // 额外注入 ctx.uiRequestHandler + ctx.dialogQueue + channel registry。
27
-
28
- import { spawn } from "node:child_process";
29
- import * as fs from "node:fs";
30
-
31
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
32
-
33
- // ── mock modules(与 run-spawn-*.test.ts 一致)──
34
-
35
- vi.mock("node:child_process", async () => {
36
- const { FakeChild } = await import("./helpers/spawn-mock.ts");
37
- return {
38
- spawn: vi.fn(() => new FakeChild()),
39
- // buildEnvBlock 的 git branch 调用(execFile 异步):默认 err-first 兜底 → catch → branch=""
40
- execFile: vi.fn(
41
- (
42
- _cmd: string,
43
- _args: readonly string[],
44
- _opts: unknown,
45
- cb: (err: Error | null, stdout?: string, stderr?: string) => void,
46
- ) => cb(new Error("execFile not configured in this test")),
47
- ),
48
- };
49
- });
50
-
51
- vi.mock("node:fs", async () => {
52
- const actual = await import("node:fs");
53
- return {
54
- default: {
55
- ...actual,
56
- mkdirSync: vi.fn(),
57
- existsSync: vi.fn(() => false),
58
- appendFileSync: vi.fn(),
59
- writeFileSync: vi.fn(),
60
- readdirSync: vi.fn(() => []),
61
- },
62
- mkdirSync: vi.fn(),
63
- existsSync: vi.fn(() => false),
64
- appendFileSync: vi.fn(),
65
- writeFileSync: vi.fn(),
66
- readdirSync: vi.fn(() => []),
67
- promises: actual.promises,
68
- };
69
- });
70
-
71
- vi.mock("../alive-store.ts", () => ({
72
- writeAliveMarker: vi.fn(),
73
- }));
74
-
75
- vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
76
- writePromptToTempFile: vi.fn(async (agent: string) => {
77
- const safeName = agent.replace(/[^\w.-]+/g, "_");
78
- return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
79
- }),
80
- cleanupTempPrompt: vi.fn(async () => {}),
81
- }));
82
-
83
- import { DialogGlobalQueue } from "../dialog-queue.ts";
84
- import { runSpawn, type SessionRunnerContext } from "../engine/engines/pi/session-runner.ts";
85
- import { type ChannelHandler,createUiChannelRegistry } from "../ui-channels.ts";
86
- import { createUiRequestHandlerForMode } from "../ui-request-handler-factory.ts";
87
- import {
88
- emitStdoutLine,
89
- type FakeChild,
90
- lastSpawnedChild as lastSpawnedChildOf,
91
- makeCtx as makeCtxBase,
92
- makeOpts,
93
- makeRecord,
94
- sessionHeader,
95
- waitForSpawn as waitForSpawnOf,
96
- } from "./helpers/spawn-mock.ts";
97
-
98
- const mockSpawn = vi.mocked(spawn);
99
- const mockExistsSync = vi.mocked(fs.existsSync);
100
-
101
- const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
102
- const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
103
-
104
- // ── ask_user 协议样本(真实 Pi 格式)──
105
-
106
- const ASK_USER_MARKER = "\0XYZ_ASK_USER";
107
-
108
- interface AskUserPayload {
109
- questions: Array<{
110
- question: string;
111
- options: Array<{ label: string }>;
112
- }>;
113
- allowCancel: boolean;
114
- }
115
-
116
- const askUserPayload: AskUserPayload = {
117
- questions: [
118
- {
119
- question: "What is your preference?",
120
- options: [{ label: "Option A" }, { label: "Option B" }],
121
- },
122
- ],
123
- allowCancel: true,
124
- };
125
-
126
- /** 构造 Pi 原生 extension_ui_request stdout 行(ask_user 借道 select 通道)。 */
127
- function askUserLine(id: string): string {
128
- return JSON.stringify({
129
- type: "extension_ui_request",
130
- id,
131
- method: "select",
132
- title: ASK_USER_MARKER,
133
- options: [JSON.stringify(askUserPayload)],
134
- });
135
- }
136
-
137
- /** 读出 child.stdin 已缓冲的全部字节。 */
138
- function readStdin(child: FakeChild): string {
139
- child.stdin.pause();
140
- return child.stdin.read()?.toString() ?? "";
141
- }
142
-
143
- /** 从 child.stdin 按行拆分(去空行 + JSON.parse)。 */
144
- function readStdinLines(child: FakeChild): unknown[] {
145
- return readStdin(child)
146
- .split("\n")
147
- .map((l) => l.trim())
148
- .filter((l) => l.length > 0)
149
- .map((l) => JSON.parse(l));
150
- }
151
-
152
- /** 构造 ctx,注入 uiRequestHandler(由 createUiRequestHandlerForMode 构建)。 */
153
- function makeAskUserCtx(
154
- registry: ReturnType<typeof createUiChannelRegistry>,
155
- dialogQueue: DialogGlobalQueue,
156
- overrides: Partial<SessionRunnerContext> = {},
157
- ): SessionRunnerContext {
158
- // ctx.ui / ctx.mode 是 createUiRequestHandlerForMode 的入参。
159
- // mode='rpc' → hostMode='gui'(全透传),dialog 进 dialogQueue 串行。
160
- // ctx.ui 在 channel 命中时不被调用(channel handler 直接返回),故 ui 用 stub 即可。
161
- const ctx = {
162
- cwd: "/tmp/test",
163
- mode: "rpc" as const,
164
- sessionManager: {
165
- getSessionId: () => "s1",
166
- getSessionFile: () => undefined,
167
- getSessionDir: () => "/tmp/test/sessions",
168
- },
169
- modelRegistry: undefined,
170
- model: undefined,
171
- } as unknown as SessionRunnerContext;
172
- const handler = createUiRequestHandlerForMode(ctx as never, registry, dialogQueue);
173
- return makeCtxBase({
174
- ...overrides,
175
- uiRequestHandler: handler,
176
- dialogQueue,
177
- });
178
- }
179
-
180
- beforeEach(() => {
181
- vi.clearAllMocks();
182
- mockExistsSync.mockReturnValue(false);
183
- // 静默 stdin-writer / factory 的 warn(序列化失败降级等场景)
184
- vi.spyOn(console, "warn").mockImplementation(() => {});
185
- vi.spyOn(console, "error").mockImplementation(() => {});
186
- });
187
-
188
- afterEach(() => {
189
- vi.restoreAllMocks();
190
- });
191
-
192
- // ============================================================
193
- // E2E:完整 transit 链路(adapter → queue → factory → channel → respond)
194
- // ============================================================
195
-
196
- describe("ask_user 跨进程 transit e2e (#34)", () => {
197
- it("channel handler 命中 ask_user → 返回的 value 经 respond 回写 child.stdin", async () => {
198
- // 1. 注册 ask_user channel handler,返回固定答案(模拟用户选了 Option A)
199
- const registry = createUiChannelRegistry();
200
- const answer = JSON.stringify({ q0: "Option A" });
201
- const channelHandler: ChannelHandler = vi.fn(async () => ({ value: answer }));
202
- registry.register("ask_user", channelHandler);
203
-
204
- const dialogQueue = new DialogGlobalQueue();
205
- const ctx = makeAskUserCtx(registry, dialogQueue);
206
-
207
- // 2. 启动 runSpawn(不 await,spawn 后异步 emit 请求)
208
- const record = makeRecord();
209
- const promise = runSpawn(record, "Task: ask-user-e2e", makeOpts(), ctx);
210
-
211
- await waitForSpawn();
212
- const child = lastSpawnedChild();
213
-
214
- // 3. emit session header(runSpawn 需要 header 才能完成 close 路径)+ ask_user 请求
215
- emitStdoutLine(child, sessionHeader("sess-ask-1"));
216
- child.stdout.write(askUserLine("req-ask-1") + "\n");
217
-
218
- // 4. 给 stdout pump + channel handler (async) + respond 时间执行
219
- // PassThrough data listener flush + microtask + handler promise resolve 需若干 tick。
220
- await new Promise((r) => setTimeout(r, 30));
221
-
222
- // 5. 收尾:让 runSpawn resolve
223
- child.stdout.end();
224
- child.emit("close", 0);
225
- await promise;
226
-
227
- // 6. 断言 child.stdin 收到 extension_ui_response(value 分支)
228
- const responseLines = readStdinLines(child).filter(
229
- (l): l is { type: string; id: string; value?: string } =>
230
- typeof l === "object" && l !== null && (l as { type?: string }).type === "extension_ui_response",
231
- );
232
- expect(responseLines.length).toBe(1);
233
- expect(responseLines[0]!.id).toBe("req-ask-1");
234
- expect(responseLines[0]!.value).toBe(answer);
235
-
236
- // 7. channel handler 被调用一次,入参是 UiRequest(含 channel='ask_user' + payload)
237
- expect(channelHandler).toHaveBeenCalledTimes(1);
238
- const handlerArg = (channelHandler as ReturnType<typeof vi.fn>).mock.calls[0]?.[0] as {
239
- method: string;
240
- channel: string;
241
- channelPayload: AskUserPayload;
242
- id: string;
243
- };
244
- expect(handlerArg.method).toBe("select");
245
- expect(handlerArg.channel).toBe("ask_user");
246
- expect(handlerArg.id).toBe("req-ask-1");
247
- expect(handlerArg.channelPayload).toEqual(askUserPayload);
248
- });
249
-
250
- it("channel handler 返回 {confirmed:true} → respond 写 confirmed 分支", async () => {
251
- const registry = createUiChannelRegistry();
252
- const channelHandler: ChannelHandler = vi.fn(async () => ({ confirmed: true }));
253
- registry.register("ask_user", channelHandler);
254
-
255
- const dialogQueue = new DialogGlobalQueue();
256
- const ctx = makeAskUserCtx(registry, dialogQueue);
257
-
258
- const record = makeRecord();
259
- const promise = runSpawn(record, "Task: confirm-e2e", makeOpts(), ctx);
260
-
261
- await waitForSpawn();
262
- const child = lastSpawnedChild();
263
-
264
- emitStdoutLine(child, sessionHeader("sess-confirm"));
265
- child.stdout.write(askUserLine("req-confirm") + "\n");
266
- await new Promise((r) => setTimeout(r, 30));
267
-
268
- child.stdout.end();
269
- child.emit("close", 0);
270
- await promise;
271
-
272
- const responseLines = readStdinLines(child).filter(
273
- (l): l is { type: string; id: string; confirmed?: boolean } =>
274
- typeof l === "object" && l !== null && (l as { type?: string }).type === "extension_ui_response",
275
- );
276
- expect(responseLines).toHaveLength(1);
277
- expect(responseLines[0]!.id).toBe("req-confirm");
278
- expect(responseLines[0]!.confirmed).toBe(true);
279
- });
280
-
281
- it("channel handler 返回 {cancelled:true} → respond 写 cancelled 分支", async () => {
282
- const registry = createUiChannelRegistry();
283
- // 模拟用户取消(allowCancel 场景)
284
- const channelHandler: ChannelHandler = vi.fn(async () => ({ cancelled: true }));
285
- registry.register("ask_user", channelHandler);
286
-
287
- const dialogQueue = new DialogGlobalQueue();
288
- const ctx = makeAskUserCtx(registry, dialogQueue);
289
-
290
- const record = makeRecord();
291
- const promise = runSpawn(record, "Task: cancel-e2e", makeOpts(), ctx);
292
-
293
- await waitForSpawn();
294
- const child = lastSpawnedChild();
295
-
296
- emitStdoutLine(child, sessionHeader("sess-cancel"));
297
- child.stdout.write(askUserLine("req-cancel") + "\n");
298
- await new Promise((r) => setTimeout(r, 30));
299
-
300
- child.stdout.end();
301
- child.emit("close", 0);
302
- await promise;
303
-
304
- const responseLines = readStdinLines(child).filter(
305
- (l): l is { type: string; id: string; cancelled?: boolean } =>
306
- typeof l === "object" && l !== null && (l as { type?: string }).type === "extension_ui_response",
307
- );
308
- expect(responseLines).toHaveLength(1);
309
- expect(responseLines[0]!.id).toBe("req-cancel");
310
- expect(responseLines[0]!.cancelled).toBe(true);
311
- });
312
-
313
- it("多个 ask_user 请求 FIFO 串行 → 按到达顺序回写,channel handler 不并发", async () => {
314
- // L1 per-child 队列(createUiRequestQueue)保证同子进程内 FIFO 串行。
315
- // 验证:连续 emit 两个 ask_user,channel handler 按顺序被调,response 按顺序写回。
316
- const registry = createUiChannelRegistry();
317
- const callOrder: string[] = [];
318
- const channelHandler: ChannelHandler = vi.fn(async (req: unknown) => {
319
- callOrder.push((req as { id: string }).id);
320
- // 加延迟让两个请求有机会并发(若队列没串行)
321
- await new Promise((r) => setTimeout(r, 15));
322
- return { value: `ans-${(req as { id: string }).id}` };
323
- });
324
- registry.register("ask_user", channelHandler);
325
-
326
- const dialogQueue = new DialogGlobalQueue();
327
- const ctx = makeAskUserCtx(registry, dialogQueue);
328
-
329
- const record = makeRecord();
330
- const promise = runSpawn(record, "Task: fifo-e2e", makeOpts(), ctx);
331
-
332
- await waitForSpawn();
333
- const child = lastSpawnedChild();
334
-
335
- emitStdoutLine(child, sessionHeader("sess-fifo"));
336
- child.stdout.write(askUserLine("req-fifo-1") + "\n");
337
- child.stdout.write(askUserLine("req-fifo-2") + "\n");
338
- // 等两个 handler 都执行完(每个 ~15ms + overhead)
339
- await new Promise((r) => setTimeout(r, 80));
340
-
341
- child.stdout.end();
342
- child.emit("close", 0);
343
- await promise;
344
-
345
- // 串行:handler 按到达顺序被调,无并发
346
- expect(callOrder).toEqual(["req-fifo-1", "req-fifo-2"]);
347
- expect(channelHandler).toHaveBeenCalledTimes(2);
348
-
349
- // 两个 response 都写回,按 id 可找到
350
- const responses = readStdinLines(child).filter(
351
- (l): l is { type: string; id: string; value?: string } =>
352
- typeof l === "object" && l !== null && (l as { type?: string }).type === "extension_ui_response",
353
- );
354
- const ids = responses.map((r) => r.id).sort();
355
- expect(ids).toEqual(["req-fifo-1", "req-fifo-2"]);
356
- const r1 = responses.find((r) => r.id === "req-fifo-1");
357
- const r2 = responses.find((r) => r.id === "req-fifo-2");
358
- expect(r1?.value).toBe("ans-req-fifo-1");
359
- expect(r2?.value).toBe("ans-req-fifo-2");
360
- });
361
-
362
- it("channel 未注册 → defaultDialogForward 回 cancelled(ask_user 扩展未安装兜底)", async () => {
363
- // 不注册 ask_user channel → channelHandler 为 undefined → defaultDialogForward
364
- // defaultDialogForward 调 ctx.ui.select(这里 ctx.ui 是 stub,select 返 undefined → cancelled)
365
- const registry = createUiChannelRegistry();
366
- const dialogQueue = new DialogGlobalQueue();
367
-
368
- // ctx.ui.select 返回 undefined(模拟用户取消 / stub)
369
- const selectSpy = vi.fn(async () => undefined);
370
- const ctxBase = makeCtxBase();
371
- const fakeCtx = {
372
- cwd: "/tmp/test",
373
- mode: "rpc" as const,
374
- sessionManager: {
375
- getSessionId: () => "s1",
376
- getSessionFile: () => undefined,
377
- getSessionDir: () => "/tmp/test/sessions",
378
- },
379
- modelRegistry: undefined,
380
- model: undefined,
381
- ui: { select: selectSpy },
382
- } as never;
383
- const handler = createUiRequestHandlerForMode(fakeCtx, registry, dialogQueue);
384
- const ctx = { ...ctxBase, uiRequestHandler: handler, dialogQueue };
385
-
386
- const record = makeRecord();
387
- const promise = runSpawn(record, "Task: no-channel", makeOpts(), ctx);
388
-
389
- await waitForSpawn();
390
- const child = lastSpawnedChild();
391
-
392
- emitStdoutLine(child, sessionHeader("sess-nochannel"));
393
- child.stdout.write(askUserLine("req-nochannel") + "\n");
394
- await new Promise((r) => setTimeout(r, 30));
395
-
396
- child.stdout.end();
397
- child.emit("close", 0);
398
- await promise;
399
-
400
- // ctx.ui.select 被调(defaultDialogForward 兜底)
401
- expect(selectSpy).toHaveBeenCalledTimes(1);
402
- // response 是 cancelled(select 返 undefined → cancelled)
403
- const responses = readStdinLines(child).filter(
404
- (l): l is { type: string; id: string; cancelled?: boolean } =>
405
- typeof l === "object" && l !== null && (l as { type?: string }).type === "extension_ui_response",
406
- );
407
- expect(responses).toHaveLength(1);
408
- expect(responses[0]!.id).toBe("req-nochannel");
409
- expect(responses[0]!.cancelled).toBe(true);
410
- });
411
-
412
- it("channel handler 抛错 → respond 写 cancelled(队列不卡死)", async () => {
413
- const registry = createUiChannelRegistry();
414
- const channelHandler: ChannelHandler = vi.fn(async () => {
415
- throw new Error("handler boom");
416
- });
417
- registry.register("ask_user", channelHandler);
418
-
419
- const dialogQueue = new DialogGlobalQueue();
420
- const ctx = makeAskUserCtx(registry, dialogQueue);
421
-
422
- const record = makeRecord();
423
- const promise = runSpawn(record, "Task: handler-throws", makeOpts(), ctx);
424
-
425
- await waitForSpawn();
426
- const child = lastSpawnedChild();
427
-
428
- emitStdoutLine(child, sessionHeader("sess-throws"));
429
- child.stdout.write(askUserLine("req-throws") + "\n");
430
- await new Promise((r) => setTimeout(r, 30));
431
-
432
- child.stdout.end();
433
- child.emit("close", 0);
434
- await promise;
435
-
436
- // ui-request-queue 的 handleUiRequest catch handler 抛错 → respond cancelled
437
- const responses = readStdinLines(child).filter(
438
- (l): l is { type: string; id: string; cancelled?: boolean } =>
439
- typeof l === "object" && l !== null && (l as { type?: string }).type === "extension_ui_response",
440
- );
441
- expect(responses).toHaveLength(1);
442
- expect(responses[0]!.id).toBe("req-throws");
443
- expect(responses[0]!.cancelled).toBe(true);
444
- });
445
-
446
- it("child close 后 pending ask_user 的 handler 完成 → 不再写 stdin(signal aborted)", async () => {
447
- // 验证 R3:子进程退出时 AbortController.abort,handler 完成后 respond 跳过写入。
448
- // 构造:channel handler 在 child close 后才 resolve(模拟慢用户响应 + 子进程先退出)。
449
- const registry = createUiChannelRegistry();
450
- let resolveHandler: ((v: { value: string }) => void) | undefined;
451
- const channelHandler: ChannelHandler = vi.fn(
452
- () => new Promise<{ value: string }>((resolve) => {
453
- resolveHandler = resolve;
454
- }),
455
- );
456
- registry.register("ask_user", channelHandler);
457
-
458
- const dialogQueue = new DialogGlobalQueue();
459
- const ctx = makeAskUserCtx(registry, dialogQueue);
460
-
461
- const record = makeRecord();
462
- const promise = runSpawn(record, "Task: abort-pending", makeOpts(), ctx);
463
-
464
- await waitForSpawn();
465
- const child = lastSpawnedChild();
466
-
467
- emitStdoutLine(child, sessionHeader("sess-abort"));
468
- child.stdout.write(askUserLine("req-abort") + "\n");
469
- // 让 handler 入队(但未 resolve)
470
- await new Promise((r) => setTimeout(r, 20));
471
-
472
- // 子进程先退出(handler 仍 pending)→ onClose abort
473
- child.stdout.end();
474
- child.emit("close", 0);
475
- await promise;
476
-
477
- // 现在 handler 仍 pending;resolve 它(模拟延迟响应)
478
- expect(resolveHandler).toBeDefined();
479
- resolveHandler!({ value: "late-answer" });
480
- // 给 finally 块执行时间
481
- await new Promise((r) => setTimeout(r, 20));
482
-
483
- // child.stdin 不应收到 extension_ui_response(signal.aborted → respond 跳过)
484
- const responses = readStdinLines(child).filter(
485
- (l): l is { type: string; id: string } =>
486
- typeof l === "object" && l !== null && (l as { type?: string }).type === "extension_ui_response",
487
- );
488
- expect(responses).toHaveLength(0);
489
- });
490
- });
@@ -1,191 +0,0 @@
1
- // src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts
2
- //
3
- // [V2 决策 2] chatMode 首轮闭环——runSpawn resolve 时机(Step 4a 改动 1)。
4
- //
5
- // 改动 1:chatMode agent_settled 时提前 resolve runSpawn 的 exitCode promise(exit code 0),
6
- // 让 runAndFinalize 拿到首轮 result,进程仍保活(idle timer armed)。非 chatMode 不变
7
- //(agent_end kill → close resolve)。
8
- //
9
- // 本文件验证 resolve 时机(run-spawn-chatmode-settled.test.ts 验证 idle timer + 回调挂载点,
10
- // 收尾靠 emit close 让 resolve——隐含 agent_settled 不 resolve;本文件显式断言 agent_settled
11
- // 即 resolve,且进程未 close)。
12
- //
13
- // mock 结构与 run-spawn-chatmode-settled.test.ts 一致(FakeChild + session-pending count=0)。
14
-
15
- import { spawn } from "node:child_process";
16
- import * as fs from "node:fs";
17
-
18
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19
-
20
- vi.mock("node:child_process", async () => {
21
- const { FakeChild } = await import("./helpers/spawn-mock.ts");
22
- return {
23
- spawn: vi.fn(() => new FakeChild()),
24
- // buildEnvBlock 的 git branch 调用(execFile 异步):默认 err-first 兜底 → catch → branch=""
25
- execFile: vi.fn(
26
- (
27
- _cmd: string,
28
- _args: readonly string[],
29
- _opts: unknown,
30
- cb: (err: Error | null, stdout?: string, stderr?: string) => void,
31
- ) => cb(new Error("execFile not configured in this test")),
32
- ),
33
- };
34
- });
35
-
36
- vi.mock("node:fs", async () => {
37
- const actual = await import("node:fs");
38
- return {
39
- default: {
40
- ...actual,
41
- mkdirSync: vi.fn(),
42
- existsSync: vi.fn(() => false),
43
- appendFileSync: vi.fn(),
44
- writeFileSync: vi.fn(),
45
- readdirSync: vi.fn(() => []),
46
- },
47
- mkdirSync: vi.fn(),
48
- existsSync: vi.fn(() => false),
49
- appendFileSync: vi.fn(),
50
- writeFileSync: vi.fn(),
51
- readdirSync: vi.fn(() => []),
52
- promises: actual.promises,
53
- };
54
- });
55
-
56
- vi.mock("../alive-store.ts", () => ({ writeAliveMarker: vi.fn() }));
57
-
58
- // chatMode agent_end 早返回(continue),不读 session-pending;统一 count=0 防干扰。
59
- vi.mock("../session-pending.ts", () => ({
60
- readActivePendingFromSessionFile: vi.fn(() => ({ count: 0 })),
61
- prunePendingCursor: vi.fn(),
62
- }));
63
-
64
- vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
65
- writePromptToTempFile: vi.fn(async (agent: string) => {
66
- const safeName = agent.replace(/[^\w.-]+/g, "_");
67
- return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
68
- }),
69
- cleanupTempPrompt: vi.fn(async () => {}),
70
- }));
71
-
72
- import { runSpawn } from "../engine/engines/pi/session-runner.ts";
73
- import type { SessionRunnerContext } from "../engine/engines/pi/session-runner.ts";
74
- import { _resetLifecycleState } from "../lifecycle-manager.ts";
75
- import { createRecord } from "../execution-record.ts";
76
- import type { ExecutionRecord } from "../types.ts";
77
- import {
78
- emitStdoutLine,
79
- type FakeChild,
80
- lastSpawnedChild as lastSpawnedChildOf,
81
- makeCtx,
82
- makeOpts,
83
- makeRecord,
84
- sessionHeader,
85
- waitForSpawn as waitForSpawnOf,
86
- } from "./helpers/spawn-mock.ts";
87
-
88
- const mockSpawn = vi.mocked(spawn);
89
-
90
- const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
91
- const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
92
-
93
- /** 构造 chatMode record(spawn-mock.makeRecord 默认非 chatMode,此处显式开启)。 */
94
- function makeChatModeRecord(id = "sa-chat"): ExecutionRecord {
95
- return createRecord(id, {
96
- agent: "general-purpose",
97
- model: "test-model",
98
- mode: "background",
99
- task: "chat task",
100
- slug: "chat",
101
- startedAt: 1_000_000,
102
- rootSessionId: "root-session",
103
- parentRecordId: undefined,
104
- depth: 0,
105
- chatMode: true,
106
- });
107
- }
108
-
109
- /** 用 race 探测 promise 是否在指定时间内 settle(resolve 或 reject)。 */
110
- async function settlesWithin(p: Promise<unknown>, ms: number): Promise<boolean> {
111
- return Promise.race([
112
- p.then(
113
- () => true,
114
- () => true,
115
- ),
116
- new Promise<boolean>((r) => setTimeout(() => r(false), ms)),
117
- ]);
118
- }
119
-
120
- describe("[V2 决策 2] chatMode 首轮闭环:runSpawn resolve 时机(改动 1)", () => {
121
- beforeEach(() => {
122
- vi.clearAllMocks();
123
- _resetLifecycleState();
124
- });
125
-
126
- afterEach(() => {
127
- vi.restoreAllMocks();
128
- vi.useRealTimers();
129
- _resetLifecycleState();
130
- });
131
-
132
- it("chatMode agent_settled → runSpawn 提前 resolve(不等 close),进程仍保活", async () => {
133
- const record = makeChatModeRecord("sa-first-resolve");
134
- const promise = runSpawn(record, "Task: first-round", makeOpts(), makeCtx());
135
-
136
- await waitForSpawn();
137
- const child = lastSpawnedChild();
138
-
139
- emitStdoutLine(child, sessionHeader());
140
- emitStdoutLine(child, { type: "agent_end", willRetry: false });
141
- // 让 stdout flush 处理 agent_end(chatMode 分支 continue,不 kill)
142
- await new Promise((r) => setTimeout(r, 10));
143
- emitStdoutLine(child, { type: "agent_settled" });
144
-
145
- // [改动 1 核心] agent_settled 后 runSpawn 立即 resolve(resolveRun(0)),不等 close。
146
- // 若改动 1 未生效,promise 会 hang 等 close,settlesWithin 返回 false。
147
- expect(await settlesWithin(promise, 500)).toBe(true);
148
- const result = await promise;
149
- expect(result.success).toBe(true);
150
- expect(result.sessionId).toBe("sess-abc");
151
-
152
- // 进程仍保活:agent_settled 只 resolve promise,未 kill、未 emit close。
153
- // idle timer(5min 真实 setTimeout)armed 但未到期。
154
- expect(child.killed).toBe(false);
155
-
156
- // 收尾:手动 close(模拟后续 idle timer 超时 kill / 外部终止)。
157
- // close handler 的 cleanup 仍执行(resolve(code) 是 no-op,Promise 已 resolve)。
158
- child.stdout.end();
159
- child.stderr.end();
160
- child.emit("close", 0);
161
- });
162
-
163
- it("非 chatMode runSpawn 仍在 close resolve(agent_settled 不触发 resolve)", async () => {
164
- // makeRecord 默认非 chatMode
165
- const record = makeRecord("sa-nonchat-resolve");
166
- const promise = runSpawn(record, "Task: nonchat", makeOpts(), makeCtx());
167
-
168
- await waitForSpawn();
169
- const child = lastSpawnedChild();
170
-
171
- emitStdoutLine(child, sessionHeader());
172
- emitStdoutLine(child, { type: "agent_end", willRetry: false });
173
- // 非 chatMode agent_end → count=0 → SIGTERM kill
174
- await new Promise((r) => setTimeout(r, 10));
175
- expect(child.killed).toBe(true);
176
-
177
- // agent_settled 在非 chatMode 下被忽略(handler 直接 return,不调 resolveRun)
178
- emitStdoutLine(child, { type: "agent_settled" });
179
- await new Promise((r) => setTimeout(r, 20));
180
-
181
- // [回归防护] agent_settled 不 resolve 非 chatMode runSpawn——promise 仍 pending(等 close)。
182
- expect(await settlesWithin(promise, 200)).toBe(false);
183
-
184
- // emit close 才 resolve(现有行为,改动 1 不影响非 chatMode)
185
- child.stdout.end();
186
- child.stderr.end();
187
- child.emit("close", 143);
188
- const result = await promise;
189
- expect(result.success).toBe(true); // 信号终止(143 ≥ threshold)视为正常完成
190
- });
191
- });