@zhushanwen/pi-subagent-workflow 8.5.0 → 8.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/package.json +13 -16
  2. package/relay/relay.mjs +1 -1
  3. package/src/host/__tests__/pi-host.test.ts +214 -0
  4. package/src/host/pi-host.ts +109 -0
  5. package/src/index.ts +142 -32
  6. package/src/injectors/__tests__/engine-awareness.test.ts +292 -0
  7. package/src/injectors/__tests__/engine-section-stability.test.ts +341 -0
  8. package/src/injectors/__tests__/subagent-list-injector.test.ts +5 -5
  9. package/src/injectors/__tests__/workflow-list-injector.test.ts +5 -5
  10. package/src/injectors/engine-awareness.ts +123 -0
  11. package/src/injectors/model-list-injector.ts +1 -1
  12. package/src/injectors/subagent-list-injector.ts +8 -8
  13. package/src/injectors/workflow-list-injector.ts +8 -8
  14. package/src/interface/__tests__/detectors.test.ts +3 -3
  15. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
  16. package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
  17. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +2 -2
  18. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +1 -1
  19. package/src/interface/bg-notify-render.ts +34 -13
  20. package/src/interface/commands.ts +3 -3
  21. package/src/interface/format.ts +2 -2
  22. package/src/interface/helpers.ts +3 -3
  23. package/src/interface/list-component.ts +4 -4
  24. package/src/interface/list-shared.ts +2 -2
  25. package/src/interface/list-view.ts +2 -2
  26. package/src/interface/subagent-actions.ts +288 -19
  27. package/src/interface/subagent-tool-schema.ts +163 -0
  28. package/src/interface/subagent-tool.ts +65 -129
  29. package/src/interface/subagents.ts +5 -5
  30. package/src/interface/tool-render.ts +34 -2
  31. package/src/interface/tool-workflow-script.ts +4 -4
  32. package/src/interface/tool-workflow.ts +6 -6
  33. package/src/interface/views/WorkflowsView.ts +5 -5
  34. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
  35. package/src/interface/views/__tests__/detail-content-session-file.test.ts +2 -2
  36. package/src/interface/views/detail-content.ts +4 -4
  37. package/src/interface/views/format.ts +4 -4
  38. package/src/{orchestration/jsonl-run-store.ts → jsonl-run-store.ts} +132 -18
  39. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +0 -53
  40. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +0 -1
  41. package/src/execution/__tests__/agent-registry.test.ts +0 -363
  42. package/src/execution/__tests__/agent-result-mapper.test.ts +0 -150
  43. package/src/execution/__tests__/alive-store.test.ts +0 -147
  44. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  45. package/src/execution/__tests__/before-agent-start-injection.test.ts +0 -132
  46. package/src/execution/__tests__/bg-notify-render.test.ts +0 -256
  47. package/src/execution/__tests__/channel-registry-handshake.test.ts +0 -243
  48. package/src/execution/__tests__/chat-engine-routing.test.ts +0 -597
  49. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +0 -365
  50. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -190
  51. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +0 -215
  52. package/src/execution/__tests__/concurrency-pool.test.ts +0 -250
  53. package/src/execution/__tests__/config.test.ts +0 -110
  54. package/src/execution/__tests__/conversation-wiring.test.ts +0 -198
  55. package/src/execution/__tests__/crash-recovery.test.ts +0 -320
  56. package/src/execution/__tests__/delivery-methods.test.ts +0 -385
  57. package/src/execution/__tests__/dialog-queue.test.ts +0 -299
  58. package/src/execution/__tests__/epipe-fallback.test.ts +0 -241
  59. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -243
  60. package/src/execution/__tests__/execute-nesting.test.ts +0 -323
  61. package/src/execution/__tests__/execute-options-mapper.test.ts +0 -189
  62. package/src/execution/__tests__/execution-record.test.ts +0 -1284
  63. package/src/execution/__tests__/finalize-record.test.ts +0 -368
  64. package/src/execution/__tests__/finalized-marker.test.ts +0 -82
  65. package/src/execution/__tests__/format-schema-instruction.test.ts +0 -138
  66. package/src/execution/__tests__/format.test.ts +0 -458
  67. package/src/execution/__tests__/gc-timer.test.ts +0 -184
  68. package/src/execution/__tests__/get-record-for-action-restart.test.ts +0 -254
  69. package/src/execution/__tests__/gui-mode-dispatch.test.ts +0 -59
  70. package/src/execution/__tests__/helpers/mock-extension-api.ts +0 -30
  71. package/src/execution/__tests__/helpers/spawn-mock.ts +0 -238
  72. package/src/execution/__tests__/host-mode.test.ts +0 -87
  73. package/src/execution/__tests__/index-session-start-identity.test.ts +0 -371
  74. package/src/execution/__tests__/index-session-start.test.ts +0 -593
  75. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
  76. package/src/execution/__tests__/lifecycle-manager.test.ts +0 -337
  77. package/src/execution/__tests__/lifecycle-predicates.test.ts +0 -116
  78. package/src/execution/__tests__/list-component.test.ts +0 -401
  79. package/src/execution/__tests__/list-fields.test.ts +0 -109
  80. package/src/execution/__tests__/manifest-parentid.test.ts +0 -117
  81. package/src/execution/__tests__/model-resolver.test.ts +0 -413
  82. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +0 -287
  83. package/src/execution/__tests__/nested-visibility.test.ts +0 -325
  84. package/src/execution/__tests__/notifier-flush.test.ts +0 -574
  85. package/src/execution/__tests__/notifier-golden-snapshot.test.ts +0 -261
  86. package/src/execution/__tests__/one-shot-upgrade.test.ts +0 -205
  87. package/src/execution/__tests__/output-collector.test.ts +0 -61
  88. package/src/execution/__tests__/parent-child-matrix.test.ts +0 -336
  89. package/src/execution/__tests__/path-encoding.test.ts +0 -104
  90. package/src/execution/__tests__/pi-invocation.test.ts +0 -134
  91. package/src/execution/__tests__/record-store.test.ts +0 -1057
  92. package/src/execution/__tests__/records-cwd-isolation.test.ts +0 -91
  93. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +0 -339
  94. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -273
  95. package/src/execution/__tests__/relay-agent.test.ts +0 -448
  96. package/src/execution/__tests__/relay-env.test.ts +0 -42
  97. package/src/execution/__tests__/resource-policy.test.ts +0 -109
  98. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  99. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -267
  100. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -253
  101. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -644
  102. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  103. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  104. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  105. package/src/execution/__tests__/sdk-contract.test.ts +0 -273
  106. package/src/execution/__tests__/session-context-resolver.test.ts +0 -167
  107. package/src/execution/__tests__/session-file-gc.test.ts +0 -293
  108. package/src/execution/__tests__/session-pending.test.ts +0 -197
  109. package/src/execution/__tests__/session-reconstructor.test.ts +0 -379
  110. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -178
  111. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -311
  112. package/src/execution/__tests__/session-start-reaper.test.ts +0 -233
  113. package/src/execution/__tests__/spawn-args.test.ts +0 -434
  114. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -189
  115. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  116. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -207
  117. package/src/execution/__tests__/spawned-children.test.ts +0 -92
  118. package/src/execution/__tests__/startup-config-declaration.test.ts +0 -35
  119. package/src/execution/__tests__/status-refactor.test.ts +0 -345
  120. package/src/execution/__tests__/stdin-writer.test.ts +0 -463
  121. package/src/execution/__tests__/stream-sink-retirement.test.ts +0 -261
  122. package/src/execution/__tests__/subagent-service-abort.test.ts +0 -60
  123. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -629
  124. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +0 -180
  125. package/src/execution/__tests__/subagent-service.test.ts +0 -788
  126. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -310
  127. package/src/execution/__tests__/subprocess-agent-runner.test.ts +0 -506
  128. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  129. package/src/execution/__tests__/timeout-integration.test.ts +0 -397
  130. package/src/execution/__tests__/tombstone-store.test.ts +0 -73
  131. package/src/execution/__tests__/tool-action.test.ts +0 -385
  132. package/src/execution/__tests__/truncline-snapshot.test.ts +0 -81
  133. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  134. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  135. package/src/execution/__tests__/ui-channels.test.ts +0 -187
  136. package/src/execution/__tests__/ui-interaction-model.test.ts +0 -67
  137. package/src/execution/__tests__/ui-request-handler-factory.test.ts +0 -370
  138. package/src/execution/__tests__/ui-request-handler.test.ts +0 -204
  139. package/src/execution/__tests__/ui-request-observability.test.ts +0 -114
  140. package/src/execution/__tests__/ui-request-queue.test.ts +0 -133
  141. package/src/execution/__tests__/worktree-manager.test.ts +0 -633
  142. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -229
  143. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +0 -181
  144. package/src/execution/__tests__/worktree-registry.test.ts +0 -199
  145. package/src/execution/agent-registry.ts +0 -193
  146. package/src/execution/agent-result-mapper.ts +0 -90
  147. package/src/execution/alive-store.ts +0 -92
  148. package/src/execution/argv-mirror.ts +0 -109
  149. package/src/execution/best-effort.ts +0 -38
  150. package/src/execution/channel-registry-access.ts +0 -144
  151. package/src/execution/concurrency-pool.ts +0 -116
  152. package/src/execution/config.ts +0 -96
  153. package/src/execution/dialog-queue.ts +0 -330
  154. package/src/execution/engine/__tests__/common/data-dir.test.ts +0 -53
  155. package/src/execution/engine/__tests__/common/errors.test.ts +0 -132
  156. package/src/execution/engine/__tests__/common/event-journal.test.ts +0 -177
  157. package/src/execution/engine/__tests__/common/kill-chain.test.ts +0 -192
  158. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +0 -81
  159. package/src/execution/engine/__tests__/common/persona-router.test.ts +0 -123
  160. package/src/execution/engine/__tests__/common/pool-manager.test.ts +0 -154
  161. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  162. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  163. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +0 -141
  164. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +0 -109
  165. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +0 -101
  166. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +0 -77
  167. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +0 -104
  168. package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +0 -342
  169. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +0 -201
  170. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +0 -76
  171. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +0 -79
  172. package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -87
  173. package/src/execution/engine/__tests__/engines-declaration.test.ts +0 -36
  174. package/src/execution/engine/__tests__/model-prompt.test.ts +0 -85
  175. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  176. package/src/execution/engine/__tests__/registry.test.ts +0 -120
  177. package/src/execution/engine/__tests__/routing.test.ts +0 -231
  178. package/src/execution/engine/common/data-dir.ts +0 -62
  179. package/src/execution/engine/common/errors.ts +0 -183
  180. package/src/execution/engine/common/event-journal.ts +0 -254
  181. package/src/execution/engine/common/journal-replay.ts +0 -62
  182. package/src/execution/engine/common/kill-chain.ts +0 -221
  183. package/src/execution/engine/common/nesting-guard.ts +0 -50
  184. package/src/execution/engine/common/persona-router.ts +0 -108
  185. package/src/execution/engine/common/pool-manager.ts +0 -226
  186. package/src/execution/engine/common/schema-emulation.ts +0 -189
  187. package/src/execution/engine/common/session-view-projection.ts +0 -51
  188. package/src/execution/engine/engine-discovery.ts +0 -65
  189. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -469
  190. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  191. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
  192. package/src/execution/engine/engines/pi/pi-engine.ts +0 -415
  193. package/src/execution/engine/engines/pi/reader.ts +0 -48
  194. package/src/execution/engine/engines/pi/registration.ts +0 -35
  195. package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
  196. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
  197. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
  198. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -246
  199. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -228
  200. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  201. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -64
  202. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -127
  203. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -567
  204. package/src/execution/engine/engines/zcode/constants.ts +0 -43
  205. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -39
  206. package/src/execution/engine/engines/zcode/launcher.ts +0 -161
  207. package/src/execution/engine/engines/zcode/parser.ts +0 -436
  208. package/src/execution/engine/engines/zcode/preparer.ts +0 -363
  209. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  210. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  211. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -648
  212. package/src/execution/engine/host-task-spec.ts +0 -47
  213. package/src/execution/engine/model-prompt.ts +0 -59
  214. package/src/execution/engine/paths.ts +0 -42
  215. package/src/execution/engine/port.ts +0 -153
  216. package/src/execution/engine/registry.ts +0 -123
  217. package/src/execution/engine/routing.ts +0 -218
  218. package/src/execution/engine/types.ts +0 -304
  219. package/src/execution/execute-options-mapper.ts +0 -111
  220. package/src/execution/execution-record.ts +0 -906
  221. package/src/execution/finalize-record.ts +0 -251
  222. package/src/execution/finalized-marker.ts +0 -51
  223. package/src/execution/get-state-handshake.ts +0 -104
  224. package/src/execution/host-mode.ts +0 -56
  225. package/src/execution/idle-gc.ts +0 -47
  226. package/src/execution/lifecycle-manager.ts +0 -491
  227. package/src/execution/lifecycle-predicates.ts +0 -65
  228. package/src/execution/manifest-store.ts +0 -256
  229. package/src/execution/model-config-service.ts +0 -204
  230. package/src/execution/model-resolver.ts +0 -276
  231. package/src/execution/notifier.ts +0 -260
  232. package/src/execution/output-collector.ts +0 -88
  233. package/src/execution/path-encoding.ts +0 -60
  234. package/src/execution/pi-invocation.ts +0 -120
  235. package/src/execution/record-entry.ts +0 -132
  236. package/src/execution/record-store.ts +0 -1234
  237. package/src/execution/relay-env.ts +0 -37
  238. package/src/execution/session-context-resolver.ts +0 -64
  239. package/src/execution/session-file-gc.ts +0 -120
  240. package/src/execution/session-pending.ts +0 -164
  241. package/src/execution/session-reconstructor.ts +0 -678
  242. package/src/execution/session-runner.ts +0 -1548
  243. package/src/execution/sessions-index.ts +0 -304
  244. package/src/execution/spawn-event-adapter.ts +0 -363
  245. package/src/execution/stdin-writer.ts +0 -198
  246. package/src/execution/stream-sink.ts +0 -126
  247. package/src/execution/subagent-service.ts +0 -2119
  248. package/src/execution/subprocess-agent-runner.ts +0 -303
  249. package/src/execution/temp-prompt.ts +0 -62
  250. package/src/execution/tombstone-store.ts +0 -72
  251. package/src/execution/turn-limiter.ts +0 -102
  252. package/src/execution/types.ts +0 -910
  253. package/src/execution/ui-channels.ts +0 -216
  254. package/src/execution/ui-interaction-model.ts +0 -48
  255. package/src/execution/ui-request-handler-factory.ts +0 -212
  256. package/src/execution/ui-request-observability.ts +0 -81
  257. package/src/execution/ui-request-queue.ts +0 -174
  258. package/src/execution/worktree-manager.ts +0 -688
  259. package/src/execution/worktree-registry.ts +0 -222
  260. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +0 -336
  261. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -208
  262. package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -157
  263. package/src/orchestration/__tests__/args-validator.test.ts +0 -143
  264. package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
  265. package/src/orchestration/__tests__/config-loader.test.ts +0 -503
  266. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +0 -724
  267. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +0 -405
  268. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +0 -56
  269. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +0 -166
  270. package/src/orchestration/__tests__/execute-agent-call.test.ts +0 -314
  271. package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +0 -171
  272. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +0 -1030
  273. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -277
  274. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +0 -96
  275. package/src/orchestration/__tests__/lifecycle.test.ts +0 -625
  276. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +0 -1971
  277. package/src/orchestration/__tests__/script-lint.test.ts +0 -831
  278. package/src/orchestration/__tests__/skill-discovery.test.ts +0 -157
  279. package/src/orchestration/__tests__/test-mocks.ts +0 -197
  280. package/src/orchestration/__tests__/worker-host.test.ts +0 -120
  281. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +0 -164
  282. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +0 -520
  283. package/src/orchestration/__tests__/worker-script-builder.test.ts +0 -309
  284. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +0 -110
  285. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -317
  286. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +0 -110
  287. package/src/orchestration/__tests__/workflows-e2e.test.ts +0 -537
  288. package/src/orchestration/agent-opts-resolver.ts +0 -93
  289. package/src/orchestration/args-validator.ts +0 -127
  290. package/src/orchestration/concurrency-gate.ts +0 -69
  291. package/src/orchestration/config-loader.ts +0 -315
  292. package/src/orchestration/error-recovery.ts +0 -862
  293. package/src/orchestration/execute-agent-call.ts +0 -217
  294. package/src/orchestration/launcher.ts +0 -410
  295. package/src/orchestration/lifecycle.ts +0 -396
  296. package/src/orchestration/models/__tests__/budget.test.ts +0 -367
  297. package/src/orchestration/models/__tests__/trace.test.ts +0 -408
  298. package/src/orchestration/models/agent-call.ts +0 -83
  299. package/src/orchestration/models/budget.ts +0 -148
  300. package/src/orchestration/models/ports.ts +0 -164
  301. package/src/orchestration/models/run-runtime.ts +0 -89
  302. package/src/orchestration/models/run-spec.ts +0 -83
  303. package/src/orchestration/models/run-state.ts +0 -44
  304. package/src/orchestration/models/trace.ts +0 -183
  305. package/src/orchestration/models/types.ts +0 -292
  306. package/src/orchestration/models/workflow-run.ts +0 -254
  307. package/src/orchestration/models/workflow-script-registry.ts +0 -35
  308. package/src/orchestration/models/workflow-script.ts +0 -117
  309. package/src/orchestration/script-lint.ts +0 -766
  310. package/src/orchestration/skill-discovery.ts +0 -92
  311. package/src/orchestration/worker-handle.ts +0 -115
  312. package/src/orchestration/worker-host.ts +0 -98
  313. package/src/orchestration/worker-script-builder.ts +0 -408
  314. package/src/orchestration/workflow-files.ts +0 -85
  315. package/src/orchestration/workflow-script-registry-impl.ts +0 -133
  316. package/src/shared/__tests__/agent-ref.test.ts +0 -34
  317. package/src/shared/__tests__/meta-parser.test.ts +0 -304
  318. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -280
  319. package/src/shared/__tests__/resource-discovery.test.ts +0 -736
  320. package/src/shared/__tests__/resource-meta.test.ts +0 -51
  321. package/src/shared/__tests__/schema-jsonify.test.ts +0 -81
  322. package/src/shared/agent-event.ts +0 -13
  323. package/src/shared/agent-ref.ts +0 -57
  324. package/src/shared/meta-parser.ts +0 -261
  325. package/src/shared/resource-discovery.ts +0 -811
  326. package/src/shared/resource-meta.ts +0 -65
  327. package/src/shared/schema-jsonify.ts +0 -56
  328. package/src/shared/xml-injection.ts +0 -35
  329. package/workflows/README.md +0 -81
  330. package/workflows/_shared/agent-refs.cjs +0 -40
  331. package/workflows/chain.js +0 -131
  332. package/workflows/map-reduce.js +0 -174
  333. package/workflows/parallel.js +0 -148
  334. package/workflows/review-fix-loop-utils.cjs +0 -1307
  335. package/workflows/review-fix-loop.js +0 -1396
  336. package/workflows/scatter-gather.js +0 -178
@@ -1,933 +0,0 @@
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
- });