@zhushanwen/subagent-core 0.2.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 (330) hide show
  1. package/README.md +114 -0
  2. package/dist/chunk-3VOERJPJ.js +22 -0
  3. package/dist/chunk-A4IVZWVX.js +31 -0
  4. package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
  5. package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
  6. package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
  7. package/dist/execution/engine/engines/zcode/constants.js +22 -0
  8. package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
  9. package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
  10. package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
  11. package/dist/execution/engine/engines/zcode/reader.js +239 -0
  12. package/dist/execution/engine/paths.cjs +55 -0
  13. package/dist/execution/engine/paths.d.cts +8 -0
  14. package/dist/execution/engine/paths.d.ts +8 -0
  15. package/dist/execution/engine/paths.js +26 -0
  16. package/dist/execution/relay-env.cjs +62 -0
  17. package/dist/execution/relay-env.d.cts +33 -0
  18. package/dist/execution/relay-env.d.ts +33 -0
  19. package/dist/execution/relay-env.js +20 -0
  20. package/dist/index.cjs +1852 -0
  21. package/dist/index.d.cts +1184 -0
  22. package/dist/index.d.ts +1184 -0
  23. package/dist/index.js +1810 -0
  24. package/dist/types-BxyAidGf.d.cts +875 -0
  25. package/dist/types-BxyAidGf.d.ts +875 -0
  26. package/package.json +99 -0
  27. package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
  28. package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
  29. package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
  30. package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
  31. package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
  32. package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
  33. package/src/__tests__/m2-append-content-probe.test.ts +73 -0
  34. package/src/__tests__/manifest-store.test.ts +368 -0
  35. package/src/__tests__/record-store-cache.test.ts +335 -0
  36. package/src/__tests__/record-store-index.test.ts +560 -0
  37. package/src/__tests__/record-store-last-line.test.ts +129 -0
  38. package/src/__tests__/review-fix-loop-script.test.ts +288 -0
  39. package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
  40. package/src/__tests__/robustness-low-batch1.test.ts +286 -0
  41. package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
  42. package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
  43. package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
  44. package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
  45. package/src/__tests__/session-runner.test.ts +69 -0
  46. package/src/__tests__/smoke.test.ts +9 -0
  47. package/src/__tests__/stream-sink.test.ts +203 -0
  48. package/src/core/__tests__/host-services.test.ts +144 -0
  49. package/src/core/__tests__/logger.test.ts +78 -0
  50. package/src/core/__tests__/notify-ports.test.ts +104 -0
  51. package/src/core/host-services.ts +97 -0
  52. package/src/core/logger.ts +47 -0
  53. package/src/core/notify-ports.ts +126 -0
  54. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
  55. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  56. package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
  57. package/src/execution/__tests__/alive-store.test.ts +147 -0
  58. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
  59. package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
  60. package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
  61. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  62. package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
  63. package/src/execution/__tests__/config.test.ts +302 -0
  64. package/src/execution/__tests__/delivery-methods.test.ts +424 -0
  65. package/src/execution/__tests__/dialog-queue.test.ts +299 -0
  66. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  67. package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
  68. package/src/execution/__tests__/execute-nesting.test.ts +323 -0
  69. package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
  70. package/src/execution/__tests__/execution-record.test.ts +1394 -0
  71. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  72. package/src/execution/__tests__/finalize-record.test.ts +367 -0
  73. package/src/execution/__tests__/finalized-marker.test.ts +104 -0
  74. package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
  75. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  76. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  77. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  78. package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
  79. package/src/execution/__tests__/host-mode.test.ts +87 -0
  80. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  81. package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
  82. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  83. package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
  84. package/src/execution/__tests__/model-resolver.test.ts +465 -0
  85. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  86. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  87. package/src/execution/__tests__/output-collector.test.ts +358 -0
  88. package/src/execution/__tests__/path-encoding.test.ts +104 -0
  89. package/src/execution/__tests__/pi-invocation.test.ts +134 -0
  90. package/src/execution/__tests__/record-store.test.ts +1057 -0
  91. package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
  92. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
  93. package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
  94. package/src/execution/__tests__/relay-env.test.ts +42 -0
  95. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  96. package/src/execution/__tests__/rpc-mode.test.ts +89 -0
  97. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  98. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  99. package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
  100. package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
  101. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  102. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
  103. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  104. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  105. package/src/execution/__tests__/session-file-gc.test.ts +293 -0
  106. package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
  107. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  108. package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
  109. package/src/execution/__tests__/spawn-args.test.ts +445 -0
  110. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
  111. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  112. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
  113. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  114. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  115. package/src/execution/__tests__/stdin-writer.test.ts +462 -0
  116. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  117. package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
  118. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  119. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  120. package/src/execution/__tests__/subagent-service.test.ts +788 -0
  121. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  122. package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
  123. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  124. package/src/execution/__tests__/timeout-integration.test.ts +615 -0
  125. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  126. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  127. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  128. package/src/execution/__tests__/ui-channels.test.ts +187 -0
  129. package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
  130. package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
  131. package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
  132. package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
  133. package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
  134. package/src/execution/__tests__/worktree-manager.test.ts +633 -0
  135. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
  136. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  137. package/src/execution/__tests__/worktree-registry.test.ts +199 -0
  138. package/src/execution/agent-registry.ts +199 -0
  139. package/src/execution/agent-result-mapper.ts +90 -0
  140. package/src/execution/alive-store.ts +119 -0
  141. package/src/execution/argv-mirror.ts +113 -0
  142. package/src/execution/best-effort.ts +38 -0
  143. package/src/execution/channel-registry-access.ts +144 -0
  144. package/src/execution/concurrency-pool.ts +116 -0
  145. package/src/execution/config.ts +165 -0
  146. package/src/execution/dialog-queue.ts +330 -0
  147. package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
  148. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  149. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  150. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  151. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  152. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  153. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  154. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  155. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  156. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  157. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  158. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  159. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  160. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  161. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  162. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  163. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  164. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  165. package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
  166. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  167. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  168. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  169. package/src/execution/engine/common/data-dir.ts +66 -0
  170. package/src/execution/engine/common/errors.ts +183 -0
  171. package/src/execution/engine/common/event-journal.ts +254 -0
  172. package/src/execution/engine/common/journal-replay.ts +62 -0
  173. package/src/execution/engine/common/kill-chain.ts +221 -0
  174. package/src/execution/engine/common/nesting-guard.ts +50 -0
  175. package/src/execution/engine/common/persona-router.ts +108 -0
  176. package/src/execution/engine/common/pool-manager.ts +226 -0
  177. package/src/execution/engine/common/schema-emulation.ts +189 -0
  178. package/src/execution/engine/common/session-view-projection.ts +51 -0
  179. package/src/execution/engine/engine-discovery.ts +65 -0
  180. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  181. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  182. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  183. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  184. package/src/execution/engine/engines/pi/reader.ts +48 -0
  185. package/src/execution/engine/engines/pi/registration.ts +35 -0
  186. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  187. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  188. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  189. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  190. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  191. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  192. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
  193. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  194. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
  195. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  196. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  197. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  198. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  199. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  200. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  201. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  202. package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
  203. package/src/execution/engine/host-task-spec.ts +47 -0
  204. package/src/execution/engine/model-prompt.ts +158 -0
  205. package/src/execution/engine/paths.ts +42 -0
  206. package/src/execution/engine/port.ts +153 -0
  207. package/src/execution/engine/registry.ts +133 -0
  208. package/src/execution/engine/routing.ts +218 -0
  209. package/src/execution/engine/types.ts +309 -0
  210. package/src/execution/execute-options-mapper.ts +115 -0
  211. package/src/execution/execution-record.ts +984 -0
  212. package/src/execution/finalize-record.ts +254 -0
  213. package/src/execution/finalized-marker.ts +70 -0
  214. package/src/execution/get-state-handshake.ts +104 -0
  215. package/src/execution/host-mode.ts +56 -0
  216. package/src/execution/idle-gc.ts +47 -0
  217. package/src/execution/lifecycle-manager.ts +513 -0
  218. package/src/execution/lifecycle-predicates.ts +65 -0
  219. package/src/execution/manifest-store.ts +256 -0
  220. package/src/execution/model-config-service.ts +250 -0
  221. package/src/execution/model-resolver.ts +248 -0
  222. package/src/execution/notifier.ts +372 -0
  223. package/src/execution/notify-ledger.ts +580 -0
  224. package/src/execution/output-collector.ts +228 -0
  225. package/src/execution/path-encoding.ts +60 -0
  226. package/src/execution/pi-invocation.ts +120 -0
  227. package/src/execution/record-entry.ts +132 -0
  228. package/src/execution/record-store.ts +1265 -0
  229. package/src/execution/relay-env.ts +37 -0
  230. package/src/execution/session-context-resolver.ts +64 -0
  231. package/src/execution/session-file-gc.ts +120 -0
  232. package/src/execution/session-pending.ts +170 -0
  233. package/src/execution/session-reconstructor.ts +678 -0
  234. package/src/execution/session-runner.ts +1789 -0
  235. package/src/execution/sessions-index.ts +304 -0
  236. package/src/execution/spawn-event-adapter.ts +363 -0
  237. package/src/execution/stdin-writer.ts +198 -0
  238. package/src/execution/stream-sink.ts +126 -0
  239. package/src/execution/subagent-service.ts +2268 -0
  240. package/src/execution/subprocess-agent-runner.ts +317 -0
  241. package/src/execution/temp-prompt.ts +62 -0
  242. package/src/execution/tombstone-store.ts +72 -0
  243. package/src/execution/turn-limiter.ts +102 -0
  244. package/src/execution/types.ts +1023 -0
  245. package/src/execution/ui-channels.ts +216 -0
  246. package/src/execution/ui-interaction-model.ts +48 -0
  247. package/src/execution/ui-request-handler-factory.ts +243 -0
  248. package/src/execution/ui-request-observability.ts +81 -0
  249. package/src/execution/ui-request-queue.ts +184 -0
  250. package/src/execution/worktree-manager.ts +688 -0
  251. package/src/execution/worktree-registry.ts +279 -0
  252. package/src/index.ts +87 -0
  253. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
  254. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
  255. package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
  256. package/src/orchestration/__tests__/args-validator.test.ts +143 -0
  257. package/src/orchestration/__tests__/config-loader.test.ts +503 -0
  258. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
  259. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
  260. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
  261. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  262. package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
  263. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
  264. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  265. package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
  266. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  267. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
  268. package/src/orchestration/__tests__/script-lint.test.ts +831 -0
  269. package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
  270. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  271. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  272. package/src/orchestration/__tests__/worker-host.test.ts +120 -0
  273. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  274. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
  275. package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
  276. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
  277. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
  278. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  279. package/src/orchestration/agent-opts-resolver.ts +174 -0
  280. package/src/orchestration/args-validator.ts +127 -0
  281. package/src/orchestration/config-loader.ts +316 -0
  282. package/src/orchestration/error-recovery.ts +1018 -0
  283. package/src/orchestration/execute-agent-call.ts +256 -0
  284. package/src/orchestration/launcher.ts +455 -0
  285. package/src/orchestration/lifecycle.ts +399 -0
  286. package/src/orchestration/models/__tests__/budget.test.ts +307 -0
  287. package/src/orchestration/models/agent-call.ts +83 -0
  288. package/src/orchestration/models/budget.ts +118 -0
  289. package/src/orchestration/models/ports.ts +164 -0
  290. package/src/orchestration/models/run-runtime.ts +104 -0
  291. package/src/orchestration/models/run-spec.ts +83 -0
  292. package/src/orchestration/models/run-state.ts +44 -0
  293. package/src/orchestration/models/trace.ts +183 -0
  294. package/src/orchestration/models/types.ts +301 -0
  295. package/src/orchestration/models/workflow-run.ts +254 -0
  296. package/src/orchestration/models/workflow-script-registry.ts +35 -0
  297. package/src/orchestration/models/workflow-script.ts +117 -0
  298. package/src/orchestration/script-lint.ts +766 -0
  299. package/src/orchestration/skill-discovery.ts +135 -0
  300. package/src/orchestration/worker-handle.ts +115 -0
  301. package/src/orchestration/worker-host.ts +98 -0
  302. package/src/orchestration/worker-script-builder.ts +421 -0
  303. package/src/orchestration/workflow-files.ts +85 -0
  304. package/src/orchestration/workflow-script-registry-impl.ts +133 -0
  305. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  306. package/src/shared/__tests__/meta-parser.test.ts +304 -0
  307. package/src/shared/__tests__/model-ref.test.ts +306 -0
  308. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
  309. package/src/shared/__tests__/resource-discovery.test.ts +749 -0
  310. package/src/shared/__tests__/resource-meta.test.ts +51 -0
  311. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  312. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  313. package/src/shared/agent-event.ts +13 -0
  314. package/src/shared/agent-ref.ts +57 -0
  315. package/src/shared/meta-parser.ts +261 -0
  316. package/src/shared/model-ref.ts +286 -0
  317. package/src/shared/resource-discovery.ts +835 -0
  318. package/src/shared/resource-meta.ts +65 -0
  319. package/src/shared/schema-env.ts +44 -0
  320. package/src/shared/schema-jsonify.ts +58 -0
  321. package/src/shared/timer-delay.ts +54 -0
  322. package/src/shared/xml-injection.ts +35 -0
  323. package/workflows/README.md +81 -0
  324. package/workflows/_shared/agent-refs.cjs +40 -0
  325. package/workflows/chain.js +137 -0
  326. package/workflows/map-reduce.js +180 -0
  327. package/workflows/parallel.js +154 -0
  328. package/workflows/review-fix-loop-utils.cjs +1542 -0
  329. package/workflows/review-fix-loop.js +1605 -0
  330. package/workflows/scatter-gather.js +184 -0
