@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,387 +0,0 @@
1
- // src/execution/__tests__/run-and-finalize-anchoring.test.ts
2
- //
3
- // runAndFinalize 特征锚定基线(第二批复杂度债务 U04:锚定测试先行)。
4
- //
5
- // 既有 run-and-finalize-chatmode.test.ts 覆盖 settle 阶段的 chatMode × closeAfterRound
6
- // 分流;本文件补直接分支缺口,重构前后行为逐字节对照(错误文案 / 终态写盘 / 事件顺序):
7
- // G1 pooled acquire 成功 → acquire → runSpawn → release 调用次序 + 轮次回退事件(running)
8
- // G2 排队 acquire 被中止(signal.aborted)→ finalizeAborted("cancelled by user")
9
- // G3 acquire 非中止失败 → finalizeFailed 合成 "aborted" 文案(原始错误被吞——现状怪癖锚定)
10
- // G4 非 chatMode + runSpawn 抛错 → finalizeFailed:closed/gc + archive + manifest 落盘
11
- // G5 chatMode + runSpawn 抛错 → MF-6 回退:record.result="round did not complete: <msg>"
12
- // G6 chatMode + 抛错 + CAS 输(record 已 closed)→ 不回退、不 archive、原样返回合成 result
13
- // G7 chatMode + idle timer armed → early return:settle 不执行(round/closeAfterRound 不动)
14
- // G8 settle 阶段 CAS 输(record 已 closed)→ 跳过收尾、closeAfterRound 不消费
15
- // G9 非 chatMode + 失败 result → else 分支:closed/gc + archive + manifest 落盘 + endedAt
16
- // G10 运行中 abort → closedReason="cancelled" 终态化,result 原样返回(不改写为 cancelled 形态)
17
- //
18
- // harness 与 run-and-finalize-chatmode.test.ts 同形态:mock session-runner.runSpawn,
19
- // 走 SubagentService 真实 runAndFinalize 分流逻辑,断言「最终可观察状态」。
20
-
21
- import * as fs from "node:fs";
22
- import * as os from "node:os";
23
- import * as path from "node:path";
24
-
25
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
26
-
27
- // mock logger(doFinalizeRecord 的 manifest 写入降级路径用 logger.error)
28
- const { loggerMock } = vi.hoisted(() => ({
29
- loggerMock: { debug: vi.fn(), error: vi.fn(), warn: vi.fn() },
30
- }));
31
- vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
32
-
33
- // mock session-runner:runSpawn 受控返回 result / 抛错,killAllSpawnedChildren 空实现。
34
- // 注意:必须在 import SubagentService 之前 mock(vi.mock 提升到顶部)。
35
- vi.mock("../engine/engines/pi/session-runner.ts", () => ({
36
- runSpawn: vi.fn(),
37
- killAllSpawnedChildren: vi.fn(),
38
- getChildByRecord: vi.fn(() => undefined),
39
- registerSpawnedChildForRecord: vi.fn(),
40
- killRecordChildWithEscalation: vi.fn(),
41
- spawnedChildren: new Map(),
42
- }));
43
-
44
- import { runSpawn } from "../engine/engines/pi/session-runner.ts";
45
- import type { SessionRunnerContext } from "../engine/engines/pi/session-runner.ts";
46
- import { armIdleTimer, disarmIdleTimer } from "../lifecycle-manager.ts";
47
- import { createRecord } from "../execution-record.ts";
48
- import { ModelConfigService } from "../model-config-service.ts";
49
- import type { ModelInfo } from "../model-resolver.ts";
50
- import { getSubagentRecordsDir } from "../path-encoding.ts";
51
- import { RecordStore } from "../record-store.ts";
52
- import type { ConcurrencyPool } from "../concurrency-pool.ts";
53
- import { SubagentService } from "../subagent-service.ts";
54
- import type { PiLike } from "../subagent-service.ts";
55
- import type {
56
- AgentResult,
57
- ExecutionRecord,
58
- ExecuteOptions,
59
- } from "../types.ts";
60
-
61
- const mockRunSpawn = vi.mocked(runSpawn);
62
-
63
- // ── 最小合法 ModelInfo(resolveModel 未注入 registry 会抛错,故 stub;runSpawn 被 mock 不消费)──
64
- const STUB_MODEL: ModelInfo = {
65
- id: "test-model",
66
- name: "Test",
67
- provider: "test",
68
- reasoning: false,
69
- };
70
-
71
- function makeTmpAgentDir(): string {
72
- return fs.mkdtempSync(path.join(os.tmpdir(), "raf-anchor-test-"));
73
- }
74
-
75
- function makePi(): PiLike & {
76
- appendEntry: ReturnType<typeof vi.fn<(customType: string, data?: unknown) => void>>;
77
- events: { emit: ReturnType<typeof vi.fn<(channel: string, data: unknown) => void>> };
78
- sendMessage: ReturnType<typeof vi.fn<(message: Parameters<PiLike["sendMessage"]>[0], options?: Parameters<PiLike["sendMessage"]>[1]) => void>>;
79
- } {
80
- return {
81
- appendEntry: vi.fn((customType: string, data?: unknown) => {}),
82
- events: { emit: vi.fn((channel: string, data: unknown) => {}) },
83
- sendMessage: vi.fn(() => {}),
84
- };
85
- }
86
-
87
- function makeResult(success: boolean): AgentResult {
88
- return {
89
- text: success ? "done" : "err",
90
- turns: 1,
91
- durationMs: 100,
92
- success,
93
- error: success ? undefined : "boom",
94
- sessionId: "sess-1",
95
- toolCalls: [],
96
- };
97
- }
98
-
99
- function makeRecord(chatMode: boolean, id: string): ExecutionRecord {
100
- return createRecord(id, {
101
- agent: "general-purpose",
102
- model: "test-model",
103
- mode: "background",
104
- task: "do something",
105
- slug: "test",
106
- startedAt: 1000,
107
- rootSessionId: "root-session",
108
- chatMode,
109
- });
110
- }
111
-
112
- /** 暴露 runAndFinalize + store + pool 的私有访问接口(测试专用 cast)。 */
113
- interface ServiceInternals {
114
- store: RecordStore;
115
- pool: ConcurrencyPool;
116
- runAndFinalize: (
117
- record: ExecutionRecord,
118
- opts: ExecuteOptions,
119
- ctx: SessionRunnerContext,
120
- identity: {
121
- agent: string;
122
- agentConfig: unknown;
123
- resolved: { model: ModelInfo; thinkingLevel: string | undefined };
124
- },
125
- signal: AbortSignal | undefined,
126
- priority: number,
127
- ) => Promise<AgentResult>;
128
- }
129
-
130
- describe("runAndFinalize 特征锚定基线(U04 重构前后行为对照)", () => {
131
- let agentDir: string;
132
- let modelService: ModelConfigService;
133
- let service: SubagentService;
134
- let internals: ServiceInternals;
135
- let pi: ReturnType<typeof makePi>;
136
- let armedTimerRecordIds: string[];
137
-
138
- beforeEach(() => {
139
- // rootCwd 推导基线:PI_SUBAGENT_ROOT_CWD 若被宿主 env 污染会改写 recordsDir 编码段,
140
- // manifest 落盘断言(G4/G9)依赖「rootCwd = init.cwd」的未设基线。
141
- delete process.env.PI_SUBAGENT_ROOT_CWD;
142
- agentDir = makeTmpAgentDir();
143
- modelService = new ModelConfigService({ agentDir, cwd: agentDir });
144
- service = new SubagentService({ cwd: agentDir, modelService });
145
- pi = makePi();
146
- service.initSession({ pi, sessionId: "root-session" });
147
- internals = service as unknown as ServiceInternals;
148
- armedTimerRecordIds = [];
149
- mockRunSpawn.mockReset();
150
- });
151
-
152
- afterEach(() => {
153
- for (const id of armedTimerRecordIds) disarmIdleTimer(id);
154
- service.dispose();
155
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
156
- });
157
-
158
- /** 直接调私有 runAndFinalize(mock runSpawn 后走完整分流逻辑)。 */
159
- async function callRunAndFinalize(
160
- record: ExecutionRecord,
161
- mockImpl: () => void,
162
- signal?: AbortSignal,
163
- ): Promise<AgentResult> {
164
- mockImpl();
165
- const opts: ExecuteOptions = { task: "do something", slug: "test" };
166
- const ctx: SessionRunnerContext = {
167
- cwd: agentDir,
168
- agentDir,
169
- skillDirs: [],
170
- mainCwd: agentDir,
171
- sessionRootId: "s-root",
172
- rootCwd: agentDir,
173
- };
174
- const identity = {
175
- agent: "general-purpose",
176
- agentConfig: undefined,
177
- resolved: { model: STUB_MODEL, thinkingLevel: undefined },
178
- };
179
- return internals.runAndFinalize(record, opts, ctx, identity, signal, 0);
180
- }
181
-
182
- function manifestPath(recordId: string): string {
183
- return path.join(getSubagentRecordsDir(agentDir, agentDir), `${recordId}.json`);
184
- }
185
-
186
- it("G1: pooled acquire 成功 → acquire → runSpawn → release 次序锚定 + 轮次回退事件(running)", async () => {
187
- const record = makeRecord(false, "sa-anchor-g1");
188
- internals.store.register(record);
189
- const pool = internals.pool;
190
- const acquireSpy = vi.spyOn(pool, "acquire");
191
- const releaseSpy = vi.spyOn(pool, "release");
192
-
193
- const result = await callRunAndFinalize(record, () => mockRunSpawn.mockResolvedValueOnce(makeResult(true)));
194
-
195
- expect(result.text).toBe("done");
196
- // 事件顺序:acquire 先于 runSpawn,release(finally 收口)最后。
197
- expect(acquireSpy).toHaveBeenCalledTimes(1);
198
- expect(mockRunSpawn).toHaveBeenCalledTimes(1);
199
- expect(releaseSpy).toHaveBeenCalledTimes(1);
200
- const order = acquireSpy.mock.invocationCallOrder[0];
201
- const spawnOrder = mockRunSpawn.mock.invocationCallOrder[0];
202
- const releaseOrder = releaseSpy.mock.invocationCallOrder[0];
203
- expect(order).toBeLessThan(spawnOrder);
204
- expect(spawnOrder).toBeLessThan(releaseOrder);
205
- // one-shot 成功无 closeAfterRound → finalizeRoundToIdle:留内存 + 进程死注销事件(running)。
206
- expect(record.status).toBe("running");
207
- expect(internals.store.getMutable(record.id)).toBe(record);
208
- expect(pi.events.emit).toHaveBeenCalledWith("pending:unregister", { id: record.id, reason: "running" });
209
- });
210
-
211
- it("G2: 排队 acquire 被中止(signal.aborted)→ finalizeAborted:'cancelled by user' + closed/cancelled + archive", async () => {
212
- const record = makeRecord(false, "sa-anchor-g2");
213
- internals.store.register(record);
214
- const releaseSpy = vi.spyOn(internals.pool, "release");
215
- const controller = new AbortController();
216
- controller.abort(); // pre-aborted:signal.aborted = true
217
- // 真实 ConcurrencyPool.acquire 对 pre-aborted signal 并不保证 reject——
218
- // 显式 mock 排队中止(acquire reject + signal.aborted)以触发 catch 的 S1 分支。
219
- vi.spyOn(internals.pool, "acquire").mockRejectedValueOnce(new Error("AbortError: queued acquire aborted"));
220
-
221
- const result = await callRunAndFinalize(record, () => mockRunSpawn.mockResolvedValueOnce(makeResult(true)), controller.signal);
222
-
223
- // 错误路径文案逐字节:S1 对齐已运行被 abort 的 cancel 语义。
224
- expect(result.error).toBe("cancelled by user");
225
- expect(result.success).toBe(false);
226
- // 终态:closed + cancelled + archive(写盘收尾走完),runSpawn 未触达。
227
- expect(record.status).toBe("closed");
228
- expect(record.closedReason).toBe("cancelled");
229
- expect(record.endedAt).toBeDefined();
230
- expect(internals.store.getMutable(record.id)).toBeUndefined();
231
- expect(mockRunSpawn).not.toHaveBeenCalled();
232
- // acquire 失败早退(try/finally 之前)→ 池槽不归还(acquired=false 语义)。
233
- expect(releaseSpy).not.toHaveBeenCalled();
234
- expect(pi.events.emit).toHaveBeenCalledWith("pending:unregister", { id: record.id, reason: "closed" });
235
- });
236
-
237
- it("G3: acquire 非中止失败 → finalizeFailed 合成 'aborted' 文案(原始错误被吞——现状怪癖锚定)", async () => {
238
- const record = makeRecord(false, "sa-anchor-g3");
239
- internals.store.register(record);
240
- const releaseSpy = vi.spyOn(internals.pool, "release");
241
- vi.spyOn(internals.pool, "acquire").mockRejectedValueOnce(new Error("pool exploded"));
242
-
243
- const result = await callRunAndFinalize(record, () => mockRunSpawn.mockResolvedValueOnce(makeResult(true)));
244
-
245
- // 现状:catch 吞掉原始错误,统一合成 new Error("aborted") → toErrorMessage = "aborted"。
246
- // "pool exploded" 不透传——重构必须保持该文案(行为逐字节不变)。
247
- expect(result.error).toBe("aborted");
248
- expect(result.success).toBe(false);
249
- expect(record.status).toBe("closed");
250
- expect(record.closedReason).toBe("gc");
251
- expect(internals.store.getMutable(record.id)).toBeUndefined();
252
- expect(mockRunSpawn).not.toHaveBeenCalled();
253
- expect(releaseSpy).not.toHaveBeenCalled();
254
- });
255
-
256
- it("G4: 非 chatMode + runSpawn 抛错 → finalizeFailed:error 透传 + closed/gc + archive + manifest 落盘", async () => {
257
- const record = makeRecord(false, "sa-anchor-g4");
258
- internals.store.register(record);
259
-
260
- const result = await callRunAndFinalize(record, () => mockRunSpawn.mockRejectedValueOnce(new Error("spawn exploded")));
261
-
262
- // 错误文案:toErrorMessage(Error) → .message 逐字节透传。
263
- expect(result.error).toBe("spawn exploded");
264
- expect(result.success).toBe(false);
265
- // 终态写盘:closed + gc + archive + manifest json 落盘。
266
- expect(record.status).toBe("closed");
267
- expect(record.closedReason).toBe("gc");
268
- expect(record.endedAt).toBeDefined();
269
- expect(internals.store.getMutable(record.id)).toBeUndefined();
270
- expect(fs.existsSync(manifestPath(record.id))).toBe(true);
271
- expect(pi.events.emit).toHaveBeenCalledWith("pending:unregister", { id: record.id, reason: "closed" });
272
- });
273
-
274
- it("G5: chatMode + runSpawn 抛错 → MF-6 回退:'round did not complete: <msg>' + running + round+1 + 留内存", async () => {
275
- const record = makeRecord(true, "sa-anchor-g5");
276
- internals.store.register(record);
277
-
278
- const result = await callRunAndFinalize(record, () => mockRunSpawn.mockRejectedValueOnce(new Error("spawn creation failed")));
279
-
280
- // 合成 failed result 错误文案透传(swallow,不 re-throw)。
281
- expect(result.error).toBe("spawn creation failed");
282
- // MF-6:不销毁对话——finalizeRoundToIdle 回退 running-resumable(留内存可重试 message/close)。
283
- expect(record.status).toBe("running");
284
- expect(record.closedReason).toBeUndefined(); // [S10] 不残留 gc
285
- expect(record.round).toBe(1);
286
- expect(internals.store.getMutable(record.id)).toBe(record);
287
- // MF-2 写点:失败轮 error 优先 → record.result 固定前缀文案(notify 正文逐字节)。
288
- expect(record.result).toBe("round did not complete: spawn creation failed");
289
- // 事件:进程死注销 reason=running(非 closed——record 留内存非终态)。
290
- expect(pi.events.emit).toHaveBeenCalledWith("pending:unregister", { id: record.id, reason: "running" });
291
- });
292
-
293
- it("G6: chatMode + 抛错 + CAS 输(record 已 closed)→ 不回退不 archive,原样返回合成 result", async () => {
294
- const record = makeRecord(true, "sa-anchor-g6");
295
- record.status = "closed"; // cancel/收尾抢先:tryTransition(closed, gc) 必败
296
- internals.store.register(record);
297
-
298
- const result = await callRunAndFinalize(record, () => mockRunSpawn.mockRejectedValueOnce(new Error("spawn creation failed")));
299
-
300
- expect(result.error).toBe("spawn creation failed");
301
- // CAS 输:无任何收尾副作用——状态/轮次不动、留内存、无注销事件。
302
- expect(record.status).toBe("closed");
303
- expect(record.round).toBe(0);
304
- expect(internals.store.getMutable(record.id)).toBe(record);
305
- expect(pi.events.emit).not.toHaveBeenCalled();
306
- });
307
-
308
- it("G7: chatMode + idle timer armed → early return:settle 不执行(round/closeAfterRound 不动)", async () => {
309
- const record = makeRecord(true, "sa-anchor-g7");
310
- record.status = "running";
311
- record.round = 1;
312
- internals.store.register(record);
313
- // V2 决策 2/3 形态:onRoundSettled 已 arm idle timer(agent_settled 提前闭环)。
314
- armIdleTimer(record.id, () => {}, 60_000);
315
- armedTimerRecordIds.push(record.id);
316
-
317
- const result = await callRunAndFinalize(record, () => mockRunSpawn.mockResolvedValueOnce(makeResult(true)));
318
-
319
- // early return:原样返回 runSpawn result,settle 阶段未执行。
320
- expect(result.text).toBe("done");
321
- expect(record.status).toBe("running");
322
- expect(record.round).toBe(1); // 未 +1(round+1 归 onRoundSettled,非本函数)
323
- expect(record.closeAfterRound).toBeUndefined();
324
- expect(internals.store.getMutable(record.id)).toBe(record);
325
- expect(pi.events.emit).not.toHaveBeenCalled();
326
- });
327
-
328
- it("G8: settle 阶段 CAS 输(record 已 closed)→ 跳过收尾、closeAfterRound 不消费", async () => {
329
- const record = makeRecord(false, "sa-anchor-g8");
330
- record.status = "closed"; // cancel 抢先(A2-1 形态)
331
- record.closeAfterRound = true;
332
- internals.store.register(record);
333
-
334
- const result = await callRunAndFinalize(record, () => mockRunSpawn.mockResolvedValueOnce(makeResult(true)));
335
-
336
- // 原样返回(不改写为 cancelled 形态);收尾全跳过。
337
- expect(result.text).toBe("done");
338
- expect(result.success).toBe(true);
339
- expect(record.status).toBe("closed");
340
- expect(record.closeAfterRound).toBe(true); // 标志不被本路径消费
341
- expect(record.endedAt).toBeUndefined();
342
- expect(internals.store.getMutable(record.id)).toBe(record);
343
- expect(pi.events.emit).not.toHaveBeenCalled();
344
- });
345
-
346
- it("G9: 非 chatMode + 失败 result → else 分支:closed/gc + archive + manifest 落盘 + endedAt", async () => {
347
- const record = makeRecord(false, "sa-anchor-g9");
348
- internals.store.register(record);
349
-
350
- const result = await callRunAndFinalize(record, () => mockRunSpawn.mockResolvedValueOnce(makeResult(false)));
351
-
352
- // 失败 result 原样返回(错误文案不经本函数改写)。
353
- expect(result.success).toBe(false);
354
- expect(result.error).toBe("boom");
355
- // 非 chatMode 失败 = 一次性销毁(archive + manifest 落盘)。
356
- expect(record.status).toBe("closed");
357
- expect(record.closedReason).toBe("gc");
358
- expect(record.endedAt).toBeDefined();
359
- expect(internals.store.getMutable(record.id)).toBeUndefined();
360
- expect(fs.existsSync(manifestPath(record.id))).toBe(true);
361
- expect(pi.events.emit).toHaveBeenCalledWith("pending:unregister", { id: record.id, reason: "closed" });
362
- });
363
-
364
- it("G10: 运行中 abort → closedReason='cancelled' 终态化,result 原样返回(不改写)", async () => {
365
- const record = makeRecord(false, "sa-anchor-g10");
366
- internals.store.register(record);
367
- const controller = new AbortController();
368
- // acquire(signal 未中止 → 放行)后、runSpawn resolve 前中止。
369
- mockRunSpawn.mockImplementationOnce(async () => {
370
- await new Promise((r) => { setTimeout(r, 15); });
371
- return makeResult(true);
372
- });
373
- setTimeout(() => controller.abort(), 5);
374
-
375
- const result = await callRunAndFinalize(record, () => {}, controller.signal);
376
-
377
- // result 原样返回(success true 不被改写为 cancelled 形态——现状锚定)。
378
- expect(result.success).toBe(true);
379
- expect(result.text).toBe("done");
380
- // 终态:closed + cancelled(abort 折入 closed)+ archive。
381
- expect(record.status).toBe("closed");
382
- expect(record.closedReason).toBe("cancelled");
383
- expect(record.endedAt).toBeDefined();
384
- expect(internals.store.getMutable(record.id)).toBeUndefined();
385
- expect(pi.events.emit).toHaveBeenCalledWith("pending:unregister", { id: record.id, reason: "closed" });
386
- });
387
- });
@@ -1,277 +0,0 @@
1
- // src/execution/__tests__/run-and-finalize-chatmode.test.ts
2
- //
3
- // runAndFinalize 的 chatMode idle 分流测试(M2-A)。
4
- //
5
- // mock session-runner.runSpawn(受控返回 success/failed result),走 SubagentService
6
- // 真实的 runAndFinalize 分流逻辑,验证:
7
- // - chatMode + done → doFinalizeRoundToIdle:record.status=idle、留内存、round+1
8
- // - chatMode + failed → MF-6:回退 idle(可恢复),不销毁(agent 可重试 message 或 close)
9
- // - 非 chatMode + done → doFinalizeRecord:record.status=done、archived(现有行为不变)
10
- //
11
- // 分流效果通过「最终可观察状态」验证(record.status + store.getMutable 是否在内存),
12
- // 不依赖 spy 私有方法调用,更稳健。
13
-
14
- import * as fs from "node:fs";
15
- import * as os from "node:os";
16
- import * as path from "node:path";
17
-
18
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19
-
20
- // mock logger(doFinalizeRecord 的 manifest 写入降级路径用 logger.error)
21
- const { loggerMock } = vi.hoisted(() => ({
22
- loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
23
- }));
24
- vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
25
-
26
- // mock session-runner:runSpawn 受控返回 result,killAllSpawnedChildren 空实现。
27
- // 注意:必须在 import SubagentService 之前 mock(vi.mock 提升到顶部)。
28
- vi.mock("../engine/engines/pi/session-runner.ts", () => ({
29
- runSpawn: vi.fn(),
30
- killAllSpawnedChildren: vi.fn(),
31
- getChildByRecord: vi.fn(() => undefined),
32
- registerSpawnedChildForRecord: vi.fn(),
33
- killRecordChildWithEscalation: vi.fn(),
34
- spawnedChildren: new Map(),
35
- }));
36
-
37
- import { runSpawn } from "../engine/engines/pi/session-runner.ts";
38
- import type { SessionRunnerContext } from "../engine/engines/pi/session-runner.ts";
39
- import { createRecord } from "../execution-record.ts";
40
- import { ModelConfigService } from "../model-config-service.ts";
41
- import type { ModelInfo } from "../model-resolver.ts";
42
- import { RecordStore, type StatusFilter } from "../record-store.ts";
43
- import { SubagentService } from "../subagent-service.ts";
44
- import type { PiLike } from "../subagent-service.ts";
45
- import type {
46
- AgentResult,
47
- ExecutionRecord,
48
- ExecuteOptions,
49
- } from "../types.ts";
50
-
51
- const mockRunSpawn = vi.mocked(runSpawn);
52
-
53
- // ── 最小合法 ModelInfo(resolveModel 未注入 registry 会抛错,故 stub;runSpawn 被 mock 不消费)──
54
- const STUB_MODEL: ModelInfo = {
55
- id: "test-model",
56
- name: "Test",
57
- provider: "test",
58
- reasoning: false,
59
- };
60
-
61
- function makeTmpAgentDir(): string {
62
- return fs.mkdtempSync(path.join(os.tmpdir(), "chatmode-test-"));
63
- }
64
-
65
- function makePi(): PiLike & {
66
- appendEntry: ReturnType<typeof vi.fn<(customType: string, data?: unknown) => void>>;
67
- events: { emit: ReturnType<typeof vi.fn<(channel: string, data: unknown) => void>> };
68
- sendMessage: ReturnType<typeof vi.fn<(message: Parameters<PiLike["sendMessage"]>[0], options?: Parameters<PiLike["sendMessage"]>[1]) => void>>;
69
- } {
70
- return {
71
- appendEntry: vi.fn((customType: string, data?: unknown) => {}),
72
- events: { emit: vi.fn((channel: string, data: unknown) => {}) },
73
- sendMessage: vi.fn(() => {}),
74
- };
75
- }
76
-
77
- function makeResult(success: boolean): AgentResult {
78
- return {
79
- text: success ? "done" : "err",
80
- turns: 1,
81
- durationMs: 100,
82
- success,
83
- error: success ? undefined : "boom",
84
- sessionId: "sess-1",
85
- toolCalls: [],
86
- };
87
- }
88
-
89
- function makeRecord(chatMode: boolean, id = "sa-test"): ExecutionRecord {
90
- return createRecord(id, {
91
- agent: "general-purpose",
92
- model: "test-model",
93
- mode: "background",
94
- task: "do something",
95
- slug: "test",
96
- startedAt: 1000,
97
- rootSessionId: "root-session",
98
- chatMode,
99
- });
100
- }
101
-
102
- /** 暴露 runAndFinalize + store 的私有访问接口(测试专用 cast)。 */
103
- interface ServiceInternals {
104
- store: RecordStore;
105
- runAndFinalize: (
106
- record: ExecutionRecord,
107
- opts: ExecuteOptions,
108
- ctx: SessionRunnerContext,
109
- identity: {
110
- agent: string;
111
- agentConfig: unknown;
112
- resolved: { model: ModelInfo; thinkingLevel: string | undefined };
113
- },
114
- signal: undefined,
115
- priority: number,
116
- ) => Promise<AgentResult>;
117
- }
118
-
119
- describe("runAndFinalize chatMode idle 分流 (M2-A)", () => {
120
- let agentDir: string;
121
- let modelService: ModelConfigService;
122
- let service: SubagentService;
123
- let internals: ServiceInternals;
124
-
125
- beforeEach(() => {
126
- agentDir = makeTmpAgentDir();
127
- modelService = new ModelConfigService({ agentDir, cwd: agentDir });
128
- service = new SubagentService({ cwd: agentDir, modelService });
129
- service.initSession({ pi: makePi(), sessionId: "root-session" });
130
- internals = service as unknown as ServiceInternals;
131
- mockRunSpawn.mockReset();
132
- });
133
-
134
- afterEach(() => {
135
- service.dispose();
136
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
137
- });
138
-
139
- /** 直接调私有 runAndFinalize(mock runSpawn 后走完整分流逻辑)。 */
140
- async function callRunAndFinalize(record: ExecutionRecord, success: boolean): Promise<void> {
141
- mockRunSpawn.mockResolvedValueOnce(makeResult(success));
142
- const opts: ExecuteOptions = { task: "do something", slug: "test" };
143
- const ctx: SessionRunnerContext = {
144
- cwd: agentDir,
145
- agentDir,
146
- skillDirs: [],
147
- mainCwd: agentDir,
148
- sessionRootId: "s-root",
149
- rootCwd: agentDir,
150
- };
151
- const identity = {
152
- agent: "general-purpose",
153
- agentConfig: undefined,
154
- resolved: { model: STUB_MODEL, thinkingLevel: undefined },
155
- };
156
- await internals.runAndFinalize(record, opts, ctx, identity, undefined, 0);
157
- }
158
-
159
- it("chatMode + done → record running(旧 idle 折入),留内存,round 0→1", async () => {
160
- const record = makeRecord(true);
161
- internals.store.register(record);
162
- await callRunAndFinalize(record, true);
163
-
164
- expect(record.status).toBe("running");
165
- expect(record.round).toBe(1);
166
- // record 留内存(未 archive)——idle 分流核心断言(v4 B-1:idle 折入 running)
167
- expect(internals.store.getMutable(record.id)).toBe(record);
168
- });
169
-
170
- it("chatMode + failed → MF-6:回退 running(可恢复),不销毁对话(agent 可重试 message 或 close)", async () => {
171
- const record = makeRecord(true);
172
- internals.store.register(record);
173
- await callRunAndFinalize(record, false);
174
-
175
- // MF-6:chatMode 失败不终态销毁——record 回退 running(留内存),agent 可重试 message 或 close。
176
- expect(record.status).toBe("running");
177
- expect(internals.store.getMutable(record.id)).toBe(record); // 留内存(未 archive)
178
- // record.result 被 finalizeRoundToIdle 设值(MF-2:否则 notifier idle 恒 (empty))。
179
- // makeResult(false) 返回 text:"err" + error:"boom";text 非空 → record.result="err"。
180
- expect(record.result).toBeTruthy();
181
- });
182
-
183
- it("非 chatMode + done → record running(SP-5: 一次性完成后保持活跃,等待 message 触发升级)", async () => {
184
- const record = makeRecord(false);
185
- internals.store.register(record);
186
- await callRunAndFinalize(record, true);
187
-
188
- // SP-5: one-shot 完成后 record 保持 running(旧 idle,非终态归档),用户可通过 message 续聊。
189
- expect(record.status).toBe("running");
190
- expect(internals.store.getMutable(record.id)).toBe(record); // 留内存
191
- });
192
-
193
- it("chatMode 第二轮 done → round 累加(1→2,续聊场景)", async () => {
194
- // 模拟 resume 续聊:第一轮已完成(round=1,旧 idle 折入 running),第二轮 runAndFinalize 再次 done
195
- const record = makeRecord(true);
196
- record.status = "running"; // 第一轮已完成(v4 B-1:旧 idle 折入 running)
197
- record.round = 1;
198
- internals.store.register(record);
199
- await callRunAndFinalize(record, true);
200
-
201
- // 续聊第二轮 done:tryTransition(running→closed) 成功 → chatMode 分流 → finalizeRoundToIdle
202
- // → status 回到 running、round 1→2(record 保持活跃,等待下一轮续聊)。
203
- expect(record.status).toBe("running");
204
- expect(record.round).toBe(2);
205
- expect(mockRunSpawn).toHaveBeenCalled();
206
- });
207
-
208
- it("chatMode + done + closeAfterRound=true → finalizeRecord(done)(不进 idle,M2-B3)", async () => {
209
- // close 优雅关闭(force:false):running 时置 closeAfterRound=true,
210
- // runAndFinalize done 分流终态化为 done(而非进 idle)。
211
- const record = makeRecord(true);
212
- record.status = "running";
213
- record.closeAfterRound = true;
214
- internals.store.register(record);
215
- await callRunAndFinalize(record, true);
216
-
217
- expect(record.status).toBe("closed");
218
- expect(record.closeAfterRound).toBeUndefined(); // 清标志
219
- // 终态化 → archive → 内存无(与 idle 分流「留内存」对比)
220
- expect(internals.store.getMutable(record.id)).toBeUndefined();
221
- });
222
-
223
- it("chatMode + done + 无 closeAfterRound → 进 running(旧 idle 折入,M2-A 行为不变)", async () => {
224
- const record = makeRecord(true);
225
- record.status = "running";
226
- internals.store.register(record);
227
- await callRunAndFinalize(record, true);
228
-
229
- expect(record.status).toBe("running");
230
- expect(record.round).toBe(1);
231
- expect(internals.store.getMutable(record.id)).toBe(record);
232
- });
233
-
234
- it("[M5] 非 chatMode(one-shot)+ done + closeAfterRound=true → 轮完成时消费标志终态化(不进 running-resumable)", async () => {
235
- // one-shot busy 时 close(force:false) 置 closeAfterRound——旧代码非 chatMode 成功分支走
236
- // finalizeRoundToIdle 不消费,tool 返回 {closed:true} 谎报:record 永久 running-resumable、
237
- // 5min idle timer 杀进程、期间还能继续收 message。修复后在本分支消费终态化。
238
- const record = makeRecord(false);
239
- record.status = "running";
240
- record.closeAfterRound = true;
241
- internals.store.register(record);
242
- await callRunAndFinalize(record, true);
243
-
244
- expect(record.status).toBe("closed");
245
- expect(record.closedReason).toBe("user-close");
246
- expect(record.closeAfterRound).toBeUndefined(); // 标志已消费
247
- expect(internals.store.getMutable(record.id)).toBeUndefined(); // archived
248
- });
249
-
250
- it("[M5] chatMode + failed + closeAfterRound=true → 轮完成时消费标志终态化(不回退 resumable)", async () => {
251
- // 失败轮也视为「轮已完成」:close 意图兑现终态化(closedReason=gc),否则标志残留、
252
- // record 被 tool 谎报 closed 后仍可继续收 message。
253
- const record = makeRecord(true);
254
- record.status = "running";
255
- record.closeAfterRound = true;
256
- internals.store.register(record);
257
- await callRunAndFinalize(record, false); // 失败轮
258
-
259
- expect(record.status).toBe("closed");
260
- expect(record.closedReason).toBe("gc"); // !success → gc
261
- expect(record.closeAfterRound).toBeUndefined();
262
- expect(internals.store.getMutable(record.id)).toBeUndefined();
263
- });
264
-
265
- it("[S10] chatMode + failed(无 closeAfterRound)→ 回退 running 后 closedReason 不残留", async () => {
266
- // tryTransition 先设 closed+gc,finalizeRoundToIdle 回退 running——旧代码 closedReason="gc"
267
- // 残留在 running record 上(泄漏进 notify 载荷与 list 投影)。
268
- const record = makeRecord(true);
269
- record.status = "running";
270
- internals.store.register(record);
271
- await callRunAndFinalize(record, false);
272
-
273
- expect(record.status).toBe("running"); // MF-6 回退 resumable
274
- expect(record.closedReason).toBeUndefined(); // [S10] 不残留
275
- expect(internals.store.getMutable(record.id)).toBe(record);
276
- });
277
- });