@zhushanwen/subagent-core 0.5.1 → 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 (318) 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-Dv4QhSJ_.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +835 -668
  6. package/dist/{types-Dv4QhSJ_.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +835 -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 +14123 -15467
  20. package/dist/index.d.cts +1179 -1540
  21. package/dist/index.d.ts +1179 -1540
  22. package/dist/index.js +7694 -10986
  23. package/dist.bundle/index.cjs +33485 -0
  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__/agent-registry.test.ts +1 -1
  30. package/src/execution/__tests__/alive-store.test.ts +1 -1
  31. package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
  32. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
  33. package/src/execution/__tests__/collect-budget.test.ts +291 -0
  34. package/src/execution/__tests__/collect-coordinator-service.test.ts +316 -0
  35. package/src/execution/__tests__/collect-coordinator.test.ts +371 -0
  36. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +229 -0
  37. package/src/execution/__tests__/config-collect-sync.test.ts +120 -0
  38. package/src/execution/__tests__/config.test.ts +1 -1
  39. package/src/execution/__tests__/delivery-methods.test.ts +179 -237
  40. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
  41. package/src/execution/__tests__/execute-nesting.test.ts +20 -13
  42. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +21 -34
  43. package/src/execution/__tests__/finalized-marker.test.ts +1 -1
  44. package/src/execution/__tests__/gc-timer.test.ts +8 -23
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
  46. package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
  47. package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
  48. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
  49. package/src/execution/__tests__/idle-gc.test.ts +200 -0
  50. package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +1 -1
  52. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
  53. package/src/execution/__tests__/notify-batch.test.ts +500 -0
  54. package/src/execution/__tests__/notify-ledger.test.ts +27 -0
  55. package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
  56. package/src/execution/__tests__/record-entry-collect.test.ts +102 -0
  57. package/src/execution/__tests__/record-store.test.ts +60 -6
  58. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
  59. package/src/execution/__tests__/relay-env.test.ts +12 -1
  60. package/src/execution/__tests__/round-supervisor.test.ts +293 -0
  61. package/src/execution/__tests__/session-file-gc.test.ts +1 -1
  62. package/src/execution/__tests__/session-pending.test.ts +119 -17
  63. package/src/execution/__tests__/start-collect-guard.test.ts +255 -0
  64. package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
  65. package/src/execution/__tests__/subagent-actions-core.test.ts +6 -0
  66. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +14 -11
  67. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
  68. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
  69. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
  70. package/src/execution/__tests__/subagent-service.test.ts +2 -2
  71. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
  72. package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
  73. package/src/execution/__tests__/sync-collect-recovery.test.ts +1102 -0
  74. package/src/execution/__tests__/tombstone-store.test.ts +1 -1
  75. package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
  76. package/src/execution/__tests__/worktree-git-ops.test.ts +1 -1
  77. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +2 -2
  78. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +1 -1
  79. package/src/execution/__tests__/worktree-registry.test.ts +1 -1
  80. package/src/execution/agent-registry.ts +6 -2
  81. package/src/execution/collect-coordinator.ts +200 -0
  82. package/src/execution/config.ts +77 -5
  83. package/src/execution/dialog-queue.ts +6 -73
  84. package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
  85. package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
  86. package/src/execution/engine/__tests__/common/event-journal.test.ts +1 -1
  87. package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
  88. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
  89. package/src/execution/engine/__tests__/common/pool-manager.test.ts +2 -2
  90. package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
  91. package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
  92. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
  93. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
  94. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
  95. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
  96. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
  97. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
  98. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
  99. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +82 -55
  100. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
  101. package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
  102. package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
  103. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +8 -3
  104. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
  105. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
  106. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
  107. package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
  108. package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
  109. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
  110. package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
  111. package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
  112. package/src/execution/engine/__tests__/registry.test.ts +206 -1
  113. package/src/execution/engine/__tests__/routing.test.ts +97 -0
  114. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
  115. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
  116. package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
  117. package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
  118. package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
  119. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
  120. package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
  121. package/src/execution/engine/client/client-options.ts +103 -0
  122. package/src/execution/engine/client/engine-client.ts +687 -0
  123. package/src/execution/engine/client/mirror.ts +167 -0
  124. package/src/execution/engine/client/pid-file.ts +392 -0
  125. package/src/execution/engine/client/reaper.ts +95 -0
  126. package/src/execution/engine/client/remote-engine.ts +441 -0
  127. package/src/execution/engine/client/reverse-router.ts +255 -0
  128. package/src/execution/engine/common/capability-gate.ts +109 -22
  129. package/src/execution/engine/common/errors.ts +9 -25
  130. package/src/execution/engine/common/event-journal.ts +4 -50
  131. package/src/execution/engine/common/journal-replay.ts +3 -1
  132. package/src/execution/engine/common/kill-chain.ts +18 -251
  133. package/src/execution/engine/common/nesting-guard.ts +18 -114
  134. package/src/execution/engine/common/pool-manager.ts +4 -0
  135. package/src/execution/engine/common/session-view-service.ts +56 -84
  136. package/src/execution/engine/config.ts +151 -0
  137. package/src/execution/engine/d8-compat.ts +346 -0
  138. package/src/execution/engine/engine-discovery-roots.ts +107 -0
  139. package/src/execution/engine/engine-discovery-scan.ts +269 -0
  140. package/src/execution/engine/engine-discovery.ts +30 -7
  141. package/src/execution/engine/engine-inspect-package.ts +282 -0
  142. package/src/execution/engine/engine-manifest.ts +244 -0
  143. package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
  144. package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
  145. package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
  146. package/src/execution/engine/host/host-bridge.ts +150 -0
  147. package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
  148. package/src/execution/engine/host/pi-host-binding.ts +91 -0
  149. package/src/execution/engine/host/spawned-children.ts +139 -0
  150. package/src/execution/engine/host-task-spec.ts +29 -2
  151. package/src/execution/engine/model-validation.ts +55 -2
  152. package/src/execution/engine/paths.ts +16 -41
  153. package/src/execution/engine/port.ts +48 -2
  154. package/src/execution/engine/registry.ts +153 -34
  155. package/src/execution/engine/routing.ts +225 -52
  156. package/src/execution/engine/types.ts +25 -200
  157. package/src/execution/execution-record.ts +7 -0
  158. package/src/execution/finalize-record.ts +40 -11
  159. package/src/execution/idle-gc.ts +89 -14
  160. package/src/execution/lifecycle-manager.ts +10 -7
  161. package/src/execution/lifecycle-predicates.ts +10 -8
  162. package/src/execution/notifier.ts +234 -25
  163. package/src/execution/notify-host.ts +21 -1
  164. package/src/execution/record-entry.ts +16 -0
  165. package/src/execution/record-store.ts +189 -31
  166. package/src/execution/relay-env.ts +19 -36
  167. package/src/execution/round-supervisor/domain.ts +89 -0
  168. package/src/execution/round-supervisor/index.ts +33 -0
  169. package/src/execution/round-supervisor/notify-accounting.ts +87 -0
  170. package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
  171. package/src/execution/round-supervisor/service-binding.test.ts +661 -0
  172. package/src/execution/round-supervisor/service-binding.ts +273 -0
  173. package/src/execution/round-supervisor/supervisor.ts +426 -0
  174. package/src/execution/session-pending.ts +68 -74
  175. package/src/execution/settled-watchdog.ts +39 -0
  176. package/src/execution/subagent-actions-core.ts +62 -6
  177. package/src/execution/subagent-service.ts +1161 -406
  178. package/src/execution/subprocess-agent-runner.ts +16 -8
  179. package/src/execution/sync-rebuild.ts +87 -0
  180. package/src/execution/types.ts +104 -117
  181. package/src/execution/ui-channels.ts +13 -214
  182. package/src/execution/ui-request-handler-factory.ts +7 -4
  183. package/src/execution/ui-request-observability.ts +2 -2
  184. package/src/execution/workflow-state-root.ts +67 -0
  185. package/src/execution/worktree-git-ops.ts +5 -0
  186. package/src/execution/worktree-manager.ts +11 -1
  187. package/src/index.ts +55 -26
  188. package/src/orchestration/__tests__/config-loader.test.ts +1 -1
  189. package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
  190. package/src/orchestration/__tests__/file-run-store-prune.test.ts +1 -1
  191. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +1 -1
  192. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +1 -1
  193. package/src/orchestration/__tests__/script-generate.test.ts +5 -5
  194. package/src/orchestration/__tests__/skill-discovery.test.ts +2 -2
  195. package/src/orchestration/__tests__/workflow-files.test.ts +10 -10
  196. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +1 -1
  197. package/src/orchestration/execute-agent-call.ts +1 -1
  198. package/src/orchestration/file-run-store.ts +144 -59
  199. package/src/orchestration/models/types.ts +43 -42
  200. package/src/shared/__tests__/atomic-write.test.ts +1 -1
  201. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +5 -5
  202. package/src/shared/zcode-model-ref.ts +31 -0
  203. package/dist/chunk-43ONBFZX.js +0 -240
  204. package/dist/chunk-A4IVZWVX.js +0 -31
  205. package/dist/chunk-APZY4IME.js +0 -27
  206. package/dist/chunk-V3VHZ2VX.js +0 -59
  207. package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
  208. package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
  209. package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
  210. package/dist/execution/engine/engines/zcode/constants.js +0 -54
  211. package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
  212. package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
  213. package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
  214. package/dist/execution/engine/engines/zcode/reader.js +0 -9
  215. package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
  216. package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
  217. package/src/__tests__/session-runner.test.ts +0 -69
  218. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  219. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
  220. package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
  221. package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
  222. package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
  223. package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
  224. package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
  225. package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
  226. package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
  227. package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
  228. package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
  229. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
  230. package/src/execution/__tests__/output-collector.test.ts +0 -469
  231. package/src/execution/__tests__/pi-invocation.test.ts +0 -134
  232. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
  233. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  234. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
  235. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
  236. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
  237. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
  238. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  239. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  240. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  241. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
  242. package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
  243. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
  244. package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
  245. package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
  246. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
  247. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
  248. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
  249. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
  250. package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
  251. package/src/execution/__tests__/spawn-args.test.ts +0 -446
  252. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
  253. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  254. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
  255. package/src/execution/__tests__/spawned-children.test.ts +0 -236
  256. package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
  257. package/src/execution/__tests__/stdin-writer.test.ts +0 -464
  258. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
  259. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  260. package/src/execution/__tests__/timeout-integration.test.ts +0 -616
  261. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  262. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  263. package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
  264. package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
  265. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
  266. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  267. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
  268. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  269. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
  270. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  271. package/src/execution/engine/common/schema-emulation.ts +0 -189
  272. package/src/execution/engine/common/session-view-projection.ts +0 -51
  273. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
  274. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  275. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
  276. package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
  277. package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
  278. package/src/execution/engine/engines/pi/output-collector.ts +0 -317
  279. package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
  280. package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
  281. package/src/execution/engine/engines/pi/reader.ts +0 -59
  282. package/src/execution/engine/engines/pi/registration.ts +0 -37
  283. package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
  284. package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
  285. package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
  286. package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
  287. package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
  288. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
  289. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
  290. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
  291. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
  292. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
  293. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
  294. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  295. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
  296. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
  297. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
  298. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
  299. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
  300. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
  301. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
  302. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
  303. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
  304. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
  305. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
  306. package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
  307. package/src/execution/engine/engines/zcode/connection.ts +0 -598
  308. package/src/execution/engine/engines/zcode/constants.ts +0 -176
  309. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
  310. package/src/execution/engine/engines/zcode/parser.ts +0 -102
  311. package/src/execution/engine/engines/zcode/preparer.ts +0 -235
  312. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  313. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  314. package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
  315. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
  316. package/src/execution/round-settlement.ts +0 -93
  317. package/src/execution/ui-request-queue.ts +0 -215
  318. package/src/shared/schema-env.ts +0 -44
@@ -0,0 +1,516 @@
1
+ // EngineClient 集成测试(W2):fake 引擎 CLI 子进程(__fixtures__/fake-engine.mjs)。
2
+ //
3
+ // 覆盖(impl-plan §2.2 / 验收 A6①⑦ / A8①④ / §7.2 R9-2/R9-2b/R9-3):
4
+ // 帧往返 / initialize 握手与 pidfile 写入 / 反向通知路由(含镜像落项)/
5
+ // host/askUser ack 两阶段 / 超时域二分(数据面 10s 判死 vs 交互面不计时,fake timers)/
6
+ // 崩溃重建 3 次退避 + 不可用 / 版本协商 mismatch / killAll 组杀(POSIX 真进程)/ dispose。
7
+ // stdin fd 不外泄(R9-4②)由 SDK spawn 单测兜底,core 侧不重测。
8
+
9
+ import { spawn } from "node:child_process";
10
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
15
+
16
+ import { EngineClient, type RunRoute } from "../engine-client.ts";
17
+ import { isProcessAlive, pidfilePath, readPidfile, writePidfileAtomic } from "../pid-file.ts";
18
+
19
+ const FAKE_ENGINE = fileURLToPath(new URL("./__fixtures__/fake-engine.mjs", import.meta.url));
20
+
21
+ let dataDir: string;
22
+
23
+ beforeEach(() => {
24
+ dataDir = mkdtempSync(join(tmpdir(), "w2-engine-client-"));
25
+ });
26
+
27
+ afterEach(() => {
28
+ rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
29
+ vi.useRealTimers();
30
+ });
31
+
32
+ interface ClientHandle {
33
+ client: EngineClient;
34
+ cleanup: () => Promise<void>;
35
+ }
36
+
37
+ function makeClient(
38
+ overrides: Partial<ConstructorParameters<typeof EngineClient>[0]> = {},
39
+ ): ClientHandle {
40
+ const client = new EngineClient({
41
+ engineId: "fake",
42
+ command: process.execPath,
43
+ args: [FAKE_ENGINE],
44
+ hostKind: "test",
45
+ hostVersion: "test-host-1.0",
46
+ dataDir,
47
+ envPrefixes: [],
48
+ ...overrides,
49
+ });
50
+ return {
51
+ client,
52
+ cleanup: () => client.dispose(),
53
+ };
54
+ }
55
+
56
+ /** 轮询等待谓词成立(真实子进程信号传播有毫秒级延迟)。 */
57
+ async function waitFor(predicate: () => boolean, timeoutMs = 5_000, stepMs = 25): Promise<void> {
58
+ const deadline = Date.now() + timeoutMs;
59
+ while (Date.now() < deadline) {
60
+ if (predicate()) return;
61
+ await new Promise((resolve) => setTimeout(resolve, stepMs));
62
+ }
63
+ throw new Error("waitFor: condition not met within timeout");
64
+ }
65
+
66
+ /** spawn 一个立刻退出的进程,取其 pid(已死宿主构造)。 */
67
+ function spawnDeadHost(): Promise<number> {
68
+ return new Promise((resolve) => {
69
+ const child = spawn(process.execPath, ["-e", "process.exit(0)"]);
70
+ child.once("exit", () => resolve(child.pid!));
71
+ });
72
+ }
73
+
74
+ describe("EngineClient 帧往返与握手", () => {
75
+ it("ensureConnected:spawn + initialize 握手 → ready(进程即引擎 CLI,pid 独立)", async () => {
76
+ const { client, cleanup } = makeClient();
77
+ await client.ensureConnected();
78
+ expect(client.currentState).toBe("ready");
79
+ expect(client.enginePid).toBeDefined();
80
+ expect(client.enginePid).not.toBe(process.pid);
81
+ await cleanup();
82
+ });
83
+
84
+ it("spawn 成功后原子写实例维度 pidfile(engine.<hostKind>.<hostPid>.pid,含 enginePid/hostPid/engineStartTime);dispose 清理", async () => {
85
+ const { client, cleanup } = makeClient();
86
+ await client.ensureConnected();
87
+ const path = pidfilePath(dataDir, "fake", "test", process.pid);
88
+ expect(existsSync(path)).toBe(true);
89
+ const content = readPidfile(path);
90
+ expect(content).toMatchObject({
91
+ enginePid: client.enginePid,
92
+ hostPid: process.pid,
93
+ hostKind: "test",
94
+ engineId: "fake",
95
+ });
96
+ expect(content?.engineStartTime).not.toBeNull(); // POSIX 真值(R9-3b)
97
+ await cleanup();
98
+ expect(existsSync(path)).toBe(false);
99
+ });
100
+
101
+ it("ping 往返(请求-应答 id 关联;不设墙钟)", async () => {
102
+ const { client, cleanup } = makeClient();
103
+ await client.ping();
104
+ expect(client.currentState).toBe("ready");
105
+ await cleanup();
106
+ });
107
+
108
+ it("版本协商越界:engine_protocol_mismatch(含双方版本 + 升级指引)+ 该引擎标记不可用", async () => {
109
+ const { client, cleanup } = makeClient({
110
+ args: [FAKE_ENGINE, "--protocol-version", "99"],
111
+ });
112
+ await expect(client.ensureConnected()).rejects.toMatchObject({ code: "engine_protocol_mismatch" });
113
+ expect(client.currentState).toBe("unavailable");
114
+ await expect(client.ensureConnected()).rejects.toMatchObject({ code: "engine_protocol_mismatch" });
115
+ await expect(cleanup()).resolves.toBeUndefined(); // dispose 幂等不抛
116
+ }, 20_000);
117
+
118
+ it("initialize 应答与 manifest 诊断不一致 → warn 留痕(仅诊断,不阻断 ready)", async () => {
119
+ const warnMessages: string[] = [];
120
+ const { client, cleanup } = makeClient({
121
+ manifestDiagnostics: {
122
+ capabilities: {
123
+ schemaEnforcement: "native",
124
+ steer: "native",
125
+ conversation: "native",
126
+ personaInjection: "file",
127
+ eventGranularity: "coarse",
128
+ sandbox: "native",
129
+ sessionRead: "partial",
130
+ resume: "native",
131
+ interrupt: "native",
132
+ permissionMode: "native",
133
+ maxTurns: true,
134
+ },
135
+ },
136
+ });
137
+ const original = console.warn;
138
+ console.warn = (...parts: unknown[]) => {
139
+ warnMessages.push(parts.join(" "));
140
+ };
141
+ try {
142
+ await client.ensureConnected();
143
+ } finally {
144
+ console.warn = original;
145
+ }
146
+ expect(client.currentState).toBe("ready");
147
+ expect(warnMessages.some((m) => m.includes("capabilities differ from manifest"))).toBe(true);
148
+ await cleanup();
149
+ });
150
+ });
151
+
152
+ describe("EngineClient 反向通知路由(run 作用域 + 镜像)", () => {
153
+ it("event 通知 / streamDelta / poolResolved / handleReady 路由到 run 作用域回调;handleReady 回填 partial handle", async () => {
154
+ const events: unknown[] = [];
155
+ const deltas: string[] = [];
156
+ const poolKeys: string[] = [];
157
+ const readyPartials: Array<{ sessionRef: Record<string, string>; poolKey: string }> = [];
158
+ const route: RunRoute = {
159
+ onEvent: (e) => {
160
+ events.push(e);
161
+ },
162
+ onStreamDelta: (d) => {
163
+ deltas.push(d);
164
+ },
165
+ onPoolResolved: (p) => {
166
+ poolKeys.push(p);
167
+ },
168
+ onHandleReady: (partial) => {
169
+ readyPartials.push(partial);
170
+ },
171
+ };
172
+ const { client, cleanup } = makeClient({
173
+ args: [
174
+ FAKE_ENGINE,
175
+ "--run-actions",
176
+ JSON.stringify([
177
+ { op: "emit", seq: 1, event: { type: "text_delta", delta: "hello" } },
178
+ { op: "streamDelta", delta: "stream-1" },
179
+ { op: "poolResolved", poolKey: "pool-x" },
180
+ { op: "handleReady", sessionRef: { sessionId: "s-1" }, poolKey: "pool-x" },
181
+ ]),
182
+ ],
183
+ });
184
+ const unregister = client.registerRunRoute("run-1", route);
185
+ await client.ensureConnected();
186
+ const result = (await client.request("run", {
187
+ runId: "run-1",
188
+ task: { prompt: "p" },
189
+ ctx: { poolKey: "pool-x", cwd: dataDir },
190
+ })) as { handle: { sessionRef: Record<string, string> }; outcome: { content: string } };
191
+ expect(result.outcome.content).toBe("fake-content-run-1");
192
+ expect(events).toContainEqual({ type: "text_delta", delta: "hello" });
193
+ expect(deltas).toEqual(["stream-1"]);
194
+ expect(poolKeys).toEqual(["pool-x"]);
195
+ expect(readyPartials).toEqual([{ sessionRef: { sessionId: "s-1" }, poolKey: "pool-x" }]);
196
+ expect(client.getPartialHandle()).toEqual({ sessionRef: { sessionId: "s-1" }, poolKey: "pool-x" });
197
+ unregister();
198
+ await cleanup();
199
+ });
200
+
201
+ it("host/childSpawned + host/childStateChanged → 镜像落项/更新 + onMirrorChanged 广播", async () => {
202
+ const mirrorEvents: string[] = [];
203
+ const { client, cleanup } = makeClient({
204
+ onMirrorChanged: (e) => mirrorEvents.push(e.reason),
205
+ args: [
206
+ FAKE_ENGINE,
207
+ "--run-actions",
208
+ JSON.stringify([
209
+ { op: "childSpawned", pid: 4242, recordId: "rec-1" },
210
+ { op: "childStateChanged", pid: 4242, recordId: "rec-1", state: "exited", killed: true, exitCode: 3 },
211
+ ]),
212
+ ],
213
+ });
214
+ await client.ensureConnected();
215
+ await client.request("run", { runId: "run-1", task: { prompt: "p" }, ctx: { poolKey: "shared", cwd: dataDir } });
216
+ expect(mirrorEvents).toContain("childSpawned");
217
+ expect(mirrorEvents).toContain("childStateChanged");
218
+ expect(client.mirror.getEntry(4242)).toMatchObject({
219
+ pid: 4242,
220
+ state: "exited",
221
+ killed: true,
222
+ exitCode: 3,
223
+ });
224
+ await cleanup();
225
+ // dispose → 整体置死 + 清空(失效语义 2+3)
226
+ expect(client.mirror.size).toBe(0);
227
+ });
228
+
229
+ it("host/log 数据面 → 注入的 log 出口(缺省 logger facade 不抛)", async () => {
230
+ const logs: Array<{ level: string; component: string; message: string }> = [];
231
+ const { client, cleanup } = makeClient({
232
+ log: (p) => logs.push({ level: p.level, component: p.component, message: p.message }),
233
+ args: [
234
+ FAKE_ENGINE,
235
+ "--run-actions",
236
+ JSON.stringify([{ op: "log", level: "warn", component: "fake-comp", message: "log-line-1" }]),
237
+ ],
238
+ });
239
+ await client.ensureConnected();
240
+ await client.request("run", { runId: "run-1", task: { prompt: "p" }, ctx: { poolKey: "shared", cwd: dataDir } });
241
+ expect(logs).toContainEqual({ level: "warn", component: "fake-comp", message: "log-line-1" });
242
+ await cleanup();
243
+ });
244
+ });
245
+
246
+ describe("host/askUser ack 两阶段(R9-2)", () => {
247
+ it("uiRequestHandler 注入:先回 {ack:true},handler 结果异步补帧②(引擎 echo 结果 event)", async () => {
248
+ const events: Array<{ type: string; message?: string }> = [];
249
+ const { client, cleanup } = makeClient({
250
+ uiRequestHandler: async (req) => {
251
+ expect(req.method).toBe("select");
252
+ return { value: "answer-A" };
253
+ },
254
+ args: [
255
+ FAKE_ENGINE,
256
+ "--run-actions",
257
+ JSON.stringify([
258
+ { op: "emit", seq: 1, event: { type: "text_delta", delta: "before" } },
259
+ { op: "askUser", request: { method: "select", id: "q1", title: "pick" } },
260
+ ]),
261
+ ],
262
+ });
263
+ const unregister = client.registerRunRoute("run-1", {
264
+ onEvent: (e) => { events.push(e as { type: string; message?: string }); },
265
+ });
266
+ await client.ensureConnected();
267
+ await client.request("run", { runId: "run-1", task: { prompt: "p" }, ctx: { poolKey: "shared", cwd: dataDir } });
268
+ unregister();
269
+ const echo = events.find((e) => e.message?.startsWith("askUser-result:"));
270
+ expect(echo?.message).toContain('"value":"answer-A"');
271
+ await cleanup();
272
+ });
273
+
274
+ it("handler 抛错 → 引擎收 {cancelled:true}(dialog-queue 应答链同款兜底)", async () => {
275
+ const events: Array<{ type: string; message?: string }> = [];
276
+ const { client, cleanup } = makeClient({
277
+ uiRequestHandler: async () => {
278
+ throw new Error("handler exploded");
279
+ },
280
+ args: [
281
+ FAKE_ENGINE,
282
+ "--run-actions",
283
+ JSON.stringify([{ op: "askUser", request: { method: "confirm", id: "q2" } }]),
284
+ ],
285
+ });
286
+ const unregister = client.registerRunRoute("run-1", {
287
+ onEvent: (e) => { events.push(e as { type: string; message?: string }); },
288
+ });
289
+ await client.ensureConnected();
290
+ await client.request("run", { runId: "run-1", task: { prompt: "p" }, ctx: { poolKey: "shared", cwd: dataDir } });
291
+ unregister();
292
+ const echo = events.find((e) => e.message?.startsWith("askUser-result:"));
293
+ expect(echo?.message).toContain('"cancelled":true');
294
+ await cleanup();
295
+ });
296
+
297
+ it("无 uiRequestHandler → {unsupported:true}(未实现的交互能力,引擎自行降级)", async () => {
298
+ const events: Array<{ type: string; message?: string }> = [];
299
+ const { client, cleanup } = makeClient({
300
+ args: [
301
+ FAKE_ENGINE,
302
+ "--run-actions",
303
+ JSON.stringify([{ op: "askUser", request: { method: "input", id: "q3" } }]),
304
+ ],
305
+ });
306
+ const unregister = client.registerRunRoute("run-1", {
307
+ onEvent: (e) => { events.push(e as { type: string; message?: string }); },
308
+ });
309
+ await client.ensureConnected();
310
+ await client.request("run", { runId: "run-1", task: { prompt: "p" }, ctx: { poolKey: "shared", cwd: dataDir } });
311
+ unregister();
312
+ const echo = events.find((e) => e.message?.startsWith("askUser-result:"));
313
+ expect(echo?.message).toContain('"unsupported":true');
314
+ await cleanup();
315
+ });
316
+ });
317
+
318
+ describe("超时域二分(fake timers,R9-2 / R9-2b)", () => {
319
+ it("快答数据面挂起 10s → 引擎故障:杀进程 + 在途 run 失败(engine_crashed)", async () => {
320
+ vi.useFakeTimers();
321
+ const { client, cleanup } = makeClient({
322
+ args: [
323
+ FAKE_ENGINE,
324
+ "--run-actions",
325
+ // streamDelta 挂起宿主消费方 + 引擎长 delay 保持 run 在途(引擎内部 timer
326
+ // 是独立进程的真实时钟,不受宿主 fake timers 影响)
327
+ JSON.stringify([
328
+ { op: "streamDelta", delta: "hang" },
329
+ { op: "delay", ms: 60_000 },
330
+ ]),
331
+ ],
332
+ });
333
+ const unregister = client.registerRunRoute("run-1", {
334
+ onStreamDelta: () => new Promise<void>(() => {}), // 宿主消费方死锁
335
+ });
336
+ await client.ensureConnected();
337
+ const runFailed = expect(
338
+ client.request("run", { runId: "run-1", task: { prompt: "p" }, ctx: { poolKey: "shared", cwd: dataDir } }),
339
+ ).rejects.toMatchObject({ code: "engine_crashed" });
340
+ // 分步推进:streamDelta 反向请求是真实 IO(引擎收到 run → 回帧),guardTimer
341
+ // 注册于帧到达之后——一次性 advance(10s) 会与该 IO 竞态(推进时守卫尚未注册)。
342
+ // 循环小步推进直到杀链生效(teardown → state 离开 ready)。
343
+ for (let i = 0; i < 200 && client.currentState === "ready"; i += 1) {
344
+ await vi.advanceTimersByTimeAsync(500);
345
+ }
346
+ await runFailed;
347
+ expect(client.currentState).not.toBe("ready"); // 杀链已执行
348
+ unregister();
349
+ await cleanup();
350
+ }, 20_000);
351
+
352
+ it("人机交互面 ack 后永不计时:handler 挂 60s 不判引擎故障(R9-2 负向 + ADR-0047)", async () => {
353
+ vi.useFakeTimers();
354
+ const { client, cleanup } = makeClient({
355
+ uiRequestHandler: () => new Promise(() => {}), // 用户永不回答
356
+ args: [
357
+ FAKE_ENGINE,
358
+ "--run-actions",
359
+ JSON.stringify([{ op: "askUser", request: { method: "select", id: "q-hang" } }]),
360
+ ],
361
+ });
362
+ await client.ensureConnected();
363
+ const enginePid = client.enginePid!;
364
+ void client
365
+ .request("run", { runId: "run-1", task: { prompt: "p" }, ctx: { poolKey: "shared", cwd: dataDir } })
366
+ .catch(() => {});
367
+ await vi.advanceTimersByTimeAsync(60_000);
368
+ // ack 已回(引擎继续跑),handler 挂起不触发任何杀链
369
+ expect(client.currentState).toBe("ready");
370
+ expect(client.enginePid).toBe(enginePid);
371
+ expect(isProcessAlive(enginePid)).toBe(true);
372
+ // 负向断言完成后必须走杀链收尾:不 dispose = 引擎进程泄漏为常驻孤儿
373
+ // (fake 引擎 stdin EOF 后保活不自灭),且带 exit 监听的 client 跨文件存活。
374
+ vi.useRealTimers(); // 杀链(dispose 帧 3s 上界 / SIGKILL 收尸)走真实时钟
375
+ await cleanup();
376
+ }, 20_000);
377
+ });
378
+
379
+ describe("引擎崩溃与重建(A8①③)", () => {
380
+ it("run 中引擎进程死亡:在途 run 失败(engine_crashed + stderr 尾)+ 镜像清空 + pidfile 清理", async () => {
381
+ const { client, cleanup } = makeClient({
382
+ args: [
383
+ FAKE_ENGINE,
384
+ "--run-actions",
385
+ JSON.stringify([
386
+ { op: "childSpawned", pid: 5151, recordId: "rec-crash" },
387
+ { op: "exit", code: 137, stderr: "mid-run crash sample\n" },
388
+ ]),
389
+ ],
390
+ });
391
+ await client.ensureConnected();
392
+ expect(client.enginePid).toBeDefined();
393
+ const path = pidfilePath(dataDir, "fake", "test", process.pid);
394
+ await expect(
395
+ client.request("run", { runId: "run-1", task: { prompt: "p" }, ctx: { poolKey: "shared", cwd: dataDir } }),
396
+ ).rejects.toSatisfy((err: Error & { code?: string }) => {
397
+ expect(err.code).toBe("engine_crashed");
398
+ expect(err.message).toContain("stderr tail");
399
+ expect(err.message).toContain("mid-run crash sample");
400
+ return true;
401
+ });
402
+ expect(client.currentState).toBe("exited");
403
+ expect(client.mirror.size).toBe(0); // 崩溃后镜像清空(isResumable 回落「无句柄」)
404
+ await waitFor(() => !existsSync(path)); // pidfile 随引擎死亡清理
405
+ await cleanup();
406
+ });
407
+
408
+ it("stderr 常驻排空:环形缓冲只留尾 400 字符(宿主侧不落盘)", async () => {
409
+ const { client, cleanup } = makeClient({ args: [FAKE_ENGINE, "--mode", "crash"] });
410
+ // crash 模式:spawn 即死(stderr 写 600+ 字符样本)→ initialize 无应答 →
411
+ // 重建循环走完(真实退避 1+2+4=7s)→ 标记不可用 reject engine_crashed。
412
+ await expect(client.ensureConnected()).rejects.toMatchObject({ code: "engine_crashed" });
413
+ expect(client.stderrTailText.length).toBeLessThanOrEqual(401);
414
+ expect(client.stderrTailText).toContain("x"); // 尾部内容保留(头部被截)
415
+ expect(existsSync(pidfilePath(dataDir, "fake", "test", process.pid))).toBe(false);
416
+ await cleanup();
417
+ }, 30_000);
418
+
419
+ it("崩溃重建:初建失败 + 3 次重建退避 1s/2s/4s 全败 → 标记不可用(恒 throw,含恢复指引)", async () => {
420
+ const { client, cleanup } = makeClient({ args: [FAKE_ENGINE, "--mode", "crash"] });
421
+ // 真实退避序列:初建(即时崩) → 1s → 重建1(崩) → 2s → 重建2(崩) → 4s →
422
+ // 重建3(崩) → 不可用。每次 spawn→崩溃是真实子进程 IO,无法用 fake timers
423
+ // 推进(advance 不等 IO),故整段走真实时钟(约 7s + 启动开销)。
424
+ await expect(client.ensureConnected()).rejects.toSatisfy(
425
+ (err: Error & { code?: string; recovery?: string }) => {
426
+ expect(err.code).toBe("engine_crashed");
427
+ expect(err.message).toContain("failed to start after 4 attempts");
428
+ expect(err.recovery).toContain("unavailable until the next host start");
429
+ return true;
430
+ },
431
+ );
432
+ expect(client.currentState).toBe("unavailable");
433
+ await expect(client.ensureConnected()).rejects.toMatchObject({ code: "engine_crashed" });
434
+ await cleanup();
435
+ }, 30_000);
436
+
437
+ it("重建恢复:进程意外死亡后下次 ensureConnected 重建成功(新 pid);连接复用不重建", async () => {
438
+ const { client, cleanup } = makeClient();
439
+ await client.ensureConnected();
440
+ const firstPid = client.enginePid!;
441
+ process.kill(firstPid, "SIGKILL"); // 模拟崩溃(单杀制造 exit 事件)
442
+ await waitFor(() => client.currentState === "exited");
443
+ await client.ensureConnected();
444
+ expect(client.currentState).toBe("ready");
445
+ expect(client.enginePid).not.toBe(firstPid);
446
+ const pidAfterRebuild = client.enginePid;
447
+ await client.ensureConnected(); // 连接复用:不重建
448
+ expect(client.enginePid).toBe(pidAfterRebuild);
449
+ await cleanup();
450
+ });
451
+ });
452
+
453
+ describe("收割(POSIX 进程组;范围 = 一代子进程 + 组内后代,R9-1)", () => {
454
+ it("killAll:负 pid 组杀连带组内后代(引擎 + grandchild 全灭)+ 镜像置死 + pidfile 清理", async () => {
455
+ const { client } = makeClient({
456
+ args: [
457
+ FAKE_ENGINE,
458
+ "--run-actions",
459
+ JSON.stringify([{ op: "spawnGrandchild", recordId: "rec-grand" }]),
460
+ ],
461
+ });
462
+ const unregister = client.registerRunRoute("run-1", {});
463
+ void client
464
+ .ensureConnected()
465
+ .then(() =>
466
+ client.request("run", { runId: "run-1", task: { prompt: "p" }, ctx: { poolKey: "shared", cwd: dataDir } }),
467
+ )
468
+ .catch(() => {});
469
+ await waitFor(() => client.mirror.size > 0);
470
+ const grandchildPid = client.mirror.snapshot()[0]!.pid;
471
+ const enginePid = client.enginePid!;
472
+ expect(isProcessAlive(grandchildPid)).toBe(true);
473
+
474
+ await client.killAll("test harvest");
475
+ // 组内双亡(detached 后代不覆盖——R9-1 已接受代价,不在断言范围)
476
+ await waitFor(() => isProcessAlive(grandchildPid) === false);
477
+ await waitFor(() => isProcessAlive(enginePid) === false);
478
+ expect(client.mirror.size).toBe(0);
479
+ expect(existsSync(pidfilePath(dataDir, "fake", "test", process.pid))).toBe(false);
480
+ unregister();
481
+ }, 20_000);
482
+
483
+ it("dispose 幂等且引擎自然退出;killAll 在进程已死时不抛", async () => {
484
+ const { client, cleanup } = makeClient();
485
+ await client.ensureConnected();
486
+ const enginePid = client.enginePid!;
487
+ await client.dispose();
488
+ await waitFor(() => isProcessAlive(enginePid) === false);
489
+ expect(existsSync(pidfilePath(dataDir, "fake", "test", process.pid))).toBe(false);
490
+ await expect(client.dispose()).resolves.toBeUndefined(); // 幂等
491
+ await expect(client.killAll("again")).resolves.toBeUndefined();
492
+ await cleanup();
493
+ });
494
+ });
495
+
496
+ describe("pidfile 启动期清扫(EngineClient 接线,R9-3)", () => {
497
+ it("首次 ensureConnected 前清扫同 id 陈旧 pidfile(宿主死残留被删;自己的文件照常写入)", async () => {
498
+ const staleHostPid = await spawnDeadHost();
499
+ const stalePath = pidfilePath(dataDir, "fake", "pi", staleHostPid);
500
+ writePidfileAtomic(stalePath, {
501
+ enginePid: 999999999,
502
+ hostPid: staleHostPid,
503
+ engineStartTime: null,
504
+ engineId: "fake",
505
+ hostKind: "pi",
506
+ writtenAt: new Date().toISOString(),
507
+ });
508
+ expect(existsSync(stalePath)).toBe(true);
509
+
510
+ const { client, cleanup } = makeClient();
511
+ await client.ensureConnected(); // 启动清扫发生在这里
512
+ expect(existsSync(stalePath)).toBe(false); // 陈旧文件被清(防 N 次崩溃重启堆积)
513
+ expect(existsSync(pidfilePath(dataDir, "fake", "test", process.pid))).toBe(true);
514
+ await cleanup();
515
+ });
516
+ });
@@ -0,0 +1,67 @@
1
+ // mirror 单元测试(W2,impl-plan §2.2「镜像失效语义」逐条):
2
+ // 1. 未收 childSpawned 前 = 无句柄(getChildByRecord undefined 等价);
3
+ // 2. killAll 整体置死(killed=true + 状态广播);
4
+ // 3. 置死后清空,isResumable 回落「无句柄」;
5
+ // killed 必含判据 = `entry !== undefined && !entry.killed`。
6
+
7
+ import { describe, expect, it } from "vitest";
8
+
9
+ import { SpawnedChildrenMirror } from "../mirror.ts";
10
+
11
+ describe("SpawnedChildrenMirror", () => {
12
+ it("未收 childSpawned 前 = 无句柄:getChildByRecord/hasLiveProcessHandle/isResumable 全部回落无句柄", () => {
13
+ const mirror = new SpawnedChildrenMirror();
14
+ expect(mirror.getChildByRecord("rec-1")).toBeUndefined();
15
+ expect(mirror.getEntry(4242)).toBeUndefined();
16
+ expect(mirror.hasLiveProcessHandle("rec-1")).toBe(false);
17
+ expect(mirror.isResumable("rec-1")).toBe(false);
18
+ });
19
+
20
+ it("childSpawned 落项后句柄可达;childStateChanged 更新状态(killed 必含)", () => {
21
+ const mirror = new SpawnedChildrenMirror();
22
+ mirror.recordSpawned(101, "rec-1");
23
+ expect(mirror.getChildByRecord("rec-1")).toMatchObject({ pid: 101, recordId: "rec-1", state: "running", killed: false });
24
+ expect(mirror.isResumable("rec-1")).toBe(true);
25
+
26
+ mirror.recordStateChanged({ pid: 101, recordId: "rec-1", state: "exited", killed: true, exitCode: 0 });
27
+ expect(mirror.getChildByRecord("rec-1")).toMatchObject({ state: "exited", killed: true, exitCode: 0 });
28
+ // exited + killed → 谓词回落 false(判据 child !== undefined && !child.killed)
29
+ expect(mirror.isResumable("rec-1")).toBe(false);
30
+ });
31
+
32
+ it("killAll:全部镜像项整体置死(killed=true + state=exited)+ 逐项广播 + 清空", () => {
33
+ const mirror = new SpawnedChildrenMirror();
34
+ mirror.recordSpawned(101, "rec-1");
35
+ mirror.recordSpawned(102, "rec-2");
36
+ const events: Array<{ reason: string; pid?: number; recordId?: string }> = [];
37
+ mirror.onChange((e) => events.push(e));
38
+
39
+ mirror.killAll();
40
+
41
+ // 置死广播先于清空(失效语义 2:宿主谓词/通知先看到 killed=true)
42
+ const killEvents = events.filter((e) => e.reason === "killedAll");
43
+ expect(killEvents.map((e) => e.pid).sort()).toEqual([101, 102]);
44
+ // 广播后镜像清空(失效语义 3:isResumable 回落「无句柄」)
45
+ expect(mirror.size).toBe(0);
46
+ expect(mirror.getChildByRecord("rec-1")).toBeUndefined();
47
+ expect(mirror.snapshot()).toEqual([]);
48
+ });
49
+
50
+ it("killAll 幂等:空镜像也发一次广播(订阅方依赖收敛信号)", () => {
51
+ const mirror = new SpawnedChildrenMirror();
52
+ const events: string[] = [];
53
+ mirror.onChange((e) => events.push(e.reason));
54
+ mirror.killAll();
55
+ expect(events).toEqual(["killedAll"]);
56
+ });
57
+
58
+ it("同一 pid 的 childStateChanged 覆盖原项;多 record 各自独立", () => {
59
+ const mirror = new SpawnedChildrenMirror();
60
+ mirror.recordSpawned(101, "rec-1");
61
+ mirror.recordSpawned(102, "rec-2");
62
+ mirror.recordStateChanged({ pid: 101, recordId: "rec-1", state: "exited", killed: false, exitCode: 0 });
63
+ expect(mirror.size).toBe(2);
64
+ expect(mirror.getChildByRecord("rec-1")).toMatchObject({ state: "exited", killed: false });
65
+ expect(mirror.isResumable("rec-2")).toBe(true);
66
+ });
67
+ });