@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,612 @@
1
+ // src/execution/__tests__/subprocess-agent-runner.test.ts
2
+ //
3
+ // Wave 4: SubprocessAgentRunner 委托重写测试
4
+ //
5
+ // 覆盖 test-matrix 用例:
6
+ // T3.1 (正常): SAR 委托 executeAndAwait 返回 content
7
+ // T3.2 (正常): parsedOutput 透传
8
+ // T3.4 (边界): cwd 透传(非 git worktree)
9
+ // T3.5 (边界): model 填底(opts.model 空 → ctxModel)
10
+ // T3.6 (异常): timeoutMs 超时 → signal abort → error
11
+ // T3.7 (正常): onEvent 桥接 AgentEvent 透传
12
+ // T3.17 (NFR): mergeTimeoutSignal listener 清理
13
+ // T3.18 (NFR): dispose 兜底覆盖(delegate 后子进程进 spawnedChildren)
14
+ // T3.19 (NFR): AgentCallOpts→ExecuteOptions 映射保真
15
+
16
+ import { mkdtempSync, readdirSync, rmSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+
20
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
21
+
22
+ // u0-data-discovery 注入化:getEngineDataDir 回退段经 HostServices.dataRoot() 端口,
23
+ // 未 configureCore 即消费会抛 core_host_not_configured(SAR.run 内部调用,dataRoot
24
+ // 值不被本文件断言)——端口态显式配置,测试隔离经 resetCoreForTests
25
+ import { configureCore, resetCoreForTests } from "../../core/host-services.ts";
26
+
27
+ import type { AgentCallOpts, AgentResult } from "../../orchestration/models/types.ts";
28
+ import { ModelConfigService, setModelConfigService } from "../model-config-service.ts";
29
+ import type { ModelRegistryLike } from "../model-resolver.ts";
30
+ import { replayJournal } from "../engine/common/event-journal.ts";
31
+ import type { SubprocessAgentRunnerDeps } from "../subprocess-agent-runner.ts";
32
+ import { SubprocessAgentRunner } from "../subprocess-agent-runner.ts";
33
+
34
+ // ── 测试辅助 ──
35
+
36
+ /** 构造 mock SubagentService.executeAndAwait 返回值 */
37
+ function makeMockResult(overrides: Partial<AgentResult> = {}): AgentResult {
38
+ return {
39
+ content: "OK",
40
+ parsedOutput: undefined,
41
+ durationMs: 100,
42
+ error: undefined,
43
+ sessionId: undefined,
44
+ toolCalls: [],
45
+ ...overrides,
46
+ };
47
+ }
48
+
49
+ /** 创建 mock SubagentService(只实现 executeAndAwait) */
50
+ function createMockService(impl?: typeof vi.fn) {
51
+ const executeAndAwait = impl ?? vi.fn().mockResolvedValue(makeMockResult());
52
+ return { executeAndAwait } as unknown as {
53
+ executeAndAwait: (
54
+ opts: Record<string, unknown>,
55
+ signal?: AbortSignal,
56
+ onEvent?: (e: Record<string, unknown>) => void,
57
+ stream?: unknown,
58
+ ) => Promise<AgentResult>;
59
+ };
60
+ }
61
+
62
+ function makeBaseOpts(): AgentCallOpts {
63
+ return {
64
+ prompt: "test task",
65
+ agent: "worker",
66
+ cwd: "/some/path",
67
+ schema: undefined,
68
+ model: undefined,
69
+ scene: undefined,
70
+ description: undefined,
71
+ timeoutMs: undefined,
72
+ skill: undefined,
73
+ skillPath: undefined,
74
+ appendSystemPrompt: undefined,
75
+ schemaEnv: undefined,
76
+ };
77
+ }
78
+
79
+ // ── T3.1: 正常路径 ──
80
+
81
+ describe("SubprocessAgentRunner (wave-4 delegate)", () => {
82
+ beforeEach(() => {
83
+ configureCore({ dataRoot: () => "/fake-sar-data-root", log: () => {} });
84
+ });
85
+
86
+ afterEach(() => {
87
+ resetCoreForTests();
88
+ });
89
+
90
+ // ────────────────────────────────────────────────
91
+ // T3.1: 正常路径 — SAR 委托 executeAndAwait 返回 content
92
+ // ────────────────────────────────────────────────
93
+ describe("T3.1 主流程", () => {
94
+ it("委托 executeAndAwait 并返回 content", async () => {
95
+ const mockService = createMockService(
96
+ vi.fn().mockResolvedValue(makeMockResult({ content: "hello world" })),
97
+ );
98
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
99
+ const sar = new SubprocessAgentRunner(deps);
100
+
101
+ const opts = makeBaseOpts();
102
+ const signal = new AbortController().signal;
103
+
104
+ const result = await sar.run(opts, signal);
105
+
106
+ expect(result.content).toBe("hello world");
107
+ expect(mockService.executeAndAwait).toHaveBeenCalledTimes(1);
108
+ });
109
+
110
+ it("不 reject — 失败信息入 result.error", async () => {
111
+ const mockService = createMockService(
112
+ vi.fn().mockResolvedValue(makeMockResult({ error: "some error" })),
113
+ );
114
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
115
+ const sar = new SubprocessAgentRunner(deps);
116
+
117
+ const result = await sar.run(makeBaseOpts(), new AbortController().signal);
118
+ expect(result.error).toBe("some error");
119
+ });
120
+ });
121
+
122
+ // ────────────────────────────────────────────────
123
+ // T3.2: parsedOutput 透传
124
+ // ────────────────────────────────────────────────
125
+ describe("T3.2 parsedOutput 透传", () => {
126
+ it("parsedOutput 直通", async () => {
127
+ const parsedData = { x: 1, y: 2 };
128
+ const mockService = createMockService(
129
+ vi.fn().mockResolvedValue(makeMockResult({ content: "ok", parsedOutput: parsedData })),
130
+ );
131
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
132
+ const sar = new SubprocessAgentRunner(deps);
133
+
134
+ const result = await sar.run(makeBaseOpts(), new AbortController().signal);
135
+ expect(result.parsedOutput).toEqual(parsedData);
136
+ });
137
+ });
138
+
139
+ // ────────────────────────────────────────────────
140
+ // T3.4: cwd 透传
141
+ // ────────────────────────────────────────────────
142
+ describe("T3.4 cwd 透传", () => {
143
+ it("cwd 传入 executeAndAwait 的 ExecuteOptions", async () => {
144
+ let capturedOpts: Record<string, unknown> | undefined;
145
+ const mockService = createMockService(
146
+ vi.fn().mockImplementation((opts: Record<string, unknown>) => {
147
+ capturedOpts = opts;
148
+ return Promise.resolve(makeMockResult());
149
+ }),
150
+ );
151
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
152
+ const sar = new SubprocessAgentRunner(deps);
153
+
154
+ const opts = { ...makeBaseOpts(), cwd: "/custom/cwd" };
155
+ await sar.run(opts, new AbortController().signal);
156
+
157
+ expect(capturedOpts).toBeDefined();
158
+ expect(capturedOpts!.cwd).toBe("/custom/cwd");
159
+ });
160
+ });
161
+
162
+ // ────────────────────────────────────────────────
163
+ // T3.5: model 填底
164
+ // ────────────────────────────────────────────────
165
+ describe("T3.5 model 填底 (D-008)", () => {
166
+ it("opts.model 优先", async () => {
167
+ let capturedOpts: Record<string, unknown> | undefined;
168
+ const mockService = createMockService(
169
+ vi.fn().mockImplementation((opts: Record<string, unknown>) => {
170
+ capturedOpts = opts;
171
+ return Promise.resolve(makeMockResult());
172
+ }),
173
+ );
174
+ const ctxModel = { id: "ctx-model", provider: "test", input: [] };
175
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService, ctxModel };
176
+ const sar = new SubprocessAgentRunner(deps);
177
+
178
+ const opts = { ...makeBaseOpts(), model: "explicit-model" };
179
+ await sar.run(opts, new AbortController().signal);
180
+
181
+ expect(capturedOpts!.model).toBe("explicit-model");
182
+ });
183
+
184
+ it("opts.model 空 → ctxModel", async () => {
185
+ let capturedOpts: Record<string, unknown> | undefined;
186
+ const mockService = createMockService(
187
+ vi.fn().mockImplementation((opts: Record<string, unknown>) => {
188
+ capturedOpts = opts;
189
+ return Promise.resolve(makeMockResult());
190
+ }),
191
+ );
192
+ const ctxModel = { id: "ctx-model", provider: "test", input: [] };
193
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService, ctxModel };
194
+ const sar = new SubprocessAgentRunner(deps);
195
+
196
+ const opts = { ...makeBaseOpts(), model: undefined };
197
+ await sar.run(opts, new AbortController().signal);
198
+
199
+ // 修复后:opts.model 不再从 ctxModel.id 填底,ctxModel 作为完整对象透传
200
+ expect(capturedOpts!.model).toBeUndefined();
201
+ expect(capturedOpts!.ctxModel).toBe(ctxModel);
202
+ });
203
+ });
204
+
205
+ // ────────────────────────────────────────────────
206
+ // [U1 D2] RunContext.modelRef 接入:ctxModel 继承路径孪生守卫
207
+ // ────────────────────────────────────────────────
208
+ describe("U1 ctxModel twin guard (RunContext.modelRef 接入)", () => {
209
+ const prevDataDir = process.env["XYZ_AGENT_DATA_DIR"];
210
+ let tmpRoot = "";
211
+
212
+ /** 装配含指定快照的 ModelConfigService 单例(initModel 注入 registry)。 */
213
+ function installModelService(registry: ModelRegistryLike): void {
214
+ tmpRoot = mkdtempSync(join(tmpdir(), "sar-model-ref-"));
215
+ process.env["XYZ_AGENT_DATA_DIR"] = join(tmpRoot, "engine-data");
216
+ const svc = new ModelConfigService({ agentDir: join(tmpRoot, "agent"), cwd: tmpRoot });
217
+ svc.initModel({ modelRegistry: registry, sessionId: "sar-guard" });
218
+ setModelConfigService(svc);
219
+ }
220
+
221
+ afterEach(() => {
222
+ // 单例 slot 无 reset API,重装一个空注册服务隔离后续用例(同文件后续
223
+ // describe 不消费 ctxModel 守卫,getGlobalConfig 对空目录返回默认值)
224
+ if (tmpRoot) {
225
+ const svc = new ModelConfigService({ agentDir: join(tmpRoot, "agent"), cwd: tmpRoot });
226
+ svc.initModel({ modelRegistry: { getAvailable: () => [], find: () => undefined, hasConfiguredAuth: () => false }, sessionId: "cleanup" });
227
+ setModelConfigService(svc);
228
+ rmSync(tmpRoot, { recursive: true, force: true });
229
+ tmpRoot = "";
230
+ }
231
+ if (prevDataDir === undefined) delete process.env["XYZ_AGENT_DATA_DIR"];
232
+ else process.env["XYZ_AGENT_DATA_DIR"] = prevDataDir;
233
+ });
234
+
235
+ it("P-A2 拒单路径:ctxModel 命中但 registry 含孪生 → run 返回 error(ambiguous),executeAndAwait 未被调", async () => {
236
+ const models = [
237
+ { id: "GLM-5.3-Flash", name: "flash", provider: "zai-coding-cn", reasoning: false },
238
+ { id: "glm-5.3-flash", name: "flash lower", provider: "zai-coding-cn", reasoning: false },
239
+ ];
240
+ installModelService({
241
+ getAvailable: () => models,
242
+ find: (p, id) => models.find((m) => m.provider === p && m.id === id),
243
+ hasConfiguredAuth: () => true,
244
+ });
245
+ const mockService = createMockService();
246
+ const sar = new SubprocessAgentRunner({
247
+ subagentService: mockService as unknown as SubprocessAgentRunnerDeps["subagentService"],
248
+ ctxModel: { id: "GLM-5.3-Flash", name: "flash", provider: "zai-coding-cn", reasoning: false },
249
+ });
250
+
251
+ const result = await sar.run(makeBaseOpts(), new AbortController().signal);
252
+
253
+ expect(result.error).toMatch(/ambiguous case variants/);
254
+ expect(result.content).toBe("");
255
+ // 守卫在 engine.run 之前:不产生任何 record/spawn(委托零发生)
256
+ expect(mockService.executeAndAwait).not.toHaveBeenCalled();
257
+ });
258
+
259
+ it("P-A2 放行路径:无孪生 registry 下同 ctxModel 正常委托(守卫零误伤)", async () => {
260
+ const models = [
261
+ { id: "GLM-5.3-Flash", name: "flash", provider: "zai-coding-cn", reasoning: false },
262
+ ];
263
+ installModelService({
264
+ getAvailable: () => models,
265
+ find: (p, id) => models.find((m) => m.provider === p && m.id === id),
266
+ hasConfiguredAuth: () => true,
267
+ });
268
+ const mockService = createMockService(
269
+ vi.fn().mockResolvedValue(makeMockResult({ content: "ok" })),
270
+ );
271
+ const sar = new SubprocessAgentRunner({
272
+ subagentService: mockService as unknown as SubprocessAgentRunnerDeps["subagentService"],
273
+ ctxModel: { id: "GLM-5.3-Flash", name: "flash", provider: "zai-coding-cn", reasoning: false },
274
+ });
275
+
276
+ const result = await sar.run(makeBaseOpts(), new AbortController().signal);
277
+
278
+ expect(result.error).toBeUndefined();
279
+ expect(result.content).toBe("ok");
280
+ expect(mockService.executeAndAwait).toHaveBeenCalledTimes(1);
281
+ });
282
+
283
+ it("ctxModel 未指定时守卫跳过(无单例也不阻断)", async () => {
284
+ const mockService = createMockService(
285
+ vi.fn().mockResolvedValue(makeMockResult({ content: "ok" })),
286
+ );
287
+ const sar = new SubprocessAgentRunner({
288
+ subagentService: mockService as unknown as SubprocessAgentRunnerDeps["subagentService"],
289
+ });
290
+ const result = await sar.run(makeBaseOpts(), new AbortController().signal);
291
+ expect(result.content).toBe("ok");
292
+ expect(mockService.executeAndAwait).toHaveBeenCalledTimes(1);
293
+ });
294
+ });
295
+
296
+ // ────────────────────────────────────────────────
297
+ // H1: SAR ctxModel 刷新(model_select 后不再 stale)
298
+ // ────────────────────────────────────────────────
299
+ describe("H1 ctxModel refresh via updateCtxModel", () => {
300
+ it("updateCtxModel 后 run() 传入新的 ctxModel 而非旧值", async () => {
301
+ let capturedOpts: Record<string, unknown> | undefined;
302
+ const mockService = createMockService(
303
+ vi.fn().mockImplementation((opts: Record<string, unknown>) => {
304
+ capturedOpts = opts;
305
+ return Promise.resolve(makeMockResult());
306
+ }),
307
+ );
308
+ const oldModel = { id: "old-model", provider: "test", input: [] };
309
+ const newModel = { id: "new-model", provider: "test", input: [] };
310
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService, ctxModel: oldModel };
311
+ const sar = new SubprocessAgentRunner(deps);
312
+
313
+ // 模拟 model_select:刷新 ctxModel
314
+ sar.updateCtxModel(newModel);
315
+
316
+ const opts = { ...makeBaseOpts(), model: undefined };
317
+ await sar.run(opts, new AbortController().signal);
318
+
319
+ expect(capturedOpts!.ctxModel).toBe(newModel);
320
+ expect(capturedOpts!.ctxModel).not.toBe(oldModel);
321
+ });
322
+ });
323
+
324
+ // ────────────────────────────────────────────────
325
+ // T3.6: timeoutMs 超时 → signal abort → error
326
+ // ────────────────────────────────────────────────
327
+ describe("T3.6 timeoutMs 超时", () => {
328
+ it("timeoutMs > 0 → merged signal 传给 executeAndAwait", async () => {
329
+ let capturedSignal: AbortSignal | undefined;
330
+ const mockService = createMockService(
331
+ vi.fn().mockImplementation((_opts: Record<string, unknown>, signal?: AbortSignal) => {
332
+ capturedSignal = signal;
333
+ return Promise.resolve(makeMockResult());
334
+ }),
335
+ );
336
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
337
+ const sar = new SubprocessAgentRunner(deps);
338
+
339
+ const opts = { ...makeBaseOpts(), timeoutMs: 5000 };
340
+ await sar.run(opts, new AbortController().signal);
341
+
342
+ // 有 timeoutMs 时 merged signal 不等于原始 signal
343
+ expect(capturedSignal).toBeDefined();
344
+ });
345
+
346
+ it("timeoutMs 到期 → merged signal.aborted=true", async () => {
347
+ vi.useFakeTimers();
348
+ let capturedSignal: AbortSignal | undefined;
349
+ const mockService = createMockService(
350
+ vi.fn().mockImplementation((_opts: Record<string, unknown>, signal?: AbortSignal) => {
351
+ capturedSignal = signal;
352
+ return Promise.resolve(makeMockResult());
353
+ }),
354
+ );
355
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
356
+ const sar = new SubprocessAgentRunner(deps);
357
+
358
+ const opts = { ...makeBaseOpts(), timeoutMs: 50 };
359
+ sar.run(opts, new AbortController().signal);
360
+
361
+ // 推进 51ms 后 merged signal 应 abort
362
+ vi.advanceTimersByTime(51);
363
+
364
+ expect(capturedSignal?.aborted).toBe(true);
365
+ vi.useRealTimers();
366
+ });
367
+ });
368
+
369
+ // ────────────────────────────────────────────────
370
+ // T3.7: onEvent 桥接
371
+ // ────────────────────────────────────────────────
372
+ describe("T3.7 onEvent 桥接", () => {
373
+ it("executeAndAwait 的 onEvent 透传到 workflow onEvent", async () => {
374
+ const mockService = createMockService(
375
+ vi.fn().mockImplementation(
376
+ (_opts: Record<string, unknown>, _signal?: AbortSignal, onEvent?: (e: Record<string, unknown>) => void) => {
377
+ // 模拟 executeAndAwait 触发 onEvent
378
+ onEvent?.({ type: "tool_start", toolName: "read", args: { path: "/a.txt" } });
379
+ return Promise.resolve(makeMockResult());
380
+ },
381
+ ),
382
+ );
383
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
384
+ const sar = new SubprocessAgentRunner(deps);
385
+
386
+ const workflowOnEvent = vi.fn();
387
+ await sar.run(makeBaseOpts(), new AbortController().signal, workflowOnEvent);
388
+
389
+ // workflow 的 onEvent 应被调用
390
+ expect(workflowOnEvent).toHaveBeenCalledTimes(1);
391
+ expect(workflowOnEvent).toHaveBeenCalledWith({
392
+ type: "tool_start",
393
+ toolName: "read",
394
+ args: { path: "/a.txt" },
395
+ });
396
+ });
397
+
398
+ it("无 workflow onEvent → 引擎仍收到包装 onEvent(P2 journal 通道恒开)", async () => {
399
+ let engineOnEvent: ((e: Record<string, unknown>) => void) | undefined;
400
+ const mockService = createMockService(
401
+ vi.fn().mockImplementation(
402
+ (_opts: Record<string, unknown>, _signal?: AbortSignal, onEvent?: (e: Record<string, unknown>) => void) => {
403
+ engineOnEvent = onEvent;
404
+ return Promise.resolve(makeMockResult());
405
+ },
406
+ ),
407
+ );
408
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
409
+ const sar = new SubprocessAgentRunner(deps);
410
+
411
+ await sar.run(makeBaseOpts(), new AbortController().signal);
412
+
413
+ // P2 接线(设计 D6 第②级):SAR 包装 onEvent 写 journal 后转发——原 onEvent
414
+ // 未传时也恒传包装版(全引擎免费获得 journal;P1「undefined 透传」细节已被
415
+ // 有意变更,本用例锁定新语义:引擎侧 onEvent 通道恒开)
416
+ expect(typeof engineOnEvent).toBe("function");
417
+ // 包装版不向外抛(journal append 是同步入队)
418
+ expect(() => engineOnEvent?.({ type: "turn_end" })).not.toThrow();
419
+ });
420
+
421
+ it("P2 journal 接线:run 期间事件落盘 journal-<taskId>.jsonl(中立格式,可重放)", async () => {
422
+ vi.useFakeTimers({ toFake: [] }); // 真实 timers:journal 写盘走真实 fs
423
+ const dataDir = mkdtempSync(join(tmpdir(), "sar-journal-test-"));
424
+ const prevEnv = process.env.XYZ_AGENT_DATA_DIR;
425
+ process.env.XYZ_AGENT_DATA_DIR = dataDir;
426
+ try {
427
+ const mockService = createMockService(
428
+ vi.fn().mockImplementation(
429
+ (_opts: Record<string, unknown>, _signal?: AbortSignal, onEvent?: (e: Record<string, unknown>) => void) => {
430
+ onEvent?.({ type: "tool_start", toolName: "bash", args: { cmd: "ls" } });
431
+ onEvent?.({ type: "turn_end" });
432
+ return Promise.resolve(makeMockResult());
433
+ },
434
+ ),
435
+ );
436
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
437
+ const sar = new SubprocessAgentRunner(deps);
438
+
439
+ const workflowOnEvent = vi.fn();
440
+ await sar.run(makeBaseOpts(), new AbortController().signal, workflowOnEvent);
441
+
442
+ // journal 落在 <dataDir>/engines/pi/shared/journal-<taskId>.jsonl
443
+ const journalFile = join(dataDir, "engines", "pi", "shared");
444
+ const files = readdirSync(journalFile).filter((f) => f.startsWith("journal-") && f.endsWith(".jsonl"));
445
+ expect(files.length).toBe(1);
446
+ const replayed = replayJournal(join(journalFile, files[0] ?? ""));
447
+ expect(replayed).toEqual([
448
+ { type: "tool_start", toolName: "bash", args: { cmd: "ls" } },
449
+ { type: "turn_end" },
450
+ ]);
451
+ // workflow onEvent 照常透传(journal 包装不吞事件)
452
+ expect(workflowOnEvent).toHaveBeenCalledTimes(2);
453
+ } finally {
454
+ if (prevEnv === undefined) delete process.env.XYZ_AGENT_DATA_DIR;
455
+ else process.env.XYZ_AGENT_DATA_DIR = prevEnv;
456
+ rmSync(dataDir, { recursive: true, force: true });
457
+ }
458
+ });
459
+ });
460
+
461
+ // ────────────────────────────────────────────────
462
+ // T3.17: mergeTimeoutSignal listener 清理
463
+ // ────────────────────────────────────────────────
464
+ describe("T3.17 mergeTimeoutSignal listener 清理", () => {
465
+ it("外部 signal abort → timer 清理", async () => {
466
+ vi.useFakeTimers();
467
+ const mockService = createMockService(
468
+ vi.fn().mockResolvedValue(makeMockResult()),
469
+ );
470
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
471
+ const sar = new SubprocessAgentRunner(deps);
472
+
473
+ const ctrl = new AbortController();
474
+ const opts = { ...makeBaseOpts(), timeoutMs: 5000 };
475
+
476
+ // 启动 run(不 await,让它在后台)
477
+ const runPromise = sar.run(opts, ctrl.signal);
478
+
479
+ // 外部 abort
480
+ ctrl.abort();
481
+
482
+ await runPromise;
483
+
484
+ // 推进时间——timer 应已清理,不会造成副作用
485
+ vi.advanceTimersByTime(6000);
486
+ // 无异常 = listener 已正确清理
487
+ vi.useRealTimers();
488
+ });
489
+ });
490
+
491
+ // ────────────────────────────────────────────────
492
+ // T3.19: AgentCallOpts → ExecuteOptions 映射保真
493
+ // ────────────────────────────────────────────────
494
+ describe("T3.19 映射保真", () => {
495
+ it("prompt → task, agent → agent, schemaEnv 透传", async () => {
496
+ let capturedOpts: Record<string, unknown> | undefined;
497
+ const mockService = createMockService(
498
+ vi.fn().mockImplementation((opts: Record<string, unknown>) => {
499
+ capturedOpts = opts;
500
+ return Promise.resolve(makeMockResult());
501
+ }),
502
+ );
503
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
504
+ const sar = new SubprocessAgentRunner(deps);
505
+
506
+ const opts: AgentCallOpts = {
507
+ ...makeBaseOpts(),
508
+ prompt: "do the thing",
509
+ agent: "code-reviewer",
510
+ schemaEnv: '{"type":"object"}',
511
+ skillPath: "/skills/code-review.md",
512
+ };
513
+ await sar.run(opts, new AbortController().signal);
514
+
515
+ expect(capturedOpts).toBeDefined();
516
+ expect(capturedOpts!.task).toBe("do the thing");
517
+ expect(capturedOpts!.agent).toBe("code-reviewer");
518
+ expect(capturedOpts!.skillPath).toBe("/skills/code-review.md");
519
+ // schemaEnv 应通过 ExecuteOptions 透传
520
+ expect((capturedOpts as Record<string, unknown>).schemaEnv).toBe('{"type":"object"}');
521
+ });
522
+
523
+ it("schema 透传为原始对象", async () => {
524
+ let capturedOpts: Record<string, unknown> | undefined;
525
+ const mockService = createMockService(
526
+ vi.fn().mockImplementation((opts: Record<string, unknown>) => {
527
+ capturedOpts = opts;
528
+ return Promise.resolve(makeMockResult());
529
+ }),
530
+ );
531
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
532
+ const sar = new SubprocessAgentRunner(deps);
533
+
534
+ const opts = {
535
+ ...makeBaseOpts(),
536
+ schema: { type: "object", properties: { name: { type: "string" } } },
537
+ };
538
+ await sar.run(opts, new AbortController().signal);
539
+
540
+ expect(capturedOpts!.schema).toEqual({
541
+ type: "object",
542
+ properties: { name: { type: "string" } },
543
+ });
544
+ });
545
+ });
546
+
547
+ // ────────────────────────────────────────────────
548
+ // T3.18: executeAndAwait throw → catch → error
549
+ // ────────────────────────────────────────────────
550
+ describe("T3.18 异常处理", () => {
551
+ it("executeAndAwait throw → 不 reject,返回 error", async () => {
552
+ const mockService = createMockService(
553
+ vi.fn().mockRejectedValue(new Error("nesting depth exceeded")),
554
+ );
555
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
556
+ const sar = new SubprocessAgentRunner(deps);
557
+
558
+ const result = await sar.run(makeBaseOpts(), new AbortController().signal);
559
+ expect(result.error).toContain("nesting depth exceeded");
560
+ expect(result.content).toBe("");
561
+ });
562
+
563
+ it("非 Error throw → error 字段含 message", async () => {
564
+ const mockService = createMockService(
565
+ vi.fn().mockRejectedValue("raw string error"),
566
+ );
567
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
568
+ const sar = new SubprocessAgentRunner(deps);
569
+
570
+ const result = await sar.run(makeBaseOpts(), new AbortController().signal);
571
+ expect(result.error).toBe("raw string error");
572
+ });
573
+ });
574
+
575
+ // ────────────────────────────────────────────────
576
+ // U1: stream 透传给 executeAndAwait
577
+ // ────────────────────────────────────────────────
578
+ describe("U1 stream 透传", () => {
579
+ it("SAR.run 传 stream → executeAndAwait 第 4 参收到同一 stream 对象", async () => {
580
+ let capturedStream: unknown;
581
+ const mockService = createMockService(
582
+ vi.fn().mockImplementation((_opts, _sig, _onEvt, stream) => {
583
+ capturedStream = stream;
584
+ return Promise.resolve(makeMockResult());
585
+ }),
586
+ );
587
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
588
+ const sar = new SubprocessAgentRunner(deps);
589
+
590
+ const fakeStream = { onDelta: vi.fn(), dispose: vi.fn() };
591
+ await sar.run(makeBaseOpts(), new AbortController().signal, undefined, fakeStream as never);
592
+
593
+ expect(capturedStream).toBe(fakeStream);
594
+ });
595
+
596
+ it("SAR.run 不传 stream → executeAndAwait 第 4 参为 undefined", async () => {
597
+ let capturedStream: unknown = "sentinel";
598
+ const mockService = createMockService(
599
+ vi.fn().mockImplementation((_opts, _sig, _onEvt, stream) => {
600
+ capturedStream = stream;
601
+ return Promise.resolve(makeMockResult());
602
+ }),
603
+ );
604
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
605
+ const sar = new SubprocessAgentRunner(deps);
606
+
607
+ await sar.run(makeBaseOpts(), new AbortController().signal);
608
+
609
+ expect(capturedStream).toBeUndefined();
610
+ });
611
+ });
612
+ });
@@ -0,0 +1,53 @@
1
+ // src/__tests__/temp-prompt.test.ts
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+
5
+ import { describe, expect, it } from "vitest";
6
+
7
+ import { cleanupTempPrompt, writePromptToTempFile } from "../temp-prompt.ts";
8
+
9
+ describe("writePromptToTempFile", () => {
10
+ it("创建临时目录 + 写入 prompt 文件", async () => {
11
+ const result = await writePromptToTempFile("test-agent", "You are a helper.");
12
+ expect(result.dir).toBeTruthy();
13
+ expect(result.filePath).toBeTruthy();
14
+ expect(fs.existsSync(result.dir)).toBe(true);
15
+ expect(fs.existsSync(result.filePath)).toBe(true);
16
+
17
+ const content = fs.readFileSync(result.filePath, "utf-8");
18
+ expect(content).toBe("You are a helper.");
19
+
20
+ // 清理
21
+ await cleanupTempPrompt(result);
22
+ expect(fs.existsSync(result.dir)).toBe(false);
23
+ });
24
+
25
+ it("agentName 非法字符替换为下划线", async () => {
26
+ const result = await writePromptToTempFile("my agent/v2:spec", "prompt");
27
+ expect(path.basename(result.filePath)).toBe("prompt-my_agent_v2_spec.md");
28
+ await cleanupTempPrompt(result);
29
+ });
30
+
31
+ it("文件权限 0o600(仅 owner 读写)", async () => {
32
+ const result = await writePromptToTempFile("secure", "secret prompt");
33
+ const stat = fs.statSync(result.filePath);
34
+ // 只取低 9 位(rwxrwxrwx),忽略文件类型位
35
+ const mode = stat.mode & 0o777;
36
+ expect(mode).toBe(0o600);
37
+ await cleanupTempPrompt(result);
38
+ });
39
+
40
+ it("多字节内容(中文)正确写入", async () => {
41
+ const result = await writePromptToTempFile("cn", "你是一个助手。");
42
+ const content = fs.readFileSync(result.filePath, "utf-8");
43
+ expect(content).toBe("你是一个助手。");
44
+ await cleanupTempPrompt(result);
45
+ });
46
+ });
47
+
48
+ describe("cleanupTempPrompt", () => {
49
+ it("不存在的目录不抛错(best-effort)", async () => {
50
+ const fake = { dir: "/nonexistent/path/xyz", filePath: "/nonexistent/path/xyz/p.md" };
51
+ await expect(cleanupTempPrompt(fake)).resolves.not.toThrow();
52
+ });
53
+ });