@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,693 +0,0 @@
1
- // src/execution/__tests__/subagent-service-message-close.test.ts
2
- //
3
- // M2-B3 service 层:getRecordForAction 归属守卫 + closeSubagent 行为分流 + closeChatIdle。
4
- //
5
- // 沿用 run-and-finalize-chatmode.test.ts 的 mock 模式(真实 SubagentService + mock runSpawn +
6
- // ServiceInternals cast 暴露 store)。验证 M2-B3 新增的归属守卫与 close 行为分流。
7
-
8
- import * as fs from "node:fs";
9
- import * as os from "node:os";
10
- import * as path from "node:path";
11
- import type { ChildProcess } from "node:child_process";
12
-
13
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
14
-
15
- // mock logger(doFinalizeRecord manifest 写入降级路径用)
16
- const { loggerMock, killEscalationSpy } = vi.hoisted(() => ({
17
- loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
18
- killEscalationSpy: vi.fn(),
19
- }));
20
- vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
21
-
22
- // mock session-runner(import 链需要 runSpawn/killAllSpawnedChildren/getChildByRecord 存在)
23
- vi.mock("../engine/engines/pi/session-runner.ts", () => ({
24
- runSpawn: vi.fn(),
25
- killAllSpawnedChildren: vi.fn(),
26
- getChildByRecord: vi.fn(() => undefined),
27
- registerSpawnedChildForRecord: vi.fn(),
28
- killRecordChildWithEscalation: killEscalationSpy,
29
- spawnedChildren: new Map(),
30
- }));
31
-
32
- import { createRecord, updateFromEvent } from "../execution-record.ts";
33
- import { createNotifyHost } from "../notify-host.ts";
34
- import { ModelConfigService } from "../model-config-service.ts";
35
- import { RecordStore } from "../record-store.ts";
36
- import { SubagentService } from "../subagent-service.ts";
37
- import type { PiLike } from "../subagent-service.ts";
38
- import type { ExecutionRecord } from "../types.ts";
39
- import { getChildByRecord } from "../engine/engines/pi/session-runner.ts";
40
-
41
- // [T2④] kill 收敛入口 spy(工厂引用,断言服务侧三条路径收敛到它)
42
- const killRecordChildWithEscalationMock = killEscalationSpy;
43
- import {
44
- armIdleTimer,
45
- disarmIdleTimer,
46
- hasIdleTimer,
47
- _resetLifecycleState,
48
- } from "../lifecycle-manager.ts";
49
-
50
- function makeTmpAgentDir(): string {
51
- return fs.mkdtempSync(path.join(os.tmpdir(), "msg-close-svc-"));
52
- }
53
-
54
- function makePi(): PiLike & {
55
- appendEntry: ReturnType<typeof vi.fn>;
56
- events: { emit: ReturnType<typeof vi.fn> };
57
- sendMessage: ReturnType<typeof vi.fn>;
58
- } {
59
- return { appendEntry: vi.fn(), events: { emit: vi.fn() }, sendMessage: vi.fn() } as unknown as PiLike & {
60
- appendEntry: ReturnType<typeof vi.fn>;
61
- events: { emit: ReturnType<typeof vi.fn> };
62
- sendMessage: ReturnType<typeof vi.fn>;
63
- };
64
- }
65
-
66
- /** 构造 chatMode background record(带 controller,模拟真实 background record)。 */
67
- function makeRecord(overrides: Partial<ExecutionRecord> & { id?: string } = {}): ExecutionRecord {
68
- const { id = "sa-test", ...rest } = overrides;
69
- const r = createRecord(id, {
70
- agent: "general-purpose",
71
- model: "test/model",
72
- mode: "background",
73
- task: "test",
74
- slug: "test",
75
- startedAt: 1000,
76
- rootSessionId: "root-session",
77
- chatMode: true,
78
- controller: new AbortController(),
79
- });
80
- Object.assign(r, rest);
81
- return r;
82
- }
83
-
84
- interface ServiceInternals {
85
- store: RecordStore;
86
- sessionRootId: string | null;
87
- }
88
-
89
- type MockPi = ReturnType<typeof makePi>;
90
-
91
- function setup(): {
92
- agentDir: string;
93
- service: SubagentService;
94
- store: RecordStore;
95
- sessionRootId: string;
96
- pi: MockPi;
97
- } {
98
- const agentDir = makeTmpAgentDir();
99
- const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
100
- const service = new SubagentService({ cwd: agentDir, modelService });
101
- // [C2] pi 提为外层引用(对照 real-chain :139-140 形态)——close 现状语义断言需读
102
- // pi.sendMessage.mock.calls(末条通知 content 指针行),内联构造不保留引用。
103
- const pi = makePi();
104
- service.initSession({ pi, sessionId: "root-session" });
105
- const internals = service as unknown as ServiceInternals;
106
- return { agentDir, service, store: internals.store, sessionRootId: internals.sessionRootId!, pi };
107
- }
108
-
109
- // ============================================================
110
- // getRecordForAction 归属守卫(决策 3)
111
- // ============================================================
112
-
113
- describe("getRecordForAction 归属守卫(决策 3)", () => {
114
- let agentDir: string;
115
- let service: SubagentService;
116
- let store: RecordStore;
117
- let sessionRootId: string;
118
-
119
- beforeEach(() => {
120
- ({ agentDir, service, store, sessionRootId } = setup());
121
- });
122
-
123
- afterEach(() => {
124
- service.dispose();
125
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
126
- });
127
-
128
- it("正常(rootSessionId 匹配)→ 返回可变 record", () => {
129
- const record = makeRecord({ rootSessionId: sessionRootId });
130
- store.register(record);
131
- expect(service.chatActions.getRecordForAction(record.id)).toBe(record);
132
- });
133
-
134
- it("不存在 → throw not found or not owned", () => {
135
- expect(() => service.chatActions.getRecordForAction("sa-nonexistent")).toThrow(/not found or not owned/);
136
- });
137
-
138
- it("rootSessionId 不匹配 → throw not found or not owned(不区分 not found vs not owned,防信息泄露)", () => {
139
- const record = makeRecord({ id: "sa-other", rootSessionId: "other-session" });
140
- store.register(record);
141
- expect(() => service.chatActions.getRecordForAction("sa-other")).toThrow(/not found or not owned/);
142
- });
143
- });
144
-
145
- // ============================================================
146
- // closeSubagent 行为分流
147
- // ============================================================
148
-
149
- describe("closeSubagent 行为分流", () => {
150
- let agentDir: string;
151
- let service: SubagentService;
152
- let store: RecordStore;
153
- let pi: MockPi;
154
-
155
- beforeEach(() => {
156
- ({ agentDir, service, store, pi } = setup());
157
- });
158
-
159
- afterEach(() => {
160
- service.dispose();
161
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
162
- });
163
-
164
- it("running + force:false + 有活进程 → 置 closeAfterRound=true(不立即终态,等轮完)", async () => {
165
- // v4 B-1:closeSubagent running 分支按 isResumable 区分——有活进程(isResumable=false)
166
- // 走 closeAfterRound;无活进程(isResumable=true)走 closeChatIdle。本用例 mock 活进程。
167
- vi.mocked(getChildByRecord).mockReturnValueOnce({ killed: false } as never);
168
- const record = makeRecord({ status: "running" });
169
- store.register(record);
170
-
171
- await service.chatActions.closeSubagent(record, false);
172
-
173
- expect(record.closeAfterRound).toBe(true);
174
- expect(record.status).toBe("running"); // 仍 running
175
- expect(store.getMutable(record.id)).toBe(record); // 留内存
176
- });
177
-
178
- it("running + 无活进程(旧 idle)→ closeChatIdle(closed 终态化 + archive)", async () => {
179
- // v4 B-1:旧 idle 折入 running。running + force:false + isResumable=true(getChildByRecord
180
- // 默认 mock 返回 undefined = 无活进程)→ closeChatIdle 立即终态化。
181
- const record = makeRecord({ status: "running", round: 1 });
182
- record.sessionFile = path.join(agentDir, "test.jsonl");
183
- store.register(record);
184
-
185
- await service.chatActions.closeSubagent(record, false);
186
-
187
- expect(record.status).toBe("closed");
188
- expect(store.getMutable(record.id)).toBeUndefined(); // archived
189
- });
190
-
191
- it("running + force:true → cancelBackground(closed+cancelled 终态化 + archive)", async () => {
192
- const record = makeRecord({ status: "running", sessionFile: path.join(agentDir, "run.jsonl") });
193
- store.register(record);
194
-
195
- await service.chatActions.closeSubagent(record, true);
196
-
197
- // v4 B-1:cancelled 折入 closed(closedReason='cancelled' 区分用户取消语义)
198
- expect(record.status).toBe("closed");
199
- expect(record.closedReason).toBe("cancelled");
200
- expect(store.getMutable(record.id)).toBeUndefined(); // archived
201
- });
202
-
203
- it("终态(done)→ 幂等 no-op(不改状态、不 archive)", async () => {
204
- const record = makeRecord({ status: "closed" });
205
- store.register(record);
206
-
207
- await service.chatActions.closeSubagent(record, false);
208
-
209
- expect(record.status).toBe("closed"); // 不变
210
- });
211
-
212
- // ── [M5] Path A(isIdle timer armed + 保活进程)立即终态化 ─────────────
213
-
214
- it("[M5] Path A(timer armed + 活进程)+ force:false → 立即终态化(closed+user-close + kill 进程 + disarm timer),不置 closeAfterRound", async () => {
215
- // 旧代码 Path A 走 else 置 closeAfterRound,但 chatMode 轮完成恒经 agent_settled →
216
- // onRoundSettled → runAndFinalize early return(isIdle 恒 true),标志无人消费,
217
- // tool 却返回 {closed:true}(谎报)。
218
- // [T2④] closeChatIdle 的 kill 收敛到 killRecordChildWithEscalation(recordId 入口),
219
- // 不再读 child 句柄——无 child mock(Once mock 不被消费会泄漏劫持后续用例)
220
- const record = makeRecord({ status: "running", round: 1 });
221
- record.sessionFile = path.join(agentDir, "path-a.jsonl");
222
- store.register(record);
223
- armIdleTimer(record.id, () => {}); // Path A:轮次完成、进程保活等待续聊
224
- expect(hasIdleTimer(record.id)).toBe(true);
225
-
226
- try {
227
- await service.chatActions.closeSubagent(record, false);
228
- } finally {
229
- disarmIdleTimer(record.id); // 断言失败路径的兜底清理
230
- }
231
-
232
- expect(record.closeAfterRound).toBeUndefined(); // 不置标志
233
- expect(record.status).toBe("closed");
234
- expect(record.closedReason).toBe("user-close");
235
- expect(store.getMutable(record.id)).toBeUndefined(); // archived
236
- // [T2④] kill 收敛到 killRecordChildWithEscalation(SIGTERM + 30s SIGKILL 升级)
237
- expect(killRecordChildWithEscalationMock).toHaveBeenCalledWith(record.id, "closeChatIdle");
238
- expect(hasIdleTimer(record.id)).toBe(false); // timer disarmed
239
- });
240
-
241
- it("[M5] Path B(无活进程、timer 未 armed)+ force:false → 立即终态化(同旧 isResumable 行为)+ 终态通知", async () => {
242
- const record = makeRecord({ status: "running", round: 1 });
243
- record.sessionFile = path.join(agentDir, "path-b.jsonl");
244
- store.register(record);
245
-
246
- await service.chatActions.closeSubagent(record, false);
247
-
248
- expect(record.status).toBe("closed");
249
- expect(record.closedReason).toBe("user-close");
250
- expect(store.getMutable(record.id)).toBeUndefined();
251
- // [C-1] close 终态通知(设计 D2 路径②):chatMode close 后父 agent 收到终态通知——
252
- // 正文空串(doneResult.text 恒空)+ 轮次统计 + Full transcript 指针行
253
- expect(pi.sendMessage).toHaveBeenCalledTimes(1);
254
- const sent = pi.sendMessage.mock.calls[0]![0] as { content: string };
255
- expect(sent.content).toContain("completed after 1 round.");
256
- expect(sent.content).toContain(`\n\nFull transcript: ${record.sessionFile}`);
257
- });
258
-
259
- // ── [M6] cancelBackground 显式 kill(chatMode 热路径轮中 cancel)────────
260
-
261
- it("[M6] running + force:true + 活进程(热路径轮中)→ cancelBackground 显式 SIGTERM + disarm idle timer", async () => {
262
- // chatMode 首轮 agent_settled 后 runSpawn 提前 resolveRun(0) 返回,
263
- // abort→kill listener 已被 removeEventListener——热路径轮中 cancel 只能靠显式 kill。
264
- // [T2④] cancelBackground 的 kill 收敛到 killRecordChildWithEscalation(recordId 入口),
265
- // 不再读 child 句柄
266
- const record = makeRecord({ status: "running", sessionFile: path.join(agentDir, "hot.jsonl") });
267
- record.controller = new AbortController();
268
- store.register(record);
269
- armIdleTimer(record.id, () => {}); // Path A 等待续聊态(cancel 时必须 disarm)
270
- expect(hasIdleTimer(record.id)).toBe(true);
271
-
272
- try {
273
- await service.chatActions.closeSubagent(record, true);
274
- } finally {
275
- disarmIdleTimer(record.id); // 断言失败路径的兜底清理
276
- }
277
-
278
- // [T2④] kill 收敛到 killRecordChildWithEscalation(SIGTERM + 升级)
279
- expect(killRecordChildWithEscalationMock).toHaveBeenCalledWith(record.id, "cancelBackground");
280
- expect(record.status).toBe("closed");
281
- expect(record.closedReason).toBe("cancelled");
282
- expect(hasIdleTimer(record.id)).toBe(false); // timer disarmed
283
- expect(store.getMutable(record.id)).toBeUndefined(); // archived
284
- });
285
- });
286
-
287
- // ============================================================
288
- // [A2-1] cancel 迟到:record 已终态(detached 已 finalize)→ CAS 拒绝
289
- // ============================================================
290
-
291
- describe("cancelBackground CAS 抢锁(A2-1)——cancel 迟到不覆写已完成 record", () => {
292
- let agentDir: string;
293
- let service: SubagentService;
294
- let store: RecordStore;
295
- let pi: MockPi;
296
-
297
- beforeEach(() => {
298
- ({ agentDir, service, store, pi } = setup());
299
- });
300
-
301
- afterEach(() => {
302
- service.dispose();
303
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
304
- });
305
-
306
- it("record 已 closed 未 archive(doFinalizeRecord Step 0 await 窗口形态)→ cancel 返回 false,不覆写终态/不写 tombstone/不 notify/不 archive", () => {
307
- const sessionFile = path.join(agentDir, "late-cancel.jsonl");
308
- const record = makeRecord({ status: "closed", sessionFile });
309
- // 模拟 detached 已 finalize 的终态快照(closedReason=gc 正常完成,非 cancelled)
310
- record.closedReason = "gc";
311
- record.endedAt = 5555;
312
- record.result = "detached final result";
313
- store.register(record); // 未 archive:恰好处于 finalize await 窗口(收尾后段才 archive)
314
-
315
- const cancelled = service.cancel(record.id);
316
-
317
- expect(cancelled).toBe(false); // CAS 没抢到——不谎报 true
318
- expect(record.status).toBe("closed");
319
- expect(record.closedReason).toBe("gc"); // 不被 completeRecord 改写为 cancelled
320
- expect(record.endedAt).toBe(5555); // finalize 冻结的终态值不被重写
321
- expect(record.result).toBe("detached final result"); // 不被空 cancelled result 覆写
322
- // 不写 cancelled tombstone(与 finalized marker 双标 = record 双重终态化)
323
- expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(false);
324
- expect(store.getMutable(record.id)).toBe(record); // 不 archive(record 留内存)
325
- expect(pi.sendMessage).not.toHaveBeenCalled(); // 不重复 notify(detached finalize 已发)
326
- });
327
-
328
- it("record running → cancel 返回 true 且终态化(CAS 正常路径不回归)", () => {
329
- const sessionFile = path.join(agentDir, "cancel-ok.jsonl");
330
- const record = makeRecord({ status: "running", sessionFile });
331
- store.register(record);
332
-
333
- expect(service.cancel(record.id)).toBe(true);
334
- expect(record.status).toBe("closed");
335
- expect(record.closedReason).toBe("cancelled");
336
- expect(store.getMutable(record.id)).toBeUndefined(); // archived
337
- });
338
- });
339
-
340
- // ============================================================
341
- // [M5] closeAfterRound 消费点:onRoundSettled(chatMode 热路径轮完成)
342
- // ============================================================
343
-
344
- describe("[M5] closeAfterRound 消费点挂 onRoundSettled(chatMode 轮完成终态化)", () => {
345
- let agentDir: string;
346
- let service: SubagentService;
347
- let store: RecordStore;
348
-
349
- beforeEach(() => {
350
- vi.mocked(getChildByRecord).mockReset();
351
- vi.mocked(getChildByRecord).mockReturnValue(undefined);
352
- ({ agentDir, service, store } = setup());
353
- });
354
-
355
- afterEach(() => {
356
- service.dispose();
357
- _resetLifecycleState();
358
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
359
- });
360
-
361
- it("热路径轮完成(onRoundSettled)+ closeAfterRound=true → 消费标志终态化(closed+user-close + archive)", async () => {
362
- // 热路径轮不经 runAndFinalize(deliverMessage 直接 sendPromptCommand),旧消费点
363
- // (runAndFinalize CAS 分支)不可达——唯一可达的收尾点是 onRoundSettled。
364
- const killFn = vi.fn(() => true);
365
- const child = { killed: false, kill: killFn } as unknown as ChildProcess;
366
- // mockReturnValue(非 Once):notifyComplete → hasRunningBackground → hasLiveProcessHandle
367
- // 也会读 getChildByRecord,Once mock 会被提前消费掉。
368
- vi.mocked(getChildByRecord).mockReturnValue(child);
369
- const internals = service as unknown as { buildSessionRunnerContext(): { onRoundSettled?: (r: ExecutionRecord) => void } };
370
- const ctx = internals.buildSessionRunnerContext();
371
- expect(typeof ctx.onRoundSettled).toBe("function");
372
-
373
- const record = makeRecord({ id: "sa-m5", status: "running" });
374
- record.sessionFile = path.join(agentDir, "m5.jsonl");
375
- store.register(record);
376
- record.closeAfterRound = true; // busy 轮中 close(force:false) 置的标志
377
- armIdleTimer(record.id, () => {}); // session-runner 在 onRoundSettled 前已 arm(时序还原)
378
- try {
379
- ctx.onRoundSettled!(record);
380
- // closeAfterRoundSettled 是 async(finalizeRecord 内 manifest 写入)——等微任务收尾完成
381
- await vi.waitFor(() => expect(store.getMutable(record.id)).toBeUndefined());
382
- } finally {
383
- disarmIdleTimer(record.id);
384
- }
385
-
386
- expect(record.closeAfterRound).toBeUndefined(); // 标志已消费
387
- expect(record.status).toBe("closed");
388
- expect(record.closedReason).toBe("user-close");
389
- // [T2④] kill 收敛到 killRecordChildWithEscalation(SIGTERM + 升级)
390
- expect(killRecordChildWithEscalationMock).toHaveBeenCalledWith(record.id, "closeAfterRoundSettled");
391
- expect(hasIdleTimer(record.id)).toBe(false);
392
- });
393
-
394
- it("onRoundSettled 无 closeAfterRound → 不终态化(record 保持 running-resumable,轮完成通知照常)", async () => {
395
- const internals = service as unknown as { buildSessionRunnerContext(): { onRoundSettled?: (r: ExecutionRecord) => void } };
396
- const ctx = internals.buildSessionRunnerContext();
397
- const record = makeRecord({ id: "sa-m5-keep", status: "running" });
398
- store.register(record);
399
- armIdleTimer(record.id, () => {});
400
- try {
401
- ctx.onRoundSettled!(record);
402
- } finally {
403
- disarmIdleTimer(record.id);
404
- }
405
-
406
- expect(record.status).toBe("running"); // 不终态化
407
- expect(record.round).toBe(1); // round 累加照常
408
- expect(store.getMutable(record.id)).toBe(record); // 留内存
409
- });
410
- });
411
-
412
- // ============================================================
413
- // [C2 wave2] close 现状语义 + sessionFile 条件透传
414
- // ============================================================
415
-
416
- describe("[C2] close 现状语义 + sessionFile 条件透传", () => {
417
- let agentDir: string;
418
- let service: SubagentService;
419
- let store: RecordStore;
420
- let pi: MockPi;
421
-
422
- beforeEach(() => {
423
- vi.mocked(getChildByRecord).mockReset();
424
- // 默认无活进程(Path B / isResumable 形态);hasRunningBackground 亦读此 mock
425
- vi.mocked(getChildByRecord).mockReturnValue(undefined);
426
- ({ agentDir, service, store, pi } = setup());
427
- });
428
-
429
- afterEach(() => {
430
- service.dispose();
431
- _resetLifecycleState();
432
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
433
- });
434
-
435
- it("close 语义:轮次通知(含指针行)发出后 closeAfterRoundSettled 终态化 + 终态通知,两条都送达", async () => {
436
- // [C-1] 修复后语义:终态通知经 notifyClosed 显式发送(dedup 身份 = 裸 id,与轮次通知
437
- // 的 id:round key 区分)——close 后父 agent 收到「最后一轮轮次通知 + 终态通知」两条。
438
- const internals = service as unknown as { buildSessionRunnerContext(): { onRoundSettled?: (r: ExecutionRecord) => void } };
439
- const ctx = internals.buildSessionRunnerContext();
440
- const record = makeRecord({ id: "sa-c2-close", status: "running" });
441
- record.sessionFile = path.join(agentDir, "c2-close.jsonl");
442
- store.register(record);
443
- record.closeAfterRound = true; // busy 轮中 close(force:false) 置的标志
444
- armIdleTimer(record.id, () => {});
445
- try {
446
- ctx.onRoundSettled!(record);
447
- // closeAfterRoundSettled 是 async(finalizeRecord 链)——终态通知在链末尾的
448
- // notifyClosed,等它发出而不是只等 archive(archive 在链中段,先于通知)
449
- await vi.waitFor(() => expect(pi.sendMessage).toHaveBeenCalledTimes(2));
450
- await vi.waitFor(() => expect(store.getMutable(record.id)).toBeUndefined());
451
- } finally {
452
- disarmIdleTimer(record.id);
453
- }
454
-
455
- // 轮次通知 1 条 + 终态通知 1 条(dedup 不吞)
456
- expect(pi.sendMessage).toHaveBeenCalledTimes(2);
457
- // 第 1 条:轮次通知含 Full transcript 指针行(chatMode:true + sessionFile 有值 → 透传)
458
- const roundMsg = pi.sendMessage.mock.calls[0]![0] as { content: string };
459
- expect(roundMsg.content).toContain("finished a round");
460
- expect(roundMsg.content).toContain(`\n\nFull transcript: ${record.sessionFile}`);
461
- // 第 2 条:终态通知含轮次统计 + 指针行
462
- const closeMsg = pi.sendMessage.mock.calls[1]![0] as { content: string };
463
- expect(closeMsg.content).toContain("completed after 1 round.");
464
- expect(closeMsg.content).toContain(`\n\nFull transcript: ${record.sessionFile}`);
465
- });
466
-
467
- it("one-shot 条件透传(R4 必选):chatMode:false + sessionFile 有值 → 通知不透传 sessionFile、content 无指针行", () => {
468
- // 锁死 toNotifyRecord 的 chatMode 条件(C2C2 契约)——漏加条件时 notifier 单测不红
469
- //(notifier 层只见最终字段),此用例走真实 SubagentService + toNotifyRecord 路径必红。
470
- // 同时服务 C2TC6 结构级断言。
471
- const record = makeRecord({ id: "sa-c2-oneshot", status: "running", chatMode: false, result: "done" });
472
- record.sessionFile = path.join(agentDir, "c2-oneshot.jsonl");
473
- store.register(record);
474
-
475
- // running + 无活进程(getChildByRecord mock undefined)→ isResumable 放行 →
476
- // 非 chatMode 映射 closed 通知,立即 flush(无其他 running background)
477
- // [D4-①] notifyComplete 已随通知簇搬至 notify-host——按轴直接构造(同 pi/store
478
- // 注入,行为与经 Service 的原路径等价;dedup 集为空与单次调用断言相容)
479
- createNotifyHost({ getPi: () => pi, listRunning: () => store.listRunning(), getIsIdle: () => undefined })
480
- .notifyComplete(record);
481
-
482
- expect(pi.sendMessage).toHaveBeenCalledTimes(1);
483
- const msg = pi.sendMessage.mock.calls[0]![0] as {
484
- content: string;
485
- details?: { sessionFile?: string };
486
- };
487
- // 结构级:one-shot 通知的 BgNotifyRecord 不含 sessionFile
488
- expect(msg.details?.sessionFile).toBeUndefined();
489
- // 语义级:正文无指针行,且与改造前逐字节一致(G4)
490
- expect(msg.content).not.toContain("Full transcript");
491
- expect(msg.content).toBe('Subagent "general-purpose" (sa-c2-oneshot) completed. Result:\ndone');
492
- });
493
-
494
- it("chatMode:true 对照透传:isResumable 放行的轮次通知携带 sessionFile + 指针行(漏加 chatMode 条件时本用例红)", () => {
495
- const record = makeRecord({ id: "sa-c2-chat", status: "running", result: "round reply" });
496
- record.sessionFile = path.join(agentDir, "c2-chat.jsonl");
497
- store.register(record);
498
-
499
- createNotifyHost({ getPi: () => pi, listRunning: () => store.listRunning(), getIsIdle: () => undefined })
500
- .notifyComplete(record);
501
-
502
- expect(pi.sendMessage).toHaveBeenCalledTimes(1);
503
- const msg = pi.sendMessage.mock.calls[0]![0] as {
504
- content: string;
505
- details?: { sessionFile?: string };
506
- };
507
- expect(msg.details?.sessionFile).toBe(path.join(agentDir, "c2-chat.jsonl"));
508
- expect(msg.content).toContain(`\n\nFull transcript: ${path.join(agentDir, "c2-chat.jsonl")}`);
509
- });
510
- });
511
-
512
- // ============================================================
513
- // [C-4] chatMode 轮次增量(onRoundSettled 写点)+ close 终态通知
514
- // ============================================================
515
-
516
- describe("[C-4] onRoundSettled 轮次增量 + [C-1] close 终态通知", () => {
517
- let agentDir: string;
518
- let service: SubagentService;
519
- let store: RecordStore;
520
- let pi: MockPi;
521
-
522
- beforeEach(() => {
523
- vi.mocked(getChildByRecord).mockReset();
524
- // 默认无活进程:hasRunningBackground=false → notify 立即 flush,sendMessage 同步可断言
525
- vi.mocked(getChildByRecord).mockReturnValue(undefined);
526
- ({ agentDir, service, store, pi } = setup());
527
- });
528
-
529
- afterEach(() => {
530
- service.dispose();
531
- _resetLifecycleState();
532
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
533
- });
534
-
535
- /** 真实回调链(与生产一致:agent_settled → onRoundSettled),turns 经 updateFromEvent 累积。 */
536
- function settleRound(record: ExecutionRecord): void {
537
- (service as unknown as {
538
- buildSessionRunnerContext(): { onRoundSettled?: (r: ExecutionRecord) => void };
539
- }).buildSessionRunnerContext().onRoundSettled!(record);
540
- }
541
-
542
- it("空增量轮:通知正文占位 (no output this round),不含上一轮文本(D5,onRoundSettled 写点)", () => {
543
- const record = makeRecord({ id: "sa-empty-round", status: "running" });
544
- record.sessionFile = path.join(agentDir, "empty-round.jsonl");
545
- store.register(record);
546
- armIdleTimer(record.id, () => {});
547
- try {
548
- // 第 1 轮:有真实回复
549
- updateFromEvent(record, { type: "text_delta", delta: "ROUND-ONE-REPLY" });
550
- updateFromEvent(record, { type: "turn_end" });
551
- settleRound(record);
552
-
553
- // 第 2 轮:零新事件(纯空转/工具轮形态)→ 空增量
554
- settleRound(record);
555
- } finally {
556
- disarmIdleTimer(record.id);
557
- }
558
-
559
- expect(pi.sendMessage).toHaveBeenCalledTimes(2);
560
- const round1 = pi.sendMessage.mock.calls[0]![0] as { content: string };
561
- expect(round1.content).toContain("ROUND-ONE-REPLY");
562
- // D5 核心:空增量轮占位,不沿用旧 result(= 上一轮增量 → 父 agent 误读为原样重复回复)
563
- const round2 = pi.sendMessage.mock.calls[1]![0] as { content: string };
564
- expect(round2.content).toContain("(no output this round)");
565
- expect(round2.content).not.toContain("ROUND-ONE-REPLY");
566
- });
567
-
568
- it("多轮增量:第 2 条通知只含第 2 轮文本、不含第 1 轮(D1/G1,roundBaseTurnIndex 推进)", () => {
569
- const record = makeRecord({ id: "sa-multi-round", status: "running" });
570
- record.sessionFile = path.join(agentDir, "multi-round.jsonl");
571
- store.register(record);
572
- armIdleTimer(record.id, () => {});
573
- try {
574
- // 第 1 轮
575
- updateFromEvent(record, { type: "text_delta", delta: "ALPHA-ROUND-ONE" });
576
- updateFromEvent(record, { type: "turn_end" });
577
- settleRound(record);
578
-
579
- // 第 2 轮:turn_end 已闭合前轮 turn → text_delta 开新 turn
580
- updateFromEvent(record, { type: "text_delta", delta: "BETA-ROUND-TWO" });
581
- updateFromEvent(record, { type: "turn_end" });
582
- settleRound(record);
583
- } finally {
584
- disarmIdleTimer(record.id);
585
- }
586
-
587
- expect(pi.sendMessage).toHaveBeenCalledTimes(2);
588
- const round1 = pi.sendMessage.mock.calls[0]![0] as { content: string };
589
- expect(round1.content).toContain("ALPHA-ROUND-ONE");
590
- expect(round1.content).not.toContain("BETA-ROUND-TWO");
591
- const round2 = pi.sendMessage.mock.calls[1]![0] as { content: string };
592
- expect(round2.content).toContain("BETA-ROUND-TWO");
593
- expect(round2.content).not.toContain("ALPHA-ROUND-ONE");
594
- // base 推进写点:第 2 轮后边界 = 2 个 turn(第 3 轮增量将从新 turn 起)
595
- expect(record.roundBaseTurnIndex).toBe(2);
596
- });
597
-
598
- it("[C-1] idle close(closeChatIdle)后父侧收到终态通知:正文空串 + Full transcript 指针行,轮次通知与终态通知都送达(dedup 不吞)", async () => {
599
- const record = makeRecord({ id: "sa-idle-close", status: "running" });
600
- record.sessionFile = path.join(agentDir, "idle-close.jsonl");
601
- store.register(record);
602
- armIdleTimer(record.id, () => {}); // Path A:轮次完成、进程保活等待续聊
603
- try {
604
- updateFromEvent(record, { type: "text_delta", delta: "LAST-ROUND-INCREMENT" });
605
- updateFromEvent(record, { type: "turn_end" });
606
- settleRound(record); // 第 1 条:轮次通知
607
- await service.chatActions.closeSubagent(record, false); // idle close(D2 路径②)
608
- } finally {
609
- disarmIdleTimer(record.id);
610
- }
611
-
612
- // dedup 不吞:轮次通知(key=id:1)+ 终态通知(key=裸 id)两条都送达
613
- expect(pi.sendMessage).toHaveBeenCalledTimes(2);
614
- const roundMsg = pi.sendMessage.mock.calls[0]![0] as { content: string };
615
- expect(roundMsg.content).toContain("finished a round");
616
- expect(roundMsg.content).toContain("LAST-ROUND-INCREMENT");
617
- // 终态通知:正文空串占位(不含上一轮增量,D2 路径②)+ 轮次统计 + 指针行
618
- const closeMsg = pi.sendMessage.mock.calls[1]![0] as { content: string };
619
- expect(closeMsg.content).toContain("completed after 1 round.");
620
- expect(closeMsg.content).toContain(`\n\nFull transcript: ${record.sessionFile}`);
621
- expect(closeMsg.content).not.toContain("LAST-ROUND-INCREMENT");
622
- });
623
-
624
- it("[C-1] 路径①(closeAfterRoundSettled):轮次通知与终态通知两条都送达,终态携带本轮增量 + 轮次统计 + 指针行", async () => {
625
- const record = makeRecord({ id: "sa-path-one", status: "running" });
626
- record.sessionFile = path.join(agentDir, "path-one.jsonl");
627
- store.register(record);
628
- record.closeAfterRound = true; // busy 轮中 close(force:false) 置的标志
629
- armIdleTimer(record.id, () => {});
630
- try {
631
- updateFromEvent(record, { type: "text_delta", delta: "PATH-ONE-ROUND-REPLY" });
632
- updateFromEvent(record, { type: "turn_end" });
633
- settleRound(record); // 轮次通知 + 消费标志 → closeAfterRoundSettled 终态化
634
- // 终态通知在 async finalize 链末尾的 notifyClosed——直接等它发出
635
- await vi.waitFor(() => expect(pi.sendMessage).toHaveBeenCalledTimes(2));
636
- await vi.waitFor(() => expect(store.getMutable(record.id)).toBeUndefined());
637
- } finally {
638
- disarmIdleTimer(record.id);
639
- }
640
-
641
- expect(pi.sendMessage).toHaveBeenCalledTimes(2);
642
- const roundMsg = pi.sendMessage.mock.calls[0]![0] as { content: string };
643
- expect(roundMsg.content).toContain("finished a round");
644
- expect(roundMsg.content).toContain("PATH-ONE-ROUND-REPLY");
645
- // 路径①终态:合成 result 沿用 record.result(= 本轮增量,D2 路径①)+ 统计 + 指针
646
- const closeMsg = pi.sendMessage.mock.calls[1]![0] as { content: string };
647
- expect(closeMsg.content).toContain("completed after 1 round.");
648
- expect(closeMsg.content).toContain("PATH-ONE-ROUND-REPLY");
649
- expect(closeMsg.content).toContain(`\n\nFull transcript: ${record.sessionFile}`);
650
- });
651
-
652
- it("[G4] 对照:one-shot(chatMode:false)close → 不发终态通知(现状行为字节不变)", async () => {
653
- const record = makeRecord({ id: "sa-oneshot-close", status: "running", chatMode: false });
654
- record.sessionFile = path.join(agentDir, "oneshot-close.jsonl");
655
- store.register(record);
656
-
657
- await service.chatActions.closeSubagent(record, false); // resumable → closeChatIdle → notifyClosed 拒绝
658
-
659
- expect(record.status).toBe("closed");
660
- expect(pi.sendMessage).not.toHaveBeenCalled();
661
- });
662
-
663
- it("[W16 P-1 回归] close 终态 subagent-record entry 的 result == 轮终真实值(closeChatIdle 不抹空)", async () => {
664
- // 完整生命周期:register(entry: running)→ 轮终(record.result = 轮终真实值)→
665
- // idle close(archive 落 close 终态 entry——D4 重建源)。
666
- // 缺陷形态(verifier W16 实测复现):closeChatIdle 合成 text:"" 经 completeRecord
667
- // 覆盖 record.result → close entry 的 result=''(轮终 'ok' 被抹空),W18 消费后
668
- // 重开 session result 回退空串。修复:doneResult.text 沿用 record.result。
669
- const record = makeRecord({ id: "sa-close-entry-result", status: "running" });
670
- record.sessionFile = path.join(agentDir, "close-entry-result.jsonl");
671
- store.register(record);
672
- armIdleTimer(record.id, () => {});
673
- try {
674
- updateFromEvent(record, { type: "text_delta", delta: "ok" });
675
- updateFromEvent(record, { type: "turn_end" });
676
- settleRound(record); // 轮终:onRoundSettled 写 record.result = "ok"
677
- expect(record.result).toBe("ok"); // 前提锚点:轮终真实值在位
678
- await service.chatActions.closeSubagent(record, false); // idle close → completeRecord + archive
679
- } finally {
680
- disarmIdleTimer(record.id);
681
- }
682
-
683
- // appendEntry 序列中的 subagent-record 快照:register(running)→ close(closed)。
684
- const snapshots = pi.appendEntry.mock.calls
685
- .filter(([type]) => type === "subagent-record")
686
- .map(([, data]) => data as { status: string; result?: string });
687
- expect(snapshots.length).toBeGreaterThanOrEqual(2);
688
- const closeEntry = snapshots[snapshots.length - 1]!;
689
- expect(closeEntry.status).toBe("closed");
690
- // 修复断言:close 终态 entry 保持轮终真实值(缺陷形态为 '')
691
- expect(closeEntry.result).toBe("ok");
692
- });
693
- });