@zhushanwen/subagent-core 0.6.0 → 0.7.1

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
@@ -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
+ });