@zhushanwen/subagent-core 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-KXVBIVOU.js +16 -0
  3. package/dist/chunk-T2SYBW3I.js +24 -0
  4. package/dist/chunk-VURUAGMM.js +2305 -0
  5. package/dist/{types-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
  6. package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -668
  7. package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
  8. package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
  9. package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
  10. package/dist/execution/engine/engine-discovery-scan.js +20 -0
  11. package/dist/execution/engine/paths.cjs +6 -23
  12. package/dist/execution/engine/paths.d.cts +1 -8
  13. package/dist/execution/engine/paths.d.ts +1 -8
  14. package/dist/execution/engine/paths.js +1 -1
  15. package/dist/execution/relay-env.cjs +12 -28
  16. package/dist/execution/relay-env.d.cts +1 -33
  17. package/dist/execution/relay-env.d.ts +1 -33
  18. package/dist/execution/relay-env.js +5 -3
  19. package/dist/index.cjs +14045 -16059
  20. package/dist/index.d.cts +1035 -1579
  21. package/dist/index.d.ts +1035 -1579
  22. package/dist/index.js +9838 -13800
  23. package/dist.bundle/index.cjs +28875 -30323
  24. package/package.json +20 -26
  25. package/src/__tests__/record-store-last-line.test.ts +16 -7
  26. package/src/core/error-message.ts +7 -6
  27. package/src/core/host-services.ts +8 -2
  28. package/src/core/notify-ports.ts +17 -2
  29. package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
  30. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
  31. package/src/execution/__tests__/collect-coordinator-service.test.ts +36 -47
  32. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
  33. package/src/execution/__tests__/delivery-methods.test.ts +179 -237
  34. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
  35. package/src/execution/__tests__/execute-nesting.test.ts +20 -13
  36. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +20 -33
  37. package/src/execution/__tests__/gc-timer.test.ts +7 -22
  38. package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
  39. package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
  40. package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
  41. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
  42. package/src/execution/__tests__/idle-gc.test.ts +200 -0
  43. package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
  44. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
  45. package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
  46. package/src/execution/__tests__/record-store.test.ts +60 -6
  47. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
  48. package/src/execution/__tests__/round-supervisor.test.ts +293 -0
  49. package/src/execution/__tests__/session-pending.test.ts +119 -17
  50. package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
  51. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -11
  52. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
  53. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
  54. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
  55. package/src/execution/__tests__/subagent-service.test.ts +2 -2
  56. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
  57. package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
  58. package/src/execution/__tests__/sync-collect-recovery.test.ts +21 -36
  59. package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
  60. package/src/execution/agent-registry.ts +6 -2
  61. package/src/execution/dialog-queue.ts +6 -73
  62. package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
  63. package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
  64. package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
  65. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
  66. package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
  67. package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
  68. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
  69. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
  70. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
  71. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
  72. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
  73. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
  74. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
  75. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +81 -54
  76. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
  77. package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
  78. package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
  79. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +7 -2
  80. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
  81. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
  82. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
  83. package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
  84. package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
  85. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
  86. package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
  87. package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
  88. package/src/execution/engine/__tests__/registry.test.ts +206 -1
  89. package/src/execution/engine/__tests__/routing.test.ts +97 -0
  90. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
  91. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
  92. package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
  93. package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
  94. package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
  95. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
  96. package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
  97. package/src/execution/engine/client/client-options.ts +103 -0
  98. package/src/execution/engine/client/engine-client.ts +687 -0
  99. package/src/execution/engine/client/mirror.ts +167 -0
  100. package/src/execution/engine/client/pid-file.ts +392 -0
  101. package/src/execution/engine/client/reaper.ts +95 -0
  102. package/src/execution/engine/client/remote-engine.ts +441 -0
  103. package/src/execution/engine/client/reverse-router.ts +255 -0
  104. package/src/execution/engine/common/capability-gate.ts +109 -22
  105. package/src/execution/engine/common/errors.ts +9 -25
  106. package/src/execution/engine/common/event-journal.ts +4 -50
  107. package/src/execution/engine/common/journal-replay.ts +3 -1
  108. package/src/execution/engine/common/kill-chain.ts +18 -251
  109. package/src/execution/engine/common/nesting-guard.ts +18 -114
  110. package/src/execution/engine/common/pool-manager.ts +4 -0
  111. package/src/execution/engine/common/session-view-service.ts +56 -84
  112. package/src/execution/engine/config.ts +151 -0
  113. package/src/execution/engine/d8-compat.ts +346 -0
  114. package/src/execution/engine/engine-discovery-roots.ts +107 -0
  115. package/src/execution/engine/engine-discovery-scan.ts +269 -0
  116. package/src/execution/engine/engine-discovery.ts +30 -7
  117. package/src/execution/engine/engine-inspect-package.ts +282 -0
  118. package/src/execution/engine/engine-manifest.ts +244 -0
  119. package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
  120. package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
  121. package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
  122. package/src/execution/engine/host/host-bridge.ts +150 -0
  123. package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
  124. package/src/execution/engine/host/pi-host-binding.ts +91 -0
  125. package/src/execution/engine/host/spawned-children.ts +139 -0
  126. package/src/execution/engine/host-task-spec.ts +29 -2
  127. package/src/execution/engine/model-validation.ts +55 -2
  128. package/src/execution/engine/paths.ts +16 -41
  129. package/src/execution/engine/port.ts +48 -2
  130. package/src/execution/engine/registry.ts +153 -34
  131. package/src/execution/engine/routing.ts +225 -52
  132. package/src/execution/engine/types.ts +25 -200
  133. package/src/execution/finalize-record.ts +25 -0
  134. package/src/execution/idle-gc.ts +89 -14
  135. package/src/execution/lifecycle-manager.ts +10 -7
  136. package/src/execution/lifecycle-predicates.ts +10 -8
  137. package/src/execution/record-store.ts +29 -2
  138. package/src/execution/relay-env.ts +19 -36
  139. package/src/execution/round-supervisor/domain.ts +89 -0
  140. package/src/execution/round-supervisor/index.ts +33 -0
  141. package/src/execution/round-supervisor/notify-accounting.ts +87 -0
  142. package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
  143. package/src/execution/round-supervisor/service-binding.test.ts +661 -0
  144. package/src/execution/round-supervisor/service-binding.ts +273 -0
  145. package/src/execution/round-supervisor/supervisor.ts +426 -0
  146. package/src/execution/session-pending.ts +68 -74
  147. package/src/execution/settled-watchdog.ts +39 -0
  148. package/src/execution/subagent-service.ts +746 -400
  149. package/src/execution/subprocess-agent-runner.ts +16 -8
  150. package/src/execution/types.ts +41 -117
  151. package/src/execution/ui-channels.ts +13 -214
  152. package/src/execution/ui-request-handler-factory.ts +7 -4
  153. package/src/execution/ui-request-observability.ts +2 -2
  154. package/src/execution/workflow-state-root.ts +67 -0
  155. package/src/execution/worktree-git-ops.ts +5 -0
  156. package/src/execution/worktree-manager.ts +11 -1
  157. package/src/index.ts +55 -26
  158. package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
  159. package/src/orchestration/execute-agent-call.ts +1 -1
  160. package/src/orchestration/file-run-store.ts +144 -59
  161. package/src/orchestration/models/types.ts +43 -42
  162. package/src/shared/zcode-model-ref.ts +31 -0
  163. package/dist/chunk-43ONBFZX.js +0 -240
  164. package/dist/chunk-A4IVZWVX.js +0 -31
  165. package/dist/chunk-APZY4IME.js +0 -27
  166. package/dist/chunk-V3VHZ2VX.js +0 -59
  167. package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
  168. package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
  169. package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
  170. package/dist/execution/engine/engines/zcode/constants.js +0 -54
  171. package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
  172. package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
  173. package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
  174. package/dist/execution/engine/engines/zcode/reader.js +0 -9
  175. package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
  176. package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
  177. package/src/__tests__/session-runner.test.ts +0 -69
  178. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  179. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
  180. package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
  181. package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
  182. package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
  183. package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
  184. package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
  185. package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
  186. package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
  187. package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
  188. package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
  189. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
  190. package/src/execution/__tests__/output-collector.test.ts +0 -469
  191. package/src/execution/__tests__/pi-invocation.test.ts +0 -166
  192. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
  193. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  194. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
  195. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
  196. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
  197. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
  198. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  199. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  200. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  201. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
  202. package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
  203. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
  204. package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
  205. package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
  206. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
  207. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
  208. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
  209. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
  210. package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
  211. package/src/execution/__tests__/spawn-args.test.ts +0 -446
  212. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
  213. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  214. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
  215. package/src/execution/__tests__/spawned-children.test.ts +0 -236
  216. package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
  217. package/src/execution/__tests__/stdin-writer.test.ts +0 -464
  218. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
  219. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  220. package/src/execution/__tests__/timeout-integration.test.ts +0 -616
  221. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  222. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  223. package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
  224. package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
  225. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
  226. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  227. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
  228. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  229. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
  230. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  231. package/src/execution/engine/common/schema-emulation.ts +0 -189
  232. package/src/execution/engine/common/session-view-projection.ts +0 -51
  233. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
  234. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  235. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
  236. package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
  237. package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
  238. package/src/execution/engine/engines/pi/output-collector.ts +0 -317
  239. package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
  240. package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
  241. package/src/execution/engine/engines/pi/reader.ts +0 -59
  242. package/src/execution/engine/engines/pi/registration.ts +0 -37
  243. package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
  244. package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
  245. package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
  246. package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
  247. package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
  248. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
  249. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
  250. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
  251. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
  252. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
  253. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
  254. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  255. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
  256. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
  257. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
  258. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
  259. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
  260. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
  261. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
  262. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
  263. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
  264. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
  265. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
  266. package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
  267. package/src/execution/engine/engines/zcode/connection.ts +0 -598
  268. package/src/execution/engine/engines/zcode/constants.ts +0 -176
  269. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
  270. package/src/execution/engine/engines/zcode/parser.ts +0 -102
  271. package/src/execution/engine/engines/zcode/preparer.ts +0 -235
  272. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  273. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  274. package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
  275. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
  276. package/src/execution/round-settlement.ts +0 -93
  277. package/src/execution/ui-request-queue.ts +0 -215
  278. package/src/shared/schema-env.ts +0 -44
@@ -1,298 +0,0 @@
1
- // src/__tests__/ui-request-queue.test.ts
2
- //
3
- // W3 测试:UI 请求队列机制(L1 per-child createUiRequestQueue)
4
- // 1. 多个请求按 FIFO 顺序处理
5
- // 2. 第一个请求未完成时第二个不开始
6
- //
7
- // 直接测试 createUiRequestQueue 的队列逻辑(纯函数,不需要 mock runSpawn)。
8
- // 用 fake child(PassThrough stdin)+ 手动控制 Promise resolve 时序。
9
- //
10
- // 协议迁移(W2):enqueue 第二参从旧 Record params(含 questions/context)改为
11
- // ExtensionUiRequest(method 平铺);handler 签名从 (questions,context) 改为
12
- // UiRequestHandler (req: UiRequest) => Promise<UiResponse>。
13
-
14
- import type { ChildProcess } from "node:child_process";
15
- import { PassThrough } from "node:stream";
16
-
17
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
18
-
19
- import {
20
- createUiRequestQueue,
21
- type UiRequest,
22
- type UiRequestHandler,
23
- type UiResponse,
24
- } from "../ui-request-queue.ts";
25
-
26
- beforeEach(() => {
27
- vi.useFakeTimers();
28
- });
29
-
30
- afterEach(() => {
31
- vi.useRealTimers();
32
- });
33
-
34
- function makeFakeChild(): ChildProcess {
35
- const stdin = new PassThrough();
36
- return {
37
- stdin,
38
- killed: false,
39
- pid: 10001,
40
- on: vi.fn(),
41
- removeListener: vi.fn(),
42
- } as unknown as ChildProcess;
43
- }
44
-
45
- /** 构造 select method 的 ExtensionUiRequest(ask_user 借道 select dialog 通道)。 */
46
- function makeSelectReq(question: string): { method: "select"; title: string; options: string[] } {
47
- return {
48
- method: "select",
49
- title: question,
50
- options: [JSON.stringify({ question, options: [{ label: "A" }] })],
51
- };
52
- }
53
-
54
- describe("UI 请求队列", () => {
55
- it("多个 extension_ui_request 按 FIFO 顺序处理", async () => {
56
- const callOrder: string[] = [];
57
- // 每个请求返回独立的可控 Promise
58
- const resolvers: Array<(v: UiResponse) => void> = [];
59
-
60
- const handler: UiRequestHandler = vi.fn((req: UiRequest) => {
61
- callOrder.push(req.title ?? "");
62
- return new Promise<UiResponse>((resolve) => {
63
- resolvers.push(resolve);
64
- });
65
- }) as unknown as UiRequestHandler;
66
-
67
- const child = makeFakeChild();
68
- const ctx = { uiRequestHandler: handler } as Parameters<
69
- typeof createUiRequestQueue
70
- >[1];
71
- const enqueue = createUiRequestQueue(child, ctx);
72
-
73
- // 快速入队三个请求(handler 被调用但不 resolve)
74
- enqueue("r1", makeSelectReq("Q1"));
75
- enqueue("r2", makeSelectReq("Q2"));
76
- enqueue("r3", makeSelectReq("Q3"));
77
-
78
- // 第一个请求立即开始处理
79
- expect(handler).toHaveBeenCalledTimes(1);
80
- expect(callOrder).toEqual(["Q1"]);
81
-
82
- // resolve 第一个 → 第二个开始处理
83
- resolvers[0]({ value: "a1" });
84
- await vi.advanceTimersByTimeAsync(0);
85
- expect(handler).toHaveBeenCalledTimes(2);
86
- expect(callOrder).toEqual(["Q1", "Q2"]);
87
-
88
- // resolve 第二个 → 第三个开始处理
89
- resolvers[1]({ value: "a2" });
90
- await vi.advanceTimersByTimeAsync(0);
91
- expect(handler).toHaveBeenCalledTimes(3);
92
- expect(callOrder).toEqual(["Q1", "Q2", "Q3"]);
93
- });
94
-
95
- it("第一个请求未 resolve 时第二个不调用 uiRequestHandler", async () => {
96
- const callOrder: string[] = [];
97
- let firstResolve: (v: UiResponse) => void;
98
-
99
- const handler: UiRequestHandler = vi.fn((req: UiRequest) => {
100
- callOrder.push(req.title ?? "");
101
- if (req.title === "Q1") {
102
- return new Promise<UiResponse>((resolve) => {
103
- firstResolve = resolve;
104
- });
105
- }
106
- return Promise.resolve<UiResponse>({ value: "done" });
107
- }) as unknown as UiRequestHandler;
108
-
109
- const child = makeFakeChild();
110
- const ctx = { uiRequestHandler: handler } as Parameters<
111
- typeof createUiRequestQueue
112
- >[1];
113
- const enqueue = createUiRequestQueue(child, ctx);
114
-
115
- enqueue("r1", makeSelectReq("Q1"));
116
- enqueue("r2", makeSelectReq("Q2"));
117
-
118
- // 只有 Q1 被调用,Q2 还在队列里
119
- expect(handler).toHaveBeenCalledTimes(1);
120
- expect(callOrder).toEqual(["Q1"]);
121
-
122
- // 等一下,Q2 仍然不应该被调用
123
- await vi.advanceTimersByTimeAsync(100);
124
- expect(handler).toHaveBeenCalledTimes(1);
125
- expect(callOrder).toEqual(["Q1"]);
126
-
127
- // resolve Q1 → Q2 才开始
128
- firstResolve!({ value: "a1" });
129
- await vi.advanceTimersByTimeAsync(0);
130
- expect(handler).toHaveBeenCalledTimes(2);
131
- expect(callOrder).toEqual(["Q1", "Q2"]);
132
- });
133
- });
134
-
135
- // ── LC-3/T2⑦ 验收⑤:timeout 字段透传链路(L1 队列 → handler 段)──
136
- // 下游段(handler → dialogQueue 消费)由 dialog-queue.test.ts / ui-request-handler-factory.test.ts 锁死。
137
-
138
- describe("UI 请求队列 — timeout 字段透传(LC-3/T2⑦)", () => {
139
- it("ExtensionUiRequest.timeout 经 handleUiRequest 到达 uiRequestHandler 收到的 UiRequest", async () => {
140
- const received: UiRequest[] = [];
141
- const handler: UiRequestHandler = (req: UiRequest) => {
142
- received.push(req);
143
- return Promise.resolve<UiResponse>({ value: "ok" });
144
- };
145
-
146
- const child = makeFakeChild();
147
- const ctx = { uiRequestHandler: handler } as Parameters<
148
- typeof createUiRequestQueue
149
- >[1];
150
- const enqueue = createUiRequestQueue(child, ctx);
151
-
152
- enqueue("r1", { ...makeSelectReq("Q1"), timeout: 5000 });
153
- await vi.advanceTimersByTimeAsync(0);
154
-
155
- expect(received).toHaveLength(1);
156
- expect(received[0].timeout).toBe(5000);
157
- });
158
- });
159
-
160
- // ── extractMethodFields 字段复制链路(经 createUiRequestQueue → handleUiRequest 锁定)──
161
- // 13 个 method-specific 字段的复制语义:typed 守卫字段(string/number/array)验型通过才复制、
162
- // presence-only 字段(statusText/widgetLines/widgetPlacement)仅 in 检查直赋、缺失字段不复制。
163
- // 协议形状来自 JSON 反序列化,坏类型运行时可达——用例以 as unknown as 构造越界形状做防御锁定。
164
-
165
- describe("UI 请求队列 — method-specific 字段复制(extractMethodFields 链路)", () => {
166
- function makeQueue(handler: UiRequestHandler): { enqueue: ReturnType<typeof createUiRequestQueue> } {
167
- const child = makeFakeChild();
168
- const ctx = { uiRequestHandler: handler } as Parameters<
169
- typeof createUiRequestQueue
170
- >[1];
171
- return { enqueue: createUiRequestQueue(child, ctx) };
172
- }
173
-
174
- it("全字段请求:typed 守卫字段与 presence-only 字段全部复制到 UiRequest", async () => {
175
- const received: UiRequest[] = [];
176
- const { enqueue } = makeQueue((req) => {
177
- received.push(req);
178
- return Promise.resolve<UiResponse>({ ack: true });
179
- });
180
-
181
- // 13 字段全量形状(协议 union 无单变体携带全部字段,运行时平铺可达)
182
- const req = {
183
- method: "select",
184
- title: "t",
185
- options: ["a", "b"],
186
- message: "m",
187
- placeholder: "p",
188
- prefill: "pf",
189
- notifyType: "warning",
190
- statusKey: "sk",
191
- statusText: "st",
192
- widgetKey: "wk",
193
- widgetLines: ["l1"],
194
- widgetPlacement: "belowEditor",
195
- text: "tx",
196
- timeout: 2500,
197
- } as unknown as Parameters<ReturnType<typeof createUiRequestQueue>>[1];
198
-
199
- enqueue("r1", req);
200
- await vi.advanceTimersByTimeAsync(0);
201
-
202
- expect(received).toHaveLength(1);
203
- expect(received[0]).toMatchObject({
204
- title: "t",
205
- options: ["a", "b"],
206
- message: "m",
207
- placeholder: "p",
208
- prefill: "pf",
209
- notifyType: "warning",
210
- statusKey: "sk",
211
- statusText: "st",
212
- widgetKey: "wk",
213
- widgetLines: ["l1"],
214
- widgetPlacement: "belowEditor",
215
- text: "tx",
216
- timeout: 2500,
217
- });
218
- });
219
-
220
- it("typed 守卫字段类型不符时跳过复制,其余字段照常", async () => {
221
- const received: UiRequest[] = [];
222
- const { enqueue } = makeQueue((req) => {
223
- received.push(req);
224
- return Promise.resolve<UiResponse>({ ack: true });
225
- });
226
-
227
- const req = {
228
- method: "select",
229
- title: "kept-title", // 合法(title 非法值会在链路上游 startsWith 处炸掉整条请求,见下一条用例)
230
- options: "not-array", // string ≠ array → 跳过
231
- timeout: "5000", // string ≠ number → 跳过
232
- message: "kept",
233
- } as unknown as Parameters<ReturnType<typeof createUiRequestQueue>>[1];
234
-
235
- enqueue("r1", req);
236
- await vi.advanceTimersByTimeAsync(0);
237
-
238
- expect(received).toHaveLength(1);
239
- expect(received[0].title).toBe("kept-title");
240
- expect("options" in received[0]).toBe(false);
241
- expect("timeout" in received[0]).toBe(false);
242
- expect(received[0].message).toBe("kept");
243
- });
244
-
245
- it("typed 非法 title(number)在上游 startsWith 处炸链路 → handler 不被调、单请求失败被吞不阻塞队列", async () => {
246
- // HEAD 真实行为锚定(探针实证):title: 42 在 extractMethodFields 之前的环节
247
- // 触发 str.startsWith is not a function,异常被 handleUiRequest 的 .catch 吞掉
248
- //(注释声明的「单个请求失败不阻塞后续」语义)——守卫的「跳过该字段」对 title 不可达。
249
- const received: UiRequest[] = [];
250
- const { enqueue } = makeQueue((req) => {
251
- received.push(req);
252
- return Promise.resolve<UiResponse>({ ack: true });
253
- });
254
-
255
- const badReq = {
256
- method: "select",
257
- title: 42,
258
- message: "kept",
259
- } as unknown as Parameters<ReturnType<typeof createUiRequestQueue>>[1];
260
- enqueue("r1", badReq);
261
- await vi.advanceTimersByTimeAsync(0);
262
- expect(received).toHaveLength(0); // handler 未被调(链路在上游失败)
263
-
264
- // 队列不阻塞:后续合法请求照常处理
265
- enqueue("r2", {
266
- method: "select",
267
- title: "Q2",
268
- options: [JSON.stringify({ question: "Q2", options: [{ label: "A" }] })],
269
- });
270
- await vi.advanceTimersByTimeAsync(0);
271
- expect(received).toHaveLength(1);
272
- expect(received[0].title).toBe("Q2");
273
- });
274
-
275
- it("presence-only 字段仅 in 检查直赋;未声明的字段不复制(保持 UiRequest 可选)", async () => {
276
- const received: UiRequest[] = [];
277
- const { enqueue } = makeQueue((req) => {
278
- received.push(req);
279
- return Promise.resolve<UiResponse>({ ack: true });
280
- });
281
-
282
- // statusText 存在但值为 undefined(statusText: string | undefined 变体合法形态)→ 仍复制
283
- const req = {
284
- method: "setStatus",
285
- statusKey: "k",
286
- statusText: undefined,
287
- unknownField: "should-not-copy",
288
- } as unknown as Parameters<ReturnType<typeof createUiRequestQueue>>[1];
289
-
290
- enqueue("r1", req);
291
- await vi.advanceTimersByTimeAsync(0);
292
-
293
- expect(received).toHaveLength(1);
294
- expect("statusText" in received[0]).toBe(true);
295
- expect(received[0].statusText).toBeUndefined();
296
- expect("unknownField" in received[0]).toBe(false);
297
- });
298
- });
@@ -1,230 +0,0 @@
1
- // src/execution/__tests__/worktree-pid-registration.integration.test.ts
2
- //
3
- // [worktree-reaper-fix] 端到端集成测试:验证 worktree pid 注册链路(接线层)。
4
- //
5
- // 背景:2026-08-11 生产事故——reaper 误清活 worktree。根因:pid 补全代码唯一生产调用点
6
- // 挂在 session-runner 的 header 分支(RPC mode 永不触发),注册表 pid 恒为 0,超
7
- // SPAWN_GRACE_MS(60s) 后任意 session_start 触发的 scan() 必然误删活 worktree。
8
- // 修复:spawn() 返回后同步补 pid。
9
- //
10
- // 为什么用真实 spawn(而非现有 run-spawn-* 的 FakeChild mock):
11
- // 现有测试全 mock registerPid(session-start-reaper/crash-recovery/index-session-start/
12
- // stream-sink-guard),验证的是「mock 了补全回调后的 reaper 行为」,从未验证
13
- // 「真实调用链中补全回调是否被调用」——接线错误零检测能力(结构性盲区)。
14
- // 本测试走真实链路:真实 git repo + 真实 worktree 创建 + 真实 spawn node 子进程 +
15
- // 真实注册表文件,仅 mock ./pi-invocation.ts(把 pi 二进制替换为 node -e 脚本)。
16
- //
17
- // mock 最小化原则:
18
- // - node:child_process 不 mock(真实 spawn / spawnSync git)
19
- // - node:fs 不 mock(真实目录/文件:worktree checkout、注册表 JSON)
20
- // - alive-store 不 mock(真实 process.kill(pid, 0) 探活)
21
- // - 仅 vi.mock("./pi-invocation.ts"):getPiInvocation 返回 node -e 脚本
22
- // - fake timers 仅 toFake: ["Date"]:推进注册表宽限判定用,不干扰真实 I/O 事件
23
-
24
- import { spawnSync } from "node:child_process";
25
- import * as fs from "node:fs";
26
- import * as os from "node:os";
27
- import * as path from "node:path";
28
-
29
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
30
-
31
- // vi.hoisted:vi.mock 工厂体内不能引用顶层 let/const(提升限制),脚本字符串必须放这。
32
- // scriptHolder 是可变对象:getPiInvocation 每次调用时读它(工厂函数体在运行时执行),
33
- // 用例内可切换长驻/短命脚本。
34
- const { scriptHolder, LONG_RUNNING_SCRIPT, SHORT_LIVED_SCRIPT } = vi.hoisted(() => {
35
- const scriptHolder: { script: string } = { script: "process.exit(0)" };
36
- return {
37
- scriptHolder,
38
- // 长驻脚本:90s 后退出(测试在 61s scan 时它必须还活着,验证「活 worktree 不被清」)
39
- LONG_RUNNING_SCRIPT:
40
- "setTimeout(() => process.exit(0), 90000);",
41
- // 短命脚本:立即退出(模拟快速完成的子 agent,验证「真孤儿被回收」)
42
- SHORT_LIVED_SCRIPT: "process.exit(0)",
43
- };
44
- });
45
-
46
- vi.mock("./pi-invocation.ts", () => ({
47
- getPiInvocation: (userArgs: string[]) => ({
48
- command: process.execPath,
49
- args: ["-e", scriptHolder.script, ...userArgs],
50
- }),
51
- }));
52
-
53
- import { WorktreeManager } from "../worktree-manager.ts";
54
- import { WorktreeRegistry, SPAWN_GRACE_MS } from "../worktree-registry.ts";
55
- import { runSpawn } from "../engine/engines/pi/session-runner.ts";
56
- import type { WorktreeHandle } from "../types.ts";
57
- import { makeCtx, makeOpts, makeRecord } from "./helpers/spawn-mock.ts";
58
-
59
- // ── 测试夹具:临时 git repo + 临时 agentDir(避免污染 ~/.pi/agent)──
60
-
61
- let tmpRoot: string;
62
- let repoDir: string;
63
- let agentDir: string;
64
- let wtm: WorktreeManager;
65
- let registry: WorktreeRegistry;
66
- let handle: WorktreeHandle | undefined;
67
- let spawnedPid: number | undefined;
68
-
69
- function git(args: string[], cwd: string): string {
70
- const r = spawnSync("git", args, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
71
- return (r.stdout ?? "").trim();
72
- }
73
-
74
- /** 初始化临时 git repo(至少一个 commit,worktreeManager.create 需要 clean tree + HEAD)。 */
75
- function initRepo(): void {
76
- repoDir = path.join(tmpRoot, "repo");
77
- fs.mkdirSync(repoDir, { recursive: true });
78
- git(["init", "-b", "main"], repoDir);
79
- git(["config", "user.email", "test@test.local"], repoDir);
80
- git(["config", "user.name", "test"], repoDir);
81
- git(["commit", "--allow-empty", "-m", "init"], repoDir);
82
- }
83
-
84
- /** 从注册表文件读指定 branch 的条目(真实文件,轮询用)。 */
85
- function readEntry(branch: string): { pid: number; createdAt: number } | undefined {
86
- return registry.load().find((e) => e.branch === branch);
87
- }
88
-
89
- /** 轮询注册表直到 pid 补全(真实 fs 读 + 真实 setTimeout 轮询)。 */
90
- async function waitForPid(branch: string, timeoutMs = 5000): Promise<number> {
91
- const start = Date.now();
92
- while (Date.now() - start < timeoutMs) {
93
- const entry = readEntry(branch);
94
- if (entry && entry.pid !== 0) return entry.pid;
95
- await new Promise((r) => setTimeout(r, 20));
96
- }
97
- throw new Error(`pid not registered within ${timeoutMs}ms (branch=${branch})`);
98
- }
99
-
100
- /** 清理:kill 子进程 + worktree cleanup + 删除临时目录。 */
101
- async function cleanup(): Promise<void> {
102
- if (spawnedPid) {
103
- try {
104
- process.kill(spawnedPid, "SIGKILL");
105
- } catch {
106
- // 已退出
107
- }
108
- spawnedPid = undefined;
109
- }
110
- if (handle) {
111
- try {
112
- await wtm.cleanup(handle);
113
- } catch (err) {
114
- // best-effort:git worktree remove 失败不阻断测试清理
115
- // eslint-disable-next-line no-console -- 测试 teardown 兜底留痕(清理失败不阻断后续 rmSync),非产品日志面
116
- console.warn("worktree cleanup failed in test teardown", err);
117
- }
118
- handle = undefined;
119
- }
120
- try {
121
- fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
122
- } catch {
123
- // best-effort
124
- }
125
- }
126
-
127
- beforeEach(() => {
128
- tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "wt-reaper-it-"));
129
- agentDir = path.join(tmpRoot, "agent");
130
- initRepo();
131
- wtm = new WorktreeManager(agentDir);
132
- registry = new WorktreeRegistry(agentDir);
133
- });
134
-
135
- afterEach(async () => {
136
- vi.useRealTimers();
137
- await cleanup();
138
- });
139
-
140
- // ── 用例 ──
141
-
142
- // [HISTORICAL] 2026-08-20 PR #185:真实 spawn node 子进程 + 真实 git worktree + 注册表
143
- // 文件锁集成用例显式超时——整包满并行 + 系统余载下真实子进程链路超 vitest 默认 5s
144
- // testTimeout(对齐本包 worktree-registry D5a / process-shutdown-hook 口径)。
145
- describe("worktree pid 注册链路(真实 spawn 集成)", { timeout: 30_000 }, () => {
146
- it("正向:spawn 返回后注册表 pid 同步补全,活 worktree 超宽限不被 scan 误清", async () => {
147
- // 0. 长驻脚本(子进程 90s 内不退出,模拟长跑子 agent)
148
- scriptHolder.script = LONG_RUNNING_SCRIPT; // 1. 真实创建 worktree(pid=0 占位)
149
- handle = await wtm.create(repoDir, "rec-1");
150
- expect(readEntry(handle.branch)).toMatchObject({ pid: 0 });
151
-
152
- // 2. runSpawn 挂后台(不 await——长驻子进程 close 不触发,await 会挂死),
153
- // ctx.onWorktreePid 接真实 registerPid(模拟 subagent-service 接线)
154
- const ctx = makeCtx({
155
- agentDir,
156
- cwd: repoDir,
157
- mainCwd: repoDir,
158
- rootCwd: repoDir,
159
- onWorktreePid: (branch: string, pid: number) => wtm.registerPid(branch, pid),
160
- });
161
- const runPromise = runSpawn(
162
- makeRecord(),
163
- "test task",
164
- makeOpts({ worktree: handle }),
165
- ctx,
166
- );
167
-
168
- // 3. 断言 spawn 后 pid 已补全(真实注册表文件轮询)——修复前此步超时红
169
- spawnedPid = await waitForPid(handle.branch);
170
- expect(spawnedPid).toBeGreaterThan(0);
171
-
172
- // 4. 推进时钟超 SPAWN_GRACE_MS(仅 fake Date,不干扰真实 I/O)
173
- vi.useFakeTimers({ toFake: ["Date"] });
174
- vi.setSystemTime(Date.now() + SPAWN_GRACE_MS + 1000);
175
-
176
- // 5. scan():活 worktree 必须不被清(修复前:pid=0 超宽限 → 误删 → 红)
177
- await wtm.scan();
178
- expect(fs.existsSync(handle.path)).toBe(true);
179
-
180
- // 6. 收尾:真实时钟恢复 + kill 子进程让 runPromise settle
181
- vi.useRealTimers();
182
- try {
183
- process.kill(spawnedPid, "SIGTERM");
184
- } catch {
185
- // 已退出
186
- }
187
- await runPromise;
188
-
189
- // 7. 反向:进程死后 scan 回收真孤儿
190
- await wtm.scan();
191
- expect(fs.existsSync(handle.path)).toBe(false);
192
- const entryAfter = readEntry(handle.branch);
193
- expect(entryAfter).toBeUndefined();
194
- }, 15000);
195
-
196
- it("反向:短命子进程退出后,pid>0 且进程死 → scan 立即回收", async () => {
197
- // 0. 短命脚本(子进程立即退出,模拟快速完成的子 agent)
198
- scriptHolder.script = SHORT_LIVED_SCRIPT;
199
- // 1. 真实创建 worktree
200
- handle = await wtm.create(repoDir, "rec-2");
201
-
202
- // 2. 短命脚本子进程:spawn 后同步补 pid(修复前 pid=0,且未超宽限 → 不回收 → 红)
203
- const ctx = makeCtx({
204
- agentDir,
205
- cwd: repoDir,
206
- mainCwd: repoDir,
207
- rootCwd: repoDir,
208
- onWorktreePid: (branch: string, pid: number) => wtm.registerPid(branch, pid),
209
- });
210
- const result = await runSpawn(
211
- makeRecord(),
212
- "test task",
213
- makeOpts({ worktree: handle }),
214
- ctx,
215
- );
216
- // 历史「status !== error」弱断言:AgentResult 无 status 字段,经视图访问保持断言语义不变
217
- expect((result as { status?: string }).status).not.toBe("error"); // 进程正常退出(exit 0),非 spawn 失败
218
-
219
- // 3. pid 已补全(短命进程退出后 pid 仍有效,registerPid 同步执行不受退出影响)
220
- const entry = readEntry(handle.branch);
221
- expect(entry).toBeDefined();
222
- expect(entry!.pid).toBeGreaterThan(0);
223
- spawnedPid = entry!.pid;
224
-
225
- // 4. scan:pid>0 且进程死 → 立即判孤儿回收(无需等宽限)
226
- await wtm.scan();
227
- expect(fs.existsSync(handle.path)).toBe(false);
228
- expect(readEntry(handle.branch)).toBeUndefined();
229
- }, 15000);
230
- });
@@ -1,128 +0,0 @@
1
- // schema-emulation.test.ts —— 三级容错提取逐级用例 + ajv 校验 + tail 载体。
2
- //
3
- // 三视角:①构建者——三级容错每级有独立触达用例(直接 parse / fence / 括号扫描);
4
- // ②使用者——ok:false 时 error 简述 + tail 足以构造重试 prompt;③观察者——prompt
5
- // 注入段含 schema 声明与输出约定(emulated 引擎拼进 prompt 后模型可遵循)。
6
-
7
- import { describe, expect, it } from "vitest";
8
-
9
- import {
10
- buildSchemaEmulationSegment,
11
- extractAndValidateStructuredOutput,
12
- SCHEMA_EMULATION_TAIL_CHARS,
13
- } from "../../common/schema-emulation.ts";
14
-
15
- const objectSchema = {
16
- type: "object",
17
- properties: { verdict: { type: "string" }, issues: { type: "array", items: { type: "string" } } },
18
- required: ["verdict"],
19
- } as const;
20
-
21
- describe("buildSchemaEmulationSegment", () => {
22
- it("注入段含 schema JSON 声明 + 输出格式约定(用户可见 prompt 断言)", () => {
23
- const segment = buildSchemaEmulationSegment(objectSchema);
24
- expect(segment).toContain("## Structured Output Requirement");
25
- expect(segment).toContain(JSON.stringify(objectSchema));
26
- expect(segment).toMatch(/```json/);
27
- expect(segment).toMatch(/ONLY the JSON value/);
28
- expect(segment).toMatch(/JSON Schema \(draft-07\)/);
29
- });
30
- });
31
-
32
- describe("extractAndValidateStructuredOutput 三级容错", () => {
33
- it("第 1 级:输出整体即合法 JSON → 直接 parse 通过 + ajv 通过", () => {
34
- const text = JSON.stringify({ verdict: "pass", issues: [] });
35
- const result = extractAndValidateStructuredOutput(text, objectSchema);
36
- expect(result).toEqual({ ok: true, parsed: { verdict: "pass", issues: [] } });
37
- });
38
-
39
- it("第 2 级:markdown code fence 包裹 → 剥 fence 后 parse 通过", () => {
40
- const text = "Here is my answer:\n```json\n{\"verdict\": \"fail\", \"issues\": [\"a\", \"b\"]}\n```\n";
41
- const result = extractAndValidateStructuredOutput(text, objectSchema);
42
- expect(result).toEqual({ ok: true, parsed: { verdict: "fail", issues: ["a", "b"] } });
43
- });
44
-
45
- it("第 2 级:无语言标注的裸 fence 也能剥", () => {
46
- const text = "```\n{\"verdict\": \"pass\"}\n```";
47
- const result = extractAndValidateStructuredOutput(text, objectSchema);
48
- expect(result).toEqual({ ok: true, parsed: { verdict: "pass" } });
49
- });
50
-
51
- it("第 3 级:前后杂文本包裹 → 首尾括号扫描提取", () => {
52
- const text = "Sure! The review result is:\n\n{\"verdict\": \"fail\", \"issues\": [\"x\"]}\n\nHope this helps.";
53
- const result = extractAndValidateStructuredOutput(text, objectSchema);
54
- expect(result).toEqual({ ok: true, parsed: { verdict: "fail", issues: ["x"] } });
55
- });
56
-
57
- it("第 3 级:array 根也支持(首个 '[' 到末个 ']')", () => {
58
- const arraySchema = { type: "array", items: { type: "string" } };
59
- const result = extractAndValidateStructuredOutput("prefix [\"a\",\"b\"] suffix", arraySchema);
60
- expect(result).toEqual({ ok: true, parsed: ["a", "b"] });
61
- });
62
-
63
- it("提取成功但 ajv 校验失败 → ok:false 含 ajv 错误明细与原始尾部", () => {
64
- // verdict 缺失(required)+ issues 类型错
65
- const text = '{"issues": "not-an-array"}';
66
- const result = extractAndValidateStructuredOutput(text, objectSchema);
67
- expect(result.ok).toBe(false);
68
- if (!result.ok) {
69
- expect(result.error).toContain("Schema validation failed");
70
- expect(result.error).toContain("verdict");
71
- expect(result.tail).toBe(text);
72
- }
73
- });
74
-
75
- it("三级提取全失败(无任何 JSON)→ ok:false 且 error 指明三级路径", () => {
76
- const result = extractAndValidateStructuredOutput("no json here at all", objectSchema);
77
- expect(result.ok).toBe(false);
78
- if (!result.ok) {
79
- expect(result.error).toContain("3-stage");
80
- expect(result.error).toContain("code-fence");
81
- expect(result.error).toContain("bracket scan");
82
- }
83
- });
84
-
85
- it("提取成功但 JSON 语法损坏(括号扫描后仍 parse 失败)→ ok:false", () => {
86
- const result = extractAndValidateStructuredOutput("result: {verdict: pass}", objectSchema);
87
- expect(result.ok).toBe(false);
88
- });
89
-
90
- it("ajv 编译失败(非法 schema)→ ok:false 指明 host 侧编译失败", () => {
91
- // type: "foo" 是 ajv 编译期拒绝的非法类型值
92
- const result = extractAndValidateStructuredOutput('{"a":1}', { type: "foo" });
93
- expect(result.ok).toBe(false);
94
- if (!result.ok) {
95
- expect(result.error).toContain("compilation failed");
96
- }
97
- });
98
- });
99
-
100
- describe("tail 载体(错误展示)", () => {
101
- it(`tail 截断到原始输出尾部 ${SCHEMA_EMULATION_TAIL_CHARS} 字`, () => {
102
- const head = "H".repeat(300);
103
- const tailPart = "T".repeat(400);
104
- const text = `${head}${tailPart}`; // 700 字,尾部 500 = 末段(100H + 400T)
105
- const result = extractAndValidateStructuredOutput(text, objectSchema);
106
- expect(result.ok).toBe(false);
107
- if (!result.ok) {
108
- expect(result.tail.length).toBe(SCHEMA_EMULATION_TAIL_CHARS);
109
- expect(result.tail).toBe("H".repeat(100) + "T".repeat(400));
110
- }
111
- });
112
-
113
- it("短输出 tail 为原文(不截断)", () => {
114
- const text = "short broken output";
115
- const result = extractAndValidateStructuredOutput(text, objectSchema);
116
- expect(result.ok).toBe(false);
117
- if (!result.ok) {
118
- expect(result.tail).toBe(text);
119
- }
120
- });
121
- });
122
-
123
- describe("D4 硬分流守护(静态断言)", () => {
124
- it("本模块的 ajv 只在 emulated 路径——native 引擎(pi-env 链路)不 import 本模块(由全仓 grep 守护,见任务报告)", () => {
125
- // 占位用例:硬分流是 import 约束,运行时无对应断言点;保留用例记录守护方式
126
- expect(typeof extractAndValidateStructuredOutput).toBe("function");
127
- });
128
- });