@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,809 @@
1
+ /**
2
+ * error-recovery handlers — handleWorkerExit/Error/ScriptError + postBudgetUpdate 测试。
3
+ *
4
+ * 参考 error-recovery-workflow-call.test.ts 的 mock 构建。通过 vi.useFakeTimers() 跳过
5
+ * scheduleRebuild 的指数退避(1s/2s/4s)。
6
+ *
7
+ * 覆盖:
8
+ * - handleWorkerExit:code=0 正常退出(no-op) / code!=0 委托 handleWorkerError / stale handle 过滤
9
+ * - handleWorkerError:超限(count > MAX=3)→ transition done,failed + emit pending:unregister
10
+ * / 未超限 → rebuildRuntime(workerHost.start 重建)
11
+ * - handleScriptError:超限 → transition done,failed / workerLogs 捕获
12
+ * - postBudgetUpdate:postMessage budget-update(usedTokens/usedCost)
13
+ * - stale handle 过滤(handle.isCurrent=false)+ terminal stale 守卫(isTerminal 语义)
14
+ * - rebuildRuntime:worker 崩溃后 workerHost.start + scheduleTimeBudget 重排 + replaceRuntime
15
+ * + 在飞 call 清理(discardInFlightCalls 生效:在飞清除、done 保留)
16
+ */
17
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
18
+
19
+ import {
20
+ handleScriptError,
21
+ handleWorkerError,
22
+ handleWorkerExit,
23
+ handleWorkerMessage,
24
+ postBudgetUpdate,
25
+ rebuildRuntime,
26
+ } from "../error-recovery.ts";
27
+ import { Budget } from "../models/budget.ts";
28
+ import { RunRuntime } from "../models/run-runtime.ts";
29
+ import { Trace } from "../models/trace.ts";
30
+ import type { AgentResult } from "../models/types.ts";
31
+ import { WorkflowRun } from "../models/workflow-run.ts";
32
+ import type { LifecycleDeps, WorkerHandlers } from "../models/ports.ts";
33
+ import type { WorkerHandle } from "../worker-handle.ts";
34
+
35
+ // ── helpers ──────────────────────────────────────────────────
36
+
37
+ /** 构造一个 status="running" 的 mock WorkflowRun,meta 可配置。 */
38
+ function makeRunningRun(opts: {
39
+ workerErrorCount?: number;
40
+ scriptErrorCount?: number;
41
+ budgetTimeMs?: number;
42
+ postMessage?: ReturnType<typeof vi.fn>;
43
+ /** [F1] 预置本 runtime 代际已收到终态消息(return/error)。 */
44
+ receivedTerminalMessage?: boolean;
45
+ } = {}): WorkflowRun {
46
+ return {
47
+ state: {
48
+ status: "running",
49
+ budget: { usedTokens: 50, usedCost: 0.1 },
50
+ // L9: errorLogs 现在用 push 追加——必须是真实数组,不能省略
51
+ errorLogs: [],
52
+ // rebuildRuntime 内 discardInFlightCalls 遍历 calls + 移除 trace 节点——
53
+ // 必须是真实 Map(空 = 无在飞 call,discard 为 no-op)
54
+ calls: new Map(),
55
+ trace: { removeByStepIndex: vi.fn() },
56
+ },
57
+ meta: {
58
+ startedAt: new Date().toISOString(),
59
+ workerErrorCount: opts.workerErrorCount,
60
+ scriptErrorCount: opts.scriptErrorCount,
61
+ },
62
+ spec: {
63
+ scriptName: "test-wf",
64
+ scriptSource: "execute() {}",
65
+ args: {},
66
+ budgetTimeMs: opts.budgetTimeMs,
67
+ },
68
+ runtime: {
69
+ worker: { postMessage: opts.postMessage ?? vi.fn() },
70
+ receivedTerminalMessage: opts.receivedTerminalMessage,
71
+ },
72
+ // transition 副作用——run.state.status 由调用方通过 mock 控制后再次断言
73
+ transition(target: string, reason?: string): void {
74
+ this.state.status = target;
75
+ if (target === "done") this.state.reason = reason;
76
+ },
77
+ replaceRuntime(rt: unknown): void {
78
+ this.runtime = rt;
79
+ },
80
+ } as unknown as WorkflowRun;
81
+ }
82
+
83
+ /** LifecycleDeps mock:store/workerHost/runner/eventBus/scheduleTimeBudget 可观察。 */
84
+ function makeDeps(opts: {
85
+ scheduleTimeBudget?: LifecycleDeps["scheduleTimeBudget"];
86
+ } = {}): LifecycleDeps & {
87
+ store: { save: ReturnType<typeof vi.fn> };
88
+ workerHost: { start: ReturnType<typeof vi.fn> };
89
+ eventBus: { emit: ReturnType<typeof vi.fn> };
90
+ onRunDone: ReturnType<typeof vi.fn>;
91
+ log: ReturnType<typeof vi.fn>;
92
+ } {
93
+ return {
94
+ store: { save: vi.fn(async () => {}) },
95
+ workerHost: { start: vi.fn(() => ({ postMessage: vi.fn() })) },
96
+ runner: { run: vi.fn(async () => ({})) },
97
+ runs: new Map(),
98
+ eventBus: { emit: vi.fn() },
99
+ onRunDone: vi.fn(),
100
+ log: vi.fn(),
101
+ scheduleTimeBudget: opts.scheduleTimeBudget,
102
+ } as unknown as ReturnType<typeof makeDeps>;
103
+ }
104
+
105
+ /** WorkerHandlers 占位(handler 路径递归调本对象上的回调,但测试场景不触发)。 */
106
+ function makeHandlers(): WorkerHandlers {
107
+ return {
108
+ onMessage: vi.fn(async () => {}),
109
+ onError: vi.fn(async () => {}),
110
+ onExit: vi.fn(async () => {}),
111
+ } as unknown as WorkerHandlers;
112
+ }
113
+
114
+ /** 构造 mock WorkerHandle(isCurrent 可配)。 */
115
+ function makeHandle(isCurrent = true): WorkerHandle {
116
+ return { isCurrent } as unknown as WorkerHandle;
117
+ }
118
+
119
+ beforeEach(() => {
120
+ vi.useFakeTimers();
121
+ });
122
+
123
+ afterEach(() => {
124
+ vi.useRealTimers();
125
+ });
126
+
127
+ // ── handleWorkerExit ─────────────────────────────────────────
128
+
129
+ describe("handleWorkerExit", () => {
130
+ it("code=0 且已收到终态消息:no-op(不 transition、不 save)", async () => {
131
+ // [F1] 语义更新:exit(0) no-op 的前提是本代际已交付 return/error(正常收尾退出,
132
+ // 或 script-error 重试退避窗口)。无终态消息的 exit(0) 现转 done,failed,
133
+ // 见下方用例与 worker-exit-without-result.test.ts。
134
+ const run = makeRunningRun({ receivedTerminalMessage: true });
135
+ const deps = makeDeps();
136
+ const handle = makeHandle(true);
137
+
138
+ await handleWorkerExit(run, 0, handle, deps, makeHandlers());
139
+
140
+ expect(run.state.status).toBe("running"); // 未改
141
+ expect(deps.store.save).not.toHaveBeenCalled();
142
+ expect(deps.eventBus.emit).not.toHaveBeenCalled();
143
+ });
144
+
145
+ it("code=0 且无终态消息:[F1] 转 done,failed(不可克隆 return 被吞的悬挂防线)", async () => {
146
+ const run = makeRunningRun();
147
+ const deps = makeDeps();
148
+ const handle = makeHandle(true);
149
+
150
+ await handleWorkerExit(run, 0, handle, deps, makeHandlers());
151
+
152
+ expect(run.state.status).toBe("done");
153
+ expect(run.state.reason).toBe("failed");
154
+ expect(run.state.error).toContain("structured-cloneable");
155
+ expect(deps.store.save).toHaveBeenCalledTimes(1);
156
+ expect(deps.onRunDone).toHaveBeenCalledTimes(1);
157
+ });
158
+
159
+ it("code!=0 异常退出:委托 handleWorkerError → 超 MAX 重试 → transition done,failed", async () => {
160
+ // workerErrorCount 已达 MAX=3 → handleWorkerError 内 count=4 > 3 → failed
161
+ const run = makeRunningRun({ workerErrorCount: 3 });
162
+ const deps = makeDeps();
163
+ const handle = makeHandle(true);
164
+
165
+ await handleWorkerExit(run, 1, handle, deps, makeHandlers());
166
+
167
+ expect(run.state.status).toBe("done");
168
+ expect(run.state.reason).toBe("failed");
169
+ expect(run.state.error).toContain("Worker exited with code 1");
170
+ // 持久化 + 完成通知
171
+ expect(deps.store.save).toHaveBeenCalledTimes(1);
172
+ expect(deps.onRunDone).toHaveBeenCalledTimes(1);
173
+ });
174
+
175
+ it("stale handle(isCurrent=false):丢弃 exit 事件,不处理", async () => {
176
+ const run = makeRunningRun();
177
+ const deps = makeDeps();
178
+ const staleHandle = makeHandle(false);
179
+
180
+ await handleWorkerExit(run, 1, staleHandle, deps, makeHandlers());
181
+
182
+ // 状态未变,store 未 save
183
+ expect(run.state.status).toBe("running");
184
+ expect(deps.store.save).not.toHaveBeenCalled();
185
+ });
186
+
187
+ it("run 已终态(done):stale 守卫前置丢弃", async () => {
188
+ const run = makeRunningRun();
189
+ run.state.status = "done";
190
+ (run.state as { reason?: string }).reason = "completed";
191
+ const deps = makeDeps();
192
+ const handle = makeHandle(true);
193
+
194
+ await handleWorkerExit(run, 1, handle, deps, makeHandlers());
195
+
196
+ expect(deps.store.save).not.toHaveBeenCalled();
197
+ });
198
+ });
199
+
200
+ // ── handleWorkerError ────────────────────────────────────────
201
+
202
+ describe("handleWorkerError", () => {
203
+ it("count > MAX(3):transition done,failed + save + emit pending:unregister", async () => {
204
+ // workerErrorCount=3 → count=4 > MAX
205
+ const run = makeRunningRun({ workerErrorCount: 3 });
206
+ const deps = makeDeps();
207
+
208
+ await handleWorkerError(run, new Error("worker boom"), deps, makeHandlers());
209
+
210
+ expect(run.meta.workerErrorCount).toBe(4);
211
+ expect(run.state.status).toBe("done");
212
+ expect(run.state.reason).toBe("failed");
213
+ expect(run.state.error).toBe("worker boom");
214
+ expect(deps.store.save).toHaveBeenCalledTimes(1);
215
+ expect(deps.eventBus.emit).toHaveBeenCalledWith("pending:unregister", {
216
+ id: undefined, // mock run 无 runId
217
+ reason: "failed",
218
+ });
219
+ expect(deps.onRunDone).toHaveBeenCalledTimes(1);
220
+ });
221
+
222
+ it("count <= MAX:退避 + rebuildRuntime(workerHost.start 重建新 runtime)", async () => {
223
+ const run = makeRunningRun({ workerErrorCount: 0 }); // count=1 <= MAX
224
+ const deps = makeDeps();
225
+
226
+ const promise = handleWorkerError(run, new Error("transient"), deps, makeHandlers());
227
+
228
+ // 推进指数退避(第 1 次重试:1s)
229
+ await vi.advanceTimersByTimeAsync(1000);
230
+ await promise;
231
+
232
+ expect(run.meta.workerErrorCount).toBe(1);
233
+ // 状态仍 running(重试不改 status)
234
+ expect(run.state.status).toBe("running");
235
+ // workerHost.start 被调(rebuildRuntime 内重建 worker)
236
+ expect(deps.workerHost.start).toHaveBeenCalledTimes(1);
237
+ });
238
+
239
+ it("终态(done):stale 守卫前置丢弃(不递增 workerErrorCount)", async () => {
240
+ const run = makeRunningRun();
241
+ run.state.status = "done";
242
+ (run.state as { reason?: string }).reason = "completed";
243
+ const deps = makeDeps();
244
+
245
+ await handleWorkerError(run, new Error("stale"), deps, makeHandlers());
246
+
247
+ expect(run.meta.workerErrorCount).toBeUndefined(); // 未递增
248
+ expect(deps.store.save).not.toHaveBeenCalled();
249
+ });
250
+ });
251
+
252
+ // ── handleScriptError ────────────────────────────────────────
253
+
254
+ describe("handleScriptError", () => {
255
+ it("count > MAX(3):transition done,failed + 捕获 workerLogs", async () => {
256
+ const run = makeRunningRun({ scriptErrorCount: 3 }); // count=4 > MAX
257
+ const deps = makeDeps();
258
+ const workerLogs = [
259
+ { level: "error" as const, message: "line 5 boom" },
260
+ ];
261
+
262
+ await handleScriptError(run, "TypeError: x is undefined", workerLogs, deps, makeHandlers());
263
+
264
+ expect(run.meta.scriptErrorCount).toBe(4);
265
+ expect(run.state.status).toBe("done");
266
+ expect(run.state.reason).toBe("failed");
267
+ expect(run.state.error).toContain("Workflow failed after 3 retries");
268
+ expect(run.state.error).toContain("TypeError: x is undefined");
269
+ // workerLogs 捕获到 errorLogs
270
+ expect(run.state.errorLogs).toEqual(workerLogs);
271
+ expect(deps.store.save).toHaveBeenCalledTimes(1);
272
+ expect(deps.onRunDone).toHaveBeenCalledTimes(1);
273
+ });
274
+
275
+ it("count <= MAX:退避 + rebuildRuntime", async () => {
276
+ const run = makeRunningRun({ scriptErrorCount: 1 }); // count=2 <= MAX
277
+ const deps = makeDeps();
278
+
279
+ const promise = handleScriptError(run, "ReferenceError", [], deps, makeHandlers());
280
+
281
+ // 第 2 次重试退避:2s
282
+ await vi.advanceTimersByTimeAsync(2000);
283
+ await promise;
284
+
285
+ expect(run.meta.scriptErrorCount).toBe(2);
286
+ expect(run.state.status).toBe("running");
287
+ expect(deps.workerHost.start).toHaveBeenCalledTimes(1);
288
+ });
289
+
290
+ it("terminal 状态:stale 守卫前置丢弃", async () => {
291
+ const run = makeRunningRun();
292
+ run.state.status = "done";
293
+ (run.state as { reason?: string }).reason = "completed";
294
+ const deps = makeDeps();
295
+
296
+ await handleScriptError(run, "late error", [], deps, makeHandlers());
297
+
298
+ expect(run.meta.scriptErrorCount).toBeUndefined();
299
+ expect(deps.store.save).not.toHaveBeenCalled();
300
+ });
301
+ });
302
+
303
+ // ── postBudgetUpdate ─────────────────────────────────────────
304
+
305
+ describe("postBudgetUpdate", () => {
306
+ it("向 worker postMessage budget-update(usedTokens/usedCost)", () => {
307
+ const postMessage = vi.fn();
308
+ const run = makeRunningRun({ postMessage });
309
+
310
+ postBudgetUpdate(run);
311
+
312
+ expect(postMessage).toHaveBeenCalledWith({
313
+ type: "budget-update",
314
+ budget: { usedTokens: 50, usedCost: 0.1 },
315
+ });
316
+ });
317
+
318
+ it("runtime 不存在时 no-op(不抛错)", () => {
319
+ const run = makeRunningRun();
320
+ // runtime.worker.postMessage 为 undefined 时应安全
321
+ run.runtime = undefined;
322
+
323
+ expect(() => postBudgetUpdate(run)).not.toThrow();
324
+ });
325
+ });
326
+
327
+ // ── rebuildRuntime ───────────────────────────────────────────
328
+
329
+ describe("rebuildRuntime", () => {
330
+ it("worker 崩溃后重建:workerHost.start + replaceRuntime(保持 running)", () => {
331
+ const run = makeRunningRun({ budgetTimeMs: 0 }); // 无时间预算
332
+ const deps = makeDeps();
333
+
334
+ rebuildRuntime(run, deps, makeHandlers());
335
+
336
+ // workerHost.start 被调(构造新 worker)
337
+ expect(deps.workerHost.start).toHaveBeenCalledTimes(1);
338
+ // replaceRuntime 被调(新 runtime 绑定,mock 内仅替换 runtime 字段)
339
+ expect(run.runtime).toBeDefined();
340
+ // status 仍 running(replaceRuntime 不改 status)
341
+ expect(run.state.status).toBe("running");
342
+ });
343
+
344
+ it("带 budgetTimeMs 时重排 scheduleTimeBudget 计时器", () => {
345
+ const run = makeRunningRun({ budgetTimeMs: 5000 });
346
+ const scheduleTimeBudget = vi.fn(() => undefined);
347
+ const deps = makeDeps({ scheduleTimeBudget });
348
+
349
+ rebuildRuntime(run, deps, makeHandlers());
350
+
351
+ // D-12 regression fix (round-2 #2):replaceRuntime 后重排时间预算
352
+ expect(scheduleTimeBudget).toHaveBeenCalledTimes(1);
353
+ // 第 1 参 = runId(mock run 无 runId),第 2 参 = budgetTimeMs
354
+ const args = scheduleTimeBudget.mock.calls[0]!;
355
+ expect(args[1]).toBe(5000);
356
+ });
357
+
358
+ it("无 scheduleTimeBudget 注入时不重排(向后兼容,不抛错)", () => {
359
+ const run = makeRunningRun({ budgetTimeMs: 5000 });
360
+ const deps = makeDeps({ scheduleTimeBudget: undefined });
361
+
362
+ expect(() => rebuildRuntime(run, deps, makeHandlers())).not.toThrow();
363
+ expect(deps.workerHost.start).toHaveBeenCalledTimes(1);
364
+ });
365
+
366
+ it("rebuildRuntime 后同步清理在飞 call(status !== done 清除、done 保留)", () => {
367
+ const run = makeRunningRun({ budgetTimeMs: 0 });
368
+ // 注入在飞 call(running)与已完成 call(done)+ trace 节点移除 spy
369
+ const removeByStepIndex = vi.fn();
370
+ const calls = new Map<number, { id: number; status: string }>([
371
+ [7, { id: 7, status: "running" }],
372
+ [8, { id: 8, status: "done" }],
373
+ ]);
374
+ (run.state as { calls: unknown }).calls = calls;
375
+ (run.state as { trace: unknown }).trace = { removeByStepIndex };
376
+ const deps = makeDeps();
377
+
378
+ rebuildRuntime(run, deps, makeHandlers());
379
+
380
+ // 在飞 call(running)被移除(含 trace 节点);已完成 call(done)保留供重跑 replay
381
+ expect(calls.has(7)).toBe(false);
382
+ expect(calls.has(8)).toBe(true);
383
+ expect(removeByStepIndex).toHaveBeenCalledTimes(1);
384
+ expect(removeByStepIndex).toHaveBeenCalledWith(7);
385
+ // 重建本身不受影响
386
+ expect(deps.workerHost.start).toHaveBeenCalledTimes(1);
387
+ });
388
+ });
389
+
390
+ // ── orphan call guard(S7-second 竞态回归) ─────────────────
391
+ //
392
+ // 复刻 gate-report §3 的竞态形态:dispatch agent-call → 立即 rebuildRuntime(同步
393
+ // discard 在飞 call)→ 旧 dispatch 的 promise 以失败/成功 resolve → 迟到的旧代际
394
+ // 结果不得经 postAgentResult 投给**新 worker** 的同 callId pending(否则重跑中的
395
+ // agent() 被旧结果劫持 resolve 为空串 → 脚本假成功,PHASE_B 子进程被连带收割)。
396
+ //
397
+ // 用真实 WorkflowRun/RunRuntime/Trace/Budget(而非 makeRunningRun 的简化 mock):
398
+ // rebuildRuntime → replaceRuntime → release 的 abort 旧 controller / terminate 旧
399
+ // worker / discardInFlightCalls 全链路需要真实聚合根行为才成立。
400
+
401
+ // ── race-F3:rebuild 时间预算折算(已耗墙钟不重置) ─────────────
402
+ //
403
+ // 旧实现 rebuildRuntime 重排计时器用满额 budgetTimeMs——每吃一次 worker/script
404
+ // 错误重试就重置一次预算,最坏 6 次重试放大 ~6× 墙钟。修复后:
405
+ // - 重排值 = max(0, 原预算 - 已耗墙钟)(从 run.meta.startedAt 推算)
406
+ // - 重试前已耗尽 → 不 rebuild,直接 done,time_limited 终态
407
+ //
408
+ // 确定性说明:本文件 beforeEach 已 useFakeTimers()(默认 fake Date)——Date.now
409
+ // 冻结,startedAt 倒拨值即精确已耗墙钟,断言可精确等值。
410
+ describe("race-F3: rebuild 时间预算折算", () => {
411
+ it("剩余 30% → 重排预算 = 30% 而非满额", () => {
412
+ const run = makeRunningRun({ budgetTimeMs: 5000 });
413
+ // 已耗 70%(3500ms)→ 剩余 1500ms;fake Date 冻结,elapsed 精确
414
+ run.meta.startedAt = new Date(Date.now() - 3500).toISOString();
415
+ const scheduleTimeBudget = vi.fn(() => undefined);
416
+ const deps = makeDeps({ scheduleTimeBudget });
417
+
418
+ rebuildRuntime(run, deps, makeHandlers());
419
+
420
+ expect(scheduleTimeBudget).toHaveBeenCalledTimes(1);
421
+ const args = scheduleTimeBudget.mock.calls[0]!;
422
+ expect(args[1]).toBe(1500);
423
+ // run 保持 running(正常 rebuild 路径不受影响)
424
+ expect(run.state.status).toBe("running");
425
+ });
426
+
427
+ it("重试前预算已耗尽(已耗 > 预算)→ 不 rebuild,直接 done,time_limited", async () => {
428
+ const run = makeRunningRun({ budgetTimeMs: 5000 });
429
+ // 已耗 6000ms > 预算 5000ms(退避 advance 1000ms 后已耗 7000ms,仍耗尽)
430
+ run.meta.startedAt = new Date(Date.now() - 6000).toISOString();
431
+ const scheduleTimeBudget = vi.fn(() => undefined);
432
+ const deps = makeDeps({ scheduleTimeBudget });
433
+
434
+ const p = handleScriptError(run, "boom", [], deps, makeHandlers());
435
+ await vi.advanceTimersByTimeAsync(1000); // 推进退避(1s)
436
+ await p;
437
+
438
+ // 不 rebuild:不启新 worker、不重排计时器
439
+ expect(deps.workerHost.start).not.toHaveBeenCalled();
440
+ expect(scheduleTimeBudget).not.toHaveBeenCalled();
441
+ // 直接 time_limited 终态 + 持久化 + 注销通知 + onRunDone
442
+ expect(run.state.status).toBe("done");
443
+ expect(run.state.reason).toBe("time_limited");
444
+ expect(deps.store.save).toHaveBeenCalled();
445
+ expect(deps.eventBus.emit).toHaveBeenCalledWith(
446
+ "pending:unregister",
447
+ expect.objectContaining({ reason: "time_limited" }),
448
+ );
449
+ expect(deps.onRunDone).toHaveBeenCalled();
450
+ });
451
+
452
+ it("预算未耗尽(fresh run)→ 照常 rebuild,不误转 time_limited(防误伤回归)", async () => {
453
+ const run = makeRunningRun({ budgetTimeMs: 5000 }); // startedAt ≈ now,remaining 满
454
+ const scheduleTimeBudget = vi.fn(() => undefined);
455
+ const deps = makeDeps({ scheduleTimeBudget });
456
+
457
+ const p = handleScriptError(run, "boom", [], deps, makeHandlers());
458
+ await vi.advanceTimersByTimeAsync(1000);
459
+ await p;
460
+
461
+ expect(deps.workerHost.start).toHaveBeenCalledTimes(1);
462
+ expect(scheduleTimeBudget).toHaveBeenCalledTimes(1);
463
+ expect(run.state.status).toBe("running");
464
+ });
465
+ });
466
+
467
+ /** 手动控制的 deferred——精确编排「dispatch 挂起 → rebuild → 旧 promise settle」交错。 */
468
+ interface Deferred<T> {
469
+ promise: Promise<T>;
470
+ resolve: (value: T) => void;
471
+ reject: (err: unknown) => void;
472
+ }
473
+
474
+ function createDeferred<T>(): Deferred<T> {
475
+ let resolve!: (value: T) => void;
476
+ let reject!: (err: unknown) => void;
477
+ const promise = new Promise<T>((res, rej) => {
478
+ resolve = res;
479
+ reject = rej;
480
+ });
481
+ return { promise, resolve, reject };
482
+ }
483
+
484
+ /** flush 微任务队列(dispatch 的 fire-and-forget promise 链推进到稳定态)。 */
485
+ async function flushMicrotasks(ticks = 10): Promise<void> {
486
+ for (let i = 0; i < ticks; i++) {
487
+ // eslint-disable-next-line no-await-in-loop -- 排空微任务队列的固定 tick 循环(fire-and-forget promise 链推进到稳定态),非逐项等待
488
+ await Promise.resolve();
489
+ }
490
+ }
491
+
492
+ /** 构造真实 WorkflowRun(真实状态机/replaceRuntime/Trace/Budget)+ 初始 RunRuntime。 */
493
+ function makeRealRun(runId: string, opts: { budgetTimeMs?: number } = {}): WorkflowRun {
494
+ const run = new WorkflowRun(
495
+ runId,
496
+ {
497
+ scriptName: "test-wf",
498
+ scriptSource: "agent('hi')",
499
+ args: {},
500
+ scriptPath: "/tmp/test-wf.js",
501
+ budgetTimeMs: opts.budgetTimeMs,
502
+ },
503
+ {
504
+ status: "running",
505
+ budget: new Budget(),
506
+ calls: new Map(),
507
+ trace: new Trace(),
508
+ errorLogs: [],
509
+ },
510
+ { startedAt: new Date().toISOString() },
511
+ );
512
+ // 初始 worker mock 必须带 terminate——真实 replaceRuntime 会 release 旧 runtime
513
+ const initialWorker = {
514
+ postMessage: vi.fn(),
515
+ terminate: vi.fn(async () => {}),
516
+ } as unknown as WorkerHandle;
517
+ run.assignRuntime(new RunRuntime(initialWorker, new AbortController()));
518
+ return run;
519
+ }
520
+
521
+ /** agent-call 消息(opts 无 skill/schema——resolveAgentOpts 直通,无 IO)。 */
522
+ function makeAgentCallMsg(callId: number): unknown {
523
+ return {
524
+ type: "agent-call",
525
+ callId,
526
+ opts: { prompt: "test task", agent: "worker", description: "test-slug" },
527
+ };
528
+ }
529
+
530
+ /** 从 postMessage spy 的调用记录中找 type:"agent-result" 且 callId 匹配的报文。 */
531
+ function findAgentResultPost(
532
+ postMessage: ReturnType<typeof vi.fn>,
533
+ callId: number,
534
+ ): { type: string; callId: number; result: AgentResult; cached: boolean } | undefined {
535
+ for (const call of postMessage.mock.calls) {
536
+ const msg = call[0] as { type?: string; callId?: number };
537
+ if (msg?.type === "agent-result" && msg.callId === callId) {
538
+ return msg as never;
539
+ }
540
+ }
541
+ return undefined;
542
+ }
543
+
544
+ describe("orphan call guard(rebuild 后迟到 completion 不投递新 worker)", () => {
545
+ it("失败路径:discard + 重跑替换后,旧 dispatch 的失败 completion 不投新 worker、不复活 Map 条目", async () => {
546
+ const run = makeRealRun("wf-orphan-1");
547
+ const deps = makeDeps();
548
+ // rebuild 的新 worker——postMessage 记录用于断言「孤儿结果未投递」
549
+ const newWorkerPost = vi.fn();
550
+ deps.workerHost.start.mockImplementation(
551
+ () =>
552
+ ({ postMessage: newWorkerPost, terminate: vi.fn(async () => {}) }) as unknown as WorkerHandle,
553
+ );
554
+ const handlers = makeHandlers();
555
+
556
+ // 旧 runtime 代际:dispatch callId=1,runner 挂起(模拟在飞子进程)
557
+ const deferredA = createDeferred<AgentResult>();
558
+ deps.runner.run.mockImplementation(() => deferredA.promise);
559
+ await handleWorkerMessage(run, makeAgentCallMsg(1), deps, handlers);
560
+ await flushMicrotasks();
561
+ expect(run.state.calls.get(1)?.status).toBe("running");
562
+
563
+ // worker 崩溃 → rebuildRuntime:replaceRuntime(abort 旧 controller)+ 同步 discard 在飞 call
564
+ rebuildRuntime(run, deps, handlers);
565
+ expect(run.state.calls.has(1)).toBe(false);
566
+ expect(run.state.trace.find(1)).toBeUndefined();
567
+
568
+ // 新 worker 重跑脚本:同 callId=1 再 dispatch(重跑实例 B 挂起在飞)
569
+ const deferredB = createDeferred<AgentResult>();
570
+ deps.runner.run.mockImplementation(() => deferredB.promise);
571
+ await handleWorkerMessage(run, makeAgentCallMsg(1), deps, handlers);
572
+ await flushMicrotasks();
573
+ const rerunCall = run.state.calls.get(1);
574
+ expect(rerunCall).toBeDefined();
575
+
576
+ // 旧 dispatch 的 promise 以失败 resolve(abort 收割子进程后的迟到 finalize,
577
+ // error 含 "aborted" 命中 stale-context 快速路径——真实形态)
578
+ deferredA.resolve({ content: "", error: "Subprocess aborted by runtime shutdown" });
579
+ await flushMicrotasks();
580
+
581
+ // 孤儿失败结果不得投给新 worker(否则劫持重跑 pending 为空串假成功)
582
+ expect(findAgentResultPost(newWorkerPost, 1)).toBeUndefined();
583
+ // calls Map 无孤儿复活条目:callId=1 仍是重跑实例 B
584
+ expect(run.state.calls.get(1)).toBe(rerunCall);
585
+ });
586
+
587
+ it("成功路径:discard 后旧 dispatch 的成功 completion 不投新 worker、不复活 Map/trace 条目", async () => {
588
+ const run = makeRealRun("wf-orphan-2");
589
+ const deps = makeDeps();
590
+ const newWorkerPost = vi.fn();
591
+ deps.workerHost.start.mockImplementation(
592
+ () =>
593
+ ({ postMessage: newWorkerPost, terminate: vi.fn(async () => {}) }) as unknown as WorkerHandle,
594
+ );
595
+
596
+ const deferred = createDeferred<AgentResult>();
597
+ deps.runner.run.mockImplementation(() => deferred.promise);
598
+ await handleWorkerMessage(run, makeAgentCallMsg(2), deps, makeHandlers());
599
+ await flushMicrotasks();
600
+
601
+ rebuildRuntime(run, deps, makeHandlers());
602
+ expect(run.state.calls.has(2)).toBe(false);
603
+
604
+ // 旧 promise 以成功 resolve(runner 已完成、结果晚于 rebuild 到达)
605
+ deferred.resolve({ content: "late success", durationMs: 5, error: undefined, toolCalls: [] });
606
+ await flushMicrotasks();
607
+
608
+ expect(findAgentResultPost(newWorkerPost, 2)).toBeUndefined();
609
+ expect(run.state.calls.has(2)).toBe(false);
610
+ expect(run.state.trace.find(2)).toBeUndefined();
611
+ });
612
+
613
+ it("catch 路径:discard 后旧 dispatch 的异常 reject 不投新 worker、不复活 Map 条目", async () => {
614
+ // catch 块的 logger.error 会打印——静默防噪音
615
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {});
616
+ try {
617
+ const run = makeRealRun("wf-orphan-3");
618
+ const deps = makeDeps();
619
+ const newWorkerPost = vi.fn();
620
+ deps.workerHost.start.mockImplementation(
621
+ () =>
622
+ ({ postMessage: newWorkerPost, terminate: vi.fn(async () => {}) }) as unknown as WorkerHandle,
623
+ );
624
+
625
+ const deferred = createDeferred<AgentResult>();
626
+ deps.runner.run.mockImplementation(() => deferred.promise);
627
+ await handleWorkerMessage(run, makeAgentCallMsg(3), deps, makeHandlers());
628
+ await flushMicrotasks();
629
+
630
+ rebuildRuntime(run, deps, makeHandlers());
631
+ expect(run.state.calls.has(3)).toBe(false);
632
+
633
+ deferred.reject(new Error("runner exploded"));
634
+ await flushMicrotasks();
635
+
636
+ expect(findAgentResultPost(newWorkerPost, 3)).toBeUndefined();
637
+ expect(run.state.calls.has(3)).toBe(false);
638
+ } finally {
639
+ spy.mockRestore();
640
+ }
641
+ });
642
+
643
+ it("非孤儿正常路径:成功 completion 照常投递 agent-result(守卫不误伤)", async () => {
644
+ const run = makeRealRun("wf-orphan-4");
645
+ const deps = makeDeps();
646
+ const postMessage = run.runtime!.worker.postMessage as ReturnType<typeof vi.fn>;
647
+ deps.runner.run.mockImplementation(
648
+ async () => ({ content: "real result", durationMs: 1, error: undefined, toolCalls: [] }) as AgentResult,
649
+ );
650
+
651
+ await handleWorkerMessage(run, makeAgentCallMsg(4), deps, makeHandlers());
652
+ await flushMicrotasks(20);
653
+
654
+ const posted = findAgentResultPost(postMessage, 4);
655
+ expect(posted).toBeDefined();
656
+ expect(posted?.result.content).toBe("real result");
657
+ expect(run.state.calls.get(4)?.status).toBe("done");
658
+ expect(run.state.trace.find(4)?.status).toBe("completed");
659
+ });
660
+
661
+ it("U6/S3 场景重放:discard + 重跑替换后,旧 finalize 不污染重跑新 trace 节点(旧 call 实例仍 markDone)", async () => {
662
+ const run = makeRealRun("wf-orphan-s3");
663
+ const deps = makeDeps();
664
+ const newWorkerPost = vi.fn();
665
+ deps.workerHost.start.mockImplementation(
666
+ () => ({ postMessage: newWorkerPost, terminate: vi.fn(async () => {}) }) as unknown as WorkerHandle,
667
+ );
668
+ const handlers = makeHandlers();
669
+
670
+ // 旧代际 dispatch callId=9:runner 挂起(模拟在飞子进程)
671
+ const deferredA = createDeferred<AgentResult>();
672
+ deps.runner.run.mockImplementation(() => deferredA.promise);
673
+ await handleWorkerMessage(run, makeAgentCallMsg(9), deps, handlers);
674
+ await flushMicrotasks();
675
+ const oldCall = run.state.calls.get(9);
676
+ expect(oldCall?.status).toBe("running");
677
+
678
+ // rebuild:discard 移除旧 call 条目 + trace 节点(replaceRuntime 同步 abort 旧 signal)
679
+ rebuildRuntime(run, deps, handlers);
680
+ expect(run.state.calls.has(9)).toBe(false);
681
+ expect(run.state.trace.find(9)).toBeUndefined();
682
+
683
+ // 重跑 dispatch 同 callId=9:新实例 + 新 trace 节点 running,挂起在飞
684
+ const deferredB = createDeferred<AgentResult>();
685
+ deps.runner.run.mockImplementation(() => deferredB.promise);
686
+ await handleWorkerMessage(run, makeAgentCallMsg(9), deps, handlers);
687
+ await flushMicrotasks();
688
+ const rerunCall = run.state.calls.get(9);
689
+ expect(rerunCall).toBeDefined();
690
+ expect(rerunCall).not.toBe(oldCall);
691
+ expect(run.state.trace.find(9)?.status).toBe("running");
692
+
693
+ // 旧 runner promise 以非 stale 失败 resolve——rebuild 已 abort 旧 signal,旧
694
+ // executeAgentCall 醒来走 signal.aborted finalize 调用点(错误文案不含 stale
695
+ // 模式词,确保不进 stale 分支)。红性锚点:无 OB2 守卫时此处 trace.update(9)
696
+ // 命中重跑新节点 → 短暂污染为 failed。
697
+ deferredA.resolve({ content: "", durationMs: 3, error: "old generation failure", toolCalls: [] });
698
+ await flushMicrotasks();
699
+
700
+ // 新 trace 节点未被旧 finalize 污染:仍 running、无 result、无 completedAt
701
+ const newNode = run.state.trace.find(9);
702
+ expect(newNode?.status).toBe("running");
703
+ expect(newNode?.result).toBeUndefined();
704
+ expect(newNode?.completedAt).toBeUndefined();
705
+ // 旧实例 markDone 保留(dispatch 层 catch 路径依赖 call.status 语义)
706
+ expect(oldCall?.status).toBe("done");
707
+ expect(oldCall?.result?.error).toBe("old generation failure");
708
+ // 重跑实例仍在飞,未被旧 completion 干扰;孤儿结果不投新 worker
709
+ expect(rerunCall?.status).toBe("running");
710
+ expect(findAgentResultPost(newWorkerPost, 9)).toBeUndefined();
711
+
712
+ // 收尾:resolve 重跑 deferred,让挂起的 promise 链走完(非孤儿 → 正常完成路径)
713
+ deferredB.resolve({ content: "rerun ok", durationMs: 1, error: undefined, toolCalls: [] });
714
+ await flushMicrotasks();
715
+ expect(run.state.trace.find(9)?.status).toBe("completed");
716
+ });
717
+ });
718
+
719
+ // ── rebuildRuntime 可观察性(OB3 日志点 L1-L4) ─────────────
720
+
721
+ /** 从 deps.log 的 mock 调用记录提取 (message, payload)——message 非字符串时置空串(断言自然失败)。 */
722
+ function toLogEntries(calls: unknown[][]): Array<{ message: string; payload: unknown }> {
723
+ return calls.map((c) => ({
724
+ message: typeof c[2] === "string" ? c[2] : "",
725
+ payload: c[3],
726
+ }));
727
+ }
728
+
729
+ describe("rebuildRuntime 可观察性(OB3 日志点)", () => {
730
+ it("U7: 无 budgetTimeMs 时按序打 L1 start → L4 complete(payload 含 runId),无 L2 重排日志", () => {
731
+ const run = makeRealRun("wf-rebuild-log-1");
732
+ const deps = makeDeps();
733
+
734
+ rebuildRuntime(run, deps, makeHandlers());
735
+
736
+ const entries = toLogEntries(deps.log.mock.calls);
737
+ const messages = entries.map((e) => e.message);
738
+ expect(messages).toContain("runtime rebuild start");
739
+ expect(messages).toContain("runtime rebuild complete");
740
+ // L4 在 L1 之后(顺序锚)
741
+ expect(messages.indexOf("runtime rebuild complete")).toBeGreaterThan(
742
+ messages.indexOf("runtime rebuild start"),
743
+ );
744
+ // 无 budgetTimeMs → 该分支本就跳过重排,L2 不打
745
+ expect(messages).not.toContain("time budget rescheduled");
746
+ // L1/L4 payload 含 runId
747
+ const l1 = entries.find((e) => e.message === "runtime rebuild start");
748
+ expect(l1?.payload).toMatchObject({ runId: "wf-rebuild-log-1" });
749
+ const l4 = entries.find((e) => e.message === "runtime rebuild complete");
750
+ expect(l4?.payload).toEqual({ runId: "wf-rebuild-log-1" });
751
+ });
752
+
753
+ it("U7: 带 budgetTimeMs + scheduleTimeBudget 注入时含 L2(在 L1 之后、L3 之前,payload 含 budgetTimeMs)", () => {
754
+ const run = makeRealRun("wf-rebuild-log-2", { budgetTimeMs: 5000 });
755
+ const scheduleTimeBudget = vi.fn(() => undefined);
756
+ const deps = makeDeps({ scheduleTimeBudget });
757
+
758
+ rebuildRuntime(run, deps, makeHandlers());
759
+
760
+ const entries = toLogEntries(deps.log.mock.calls);
761
+ const messages = entries.map((e) => e.message);
762
+ expect(messages).toContain("time budget rescheduled");
763
+ expect(messages.indexOf("time budget rescheduled")).toBeGreaterThan(
764
+ messages.indexOf("runtime rebuild start"),
765
+ );
766
+ expect(messages.indexOf("time budget rescheduled")).toBeLessThan(
767
+ messages.indexOf("in-flight calls discarded"),
768
+ );
769
+ const l2 = entries.find((e) => e.message === "time budget rescheduled");
770
+ expect(l2?.payload).toEqual({ runId: "wf-rebuild-log-2", budgetTimeMs: 5000 });
771
+ });
772
+
773
+ it("U8: 含 2 个在飞 call 的 run 经 rebuild → L3 callIds 升序、count === 2,与实际被弃 callId 一致", async () => {
774
+ const run = makeRealRun("wf-rebuild-log-3");
775
+ const deps = makeDeps();
776
+ const handlers = makeHandlers();
777
+
778
+ // 两个在飞 call(dispatch 插入序 5 → 3,验证 L3 callIds 是升序而非插入序)
779
+ const deferreds = [createDeferred<AgentResult>(), createDeferred<AgentResult>()];
780
+ let next = 0;
781
+ deps.runner.run.mockImplementation(() => {
782
+ const d = deferreds[next];
783
+ next += 1;
784
+ return d!.promise;
785
+ });
786
+ await handleWorkerMessage(run, makeAgentCallMsg(5), deps, handlers);
787
+ await handleWorkerMessage(run, makeAgentCallMsg(3), deps, handlers);
788
+ await flushMicrotasks();
789
+ expect(run.state.calls.get(5)?.status).toBe("running");
790
+ expect(run.state.calls.get(3)?.status).toBe("running");
791
+
792
+ rebuildRuntime(run, deps, makeHandlers());
793
+
794
+ // L3 payload:callIds 升序 [3, 5]、count === 2(即 discardInFlightCalls 返回值)
795
+ const entries = toLogEntries(deps.log.mock.calls);
796
+ const l3 = entries.find((e) => e.message === "in-flight calls discarded");
797
+ expect(l3?.payload).toEqual({ runId: "wf-rebuild-log-3", callIds: [3, 5], count: 2 });
798
+ // 返回值(经 L3 暴露)与实际被弃 callId 一致:Map/trace 条目均已移除
799
+ expect(run.state.calls.has(5)).toBe(false);
800
+ expect(run.state.calls.has(3)).toBe(false);
801
+ expect(run.state.trace.find(5)).toBeUndefined();
802
+ expect(run.state.trace.find(3)).toBeUndefined();
803
+
804
+ // 收尾:resolve 两个挂起的 deferred(孤儿守卫 drop,无投递无污染)
805
+ deferreds[0]!.resolve({ content: "", durationMs: 1, error: undefined, toolCalls: [] });
806
+ deferreds[1]!.resolve({ content: "", durationMs: 1, error: undefined, toolCalls: [] });
807
+ await flushMicrotasks();
808
+ });
809
+ });