@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
@@ -0,0 +1,558 @@
1
+ // engine-discovery-scan.test.ts —— [W4] 引擎发现器(impl-plan §2.4 / A4/A12):
2
+ // manifest schema 字段级正反例、三级搜索路径、L1 分隔符与非法条目、engines.json 投影
3
+ // 合流与幂等零写、同 id 覆盖留痕、bin 不可执行标记、ensureEngineDiscovered 补扫。
4
+ //
5
+ // 纪律:fixture 一律 mkdtempSync(tmpdir()) 自建自删(禁真实数据目录);L2 根在用例内
6
+ // 显式传 nodeModuleRoots(屏蔽宿主 node_modules 的真实引擎包污染);env 用注入对象
7
+ // (不动 process.env——vitest.setup 净化面之外零残留)。
8
+
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+
13
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
14
+
15
+ import type { SubagentEnginesFile } from "@xyz-agent/extension-protocol";
16
+
17
+ import { configureCore, resetCoreForTests } from "../../../core/host-services.ts";
18
+ import type { DiscoveryRoot } from "../../../core/host-services.ts";
19
+ import { clearEngines, getEngine, hasEngine, listEngines, registerEngine } from "../registry.ts";
20
+ import type { EnginePort } from "../port.ts";
21
+ import { getEnginesFilePath, syncEnginesFile } from "../engine-discovery.ts";
22
+ import {
23
+ ENGINE_ROOTS_ENV,
24
+ deriveNodeModuleRoots,
25
+ discoverAndRegisterEngines,
26
+ ensureEngineDiscovered,
27
+ parseEngineRootsEnv,
28
+ scanEngines,
29
+ } from "../engine-discovery-scan.ts";
30
+
31
+ // ── fixture helpers ─────────────────────────────────────────────
32
+
33
+ /** 可执行 bin 脚本内容(POSIX shebang;发现器只做 accessSync(X_OK),不执行)。 */
34
+ const BIN_CONTENT = "#!/bin/sh\nexit 0\n";
35
+
36
+ interface PkgOptions {
37
+ /** bin 文件权限;缺省 0o755(可执行)。0o644 用于「不可执行」反例。 */
38
+ binMode?: number;
39
+ /** bin 文件不落盘(bin 缺失反例)。 */
40
+ omitBin?: boolean;
41
+ }
42
+
43
+ /** 在 rootDir 下造一个带 manifest 的引擎包,返回包目录。 */
44
+ function makeEnginePkg(
45
+ rootDir: string,
46
+ pkgDirName: string,
47
+ manifest: Record<string, unknown>,
48
+ opts: PkgOptions = {},
49
+ ): string {
50
+ const pkgDir = path.join(rootDir, pkgDirName);
51
+ fs.mkdirSync(pkgDir, { recursive: true });
52
+ const binRel = "./bin/cli.mjs";
53
+ fs.writeFileSync(
54
+ path.join(pkgDir, "package.json"),
55
+ JSON.stringify({
56
+ name: `test-${pkgDirName}`,
57
+ bin: { "test-engine-cli": binRel },
58
+ "xyz-agent": { subagentEngine: { id: pkgDirName, bin: "test-engine-cli", protocol: 1, ...manifest } },
59
+ }),
60
+ );
61
+ if (!opts.omitBin) {
62
+ const binPath = path.join(pkgDir, binRel);
63
+ fs.mkdirSync(path.dirname(binPath), { recursive: true });
64
+ fs.writeFileSync(binPath, BIN_CONTENT);
65
+ fs.chmodSync(binPath, opts.binMode ?? 0o755);
66
+ }
67
+ return pkgDir;
68
+ }
69
+
70
+ /** 收集宿主日志的 HostServices 注入(warn/debug 留痕断言用)。 */
71
+ function makeLogCollector(): { logs: Array<{ level: string; message: string }>; host: Parameters<typeof configureCore>[0] } {
72
+ const logs: Array<{ level: string; message: string }> = [];
73
+ const host = {
74
+ dataRoot: () => "/test-data-root",
75
+ log: (level: string, _component: string, message: string) => {
76
+ logs.push({ level, message });
77
+ },
78
+ };
79
+ return { logs, host: host as unknown as Parameters<typeof configureCore>[0] };
80
+ }
81
+
82
+ /** 扫描选项基线:屏蔽 L2(宿主 node_modules 污染)、env 注入(roots 走 L1 env 根)、
83
+ * 数据根走 env。 */
84
+ function scanOpts(overrides: {
85
+ env?: Record<string, string | undefined>;
86
+ /** L1 发现根(经 ENGINE_ROOTS_ENV 注入;与 overrides.env 拼接)。 */
87
+ roots?: string[];
88
+ nodeModuleRoots?: string[];
89
+ extraRoots?: DiscoveryRoot[];
90
+ agentDir?: string;
91
+ }) {
92
+ const env: Record<string, string | undefined> = {
93
+ XYZ_AGENT_DATA_DIR: "/test-data-dir",
94
+ ...overrides.env,
95
+ };
96
+ if (overrides.roots !== undefined) {
97
+ env[ENGINE_ROOTS_ENV] = overrides.roots.join(path.delimiter);
98
+ }
99
+ return {
100
+ hostKind: "test",
101
+ env,
102
+ nodeModuleRoots: overrides.nodeModuleRoots ?? [],
103
+ ...(overrides.extraRoots !== undefined ? { extraRoots: overrides.extraRoots } : {}),
104
+ ...(overrides.agentDir !== undefined ? { agentDir: overrides.agentDir } : {}),
105
+ };
106
+ }
107
+
108
+ function stubEngine(id: string): EnginePort {
109
+ return {
110
+ id,
111
+ capabilities: () => ({
112
+ schemaEnforcement: "emulated",
113
+ steer: "unsupported",
114
+ conversation: "unsupported",
115
+ personaInjection: "prompt",
116
+ eventGranularity: "coarse",
117
+ sandbox: "none",
118
+ sessionRead: "outcome-only",
119
+ resume: "unsupported",
120
+ interrupt: "kill-only",
121
+ permissionMode: "ignored",
122
+ maxTurns: false,
123
+ }),
124
+ probe: async () => ({ ok: true, engineVersion: "test", checks: [] }),
125
+ run: async () => {
126
+ throw new Error("unused");
127
+ },
128
+ interact: async () => {
129
+ throw new Error("unused");
130
+ },
131
+ read: async () => ({ engineId: id, turns: [], source: "outcome-only" }),
132
+ };
133
+ }
134
+
135
+ const FULL_CAPABILITIES = {
136
+ schemaEnforcement: "native",
137
+ steer: "native",
138
+ conversation: "native",
139
+ personaInjection: "file",
140
+ eventGranularity: "stream",
141
+ sandbox: "native",
142
+ sessionRead: "full",
143
+ resume: "native",
144
+ interrupt: "native",
145
+ permissionMode: "native",
146
+ maxTurns: true,
147
+ };
148
+
149
+ let tmpRoot: string;
150
+ let collectedLogs: Array<{ level: string; message: string }>;
151
+
152
+ beforeEach(() => {
153
+ tmpRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "engine-scan-"));
154
+ const collector = makeLogCollector();
155
+ collectedLogs = collector.logs;
156
+ configureCore(collector.host);
157
+ });
158
+
159
+ afterEach(() => {
160
+ resetCoreForTests();
161
+ clearEngines();
162
+ fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
163
+ });
164
+
165
+ // ── manifest schema 字段级(§2.4)────────────────────────────────
166
+
167
+ describe("manifest schema 字段级解析", () => {
168
+ it("正例:完整 manifest → discovered,capabilities/modelCatalog/displayName 原样入快照", () => {
169
+ makeEnginePkg(tmpRoot, "foo", {
170
+ displayName: "Foo Engine",
171
+ description: "test engine",
172
+ envPrefixes: ["FOO_"],
173
+ modelCatalog: { dynamic: false, models: [{ id: "m1", aliases: ["one"], canonicalRef: "prov/m1" }] },
174
+ capabilities: FULL_CAPABILITIES,
175
+ });
176
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
177
+ expect(result.discovered).toHaveLength(1);
178
+ const entry = result.discovered[0];
179
+ expect(entry.id).toBe("foo");
180
+ expect(entry.descriptor.kind).toBe("cli");
181
+ expect(entry.descriptor.capabilities).toEqual(FULL_CAPABILITIES);
182
+ expect(entry.descriptor.manifest).toEqual({
183
+ displayName: "Foo Engine",
184
+ modelCatalog: { dynamic: false, models: [{ id: "m1", aliases: ["one"], canonicalRef: "prov/m1" }] },
185
+ });
186
+ });
187
+
188
+ it("bin 解析:manifest bin = package.json bin map 的 key → 入口绝对路径", () => {
189
+ makeEnginePkg(tmpRoot, "binmap", { capabilities: FULL_CAPABILITIES });
190
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
191
+ expect(result.discovered[0].descriptor.command).toBe(
192
+ path.join(tmpRoot, "binmap", "bin/cli.mjs"),
193
+ );
194
+ });
195
+
196
+ it("必需字段缺失 → skip:id / bin / protocol 各自缺失均不装载", () => {
197
+ for (const key of ["id", "bin", "protocol"]) {
198
+ const manifest: Record<string, unknown> = { capabilities: FULL_CAPABILITIES };
199
+ if (key === "protocol") manifest["protocol"] = undefined;
200
+ else if (key === "bin") manifest["bin"] = undefined;
201
+ else manifest["id"] = undefined;
202
+ makeEnginePkg(tmpRoot, `missing-${key}`, manifest);
203
+ }
204
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
205
+ expect(result.discovered).toHaveLength(0);
206
+ expect(result.skipped).toHaveLength(3);
207
+ for (const key of ["id", "bin", "protocol"]) {
208
+ expect(result.skipped.some((s) => s.reason.includes(key))).toBe(true);
209
+ }
210
+ expect(collectedLogs.filter((l) => l.level === "warn").length).toBeGreaterThanOrEqual(3);
211
+ });
212
+
213
+ it("protocol 不兼容(2 越界)→ unusable 不装载;protocol 1 兼容", () => {
214
+ makeEnginePkg(tmpRoot, "future", { protocol: 2, capabilities: FULL_CAPABILITIES });
215
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
216
+ expect(result.discovered).toHaveLength(0);
217
+ expect(result.unusable).toHaveLength(1);
218
+ expect(result.unusable[0].id).toBe("future");
219
+ expect(result.unusable[0].reason).toContain("protocol");
220
+ });
221
+
222
+ it("capabilities 缺键取最保守值 + warn;未知键忽略 + warn", () => {
223
+ makeEnginePkg(tmpRoot, "partial-caps", {
224
+ capabilities: { schemaEnforcement: "native", unknownKey: "x" },
225
+ });
226
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
227
+ expect(result.discovered).toHaveLength(1);
228
+ expect(result.discovered[0].descriptor.capabilities).toEqual({
229
+ schemaEnforcement: "native",
230
+ steer: "unsupported",
231
+ conversation: "unsupported",
232
+ personaInjection: "prompt",
233
+ eventGranularity: "coarse",
234
+ sandbox: "none",
235
+ sessionRead: "outcome-only",
236
+ resume: "unsupported",
237
+ interrupt: "kill-only",
238
+ permissionMode: "ignored",
239
+ maxTurns: false,
240
+ });
241
+ const warns = collectedLogs.filter((l) => l.level === "warn").map((l) => l.message);
242
+ expect(warns.some((m) => m.includes("capabilities.steer missing"))).toBe(true);
243
+ expect(warns.some((m) => m.includes("unknown capabilities key(s) ignored: unknownKey"))).toBe(true);
244
+ // 未知键不进 capabilities(词表封闭)
245
+ expect(Object.keys(result.discovered[0].descriptor.capabilities)).toHaveLength(11);
246
+ });
247
+
248
+ it("capabilities 整段缺失 → 全保守 + warn(包仍装载)", () => {
249
+ makeEnginePkg(tmpRoot, "no-caps", {});
250
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
251
+ expect(result.discovered).toHaveLength(1);
252
+ expect(result.discovered[0].descriptor.capabilities.maxTurns).toBe(false);
253
+ expect(result.discovered[0].descriptor.capabilities.steer).toBe("unsupported");
254
+ expect(collectedLogs.some((l) => l.message.includes("most conservative values"))).toBe(true);
255
+ });
256
+
257
+ it("envPrefixes 缺省 [];空串/含 */非法形态/保留前缀 → 丢弃该前缀 + warn,包仍可用", () => {
258
+ makeEnginePkg(tmpRoot, "bad-prefixes", {
259
+ envPrefixes: ["", "HAS*STAR", "BAD-DASH", "XYZ_HOST", "xyz_agent_", "GOOD_", "good_"],
260
+ capabilities: FULL_CAPABILITIES,
261
+ });
262
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
263
+ // 包仍可用(A12②):装载成功
264
+ expect(result.discovered).toHaveLength(1);
265
+ expect(result.skipped).toHaveLength(0);
266
+ const warns = collectedLogs.filter((l) => l.level === "warn").map((l) => l.message);
267
+ // 非法形态逐条拒绝(空串 / 含 * / 非法字符)
268
+ expect(warns.filter((m) => m.includes("dropping the prefix")).length).toBeGreaterThanOrEqual(5);
269
+ // 保留前缀大小写不敏感(XYZ_HOST / xyz_agent_ 均拒)
270
+ expect(warns.some((m) => m.includes("reserved host prefix"))).toBe(true);
271
+ });
272
+
273
+ it("modelCatalog 三态:缺省不注入(undefined);null 等价省略;显式数组原样;dynamic 缺省 true", () => {
274
+ makeEnginePkg(tmpRoot, "mc-omitted", { capabilities: FULL_CAPABILITIES });
275
+ makeEnginePkg(tmpRoot, "mc-null", { modelCatalog: null, capabilities: FULL_CAPABILITIES });
276
+ makeEnginePkg(tmpRoot, "mc-explicit", {
277
+ modelCatalog: { models: [{ id: "m1" }] },
278
+ capabilities: FULL_CAPABILITIES,
279
+ });
280
+ makeEnginePkg(tmpRoot, "mc-no-models", { modelCatalog: { dynamic: false }, capabilities: FULL_CAPABILITIES });
281
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
282
+ const manifestOf = (id: string) => result.discovered.find((d) => d.id === id)!.descriptor.manifest!;
283
+ // 缺省 = 不注入(解析器不得把省略填成 models: []——「无枚举面」语义必须可达)
284
+ expect("modelCatalog" in manifestOf("mc-omitted")).toBe(false);
285
+ expect(manifestOf("mc-null").modelCatalog).toBeNull();
286
+ expect(manifestOf("mc-explicit").modelCatalog).toEqual({ dynamic: true, models: [{ id: "m1" }] });
287
+ // models 缺失 = 无有效枚举面 → null(不是 []——[] 是作者显式「有面但空」声明)
288
+ expect(manifestOf("mc-no-models").modelCatalog).toBeNull();
289
+ });
290
+
291
+ it("displayName 可选(缺省不入快照,消费面回落 id);description 仅形态校验", () => {
292
+ makeEnginePkg(tmpRoot, "plain", { capabilities: FULL_CAPABILITIES });
293
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
294
+ expect("displayName" in result.discovered[0].descriptor.manifest!).toBe(false);
295
+ });
296
+ });
297
+
298
+ // ── 三级搜索路径 ─────────────────────────────────────────────────
299
+
300
+ describe("三级搜索路径", () => {
301
+ it("L1 env 根:多根 + org 分组二层布局都命中", () => {
302
+ makeEnginePkg(path.join(tmpRoot, "r1"), "from-env-root", { capabilities: FULL_CAPABILITIES });
303
+ // org 分组:<root>/@scope/<pkg>/package.json
304
+ makeEnginePkg(path.join(tmpRoot, "r2", "@scope"), "scoped-engine", { capabilities: FULL_CAPABILITIES });
305
+ const result = scanEngines(
306
+ scanOpts({ env: { [ENGINE_ROOTS_ENV]: `${path.join(tmpRoot, "r1")}${path.delimiter}${path.join(tmpRoot, "r2")}` } }),
307
+ );
308
+ expect(result.discovered.map((d) => d.id).sort()).toEqual(["from-env-root", "scoped-engine"]);
309
+ });
310
+
311
+ it("L1 宿主根第二通道:HostServices.discoveryRoots().engines 合并扫描", () => {
312
+ makeEnginePkg(path.join(tmpRoot, "hostroot"), "from-host-root", { capabilities: FULL_CAPABILITIES });
313
+ resetCoreForTests();
314
+ const collector = makeLogCollector();
315
+ collectedLogs = collector.logs;
316
+ configureCore({
317
+ ...collector.host,
318
+ discoveryRoots: () => ({ engines: [{ dir: path.join(tmpRoot, "hostroot"), source: "host-npm" }] }),
319
+ } as Parameters<typeof configureCore>[0]);
320
+ const result = scanEngines(scanOpts({}));
321
+ expect(result.discovered.map((d) => d.id)).toEqual(["from-host-root"]);
322
+ expect(result.discovered[0].source).toBe("host-npm");
323
+ });
324
+
325
+ it("L2 node 解析根:nodeModuleRoots 注入命中;deriveNodeModuleRoots 沿上溯链收 node_modules", () => {
326
+ makeEnginePkg(path.join(tmpRoot, "nm"), "from-node-modules", { capabilities: FULL_CAPABILITIES });
327
+ const result = scanEngines(scanOpts({ nodeModuleRoots: [path.join(tmpRoot, "nm")] }));
328
+ expect(result.discovered.map((d) => d.id)).toEqual(["from-node-modules"]);
329
+
330
+ // deriveNodeModuleRoots:entry 位于 <tmp>/a/b/entry.js,<tmp>/node_modules 存在 → 命中
331
+ fs.mkdirSync(path.join(tmpRoot, "a", "b"), { recursive: true });
332
+ fs.mkdirSync(path.join(tmpRoot, "node_modules"));
333
+ fs.writeFileSync(path.join(tmpRoot, "a", "b", "entry.js"), "// entry");
334
+ const derived = deriveNodeModuleRoots(path.join(tmpRoot, "a", "b", "entry.js"));
335
+ expect(derived).toContain(path.join(tmpRoot, "node_modules"));
336
+ // 由内向外:更深的 node_modules 在前
337
+ expect(derived.indexOf(path.join(tmpRoot, "node_modules"))).toBeGreaterThan(
338
+ derived.indexOf(path.join(tmpRoot, "a", "node_modules")),
339
+ );
340
+ });
341
+
342
+ it("L3 显式配置:enabled:false 不装载(A12⑤);command 缺失跳过;config 透传 + cwd 透传", () => {
343
+ makeEnginePkg(tmpRoot, "l3-disabled", { capabilities: FULL_CAPABILITIES });
344
+ const agentDir = path.join(tmpRoot, "agent");
345
+ fs.mkdirSync(path.join(agentDir, "subagents"), { recursive: true });
346
+ fs.writeFileSync(
347
+ path.join(agentDir, "subagents", "config.json"),
348
+ JSON.stringify({
349
+ engines: {
350
+ "l3-disabled": { enabled: false, command: "whatever" },
351
+ "l3-broken": { args: [] },
352
+ "l3-ok": {
353
+ command: path.join(tmpRoot, "l3-disabled", "bin/cli.mjs"),
354
+ args: ["--flag"],
355
+ config: { apiKeyHint: "no-secrets-here" },
356
+ cwd: tmpRoot,
357
+ },
358
+ },
359
+ }),
360
+ );
361
+ const result = scanEngines(scanOpts({ agentDir }));
362
+ expect(result.discovered.map((d) => d.id)).toEqual(["l3-ok"]);
363
+ expect(result.discovered[0].engineConfig).toEqual({ apiKeyHint: "no-secrets-here" });
364
+ // enabled:false 不进清单也不进 unusable(显式禁用非错误)
365
+ expect(result.discovered.some((d) => d.id === "l3-disabled")).toBe(false);
366
+ expect(result.unusable.some((u) => u.id === "l3-disabled")).toBe(false);
367
+ // command 缺失条目:warn 跳过(readExplicitEngines 丢弃,不影响其他条目)
368
+ expect(
369
+ collectedLogs.some((l) => l.message.includes("engines.l3-broken.command is required")),
370
+ ).toBe(true);
371
+ });
372
+
373
+ it("L3 裸命令名经 PATH 解析;解析失败 → unusable 不进清单(投影 = 已发现且可执行)", () => {
374
+ const binDir = path.join(tmpRoot, "pathbin");
375
+ fs.mkdirSync(binDir);
376
+ fs.writeFileSync(path.join(binDir, "l3-on-path"), BIN_CONTENT);
377
+ fs.chmodSync(path.join(binDir, "l3-on-path"), 0o755);
378
+ const agentDir = path.join(tmpRoot, "agent");
379
+ fs.mkdirSync(path.join(agentDir, "subagents"), { recursive: true });
380
+ fs.writeFileSync(
381
+ path.join(agentDir, "subagents", "config.json"),
382
+ JSON.stringify({
383
+ engines: {
384
+ "l3-path": { command: "l3-on-path" },
385
+ "l3-ghost": { command: "definitely-not-on-path-xyz" },
386
+ },
387
+ }),
388
+ );
389
+ const result = scanEngines(
390
+ scanOpts({ agentDir, env: { PATH: binDir } }),
391
+ );
392
+ expect(result.discovered.map((d) => d.id)).toEqual(["l3-path"]);
393
+ expect(result.discovered[0].descriptor.command).toBe(path.join(binDir, "l3-on-path"));
394
+ expect(result.unusable).toHaveLength(1);
395
+ expect(result.unusable[0].id).toBe("l3-ghost");
396
+ });
397
+ });
398
+
399
+ // ── L1 env 解析(分隔符 / 非法条目 / 去重)───────────────────────
400
+
401
+ describe("XYZ_AGENT_ENGINE_ROOTS env 解析", () => {
402
+ it("path.delimiter 分隔;空段跳过;非绝对路径丢弃 + warn;去重(大小写敏感)", () => {
403
+ const abs1 = path.join(tmpRoot, "a");
404
+ const abs2 = path.join(tmpRoot, "b");
405
+ const env = {
406
+ [ENGINE_ROOTS_ENV]: [abs1, "relative/path", "", abs1, abs2].join(path.delimiter),
407
+ };
408
+ const roots = parseEngineRootsEnv(env);
409
+ expect(roots).toEqual([abs1, abs2]);
410
+ expect(collectedLogs.some((l) => l.message.includes("not an absolute path"))).toBe(true);
411
+ });
412
+
413
+ it("env 未设 / 空白 → 空清单(无 warn)", () => {
414
+ expect(parseEngineRootsEnv({})).toEqual([]);
415
+ expect(parseEngineRootsEnv({ [ENGINE_ROOTS_ENV]: " " })).toEqual([]);
416
+ expect(collectedLogs).toHaveLength(0);
417
+ });
418
+ });
419
+
420
+ // ── 冲突与失败(A12)─────────────────────────────────────────────
421
+
422
+ describe("冲突与失败", () => {
423
+ it("bin 不存在 → unusable;存在但不可执行(0o644)→ unusable,均不进清单", () => {
424
+ if (process.platform === "win32") {
425
+ // Windows 无 X_OK 语义(accessSync 仅查存在性),不可执行反例只在 POSIX 跑
426
+ makeEnginePkg(tmpRoot, "missing-bin", { capabilities: FULL_CAPABILITIES }, { omitBin: true });
427
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
428
+ expect(result.discovered).toHaveLength(0);
429
+ expect(result.unusable).toHaveLength(1);
430
+ return;
431
+ }
432
+ makeEnginePkg(tmpRoot, "missing-bin", { capabilities: FULL_CAPABILITIES }, { omitBin: true });
433
+ makeEnginePkg(tmpRoot, "not-executable", { capabilities: FULL_CAPABILITIES }, { binMode: 0o644 });
434
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
435
+ expect(result.discovered).toHaveLength(0);
436
+ expect(result.unusable).toHaveLength(2);
437
+ expect(result.unusable.map((u) => u.id).sort()).toEqual(["missing-bin", "not-executable"]);
438
+ });
439
+
440
+ it("同 id 覆盖:后发现者胜 + debug 留痕(same-id override)", () => {
441
+ makeEnginePkg(path.join(tmpRoot, "first"), "dup", {
442
+ displayName: "First",
443
+ capabilities: FULL_CAPABILITIES,
444
+ });
445
+ makeEnginePkg(path.join(tmpRoot, "second"), "dup", {
446
+ displayName: "Second",
447
+ capabilities: FULL_CAPABILITIES,
448
+ });
449
+ const result = scanEngines(
450
+ scanOpts({
451
+ env: {
452
+ [ENGINE_ROOTS_ENV]: `${path.join(tmpRoot, "first")}${path.delimiter}${path.join(tmpRoot, "second")}`,
453
+ },
454
+ }),
455
+ );
456
+ expect(result.discovered).toHaveLength(1);
457
+ expect(result.discovered[0].descriptor.manifest?.displayName).toBe("Second");
458
+ expect(collectedLogs.some((l) => l.message.includes("same-id override"))).toBe(true);
459
+ });
460
+
461
+ it("坏包不阻断其他引擎(A12②):坏 manifest 与好包共存扫描,好包正常装载", () => {
462
+ // 缺 id 的坏 manifest(makeEnginePkg 的 spread 默认补齐 id,故直写 JSON)
463
+ const brokenDir = path.join(tmpRoot, "broken-id");
464
+ fs.mkdirSync(brokenDir, { recursive: true });
465
+ fs.writeFileSync(
466
+ path.join(brokenDir, "package.json"),
467
+ JSON.stringify({ name: "broken", "xyz-agent": { subagentEngine: { bin: "x", protocol: 1 } } }),
468
+ );
469
+ fs.mkdirSync(path.join(tmpRoot, "torn-json"), { recursive: true });
470
+ fs.writeFileSync(path.join(tmpRoot, "torn-json", "package.json"), "{ torn");
471
+ makeEnginePkg(tmpRoot, "healthy", { capabilities: FULL_CAPABILITIES });
472
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
473
+ expect(result.discovered.map((d) => d.id)).toEqual(["healthy"]);
474
+ expect(result.skipped.length).toBeGreaterThanOrEqual(2);
475
+ });
476
+ });
477
+
478
+ // ── 装载与投影 ───────────────────────────────────────────────────
479
+
480
+ describe("装载与注册表", () => {
481
+ it("discoverAndRegisterEngines 装载 cli descriptor:hasEngine true、getEngine 返回 id 命中的 port(构造同步不 spawn)", () => {
482
+ makeEnginePkg(tmpRoot, "loaded", { displayName: "Loaded", capabilities: FULL_CAPABILITIES });
483
+ makeEnginePkg(tmpRoot, "loaded-catalog", {
484
+ capabilities: FULL_CAPABILITIES,
485
+ modelCatalog: { models: [{ id: "m1" }] },
486
+ });
487
+ discoverAndRegisterEngines(scanOpts({ roots: [tmpRoot] }));
488
+ expect(hasEngine("loaded")).toBe(true);
489
+ const port = getEngine("loaded");
490
+ expect(port.id).toBe("loaded");
491
+ // 同步成员直读 manifest 快照(W2 RemoteEngine 形态):无 modelCatalog →
492
+ // listModels 返回 null(无枚举面)、validateModel 成员不实现(构造器摘除)
493
+ expect(port.listModels?.()).toBeNull();
494
+ expect(typeof port.validateModel).toBe("undefined");
495
+ // 显式 modelCatalog → listModels 返回数组(有枚举面)
496
+ const portWithCatalog = getEngine("loaded-catalog");
497
+ expect(portWithCatalog.listModels?.()).toEqual([{ id: "m1" }]);
498
+ });
499
+
500
+ it("ensureEngineDiscovered:快照命中零扫描;未命中补扫一次后装载", () => {
501
+ makeEnginePkg(tmpRoot, "rescan", { capabilities: FULL_CAPABILITIES });
502
+ const opts = scanOpts({ roots: [tmpRoot] });
503
+ // 未装载:miss → 补扫 → true
504
+ expect(ensureEngineDiscovered("rescan", opts)).toBe(true);
505
+ expect(hasEngine("rescan")).toBe(true);
506
+ // 快照命中:直接 true(再次调用安全)
507
+ expect(ensureEngineDiscovered("rescan", opts)).toBe(true);
508
+ // 确实不存在的 id:补扫后仍 false
509
+ expect(ensureEngineDiscovered("ghost", opts)).toBe(false);
510
+ });
511
+
512
+ it("engines.json 投影合流:发现 cli ∪ inproc 注册;幂等零写;契约 {v:1, engines} 不变", () => {
513
+ makeEnginePkg(tmpRoot, "projected", { capabilities: FULL_CAPABILITIES });
514
+ registerEngine("pi", () => stubEngine("pi"));
515
+ // syncEnginesFile 读 process.env(生产语义)——L1 根经真实 env 注入,afterEach 恢复
516
+ const prevRoots = process.env[ENGINE_ROOTS_ENV];
517
+ process.env[ENGINE_ROOTS_ENV] = tmpRoot;
518
+ try {
519
+ const agentDir = path.join(tmpRoot, "agent");
520
+ syncEnginesFile(agentDir);
521
+ const filePath = getEnginesFilePath(agentDir);
522
+ const file = JSON.parse(fs.readFileSync(filePath, "utf8")) as SubagentEnginesFile;
523
+ expect(file.v).toBe(1);
524
+ expect(file.engines.sort()).toEqual(["pi", "projected"]);
525
+ expect(typeof file.updatedAt).toBe("number");
526
+
527
+ // 幂等零写(内容不变 mtime 不动)
528
+ const statAfterFirst = fs.statSync(filePath);
529
+ syncEnginesFile(agentDir);
530
+ expect(fs.statSync(filePath).mtimeMs).toBe(statAfterFirst.mtimeMs);
531
+
532
+ // 发现面变化(新增引擎包)触发重写
533
+ makeEnginePkg(tmpRoot, "second-projected", { capabilities: FULL_CAPABILITIES });
534
+ syncEnginesFile(agentDir);
535
+ const updated = JSON.parse(fs.readFileSync(filePath, "utf8")) as SubagentEnginesFile;
536
+ expect(updated.engines.sort()).toEqual(["pi", "projected", "second-projected"]);
537
+ } finally {
538
+ if (prevRoots === undefined) delete process.env[ENGINE_ROOTS_ENV];
539
+ else process.env[ENGINE_ROOTS_ENV] = prevRoots;
540
+ }
541
+ });
542
+
543
+ it("syncEnginesFile fail-safe:发现异常不阻塞投影(发现根全部不存在时仍写 inproc 清单)", () => {
544
+ registerEngine("pi", () => stubEngine("pi"));
545
+ const agentDir = path.join(tmpRoot, "agent");
546
+ syncEnginesFile(agentDir);
547
+ const file = JSON.parse(fs.readFileSync(getEnginesFilePath(agentDir), "utf8")) as SubagentEnginesFile;
548
+ expect(file.engines).toEqual(["pi"]);
549
+ });
550
+
551
+ it("零发现零注册时投影空清单(W11 删 inproc 后的终态语义预演)", () => {
552
+ const agentDir = path.join(tmpRoot, "agent");
553
+ syncEnginesFile(agentDir);
554
+ const file = JSON.parse(fs.readFileSync(getEnginesFilePath(agentDir), "utf8")) as SubagentEnginesFile;
555
+ expect(file.engines).toEqual([]);
556
+ expect(listEngines()).toEqual([]);
557
+ });
558
+ });
@@ -0,0 +1,45 @@
1
+ // host-task-spec.test.ts —— ExecuteOptions → AgentCallOpts 直译映射单测(fork-from 载体)。
2
+ //
3
+ // 背景(W3 断链修复):fork-from 的 ExecuteOptions.forkFromSessionFile 在 chat 域协议化
4
+ // 改写后曾断链(本 mapper 不映射 → 协议 run 帧无承载位 → fork-from 轮次以无 --fork 的
5
+ // 全新 session 跑,继承历史丢失)。本文件钉死宿主侧映射面:forkFromSessionFile →
6
+ // forkSource 改名透传(三层同名的唯一改名点)+ 缺省不落键(协议负向兼容)。
7
+ // 端到端消费链由两侧专项覆盖:
8
+ // - chat 域执行链观测(subagent-workflow ended-message-and-fork-from.test 的 fake.runs);
9
+ // - pi 引擎接收面(pi-subagent-cli chat-protocol.test 一次性 run + spawn-args.test
10
+ // 的 forkSource → --fork 直测)。
11
+
12
+ import { describe, expect, it } from "vitest";
13
+
14
+ import { executeOptionsToEngineTaskSpec } from "../host-task-spec.ts";
15
+ import type { ExecuteOptions } from "../../types.ts";
16
+
17
+ function baseOpts(): ExecuteOptions {
18
+ return { task: "do work", slug: "map-test" };
19
+ }
20
+
21
+ describe("executeOptionsToEngineTaskSpec(ExecuteOptions → AgentCallOpts 直译)", () => {
22
+ it("forkFromSessionFile 存在 → forkSource 透传(fork-from 协议载体的宿主侧源头)", () => {
23
+ const spec = executeOptionsToEngineTaskSpec({
24
+ ...baseOpts(),
25
+ forkFromSessionFile: "/sessions/sa-src.jsonl",
26
+ });
27
+ expect(spec.forkSource).toBe("/sessions/sa-src.jsonl");
28
+ expect(spec.prompt).toBe("do work");
29
+ });
30
+
31
+ it("缺省 → 键不落(负向兼容:无 fork-from 的调用帧形状不变)", () => {
32
+ const spec = executeOptionsToEngineTaskSpec(baseOpts());
33
+ expect("forkSource" in spec).toBe(false);
34
+ });
35
+
36
+ it("fork 与 forkSource 互不挤占(独立可选透传;取值优先级语义归引擎侧)", () => {
37
+ const spec = executeOptionsToEngineTaskSpec({
38
+ ...baseOpts(),
39
+ fork: true,
40
+ forkFromSessionFile: "/sessions/sa-src.jsonl",
41
+ });
42
+ expect(spec.fork).toBe(true);
43
+ expect(spec.forkSource).toBe("/sessions/sa-src.jsonl");
44
+ });
45
+ });
@@ -0,0 +1,74 @@
1
+ // model-ref-split.test.ts —— [W3 契约变更④] 无斜杠 canonicalRef 的 core 侧拆分单测。
2
+ //
3
+ // 设计权威源:docs/design/subagent-engine-protocolization.md §3.3 validateModel 行
4
+ // (「core 必须改 resolveIdentityForEngine 与续聊回读对无斜杠 ref 的处理:provider="",
5
+ // id=ref,整串进 name,否则落成 "<ref>/" 畸形」)。拆分单一权威 = splitEngineModelRef,
6
+ // 写入词形 = joinEngineModelRef(provider 空串不拼斜杠);两消费方(chat 域留痕 /
7
+ // 续聊回读)必须同源,否则写入/回读对同一 ref 给出不同 provider/id。
8
+
9
+ import { describe, expect, it } from "vitest";
10
+
11
+ import { joinEngineModelRef, splitEngineModelRef } from "../model-validation.ts";
12
+
13
+ describe("splitEngineModelRef(契约④:无斜杠 ref 不落畸形)", () => {
14
+ it("有斜杠 ref:provider/id 常规拆分,name = 整串", () => {
15
+ expect(splitEngineModelRef("zai/glm-4.6")).toEqual({
16
+ provider: "zai",
17
+ id: "glm-4.6",
18
+ name: "zai/glm-4.6",
19
+ });
20
+ });
21
+
22
+ it("无斜杠 ref(引擎原样返回):provider=''、id=整串、name=整串——不落 \"<ref>/\" 畸形", () => {
23
+ expect(splitEngineModelRef("glm-4.6")).toEqual({
24
+ provider: "",
25
+ id: "glm-4.6",
26
+ name: "glm-4.6",
27
+ });
28
+ });
29
+
30
+ it("空串(引擎未实现校验面且无显式 model 的防御形态):provider/id/name 全空串", () => {
31
+ expect(splitEngineModelRef("")).toEqual({ provider: "", id: "", name: "" });
32
+ });
33
+
34
+ it("开头斜杠(异常形态防御):按无斜杠分支处理,不 crash", () => {
35
+ expect(splitEngineModelRef("/glm")).toEqual({ provider: "", id: "/glm", name: "/glm" });
36
+ });
37
+
38
+ it("多斜杠 ref:首个斜杠拆分(provider 不含斜杠、id 保留其余)", () => {
39
+ expect(splitEngineModelRef("prov/org/model")).toEqual({
40
+ provider: "prov",
41
+ id: "org/model",
42
+ name: "prov/org/model",
43
+ });
44
+ });
45
+ });
46
+
47
+ describe("joinEngineModelRef(record.model 写入词形,与拆分往返自洽)", () => {
48
+ it("provider 非空:provider/id 词形", () => {
49
+ expect(joinEngineModelRef({ provider: "zai", id: "glm-4.6", name: "zai/glm-4.6" })).toBe("zai/glm-4.6");
50
+ });
51
+
52
+ it("provider 空串(契约④):只写 id,无头斜杠——续聊回读可还原", () => {
53
+ expect(joinEngineModelRef({ provider: "", id: "glm-4.6", name: "glm-4.6" })).toBe("glm-4.6");
54
+ });
55
+
56
+ it("写入 → 回读往返(join ∘ split 恒等;split ∘ join 语义还原)", () => {
57
+ for (const modelStr of ["zai/glm-4.6", "glm-4.6", ""]) {
58
+ const split = splitEngineModelRef(modelStr);
59
+ const joined = joinEngineModelRef(split);
60
+ expect(joined).toBe(modelStr); // 写出词形 == 原 ref(无任何斜杠畸形增删)
61
+ const reSplit = splitEngineModelRef(joined);
62
+ expect(reSplit).toEqual(split); // 回读还原同一 provider/id/name
63
+ }
64
+ });
65
+
66
+ it("回归锚(旧行为对照):无斜杠 ref 旧实现落 record.model=\"<ref>/\" 且续聊还原 provider=\"<ref>\"——现两处均消除", () => {
67
+ const ref = "glm-4.6";
68
+ const split = splitEngineModelRef(ref);
69
+ // 写入侧:无尾斜杠
70
+ expect(joinEngineModelRef(split)).not.toContain("/");
71
+ // 回读侧:provider 为空串(旧行为是 "unknown"/整串,给续轮注入虚构 provider)
72
+ expect(splitEngineModelRef(joinEngineModelRef(split)).provider).toBe("");
73
+ });
74
+ });