@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,358 @@
1
+ // Medium batch 4: M4 IPC 字段校验(行为测试)
2
+ //
3
+ // M4: dispatchAgentCall / dispatchWorkflowCall 在函数最开头校验 IPC 消息字段
4
+ // (callId 非数字、opts/args 非对象、prompt/name 非字符串),畸形消息仅记日志后 return,
5
+ // 不抛 TypeError、不写 trace 节点、不 postMessage 回 worker。
6
+ //
7
+ // 通过 handleWorkerMessage 触发 dispatchAgentCall / dispatchWorkflowCall(内部函数不 export)。
8
+ // Mock 构造模式参考 orchestration/__tests__/agent-call-catch-fallback.test.ts 和
9
+ // orchestration/__tests__/error-recovery-workflow-call.test.ts。
10
+
11
+ import { describe, expect, it, vi } from "vitest";
12
+
13
+ // Mock 共享 logger,让 logger.error 可被 spy(源码已从 console.error 改为 logger.error)
14
+ const { loggerMock } = vi.hoisted(() => ({
15
+ loggerMock: {
16
+ debug: vi.fn(),
17
+ warn: vi.fn(),
18
+ error: vi.fn(),
19
+ },
20
+ }));
21
+ vi.mock("../core/logger.ts", () => ({
22
+ getLogger: () => loggerMock,
23
+ }));
24
+
25
+ import { handleWorkerMessage } from "../orchestration/error-recovery.ts";
26
+ import type { LifecycleDeps, WorkerHandlers } from "../orchestration/models/ports.ts";
27
+ import { Trace } from "../orchestration/models/trace.ts";
28
+ import type { AgentResult } from "../orchestration/models/types.ts";
29
+ import type { WorkflowRun } from "../orchestration/models/workflow-run.ts";
30
+
31
+ // ── helpers ──────────────────────────────────────────────────
32
+
33
+ /** 构造 status="running" 的 mock WorkflowRun,含 trace/budget/calls/runtime。
34
+ * 与 agent-call-catch-fallback.test.ts 同构。 */
35
+ function makeRunningRun(runId: string): WorkflowRun {
36
+ const trace = new Trace();
37
+ const controller = new AbortController();
38
+ return {
39
+ runId,
40
+ spec: { scriptName: "test-wf", scriptSource: "agent('hi')", args: {}, runId, slug: undefined },
41
+ state: {
42
+ status: "running" as const,
43
+ reason: undefined,
44
+ trace,
45
+ budget: {
46
+ usedTokens: 0,
47
+ usedCost: 0,
48
+ totalCallCount: 0,
49
+ consume: vi.fn(),
50
+ isExceeded: vi.fn(() => false),
51
+ incrementCallCount: vi.fn(),
52
+ },
53
+ calls: new Map(),
54
+ scriptResult: undefined,
55
+ },
56
+ meta: { startedAt: new Date().toISOString(), workerErrorCount: 0, scriptErrorCount: 0 },
57
+ runtime: {
58
+ controller,
59
+ worker: { postMessage: vi.fn() },
60
+ },
61
+ transition: vi.fn(),
62
+ replaceRuntime: vi.fn(),
63
+ } as unknown as WorkflowRun;
64
+ }
65
+
66
+ /** LifecycleDeps mock——runner.run 默认成功。 */
67
+ function makeDeps(): LifecycleDeps {
68
+ const runnerRun = vi.fn(async () => {
69
+ return { content: "OK", durationMs: 10, error: undefined, toolCalls: [] } as AgentResult;
70
+ });
71
+ return {
72
+ store: { save: vi.fn(async () => {}) },
73
+ workerHost: { start: vi.fn() },
74
+ runner: { run: runnerRun },
75
+ runs: new Map(),
76
+ eventBus: { emit: vi.fn() },
77
+ onRunDone: vi.fn(),
78
+ log: vi.fn(),
79
+ } as unknown as LifecycleDeps;
80
+ }
81
+
82
+ function makeHandlers(): WorkerHandlers {
83
+ return {
84
+ onMessage: vi.fn(async () => {}),
85
+ onError: vi.fn(async () => {}),
86
+ onExit: vi.fn(async () => {}),
87
+ } as unknown as WorkerHandlers;
88
+ }
89
+
90
+ /** 清空 logger.error 调用记录(校验失败会打印 malformed 消息日志,避免污染跨用例断言)。 */
91
+ function silenceConsoleError(): () => void {
92
+ loggerMock.error.mockClear();
93
+ return () => {
94
+ loggerMock.error.mockClear();
95
+ };
96
+ }
97
+
98
+ // ── M4: dispatchAgentCall 字段校验 ───────────────────────────
99
+
100
+ describe("M4: dispatchAgentCall validates IPC fields before dereferencing", () => {
101
+ it("callId 非数字 → 不抛、不写 trace 节点、不 postMessage", async () => {
102
+ const restore = silenceConsoleError();
103
+ try {
104
+ const run = makeRunningRun("wf-m4-001");
105
+ const deps = makeDeps();
106
+ const postMessage = run.runtime!.worker.postMessage as ReturnType<typeof vi.fn>;
107
+
108
+ // callId = "not-a-number"(字符串)
109
+ await handleWorkerMessage(
110
+ run,
111
+ { type: "agent-call", callId: "not-a-number", opts: { prompt: "test" } },
112
+ deps,
113
+ makeHandlers(),
114
+ );
115
+
116
+ expect(run.state.trace.toArray().length).toBe(0);
117
+ expect(run.state.calls.size).toBe(0);
118
+ expect(postMessage).not.toHaveBeenCalled();
119
+ expect(deps.store.save).not.toHaveBeenCalled();
120
+ } finally {
121
+ restore();
122
+ }
123
+ });
124
+
125
+ it("opts 为 null → 不抛、不写 trace 节点", async () => {
126
+ const restore = silenceConsoleError();
127
+ try {
128
+ const run = makeRunningRun("wf-m4-002");
129
+ const deps = makeDeps();
130
+ const postMessage = run.runtime!.worker.postMessage as ReturnType<typeof vi.fn>;
131
+
132
+ await handleWorkerMessage(
133
+ run,
134
+ { type: "agent-call", callId: 1, opts: null },
135
+ deps,
136
+ makeHandlers(),
137
+ );
138
+
139
+ expect(run.state.trace.toArray().length).toBe(0);
140
+ expect(run.state.calls.size).toBe(0);
141
+ expect(postMessage).not.toHaveBeenCalled();
142
+ } finally {
143
+ restore();
144
+ }
145
+ });
146
+
147
+ it("opts.prompt 非字符串(数字)→ 不抛、不写 trace 节点", async () => {
148
+ const restore = silenceConsoleError();
149
+ try {
150
+ const run = makeRunningRun("wf-m4-003");
151
+ const deps = makeDeps();
152
+ const postMessage = run.runtime!.worker.postMessage as ReturnType<typeof vi.fn>;
153
+
154
+ await handleWorkerMessage(
155
+ run,
156
+ { type: "agent-call", callId: 1, opts: { prompt: 123 } },
157
+ deps,
158
+ makeHandlers(),
159
+ );
160
+
161
+ expect(run.state.trace.toArray().length).toBe(0);
162
+ expect(run.state.calls.size).toBe(0);
163
+ expect(postMessage).not.toHaveBeenCalled();
164
+ } finally {
165
+ restore();
166
+ }
167
+ });
168
+
169
+ it("opts 缺失(undefined)→ 不抛、不写 trace 节点", async () => {
170
+ const restore = silenceConsoleError();
171
+ try {
172
+ const run = makeRunningRun("wf-m4-004");
173
+ const deps = makeDeps();
174
+ const postMessage = run.runtime!.worker.postMessage as ReturnType<typeof vi.fn>;
175
+
176
+ await handleWorkerMessage(
177
+ run,
178
+ { type: "agent-call", callId: 1, opts: undefined },
179
+ deps,
180
+ makeHandlers(),
181
+ );
182
+
183
+ expect(run.state.trace.toArray().length).toBe(0);
184
+ expect(run.state.calls.size).toBe(0);
185
+ expect(postMessage).not.toHaveBeenCalled();
186
+ } finally {
187
+ restore();
188
+ }
189
+ });
190
+
191
+ it("callId 非有限数(NaN)→ 不抛、不写 trace 节点", async () => {
192
+ const restore = silenceConsoleError();
193
+ try {
194
+ const run = makeRunningRun("wf-m4-005");
195
+ const deps = makeDeps();
196
+ const postMessage = run.runtime!.worker.postMessage as ReturnType<typeof vi.fn>;
197
+
198
+ await handleWorkerMessage(
199
+ run,
200
+ { type: "agent-call", callId: NaN, opts: { prompt: "test" } },
201
+ deps,
202
+ makeHandlers(),
203
+ );
204
+
205
+ expect(run.state.trace.toArray().length).toBe(0);
206
+ expect(run.state.calls.size).toBe(0);
207
+ expect(postMessage).not.toHaveBeenCalled();
208
+ } finally {
209
+ restore();
210
+ }
211
+ });
212
+
213
+ it("正常消息 → 正常派发(trace 节点创建)", async () => {
214
+ const run = makeRunningRun("wf-m4-006");
215
+ const deps = makeDeps();
216
+
217
+ await handleWorkerMessage(
218
+ run,
219
+ { type: "agent-call", callId: 1, opts: { prompt: "test", description: "test-agent" } },
220
+ deps,
221
+ makeHandlers(),
222
+ );
223
+
224
+ // 正常消息应创建 trace 节点(status=running,等待异步执行)
225
+ const nodes = run.state.trace.toArray();
226
+ expect(nodes.length).toBeGreaterThan(0);
227
+ expect(nodes[0]!.stepIndex).toBe(1);
228
+ expect(nodes[0]!.task).toBe("test");
229
+ expect(nodes[0]!.agent).toBe("test-agent");
230
+ // call 已入 run.state.calls
231
+ expect(run.state.calls.size).toBeGreaterThan(0);
232
+ });
233
+
234
+ it("校验失败打印 malformed 日志(含 callId)", async () => {
235
+ loggerMock.error.mockClear();
236
+ try {
237
+ const run = makeRunningRun("wf-m4-007");
238
+ const deps = makeDeps();
239
+
240
+ await handleWorkerMessage(
241
+ run,
242
+ { type: "agent-call", callId: "bad", opts: { prompt: "test" } },
243
+ deps,
244
+ makeHandlers(),
245
+ );
246
+
247
+ expect(loggerMock.error).toHaveBeenCalled();
248
+ const logged = String(loggerMock.error.mock.calls[0]![0]);
249
+ expect(logged).toContain("malformed agent-call");
250
+ expect(logged).toContain('"bad"');
251
+ } finally {
252
+ loggerMock.error.mockClear();
253
+ }
254
+ });
255
+ });
256
+
257
+ // ── M4: dispatchWorkflowCall 字段校验 ────────────────────────
258
+
259
+ describe("M4: dispatchWorkflowCall validates IPC fields before dereferencing", () => {
260
+ it("callId 非数字 → 不抛、不 postMessage", async () => {
261
+ const restore = silenceConsoleError();
262
+ try {
263
+ const run = makeRunningRun("wf-m4-w-001");
264
+ const deps = { onWorkflowCall: vi.fn(async () => ({ content: "ok" })) } as unknown as LifecycleDeps;
265
+ const postMessage = run.runtime!.worker.postMessage as ReturnType<typeof vi.fn>;
266
+
267
+ await handleWorkerMessage(
268
+ run,
269
+ { type: "workflow-call", callId: "bad", name: "sub", args: {} },
270
+ deps,
271
+ makeHandlers(),
272
+ );
273
+
274
+ expect(deps.onWorkflowCall).not.toHaveBeenCalled();
275
+ expect(postMessage).not.toHaveBeenCalled();
276
+ } finally {
277
+ restore();
278
+ }
279
+ });
280
+
281
+ it("name 非字符串 → 不抛、不 postMessage", async () => {
282
+ const restore = silenceConsoleError();
283
+ try {
284
+ const run = makeRunningRun("wf-m4-w-002");
285
+ const deps = { onWorkflowCall: vi.fn(async () => ({ content: "ok" })) } as unknown as LifecycleDeps;
286
+ const postMessage = run.runtime!.worker.postMessage as ReturnType<typeof vi.fn>;
287
+
288
+ await handleWorkerMessage(
289
+ run,
290
+ { type: "workflow-call", callId: 1, name: 42, args: {} },
291
+ deps,
292
+ makeHandlers(),
293
+ );
294
+
295
+ expect(deps.onWorkflowCall).not.toHaveBeenCalled();
296
+ expect(postMessage).not.toHaveBeenCalled();
297
+ } finally {
298
+ restore();
299
+ }
300
+ });
301
+
302
+ it("args 为 null → 不抛、不 postMessage", async () => {
303
+ const restore = silenceConsoleError();
304
+ try {
305
+ const run = makeRunningRun("wf-m4-w-003");
306
+ const deps = { onWorkflowCall: vi.fn(async () => ({ content: "ok" })) } as unknown as LifecycleDeps;
307
+ const postMessage = run.runtime!.worker.postMessage as ReturnType<typeof vi.fn>;
308
+
309
+ await handleWorkerMessage(
310
+ run,
311
+ { type: "workflow-call", callId: 1, name: "sub", args: null },
312
+ deps,
313
+ makeHandlers(),
314
+ );
315
+
316
+ expect(deps.onWorkflowCall).not.toHaveBeenCalled();
317
+ expect(postMessage).not.toHaveBeenCalled();
318
+ } finally {
319
+ restore();
320
+ }
321
+ });
322
+
323
+ it("校验失败打印 malformed 日志", async () => {
324
+ loggerMock.error.mockClear();
325
+ try {
326
+ const run = makeRunningRun("wf-m4-w-004");
327
+ const deps = { onWorkflowCall: vi.fn(async () => ({ content: "ok" })) } as unknown as LifecycleDeps;
328
+
329
+ await handleWorkerMessage(
330
+ run,
331
+ { type: "workflow-call", callId: "bad", name: 42, args: {} },
332
+ deps,
333
+ makeHandlers(),
334
+ );
335
+
336
+ expect(loggerMock.error).toHaveBeenCalled();
337
+ const logged = String(loggerMock.error.mock.calls[0]![0]);
338
+ expect(logged).toContain("malformed workflow-call");
339
+ } finally {
340
+ loggerMock.error.mockClear();
341
+ }
342
+ });
343
+
344
+ it("正常消息 → 正常派发(onWorkflowCall 被调用)", async () => {
345
+ const run = makeRunningRun("wf-m4-w-005");
346
+ const onWorkflowCall = vi.fn(async () => ({ content: "ok" }));
347
+ const deps = { onWorkflowCall } as unknown as LifecycleDeps;
348
+
349
+ await handleWorkerMessage(
350
+ run,
351
+ { type: "workflow-call", callId: 1, name: "sub", args: { k: 1 } },
352
+ deps,
353
+ makeHandlers(),
354
+ );
355
+
356
+ expect(onWorkflowCall).toHaveBeenCalledWith("sub", { k: 1 }, run);
357
+ });
358
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * session-runner message_update 分流测试。
3
+ *
4
+ * 核心回归点:toolcall_delta(工具入参 JSON 增量)不得被当成 text_delta
5
+ * forward 到 subagent overlay 的 streaming content——否则 assistant 正文会
6
+ * 原样流出 {"path":"..."}{"command":"..."} 工具参数 JSON 串,表现为对话末尾
7
+ * JSON 与 text 混杂、无 ICON+title 工具卡片前缀(用户实际报告的现象)。
8
+ *
9
+ * 根因:pi-ai AssistantMessageEvent 的 type 集合含 text_delta / thinking_delta /
10
+ * toolcall_delta / signature_delta 等(见 @earendil-works/pi-ai types.d.ts)。
11
+ * 旧实现 `ame?.delta !== undefined` 反向排除(只拦 thinking_delta),toolcall_delta
12
+ * 因带 delta 字段被误判为 text_delta。正向判定(只放行 text_delta)根治。
13
+ */
14
+ import { describe, it, expect } from "vitest";
15
+ import { mapAssistantMessageDelta } from "../execution/session-runner.js";
16
+
17
+ describe("mapAssistantMessageDelta: assistantMessageEvent → streaming 事件分流", () => {
18
+ it("text_delta → forward 为 text_delta", () => {
19
+ expect(mapAssistantMessageDelta({ type: "text_delta", delta: "hello" })).toEqual({
20
+ type: "text_delta",
21
+ delta: "hello",
22
+ });
23
+ });
24
+
25
+ it("thinking_delta → forward 为 thinking_delta(走独立通道,不进 text stream)", () => {
26
+ expect(mapAssistantMessageDelta({ type: "thinking_delta", delta: "思考" })).toEqual({
27
+ type: "thinking_delta",
28
+ delta: "思考",
29
+ });
30
+ });
31
+
32
+ it("thinking_delta 缺 delta 兜底空串(不抛错,与旧实现行为一致)", () => {
33
+ expect(mapAssistantMessageDelta({ type: "thinking_delta" })).toEqual({
34
+ type: "thinking_delta",
35
+ delta: "",
36
+ });
37
+ });
38
+
39
+ it("toolcall_delta → null(工具入参 JSON 增量绝不混入 text)", () => {
40
+ // 核心回归:旧实现 ame?.delta !== undefined 会把 toolcall_delta 当 text_delta
41
+ expect(mapAssistantMessageDelta({ type: "toolcall_delta", delta: '{"path":"/x"}' })).toBeNull();
42
+ });
43
+
44
+ it("toolcall_delta 的部分 JSON 片段也不混入(流式累积的中间态)", () => {
45
+ expect(mapAssistantMessageDelta({ type: "toolcall_delta", delta: '{"path":"' })).toBeNull();
46
+ expect(mapAssistantMessageDelta({ type: "toolcall_delta", delta: "/Users/foo/bar" })).toBeNull();
47
+ expect(mapAssistantMessageDelta({ type: "toolcall_delta", delta: '"}}' })).toBeNull();
48
+ });
49
+
50
+ it("其他带 delta 的 type(signature_delta 等未来扩展)也不混入 text", () => {
51
+ expect(mapAssistantMessageDelta({ type: "signature_delta", delta: "sig-xyz" })).toBeNull();
52
+ });
53
+
54
+ it("非 delta 事件(start/end/done 等)返回 null", () => {
55
+ expect(mapAssistantMessageDelta({ type: "text_start" })).toBeNull();
56
+ expect(mapAssistantMessageDelta({ type: "text_end" })).toBeNull();
57
+ expect(mapAssistantMessageDelta({ type: "toolcall_start" })).toBeNull();
58
+ expect(mapAssistantMessageDelta({ type: "toolcall_end" })).toBeNull();
59
+ expect(mapAssistantMessageDelta({ type: "done" })).toBeNull();
60
+ });
61
+
62
+ it("缺 type 或缺 delta 的异常输入返回 null(不抛错)", () => {
63
+ expect(mapAssistantMessageDelta({})).toBeNull();
64
+ // text_delta 缺 delta → null(无法 forward 空 delta 增量)
65
+ expect(mapAssistantMessageDelta({ type: "text_delta" })).toBeNull();
66
+ // 有 delta 无 type → null(未知来源,不放行)
67
+ expect(mapAssistantMessageDelta({ delta: "x" })).toBeNull();
68
+ });
69
+ });
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { CORE_PACKAGE_VERSION } from "../index";
4
+
5
+ describe("subagent-core scaffold smoke", () => {
6
+ it("exposes CORE_PACKAGE_VERSION", () => {
7
+ expect(CORE_PACKAGE_VERSION).toBe("0.1.0");
8
+ });
9
+ });
@@ -0,0 +1,203 @@
1
+ /**
2
+ * SubagentStream — text_delta streaming 生命周期测试。
3
+ *
4
+ * 覆盖:
5
+ * - leading edge:第一个 delta 立即 flush
6
+ * - trailing edge:100ms 内多个 delta 合并为一次 flush
7
+ * - 多行文本:delta 含换行符时 split("\n") 正确
8
+ * - dispose:清除 widget + 清 timer(幂等)
9
+ * - dispose 后 onDelta 静默丢弃
10
+ * - widgetKey 格式(subagent-stream-<recordId>)
11
+ *
12
+ * SubagentStream 内聚 buffer/timer 状态,通过 StreamSink 接口输出。
13
+ * 测试用 mock sink 收集 setWidget 调用,vi.useFakeTimers 控制合并窗口时序。
14
+ */
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
+
17
+ import { type StreamSink, SubagentStream } from "../execution/stream-sink.ts";
18
+
19
+ /** mock sink:收集所有 setWidget 调用,便于断言。 */
20
+ function createMockSink(): StreamSink & { calls: { key: string; lines: string[] | undefined }[] } {
21
+ const calls: { key: string; lines: string[] | undefined }[] = [];
22
+ return {
23
+ calls,
24
+ setWidget(key: string, lines: string[] | undefined): void {
25
+ calls.push({ key, lines });
26
+ },
27
+ };
28
+ }
29
+
30
+ describe("SubagentStream", () => {
31
+ beforeEach(() => {
32
+ vi.useFakeTimers();
33
+ });
34
+ afterEach(() => {
35
+ vi.useRealTimers();
36
+ });
37
+
38
+ // ============================================================
39
+ // U1: leading edge — 第一个 delta 立即触发 flush
40
+ // ============================================================
41
+ describe("leading edge", () => {
42
+ it("第一个 delta 立即 flush,不等 timer", () => {
43
+ const sink = createMockSink();
44
+ const stream = new SubagentStream("bg-test", sink);
45
+
46
+ stream.onDelta("Hello");
47
+
48
+ expect(sink.calls).toHaveLength(1);
49
+ expect(sink.calls[0]).toEqual({
50
+ key: "subagent-stream-bg-test",
51
+ lines: ["Hello"],
52
+ });
53
+ });
54
+ });
55
+
56
+ // ============================================================
57
+ // U2: trailing edge — 100ms 内多个 delta 合并为一次 flush
58
+ // ============================================================
59
+ describe("trailing edge", () => {
60
+ it("leading 之后的 delta 在 100ms 窗口内合并为一次 flush", () => {
61
+ const sink = createMockSink();
62
+ const stream = new SubagentStream("bg-merge", sink);
63
+
64
+ // leading flush
65
+ stream.onDelta("A");
66
+ expect(sink.calls).toHaveLength(1);
67
+
68
+ // trailing delta 进入 buffer,等 timer
69
+ stream.onDelta("B");
70
+ stream.onDelta("C");
71
+ expect(sink.calls).toHaveLength(1); // 还没 flush
72
+
73
+ // 推进 timer
74
+ vi.advanceTimersByTime(100);
75
+
76
+ expect(sink.calls).toHaveLength(2); // leading + trailing
77
+ expect(sink.calls[1]).toEqual({
78
+ key: "subagent-stream-bg-merge",
79
+ lines: ["ABC"], // 累积全文
80
+ });
81
+ });
82
+ });
83
+
84
+ // ============================================================
85
+ // U3: 多行文本 — delta 含换行符时 split 正确
86
+ // ============================================================
87
+ describe("多行文本", () => {
88
+ it("delta 含换行符 → widgetLines 按行拆分", () => {
89
+ const sink = createMockSink();
90
+ const stream = new SubagentStream("bg-multi", sink);
91
+
92
+ stream.onDelta("line1\nline2\nline3");
93
+
94
+ expect(sink.calls[0]).toEqual({
95
+ key: "subagent-stream-bg-multi",
96
+ lines: ["line1", "line2", "line3"],
97
+ });
98
+ });
99
+ });
100
+
101
+ // ============================================================
102
+ // U4: dispose — 清除 widget + 清 timer(幂等)
103
+ // ============================================================
104
+ describe("dispose", () => {
105
+ it("dispose 清除 widget(setWidget key, undefined)", () => {
106
+ const sink = createMockSink();
107
+ const stream = new SubagentStream("bg-disp", sink);
108
+
109
+ stream.onDelta("X"); // leading flush
110
+ stream.onDelta("Y"); // 开始 trailing timer
111
+ stream.dispose();
112
+
113
+ // dispose 调 setWidget(key, undefined) 清除 widget
114
+ const lastCall = sink.calls[sink.calls.length - 1];
115
+ expect(lastCall).toEqual({
116
+ key: "subagent-stream-bg-disp",
117
+ lines: undefined,
118
+ });
119
+ });
120
+
121
+ it("dispose 后 trailing timer 不再触发 flush", () => {
122
+ const sink = createMockSink();
123
+ const stream = new SubagentStream("bg-timer", sink);
124
+
125
+ stream.onDelta("X"); // leading flush
126
+ stream.onDelta("Y"); // 开始 trailing timer
127
+ stream.dispose(); // 清 timer
128
+
129
+ const callCountBefore = sink.calls.length;
130
+ vi.advanceTimersByTime(200); // timer 应已被清
131
+
132
+ expect(sink.calls.length).toBe(callCountBefore); // 无新 flush
133
+ });
134
+
135
+ it("dispose 幂等——二次调用静默无副作用", () => {
136
+ const sink = createMockSink();
137
+ const stream = new SubagentStream("bg-idem", sink);
138
+
139
+ stream.dispose();
140
+ const callCountAfterFirst = sink.calls.length;
141
+
142
+ stream.dispose(); // 二次 dispose
143
+ expect(sink.calls.length).toBe(callCountAfterFirst); // 无新调用
144
+ });
145
+ });
146
+
147
+ // ============================================================
148
+ // U5: dispose 后 onDelta 静默丢弃
149
+ // ============================================================
150
+ describe("dispose 后 onDelta", () => {
151
+ it("dispose 后调 onDelta 不触发任何 setWidget", () => {
152
+ const sink = createMockSink();
153
+ const stream = new SubagentStream("bg-late", sink);
154
+
155
+ stream.dispose();
156
+ const callCountAfterDispose = sink.calls.length;
157
+
158
+ stream.onDelta("late delta");
159
+ vi.advanceTimersByTime(200); // 确保没有延迟 flush
160
+
161
+ expect(sink.calls.length).toBe(callCountAfterDispose); // 无新调用
162
+ });
163
+ });
164
+
165
+ // ============================================================
166
+ // U6: widgetKey 格式
167
+ // ============================================================
168
+ describe("widgetKey 格式", () => {
169
+ it("widgetKey = subagent-stream-<recordId>", () => {
170
+ const sink = createMockSink();
171
+ const stream = new SubagentStream("bg-abc123", sink);
172
+
173
+ stream.onDelta("x");
174
+
175
+ expect(sink.calls[0]?.key).toBe("subagent-stream-bg-abc123");
176
+ });
177
+ });
178
+
179
+ // ============================================================
180
+ // U7: 空 delta 静默丢弃(不消耗 leading edge)
181
+ // ============================================================
182
+ describe("空 delta", () => {
183
+ it("空字符串 onDelta 不触发 setWidget", () => {
184
+ const sink = createMockSink();
185
+ const stream = new SubagentStream("bg-empty", sink);
186
+
187
+ stream.onDelta("");
188
+
189
+ expect(sink.calls).toHaveLength(0);
190
+ });
191
+
192
+ it("空 delta 不消耗 leading edge——后续真实 delta 仍立即 flush", () => {
193
+ const sink = createMockSink();
194
+ const stream = new SubagentStream("bg-empty", sink);
195
+
196
+ stream.onDelta(""); // 空串,不应消耗 leading
197
+ stream.onDelta("Hi"); // 首个真实 delta,应立即 flush
198
+
199
+ expect(sink.calls).toHaveLength(1);
200
+ expect(sink.calls[0]?.lines).toEqual(["Hi"]);
201
+ });
202
+ });
203
+ });