@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,933 @@
1
+ // src/__tests__/run-spawn-integration.test.ts
2
+ //
3
+ // runSpawn 集成测试(spawn 改造核心函数)。
4
+ //
5
+ // runSpawn 负责 spawn pi 子进程、pump stdout JSON 事件流、signal/maxTurns 终止、
6
+ // identity 补写、exitCode 判定。此前完全无集成测试——本文件覆盖审查发现的关键路径。
7
+ //
8
+ // mock 策略(参考 worktree-manager.test.ts 的 mock 模式):
9
+ // - node:child_process.spawn → 返回 FakeChild(EventEmitter + PassThrough),
10
+ // 测试用控制器 emit data/close/error 控制时序。
11
+ // - node:child_process.execFile → err-first callback 默认兜底(buildEnvBlock 的 git
12
+ // branch 调用失败 → catch → branch="")。
13
+ // - node:fs 同步方法 → mock(mkdirSync/existsSync/appendFileSync/writeFileSync 等),
14
+ // 避免 sessionDir/sessionFile 触碰真实文件系统。
15
+ // - fs.promises.* → 保留真实实现(temp-prompt 整体被 mock,不触发真实 I/O)。
16
+ // - temp-prompt → mock(writePromptToTempFile 返回固定路径,消除 fake-timers flaky)。
17
+ // - alive-store.writeAliveMarker → mock(避免写 .alive sidecar)。
18
+ //
19
+ // mock 工厂 + FakeChild class + 工具函数(lastSpawnedChild/waitForSpawn/emitStdoutLine/
20
+ // sessionHeader/makeRecord/makeOpts/makeCtx)抽到 helpers/spawn-mock.ts,与
21
+ // run-spawn-edges.test.ts / run-spawn-rpc-mode.test.ts 三文件共享。vi.mock 工厂内用
22
+ // `await import("./helpers/spawn-mock.ts")` 取回 FakeChild(绕开 vitest 的 hoisting 限制——
23
+ // 工厂函数体不能引用顶层 import 变量,但 async 工厂内的 await import 是运行时求值)。
24
+
25
+ import { spawn } from "node:child_process";
26
+ import * as fs from "node:fs";
27
+
28
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
29
+
30
+ // ── mock modules(工厂体共享自 helpers/spawn-mock.ts;vi.mock 必须各文件独立声明)──
31
+
32
+ vi.mock("node:child_process", async () => {
33
+ const { FakeChild } = await import("./helpers/spawn-mock.ts");
34
+ return {
35
+ spawn: vi.fn(() => new FakeChild()),
36
+ // buildEnvBlock 的 git branch 调用(execFile 异步):默认 err-first 兜底 → catch → branch=""
37
+ execFile: vi.fn(
38
+ (
39
+ _cmd: string,
40
+ _args: readonly string[],
41
+ _opts: unknown,
42
+ cb: (err: Error | null, stdout?: string, stderr?: string) => void,
43
+ ) => cb(new Error("execFile not configured in this test")),
44
+ ),
45
+ };
46
+ });
47
+
48
+ // node:fs:同步方法 mock(runSpawn 用到的全部),promises 保留真实实现(temp-prompt 用)。
49
+ vi.mock("node:fs", async () => {
50
+ const actual = await import("node:fs");
51
+ return {
52
+ default: {
53
+ ...actual,
54
+ mkdirSync: vi.fn(),
55
+ existsSync: vi.fn(() => false),
56
+ appendFileSync: vi.fn(),
57
+ writeFileSync: vi.fn(),
58
+ readdirSync: vi.fn(() => []),
59
+ },
60
+ mkdirSync: vi.fn(),
61
+ existsSync: vi.fn(() => false),
62
+ appendFileSync: vi.fn(),
63
+ writeFileSync: vi.fn(),
64
+ readdirSync: vi.fn(() => []),
65
+ promises: actual.promises,
66
+ };
67
+ });
68
+
69
+ vi.mock("../alive-store.ts", () => ({
70
+ writeAliveMarker: vi.fn(),
71
+ }));
72
+
73
+ // temp-prompt:mock 掉真实 fs.promises I/O(mkdtemp/writeFile/rm)。
74
+ // 原先保留真实实现导致 fake-timers 测试偶发 flaky——writePromptToTempFile 的真实异步
75
+ // I/O 在 CI 慢机器上无法在 advanceTimersByTimeAsync 的有限步数内 resolve,spawn 永不触发。
76
+ vi.mock("../temp-prompt.ts", () => ({
77
+ writePromptToTempFile: vi.fn(async (agent: string) => {
78
+ const safeName = agent.replace(/[^\w.-]+/g, "_");
79
+ return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
80
+ }),
81
+ cleanupTempPrompt: vi.fn(async () => {}),
82
+ }));
83
+
84
+ import { runSpawn } from "../session-runner.ts";
85
+ import {
86
+ emitStdoutLine,
87
+ type FakeChild,
88
+ lastSpawnedChild as lastSpawnedChildOf,
89
+ makeCtx,
90
+ makeOpts,
91
+ makeRecord,
92
+ mockSessionFileExists as mockSessionFileExistsOf,
93
+ sessionHeader,
94
+ waitForSpawn as waitForSpawnOf,
95
+ } from "./helpers/spawn-mock.ts";
96
+
97
+ const mockSpawn = vi.mocked(spawn);
98
+ const mockExistsSync = vi.mocked(fs.existsSync);
99
+ const mockAppendFileSync = vi.mocked(fs.appendFileSync);
100
+ const mockMkdirSync = vi.mocked(fs.mkdirSync);
101
+
102
+ // 绑定到本文件 mockSpawn 的 lastSpawnedChild/waitForSpawn(需读 mockSpawn.mock.results)
103
+ const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
104
+ const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
105
+ const mockSessionFileExists = (p: string): void => mockSessionFileExistsOf(mockExistsSync, p);
106
+
107
+ // ============================================================
108
+ // 测试
109
+ // ============================================================
110
+
111
+ describe("runSpawn", () => {
112
+ beforeEach(() => {
113
+ vi.clearAllMocks();
114
+ // existsSync 默认 false(sessionFile 不存在兜底路径)
115
+ mockExistsSync.mockReturnValue(false);
116
+ });
117
+
118
+ afterEach(() => {
119
+ vi.restoreAllMocks();
120
+ });
121
+
122
+ // ── 1. 正常路径(happy path)──
123
+ describe("正常路径", () => {
124
+ it("header + turn_end 事件 + close(0) → success=true,record.sessionFile 被设置,turnCount 正确", async () => {
125
+ const record = makeRecord();
126
+ const promise = runSpawn(record, "Task: hello", makeOpts(), makeCtx());
127
+
128
+ // 等待 spawn 被调用拿到 child
129
+ // 等待 spawn 被调用拿到 child
130
+ await waitForSpawn();
131
+ const child = lastSpawnedChild();
132
+
133
+ const expectedSessionFile =
134
+ "/tmp/test/agents/subagents/--tmp-test--/sessions/2026-07-03T12-00-00-000Z_sess-abc.jsonl";
135
+ // 进程退出后 existsSync(record.sessionFile) 校验通过 → 补写 identity
136
+ mockSessionFileExists(expectedSessionFile);
137
+
138
+ // emit stdout:header + 2 个 turn_end
139
+ emitStdoutLine(child, sessionHeader("sess-abc"));
140
+ emitStdoutLine(child, { type: "turn_end" });
141
+ emitStdoutLine(child, { type: "turn_end" });
142
+ child.stdout.end();
143
+ child.emit("close", 0);
144
+
145
+ const result = await promise;
146
+
147
+ expect(result.success).toBe(true);
148
+ expect(result.turns).toBe(2); // 2 个 turn_end
149
+ expect(record.turnCount).toBe(2);
150
+ // record.sessionFile 由 header 推导路径回填
151
+ expect(record.sessionFile).toBe(expectedSessionFile);
152
+ expect(result.sessionFile).toBe(expectedSessionFile);
153
+ // sessionId 来自 header.id
154
+ expect(result.sessionId).toBe("sess-abc");
155
+ });
156
+
157
+ it("collectResult 文本/toolCalls 从 record 派生(text_delta + tool 调用累积进 turns)", async () => {
158
+ const record = makeRecord();
159
+ const promise = runSpawn(record, "Task: write", makeOpts(), makeCtx());
160
+
161
+ // 等待 spawn 被调用拿到 child
162
+ await waitForSpawn();
163
+ const child = lastSpawnedChild();
164
+ mockSessionFileExists(
165
+ "/tmp/test/agents/subagents/--tmp-test--/sessions/2026-07-03T12-00-00-000Z_sess-x.jsonl",
166
+ );
167
+
168
+ emitStdoutLine(child, sessionHeader("sess-x"));
169
+ emitStdoutLine(child, { type: "message_update", assistantMessageEvent: { type: "text_delta", delta: "Hello " } });
170
+ emitStdoutLine(child, { type: "message_update", assistantMessageEvent: { type: "text_delta", delta: "world" } });
171
+ emitStdoutLine(child, { type: "tool_execution_start", toolCallId: "t1", toolName: "bash", args: { command: "ls" } });
172
+ emitStdoutLine(child, { type: "tool_execution_end", toolCallId: "t1", toolName: "bash", result: { details: "file.txt" } });
173
+ emitStdoutLine(child, { type: "turn_end" });
174
+ child.stdout.end();
175
+ child.emit("close", 0);
176
+
177
+ const result = await promise;
178
+
179
+ expect(result.success).toBe(true);
180
+ expect(result.text).toBe("Hello world");
181
+ expect(result.toolCalls).toHaveLength(1);
182
+ expect(result.toolCalls[0]!.toolName).toBe("bash");
183
+ expect(result.toolCalls[0]!.result?.details).toBe("file.txt");
184
+ });
185
+ });
186
+
187
+ // ── 2. signal abort → child.kill(cancel 路径)──
188
+ describe("signal abort", () => {
189
+ it("abort signal 触发后 child.kill(SIGTERM) 被调用,success=false", async () => {
190
+ const controller = new AbortController();
191
+ const record = makeRecord();
192
+ const promise = runSpawn(
193
+ record,
194
+ "Task: long",
195
+ makeOpts({ signal: controller.signal }),
196
+ makeCtx(),
197
+ );
198
+
199
+ // 等待 spawn 被调用拿到 child
200
+ await waitForSpawn();
201
+ const child = lastSpawnedChild();
202
+
203
+ // abort 必须在 spawn 之后(addEventListener 已注册)。
204
+ // 用 queueMicrotask 延迟到当前微任务清空后触发,确保 listener 已挂载。
205
+ queueMicrotask(() => controller.abort());
206
+
207
+ // emit header + close(被 kill 后子进程退出,signal 终止 exitCode>=128 或 null)
208
+ emitStdoutLine(child, sessionHeader());
209
+ child.stdout.end();
210
+ child.emit("close", 143); // SIGTERM = 128+15
211
+
212
+ const result = await promise;
213
+
214
+ expect(child.killed).toBe(true);
215
+ expect(child.killSignal).toBe("SIGTERM");
216
+ // signal.aborted 路径:success=false,但 error 为 undefined(取消不算 error)
217
+ expect(result.success).toBe(false);
218
+ expect(result.error).toBeUndefined();
219
+ });
220
+ });
221
+
222
+ // ── 3. maxTurns → limiter.abort → proc.kill ──
223
+ describe("maxTurns 限制", () => {
224
+ it("maxTurns=1, graceTurns=0 → 超出 turn 后 child.kill 被调用,success=true(达限视为正常完成)", async () => {
225
+ const record = makeRecord();
226
+ const promise = runSpawn(
227
+ record,
228
+ "Task: bounded",
229
+ makeOpts({ maxTurns: 1, graceTurns: 0 }),
230
+ makeCtx(),
231
+ );
232
+
233
+ // 等待 spawn 被调用拿到 child
234
+ await waitForSpawn();
235
+ const child = lastSpawnedChild();
236
+
237
+ emitStdoutLine(child, sessionHeader());
238
+ // 第 1 个 turn_end:turn=1 >= maxTurns=1 → steer(noop)
239
+ emitStdoutLine(child, { type: "turn_end" });
240
+ // 第 2 个 turn_end:turn=2 >= maxTurns(1)+graceTurns(0)=1 → abort → proc.kill
241
+ emitStdoutLine(child, { type: "turn_end" });
242
+ child.stdout.end();
243
+ // 被 kill 后信号终止(exitCode>=128),视为正常完成
244
+ child.emit("close", 143);
245
+
246
+ const result = await promise;
247
+
248
+ expect(child.killed).toBe(true);
249
+ expect(child.killSignal).toBe("SIGTERM");
250
+ // maxTurns kill 视为正常完成(exitCode>=128 走信号终止分支 → success=true)
251
+ expect(result.success).toBe(true);
252
+ });
253
+
254
+ it("maxTurns=2, graceTurns=2 → steer 后需 graceTurns 到达才 kill(第 4 turn 才 abort)", async () => {
255
+ const record = makeRecord();
256
+ const promise = runSpawn(
257
+ record,
258
+ "Task: grace",
259
+ makeOpts({ maxTurns: 2, graceTurns: 2 }),
260
+ makeCtx(),
261
+ );
262
+
263
+ // 等待 spawn 被调用拿到 child
264
+ await waitForSpawn();
265
+ const child = lastSpawnedChild();
266
+
267
+ // emit header + 4 个 turn_end。limiter 逻辑(maxTurns=2, graceTurns=2):
268
+ // turn=1: 1<2,无动作
269
+ // turn=2: 2>=limit → steer(仅一次)
270
+ // turn=3: 3<limit+grace(4),仍在宽限内
271
+ // turn=4: 4>=4 → abort → proc.kill(SIGTERM)
272
+ // 注:PassThrough 流加 data 监听器后,缓冲块逐个在后续微任务中 flush,close 前 await
273
+ // 较长时间窗(20ms)确保全部 turn_end 已被 data 处理器消费(含 limiter.abort → kill)。
274
+ emitStdoutLine(child, sessionHeader());
275
+ emitStdoutLine(child, { type: "turn_end" }); // turn=1
276
+ emitStdoutLine(child, { type: "turn_end" }); // turn=2: steer
277
+ emitStdoutLine(child, { type: "turn_end" }); // turn=3: 宽限内
278
+ emitStdoutLine(child, { type: "turn_end" }); // turn=4: abort → kill
279
+ child.stdout.end();
280
+ await new Promise((r) => setTimeout(r, 20));
281
+ child.emit("close", 143);
282
+
283
+ const result = await promise;
284
+
285
+ // 第 4 个 turn_end 触发 abort → child.kill(SIGTERM)
286
+ expect(child.killed).toBe(true);
287
+ expect(child.killSignal).toBe("SIGTERM");
288
+ expect(record.turnCount).toBe(4);
289
+ // exitCode>=128(信号终止)→ success=true(达限视为正常完成)
290
+ expect(result.success).toBe(true);
291
+ });
292
+ });
293
+
294
+ // ── 4. 子进程非零退出(exitCode < 128)──
295
+ describe("非零退出码", () => {
296
+ it("close(1) + stderr 内容 → success=false,error 含 stderr", async () => {
297
+ const record = makeRecord();
298
+ const promise = runSpawn(record, "Task: fail", makeOpts(), makeCtx());
299
+
300
+ // 等待 spawn 被调用拿到 child
301
+ await waitForSpawn();
302
+ const child = lastSpawnedChild();
303
+
304
+ emitStdoutLine(child, sessionHeader());
305
+ child.stderr.write("Error: something went wrong\n");
306
+ child.stderr.end();
307
+ child.stdout.end();
308
+ child.emit("close", 1);
309
+
310
+ const result = await promise;
311
+
312
+ expect(result.success).toBe(false);
313
+ expect(result.error).toContain("something went wrong");
314
+ });
315
+
316
+ it("close(1) 无 stderr → error 含 exit code 信息", async () => {
317
+ const record = makeRecord();
318
+ const promise = runSpawn(record, "Task: fail", makeOpts(), makeCtx());
319
+
320
+ // 等待 spawn 被调用拿到 child
321
+ await waitForSpawn();
322
+ const child = lastSpawnedChild();
323
+
324
+ child.stdout.end();
325
+ child.stderr.end();
326
+ child.emit("close", 1);
327
+
328
+ const result = await promise;
329
+
330
+ expect(result.success).toBe(false);
331
+ expect(result.error).toContain("exited with code 1");
332
+ });
333
+ });
334
+
335
+ // ── 5. 子进程被信号终止(exitCode = null 或 >= 128)──
336
+ describe("信号终止", () => {
337
+ it("close(null) → success=true(信号终止视为正常,非 aborted)", async () => {
338
+ const record = makeRecord();
339
+ const promise = runSpawn(record, "Task: sig", makeOpts(), makeCtx());
340
+
341
+ // 等待 spawn 被调用拿到 child
342
+ await waitForSpawn();
343
+ const child = lastSpawnedChild();
344
+
345
+ emitStdoutLine(child, sessionHeader());
346
+ child.stdout.end();
347
+ child.stderr.end();
348
+ child.emit("close", null);
349
+
350
+ const result = await promise;
351
+
352
+ expect(result.success).toBe(true);
353
+ });
354
+
355
+ it("close(143)(SIGTERM,>=128)无 signal → success=true", async () => {
356
+ const record = makeRecord();
357
+ const promise = runSpawn(record, "Task: sig143", makeOpts(), makeCtx());
358
+
359
+ // 等待 spawn 被调用拿到 child
360
+ await waitForSpawn();
361
+ const child = lastSpawnedChild();
362
+
363
+ emitStdoutLine(child, sessionHeader());
364
+ child.stdout.end();
365
+ child.stderr.end();
366
+ child.emit("close", 143);
367
+
368
+ const result = await promise;
369
+
370
+ expect(result.success).toBe(true);
371
+ });
372
+ });
373
+
374
+ // ── 6. spawn 本身失败(error 事件,如 ENOENT)──
375
+ describe("spawn error 事件", () => {
376
+ it("child.emit('error') → success=false,error 含错误信息,record.lastError 被设置", async () => {
377
+ const record = makeRecord();
378
+ const promise = runSpawn(record, "Task: enoent", makeOpts(), makeCtx());
379
+
380
+ // 等待 spawn 被调用拿到 child
381
+ await waitForSpawn();
382
+ const child = lastSpawnedChild();
383
+
384
+ // spawn error 事件(command not found 等)——runSpawn 的 error handler resolve(128)
385
+ child.emit("error", new Error("spawn ENOENT"));
386
+
387
+ const result = await promise;
388
+
389
+ expect(result.success).toBe(false);
390
+ expect(result.error).toContain("spawn ENOENT");
391
+ expect(record.lastError).toContain("spawn ENOENT");
392
+ });
393
+
394
+ it("[worktree-reaper-fix] ENOENT error 消息拼 spawnCwd(避免误诊 node 被卸载)", async () => {
395
+ const record = makeRecord();
396
+ const promise = runSpawn(record, "Task: enoent-cwd", makeOpts(), makeCtx());
397
+
398
+ await waitForSpawn();
399
+ const child = lastSpawnedChild();
400
+
401
+ // ENOENT 且带 code(Node spawn 失败的真实形态)——error handler 必须拼 spawnCwd
402
+ const err = Object.assign(new Error("spawn ENOENT"), { code: "ENOENT" });
403
+ child.emit("error", err);
404
+
405
+ const result = await promise;
406
+
407
+ expect(result.success).toBe(false);
408
+ // makeCtx().cwd = "/tmp/test",无 worktree 时 spawnCwd = ctx.cwd
409
+ expect(record.lastError).toContain("/tmp/test");
410
+ });
411
+ });
412
+
413
+ // ── 7. identity 写入职责(M4 / V2 决策 5:迁移到子进程 session_start)──
414
+ describe("identity 写入职责", () => {
415
+ it("正常 close 后 session-runner 不再 fs 补写 identity(M4 迁移到子进程 session_start)", async () => {
416
+ const record = makeRecord();
417
+ const promise = runSpawn(record, "Task: identity", makeOpts(), makeCtx());
418
+
419
+ // 等待 spawn 被调用拿到 child
420
+ await waitForSpawn();
421
+ const child = lastSpawnedChild();
422
+
423
+ const sessionFile =
424
+ "/tmp/test/agents/subagents/--tmp-test--/sessions/2026-07-03T12-00-00-000Z_sess-id.jsonl";
425
+ mockSessionFileExists(sessionFile);
426
+
427
+ emitStdoutLine(child, sessionHeader("sess-id"));
428
+ emitStdoutLine(child, { type: "turn_end" });
429
+ child.stdout.end();
430
+ child.emit("close", 0);
431
+
432
+ await promise;
433
+
434
+ // [M4 / V2 决策 5] identity 不再由 session-runner fs.appendFileSync 补写——
435
+ // 改由子进程 session_start hook 用 pi.appendEntry 写(pi 自动生成 id/parentId,
436
+ // 修复旧 fs 补写缺 id/parentId 污染 _buildIndex 的 bug)。identity 写入由
437
+ // index-session-start-identity.test.ts 覆盖;此处守护 session-runner 不再 fs 写。
438
+ expect(mockAppendFileSync).not.toHaveBeenCalled();
439
+ });
440
+
441
+ it("sessionFile 不存在(existsSync=false)→ 不补写 identity(不调 appendFileSync)", async () => {
442
+ const record = makeRecord();
443
+ const promise = runSpawn(record, "Task: nofile", makeOpts(), makeCtx());
444
+
445
+ // 等待 spawn 被调用拿到 child
446
+ await waitForSpawn();
447
+ const child = lastSpawnedChild();
448
+
449
+ // existsSync 全返回 false(默认)→ record.sessionFile 经兜底查找仍不存在
450
+ emitStdoutLine(child, sessionHeader("sess-none"));
451
+ child.stdout.end();
452
+ child.emit("close", 0);
453
+
454
+ await promise;
455
+
456
+ expect(mockAppendFileSync).not.toHaveBeenCalled();
457
+ });
458
+ });
459
+
460
+ // ── 8. fork depth 环境变量传递 ──
461
+ describe("fork depth 环境变量", () => {
462
+ it("fork=true, parentForkDepth=2 → spawn env 含 PI_SUBAGENT_FORK_DEPTH=3", async () => {
463
+ const record = makeRecord();
464
+ const promise = runSpawn(
465
+ record,
466
+ "Task: fork",
467
+ makeOpts({ fork: true, parentForkDepth: 2 }),
468
+ makeCtx({ mainSessionFile: "/tmp/main.jsonl" }),
469
+ );
470
+
471
+ // 等待 spawn 被调用拿到 child
472
+ await waitForSpawn();
473
+ const child = lastSpawnedChild();
474
+
475
+ emitStdoutLine(child, sessionHeader());
476
+ child.stdout.end();
477
+ child.emit("close", 0);
478
+
479
+ await promise;
480
+
481
+ // spawn 第 3 参数是 options(含 env)
482
+ const spawnOpts = mockSpawn.mock.calls[0]?.[2] as { env: Record<string, string> };
483
+ expect(spawnOpts.env.PI_SUBAGENT_FORK_DEPTH).toBe("3");
484
+ });
485
+
486
+ it("fork=false → spawn env 不含 PI_SUBAGENT_FORK_DEPTH", async () => {
487
+ const record = makeRecord();
488
+ const promise = runSpawn(record, "Task: nofork", makeOpts(), makeCtx());
489
+
490
+ // 等待 spawn 被调用拿到 child
491
+ await waitForSpawn();
492
+ const child = lastSpawnedChild();
493
+
494
+ emitStdoutLine(child, sessionHeader());
495
+ child.stdout.end();
496
+ child.emit("close", 0);
497
+
498
+ await promise;
499
+
500
+ const spawnOpts = mockSpawn.mock.calls[0]?.[2] as { env: Record<string, string | undefined> };
501
+ expect(spawnOpts.env.PI_SUBAGENT_FORK_DEPTH).toBeUndefined();
502
+ });
503
+
504
+ it("spawn 参数含 --session-dir 且 cwd 来自 ctx.cwd", async () => {
505
+ const record = makeRecord();
506
+ const promise = runSpawn(record, "Task: args", makeOpts(), makeCtx());
507
+
508
+ // 等待 spawn 被调用拿到 child
509
+ await waitForSpawn();
510
+ const child = lastSpawnedChild();
511
+
512
+ emitStdoutLine(child, sessionHeader());
513
+ child.stdout.end();
514
+ child.emit("close", 0);
515
+
516
+ await promise;
517
+
518
+ // spawn(command, args, opts):args 是数组,应含 --session-dir
519
+ const args = mockSpawn.mock.calls[0]?.[1] as string[];
520
+ const sessionDirIdx = args.indexOf("--session-dir");
521
+ expect(sessionDirIdx).toBeGreaterThanOrEqual(0);
522
+ expect(args[sessionDirIdx + 1]).toContain("--tmp-test--");
523
+ // spawn opts.cwd 来自 ctx.cwd(无 worktree 时)
524
+ const spawnOpts = mockSpawn.mock.calls[0]?.[2] as { cwd: string };
525
+ expect(spawnOpts.cwd).toBe("/tmp/test");
526
+ });
527
+ });
528
+
529
+ // ── 9. onEvent 回调 ──
530
+ describe("onEvent 回调", () => {
531
+ it("stdout 事件经 handleSdkEvent 翻译后回调 opts.onEvent", async () => {
532
+ const onEvent = vi.fn();
533
+ const record = makeRecord();
534
+ const promise = runSpawn(record, "Task: events", makeOpts({ onEvent }), makeCtx());
535
+
536
+ // 等待 spawn 被调用拿到 child
537
+ await waitForSpawn();
538
+ const child = lastSpawnedChild();
539
+
540
+ emitStdoutLine(child, sessionHeader());
541
+ emitStdoutLine(child, { type: "turn_end" });
542
+ child.stdout.end();
543
+ child.emit("close", 0);
544
+
545
+ await promise;
546
+
547
+ // onEvent 至少被 turn_end 调过一次(onEvent 签名是单参数 event)
548
+ const turnEndCalls = onEvent.mock.calls.filter(([ev]) => (ev as { type: string }).type === "turn_end");
549
+ expect(turnEndCalls.length).toBeGreaterThanOrEqual(1);
550
+ });
551
+ });
552
+
553
+ // ── 10. sessionDir 创建 ──
554
+ describe("sessionDir 创建", () => {
555
+ it("mkdirSync 以 recursive 创建 sessionDir", async () => {
556
+ const record = makeRecord();
557
+ const promise = runSpawn(record, "Task: mkdir", makeOpts(), makeCtx());
558
+
559
+ // 等待 spawn 被调用拿到 child
560
+ await waitForSpawn();
561
+ const child = lastSpawnedChild();
562
+
563
+ emitStdoutLine(child, sessionHeader());
564
+ child.stdout.end();
565
+ child.emit("close", 0);
566
+
567
+ await promise;
568
+
569
+ expect(mockMkdirSync).toHaveBeenCalledWith(
570
+ "/tmp/test/agents/subagents/--tmp-test--/sessions",
571
+ { recursive: true },
572
+ );
573
+ });
574
+ });
575
+
576
+ // ── 11. 临时 prompt 文件清理 ──
577
+ describe("临时 prompt 文件清理", () => {
578
+ it("有 appendSystemPrompt → 创建临时文件并在 finally 清理(spawn 退出后目录消失)", async () => {
579
+ const record = makeRecord();
580
+ const promise = runSpawn(
581
+ record,
582
+ "Task: cleanup",
583
+ makeOpts({ appendSystemPrompt: ["extra instructions"] }),
584
+ makeCtx(),
585
+ );
586
+
587
+ // 等待 spawn 被调用拿到 child
588
+ await waitForSpawn();
589
+ const child = lastSpawnedChild();
590
+
591
+ emitStdoutLine(child, sessionHeader());
592
+ child.stdout.end();
593
+ child.emit("close", 0);
594
+
595
+ await promise;
596
+
597
+ // temp-prompt 已 mock(见文件顶部 vi.mock),返回固定文件名;
598
+ // 验证 spawn args 含 --append-system-prompt(说明临时文件路径被传给子进程)
599
+ const args = mockSpawn.mock.calls[0]?.[1] as string[];
600
+ const appendIdx = args.indexOf("--append-system-prompt");
601
+ expect(appendIdx).toBeGreaterThanOrEqual(0);
602
+ expect(args[appendIdx + 1]).toMatch(/prompt-general-purpose\.md$/);
603
+ });
604
+ });
605
+
606
+ // ── 12. watchdog 超时兜底(R1)──
607
+ //
608
+ // [R0/R1] SPAWN_WATCHDOG_MS = 30 * 60 * 1000(session-runner.ts 顶部)。
609
+ // 子进程卡死在单 tool 内(turn_end 永不触发)时 limiter 失效,watchdog timer 兜底 SIGTERM,
610
+ // 防止 background 槽位/资源泄漏。timer 加了 .unref()(不阻止 Node 退出)。
611
+ //
612
+ // fake timers 方案 A:describe 块内 beforeEach useFakeTimers、afterEach useRealTimers。
613
+ // 难点:waitForSpawn 内部用 `await new Promise(r => setTimeout(r, 5))` 轮询,fake timers 下
614
+ // 该 setTimeout 不会自动触发——改用 vi.advanceTimersByTimeAsync 推进时间,它会同步 flush
615
+ // 已到期的 timer + 让挂起的真实 I/O(writePromptToTempFile 的 fs.promises)resolve。
616
+ // 关键验证:unref 的 timer 在 fake timers 下仍能被 advanceTimersByTime 触发(vitest fake
617
+ // timers 忽略 unref)。
618
+ describe("watchdog 超时兜底 (R1)", () => {
619
+ beforeEach(() => {
620
+ vi.useFakeTimers();
621
+ });
622
+
623
+ afterEach(() => {
624
+ vi.useRealTimers();
625
+ });
626
+
627
+ /**
628
+ * fake timers 下推进时间直到 spawn 被调用(替代 waitForSpawn,返回 child 控制器)。
629
+ *
630
+ * [快照语义] 同 helpers/spawn-mock.ts 的 waitForSpawn:记调用时 baseline,等待其后的
631
+ * **新** spawn——支持同一测试/文件内多次 runSpawn。
632
+ *
633
+ * runSpawn 在 mkdirSync + writePromptToTempFile(真实 fs.promises I/O)之后才调 spawn。
634
+ * 每次推进 10ms 让轮询 setTimeout 触发;同时 advanceTimersByTimeAsync flush 已 resolve
635
+ * 的 I/O promise,使 runSpawn 继续走到 spawn。
636
+ */
637
+ async function waitForSpawnFake(timeoutSteps = 200): Promise<FakeChild> {
638
+ const baseline = mockSpawn.mock.results.length;
639
+ for (let i = 0; i < timeoutSteps; i++) {
640
+ if (mockSpawn.mock.results.length > baseline) break;
641
+ await vi.advanceTimersByTimeAsync(10);
642
+ }
643
+ if (mockSpawn.mock.results.length <= baseline) {
644
+ throw new Error("spawn was not called (fake timers did not progress to spawn)");
645
+ }
646
+ return lastSpawnedChild();
647
+ }
648
+
649
+ it("watchdog 超时(>30min)→ child.kill(SIGTERM) 被调用", async () => {
650
+ const record = makeRecord();
651
+ // 不 await——runSpawn 内部 await 子进程 close,watchdog 触发 kill 后还需 emit close 才 resolve
652
+ // [M-1] maxTurns=6 使 computeWatchdogMs 返回下限 30min(max(30min, 6*5min) = 30min)
653
+ const promise = runSpawn(record, "Task: hang", makeOpts({ maxTurns: 6 }), makeCtx());
654
+
655
+ const child = await waitForSpawnFake();
656
+
657
+ // spawn 后 child.killed 应为 false(watchdog 尚未触发)
658
+ expect(child.killed).toBe(false);
659
+
660
+ // 推进时间越过 watchdog 阈值(30 * 60 * 1000 + 100ms 余量)
661
+ await vi.advanceTimersByTimeAsync(30 * 60 * 1000 + 100);
662
+
663
+ // watchdog 触发 child.kill("SIGTERM")
664
+ expect(child.killed).toBe(true);
665
+ expect(child.killSignal).toBe("SIGTERM");
666
+
667
+ // 收尾:emit close 让 runSpawn 的 await promise resolve(避免悬挂)
668
+ emitStdoutLine(child, sessionHeader());
669
+ child.stdout.end();
670
+ child.emit("close", 143); // SIGTERM = 128+15
671
+
672
+ const result = await promise;
673
+ // 信号终止(>=128)视为正常完成
674
+ expect(result.success).toBe(true);
675
+ });
676
+
677
+ it("正常 close(0) → clearTimeout 生效,watchdog 不触发 kill", async () => {
678
+ const record = makeRecord();
679
+ // [M-1] maxTurns=6 使 watchdog=30min(与上面 watchdog 触发测试同基准)
680
+ const promise = runSpawn(record, "Task: quick", makeOpts({ maxTurns: 6 }), makeCtx());
681
+
682
+ const child = await waitForSpawnFake();
683
+
684
+ // 正常完成:emit header + close(0)
685
+ emitStdoutLine(child, sessionHeader());
686
+ child.stdout.end();
687
+ child.emit("close", 0);
688
+
689
+ const result = await promise;
690
+ expect(result.success).toBe(true);
691
+
692
+ // close 后 runSpawn 已 clearTimeout(watchdog);推进 30+ 分钟验证 watchdog 未触发 kill
693
+ await vi.advanceTimersByTimeAsync(30 * 60 * 1000 + 100);
694
+
695
+ expect(child.killed).toBe(false);
696
+ expect(child.killSignal).toBeUndefined();
697
+ });
698
+ });
699
+
700
+ // ── 13. stderr 截断防 OOM(R2)──
701
+ //
702
+ // [R2] stderrBuffer = (stderrBuffer + data).slice(-STDERR_MAX_CHARS)(STDERR_MAX_CHARS=64*1024)。
703
+ // 失控子进程打满 stderr 会耗尽父进程内存;保留尾部 64KB 用于 error 诊断。
704
+ describe("stderr 截断防 OOM (R2)", () => {
705
+ it("stderr > 64KB → error 中的 stderr 内容是尾部 64KB(尾部标记保留,头部标记被截断)", async () => {
706
+ const record = makeRecord();
707
+ const promise = runSpawn(record, "Task: noisy stderr", makeOpts(), makeCtx());
708
+
709
+ await waitForSpawn();
710
+ const child = lastSpawnedChild();
711
+
712
+ // 头部标记(应被截断):足够长的前缀让 64KB 窗口推过它
713
+ const HEAD_MARKER = "HEAD_MARKER_HEAD_MARKER";
714
+ child.stderr.write(HEAD_MARKER);
715
+ // 填充 > 64KB 的噪声(用 "x" 重复),把头部标记推出 64KB 尾部窗口
716
+ child.stderr.write("x".repeat(70 * 1024));
717
+ // 尾部标记(应在截断后仍保留)
718
+ child.stderr.write("TAIL_MARKER_TAIL_MARKER");
719
+
720
+ emitStdoutLine(child, sessionHeader());
721
+ child.stderr.end();
722
+ child.stdout.end();
723
+ child.emit("close", 1); // 非零退出 → error 含 stderr
724
+
725
+ const result = await promise;
726
+
727
+ // 非零退出码 → success=false
728
+ expect(result.success).toBe(false);
729
+ // error 含尾部标记(证明保留了尾部 64KB)
730
+ expect(result.error).toContain("TAIL_MARKER_TAIL_MARKER");
731
+ // error 不含头部标记(证明头部被截断丢弃)
732
+ expect(result.error).not.toContain(HEAD_MARKER);
733
+ });
734
+ });
735
+
736
+ // ── 14. UTF-8 编码跨 chunk 安全(R3)──
737
+ //
738
+ // [R3] child.stdout.setEncoding("utf8") 让流按字符边界切分,避免多字节 UTF-8
739
+ //(CJK/emoji)跨 chunk 时 toString() 产生 U+FFFD 替换符导致 JSON.parse 失败。
740
+ //
741
+ // 测法选择:用流状态(readableEncoding)验证 setEncoding("utf8") 被 runSpawn 调用过。
742
+ // 为什么不用 spy:runSpawn 在 spawn() 返回后同步调用 setEncoding,而 waitForSpawn 拿到
743
+ // child 时调用已完成,无法事后挂 spy 捕获。readableEncoding 是 PassThrough(Readable) 暴露的
744
+ // 只读属性,setEncoding("utf8") 后值变为 "utf8",是"编码确实被设置"的可靠证据。
745
+ // 端到端分片写多字节字符无法真正模拟"字节边界切断"(PassThrough + setEncoding 已在更高层
746
+ // 合并),故补一个"分片写入仍能解析"的用例佐证行缓冲健壮性。
747
+ describe("UTF-8 编码跨 chunk 安全 (R3)", () => {
748
+ it("runSpawn 对 child.stdout 和 child.stderr 调用 setEncoding('utf8')(readableEncoding 验证)", async () => {
749
+ const record = makeRecord();
750
+ const promise = runSpawn(record, "Task: utf8", makeOpts(), makeCtx());
751
+
752
+ await waitForSpawn();
753
+ const child = lastSpawnedChild();
754
+
755
+ // spawn 后 runSpawn 已同步调用 setEncoding("utf8"):readableEncoding 反映该调用结果
756
+ expect(child.stdout.readableEncoding).toBe("utf8");
757
+ expect(child.stderr.readableEncoding).toBe("utf8");
758
+
759
+ emitStdoutLine(child, sessionHeader());
760
+ child.stdout.end();
761
+ child.stderr.end();
762
+ child.emit("close", 0);
763
+
764
+ const result = await promise;
765
+ expect(result.success).toBe(true);
766
+ });
767
+
768
+ it("stdout 多次分片写入同一 JSON 行仍能正确解析 turn_end(行缓冲合并)", async () => {
769
+ // 端到端验证:数据分两次 write,runSpawn 的行缓冲(stdoutBuffer += data)合并后解析。
770
+ // 此用例验证"分片写入仍能解析",setEncoding("utf8") 已由上一用例覆盖。
771
+ const record = makeRecord();
772
+ const promise = runSpawn(record, "Task: split", makeOpts(), makeCtx());
773
+
774
+ await waitForSpawn();
775
+ const child = lastSpawnedChild();
776
+
777
+ // header 完整一行
778
+ emitStdoutLine(child, sessionHeader());
779
+ // turn_end 拆成两半写入(跨 chunk 模拟)
780
+ child.stdout.write('{"type":"turn_e');
781
+ child.stdout.write('nd"}\n');
782
+ child.stdout.end();
783
+ child.emit("close", 0);
784
+
785
+ const result = await promise;
786
+
787
+ expect(result.success).toBe(true);
788
+ expect(record.turnCount).toBe(1); // 拆开的 turn_end 仍被解析为 1 次
789
+ });
790
+ });
791
+
792
+ // ── 15. stdin prompt 注入 ──
793
+ //
794
+ // [RPC prompt 修复] pi runRpcMode 只通过 stdin RpcCommand 驱动——positional task arg
795
+ // / -p flag 在 rpc mode 下被 resolveAppMode 无视。runSpawn 必须在 spawn 后主动写
796
+ // {type:"prompt",message:<task>} 到 child.stdin,否则子进程阻塞、totalTokens 恒 0。
797
+ // sendPromptCommand 在 spawn + setEncoding 后同步执行,waitForSpawn 拿到 child 时
798
+ // 命令已在 stdin 缓冲。PassThrough.write 无 reader 时缓冲全部数据,可事后读出断言。
799
+ describe("stdin prompt 注入", () => {
800
+ it("spawn 后向 stdin 写一行 {type:prompt} 且 message 含 task 文本", async () => {
801
+ const record = makeRecord();
802
+ const taskText = "Task: hello-prompt-injection";
803
+ const promise = runSpawn(record, taskText, makeOpts(), makeCtx());
804
+
805
+ await waitForSpawn();
806
+ const child = lastSpawnedChild();
807
+
808
+ // sendPromptCommand 已同步执行——PassThrough 缓冲了写入的命令,读出来断言。
809
+ // pause() 让 PassThrough 切到暂停模式(默认 flow 模式下数据缓冲在内部),
810
+ // 然后 read() 取出全部已缓冲内容。
811
+ child.stdin.pause();
812
+ const buffered = child.stdin.read()?.toString() ?? "";
813
+
814
+ // 收尾:让 runSpawn resolve(避免悬挂)
815
+ emitStdoutLine(child, sessionHeader());
816
+ child.stdout.end();
817
+ child.stderr.end();
818
+ child.emit("close", 0);
819
+ await promise;
820
+
821
+ // 断言:缓冲含合法 JSON,type=prompt,message 含 task 文本
822
+ const lines = buffered.trim().split("\n");
823
+ expect(lines.length).toBeGreaterThanOrEqual(1);
824
+ const cmd = JSON.parse(lines[0]!) as { type: string; message: string; id?: string };
825
+ expect(cmd.type).toBe("prompt");
826
+ expect(cmd.message).toBe(taskText);
827
+ expect(typeof cmd.id).toBe("string");
828
+ });
829
+
830
+ it("child.stdin.destroyed → sendPromptCommand 不抛错(guard 生效)", async () => {
831
+ const record = makeRecord();
832
+ const promise = runSpawn(record, "Task: destroyed-stdin", makeOpts(), makeCtx());
833
+
834
+ await waitForSpawn();
835
+ const child = lastSpawnedChild();
836
+
837
+ // destroy stdin 模拟子进程已关闭输入通道;sendPromptCommand 在 spawn 时已执行过一次
838
+ //(stdin 当时未 destroyed),这里仅验证后续不抛。此用例主要保护 guard 逻辑——
839
+ // 收尾正常 close 即证明无异常抛出中断 runSpawn。
840
+ child.stdin.destroy();
841
+ emitStdoutLine(child, sessionHeader());
842
+ child.stdout.end();
843
+ child.stderr.end();
844
+ child.emit("close", 0);
845
+
846
+ const result = await promise;
847
+ expect(result.success).toBe(true);
848
+ });
849
+ });
850
+
851
+ // 注:C1(orphan 进程兜底)与 M8(stdout 边界)describe 块已移至 run-spawn-edges.test.ts,
852
+ // 拆分以保持本文件 < 1000 行(pre-commit hook 限制)。两文件各自独立声明文件级 mock。
853
+
854
+ // ============================================================
855
+ // E2E:runSpawn 从主进程 process.argv 镜像 extension/approve flag 到子进程
856
+ // 验证完整链路:runSpawn 真实执行 → 读 process.argv → buildSpawnArgs 拼参 →
857
+ // spawn 收到的 args 含镜像 flag。spawn 被 mock(FakeChild),但 runSpawn 体内逻辑
858
+ // 全真实跑,process.argv 用真实进程变量(测试中临时覆写 + 还原)。
859
+ // ============================================================
860
+ describe("E2E: 镜像主进程 argv flag 到子进程 spawn args", () => {
861
+ const originalArgv = process.argv;
862
+
863
+ afterEach(() => {
864
+ process.argv = originalArgv;
865
+ });
866
+
867
+ it("主进程 argv 含 --extension/--no-extensions/--approve → 子进程 spawn args 全部镜像", async () => {
868
+ // 模拟主 pi 进程启动参数(xyz-agent runtime 启动 pi 的真实形态)
869
+ process.argv = [
870
+ "bun", "/path/to/pi",
871
+ "--mode", "rpc",
872
+ "--no-extensions",
873
+ "--approve",
874
+ "--extension", "/exts/goal",
875
+ "--extension", "/exts/todo",
876
+ "--session-dir", "/sessions",
877
+ ];
878
+
879
+ const record = makeRecord();
880
+ const promise = runSpawn(record, "Task", makeOpts(), makeCtx());
881
+ await waitForSpawn();
882
+ const child = lastSpawnedChild();
883
+ mockSessionFileExists(
884
+ "/tmp/test/agents/subagents/--tmp-test--/sessions/2026-07-03T12-00-00-000Z_sess-mirror.jsonl",
885
+ );
886
+ emitStdoutLine(child, sessionHeader("sess-mirror"));
887
+ child.stdout.end();
888
+ child.stderr.end();
889
+ child.emit("close", 0);
890
+ await promise;
891
+
892
+ // 断言 spawn 收到的调用参数含全部镜像 flag
893
+ const spawnCall = mockSpawn.mock.calls[0];
894
+ // spawn(command, args, options) → args 是第二个参数
895
+ const spawnArgs = spawnCall[1] as string[];
896
+
897
+ expect(spawnArgs).toContain("--no-extensions");
898
+ expect(spawnArgs).toContain("--approve");
899
+ // 两个 extension 路径都镜像,顺序保留
900
+ const extIdxs = spawnArgs
901
+ .map((a, i) => (a === "--extension" ? i : -1))
902
+ .filter((i) => i >= 0);
903
+ expect(extIdxs).toHaveLength(2);
904
+ expect(spawnArgs[extIdxs[0] + 1]).toBe("/exts/goal");
905
+ expect(spawnArgs[extIdxs[1] + 1]).toBe("/exts/todo");
906
+ });
907
+
908
+ it("主进程 argv 无目标 flag → 子进程 spawn args 不含镜像 flag(向后兼容)", async () => {
909
+ // 模拟纯 pi CLI 直接跑(无 extension/approve 配置)
910
+ process.argv = ["bun", "/path/to/pi", "--mode", "rpc"];
911
+
912
+ const record = makeRecord();
913
+ const promise = runSpawn(record, "Task", makeOpts(), makeCtx());
914
+ await waitForSpawn();
915
+ const child = lastSpawnedChild();
916
+ mockSessionFileExists(
917
+ "/tmp/test/agents/subagents/--tmp-test--/sessions/2026-07-03T12-00-00-000Zsess-noflag.jsonl",
918
+ );
919
+ emitStdoutLine(child, sessionHeader("sess-noflag"));
920
+ child.stdout.end();
921
+ child.stderr.end();
922
+ child.emit("close", 0);
923
+ await promise;
924
+
925
+ const spawnCall = mockSpawn.mock.calls[0];
926
+ const spawnArgs = spawnCall[1] as string[];
927
+
928
+ expect(spawnArgs).not.toContain("--no-extensions");
929
+ expect(spawnArgs).not.toContain("--approve");
930
+ expect(spawnArgs).not.toContain("--extension");
931
+ });
932
+ });
933
+ });