@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,53 +0,0 @@
1
- // src/__tests__/temp-prompt.test.ts
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
-
5
- import { describe, expect, it } from "vitest";
6
-
7
- import { cleanupTempPrompt, writePromptToTempFile } from "../engine/engines/pi/temp-prompt.ts";
8
-
9
- describe("writePromptToTempFile", () => {
10
- it("创建临时目录 + 写入 prompt 文件", async () => {
11
- const result = await writePromptToTempFile("test-agent", "You are a helper.");
12
- expect(result.dir).toBeTruthy();
13
- expect(result.filePath).toBeTruthy();
14
- expect(fs.existsSync(result.dir)).toBe(true);
15
- expect(fs.existsSync(result.filePath)).toBe(true);
16
-
17
- const content = fs.readFileSync(result.filePath, "utf-8");
18
- expect(content).toBe("You are a helper.");
19
-
20
- // 清理
21
- await cleanupTempPrompt(result);
22
- expect(fs.existsSync(result.dir)).toBe(false);
23
- });
24
-
25
- it("agentName 非法字符替换为下划线", async () => {
26
- const result = await writePromptToTempFile("my agent/v2:spec", "prompt");
27
- expect(path.basename(result.filePath)).toBe("prompt-my_agent_v2_spec.md");
28
- await cleanupTempPrompt(result);
29
- });
30
-
31
- it("文件权限 0o600(仅 owner 读写)", async () => {
32
- const result = await writePromptToTempFile("secure", "secret prompt");
33
- const stat = fs.statSync(result.filePath);
34
- // 只取低 9 位(rwxrwxrwx),忽略文件类型位
35
- const mode = stat.mode & 0o777;
36
- expect(mode).toBe(0o600);
37
- await cleanupTempPrompt(result);
38
- });
39
-
40
- it("多字节内容(中文)正确写入", async () => {
41
- const result = await writePromptToTempFile("cn", "你是一个助手。");
42
- const content = fs.readFileSync(result.filePath, "utf-8");
43
- expect(content).toBe("你是一个助手。");
44
- await cleanupTempPrompt(result);
45
- });
46
- });
47
-
48
- describe("cleanupTempPrompt", () => {
49
- it("不存在的目录不抛错(best-effort)", async () => {
50
- const fake = { dir: "/nonexistent/path/xyz", filePath: "/nonexistent/path/xyz/p.md" };
51
- await expect(cleanupTempPrompt(fake)).resolves.not.toThrow();
52
- });
53
- });
@@ -1,616 +0,0 @@
1
- // src/__tests__/timeout-integration.test.ts
2
- //
3
- // timeoutMs / signal abort → child.kill 端到端路径测试。
4
- //
5
- // 背景:session-runner.ts 无独立 timeoutMs 字段——超时机制是 watchdog(基于
6
- // maxTurnsToWatchdogMs(opts.maxTurns) 动态计算的下限 30min timer,兜底 SIGTERM);
7
- // 外部取消通过 opts.signal (AbortSignal) 传播:onAbort → child.kill("SIGTERM")。
8
- // 故「timeout 端到端路径」实际是 watchdog timer + signal abort → child.kill 两条链路。
9
- //
10
- // 本文件聚焦三条终止语义路径(与 run-spawn-integration.test.ts §12 watchdog 测试互补,
11
- // 该文件关注 timer 边界值,本文件关注端到端 kill 语义 + 外部 signal 场景):
12
- // 1. watchdog 到期 → child.kill(maxTurns 驱动的整体超时兜底)
13
- // 2. 正常完成先于 watchdog 到期 → clearTimeout 生效,不 kill
14
- // 3. 外部 signal abort(运行中 abort / spawn 前已 aborted)→ child.kill
15
- //
16
- // mock 策略(与 run-spawn-integration.test.ts / run-spawn-edges.test.ts 一致):
17
- // - node:child_process.spawn → 返回 FakeChild(EventEmitter + PassThrough)。
18
- // - node:child_process.execFile → err-first callback 默认兜底(buildEnvBlock git branch
19
- // 失败 → catch → branch="")。
20
- // - node:fs 同步方法 → mock(避免触碰真实文件系统),promises 保留真实实现。
21
- // - temp-prompt → mock(返回固定路径,消除 fake-timers flaky)。
22
- // - alive-store.writeAliveMarker → mock。
23
-
24
- import type { PassThrough } from "node:stream";
25
-
26
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
27
-
28
- // ── mock modules ──
29
- // vitest 把 vi.mock 提升到文件顶部,工厂内部引用模块用 await import。
30
-
31
- vi.mock("node:child_process", async () => {
32
- const { EventEmitter } = await import("node:events");
33
- const { PassThrough } = await import("node:stream");
34
-
35
- // FakeChild:模拟 ChildProcess(EventEmitter + PassThrough streams)。
36
- class FakeChild extends EventEmitter {
37
- pid = 12345;
38
- stdout = new PassThrough();
39
- stdin = new PassThrough(); // runSpawn 注册 stdin EPIPE handler(session-runner),FakeChild 必须提供
40
- stderr = new PassThrough();
41
- killed = false;
42
- killSignal: string | undefined;
43
- // [race-F4] SIGKILL 升级判定读 exitCode/signalCode(真 ChildProcess 未退出时均 null),
44
- // FakeChild 同形提供才能驱动升级路径测试(undefined === null 为 false 会误判已退出)。
45
- exitCode: number | null = null;
46
- signalCode: string | null = null;
47
- kill(sig?: string): boolean {
48
- this.killed = true;
49
- this.killSignal = sig;
50
- return true;
51
- }
52
- }
53
-
54
- return {
55
- spawn: vi.fn(() => new FakeChild()),
56
- // buildEnvBlock 用 execFile 异步取 git branch:默认 err-first 兜底(catch → branch="")
57
- execFile: vi.fn(
58
- (
59
- _cmd: string,
60
- _args: readonly string[],
61
- _opts: unknown,
62
- cb: (err: Error | null, stdout?: string, stderr?: string) => void,
63
- ) => cb(new Error("execFile not configured in this test")),
64
- ),
65
- };
66
- });
67
-
68
- vi.mock("node:fs", async () => {
69
- const actual = await import("node:fs");
70
- return {
71
- default: {
72
- ...actual,
73
- mkdirSync: vi.fn(),
74
- existsSync: vi.fn(() => false),
75
- appendFileSync: vi.fn(),
76
- writeFileSync: vi.fn(),
77
- readdirSync: vi.fn(() => []),
78
- },
79
- mkdirSync: vi.fn(),
80
- existsSync: vi.fn(() => false),
81
- appendFileSync: vi.fn(),
82
- writeFileSync: vi.fn(),
83
- readdirSync: vi.fn(() => []),
84
- promises: actual.promises,
85
- };
86
- });
87
-
88
- vi.mock("../alive-store.ts", () => ({
89
- writeAliveMarker: vi.fn(),
90
- }));
91
-
92
- vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
93
- writePromptToTempFile: vi.fn(async (agent: string) => {
94
- const safeName = agent.replace(/[^\w.-]+/g, "_");
95
- return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
96
- }),
97
- cleanupTempPrompt: vi.fn(async () => {}),
98
- }));
99
-
100
- import { spawn } from "node:child_process";
101
- import * as fs from "node:fs";
102
-
103
- import { createRecord } from "../execution-record.ts";
104
- import {
105
- maxTurnsToWatchdogMs,
106
- type RunOptions,
107
- resolveSpawnWatchdogMs,
108
- runSpawn,
109
- SPAWN_WATCHDOG_ENV,
110
- type SessionRunnerContext,
111
- } from "../engine/engines/pi/session-runner.ts";
112
-
113
- const mockSpawn = vi.mocked(spawn);
114
- const mockExistsSync = vi.mocked(fs.existsSync);
115
-
116
- /**
117
- * spawn mock 返回的 fake child 类型(结构子集)。
118
- * FakeChild 定义在 vi.mock 工厂内部(作用域隔离),测试代码通过此类型访问成员。
119
- */
120
- interface FakeChild {
121
- pid: number;
122
- stdout: PassThrough;
123
- stderr: PassThrough;
124
- killed: boolean;
125
- killSignal: string | undefined;
126
- kill(sig?: string): boolean;
127
- emit(event: string, ...args: unknown[]): boolean;
128
- }
129
-
130
- /** 从最近一次 spawn 调用取回返回的 FakeChild。 */
131
- function lastSpawnedChild(): FakeChild {
132
- const result = mockSpawn.mock.results.at(-1);
133
- if (!result) throw new Error("spawn was not called yet");
134
- return result.value as FakeChild;
135
- }
136
-
137
- // ============================================================
138
- // 辅助:向 stdout 写一行(自动补换行)
139
- // ============================================================
140
-
141
- function emitStdoutLine(child: FakeChild, obj: Record<string, unknown>): void {
142
- child.stdout.write(`${JSON.stringify(obj)}\n`);
143
- }
144
-
145
- /** 构造 session header 行(stdout 首行)。 */
146
- function sessionHeader(id = "sess-abc"): Record<string, unknown> {
147
- return {
148
- type: "session",
149
- id,
150
- timestamp: "2026-07-03T12-00-00-000Z",
151
- cwd: "/tmp/test",
152
- };
153
- }
154
-
155
- // ============================================================
156
- // 辅助:构造最小合法的 record / opts / ctx
157
- // ============================================================
158
-
159
- function makeRecord() {
160
- return createRecord("run-1", {
161
- agent: "general-purpose",
162
- model: "test-model",
163
- mode: "background",
164
- task: "do something",
165
- slug: "test",
166
- startedAt: 1_000_000,
167
- rootSessionId: "root-session",
168
- parentRecordId: undefined,
169
- depth: 0,
170
- });
171
- }
172
-
173
- function makeOpts(overrides: Partial<RunOptions> = {}): RunOptions {
174
- return {
175
- resolved: {
176
- model: {
177
- id: "test-model",
178
- name: "Test Model",
179
- provider: "test",
180
- reasoning: false,
181
- },
182
- thinkingLevel: undefined,
183
- },
184
- agentConfig: undefined,
185
- appendSystemPrompt: undefined,
186
- skillPath: undefined,
187
- schema: undefined,
188
- maxTurns: undefined,
189
- graceTurns: undefined,
190
- signal: undefined,
191
- onEvent: undefined,
192
- ...overrides,
193
- };
194
- }
195
-
196
- function makeCtx(overrides: Partial<SessionRunnerContext> = {}): SessionRunnerContext {
197
- return {
198
- cwd: "/tmp/test",
199
- agentDir: "/tmp/test/agents",
200
- skillDirs: [],
201
- mainCwd: "/tmp/test",
202
- mainSessionFile: undefined,
203
- sessionRootId: "root-session-test",
204
- rootCwd: "/tmp/test",
205
- ...overrides,
206
- };
207
- }
208
-
209
- /**
210
- * fake timers 下推进时间直到 spawn 被调用(返回 child 控制器,与共享 helper 的 void 返回
211
- * 不同,故就地维护)。
212
- *
213
- * [快照语义] 同 helpers/spawn-mock.ts 的 waitForSpawn:记调用时 baseline,等待其后的
214
- * **新** spawn——支持同一测试/文件内多次 runSpawn(旧 `length === 0` 只对首次有效)。
215
- *
216
- * runSpawn 在 mkdirSync + writePromptToTempFile(mock 的 async I/O)之后才调 spawn。
217
- * 每次推进 10ms 让轮询 setTimeout 触发,advanceTimersByTimeAsync 同时 flush 已 resolve
218
- * 的 I/O promise,使 runSpawn 继续走到 spawn。
219
- */
220
- async function waitForSpawnFake(timeoutSteps = 200): Promise<FakeChild> {
221
- const baseline = mockSpawn.mock.results.length;
222
- for (let i = 0; i < timeoutSteps; i++) {
223
- if (mockSpawn.mock.results.length > baseline) break;
224
- await vi.advanceTimersByTimeAsync(10);
225
- }
226
- if (mockSpawn.mock.results.length <= baseline) {
227
- throw new Error("spawn was not called (fake timers did not progress to spawn)");
228
- }
229
- return lastSpawnedChild();
230
- }
231
-
232
- /**
233
- * 真实 timers 下轮询直到 spawn 被调用(返回 child 控制器;用于 signal abort 测试——
234
- * 不需要推进 watchdog,用 queueMicrotask 触发 abort,真实 timers 下 mock I/O 正常 resolve)。
235
- *
236
- * [快照语义] 同 helpers/spawn-mock.ts 的 waitForSpawn:记 baseline 等**新** spawn,
237
- * 支持同文件多次 runSpawn。
238
- */
239
- async function waitForSpawnReal(timeoutMs = 1000): Promise<FakeChild> {
240
- const start = Date.now();
241
- const baseline = mockSpawn.mock.results.length;
242
- while (mockSpawn.mock.results.length <= baseline) {
243
- if (Date.now() - start > timeoutMs) {
244
- throw new Error(`spawn was not called within ${timeoutMs}ms`);
245
- }
246
- await new Promise((r) => setTimeout(r, 5));
247
- }
248
- return lastSpawnedChild();
249
- }
250
-
251
- // ============================================================
252
- // 测试
253
- // ============================================================
254
-
255
- describe("timeoutMs / signal abort → child.kill 端到端路径", () => {
256
- beforeEach(() => {
257
- vi.clearAllMocks();
258
- mockExistsSync.mockReturnValue(false);
259
- // [F-4 假红源修复] env 隔离:多数用例默认 maxTurns=undefined →
260
- // resolveSpawnWatchdogMs 走 env 兑底分支,宿主 export SPAWN_WATCHDOG 即假红
261
- //(极端值 3e9 还会在 assertSafeTimerDelay fail-fast 直接炸掉 runSpawn)。
262
- // 用例内显式 stubEnv 的值照常叠加生效(inner describe 的 afterEach
263
- // unstubAllEnvs 与本处兼容)。
264
- vi.stubEnv(SPAWN_WATCHDOG_ENV, "");
265
- });
266
-
267
- afterEach(() => {
268
- vi.restoreAllMocks();
269
- vi.unstubAllEnvs();
270
- });
271
-
272
- // ── 1. watchdog 到期 → child.kill ──
273
- //
274
- // [R1] watchdog = setTimeout(() => child.kill("SIGTERM"), maxTurnsToWatchdogMs(maxTurns))。
275
- // maxTurnsToWatchdogMs 下限 30min(SPAWN_WATCHDOG_FLOOR_MS),maxTurns=6 → max(30min, 30min)=30min。
276
- // 子进程卡死(turn_end 永不触发)时 limiter 失效,watchdog 兜底 kill 防资源泄漏。
277
- describe("watchdog 到期 → signal abort → child.kill", () => {
278
- beforeEach(() => {
279
- vi.useFakeTimers();
280
- });
281
-
282
- afterEach(() => {
283
- vi.useRealTimers();
284
- });
285
-
286
- it("watchdog 到期(超过 maxTurnsToWatchdogMs 阈值)→ child.kill(SIGTERM) 被调用", async () => {
287
- const record = makeRecord();
288
- // maxTurns=6 → maxTurnsToWatchdogMs = max(30min, 6*5min) = 30min
289
- // 不 await:runSpawn 内部 await 子进程 close,watchdog kill 后还需 emit close 才 resolve
290
- const promise = runSpawn(record, "Task: hang", makeOpts({ maxTurns: 6 }), makeCtx());
291
-
292
- const child = await waitForSpawnFake();
293
-
294
- // spawn 后尚未触发 kill
295
- expect(child.killed).toBe(false);
296
-
297
- // 推进时间越过 watchdog 阈值(30 * 60 * 1000 + 100ms 余量)
298
- await vi.advanceTimersByTimeAsync(30 * 60 * 1000 + 100);
299
-
300
- // watchdog 触发 child.kill("SIGTERM")
301
- expect(child.killed).toBe(true);
302
- expect(child.killSignal).toBe("SIGTERM");
303
-
304
- // 收尾:emit close 让 runSpawn resolve(避免悬挂)
305
- emitStdoutLine(child, sessionHeader());
306
- child.stdout.end();
307
- child.emit("close", 143); // SIGTERM = 128+15
308
-
309
- const result = await promise;
310
- // 信号终止(>=128)视为正常完成
311
- expect(result.success).toBe(true);
312
- });
313
-
314
- // [race-F4] SIGKILL 升级:watchdog SIGTERM 后子进程无视信号(exitCode/signalCode
315
- // 保持 null = 未退出)→ 30s 后升级 SIGKILL。挂住子进程永不回收的兑底防线。
316
- it("[race-F4] watchdog SIGTERM 后 30s 未 exit → 升级 SIGKILL", async () => {
317
- const record = makeRecord();
318
- const promise = runSpawn(record, "Task: hang ignores SIGTERM", makeOpts({ maxTurns: 6 }), makeCtx());
319
-
320
- const child = await waitForSpawnFake();
321
-
322
- // watchdog 到期 → SIGTERM(升级路径入口)
323
- await vi.advanceTimersByTimeAsync(30 * 60 * 1000 + 100);
324
- expect(child.killSignal).toBe("SIGTERM");
325
-
326
- // 子进程无视 SIGTERM:不 emit exit/close,exitCode/signalCode 保持 null。
327
- // 推进升级窗口:非贴边断言(waitForSpawnFake 每步 10ms 推进,升级 timer 挂载
328
- // 时刻有 ≤100ms 抖动)——先推 29s(余量 > 抖动)断言未升级,再推 2s 越过窗口。
329
- await vi.advanceTimersByTimeAsync(29 * 1000);
330
- expect(child.killSignal).toBe("SIGTERM"); // 尚未升级
331
- await vi.advanceTimersByTimeAsync(2 * 1000);
332
- expect(child.killSignal).toBe("SIGKILL"); // 已升级
333
-
334
- // 收尾:SIGKILL 后子进程退出 → runSpawn resolve(信号终止视为完成)
335
- emitStdoutLine(child, sessionHeader());
336
- child.stdout.end();
337
- child.emit("close", 137); // SIGKILL = 128+9
338
- const result = await promise;
339
- expect(result.success).toBe(true);
340
- });
341
-
342
- // [race-F4] 升级 timer 的 exit 自动 clear:SIGTERM 后子进程正常退出(exit 事件到
343
- // 达),升级 timer 被清,不误发 SIGKILL(killSignal 停留在 SIGTERM)。
344
- it("[race-F4] SIGTERM 后子进程 exit → 升级 timer 被 clear,不误发 SIGKILL", async () => {
345
- const record = makeRecord();
346
- const promise = runSpawn(record, "Task: dies on SIGTERM", makeOpts({ maxTurns: 6 }), makeCtx());
347
-
348
- const child = await waitForSpawnFake();
349
-
350
- // watchdog 到期 → SIGTERM
351
- await vi.advanceTimersByTimeAsync(30 * 60 * 1000 + 100);
352
- expect(child.killSignal).toBe("SIGTERM");
353
-
354
- // 子进程响应 SIGTERM 退出:exitCode 143(SIGTERM 终止形态)+ exit + close。
355
- // exit 事件触发升级 timer clear(公共 kill-chain 的 exit 等待 promise settle → timer clear)。
356
- (child as unknown as { exitCode: number }).exitCode = 143;
357
- child.emit("exit", 143);
358
- child.stdout.end();
359
- child.emit("close", 143);
360
- const result = await promise;
361
-
362
- // 越过升级窗口:无 SIGKILL(timer 已被 exit clear),killSignal 停留在 SIGTERM
363
- await vi.advanceTimersByTimeAsync(60 * 1000);
364
- expect(child.killSignal).toBe("SIGTERM");
365
- expect(result.success).toBe(true);
366
- });
367
- });
368
-
369
- // ── 2. watchdog 默认不挂载 + env 兑底(预算语义对齐 2026-08)──
370
- //
371
- // 语义:maxTurns 未传 → env 兑底;显式 0/负 → 压过 env 不挂(U5,SP-6 参数 > env,
372
- // 旧实现 maxTurns:0 落到 env 兑底,参数显式关不掉 watchdog,已废);
373
- // env XYZ_SUBAGENT_SPAWN_WATCHDOG_MS 设置时按绝对时限兑底挂载(hang 泄漏防线
374
- // opt-in)。resolveSpawnWatchdogMs 是挂载判定唯一入口。
375
- describe("watchdog 默认不挂载 + env 兑底(预算语义对齐)", () => {
376
- beforeEach(() => {
377
- vi.useFakeTimers();
378
- });
379
-
380
- afterEach(() => {
381
- vi.useRealTimers();
382
- vi.unstubAllEnvs();
383
- });
384
-
385
- it("resolveSpawnWatchdogMs:maxTurns 未传/0/负数且 env 未设 → undefined(不挂)", () => {
386
- vi.stubEnv(SPAWN_WATCHDOG_ENV, ""); // 空串 = 未设(raw falsy)
387
- expect(resolveSpawnWatchdogMs(undefined)).toBeUndefined();
388
- expect(resolveSpawnWatchdogMs(null)).toBeUndefined();
389
- expect(resolveSpawnWatchdogMs(0)).toBeUndefined();
390
- expect(resolveSpawnWatchdogMs(-5)).toBeUndefined();
391
- });
392
-
393
- it("resolveSpawnWatchdogMs:env 设置 → 绝对时限;非法值 → undefined;maxTurns 有效优先", () => {
394
- vi.stubEnv(SPAWN_WATCHDOG_ENV, "60000");
395
- expect(resolveSpawnWatchdogMs(undefined)).toBe(60_000);
396
- // [U5] SP-6 参数 > env:显式 0 压过 env 兑底 → 不挂(旧实现落成 60000)
397
- expect(resolveSpawnWatchdogMs(0)).toBeUndefined();
398
- expect(resolveSpawnWatchdogMs(-5)).toBeUndefined();
399
- // maxTurns 有效(>0)→ turns 估算优先于 env 兑底
400
- expect(resolveSpawnWatchdogMs(6)).toBe(maxTurnsToWatchdogMs(6));
401
- vi.stubEnv(SPAWN_WATCHDOG_ENV, "abc");
402
- expect(resolveSpawnWatchdogMs(undefined)).toBeUndefined();
403
- vi.stubEnv(SPAWN_WATCHDOG_ENV, "-1");
404
- expect(resolveSpawnWatchdogMs(undefined)).toBeUndefined();
405
- });
406
-
407
- // [U1] setTimeout 2^31-1 溢出 fail-fast:溢出 delay 被 Node 置 1ms 立即触发
408
- //(watchdog 变「启动即杀」),两路径(env 兑底 / turns 估算)都必须在入口拦截。
409
- it("resolveSpawnWatchdogMs:env 值溢出(>2^31-1)→ fail-fast throw(不静默 clamp/不挂)", () => {
410
- vi.stubEnv(SPAWN_WATCHDOG_ENV, "3000000000");
411
- expect(() => resolveSpawnWatchdogMs(undefined)).toThrowError(/2147483647/);
412
- expect(() => resolveSpawnWatchdogMs(undefined)).toThrowError(/Recovery/);
413
- });
414
-
415
- it("resolveSpawnWatchdogMs:maxTurns 估算值溢出 → fail-fast throw", () => {
416
- vi.stubEnv(SPAWN_WATCHDOG_ENV, "");
417
- // maxTurns=1e6 → 估算 5e9 ms > 2^31-1,入口拦截
418
- expect(() => resolveSpawnWatchdogMs(1_000_000)).toThrowError(/2147483647/);
419
- });
420
-
421
- // [F-R3] maxTurns 垃圾值网格:NaN/""/null/0/负数(含 ±Infinity)。
422
- // 旧实现 NaN/"" 绕过 Number.isFinite 且压过 env 兑底成「显式不限」。
423
- it("resolveSpawnWatchdogMs:maxTurns NaN → fail-fast throw(压过 env);\"\"→Number(\"\")=0 显式不限;null/0/负数网格(F-R3)", () => {
424
- vi.stubEnv(SPAWN_WATCHDOG_ENV, "60000");
425
- // NaN:非有限数 fail-fast(与 timer-delay U1 同语义),不再压过 env 兑底成「显式不限」
426
- expect(() => resolveSpawnWatchdogMs(NaN)).toThrowError(/not a finite number/);
427
- expect(() => resolveSpawnWatchdogMs(NaN)).toThrowError(/Recovery/);
428
- // ±Infinity:同非有限数 fail-fast
429
- expect(() => resolveSpawnWatchdogMs(Infinity)).toThrowError(/not a finite number/);
430
- // "":Number("") === 0 → 与显式 0 同路径(显式不限,压过 env,U5 语义不回归)
431
- expect(resolveSpawnWatchdogMs("" as unknown as number)).toBeUndefined();
432
- // null → 未传语义,env 兑底生效
433
- expect(resolveSpawnWatchdogMs(null)).toBe(60_000);
434
- // 0 / 负数 → 显式不限,压过 env(既有 U5 网格不回归)
435
- expect(resolveSpawnWatchdogMs(0)).toBeUndefined();
436
- expect(resolveSpawnWatchdogMs(-5)).toBeUndefined();
437
- });
438
-
439
- it("maxTurns 未传 → 推进 56min 不 kill(旧实现 50min 估算会误杀)", async () => {
440
- vi.stubEnv(SPAWN_WATCHDOG_ENV, "");
441
- const record = makeRecord();
442
- const promise = runSpawn(record, "Task: no-turns", makeOpts({ maxTurns: undefined }), makeCtx());
443
-
444
- const child = await waitForSpawnFake();
445
- expect(child.killed).toBe(false);
446
-
447
- // 旧实现的估算默认(10 turns → 50min)已过,仍不 kill = watchdog 未挂载
448
- await vi.advanceTimersByTimeAsync(56 * 60 * 1000);
449
- expect(child.killed).toBe(false);
450
-
451
- emitStdoutLine(child, sessionHeader());
452
- child.stdout.end();
453
- child.emit("close", 0);
454
- const result = await promise;
455
- expect(result.success).toBe(true);
456
- });
457
-
458
- it("maxTurns: 0(显式关不掉的旧默认已废)→ 同样不挂 watchdog", async () => {
459
- vi.stubEnv(SPAWN_WATCHDOG_ENV, "");
460
- const record = makeRecord();
461
- const promise = runSpawn(record, "Task: zero-turns", makeOpts({ maxTurns: 0 }), makeCtx());
462
-
463
- const child = await waitForSpawnFake();
464
- await vi.advanceTimersByTimeAsync(56 * 60 * 1000);
465
- expect(child.killed).toBe(false);
466
-
467
- emitStdoutLine(child, sessionHeader());
468
- child.stdout.end();
469
- child.emit("close", 0);
470
- const result = await promise;
471
- expect(result.success).toBe(true);
472
- });
473
-
474
- // [U5] SP-6 参数 > env:显式 maxTurns:0 压过 SPAWN_WATCHDOG_ENV 兑底——
475
- // 旧实现落成 env 时限 kill,参数显式关不掉 watchdog。
476
- it("maxTurns: 0 + env=60000 → 显式 0 压过 env,61s 不 kill(U5)", async () => {
477
- vi.stubEnv(SPAWN_WATCHDOG_ENV, "60000");
478
- const record = makeRecord();
479
- const promise = runSpawn(record, "Task: zero-turns-override", makeOpts({ maxTurns: 0 }), makeCtx());
480
-
481
- const child = await waitForSpawnFake();
482
- // 推进越过 env 兑底时限(60s):若 maxTurns:0 未压过 env,此处已被 kill
483
- await vi.advanceTimersByTimeAsync(61 * 1000);
484
- expect(child.killed).toBe(false);
485
-
486
- emitStdoutLine(child, sessionHeader());
487
- child.stdout.end();
488
- child.emit("close", 0);
489
- const result = await promise;
490
- expect(result.success).toBe(true);
491
- });
492
-
493
- it("env SPAWN_WATCHDOG_MS=60000 + maxTurns 未传 → 1min 兑底 kill", async () => {
494
- vi.stubEnv(SPAWN_WATCHDOG_ENV, "60000");
495
- const record = makeRecord();
496
- const promise = runSpawn(record, "Task: env-watchdog", makeOpts(), makeCtx());
497
-
498
- const child = await waitForSpawnFake();
499
- // 非贴边断言:waitForSpawnFake 每步 10ms 推进,watchdog 挂载时刻有 ≤10ms 抖动,
500
- // 用 55s / 61s 两个窗口避开 60s 贴边比较
501
- await vi.advanceTimersByTimeAsync(55_000);
502
- expect(child.killed).toBe(false);
503
- await vi.advanceTimersByTimeAsync(6_000);
504
- expect(child.killed).toBe(true);
505
- expect(child.killSignal).toBe("SIGTERM");
506
-
507
- emitStdoutLine(child, sessionHeader());
508
- child.stdout.end();
509
- child.emit("close", 143);
510
- const result = await promise;
511
- expect(result.success).toBe(true);
512
- });
513
- });
514
-
515
- // ── 3. watchdog 到期前正常完成 → clearTimeout 生效,不 kill ──
516
- describe("watchdog 到期前正常完成 → 不 kill", () => {
517
- beforeEach(() => {
518
- vi.useFakeTimers();
519
- });
520
-
521
- afterEach(() => {
522
- vi.useRealTimers();
523
- });
524
-
525
- it("正常 close(0) 先于 watchdog 到期 → clearTimeout 生效,推进时间后 child 未被 kill", async () => {
526
- const record = makeRecord();
527
- // maxTurns=6 → watchdog=30min
528
- const promise = runSpawn(record, "Task: quick", makeOpts({ maxTurns: 6 }), makeCtx());
529
-
530
- const child = await waitForSpawnFake();
531
-
532
- // 正常完成:emit header + close(0)(远早于 30min watchdog)
533
- emitStdoutLine(child, sessionHeader());
534
- child.stdout.end();
535
- child.emit("close", 0);
536
-
537
- const result = await promise;
538
- expect(result.success).toBe(true);
539
-
540
- // close 后 runSpawn 已 clearTimeout(watchdog);推进 30+ 分钟验证 watchdog 未触发 kill
541
- await vi.advanceTimersByTimeAsync(30 * 60 * 1000 + 100);
542
-
543
- expect(child.killed).toBe(false);
544
- expect(child.killSignal).toBeUndefined();
545
- });
546
- });
547
-
548
- // ── 3. 外部 signal abort → child.kill ──
549
- //
550
- // [d] onAbort = () => child.kill("SIGTERM"),opts.signal.addEventListener("abort", onAbort, {once:true})。
551
- // 前置检查:if (opts.signal?.aborted) onAbort()——spawn 前已 aborted 时 addEventListener
552
- // 不会触发,立即 kill 兑现取消语义。
553
- describe("外部 signal abort → child.kill", () => {
554
- it("运行中 abort signal → child.kill(SIGTERM) 被调用,success=false(取消语义)", async () => {
555
- const controller = new AbortController();
556
- const record = makeRecord();
557
- const promise = runSpawn(
558
- record,
559
- "Task: cancelled",
560
- makeOpts({ signal: controller.signal }),
561
- makeCtx(),
562
- );
563
-
564
- const child = await waitForSpawnReal();
565
-
566
- // abort 必须在 spawn 之后(addEventListener 已注册)。
567
- // queueMicrotask 延迟到当前微任务清空后触发,确保 listener 已挂载。
568
- queueMicrotask(() => controller.abort());
569
-
570
- // emit header + close(被 kill 后子进程退出,signal 终止 exitCode>=128)
571
- emitStdoutLine(child, sessionHeader());
572
- child.stdout.end();
573
- child.emit("close", 143); // SIGTERM = 128+15
574
-
575
- const result = await promise;
576
-
577
- expect(child.killed).toBe(true);
578
- expect(child.killSignal).toBe("SIGTERM");
579
- // signal.aborted 路径:success=false,但 error 为 undefined(取消不算 error)
580
- expect(result.success).toBe(false);
581
- expect(result.error).toBeUndefined();
582
- });
583
-
584
- it("spawn 前已 aborted 的 signal → 前置检查立即 kill,兑现取消语义", async () => {
585
- // 覆盖 session-runner.ts L570: if (opts.signal?.aborted) onAbort()
586
- // 已 aborted 的 signal addEventListener("abort") 不会再触发回调,
587
- // 故 runSpawn 在注册 listener 后立即前置检查,直接 kill 兑现取消。
588
- const controller = new AbortController();
589
- controller.abort(); // spawn 前已 abort
590
-
591
- const record = makeRecord();
592
- const promise = runSpawn(
593
- record,
594
- "Task: pre-aborted",
595
- makeOpts({ signal: controller.signal }),
596
- makeCtx(),
597
- );
598
-
599
- const child = await waitForSpawnReal();
600
-
601
- // 前置检查在 spawn 后同步执行 → child 立即被 kill
602
- expect(child.killed).toBe(true);
603
- expect(child.killSignal).toBe("SIGTERM");
604
-
605
- // 收尾:emit close 让 runSpawn resolve
606
- emitStdoutLine(child, sessionHeader());
607
- child.stdout.end();
608
- child.emit("close", 143);
609
-
610
- const result = await promise;
611
- // signal.aborted → success=false,error undefined
612
- expect(result.success).toBe(false);
613
- expect(result.error).toBeUndefined();
614
- });
615
- });
616
- });