@@ -0,0 +1,687 @@
1
+ // src/__tests__/run-spawn-edges.test.ts
2
+ //
3
+ // runSpawn 的 stdout 边界与 orphan 进程兜底集成测试(从 run-spawn-integration.test.ts 拆出)。
4
+ //
5
+ // 本文件覆盖:
6
+ // - [C1] orphan 进程兜底:killAllSpawnedChildren 对未退出 child 发 SIGTERM。
7
+ // - [M8] stdout 边界:损坏行(非法 JSON / 缺 type)静默忽略 + 残留尾行(close 前无 \n)
8
+ // 由 close handler 再 parse。
9
+ // - [agent_end] rpc 长驻进程自然完成:agent_end(willRetry=false)→ kill SIGTERM 触发 close。
10
+ //
11
+ // mock 工厂 + FakeChild + 工具函数共享自 helpers/spawn-mock.ts(详见该文件头注释)。
12
+ // vi.mock 必须各文件独立声明(文件作用域),工厂内用 `await import` 取回 FakeChild。
13
+
14
+ import { spawn } from "node:child_process";
15
+ import * as fs from "node:fs";
16
+
17
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
18
+
19
+ vi.mock("node:child_process", async () => {
20
+ const { FakeChild } = await import("./helpers/spawn-mock.ts");
21
+ return {
22
+ spawn: vi.fn(() => new FakeChild()),
23
+ // buildEnvBlock 的 git branch 调用(execFile 异步):默认 err-first 兜底 → catch → branch=""
24
+ execFile: vi.fn(
25
+ (
26
+ _cmd: string,
27
+ _args: readonly string[],
28
+ _opts: unknown,
29
+ cb: (err: Error | null, stdout?: string, stderr?: string) => void,
30
+ ) => cb(new Error("execFile not configured in this test")),
31
+ ),
32
+ };
33
+ });
34
+
35
+ vi.mock("node:fs", async () => {
36
+ const actual = await import("node:fs");
37
+ return {
38
+ default: {
39
+ ...actual,
40
+ mkdirSync: vi.fn(),
41
+ existsSync: vi.fn(() => false),
42
+ appendFileSync: vi.fn(),
43
+ writeFileSync: vi.fn(),
44
+ readdirSync: vi.fn(() => []),
45
+ },
46
+ mkdirSync: vi.fn(),
47
+ existsSync: vi.fn(() => false),
48
+ appendFileSync: vi.fn(),
49
+ writeFileSync: vi.fn(),
50
+ readdirSync: vi.fn(() => []),
51
+ promises: actual.promises,
52
+ };
53
+ });
54
+
55
+ vi.mock("../alive-store.ts", () => ({
56
+ writeAliveMarker: vi.fn(),
57
+ }));
58
+
59
+ // [recursive-orchestration] agent_end 后代判定独立 mock(判定函数本身在 session-pending.test.ts 单独测)。
60
+ vi.mock("../session-pending.ts", () => ({
61
+ readActivePendingFromSessionFile: vi.fn(() => ({ count: 0 })),
62
+ }));
63
+
64
+ vi.mock("../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 { killAllSpawnedChildren, runSpawn, spawnedChildren, WAKEUP_GRACE_MS, computeWatchdogMs, SPAWN_WATCHDOG_ENV } from "../session-runner.ts";
73
+ import { readActivePendingFromSessionFile } from "../session-pending.ts";
74
+ import {
75
+ emitStdoutLine,
76
+ type FakeChild,
77
+ lastSpawnedChild as lastSpawnedChildOf,
78
+ makeCtx,
79
+ makeOpts,
80
+ makeRecord,
81
+ sessionHeader,
82
+ waitForSpawn as waitForSpawnOf,
83
+ } from "./helpers/spawn-mock.ts";
84
+
85
+ const mockSpawn = vi.mocked(spawn);
86
+ const mockExistsSync = vi.mocked(fs.existsSync);
87
+
88
+ // 绑定到本文件 mockSpawn 的 lastSpawnedChild/waitForSpawn(需读 mockSpawn.mock.results)
89
+ const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
90
+ const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
91
+
92
+ // ============================================================
93
+ // 测试
94
+ // ============================================================
95
+
96
+ describe("runSpawn", () => {
97
+ beforeEach(() => {
98
+ vi.clearAllMocks();
99
+ // existsSync 默认 false(sessionFile 不存在兜底路径)
100
+ mockExistsSync.mockReturnValue(false);
101
+ // [F-4 假红源修复] env 隔离:makeOpts 默认 maxTurns=undefined →
102
+ // resolveSpawnWatchdogMs 走 env 兑底分支,宿主 export SPAWN_WATCHDOG 即假红。
103
+ // 空串 = 未设(raw falsy 判定);MF-4b 用例内的手动 save/delete/restore 与本
104
+ // stub 兼容(finally 恢复到 stub 值,语义不变)。
105
+ vi.stubEnv(SPAWN_WATCHDOG_ENV, "");
106
+ });
107
+
108
+ afterEach(() => {
109
+ vi.restoreAllMocks();
110
+ vi.unstubAllEnvs();
111
+ });
112
+
113
+ // ── 1. orphan 进程兜底(C1)──
114
+ //
115
+ // [C1] runSpawn 把每个 spawned child(sync + background)注册到模块级 spawnedChildren Set。
116
+ // SubagentService.dispose 调 killAllSpawnedChildren 遍历该 Set 对仍存活的子进程发 SIGTERM,
117
+ // 覆盖 sync 子进程(controller=undefined,abortRunningControllers 跳过它)。
118
+ //
119
+ // 关键验证:
120
+ // 1. child 退出(close/error)后从 Set 移除 → killAllSpawnedChildren 不重复 kill。
121
+ // 2. child 未退出时 killAllSpawnedChildren → child.kill("SIGTERM") 被调。
122
+ // 3. 已 kill 的 child 二次调用无害(killAllSpawnedChildren 跳过 killed=true 的)。
123
+ describe("orphan 进程兜底 (C1)", () => {
124
+ it("未退出的 child → killAllSpawnedChildren 对它发 SIGTERM", async () => {
125
+ const record = makeRecord();
126
+ // 不 await——runSpawn 内部 await 子进程 close,killAllSpawnedChildren 测试在 close 前
127
+ const promise = runSpawn(record, "Task: orphan", makeOpts(), makeCtx());
128
+
129
+ await waitForSpawn();
130
+ const child = lastSpawnedChild();
131
+
132
+ // spawn 后 child.killed 应为 false(尚未被 kill)
133
+ expect(child.killed).toBe(false);
134
+
135
+ // dispose 兜底:killAllSpawnedChildren 应 kill 未退出的 child
136
+ const n = killAllSpawnedChildren();
137
+ expect(n).toBeGreaterThanOrEqual(1);
138
+ expect(child.killed).toBe(true);
139
+ expect(child.killSignal).toBe("SIGTERM");
140
+
141
+ // 收尾:emit close 让 runSpawn resolve(避免悬挂)
142
+ emitStdoutLine(child, sessionHeader());
143
+ child.stdout.end();
144
+ child.emit("close", 143);
145
+
146
+ const result = await promise;
147
+ expect(result.success).toBe(true); // 信号终止视为正常完成
148
+ });
149
+
150
+ it("已 close 的 child → 从 Set 移除,killAllSpawnedChildren 不重复 kill", async () => {
151
+ const record = makeRecord();
152
+ const promise = runSpawn(record, "Task: closed", makeOpts(), makeCtx());
153
+
154
+ await waitForSpawn();
155
+ const child = lastSpawnedChild();
156
+
157
+ // 子进程正常退出
158
+ emitStdoutLine(child, sessionHeader());
159
+ child.stdout.end();
160
+ child.emit("close", 0);
161
+
162
+ await promise;
163
+ expect(child.killed).toBe(false); // 正常退出未触发 kill
164
+
165
+ // close 后 child 已从 Set 移除;再调 killAllSpawnedChildren 不会 kill 它
166
+ const n = killAllSpawnedChildren();
167
+ expect(n).toBe(0);
168
+ expect(child.killed).toBe(false);
169
+ });
170
+
171
+ it("多个未退出 child(sync + bg)→ killAllSpawnedChildren 全部 kill", async () => {
172
+ // spawn 两个 child(模拟 sync + background 并发),都未退出。
173
+ const rec1 = makeRecord("spawn-c1");
174
+ const p1 = runSpawn(rec1, "Task: c1", makeOpts(), makeCtx());
175
+ await waitForSpawn();
176
+ const c1 = lastSpawnedChild();
177
+
178
+ const rec2 = makeRecord("spawn-c2");
179
+ const p2 = runSpawn(rec2, "Task: c2", makeOpts(), makeCtx());
180
+ // waitForSpawn 是快照语义(记 baseline 等新 spawn),天然支持二次等待
181
+ await waitForSpawn();
182
+ const c2 = lastSpawnedChild();
183
+
184
+ // c1 和 c2 是不同实例
185
+ expect(c1).not.toBe(c2);
186
+ expect(c1.killed).toBe(false);
187
+ expect(c2.killed).toBe(false);
188
+
189
+ // dispose 兜底 kill 两个
190
+ const n = killAllSpawnedChildren();
191
+ expect(n).toBeGreaterThanOrEqual(2);
192
+ expect(c1.killed).toBe(true);
193
+ expect(c2.killed).toBe(true);
194
+
195
+ // 收尾
196
+ for (const { child, promise } of [
197
+ { child: c1, promise: p1 },
198
+ { child: c2, promise: p2 },
199
+ ]) {
200
+ emitStdoutLine(child, sessionHeader());
201
+ child.stdout.end();
202
+ child.emit("close", 143);
203
+ const r = await promise;
204
+ expect(r.success).toBe(true);
205
+ }
206
+ });
207
+
208
+ // [dispose-cleanup Minor 优化2] killAllSpawnedChildren 末尾 clear spawnedChildren Set。
209
+ // 防主进程崩溃/close 事件漏触发时 Set 无限增长。正常路径 close 事件会 delete(保留 per-child
210
+ // 精细清理语义);killAllSpawnedChildren 是 dispose 全量兼底。
211
+ it("killAllSpawnedChildren 后 spawnedChildren Set 被 clear(size===0)", async () => {
212
+ // 前置清理:即他测试可能残留的 child(close 未触发场景)
213
+ killAllSpawnedChildren();
214
+ expect(spawnedChildren.size).toBe(0);
215
+
216
+ // spawn 两个未 close 的 child(模拟 close 事件漏触发的极端累积场景)
217
+ const rec1 = makeRecord("clear-c1");
218
+ const p1 = runSpawn(rec1, "Task: clear-1", makeOpts(), makeCtx());
219
+ await waitForSpawn();
220
+ const c1 = lastSpawnedChild();
221
+
222
+ const rec2 = makeRecord("clear-c2");
223
+ const p2 = runSpawn(rec2, "Task: clear-2", makeOpts(), makeCtx());
224
+ // 快照语义二次等待:等 rec2 的 runSpawn 调到 spawn
225
+ await waitForSpawn();
226
+ const c2 = lastSpawnedChild();
227
+
228
+ // 两个 child 都在 Set 中
229
+ expect(spawnedChildren.size).toBe(2);
230
+
231
+ // dispose 兼底:kill + clear
232
+ const n = killAllSpawnedChildren();
233
+ expect(n).toBe(2);
234
+ expect(c1.killed).toBe(true);
235
+ expect(c2.killed).toBe(true);
236
+ // Set 被 clear(兑底防泄漏)
237
+ expect(spawnedChildren.size).toBe(0);
238
+
239
+ // 再次调用:Set 已空,返回 0(不会重复 kill 已 kill 的 child)
240
+ const n2 = killAllSpawnedChildren();
241
+ expect(n2).toBe(0);
242
+
243
+ // 收尾
244
+ for (const { child, promise } of [
245
+ { child: c1, promise: p1 },
246
+ { child: c2, promise: p2 },
247
+ ]) {
248
+ emitStdoutLine(child, sessionHeader());
249
+ child.stdout.end();
250
+ child.emit("close", 143);
251
+ const r = await promise;
252
+ expect(r.success).toBe(true);
253
+ }
254
+ });
255
+ });
256
+
257
+ // ── 2. stdout 边界:损坏行 + 残留尾行 (M8) ──
258
+ //
259
+ // [M8] runSpawn 的 stdout 解析容错:
260
+ // - parseSpawnLine 对「非法 JSON」「合法 JSON 但缺 type 字段」归为 kind:"invalid"。
261
+ // - runSpawn 的 data 处理器只认 header/event 两类,invalid 行静默忽略(L559 注释
262
+ // "invalid 行忽略")——单行损坏不应中断整个事件流。
263
+ // - close 前 stdoutBuffer 若残留未以 \n 结尾的合法 event 行,close handler 会再 parse
264
+ // 一次(L574-579)——覆盖子进程末行漏 \n 的场景。
265
+ describe("stdout 边界:损坏行 + 残留尾行 (M8)", () => {
266
+ it("stdout 夹杂非法 JSON 行 → 该行被忽略,合法 turn_end 正常计数(不抛错)", async () => {
267
+ const record = makeRecord();
268
+ const promise = runSpawn(record, "Task: garbage", makeOpts(), makeCtx());
269
+
270
+ // 等待 spawn 被调用拿到 child
271
+ await waitForSpawn();
272
+ const child = lastSpawnedChild();
273
+
274
+ emitStdoutLine(child, sessionHeader());
275
+ // 非法 JSON 行(如 pi 的调试输出 / 进度条残片)—— parseSpawnLine 归为 invalid
276
+ child.stdout.write("this is not json\n");
277
+ // 合法 turn_end 事件
278
+ emitStdoutLine(child, { type: "turn_end" });
279
+ child.stdout.end();
280
+ child.emit("close", 0);
281
+
282
+ const result = await promise;
283
+
284
+ expect(result.success).toBe(true);
285
+ expect(record.turnCount).toBe(1); // 非法行被忽略,仅 turn_end 计数
286
+ });
287
+
288
+ it("stdout 夹杂合法 JSON 但缺 type 字段 → 该行被忽略,不抛错", async () => {
289
+ const record = makeRecord();
290
+ const promise = runSpawn(record, "Task: notype", makeOpts(), makeCtx());
291
+
292
+ // 等待 spawn 被调用拿到 child
293
+ await waitForSpawn();
294
+ const child = lastSpawnedChild();
295
+
296
+ emitStdoutLine(child, sessionHeader());
297
+ // 合法 JSON 但无 type 字段 —— parseSpawnLine 归为 invalid("missing string 'type'")
298
+ child.stdout.write('{"foo":"bar"}\n');
299
+ emitStdoutLine(child, { type: "turn_end" });
300
+ child.stdout.end();
301
+ child.emit("close", 0);
302
+
303
+ const result = await promise;
304
+
305
+ expect(result.success).toBe(true);
306
+ expect(record.turnCount).toBe(1); // 无 type 行被忽略
307
+ });
308
+
309
+ it("残留尾行(close 前未以 \\n 结尾的合法 event)→ close handler 再 parse 处理", async () => {
310
+ // 覆盖 session-runner.ts L574-579:close 前 stdoutBuffer 残留的合法 event 行。
311
+ //
312
+ // 关键:不能用 emitStdoutLine(它会补 \n,残留行在 data 处理器就被 split 消费了,
313
+ // 走不到 close handler 的残留 parse 分支)。需同步 emit data(无 \n)确保该行
314
+ // 残留在 stdoutBuffer 直到 close handler 处理。
315
+ //
316
+ // 同步 emit 的必要性:PassThrough 的 .write() 会把 data flush 排到后续微任务,
317
+ // 若先 .write() 再 emit("close"),close listener 同步执行时 stdoutBuffer 仍为空
318
+ // → 残留逻辑被跳过 → turnCount=0(测出真实 bug 风险)。直接 emit("data", ...) 同步
319
+ // 触发 data 处理器,使行残留在 buffer(split("\n") 无换行 → pop 回 buffer),close
320
+ // handler 才能捕到它。
321
+ const record = makeRecord();
322
+ const promise = runSpawn(record, "Task: tail", makeOpts(), makeCtx());
323
+
324
+ // 等待 spawn 被调用拿到 child
325
+ await waitForSpawn();
326
+ const child = lastSpawnedChild();
327
+
328
+ emitStdoutLine(child, sessionHeader());
329
+ // 合法 turn_end 但不带末尾 \n:同步 emit(绕过 write 的异步 flush)。
330
+ // data 处理器把它整体留在 stdoutBuffer(无 \n → split 后 pop 回 buffer),
331
+ // 由 close handler 的残留 parse 逻辑(L574-579)处理。
332
+ child.stdout.emit("data", JSON.stringify({ type: "turn_end" }));
333
+ child.emit("close", 0);
334
+
335
+ const result = await promise;
336
+
337
+ expect(result.success).toBe(true);
338
+ expect(record.turnCount).toBe(1); // 残留尾行被 close handler 正确解析
339
+ });
340
+
341
+ it("同一 JSON 行跨 3 次 data 事件分片 → stdoutBuffer 字符串拼接后正确解析", async () => {
342
+ // 覆盖 stdoutBuffer += data 的字符串拼接(setEncoding("utf8") 后 data 收到 string,
343
+ // 非 Buffer)。拆成 3 片写入(跨 type 字段名边界 + 跨 turn_end 值边界),验证拼接无误。
344
+ // .write() 的异步 flush 在 await promise(resolve 排在 data 微任务之后)前完成。
345
+ const record = makeRecord();
346
+ const promise = runSpawn(record, "Task: split3", makeOpts(), makeCtx());
347
+
348
+ // 等待 spawn 被调用拿到 child
349
+ await waitForSpawn();
350
+ const child = lastSpawnedChild();
351
+
352
+ emitStdoutLine(child, sessionHeader());
353
+ child.stdout.write('{"typ');
354
+ child.stdout.write('e":"turn_en');
355
+ child.stdout.write('d"}\n');
356
+ child.stdout.end();
357
+ child.emit("close", 0);
358
+
359
+ const result = await promise;
360
+
361
+ expect(result.success).toBe(true);
362
+ expect(record.turnCount).toBe(1); // 3 片拼接后解析为 1 次 turn_end
363
+ });
364
+ });
365
+
366
+ // ── 3. agent_end 自然完成(rpc 长驻进程不自动退出,需主动 kill)──
367
+ //
368
+ // [rpc agent_end] pi --mode rpc 是长驻进程(runRpcMode 末尾 return new Promise(() => {})),
369
+ // 处理完 prompt 后不退出。runSpawn 只靠 child.on("close") 判完成——如果不处理 agent_end,
370
+ // 子进程会卡到 watchdog 30 分钟兜底 kill。修复:收到 agent_end(willRetry=false)后
371
+ // 主动 child.kill("SIGTERM") 让子进程退出,触发 close → runSpawn resolve。
372
+ // willRetry=true 时 agent 会重试,不能 kill。
373
+ describe("agent_end 自然完成", () => {
374
+ // [recursive-orchestration] 条件 kill:agent_end 时读子进程 session 文件算活跃后代
375
+ // (pending:register − unregister 差集)。有活跃后代 → 保持进程 idle 等 steer 唤醒。
376
+ const mockPending = vi.mocked(readActivePendingFromSessionFile);
377
+
378
+ // [MF-3] recentUnregister 竞态分支的秒级宽限:count=0 + recentUnregister=true → keep alive
379
+ // 挂 WAKEUP_GRACE_MS(15s)定时器,到期无新 agent_end(未被唤醒)即 kill。
380
+ // 背景:旧实现挂固定 2h(WAIT_DESCENDANT_TIMEOUT_MS)——层主 closeout 的最终 agent_end
381
+ // 距最后一次 unregister <60s 必命中此分支,空等 2h 才 kill + 冒牌完成通知级联。
382
+ it("MF-3: agent_end(count=0 + recentUnregister)→ 15s 宽限到期后 kill", async () => {
383
+ mockPending.mockReturnValue({ count: 0, recentUnregister: true });
384
+ const record = makeRecord();
385
+ const promise = runSpawn(record, "Task: wake-grace", makeOpts(), makeCtx());
386
+
387
+ await waitForSpawn();
388
+ const child = lastSpawnedChild();
389
+
390
+ // 必须在 emit agent_end 之前启用 fake timers——keep-alive 定时器在 agent_end 处理器里
391
+ // 新建,新建时若已是 fake 定时器才可被 advance。不 fake setImmediate:stream .write() 的
392
+ // data flush 靠真实事件循环(微任务/nextTick)交付,用真实 setImmediate 让出事件循环。
393
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
394
+ try {
395
+ emitStdoutLine(child, sessionHeader());
396
+ emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
397
+ // 让事件 pump 处理 agent_end(stream flush 是异步的)
398
+ await new Promise((r) => setImmediate(r));
399
+ // keep alive:宽限挂起,不 kill
400
+ expect(child.killed).toBe(false);
401
+
402
+ // 宽限未到(14999ms):仍不 kill
403
+ await vi.advanceTimersByTimeAsync(WAKEUP_GRACE_MS - 1);
404
+ expect(child.killed).toBe(false);
405
+
406
+ // 第 15000ms 到期:无新 agent_end(未被唤醒)→ kill
407
+ await vi.advanceTimersByTimeAsync(1);
408
+ expect(child.killed).toBe(true);
409
+ expect(child.killSignal).toBe("SIGTERM");
410
+
411
+ // 收尾:close 让 runSpawn resolve
412
+ child.stdout.end();
413
+ child.stderr.end();
414
+ child.emit("close", 143);
415
+
416
+ const result = await promise;
417
+ expect(result.success).toBe(true);
418
+ } finally {
419
+ vi.useRealTimers();
420
+ }
421
+ });
422
+
423
+ // [MF-3] 唤醒重评估:宽限挂起期间父被 steer 唤醒(续跑产出新一轮)→ 下一次 agent_end
424
+ // 重新判定;此时后代已完成(count=0,recentUnregister=false)→ 正常 kill,不等宽限到期。
425
+ it("MF-3: 宽限期内被唤醒 → 下一次 agent_end 重新评估(后代已完成 → kill)", async () => {
426
+ mockPending
427
+ .mockReturnValueOnce({ count: 0, recentUnregister: true })
428
+ .mockReturnValueOnce({ count: 0, recentUnregister: false });
429
+ const record = makeRecord();
430
+ const promise = runSpawn(record, "Task: wake-then-done", makeOpts(), makeCtx());
431
+
432
+ await waitForSpawn();
433
+ const child = lastSpawnedChild();
434
+
435
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
436
+ try {
437
+ emitStdoutLine(child, sessionHeader());
438
+ // 第一次 agent_end:recentUnregister 竞态 → keep alive(宽限挂起)
439
+ emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
440
+ await new Promise((r) => setImmediate(r));
441
+ expect(child.killed).toBe(false);
442
+
443
+ // 被唤醒后父续跑产出新一轮 agent_end:后代已 unregister → 立即 kill(不等宽限)
444
+ emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
445
+ await new Promise((r) => setImmediate(r));
446
+ expect(child.killed).toBe(true);
447
+ expect(child.killSignal).toBe("SIGTERM");
448
+
449
+ child.stdout.end();
450
+ child.stderr.end();
451
+ child.emit("close", 143);
452
+
453
+ const result = await promise;
454
+ expect(result.success).toBe(true);
455
+ } finally {
456
+ vi.useRealTimers();
457
+ }
458
+ });
459
+
460
+ // [MF-4] count>0 分支的动态超时:等待超时 = computeWatchdogMs(maxTurns),非固定 2h。
461
+ // maxTurns=20 → 20×5min = 100min 到期 kill。若回归到固定 2h 常量,100min 处不会 kill,
462
+ // 本用例 advance(computeWatchdogMs − 1) 后仍不 kill、再 +1 才 kill 的断言会失败。
463
+ it("MF-4: agent_end(count>0)→ 等待超时 = computeWatchdogMs(maxTurns)(动态,非固定 2h)", async () => {
464
+ const maxTurns = 20;
465
+ const expected = computeWatchdogMs(maxTurns);
466
+ mockPending.mockReturnValue({ count: 2 });
467
+ const record = makeRecord();
468
+ const promise = runSpawn(record, "Task: slow-desc", makeOpts({ maxTurns }), makeCtx());
469
+
470
+ await waitForSpawn();
471
+ const child = lastSpawnedChild();
472
+
473
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
474
+ try {
475
+ emitStdoutLine(child, sessionHeader());
476
+ emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
477
+ await new Promise((r) => setImmediate(r));
478
+ // keep alive:动态超时挂起,不 kill
479
+ expect(child.killed).toBe(false);
480
+
481
+ // 未到动态超时(100min−1ms):不 kill
482
+ await vi.advanceTimersByTimeAsync(expected - 1);
483
+ expect(child.killed).toBe(false);
484
+
485
+ // 动态超时到期:kill。若误用固定 2h,此处不会 kill → 用例失败
486
+ await vi.advanceTimersByTimeAsync(1);
487
+ expect(child.killed).toBe(true);
488
+ expect(child.killSignal).toBe("SIGTERM");
489
+
490
+ child.stdout.end();
491
+ child.stderr.end();
492
+ child.emit("close", 143);
493
+
494
+ const result = await promise;
495
+ expect(result.success).toBe(true);
496
+ } finally {
497
+ vi.useRealTimers();
498
+ }
499
+ });
500
+
501
+ // [MF-4b] 预算语义对齐:maxTurns 未传且无兑底 env → count>0 分支不 re-arm watchdog,
502
+ // 不限时等待后代。若误回旧 50min 估算默认,51min 处会 kill,本用例失败。
503
+ it("MF-4b: agent_end(count>0)+ maxTurns 未传 → 不 re-arm watchdog(不限时等待)", async () => {
504
+ // hermetic:确保兑底 env 未设(若外层 shell 误设会让「不限时」断言失效)
505
+ const prevWatchdogEnv = process.env[SPAWN_WATCHDOG_ENV];
506
+ delete process.env[SPAWN_WATCHDOG_ENV];
507
+ mockPending.mockReturnValue({ count: 2 });
508
+ const record = makeRecord();
509
+ const promise = runSpawn(record, "Task: slow-desc-no-turns", makeOpts(), makeCtx());
510
+
511
+ await waitForSpawn();
512
+ const child = lastSpawnedChild();
513
+
514
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
515
+ try {
516
+ emitStdoutLine(child, sessionHeader());
517
+ emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
518
+ await new Promise((r) => setImmediate(r));
519
+ // keep alive:不 kill
520
+ expect(child.killed).toBe(false);
521
+
522
+ // 超过旧 50min 估算默认仍不 kill = watchdog 未 re-arm
523
+ await vi.advanceTimersByTimeAsync(51 * 60 * 1000);
524
+ expect(child.killed).toBe(false);
525
+
526
+ child.stdout.end();
527
+ child.stderr.end();
528
+ child.emit("close", 143);
529
+
530
+ const result = await promise;
531
+ expect(result.success).toBe(true);
532
+ } finally {
533
+ if (prevWatchdogEnv !== undefined) process.env[SPAWN_WATCHDOG_ENV] = prevWatchdogEnv;
534
+ vi.useRealTimers();
535
+ }
536
+ });
537
+
538
+ // [S-9] pending.error 分支(sessionFile 不可读 → 保守 keep-alive + re-arm dynamic watchdog)
539
+ // 集成行为 guard:session-pending 单测覆盖 error 返回值,但 session-runner 的 no-kill +
540
+ // re-arm 到 computeWatchdogMs(maxTurns) 行为无集成 guard。若 re-arm 误删/误用固定超时,
541
+ // 保守 keep-alive 会退化成永久挂起或被固定超时误杀。
542
+ it("S-9: agent_end(count=0 + error)→ 保守不 kill + watchdog re-arm 到动态超时", async () => {
543
+ const maxTurns = 20;
544
+ const expected = computeWatchdogMs(maxTurns);
545
+ mockPending.mockReturnValue({ count: 0, error: "session file unreadable: EACCES" });
546
+ const record = makeRecord();
547
+ const promise = runSpawn(record, "Task: unreadable", makeOpts({ maxTurns }), makeCtx());
548
+
549
+ await waitForSpawn();
550
+ const child = lastSpawnedChild();
551
+
552
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
553
+ try {
554
+ emitStdoutLine(child, sessionHeader());
555
+ emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
556
+ await new Promise((r) => setImmediate(r));
557
+ // 保守策略:sessionFile 不可读时不 kill(宁可空等也不误杀有后代的进程)
558
+ expect(child.killed).toBe(false);
559
+
560
+ // watchdog re-arm 到动态超时(computeWatchdogMs(maxTurns)),未到期不 kill
561
+ await vi.advanceTimersByTimeAsync(expected - 1);
562
+ expect(child.killed).toBe(false);
563
+
564
+ // 动态超时到期:kill。若 error 分支漏了 re-arm(或误用固定超时),此断言失败
565
+ await vi.advanceTimersByTimeAsync(1);
566
+ expect(child.killed).toBe(true);
567
+ expect(child.killSignal).toBe("SIGTERM");
568
+
569
+ child.stdout.end();
570
+ child.stderr.end();
571
+ child.emit("close", 143);
572
+
573
+ const result = await promise;
574
+ expect(result.success).toBe(true);
575
+ } finally {
576
+ vi.useRealTimers();
577
+ }
578
+ });
579
+
580
+ it("agent_end(willRetry=false,无活跃后代)→ child.kill(SIGTERM) 被调用,close 后 success=true", async () => {
581
+ mockPending.mockReturnValue({ count: 0 });
582
+ const record = makeRecord();
583
+ const promise = runSpawn(record, "Task: done", makeOpts(), makeCtx());
584
+
585
+ await waitForSpawn();
586
+ const child = lastSpawnedChild();
587
+
588
+ emitStdoutLine(child, sessionHeader());
589
+ // agent 自然完成(willRetry=false)
590
+ emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
591
+ child.stdout.end();
592
+ child.stderr.end();
593
+ // agent_end 触发 kill(SIGTERM) → 子进程退出(exitCode 143 = 128+15)
594
+ child.emit("close", 143);
595
+
596
+ const result = await promise;
597
+
598
+ expect(child.killed).toBe(true);
599
+ expect(child.killSignal).toBe("SIGTERM");
600
+ // 信号终止(>=128)视为正常完成
601
+ expect(result.success).toBe(true);
602
+ });
603
+
604
+ it("agent_end(willRetry=false,有活跃后代)→ 不 kill,进程保持 idle 等 steer 唤醒;后代完成后 kill", async () => {
605
+ // 第一次 agent_end:有活跃后代(count=1)→ 不 kill
606
+ mockPending.mockReturnValueOnce({ count: 1 });
607
+ // 第二次 agent_end:后代已 unregister(count=0)→ kill
608
+ mockPending.mockReturnValueOnce({ count: 0 });
609
+ const record = makeRecord();
610
+ const promise = runSpawn(record, "Task: orchestrate", makeOpts(), makeCtx());
611
+
612
+ await waitForSpawn();
613
+ const child = lastSpawnedChild();
614
+
615
+ emitStdoutLine(child, sessionHeader());
616
+ emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
617
+ // 给事件 pump 一点时间处理 agent_end(判定读 session 文件)
618
+ await new Promise((r) => setTimeout(r, 20));
619
+ expect(child.killed).toBe(false);
620
+
621
+ // 后代完成(unregister 已写入)后再次 agent_end → 无活跃后代 → kill
622
+ emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
623
+ child.stdout.end();
624
+ child.stderr.end();
625
+ child.emit("close", 143);
626
+
627
+ const result = await promise;
628
+
629
+ expect(child.killed).toBe(true);
630
+ expect(child.killSignal).toBe("SIGTERM");
631
+ expect(result.success).toBe(true);
632
+ });
633
+
634
+ it("agent_end(willRetry=true)→ child.kill 不被调用(agent 会重试,等下一个 agent_end)", async () => {
635
+ mockPending.mockReturnValue({ count: 0 });
636
+ const record = makeRecord();
637
+ const promise = runSpawn(record, "Task: retry", makeOpts(), makeCtx());
638
+
639
+ await waitForSpawn();
640
+ const child = lastSpawnedChild();
641
+
642
+ emitStdoutLine(child, sessionHeader());
643
+ // willRetry=true:agent 会重试,不应 kill
644
+ emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: true });
645
+ // 此时 child.killed 应仍为 false。给一点时间让 event pump 处理完。
646
+ await new Promise((r) => setTimeout(r, 10));
647
+ expect(child.killed).toBe(false);
648
+
649
+ // 收尾:模拟重试后的最终完成(willRetry=false)
650
+ emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
651
+ child.stdout.end();
652
+ child.stderr.end();
653
+ child.emit("close", 143);
654
+
655
+ const result = await promise;
656
+
657
+ // 最终的 agent_end(willRetry=false)触发 kill
658
+ expect(child.killed).toBe(true);
659
+ expect(child.killSignal).toBe("SIGTERM");
660
+ expect(result.success).toBe(true);
661
+ });
662
+
663
+ it("agent_end 后的后续 event 仍被 handleSdkEvent 处理(kill 不阻塞 event pump)", async () => {
664
+ mockPending.mockReturnValue({ count: 0 });
665
+ const record = makeRecord();
666
+ const promise = runSpawn(record, "Task: flush", makeOpts(), makeCtx());
667
+
668
+ await waitForSpawn();
669
+ const child = lastSpawnedChild();
670
+
671
+ emitStdoutLine(child, sessionHeader());
672
+ // agent_end kill 是 fire-and-forget(SIGTERM 异步),后续 stdout 行仍被 event pump 处理
673
+ emitStdoutLine(child, { type: "agent_end", messages: [], willRetry: false });
674
+ emitStdoutLine(child, { type: "turn_end" }); // kill 后的 turn_end 仍应被处理
675
+ child.stdout.end();
676
+ child.stderr.end();
677
+ child.emit("close", 143);
678
+
679
+ const result = await promise;
680
+
681
+ expect(child.killed).toBe(true);
682
+ expect(result.success).toBe(true);
683
+ // turn_end 被处理 → turnCount=1
684
+ expect(record.turnCount).toBe(1);
685
+ });
686
+ });
687
+ });