@zhushanwen/subagent-core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-KXVBIVOU.js +16 -0
  3. package/dist/chunk-T2SYBW3I.js +24 -0
  4. package/dist/chunk-VURUAGMM.js +2305 -0
  5. package/dist/{types-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
  6. package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -668
  7. package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
  8. package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
  9. package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
  10. package/dist/execution/engine/engine-discovery-scan.js +20 -0
  11. package/dist/execution/engine/paths.cjs +6 -23
  12. package/dist/execution/engine/paths.d.cts +1 -8
  13. package/dist/execution/engine/paths.d.ts +1 -8
  14. package/dist/execution/engine/paths.js +1 -1
  15. package/dist/execution/relay-env.cjs +12 -28
  16. package/dist/execution/relay-env.d.cts +1 -33
  17. package/dist/execution/relay-env.d.ts +1 -33
  18. package/dist/execution/relay-env.js +5 -3
  19. package/dist/index.cjs +14045 -16059
  20. package/dist/index.d.cts +1035 -1579
  21. package/dist/index.d.ts +1035 -1579
  22. package/dist/index.js +9838 -13800
  23. package/dist.bundle/index.cjs +28875 -30323
  24. package/package.json +20 -26
  25. package/src/__tests__/record-store-last-line.test.ts +16 -7
  26. package/src/core/error-message.ts +7 -6
  27. package/src/core/host-services.ts +8 -2
  28. package/src/core/notify-ports.ts +17 -2
  29. package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
  30. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
  31. package/src/execution/__tests__/collect-coordinator-service.test.ts +36 -47
  32. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
  33. package/src/execution/__tests__/delivery-methods.test.ts +179 -237
  34. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
  35. package/src/execution/__tests__/execute-nesting.test.ts +20 -13
  36. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +20 -33
  37. package/src/execution/__tests__/gc-timer.test.ts +7 -22
  38. package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
  39. package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
  40. package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
  41. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
  42. package/src/execution/__tests__/idle-gc.test.ts +200 -0
  43. package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
  44. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
  45. package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
  46. package/src/execution/__tests__/record-store.test.ts +60 -6
  47. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
  48. package/src/execution/__tests__/round-supervisor.test.ts +293 -0
  49. package/src/execution/__tests__/session-pending.test.ts +119 -17
  50. package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
  51. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -11
  52. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
  53. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
  54. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
  55. package/src/execution/__tests__/subagent-service.test.ts +2 -2
  56. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
  57. package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
  58. package/src/execution/__tests__/sync-collect-recovery.test.ts +21 -36
  59. package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
  60. package/src/execution/agent-registry.ts +6 -2
  61. package/src/execution/dialog-queue.ts +6 -73
  62. package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
  63. package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
  64. package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
  65. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
  66. package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
  67. package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
  68. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
  69. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
  70. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
  71. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
  72. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
  73. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
  74. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
  75. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +81 -54
  76. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
  77. package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
  78. package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
  79. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +7 -2
  80. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
  81. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
  82. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
  83. package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
  84. package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
  85. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
  86. package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
  87. package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
  88. package/src/execution/engine/__tests__/registry.test.ts +206 -1
  89. package/src/execution/engine/__tests__/routing.test.ts +97 -0
  90. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
  91. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
  92. package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
  93. package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
  94. package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
  95. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
  96. package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
  97. package/src/execution/engine/client/client-options.ts +103 -0
  98. package/src/execution/engine/client/engine-client.ts +687 -0
  99. package/src/execution/engine/client/mirror.ts +167 -0
  100. package/src/execution/engine/client/pid-file.ts +392 -0
  101. package/src/execution/engine/client/reaper.ts +95 -0
  102. package/src/execution/engine/client/remote-engine.ts +441 -0
  103. package/src/execution/engine/client/reverse-router.ts +255 -0
  104. package/src/execution/engine/common/capability-gate.ts +109 -22
  105. package/src/execution/engine/common/errors.ts +9 -25
  106. package/src/execution/engine/common/event-journal.ts +4 -50
  107. package/src/execution/engine/common/journal-replay.ts +3 -1
  108. package/src/execution/engine/common/kill-chain.ts +18 -251
  109. package/src/execution/engine/common/nesting-guard.ts +18 -114
  110. package/src/execution/engine/common/pool-manager.ts +4 -0
  111. package/src/execution/engine/common/session-view-service.ts +56 -84
  112. package/src/execution/engine/config.ts +151 -0
  113. package/src/execution/engine/d8-compat.ts +346 -0
  114. package/src/execution/engine/engine-discovery-roots.ts +107 -0
  115. package/src/execution/engine/engine-discovery-scan.ts +269 -0
  116. package/src/execution/engine/engine-discovery.ts +30 -7
  117. package/src/execution/engine/engine-inspect-package.ts +282 -0
  118. package/src/execution/engine/engine-manifest.ts +244 -0
  119. package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
  120. package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
  121. package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
  122. package/src/execution/engine/host/host-bridge.ts +150 -0
  123. package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
  124. package/src/execution/engine/host/pi-host-binding.ts +91 -0
  125. package/src/execution/engine/host/spawned-children.ts +139 -0
  126. package/src/execution/engine/host-task-spec.ts +29 -2
  127. package/src/execution/engine/model-validation.ts +55 -2
  128. package/src/execution/engine/paths.ts +16 -41
  129. package/src/execution/engine/port.ts +48 -2
  130. package/src/execution/engine/registry.ts +153 -34
  131. package/src/execution/engine/routing.ts +225 -52
  132. package/src/execution/engine/types.ts +25 -200
  133. package/src/execution/finalize-record.ts +25 -0
  134. package/src/execution/idle-gc.ts +89 -14
  135. package/src/execution/lifecycle-manager.ts +10 -7
  136. package/src/execution/lifecycle-predicates.ts +10 -8
  137. package/src/execution/record-store.ts +29 -2
  138. package/src/execution/relay-env.ts +19 -36
  139. package/src/execution/round-supervisor/domain.ts +89 -0
  140. package/src/execution/round-supervisor/index.ts +33 -0
  141. package/src/execution/round-supervisor/notify-accounting.ts +87 -0
  142. package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
  143. package/src/execution/round-supervisor/service-binding.test.ts +661 -0
  144. package/src/execution/round-supervisor/service-binding.ts +273 -0
  145. package/src/execution/round-supervisor/supervisor.ts +426 -0
  146. package/src/execution/session-pending.ts +68 -74
  147. package/src/execution/settled-watchdog.ts +39 -0
  148. package/src/execution/subagent-service.ts +746 -400
  149. package/src/execution/subprocess-agent-runner.ts +16 -8
  150. package/src/execution/types.ts +41 -117
  151. package/src/execution/ui-channels.ts +13 -214
  152. package/src/execution/ui-request-handler-factory.ts +7 -4
  153. package/src/execution/ui-request-observability.ts +2 -2
  154. package/src/execution/workflow-state-root.ts +67 -0
  155. package/src/execution/worktree-git-ops.ts +5 -0
  156. package/src/execution/worktree-manager.ts +11 -1
  157. package/src/index.ts +55 -26
  158. package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
  159. package/src/orchestration/execute-agent-call.ts +1 -1
  160. package/src/orchestration/file-run-store.ts +144 -59
  161. package/src/orchestration/models/types.ts +43 -42
  162. package/src/shared/zcode-model-ref.ts +31 -0
  163. package/dist/chunk-43ONBFZX.js +0 -240
  164. package/dist/chunk-A4IVZWVX.js +0 -31
  165. package/dist/chunk-APZY4IME.js +0 -27
  166. package/dist/chunk-V3VHZ2VX.js +0 -59
  167. package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
  168. package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
  169. package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
  170. package/dist/execution/engine/engines/zcode/constants.js +0 -54
  171. package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
  172. package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
  173. package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
  174. package/dist/execution/engine/engines/zcode/reader.js +0 -9
  175. package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
  176. package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
  177. package/src/__tests__/session-runner.test.ts +0 -69
  178. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  179. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
  180. package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
  181. package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
  182. package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
  183. package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
  184. package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
  185. package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
  186. package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
  187. package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
  188. package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
  189. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
  190. package/src/execution/__tests__/output-collector.test.ts +0 -469
  191. package/src/execution/__tests__/pi-invocation.test.ts +0 -166
  192. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
  193. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  194. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
  195. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
  196. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
  197. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
  198. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  199. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  200. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  201. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
  202. package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
  203. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
  204. package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
  205. package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
  206. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
  207. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
  208. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
  209. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
  210. package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
  211. package/src/execution/__tests__/spawn-args.test.ts +0 -446
  212. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
  213. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  214. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
  215. package/src/execution/__tests__/spawned-children.test.ts +0 -236
  216. package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
  217. package/src/execution/__tests__/stdin-writer.test.ts +0 -464
  218. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
  219. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  220. package/src/execution/__tests__/timeout-integration.test.ts +0 -616
  221. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  222. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  223. package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
  224. package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
  225. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
  226. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  227. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
  228. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  229. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
  230. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  231. package/src/execution/engine/common/schema-emulation.ts +0 -189
  232. package/src/execution/engine/common/session-view-projection.ts +0 -51
  233. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
  234. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  235. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
  236. package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
  237. package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
  238. package/src/execution/engine/engines/pi/output-collector.ts +0 -317
  239. package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
  240. package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
  241. package/src/execution/engine/engines/pi/reader.ts +0 -59
  242. package/src/execution/engine/engines/pi/registration.ts +0 -37
  243. package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
  244. package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
  245. package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
  246. package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
  247. package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
  248. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
  249. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
  250. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
  251. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
  252. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
  253. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
  254. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  255. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
  256. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
  257. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
  258. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
  259. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
  260. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
  261. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
  262. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
  263. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
  264. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
  265. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
  266. package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
  267. package/src/execution/engine/engines/zcode/connection.ts +0 -598
  268. package/src/execution/engine/engines/zcode/constants.ts +0 -176
  269. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
  270. package/src/execution/engine/engines/zcode/parser.ts +0 -102
  271. package/src/execution/engine/engines/zcode/preparer.ts +0 -235
  272. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  273. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  274. package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
  275. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
  276. package/src/execution/round-settlement.ts +0 -93
  277. package/src/execution/ui-request-queue.ts +0 -215
  278. package/src/shared/schema-env.ts +0 -44
@@ -1,741 +0,0 @@
1
- // engine-model-validation.test.ts —— [u-h2] 引擎感知 model 校验单测。设计权威源:
2
- // docs/design/timeout-audit-hygiene-batch.md §3.2(D2-1 路由先行 / D2-2 validateModel
3
- // 可选面 / D2-3 场景 2/3 文案 / D2-4 跨引擎候选)+ §4.2 V2-1~V2-4 可单测部分 +
4
- // §5.4 P2-1 时序回归(record/emit 顺序)单测锁定。
5
- //
6
- // 覆盖:
7
- // A. chat 路径 execute():路由先行(zcode id + zcode 引擎不再被 pi registry 拒绝,
8
- // V2-1)+ pi id 用在 zcode 的派发同步期报错(V2-2,场景 2 文案逐句断言)+
9
- // frontmatter model 归趋(D2-1)+ ctxModel 不透传 + 未实现 validateModel 的
10
- // 引擎透传兜底(现状语义)。
11
- // B. pi registry 未命中的跨引擎候选(V2-3 场景 3;唯一命中才提示;大小写错误 id
12
- // 原文案零回归 V2-4③)。
13
- // C. workflow 路径 SAR:非 pi 校验调用点(V2-4②成功形态不回归 / V2-4④错误形态
14
- // 同步期报错,不进 engine.run)。
15
- // D. P2-1 时序回归:pi 缺省路径 record entry → pending:register 既有顺序锁定;
16
- // 非 pi 校验失败零 record 零 emit。
17
- // E. zcode 引擎 validateModel 委托 resolveZcodeModelRef(真实 preparer + 临时 v2
18
- // config——同一函数两处消费,无重复实现)。
19
- //
20
- // mock 策略:node:child_process.spawn mock(pi 原路径不真 spawn);非 pi 引擎用
21
- // registry 注入的 fake EnginePort(validateModel/listModels 行为可注入);v2 config
22
- // 用真实临时文件(D 组)。
23
-
24
- import { spawn } from "node:child_process";
25
- import * as fs from "node:fs";
26
- import * as os from "node:os";
27
- import * as path from "node:path";
28
-
29
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
30
-
31
- import { configureCore, resetCoreForTests } from "../../core/host-services.ts";
32
-
33
- vi.mock("node:child_process", async () => {
34
- const { EventEmitter: EE } = await import("node:events");
35
- const { PassThrough: PT } = await import("node:stream");
36
- class FakeChild extends EE {
37
- pid = 12345;
38
- stdout = new PT();
39
- stderr = new PT();
40
- stdin = new PT();
41
- killed = false;
42
- kill(sig?: string): boolean {
43
- this.killed = true;
44
- return sig !== undefined;
45
- }
46
- }
47
- return {
48
- spawn: vi.fn(() => new FakeChild()),
49
- execFile: vi.fn(
50
- (
51
- _cmd: string,
52
- _args: readonly string[],
53
- _opts: unknown,
54
- cb: (err: Error | null, stdout?: string, stderr?: string) => void,
55
- ) => cb(new Error("execFile not configured in this test")),
56
- ),
57
- };
58
- });
59
-
60
- import { spawn as spawnMock } from "node:child_process";
61
-
62
- import type { AgentCallOpts } from "../../orchestration/models/types.ts";
63
- import type { EnginePort, RunContext } from "../engine/port.ts";
64
- import { ZcodeEngine } from "../engine/engines/zcode/zcode-engine.ts";
65
- import { ZcodePrepareError } from "../engine/engines/zcode/preparer.ts";
66
- import { clearEngines, registerEngine } from "../engine/registry.ts";
67
- import type {
68
- EngineCapabilities,
69
- EngineHandle,
70
- ProbeReport,
71
- } from "../engine/types.ts";
72
- import { ModelConfigService } from "../model-config-service.ts";
73
- import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
74
- import { SubagentService } from "../subagent-service.ts";
75
- import { SubprocessAgentRunner } from "../subprocess-agent-runner.ts";
76
- import type { SubagentService as SubagentServiceType } from "../subagent-service.ts";
77
- import type { ExecuteOptions } from "../types.ts";
78
-
79
- const mockSpawn = vi.mocked(spawnMock);
80
-
81
- // ── 既有 pi registry 内容(pi id 空间)与 zcode id 空间(互不认识——设计 §2.2)──
82
-
83
- const PI_ID = "pi-org/pi-main";
84
- const CTX_MODEL: ModelInfo = { id: "pi-main", name: "PI Main", provider: "pi-org", reasoning: false };
85
-
86
- const ZCODE_DEFAULT = "builtin:bigmodel-coding-plan/GLM-5.3";
87
- const ZCODE_FLASH = "builtin:bigmodel-coding-plan/GLM-5.3-Flash";
88
-
89
- // ============================================================
90
- // fake 引擎(validateModel/listModels 行为可注入)
91
- // ============================================================
92
-
93
- const ZCODE_LIKE_CAPS: EngineCapabilities = {
94
- schemaEnforcement: "emulated",
95
- steer: "unsupported",
96
- conversation: "unsupported",
97
- personaInjection: "prompt",
98
- eventGranularity: "coarse",
99
- sandbox: "none",
100
- sessionRead: "full",
101
- resume: "cold",
102
- interrupt: "kill-only",
103
- permissionMode: "native",
104
- maxTurns: false,
105
- };
106
-
107
- interface ValidatingEngineOpts {
108
- id: string;
109
- models: string[];
110
- /** validateModel 对非清单 id 抛出的错误(缺省按 model_not_available 形态)。 */
111
- validateModelError?: Error;
112
- /** false = 不实现 validateModel(透传兜底形态用例)。 */
113
- withValidateModel?: boolean;
114
- }
115
-
116
- class ValidatingFakeEngine implements EnginePort {
117
- readonly id: string;
118
- readonly models: string[];
119
- readonly runs: Array<{ task: AgentCallOpts; ctx: RunContext }> = [];
120
- private readonly validateModelError: Error | undefined;
121
- private readonly withValidateModel: boolean;
122
-
123
- constructor(opts: ValidatingEngineOpts) {
124
- this.id = opts.id;
125
- this.models = opts.models;
126
- this.validateModelError = opts.validateModelError;
127
- this.withValidateModel = opts.withValidateModel !== false;
128
- }
129
-
130
- capabilities(): EngineCapabilities {
131
- return ZCODE_LIKE_CAPS;
132
- }
133
- async probe(): Promise<ProbeReport> {
134
- return { ok: true, engineVersion: "fake", checks: [{ name: "bin", ok: true }] };
135
- }
136
- async run(task: AgentCallOpts, ctx: RunContext): Promise<{ handle: EngineHandle; outcome: { engineId: string; content: string } }> {
137
- this.runs.push({ task, ctx });
138
- return {
139
- handle: { data: { v: 1, engineId: this.id, sessionRef: {}, poolKey: "shared", adapterVersion: "test" } },
140
- outcome: { engineId: this.id, content: "ok" },
141
- };
142
- }
143
- async interact(_handle: EngineHandle, _action: unknown): Promise<{ ok: false; code: string; message: string }> {
144
- return { ok: false, code: "engine_capability_unsupported", message: "fake" };
145
- }
146
- async read(_handle: EngineHandle): Promise<{ engineId: string; turns: never[]; source: "outcome-only" }> {
147
- return { engineId: this.id, turns: [], source: "outcome-only" };
148
- }
149
-
150
- listModels(): Array<{ id: string; name?: string }> {
151
- return this.models.map((id) => ({ id }));
152
- }
153
-
154
- validateModel(modelRef: string | undefined): { canonicalRef: string } {
155
- if (!this.withValidateModel) throw new Error("validateModel not implemented on this fake");
156
- if (this.validateModelError !== undefined) throw this.validateModelError;
157
- if (modelRef === undefined || modelRef.trim() === "") return { canonicalRef: this.models[0]! };
158
- if (this.models.includes(modelRef)) return { canonicalRef: modelRef };
159
- throw new Error(`[model_not_available] unknown model "${modelRef}" (fake registry)`);
160
- }
161
- }
162
-
163
- // ============================================================
164
- // chat 路径装配(chat-engine-routing.test.ts 同款范式)
165
- // ============================================================
166
-
167
- function fakePiRegistry(): ModelRegistryLike {
168
- const AVAILABLE: ModelInfo[] = [CTX_MODEL];
169
- return {
170
- getAvailable: () => AVAILABLE,
171
- find: (provider: string, id: string) => AVAILABLE.find((m) => m.provider === provider && m.id === id),
172
- hasConfiguredAuth: () => true,
173
- };
174
- }
175
-
176
- interface ChatSetup {
177
- service: SubagentService;
178
- zcode: ValidatingFakeEngine;
179
- pi: {
180
- sendMessage: ReturnType<typeof vi.fn>;
181
- appendEntry: ReturnType<typeof vi.fn>;
182
- events: { emit: ReturnType<typeof vi.fn> };
183
- };
184
- /** 统一时间线(appendEntry 与 events.emit 的调用顺序——P2-1 断言用)。 */
185
- eventLog: string[];
186
- }
187
-
188
- function setupChat(agentDir: string, opts?: { defaultEngine?: string; extraEngine?: ValidatingFakeEngine }): ChatSetup {
189
- const zcode = new ValidatingFakeEngine({ id: "zcode", models: [ZCODE_DEFAULT, ZCODE_FLASH] });
190
- registerEngine("zcode", () => zcode);
191
- if (opts?.extraEngine !== undefined) registerEngine(opts.extraEngine.id, () => opts.extraEngine!);
192
- const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
193
- modelService.initModel({
194
- modelRegistry: fakePiRegistry(),
195
- sessionId: "test-session",
196
- ctxModel: CTX_MODEL,
197
- });
198
- const eventLog: string[] = [];
199
- const appendEntry = vi.fn((type: string) => {
200
- eventLog.push(`entry:${type}`);
201
- });
202
- const emit = vi.fn((channel: string) => {
203
- eventLog.push(`emit:${channel}`);
204
- });
205
- const pi = { sendMessage: vi.fn(), appendEntry, events: { emit } };
206
- const service = new SubagentService({ cwd: agentDir, modelService });
207
- service.initSession({ pi, sessionId: "test-session" });
208
- return { service, zcode, pi, eventLog };
209
- }
210
-
211
- function writeGlobalConfig(agentDir: string, cfg: Record<string, unknown>): void {
212
- fs.mkdirSync(path.join(agentDir, "subagents"), { recursive: true });
213
- fs.writeFileSync(path.join(agentDir, "subagents", "config.json"), JSON.stringify(cfg));
214
- }
215
-
216
- function writeAgentMd(dir: string, frontmatter: string): string {
217
- fs.mkdirSync(path.join(dir, "agents"), { recursive: true });
218
- const p = path.join(dir, "agents", "fm-agent.md");
219
- fs.writeFileSync(p, `---\n${frontmatter}\n---\nbody\n`);
220
- return p;
221
- }
222
-
223
- function baseOpts(agentDir: string, extra: Partial<ExecuteOptions> = {}): ExecuteOptions {
224
- return { task: "do work", slug: "validation-test", cwd: agentDir, ctxModel: CTX_MODEL, ...extra };
225
- }
226
-
227
- /**
228
- * 等待 detached finalize 链完全静止后再离开用例(flake 加固 2026-09-05)。
229
- *
230
- * fake run 立即 resolve → kickOffEngineRun 的 detached 链推进 finalizeEngineOutcome →
231
- * finalizeRecord(内含 writeManifest——fs.promises 真异步,threadpool 落盘)→
232
- * notifyComplete → pi.sendMessage。sendMessage 被调 = runEngineTask 已 await 完成
233
- * = 链上全部异步落盘已 resolve(与 chat-engine-routing 终态通知同一等待语义)。
234
- *
235
- * 缺失本等待时,链的在途写盘会跨过 afterEach 的 fs.rmSync——threadpool 写与主线程
236
- * 删除目录树竞争,rmSync 以 ENOTEMPTY 失败打翻清理(单文件跑时间隙足够恒绿,
237
- * 全量并发负载下写延迟跨窗即偶发——本文件曾因此 6 轮中 2 轮失败)。
238
- */
239
- async function settleEngineRunChain(pi: ChatSetup["pi"]): Promise<void> {
240
- await vi.waitFor(() => expect(pi.sendMessage).toHaveBeenCalled());
241
- }
242
-
243
- describe("chat 路径:路由先行 + 按目标引擎校验 model(u-h2 V2-1/V2-2)", () => {
244
- let agentDir: string;
245
-
246
- beforeEach(() => {
247
- configureCore({ dataRoot: () => "/fake-model-validation-data-root", log: () => {} });
248
- agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "engine-model-validation-"));
249
- writeGlobalConfig(agentDir, { version: 1, maxConcurrent: 6 });
250
- });
251
-
252
- afterEach(() => {
253
- clearEngines();
254
- resetCoreForTests();
255
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
256
- vi.clearAllMocks();
257
- });
258
-
259
- // ── V2-1(原 F2-A):zcode 合法模型不再被 pi registry 误导性拒绝 ──
260
-
261
- it("[V2-1a] defaultEngine=zcode 不传 engine + zcode id → 成功派发(修复前报 not a registry entry)", async () => {
262
- writeGlobalConfig(agentDir, { version: 1, maxConcurrent: 6, defaultEngine: "zcode" });
263
- const { service, zcode, pi } = setupChat(agentDir);
264
-
265
- const handle = await service.execute(baseOpts(agentDir, { model: ZCODE_FLASH }));
266
- // record 身份字段产生于 execute 同步段——resolve 后立即快照:fake run 立即 resolve,
267
- // 轮询窗口内 finalize/archive 会把 record 移出 running(waitFor 后再读是负载敏感竞态)
268
- const rec = service["collectRecords"](10, "running").find((r) => r.id === handle.subagentId);
269
- expect(rec?.engine).toBe("zcode");
270
- expect(rec?.model).toBe(ZCODE_FLASH);
271
- // 引擎派发事实单独确定性等待(runs 数组只增不删)
272
- await vi.waitFor(() => expect(zcode.runs.length).toBe(1));
273
- expect(zcode.runs[0]!.task.model).toBe(ZCODE_FLASH);
274
- // 链静止后再离开用例(afterEach rmSync 竞争防御,见 helper 注释)
275
- await settleEngineRunChain(pi);
276
- });
277
-
278
- it("[V2-1b] 显式 engine='zcode' + zcode id → 成功派发", async () => {
279
- const { service, zcode, pi } = setupChat(agentDir);
280
- const handle = await service.execute(baseOpts(agentDir, { engine: "zcode", model: ZCODE_FLASH }));
281
- // 同步段快照(竞态依据同 V2-1a)
282
- const rec = service["collectRecords"](10, "running").find((r) => r.id === handle.subagentId);
283
- expect(rec?.engine).toBe("zcode");
284
- await vi.waitFor(() => expect(zcode.runs.length).toBe(1));
285
- expect(zcode.runs[0]!.task.model).toBe(ZCODE_FLASH);
286
- await settleEngineRunChain(pi);
287
- });
288
-
289
- // ── V2-2(原 F2-B):pi id 用在 zcode → 派发同步期明确报错(场景 2 文案逐句)──
290
-
291
- it("[V2-2] pi id 派发 zcode → 同步期 EngineModelMismatchError(registry 独立 + 清单 + 修正动作)", async () => {
292
- const { service, zcode } = setupChat(agentDir);
293
-
294
- const err = await service
295
- .execute(baseOpts(agentDir, { engine: "zcode", model: PI_ID }))
296
- .catch((e: unknown) => e);
297
-
298
- expect(err).toBeInstanceOf(Error);
299
- expect((err as Error).name).toBe("EngineModelMismatchError");
300
- expect((err as { code?: string }).code).toBe("model_not_available");
301
- const msg = (err as Error).message;
302
- // ① 点破引擎与模型不配套 + registry 独立
303
- expect(msg).toContain(`model '${PI_ID}' is not available on engine 'zcode'.`);
304
- expect(msg).toContain(
305
- "Engine registries are independent — ids in <available_provider_models> (pi registry) do NOT apply to 'zcode' dispatches.",
306
- );
307
- // ② 目标引擎可用清单(数据源 listModels,带凭据)
308
- expect(msg).toContain("zcode models with configured credentials:");
309
- expect(msg).toContain(ZCODE_DEFAULT);
310
- expect(msg).toContain(ZCODE_FLASH);
311
- // ③ 修正动作:清单重试 + 按引擎区分的省略语义(zcode 引擎缺省,非「继承主 agent」)
312
- expect(msg).toContain(
313
- `👉 Retry with one of the above (exact string), or omit \`model\` to use the 'zcode' engine default (${ZCODE_DEFAULT}).`,
314
- );
315
- expect(msg).not.toContain("inherit the main agent model");
316
- // recovery 字段(错误 → 权威源 → 重试闭环)
317
- expect((err as { recovery?: string }).recovery).toContain(`'zcode' engine default (${ZCODE_DEFAULT})`);
318
- // 同步期拒绝:零执行副作用
319
- expect(zcode.runs.length).toBe(0);
320
- expect(mockSpawn).not.toHaveBeenCalled();
321
- expect(service["collectRecords"](10, "all")).toHaveLength(0);
322
- });
323
-
324
- // ── D2-1 逐层语义:frontmatter model / ctxModel / thinkingLevel ──
325
-
326
- it("[D2-1] frontmatter model(agent .md)+ engine: zcode → 原样透传给 taskSpec(作者声明不忽略)", async () => {
327
- const agentRef = writeAgentMd(agentDir, `name: fm-agent\ndescription: d\nengine: zcode\nmodel: ${ZCODE_FLASH}`);
328
- const { service, zcode, pi } = setupChat(agentDir);
329
-
330
- await service.execute(baseOpts(agentDir, { agent: agentRef }));
331
- await vi.waitFor(() => expect(zcode.runs.length).toBe(1));
332
-
333
- expect(zcode.runs[0]!.task.model).toBe(ZCODE_FLASH);
334
- await settleEngineRunChain(pi);
335
- });
336
-
337
- it("[D2-1] frontmatter model 配错(pi id + engine: zcode)→ 同步期场景 2 报错(不落引擎缺省静默续跑)", async () => {
338
- const agentRef = writeAgentMd(agentDir, `name: fm-agent\ndescription: d\nengine: zcode\nmodel: ${PI_ID}`);
339
- const { service, zcode } = setupChat(agentDir);
340
-
341
- const err = await service.execute(baseOpts(agentDir, { agent: agentRef })).catch((e: unknown) => e);
342
- expect((err as Error).message).toContain(`model '${PI_ID}' is not available on engine 'zcode'.`);
343
- expect(zcode.runs.length).toBe(0);
344
- });
345
-
346
- it("[D2-1] 无显式 model → ctxModel 不透传;validateModel(undefined) 裁决引擎缺省进 record", async () => {
347
- writeGlobalConfig(agentDir, { version: 1, maxConcurrent: 6, defaultEngine: "zcode" });
348
- const { service, zcode, pi } = setupChat(agentDir);
349
-
350
- const handle = await service.execute(baseOpts(agentDir));
351
- // 同步段快照 record 身份(竞态依据同 V2-1a)
352
- const rec = service["collectRecords"](10, "running").find((r) => r.id === handle.subagentId);
353
- expect(rec?.model).toBe(ZCODE_DEFAULT);
354
- await vi.waitFor(() => expect(zcode.runs.length).toBe(1));
355
-
356
- // taskSpec.model 不带主 agent 的 pi id(引擎缺省语义归引擎)
357
- expect(zcode.runs[0]!.task.model).toBeUndefined();
358
- await settleEngineRunChain(pi);
359
- });
360
-
361
- it("[现状语义] 未实现 validateModel 的引擎 → model 原样透传(prepare 期兜底,零强制接入)", async () => {
362
- // plain object 构造(无 validateModel/listModels 成员——未来引擎未接入校验面的形态)
363
- const bare: EnginePort = {
364
- id: "bare",
365
- capabilities: () => ZCODE_LIKE_CAPS,
366
- probe: () => Promise.resolve({ ok: true, engineVersion: "bare", checks: [{ name: "bin", ok: true }] }),
367
- run: (task) => {
368
- bareRunTasks.push(task);
369
- return Promise.resolve({
370
- handle: { data: { v: 1, engineId: "bare", sessionRef: {}, poolKey: "shared", adapterVersion: "test" } },
371
- outcome: { engineId: "bare", content: "ok" },
372
- });
373
- },
374
- interact: () => Promise.resolve({ ok: false, code: "engine_capability_unsupported", message: "bare" }),
375
- read: () => Promise.resolve({ engineId: "bare", turns: [], source: "outcome-only" }),
376
- };
377
- const bareRunTasks: AgentCallOpts[] = [];
378
- registerEngine("bare", () => bare);
379
-
380
- const { service, pi } = setupChat(agentDir);
381
- await service.execute(baseOpts(agentDir, { engine: "bare", model: "future-eng/future-model" }));
382
- await vi.waitFor(() => expect(bareRunTasks.length).toBe(1));
383
-
384
- expect(bareRunTasks[0]!.model).toBe("future-eng/future-model");
385
- await settleEngineRunChain(pi);
386
- });
387
-
388
- it("[V2-4①] pi id 派发 pi 引擎(现状主路径)行为不变:resolveModel 正常解析 + spawn 启动", async () => {
389
- const { service } = setupChat(agentDir);
390
- // mock spawn 计数文件级共享(跨用例累积)——以计数差归因本用例(单调递增,可确定性等待)
391
- const spawnCallsBefore = mockSpawn.mock.calls.length;
392
- const handle = await service.execute(baseOpts(agentDir, { model: PI_ID }));
393
-
394
- // record 身份字段产生于 execute 同步段——resolve 后立即快照(竞态依据同 V2-1a)。
395
- // pi 路径的 kickOffBackground → runSpawn 是 detached 链,不作为断言前置条件。
396
- const rec = service["collectRecords"](10, "running").find((r) => r.id === handle.subagentId);
397
- expect(rec?.engine).toBeUndefined(); // pi 缺省不盖章(D5 零变化)
398
- expect(rec?.model).toBe(PI_ID);
399
-
400
- await vi.waitFor(() => expect(mockSpawn.mock.calls.length).toBeGreaterThan(spawnCallsBefore));
401
- });
402
- });
403
-
404
- // ============================================================
405
- // B. pi 未命中跨引擎候选(V2-3 场景 3 + V2-4③ 零回归)
406
- // ============================================================
407
-
408
- describe("pi registry 未命中的跨引擎候选(u-h2 D2-4)", () => {
409
- let agentDir: string;
410
-
411
- beforeEach(() => {
412
- configureCore({ dataRoot: () => "/fake-cross-engine-data-root", log: () => {} });
413
- agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "cross-engine-"));
414
- writeGlobalConfig(agentDir, { version: 1, maxConcurrent: 6 });
415
- });
416
-
417
- afterEach(() => {
418
- clearEngines();
419
- resetCoreForTests();
420
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
421
- vi.clearAllMocks();
422
- });
423
-
424
- it("[V2-3] defaultEngine=pi + zcode id(pi 未命中,zcode 清单唯一命中)→ 错误附跨引擎候选段", async () => {
425
- const { service } = setupChat(agentDir);
426
-
427
- const err = await service.execute(baseOpts(agentDir, { model: ZCODE_FLASH })).catch((e: unknown) => e);
428
- const msg = (err as Error).message;
429
-
430
- // 既有全等裁决文案保留(V2-4③ 零回归面;首行带 (paramOverride) 来源前缀)
431
- expect(msg).toContain(`Model "${ZCODE_FLASH}" (paramOverride) is not a registry entry.`);
432
- // 场景 3 追加段:点破归属引擎 + 修正动作
433
- expect(msg).toContain(`This id matches the registry of engine 'zcode'.`);
434
- expect(msg).toContain(
435
- `👉 Retry with engine: 'zcode', or use a pi model from <available_provider_models>, ` +
436
- `or omit \`model\` to inherit the main agent model.`,
437
- );
438
- });
439
-
440
- it("[D2-4] 多引擎清单同时命中 → 不附候选(唯一命中才提示,避免误导)", async () => {
441
- const second = new ValidatingFakeEngine({ id: "zcode2", models: [ZCODE_FLASH] });
442
- const { service } = setupChat(agentDir, { extraEngine: second });
443
-
444
- const err = await service.execute(baseOpts(agentDir, { model: ZCODE_FLASH })).catch((e: unknown) => e);
445
- expect((err as Error).message).toContain("is not a registry entry");
446
- expect((err as Error).message).not.toContain("matches the registry of engine");
447
- });
448
-
449
- it("[V2-4③] pi id 大小写错误 → 仍报全等裁决错误(case variant 建议),不附跨引擎段", async () => {
450
- const { service } = setupChat(agentDir);
451
-
452
- const err = await service.execute(baseOpts(agentDir, { model: "pi-org/PI-MAIN" })).catch((e: unknown) => e);
453
- const msg = (err as Error).message;
454
- expect(msg).toContain('Model "pi-org/PI-MAIN" (paramOverride) is not a registry entry.');
455
- expect(msg).toContain("case variant");
456
- expect(msg).not.toContain("matches the registry of engine");
457
- });
458
-
459
- it("[D2-4] 跨引擎匹配按 strip thinking 后缀后的全名全等——含合法后缀的 id 仍可命中", async () => {
460
- const { service } = setupChat(agentDir);
461
- const err = await service.execute(baseOpts(agentDir, { model: `${ZCODE_FLASH}:max` })).catch((e: unknown) => e);
462
- expect((err as Error).message).toContain("This id matches the registry of engine 'zcode'.");
463
- });
464
- });
465
-
466
- // ============================================================
467
- // C. workflow 路径 SAR:非 pi 校验调用点(V2-4②/④)
468
- // ============================================================
469
-
470
- function makeSarFakeZcode(opts?: { withValidateModel?: boolean }): { engine: EnginePort; runs: Array<{ task: AgentCallOpts }> } {
471
- const engine = new ValidatingFakeEngine({ id: "zcode", models: [ZCODE_DEFAULT, ZCODE_FLASH], ...(opts ?? {}) });
472
- if (opts?.withValidateModel === false) {
473
- // 原型方法不可 deleteProperty——以同形状 plain object 替换(成员裁剪后的形态)
474
- const bare: EnginePort = {
475
- id: "zcode",
476
- capabilities: () => engine!.capabilities(),
477
- probe: () => engine!.probe(),
478
- run: (task, ctx) => engine!.run(task, ctx),
479
- interact: (handle, action) => engine!.interact(handle, action),
480
- read: (handle) => engine!.read(handle),
481
- };
482
- return { engine: bare, runs: engine.runs };
483
- }
484
- return { engine, runs: engine.runs };
485
- }
486
-
487
- function makeMockPiService() {
488
- const executeAndAwait = vi.fn(async (): Promise<{ content: string; toolCalls: never[] }> => ({
489
- content: "from-pi",
490
- toolCalls: [],
491
- }));
492
- // [D4 聚合连带] dev SAR 构造经 createPiEngine(() => service.asEngineService) 取服务
493
- // 视图,PiEngine.run 的 workflow 域委托消费 service.executeAndAwait——mock 需同时
494
- // 提供两者(asEngineService 视图与本体共享同一 spy)。
495
- const service = { executeAndAwait, asEngineService: { executeAndAwait } };
496
- return { service: service as unknown as SubagentServiceType, executeAndAwait };
497
- }
498
-
499
- function sarOpts(overrides?: Partial<AgentCallOpts>): AgentCallOpts {
500
- return { prompt: "task", description: "validation-test", ...overrides };
501
- }
502
-
503
- describe("workflow 路径 SAR:非 pi 引擎派发同步期 model 校验(u-h2 V2-4②/④)", () => {
504
- let tmpRoot: string;
505
- let agentDir: string;
506
- let prevDataDirEnv: string | undefined;
507
-
508
- beforeEach(() => {
509
- tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sar-model-validation-"));
510
- agentDir = path.join(tmpRoot, "pi-agent");
511
- fs.mkdirSync(agentDir, { recursive: true });
512
- prevDataDirEnv = process.env["XYZ_AGENT_DATA_DIR"];
513
- process.env["XYZ_AGENT_DATA_DIR"] = path.join(tmpRoot, "engine-data");
514
- clearEngines();
515
- fs.mkdirSync(path.join(agentDir, "subagents"), { recursive: true });
516
- fs.writeFileSync(
517
- path.join(agentDir, "subagents", "config.json"),
518
- JSON.stringify({ version: 1, maxConcurrent: 6 }),
519
- );
520
- });
521
-
522
- afterEach(() => {
523
- if (prevDataDirEnv === undefined) delete process.env["XYZ_AGENT_DATA_DIR"];
524
- else process.env["XYZ_AGENT_DATA_DIR"] = prevDataDirEnv;
525
- clearEngines();
526
- fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
527
- vi.clearAllMocks();
528
- });
529
-
530
- it("[V2-4②] agent({engine:'zcode', model:<zcode id>}) → 不回归:engine.run 收到原样 model", async () => {
531
- const zcode = makeSarFakeZcode();
532
- registerEngine("zcode", () => zcode.engine);
533
- const pi = makeMockPiService();
534
- const sar = new SubprocessAgentRunner({ subagentService: pi.service });
535
-
536
- const result = await sar.run(sarOpts({ engine: "zcode", model: ZCODE_FLASH }), new AbortController().signal);
537
-
538
- expect(result.error).toBeUndefined();
539
- expect(zcode.runs).toHaveLength(1);
540
- expect(zcode.runs[0]!.task.model).toBe(ZCODE_FLASH);
541
- expect(pi.executeAndAwait).not.toHaveBeenCalled();
542
- });
543
-
544
- it("[V2-4④] agent({engine:'zcode', model:<pi id>}) → 同步期场景 2 错误入 result.error,不进 engine.run", async () => {
545
- const zcode = makeSarFakeZcode();
546
- registerEngine("zcode", () => zcode.engine);
547
- const pi = makeMockPiService();
548
- const sar = new SubprocessAgentRunner({ subagentService: pi.service });
549
-
550
- const result = await sar.run(sarOpts({ engine: "zcode", model: PI_ID }), new AbortController().signal);
551
-
552
- // 「不 reject」契约:错误入 result.error(与 chat 路径同文案)
553
- expect(result.error).toContain(`model '${PI_ID}' is not available on engine 'zcode'.`);
554
- expect(result.error).toContain("Engine registries are independent");
555
- expect(result.error).toContain(ZCODE_FLASH);
556
- expect(result.error).toContain("engine default");
557
- // 同步期拒绝:未进入引擎执行(与 chat 路径 V2-2 同语义,无 prepare 期晚炸)
558
- expect(zcode.runs).toHaveLength(0);
559
- expect(pi.executeAndAwait).not.toHaveBeenCalled();
560
- });
561
-
562
- it("[现状语义] SAR fake 引擎未实现 validateModel → 校验透传,既有路由测试场景零回归", async () => {
563
- const zcode = makeSarFakeZcode({ withValidateModel: false });
564
- registerEngine("zcode", () => zcode.engine);
565
- const pi = makeMockPiService();
566
- const sar = new SubprocessAgentRunner({ subagentService: pi.service });
567
-
568
- const result = await sar.run(sarOpts({ engine: "zcode", model: "any-provider/any-model" }), new AbortController().signal);
569
-
570
- expect(result.error).toBeUndefined();
571
- expect(zcode.runs).toHaveLength(1);
572
- expect(zcode.runs[0]!.task.model).toBe("any-provider/any-model");
573
- });
574
-
575
- it("[V2-4①] workflow pi 路径不受影响:pi id 走 executeAndAwait 委托", async () => {
576
- const pi = makeMockPiService();
577
- const sar = new SubprocessAgentRunner({ subagentService: pi.service });
578
-
579
- const result = await sar.run(sarOpts({ model: PI_ID }), new AbortController().signal);
580
-
581
- expect(result.content).toBe("from-pi");
582
- expect(pi.executeAndAwait).toHaveBeenCalledTimes(1);
583
- });
584
- });
585
-
586
- // ============================================================
587
- // D. P2-1 时序回归:record 注册 / emit 顺序
588
- //
589
- // 确定性依据(flake 加固 2026-09-05):record entry(createRecordForMode → store.register
590
- // → pi.appendEntry,record-store.ts 同步调用)与 pending:register emit
591
- //(emitPendingRegister)都产生于 execute()/executeViaEngine() 的同一同步段——
592
- // service.execute() 的 Promise resolve 时两笔必然已写入 eventLog。
593
- //
594
- // 因此本组断言【不等待 detached 引擎链】(不 waitFor zcode.runs / mockSpawn):那条链
595
- // 会继续向 eventLog 追加噪声(archive 的第二笔 subagent-record entry、pending:unregister),
596
- // waitFor 的轮询窗口让断言评估时机落在链推进的任意交错点——重负载下即偶发失败形态
597
- //(单跑全绿、全量偶发)。顺序锁定语义不变:第一笔 entry:subagent-record 必须先于
598
- // 第一笔 emit:pending:register。
599
- // ============================================================
600
-
601
- describe("P2-1 路由先行 reorder 的时序回归锁定", () => {
602
- let agentDir: string;
603
-
604
- beforeEach(() => {
605
- configureCore({ dataRoot: () => "/fake-p21-data-root", log: () => {} });
606
- agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "p21-reorder-"));
607
- writeGlobalConfig(agentDir, { version: 1, maxConcurrent: 6 });
608
- });
609
-
610
- afterEach(() => {
611
- clearEngines();
612
- resetCoreForTests();
613
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
614
- vi.clearAllMocks();
615
- });
616
-
617
- it("[pi 缺省] record entry 写点先于 pending:register emit(既有顺序不变)", async () => {
618
- const { service, eventLog } = setupChat(agentDir);
619
-
620
- await service.execute(baseOpts(agentDir));
621
-
622
- const entryIdx = eventLog.indexOf("entry:subagent-record");
623
- const registerIdx = eventLog.indexOf("emit:pending:register");
624
- expect(entryIdx).toBeGreaterThanOrEqual(0);
625
- expect(registerIdx).toBeGreaterThanOrEqual(0);
626
- expect(entryIdx).toBeLessThan(registerIdx);
627
- });
628
-
629
- it("[非 pi 校验失败] 零 record entry、零 pending:register emit(不产生孤儿 record)", async () => {
630
- const { service, eventLog, zcode } = setupChat(agentDir);
631
-
632
- await expect(service.execute(baseOpts(agentDir, { engine: "zcode", model: PI_ID }))).rejects.toThrow(
633
- /is not available on engine 'zcode'/,
634
- );
635
-
636
- expect(eventLog).not.toContain("entry:subagent-record");
637
- expect(eventLog).not.toContain("emit:pending:register");
638
- expect(zcode.runs.length).toBe(0);
639
- expect(service["collectRecords"](10, "all")).toHaveLength(0);
640
- });
641
-
642
- it("[非 pi 成功] record entry → pending:register 顺序与 pi 路径一致", async () => {
643
- const { service, eventLog, zcode, pi } = setupChat(agentDir);
644
-
645
- // execute() resolve 即同步段完成点:entry 与 emit 两笔此时已确定写入 eventLog
646
- //(不依赖 detached 引擎链推进——等待该链只会给断言引入负载敏感的评估时机,
647
- // 且链的后续写入〔archive 第二笔 entry / pending:unregister〕与断言语义无关)。
648
- await service.execute(baseOpts(agentDir, { engine: "zcode", model: ZCODE_FLASH }));
649
-
650
- const entryIdx = eventLog.indexOf("entry:subagent-record");
651
- const registerIdx = eventLog.indexOf("emit:pending:register");
652
- expect(entryIdx).toBeGreaterThanOrEqual(0);
653
- expect(registerIdx).toBeGreaterThan(entryIdx);
654
- // 引擎派发事实单独确定性等待(runs 数组只增不删,与 eventLog 时序断言解耦)
655
- await vi.waitFor(() => expect(zcode.runs.length).toBe(1));
656
- // 链静止后再离开用例(afterEach rmSync 竞争防御,见 helper 注释)
657
- await settleEngineRunChain(pi);
658
- });
659
- });
660
-
661
- // ============================================================
662
- // E. zcode 引擎 validateModel 委托(真实 preparer + 临时 v2 config)
663
- // ============================================================
664
-
665
- describe("ZcodeEngine.validateModel 委托 resolveZcodeModelRef(u-h2 D2-2)", () => {
666
- let tmpRoot: string;
667
- let v2Path: string;
668
-
669
- const PROVIDER_A = "builtin:bigmodel-coding-plan";
670
- const PROVIDER_B = "e512d53e-test-provider";
671
-
672
- beforeEach(() => {
673
- tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "zcode-validate-model-"));
674
- v2Path = path.join(tmpRoot, "v2-config.json");
675
- fs.writeFileSync(
676
- v2Path,
677
- JSON.stringify({
678
- provider: {
679
- [PROVIDER_A]: {
680
- options: { apiKey: "key-a", baseURL: "https://a.example" },
681
- models: { "GLM-5.3": {}, "GLM-5.3-Flash": {} },
682
- },
683
- [PROVIDER_B]: {
684
- options: { apiKey: "key-b" },
685
- models: { "mimo-v2.5-pro": {} },
686
- },
687
- },
688
- }),
689
- );
690
- });
691
-
692
- afterEach(() => {
693
- fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
694
- });
695
-
696
- function makeEngine(): ZcodeEngine {
697
- return new ZcodeEngine({ engineDataDir: () => path.join(tmpRoot, "data"), sources: { v2ConfigPath: v2Path } });
698
- }
699
-
700
- it("显式全名命中 → canonicalRef 原样返回", () => {
701
- expect(makeEngine().validateModel(ZCODE_FLASH).canonicalRef).toBe(ZCODE_FLASH);
702
- });
703
-
704
- it("undefined → 引擎缺省模型(ZCODE_FALLBACK_DEFAULT_MODEL 链)", () => {
705
- expect(makeEngine().validateModel(undefined).canonicalRef).toBe(ZCODE_DEFAULT);
706
- });
707
-
708
- it("短名 → 缺省 provider 归一化 canonical 全名", () => {
709
- expect(makeEngine().validateModel("GLM-5.3-Flash").canonicalRef).toBe(ZCODE_FLASH);
710
- });
711
-
712
- it("未知 provider → ZcodePrepareError(model_not_available)", () => {
713
- expect(() => makeEngine().validateModel("pi-org/pi-main")).toThrow(ZcodePrepareError);
714
- try {
715
- makeEngine().validateModel("pi-org/pi-main");
716
- } catch (err) {
717
- expect((err as ZcodePrepareError).code).toBe("model_not_available");
718
- }
719
- });
720
-
721
- it("provider 存在但未配 apiKey → ZcodePrepareError(engine_credential_missing)", () => {
722
- // withKey 非空(PROVIDER_A 带凭据)+ 目标 provider 无 key → 命中 preparer「provider
723
- // 存在但未配置 apiKey」分支(非「找不到任何带 apiKey provider」分支)
724
- fs.writeFileSync(
725
- v2Path,
726
- JSON.stringify({
727
- provider: {
728
- [PROVIDER_A]: { options: { apiKey: "key-a" }, models: { "GLM-5.3": {} } },
729
- "no-key-provider": { options: { baseURL: "https://x.example" }, models: { M1: {} } },
730
- },
731
- }),
732
- );
733
- try {
734
- makeEngine().validateModel("no-key-provider/M1");
735
- throw new Error("should have thrown ZcodePrepareError");
736
- } catch (err) {
737
- expect(err).toBeInstanceOf(ZcodePrepareError);
738
- expect((err as ZcodePrepareError).code).toBe("engine_credential_missing");
739
- }
740
- });
741
- });