@zhushanwen/subagent-core 0.2.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 (330) hide show
  1. package/README.md +114 -0
  2. package/dist/chunk-3VOERJPJ.js +22 -0
  3. package/dist/chunk-A4IVZWVX.js +31 -0
  4. package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
  5. package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
  6. package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
  7. package/dist/execution/engine/engines/zcode/constants.js +22 -0
  8. package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
  9. package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
  10. package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
  11. package/dist/execution/engine/engines/zcode/reader.js +239 -0
  12. package/dist/execution/engine/paths.cjs +55 -0
  13. package/dist/execution/engine/paths.d.cts +8 -0
  14. package/dist/execution/engine/paths.d.ts +8 -0
  15. package/dist/execution/engine/paths.js +26 -0
  16. package/dist/execution/relay-env.cjs +62 -0
  17. package/dist/execution/relay-env.d.cts +33 -0
  18. package/dist/execution/relay-env.d.ts +33 -0
  19. package/dist/execution/relay-env.js +20 -0
  20. package/dist/index.cjs +1852 -0
  21. package/dist/index.d.cts +1184 -0
  22. package/dist/index.d.ts +1184 -0
  23. package/dist/index.js +1810 -0
  24. package/dist/types-BxyAidGf.d.cts +875 -0
  25. package/dist/types-BxyAidGf.d.ts +875 -0
  26. package/package.json +99 -0
  27. package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
  28. package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
  29. package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
  30. package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
  31. package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
  32. package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
  33. package/src/__tests__/m2-append-content-probe.test.ts +73 -0
  34. package/src/__tests__/manifest-store.test.ts +368 -0
  35. package/src/__tests__/record-store-cache.test.ts +335 -0
  36. package/src/__tests__/record-store-index.test.ts +560 -0
  37. package/src/__tests__/record-store-last-line.test.ts +129 -0
  38. package/src/__tests__/review-fix-loop-script.test.ts +288 -0
  39. package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
  40. package/src/__tests__/robustness-low-batch1.test.ts +286 -0
  41. package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
  42. package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
  43. package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
  44. package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
  45. package/src/__tests__/session-runner.test.ts +69 -0
  46. package/src/__tests__/smoke.test.ts +9 -0
  47. package/src/__tests__/stream-sink.test.ts +203 -0
  48. package/src/core/__tests__/host-services.test.ts +144 -0
  49. package/src/core/__tests__/logger.test.ts +78 -0
  50. package/src/core/__tests__/notify-ports.test.ts +104 -0
  51. package/src/core/host-services.ts +97 -0
  52. package/src/core/logger.ts +47 -0
  53. package/src/core/notify-ports.ts +126 -0
  54. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
  55. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  56. package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
  57. package/src/execution/__tests__/alive-store.test.ts +147 -0
  58. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
  59. package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
  60. package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
  61. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  62. package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
  63. package/src/execution/__tests__/config.test.ts +302 -0
  64. package/src/execution/__tests__/delivery-methods.test.ts +424 -0
  65. package/src/execution/__tests__/dialog-queue.test.ts +299 -0
  66. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  67. package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
  68. package/src/execution/__tests__/execute-nesting.test.ts +323 -0
  69. package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
  70. package/src/execution/__tests__/execution-record.test.ts +1394 -0
  71. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  72. package/src/execution/__tests__/finalize-record.test.ts +367 -0
  73. package/src/execution/__tests__/finalized-marker.test.ts +104 -0
  74. package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
  75. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  76. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  77. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  78. package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
  79. package/src/execution/__tests__/host-mode.test.ts +87 -0
  80. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  81. package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
  82. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  83. package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
  84. package/src/execution/__tests__/model-resolver.test.ts +465 -0
  85. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  86. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  87. package/src/execution/__tests__/output-collector.test.ts +358 -0
  88. package/src/execution/__tests__/path-encoding.test.ts +104 -0
  89. package/src/execution/__tests__/pi-invocation.test.ts +134 -0
  90. package/src/execution/__tests__/record-store.test.ts +1057 -0
  91. package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
  92. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
  93. package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
  94. package/src/execution/__tests__/relay-env.test.ts +42 -0
  95. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  96. package/src/execution/__tests__/rpc-mode.test.ts +89 -0
  97. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  98. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  99. package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
  100. package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
  101. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  102. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
  103. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  104. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  105. package/src/execution/__tests__/session-file-gc.test.ts +293 -0
  106. package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
  107. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  108. package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
  109. package/src/execution/__tests__/spawn-args.test.ts +445 -0
  110. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
  111. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  112. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
  113. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  114. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  115. package/src/execution/__tests__/stdin-writer.test.ts +462 -0
  116. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  117. package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
  118. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  119. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  120. package/src/execution/__tests__/subagent-service.test.ts +788 -0
  121. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  122. package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
  123. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  124. package/src/execution/__tests__/timeout-integration.test.ts +615 -0
  125. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  126. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  127. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  128. package/src/execution/__tests__/ui-channels.test.ts +187 -0
  129. package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
  130. package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
  131. package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
  132. package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
  133. package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
  134. package/src/execution/__tests__/worktree-manager.test.ts +633 -0
  135. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
  136. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  137. package/src/execution/__tests__/worktree-registry.test.ts +199 -0
  138. package/src/execution/agent-registry.ts +199 -0
  139. package/src/execution/agent-result-mapper.ts +90 -0
  140. package/src/execution/alive-store.ts +119 -0
  141. package/src/execution/argv-mirror.ts +113 -0
  142. package/src/execution/best-effort.ts +38 -0
  143. package/src/execution/channel-registry-access.ts +144 -0
  144. package/src/execution/concurrency-pool.ts +116 -0
  145. package/src/execution/config.ts +165 -0
  146. package/src/execution/dialog-queue.ts +330 -0
  147. package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
  148. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  149. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  150. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  151. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  152. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  153. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  154. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  155. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  156. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  157. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  158. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  159. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  160. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  161. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  162. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  163. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  164. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  165. package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
  166. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  167. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  168. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  169. package/src/execution/engine/common/data-dir.ts +66 -0
  170. package/src/execution/engine/common/errors.ts +183 -0
  171. package/src/execution/engine/common/event-journal.ts +254 -0
  172. package/src/execution/engine/common/journal-replay.ts +62 -0
  173. package/src/execution/engine/common/kill-chain.ts +221 -0
  174. package/src/execution/engine/common/nesting-guard.ts +50 -0
  175. package/src/execution/engine/common/persona-router.ts +108 -0
  176. package/src/execution/engine/common/pool-manager.ts +226 -0
  177. package/src/execution/engine/common/schema-emulation.ts +189 -0
  178. package/src/execution/engine/common/session-view-projection.ts +51 -0
  179. package/src/execution/engine/engine-discovery.ts +65 -0
  180. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  181. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  182. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  183. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  184. package/src/execution/engine/engines/pi/reader.ts +48 -0
  185. package/src/execution/engine/engines/pi/registration.ts +35 -0
  186. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  187. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  188. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  189. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  190. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  191. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  192. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
  193. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  194. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
  195. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  196. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  197. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  198. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  199. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  200. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  201. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  202. package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
  203. package/src/execution/engine/host-task-spec.ts +47 -0
  204. package/src/execution/engine/model-prompt.ts +158 -0
  205. package/src/execution/engine/paths.ts +42 -0
  206. package/src/execution/engine/port.ts +153 -0
  207. package/src/execution/engine/registry.ts +133 -0
  208. package/src/execution/engine/routing.ts +218 -0
  209. package/src/execution/engine/types.ts +309 -0
  210. package/src/execution/execute-options-mapper.ts +115 -0
  211. package/src/execution/execution-record.ts +984 -0
  212. package/src/execution/finalize-record.ts +254 -0
  213. package/src/execution/finalized-marker.ts +70 -0
  214. package/src/execution/get-state-handshake.ts +104 -0
  215. package/src/execution/host-mode.ts +56 -0
  216. package/src/execution/idle-gc.ts +47 -0
  217. package/src/execution/lifecycle-manager.ts +513 -0
  218. package/src/execution/lifecycle-predicates.ts +65 -0
  219. package/src/execution/manifest-store.ts +256 -0
  220. package/src/execution/model-config-service.ts +250 -0
  221. package/src/execution/model-resolver.ts +248 -0
  222. package/src/execution/notifier.ts +372 -0
  223. package/src/execution/notify-ledger.ts +580 -0
  224. package/src/execution/output-collector.ts +228 -0
  225. package/src/execution/path-encoding.ts +60 -0
  226. package/src/execution/pi-invocation.ts +120 -0
  227. package/src/execution/record-entry.ts +132 -0
  228. package/src/execution/record-store.ts +1265 -0
  229. package/src/execution/relay-env.ts +37 -0
  230. package/src/execution/session-context-resolver.ts +64 -0
  231. package/src/execution/session-file-gc.ts +120 -0
  232. package/src/execution/session-pending.ts +170 -0
  233. package/src/execution/session-reconstructor.ts +678 -0
  234. package/src/execution/session-runner.ts +1789 -0
  235. package/src/execution/sessions-index.ts +304 -0
  236. package/src/execution/spawn-event-adapter.ts +363 -0
  237. package/src/execution/stdin-writer.ts +198 -0
  238. package/src/execution/stream-sink.ts +126 -0
  239. package/src/execution/subagent-service.ts +2268 -0
  240. package/src/execution/subprocess-agent-runner.ts +317 -0
  241. package/src/execution/temp-prompt.ts +62 -0
  242. package/src/execution/tombstone-store.ts +72 -0
  243. package/src/execution/turn-limiter.ts +102 -0
  244. package/src/execution/types.ts +1023 -0
  245. package/src/execution/ui-channels.ts +216 -0
  246. package/src/execution/ui-interaction-model.ts +48 -0
  247. package/src/execution/ui-request-handler-factory.ts +243 -0
  248. package/src/execution/ui-request-observability.ts +81 -0
  249. package/src/execution/ui-request-queue.ts +184 -0
  250. package/src/execution/worktree-manager.ts +688 -0
  251. package/src/execution/worktree-registry.ts +279 -0
  252. package/src/index.ts +87 -0
  253. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
  254. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
  255. package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
  256. package/src/orchestration/__tests__/args-validator.test.ts +143 -0
  257. package/src/orchestration/__tests__/config-loader.test.ts +503 -0
  258. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
  259. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
  260. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
  261. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  262. package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
  263. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
  264. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  265. package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
  266. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  267. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
  268. package/src/orchestration/__tests__/script-lint.test.ts +831 -0
  269. package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
  270. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  271. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  272. package/src/orchestration/__tests__/worker-host.test.ts +120 -0
  273. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  274. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
  275. package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
  276. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
  277. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
  278. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  279. package/src/orchestration/agent-opts-resolver.ts +174 -0
  280. package/src/orchestration/args-validator.ts +127 -0
  281. package/src/orchestration/config-loader.ts +316 -0
  282. package/src/orchestration/error-recovery.ts +1018 -0
  283. package/src/orchestration/execute-agent-call.ts +256 -0
  284. package/src/orchestration/launcher.ts +455 -0
  285. package/src/orchestration/lifecycle.ts +399 -0
  286. package/src/orchestration/models/__tests__/budget.test.ts +307 -0
  287. package/src/orchestration/models/agent-call.ts +83 -0
  288. package/src/orchestration/models/budget.ts +118 -0
  289. package/src/orchestration/models/ports.ts +164 -0
  290. package/src/orchestration/models/run-runtime.ts +104 -0
  291. package/src/orchestration/models/run-spec.ts +83 -0
  292. package/src/orchestration/models/run-state.ts +44 -0
  293. package/src/orchestration/models/trace.ts +183 -0
  294. package/src/orchestration/models/types.ts +301 -0
  295. package/src/orchestration/models/workflow-run.ts +254 -0
  296. package/src/orchestration/models/workflow-script-registry.ts +35 -0
  297. package/src/orchestration/models/workflow-script.ts +117 -0
  298. package/src/orchestration/script-lint.ts +766 -0
  299. package/src/orchestration/skill-discovery.ts +135 -0
  300. package/src/orchestration/worker-handle.ts +115 -0
  301. package/src/orchestration/worker-host.ts +98 -0
  302. package/src/orchestration/worker-script-builder.ts +421 -0
  303. package/src/orchestration/workflow-files.ts +85 -0
  304. package/src/orchestration/workflow-script-registry-impl.ts +133 -0
  305. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  306. package/src/shared/__tests__/meta-parser.test.ts +304 -0
  307. package/src/shared/__tests__/model-ref.test.ts +306 -0
  308. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
  309. package/src/shared/__tests__/resource-discovery.test.ts +749 -0
  310. package/src/shared/__tests__/resource-meta.test.ts +51 -0
  311. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  312. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  313. package/src/shared/agent-event.ts +13 -0
  314. package/src/shared/agent-ref.ts +57 -0
  315. package/src/shared/meta-parser.ts +261 -0
  316. package/src/shared/model-ref.ts +286 -0
  317. package/src/shared/resource-discovery.ts +835 -0
  318. package/src/shared/resource-meta.ts +65 -0
  319. package/src/shared/schema-env.ts +44 -0
  320. package/src/shared/schema-jsonify.ts +58 -0
  321. package/src/shared/timer-delay.ts +54 -0
  322. package/src/shared/xml-injection.ts +35 -0
  323. package/workflows/README.md +81 -0
  324. package/workflows/_shared/agent-refs.cjs +40 -0
  325. package/workflows/chain.js +137 -0
  326. package/workflows/map-reduce.js +180 -0
  327. package/workflows/parallel.js +154 -0
  328. package/workflows/review-fix-loop-utils.cjs +1542 -0
  329. package/workflows/review-fix-loop.js +1605 -0
  330. package/workflows/scatter-gather.js +184 -0
@@ -0,0 +1,749 @@
1
+ // src/shared/__tests__/resource-discovery.test.ts
2
+ //
3
+ // 统一资源发现模块测试(ADR-031)。
4
+ // 验证:扫描源覆盖、优先级合并、manifest 校验、约定目录 fallback。
5
+ import * as fs from "node:fs";
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+
9
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
+
11
+ // 隔离真实用户全局目录:resource-discovery 用 homedir() 推导 user-agents 源
12
+ // (~/.agents/agents/),测试环境可能存在真实 agent 文件(如 tech-design-review.md),
13
+ // 不 mock 会导致「期望空列表/精确列表」用例被环境污染(2026-08 实测 4 个失败)。
14
+ // 用真实 tmpdir 下的子目录作 mock homedir(macOS SIP 禁止 mkdir /nonexistent-*)。
15
+ const mockHomeDir = vi.hoisted(() => {
16
+ const fs = require("node:fs");
17
+ const os = require("node:os");
18
+ const path = require("node:path");
19
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "res-disc-home-"));
20
+ return dir;
21
+ });
22
+
23
+ vi.mock("node:os", async (importOriginal) => {
24
+ const actual = await importOriginal<typeof import("node:os")>();
25
+ return { ...actual, homedir: () => mockHomeDir };
26
+ });
27
+
28
+ import {
29
+ discoverResources,
30
+ discoverResourcesSync,
31
+ findWorkspaceRoot,
32
+ processPackageSync,
33
+ getCachedFile,
34
+ getCachedFileContent,
35
+ __testResetShadowDedup,
36
+ __testInjectShadowDedupKey,
37
+ isMachineSource,
38
+ getCachedParsed,
39
+ clearFileCache,
40
+ } from "../resource-discovery.ts";
41
+ // logger 断言目标随源切换(u0-data-discovery):resource-discovery 的 logger 是
42
+ // core facade(同 component 单例引用),spyOn facade 实例即拦截模块内 logger 调用
43
+ import { getLogger } from "../../core/logger.ts";
44
+
45
+ // ============================================================
46
+ // helpers
47
+ // ============================================================
48
+
49
+ function tmpWorkspace(): string {
50
+ return fs.mkdtempSync(path.join(os.tmpdir(), "res-disc-test-"));
51
+ }
52
+
53
+ /** 与 pi 壳 agentDirKindRoots 同构的注入根:由旧 ScanConfig.agentDir 入参等价推导
54
+ * (user-pi 根 = <agentDir>/<kind>,npm 根 = <agentDir>/npm/node_modules,npm-dev
55
+ * 根 = <agentDir>/extensions)——扫描目录与形状改造前逐字一致,expected 断言不动。 */
56
+ function hostRootsFor(agentDir: string, kind: "agents" | "workflows") {
57
+ return [
58
+ { dir: path.join(agentDir, kind), source: "user-pi" },
59
+ { dir: path.join(agentDir, "npm", "node_modules"), source: "npm" },
60
+ { dir: path.join(agentDir, "extensions"), source: "npm-dev" },
61
+ ];
62
+ }
63
+
64
+ function writeFile(dir: string, name: string, content: string): string {
65
+ fs.mkdirSync(dir, { recursive: true });
66
+ const filePath = path.join(dir, name);
67
+ fs.writeFileSync(filePath, content, "utf-8");
68
+ return filePath;
69
+ }
70
+
71
+ function writePackageJson(pkgDir: string, pi: Record<string, unknown>): void {
72
+ fs.mkdirSync(pkgDir, { recursive: true });
73
+ fs.writeFileSync(
74
+ path.join(pkgDir, "package.json"),
75
+ JSON.stringify({ name: "test-pkg", pi }),
76
+ "utf-8",
77
+ );
78
+ }
79
+
80
+ // ============================================================
81
+ // findWorkspaceRoot
82
+ // ============================================================
83
+
84
+ describe("findWorkspaceRoot", () => {
85
+ it("returns cwd when no marker found", () => {
86
+ const ws = tmpWorkspace();
87
+ expect(findWorkspaceRoot(ws)).toBe(ws);
88
+ fs.rmSync(ws, { recursive: true, force: true });
89
+ });
90
+
91
+ it("finds .git root", () => {
92
+ const ws = tmpWorkspace();
93
+ fs.mkdirSync(path.join(ws, ".git"));
94
+ const sub = path.join(ws, "sub", "deep");
95
+ fs.mkdirSync(sub, { recursive: true });
96
+ expect(findWorkspaceRoot(sub)).toBe(ws);
97
+ fs.rmSync(ws, { recursive: true, force: true });
98
+ });
99
+ });
100
+
101
+ // ============================================================
102
+ // discoverResourcesSync — 扫描源覆盖 + 优先级
103
+ // ============================================================
104
+
105
+ describe("discoverResourcesSync", () => {
106
+ let ws: string;
107
+ let agentDir: string;
108
+
109
+ beforeEach(() => {
110
+ ws = tmpWorkspace();
111
+ agentDir = path.join(ws, ".fake-agent");
112
+ });
113
+ afterEach(() => {
114
+ fs.rmSync(ws, { recursive: true, force: true });
115
+ });
116
+
117
+ it("discovers agents from project .pi/agents/", () => {
118
+ writeFile(path.join(ws, ".pi", "agents"), "worker.md", "body");
119
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
120
+ // 按 source 过滤断言——用户全局目录(~/.agents/agents/)可能有真实 agent 文件,
121
+ // 测试不假设环境为空(2026-08:环境新增 tech-design-review.md 暴露此脆弱性)
122
+ const project = result.filter((r) => r.source === "project-pi");
123
+ expect(project.map((r) => path.basename(r.path))).toEqual(["worker.md"]);
124
+ expect(project[0]?.available).toBe(true);
125
+ });
126
+
127
+ it("discovers workflows from project .pi/workflows/", () => {
128
+ writeFile(path.join(ws, ".pi", "workflows"), "build.js", "const meta={name:'build'};");
129
+ const result = discoverResourcesSync({ kind: "workflows", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "workflows") });
130
+ expect(result.map((r) => path.basename(r.path))).toEqual(["build.js"]);
131
+ });
132
+
133
+ it("project .agents overrides project .pi on name clash (priority)", () => {
134
+ writeFile(path.join(ws, ".pi", "agents"), "worker.md", "pi-body");
135
+ writeFile(path.join(ws, ".agents", "agents"), "worker.md", "agents-body");
136
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
137
+ const project = result.filter((r) => r.source === "project-agents");
138
+ expect(project).toHaveLength(1);
139
+ expect(project[0]?.source).toBe("project-agents");
140
+ });
141
+
142
+ it("includes tmp source for workflows when includeTmp=true", () => {
143
+ writeFile(path.join(ws, ".pi", "workflows", ".tmp"), "temp.js", "const meta={name:'temp'};");
144
+ const result = discoverResourcesSync({
145
+ kind: "workflows",
146
+ workspaceRoot: ws,
147
+ hostRoots: hostRootsFor(agentDir, "workflows"),
148
+ includeTmp: true,
149
+ });
150
+ expect(result.map((r) => path.basename(r.path))).toEqual(["temp.js"]);
151
+ expect(result[0]?.source).toBe("project-pi-tmp");
152
+ });
153
+
154
+ it("excludes tmp source when includeTmp omitted", () => {
155
+ writeFile(path.join(ws, ".pi", "workflows", ".tmp"), "temp.js", "x");
156
+ const result = discoverResourcesSync({ kind: "workflows", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "workflows") });
157
+ expect(result).toEqual([]);
158
+ });
159
+
160
+ it("ignores _ prefix and .chain.md files", () => {
161
+ const dir = path.join(ws, ".pi", "agents");
162
+ writeFile(dir, "real.md", "body");
163
+ writeFile(dir, "_skip.md", "ignored");
164
+ writeFile(dir, "trace.chain.md", "ignored");
165
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
166
+ const project = result.filter((r) => r.source === "project-pi");
167
+ expect(project.map((r) => path.basename(r.path))).toEqual(["real.md"]);
168
+ });
169
+
170
+ it("nonexistent directories are silently skipped", () => {
171
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
172
+ // 用户全局目录可能有真实文件——只断言 project 源为空
173
+ expect(result.filter((r) => r.source === "project-pi" || r.source === "project-agents")).toEqual([]);
174
+ });
175
+ });
176
+
177
+ // ============================================================
178
+ // processPackageSync — manifest 校验
179
+ // ============================================================
180
+
181
+ describe("processPackageSync", () => {
182
+ let pkgDir: string;
183
+
184
+ beforeEach(() => {
185
+ pkgDir = tmpWorkspace();
186
+ });
187
+ afterEach(() => {
188
+ fs.rmSync(pkgDir, { recursive: true, force: true });
189
+ });
190
+
191
+ it("loads from manifest directory declaration", () => {
192
+ writeFile(path.join(pkgDir, "agents"), "worker.md", "body");
193
+ writePackageJson(pkgDir, { agents: ["./agents"] });
194
+ const result = processPackageSync(pkgDir, "agents");
195
+ expect(result).toHaveLength(1);
196
+ expect(result[0]?.available).toBe(true);
197
+ });
198
+
199
+ it("loads from manifest file declaration", () => {
200
+ writeFile(pkgDir, "worker.md", "body");
201
+ writePackageJson(pkgDir, { agents: ["./worker.md"] });
202
+ const result = processPackageSync(pkgDir, "agents");
203
+ expect(result).toHaveLength(1);
204
+ expect(result[0]?.available).toBe(true);
205
+ });
206
+
207
+ it("manifest path not exists → available=false, no fallback", () => {
208
+ writePackageJson(pkgDir, { agents: ["./nonexistent"] });
209
+ const result = processPackageSync(pkgDir, "agents");
210
+ expect(result).toHaveLength(1);
211
+ expect(result[0]?.available).toBe(false);
212
+ // 不 fallback 到约定目录
213
+ writeFile(path.join(pkgDir, "agents"), "hidden.md", "body");
214
+ const result2 = processPackageSync(pkgDir, "agents");
215
+ expect(result2).toHaveLength(1);
216
+ expect(result2[0]?.available).toBe(false);
217
+ });
218
+
219
+ it("no manifest → fallback to convention dir", () => {
220
+ writeFile(path.join(pkgDir, "agents"), "worker.md", "body");
221
+ // 无 package.json 或无 pi.agents
222
+ const result = processPackageSync(pkgDir, "agents");
223
+ expect(result).toHaveLength(1);
224
+ expect(result[0]?.available).toBe(true);
225
+ });
226
+
227
+ it("no manifest and no convention dir → empty", () => {
228
+ writePackageJson(pkgDir, { extensions: ["./index.ts"] });
229
+ const result = processPackageSync(pkgDir, "agents");
230
+ expect(result).toEqual([]);
231
+ });
232
+ });
233
+
234
+ // ============================================================
235
+ // discoverResources (async) — 基本冒烟
236
+ // ============================================================
237
+
238
+ describe("discoverResources (async)", () => {
239
+ let ws: string;
240
+
241
+ beforeEach(() => {
242
+ ws = tmpWorkspace();
243
+ });
244
+ afterEach(() => {
245
+ fs.rmSync(ws, { recursive: true, force: true });
246
+ __testResetShadowDedup();
247
+ });
248
+
249
+ it("discovers agents from project .pi/agents/ (async)", async () => {
250
+ writeFile(path.join(ws, ".pi", "agents"), "worker.md", "body");
251
+ const result = await discoverResources({
252
+ kind: "agents",
253
+ workspaceRoot: ws,
254
+ hostRoots: hostRootsFor(path.join(ws, ".fake-agent"), "agents"),
255
+ });
256
+ expect(result).toHaveLength(1);
257
+ expect(result[0]?.available).toBe(true);
258
+ });
259
+
260
+ it("discovers workflows with tmp source (async)", async () => {
261
+ writeFile(path.join(ws, ".pi", "workflows"), "build.js", "x");
262
+ writeFile(path.join(ws, ".pi", "workflows", ".tmp"), "temp.js", "x");
263
+ const result = await discoverResources({
264
+ kind: "workflows",
265
+ workspaceRoot: ws,
266
+ hostRoots: hostRootsFor(path.join(ws, ".fake-agent"), "workflows"),
267
+ includeTmp: true,
268
+ });
269
+ expect(result).toHaveLength(2);
270
+ });
271
+ });
272
+
273
+ // ============================================================
274
+ // user-extension-paths (XYZ_EXTENSION_PATHS) — dev-link 扩展发现
275
+ // ============================================================
276
+
277
+ describe("user-extension-paths (XYZ_EXTENSION_PATHS)", () => {
278
+ let ws: string;
279
+ let agentDir: string;
280
+ let savedEnv: string | undefined;
281
+
282
+ beforeEach(() => {
283
+ ws = tmpWorkspace();
284
+ agentDir = path.join(ws, ".fake-agent");
285
+ savedEnv = process.env.XYZ_EXTENSION_PATHS;
286
+ });
287
+ afterEach(() => {
288
+ if (savedEnv === undefined) delete process.env.XYZ_EXTENSION_PATHS;
289
+ else process.env.XYZ_EXTENSION_PATHS = savedEnv;
290
+ fs.rmSync(ws, { recursive: true, force: true });
291
+ __testResetShadowDedup();
292
+ });
293
+
294
+ it("discovers agents from XYZ_EXTENSION_PATHS via pi.agents manifest", () => {
295
+ const pkgDir = path.join(ws, "my-ext");
296
+ writePackageJson(pkgDir, { agents: ["./agents"] });
297
+ writeFile(path.join(pkgDir, "agents"), "custom.md", "body");
298
+ process.env.XYZ_EXTENSION_PATHS = pkgDir;
299
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
300
+ const ext = result.filter((r) => r.source === "user-extension-paths");
301
+ expect(ext.map((r) => path.basename(r.path))).toEqual(["custom.md"]);
302
+ expect(ext[0]?.source).toBe("user-extension-paths");
303
+ });
304
+
305
+ it("discovers agents via convention dir (no manifest)", () => {
306
+ const pkgDir = path.join(ws, "my-ext");
307
+ writeFile(path.join(pkgDir, "agents"), "conv.md", "body");
308
+ process.env.XYZ_EXTENSION_PATHS = pkgDir;
309
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
310
+ const ext = result.filter((r) => r.source === "user-extension-paths");
311
+ expect(ext.map((r) => path.basename(r.path))).toEqual(["conv.md"]);
312
+ expect(ext[0]?.source).toBe("user-extension-paths");
313
+ });
314
+
315
+ it("multiple paths separated by delimiter", () => {
316
+ const pkg1 = path.join(ws, "ext1");
317
+ const pkg2 = path.join(ws, "ext2");
318
+ writeFile(path.join(pkg1, "agents"), "a1.md", "body");
319
+ writeFile(path.join(pkg2, "agents"), "a2.md", "body");
320
+ process.env.XYZ_EXTENSION_PATHS = `${pkg1}${path.delimiter}${pkg2}`;
321
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
322
+ const ext = result.filter((r) => r.source === "user-extension-paths");
323
+ expect(ext.map((r) => path.basename(r.path)).sort()).toEqual(["a1.md", "a2.md"]);
324
+ });
325
+
326
+ it("overrides npm on name clash (priority: user-extension-paths > npm)", () => {
327
+ const npmPkg = path.join(agentDir, "npm", "node_modules", "test-pkg");
328
+ writePackageJson(npmPkg, { agents: ["./agents"] });
329
+ writeFile(path.join(npmPkg, "agents"), "shared.md", "npm-body");
330
+ const devPkg = path.join(ws, "dev-ext");
331
+ writePackageJson(devPkg, { agents: ["./agents"] });
332
+ writeFile(path.join(devPkg, "agents"), "shared.md", "dev-body");
333
+ process.env.XYZ_EXTENSION_PATHS = devPkg;
334
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
335
+ const shared = result.find((r) => path.basename(r.path) === "shared.md");
336
+ expect(shared?.source).toBe("user-extension-paths");
337
+ });
338
+
339
+ it("project-agents overrides user-extension-paths (project wins)", () => {
340
+ const devPkg = path.join(ws, "dev-ext");
341
+ writePackageJson(devPkg, { agents: ["./agents"] });
342
+ writeFile(path.join(devPkg, "agents"), "x.md", "dev-body");
343
+ writeFile(path.join(ws, ".agents", "agents"), "x.md", "project-body");
344
+ process.env.XYZ_EXTENSION_PATHS = devPkg;
345
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
346
+ const x = result.find((r) => path.basename(r.path) === "x.md");
347
+ expect(x?.source).toBe("project-agents");
348
+ });
349
+
350
+ it("empty/unset env → no user-extension-paths source", () => {
351
+ delete process.env.XYZ_EXTENSION_PATHS;
352
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
353
+ expect(result.filter((r) => r.source === "user-extension-paths")).toEqual([]);
354
+ });
355
+
356
+ it("async discoverResources also scans user-extension-paths", async () => {
357
+ const pkgDir = path.join(ws, "my-ext");
358
+ writePackageJson(pkgDir, { agents: ["./agents"] });
359
+ writeFile(path.join(pkgDir, "agents"), "async.md", "body");
360
+ process.env.XYZ_EXTENSION_PATHS = pkgDir;
361
+ const result = await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
362
+ const ext = result.filter((r) => r.source === "user-extension-paths");
363
+ expect(ext.map((r) => path.basename(r.path))).toEqual(["async.md"]);
364
+ expect(ext[0]?.source).toBe("user-extension-paths");
365
+ });
366
+
367
+ it("async: overrides npm on name clash (priority: user-extension-paths > npm)", async () => {
368
+ const npmPkg = path.join(agentDir, "npm", "node_modules", "test-pkg");
369
+ writePackageJson(npmPkg, { agents: ["./agents"] });
370
+ writeFile(path.join(npmPkg, "agents"), "shared.md", "npm-body");
371
+ const devPkg = path.join(ws, "dev-ext");
372
+ writePackageJson(devPkg, { agents: ["./agents"] });
373
+ writeFile(path.join(devPkg, "agents"), "shared.md", "dev-body");
374
+ process.env.XYZ_EXTENSION_PATHS = devPkg;
375
+ const result = await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
376
+ const shared = result.find((r) => path.basename(r.path) === "shared.md");
377
+ expect(shared?.source).toBe("user-extension-paths");
378
+ });
379
+
380
+ it("async: project-agents overrides user-extension-paths (project wins)", async () => {
381
+ const devPkg = path.join(ws, "dev-ext");
382
+ writePackageJson(devPkg, { agents: ["./agents"] });
383
+ writeFile(path.join(devPkg, "agents"), "x.md", "dev-body");
384
+ writeFile(path.join(ws, ".agents", "agents"), "x.md", "project-body");
385
+ process.env.XYZ_EXTENSION_PATHS = devPkg;
386
+ const result = await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
387
+ const x = result.find((r) => path.basename(r.path) === "x.md");
388
+ expect(x?.source).toBe("project-agents");
389
+ });
390
+
391
+ it("async: 4 源混合 fixture——输出序 + 优先级 + 与串行版快照等价", async () => {
392
+ // 源优先级低→高:npm < user-extension-paths < project-pi < project-agents
393
+ const npmPkg = path.join(agentDir, "npm", "node_modules", "test-pkg");
394
+ writePackageJson(npmPkg, { agents: ["./agents"] });
395
+ writeFile(path.join(npmPkg, "agents"), "shared.md", "npm-body");
396
+ const devPkg = path.join(ws, "dev-ext");
397
+ writePackageJson(devPkg, { agents: ["./agents"] });
398
+ writeFile(path.join(devPkg, "agents"), "dev.md", "dev-body");
399
+ writeFile(path.join(ws, ".pi", "agents"), "pi.md", "pi-body");
400
+ writeFile(path.join(ws, ".agents", "agents"), "shared.md", "project-body");
401
+ writeFile(path.join(ws, ".agents", "agents"), "proj.md", "proj-body");
402
+ process.env.XYZ_EXTENSION_PATHS = devPkg;
403
+ const config = { kind: "agents" as const, workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") };
404
+
405
+ const asyncResult = await discoverResources(config);
406
+
407
+ // 输出序:stem 首次插入位置固定(低优先级源先入 Map),同名后续覆盖值不移位
408
+ expect(asyncResult.map((r) => path.basename(r.path))).toEqual([
409
+ "shared.md", // 首现于 npm(最低源),位置固定
410
+ "dev.md", // user-extension-paths
411
+ "pi.md", // project-pi
412
+ "proj.md", // project-agents
413
+ ]);
414
+ // 优先级:shared.md 两源同名,project-agents(高)last-writer-wins
415
+ expect(asyncResult.find((r) => path.basename(r.path) === "shared.md")?.source).toBe(
416
+ "project-agents",
417
+ );
418
+ // 快照等价:源级并行 async 版与串行 sync 版输出逐项一致(含序与 source 标签)
419
+ expect(asyncResult).toEqual(discoverResourcesSync(config));
420
+ });
421
+
422
+ it("async: 同名遮蔽时机器源×机器源降 debug 不产生 warn(D8d 分级)", async () => {
423
+ const npmPkg = path.join(agentDir, "npm", "node_modules", "test-pkg");
424
+ writePackageJson(npmPkg, { agents: ["./agents"] });
425
+ const npmFile = writeFile(path.join(npmPkg, "agents"), "dup.md", "npm-body");
426
+ const projFile = writeFile(path.join(ws, ".agents", "agents"), "dup.md", "project-body");
427
+ const warnSpy = vi.spyOn(getLogger("subagents"), "warn");
428
+ const debugSpy = vi.spyOn(getLogger("subagents"), "debug");
429
+
430
+ try {
431
+ const result = await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
432
+
433
+ // 遮蔽仍生效(last-writer-wins 语义不变)
434
+ expect(result.find((r) => path.basename(r.path) === "dup.md")?.source).toBe("project-agents");
435
+ // npm 与 project-agents 均为机器源 → 降级 debug,不产生 warn
436
+ expect(warnSpy).not.toHaveBeenCalled();
437
+ expect(debugSpy).toHaveBeenCalledTimes(1);
438
+ const [msg, data] = debugSpy.mock.calls[0];
439
+ expect(String(msg)).toContain('duplicate agents "dup"');
440
+ expect(String(msg)).toContain("project-agents shadows npm");
441
+ expect(data).toMatchObject({ shadowed: npmFile, kept: projFile });
442
+ } finally {
443
+ warnSpy.mockRestore();
444
+ debugSpy.mockRestore();
445
+ }
446
+ });
447
+
448
+ it("(a) 机器源×用户源降 debug:npm vs user-pi 不产生 warn", async () => {
449
+ // npm 源(机器源)与 user-pi 源(用户源)——任一侧为机器源即降 debug
450
+ const npmPkg = path.join(agentDir, "npm", "node_modules", "test-pkg");
451
+ writePackageJson(npmPkg, { agents: ["./agents"] });
452
+ writeFile(path.join(npmPkg, "agents"), "dup.md", "npm-body");
453
+ // user-pi 源 = agentDir/<kind>/(agentDir 是独立于 homedir mock 的入参,
454
+ // mockHomeDir 由 vi.hoisted mkdtempSync 创建真实 tmpdir)
455
+ writeFile(path.join(agentDir, "agents"), "dup.md", "user-pi-body");
456
+ const warnSpy = vi.spyOn(getLogger("subagents"), "warn");
457
+ const debugSpy = vi.spyOn(getLogger("subagents"), "debug");
458
+
459
+ try {
460
+ await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
461
+
462
+ expect(warnSpy).not.toHaveBeenCalled();
463
+ expect(debugSpy).toHaveBeenCalled();
464
+ const [msg] = debugSpy.mock.calls[0];
465
+ expect(String(msg)).toContain('duplicate agents "dup"');
466
+ } finally {
467
+ warnSpy.mockRestore();
468
+ debugSpy.mockRestore();
469
+ }
470
+ });
471
+
472
+ it("(a) npm vs user-extension-paths 机器源×机器源降 debug", async () => {
473
+ // 两个机器源同名(npm 包 vs XYZ_EXTENSION_PATHS 注入的 dev 包,后者 source 标签为 user-extension-paths)
474
+ const npmPkg = path.join(agentDir, "npm", "node_modules", "test-pkg");
475
+ writePackageJson(npmPkg, { agents: ["./agents"] });
476
+ writeFile(path.join(npmPkg, "agents"), "shared.md", "npm-body");
477
+ const devPkg = path.join(ws, "dev-ext");
478
+ writePackageJson(devPkg, { agents: ["./agents"] });
479
+ writeFile(path.join(devPkg, "agents"), "shared.md", "dev-body");
480
+ process.env.XYZ_EXTENSION_PATHS = devPkg;
481
+ const warnSpy = vi.spyOn(getLogger("subagents"), "warn");
482
+ const debugSpy = vi.spyOn(getLogger("subagents"), "debug");
483
+
484
+ try {
485
+ await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
486
+
487
+ expect(warnSpy).not.toHaveBeenCalled();
488
+ expect(debugSpy).toHaveBeenCalled();
489
+ } finally {
490
+ warnSpy.mockRestore();
491
+ debugSpy.mockRestore();
492
+ }
493
+ });
494
+
495
+ it("(b) 双用户源重复产生 warn 且同进程第二次 discoverResources 不重复(去重生效)", async () => {
496
+ // user-pi 与 user-agents 都是用户源——需要构造两个源都有同名文件
497
+ // user-pi = agentDir/agents/(buildScanTargets 第一个 target)
498
+ // user-agents = mockHomeDir/.agents/agents/(vi.hoisted 创建的真实 tmpdir)
499
+ const userAgentsDir = path.join(mockHomeDir, ".agents", "agents");
500
+ fs.mkdirSync(userAgentsDir, { recursive: true });
501
+ try {
502
+ // user-pi: agentDir/agents/dup.md
503
+ writeFile(path.join(agentDir, "agents"), "dup.md", "user-pi-body");
504
+ // user-agents: mockHomeDir/.agents/agents/dup.md
505
+ fs.writeFileSync(path.join(userAgentsDir, "dup.md"), "user-agents-body", "utf-8");
506
+
507
+ const warnSpy = vi.spyOn(getLogger("subagents"), "warn");
508
+
509
+ try {
510
+ // 第一次调用——应产生 warn(双用户源)
511
+ await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
512
+ expect(warnSpy).toHaveBeenCalledTimes(1);
513
+ const [msg] = warnSpy.mock.calls[0];
514
+ expect(String(msg)).toContain('duplicate agents "dup"');
515
+
516
+ // 第二次调用——同进程去重,不再报
517
+ warnSpy.mockClear();
518
+ await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
519
+ expect(warnSpy).not.toHaveBeenCalled();
520
+ } finally {
521
+ warnSpy.mockRestore();
522
+ }
523
+ } finally {
524
+ fs.rmSync(userAgentsDir, { recursive: true, force: true });
525
+ }
526
+ });
527
+
528
+ it("(c) path 变化(新 key)重新报 warn", async () => {
529
+ const userAgentsDir = path.join(mockHomeDir, ".agents", "agents");
530
+ fs.mkdirSync(userAgentsDir, { recursive: true });
531
+ try {
532
+ writeFile(path.join(agentDir, "agents"), "dup.md", "user-pi-body");
533
+ fs.writeFileSync(path.join(userAgentsDir, "dup.md"), "user-agents-body", "utf-8");
534
+
535
+ const warnSpy = vi.spyOn(getLogger("subagents"), "warn");
536
+
537
+ try {
538
+ // 第一次调用——报 warn
539
+ await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
540
+ expect(warnSpy).toHaveBeenCalledTimes(1);
541
+
542
+ // 第二次调用——同 key 去重,不报
543
+ warnSpy.mockClear();
544
+ await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
545
+ expect(warnSpy).not.toHaveBeenCalled();
546
+
547
+ // 真实 path 变化产生新 key:双用户目录各加一个不同 stem(dup2.md)
548
+ // → 新遮蔽对(新 stem 新 path)→ 新 key → 重新报
549
+ writeFile(path.join(agentDir, "agents"), "dup2.md", "user-pi-body-2");
550
+ fs.writeFileSync(path.join(userAgentsDir, "dup2.md"), "user-agents-body-2", "utf-8");
551
+ warnSpy.mockClear();
552
+ await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
553
+ expect(warnSpy).toHaveBeenCalledTimes(1);
554
+ const [newMsg] = warnSpy.mock.calls[0];
555
+ expect(String(newMsg)).toContain('duplicate agents "dup2"');
556
+ } finally {
557
+ warnSpy.mockRestore();
558
+ }
559
+ } finally {
560
+ fs.rmSync(userAgentsDir, { recursive: true, force: true });
561
+ }
562
+ });
563
+
564
+ it("分级穷举:全部 8 个 ResourceSource 的机器/用户归属与 D3 一致", () => {
565
+ // 封闭枚举逐值断言,防止未来新增/修改枚举值时分级边界漂移
566
+ const machine: ResourceSource[] = ["npm", "npm-dev", "user-extension-paths", "project-pi", "project-pi-tmp", "project-agents"];
567
+ const user: ResourceSource[] = ["user-pi", "user-agents"];
568
+ for (const s of machine) expect(isMachineSource(s), `${s} 应为机器源`).toBe(true);
569
+ for (const s of user) expect(isMachineSource(s), `${s} 应为用户源`).toBe(false);
570
+ });
571
+
572
+ it("(d) cap 清空行为:超限后 clear 再 add,之前报过的 key 可重新报", async () => {
573
+ const userAgentsDir = path.join(mockHomeDir, ".agents", "agents");
574
+ fs.mkdirSync(userAgentsDir, { recursive: true });
575
+ try {
576
+ writeFile(path.join(agentDir, "agents"), "dup.md", "user-pi-body");
577
+ fs.writeFileSync(path.join(userAgentsDir, "dup.md"), "user-agents-body", "utf-8");
578
+
579
+ const warnSpy = vi.spyOn(getLogger("subagents"), "warn");
580
+
581
+ try {
582
+ // 步骤 1:首次调用——报 warn,dedup key 加入 set
583
+ await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
584
+ expect(warnSpy).toHaveBeenCalledTimes(1);
585
+ warnSpy.mockClear();
586
+
587
+ // 步骤 2:重置 set,注入 1024 个虚拟 key(不含 dedup key)
588
+ // 使 set.size = MAX,dedup key 不在 set 中
589
+ __testResetShadowDedup();
590
+ for (let i = 0; i < 1024; i++) {
591
+ __testInjectShadowDedupKey(`fake|key${i}|/a|/b`);
592
+ }
593
+
594
+ // 步骤 3:第二次调用——dedup key 不在 set → 进 else 分支 →
595
+ // size(1024) >= MAX(1024) → clear() → set 空 → add dedup key → warn
596
+ await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
597
+ expect(warnSpy).toHaveBeenCalledTimes(1);
598
+ warnSpy.mockClear();
599
+
600
+ // 步骤 4:第三次调用——dedup key 在 set 中 → 去重跳过 warn
601
+ await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
602
+ expect(warnSpy).not.toHaveBeenCalled();
603
+
604
+ // 步骤 5:再次填充到 cap——重置 + 注入 1024 个虚拟 key
605
+ // dedup key(步骤 3 add 的)已被 reset 清除
606
+ __testResetShadowDedup();
607
+ for (let i = 0; i < 1024; i++) {
608
+ __testInjectShadowDedupKey(`fake2|key${i}|/a|/b`);
609
+ }
610
+
611
+ // 步骤 6:第四次调用——cap 再次触发 clear → dedup key 重新报
612
+ await discoverResources({ kind: "agents", workspaceRoot: ws, hostRoots: hostRootsFor(agentDir, "agents") });
613
+ expect(warnSpy).toHaveBeenCalledTimes(1);
614
+ } finally {
615
+ warnSpy.mockRestore();
616
+ __testResetShadowDedup();
617
+ }
618
+ } finally {
619
+ fs.rmSync(userAgentsDir, { recursive: true, force: true });
620
+ }
621
+ });
622
+ });
623
+
624
+ // ── m5 TC4/TC5: 统一 mtime 缓存层(P-cache / P-cache-invalidation) ──
625
+
626
+ describe("m5: 统一 mtime 缓存层", () => {
627
+ it("TC4a: P-cache——文件未变时二次读取命中同一缓存条目(对象引用)", () => {
628
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "m5-cache-"));
629
+ const f = path.join(dir, "a.md");
630
+ fs.writeFileSync(f, "---\nname: x\ndescription: y\n---\nbody", "utf-8");
631
+ try {
632
+ // ESM 下 node:fs 不可 spy(vitest 限制)——命中断言用对象引用:
633
+ // getCachedFile 命中时返回缓存条目对象本身(非新建)
634
+ const first = getCachedFile(f);
635
+ const second = getCachedFile(f);
636
+ expect(first?.content).toBe("---\nname: x\ndescription: y\n---\nbody");
637
+ expect(second).toBe(first); // 同引用 = 命中缓存(未重 read)
638
+ } finally {
639
+ fs.rmSync(dir, { recursive: true, force: true });
640
+ }
641
+ });
642
+
643
+ it("TC4b: P-cache——两处消费方(getCachedFile/getCachedFileContent)共享同一缓存条目", () => {
644
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "m5-cache2-"));
645
+ const f = path.join(dir, "a.md");
646
+ fs.writeFileSync(f, "---\nname: x\ndescription: y\n---\nbody", "utf-8");
647
+ try {
648
+ const a = getCachedFile(f);
649
+ const b = getCachedFileContent(f);
650
+ // 内容一致性(命中语义由 TC4a 的对象引用断言覆盖——此处验证两 API 一致)
651
+ expect(a?.content).toBe(b);
652
+ } finally {
653
+ fs.rmSync(dir, { recursive: true, force: true });
654
+ }
655
+ });
656
+
657
+ it("TC5: P-cache-invalidation——改内容(mtime 变)后重读新内容;删文件后驱逐", () => {
658
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "m5-cache3-"));
659
+ const f = path.join(dir, "a.md");
660
+ fs.writeFileSync(f, "v1", "utf-8");
661
+ try {
662
+ expect(getCachedFileContent(f)).toBe("v1");
663
+ fs.writeFileSync(f, "v2", "utf-8");
664
+ expect(getCachedFileContent(f)).toBe("v2"); // mtime 变 → 重读
665
+ // 删除 → 驱逐(不再返回旧内容)
666
+ fs.rmSync(f);
667
+ expect(getCachedFileContent(f)).toBeNull();
668
+ } finally {
669
+ fs.rmSync(dir, { recursive: true, force: true });
670
+ }
671
+ });
672
+ });
673
+
674
+ // ── KV-cache 稳定性改造:解析结果缓存 getCachedParsed ──
675
+
676
+ describe("getCachedParsed(mtime 级解析缓存)", () => {
677
+ beforeEach(() => {
678
+ clearFileCache();
679
+ });
680
+
681
+ it("mtime 未变时 parse 只跑一次(缓存解析结果)", () => {
682
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "parsed-cache-"));
683
+ const f = path.join(dir, "a.md");
684
+ fs.writeFileSync(f, "---\nname: x\ndescription: y\n---", "utf-8");
685
+ try {
686
+ const parse = vi.fn((content: string) => (content.includes("name: x") ? "OK" : "BAD"));
687
+ const first = getCachedParsed(f, parse);
688
+ const second = getCachedParsed(f, parse);
689
+ expect(first).toBe("OK");
690
+ expect(second).toBe("OK");
691
+ expect(parse).toHaveBeenCalledTimes(1); // 第二次命中缓存,不重 parse
692
+ } finally {
693
+ fs.rmSync(dir, { recursive: true, force: true });
694
+ }
695
+ });
696
+
697
+ it("mtime 变后重新 parse;文件删除后返回 null 并驱逐", () => {
698
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "parsed-cache2-"));
699
+ const f = path.join(dir, "a.md");
700
+ fs.writeFileSync(f, "v1", "utf-8");
701
+ try {
702
+ const parse = (content: string) => content;
703
+ expect(getCachedParsed(f, parse)).toBe("v1");
704
+ fs.writeFileSync(f, "v2", "utf-8");
705
+ expect(getCachedParsed(f, parse)).toBe("v2"); // mtime 变 → 重新 parse
706
+ fs.rmSync(f);
707
+ expect(getCachedParsed(f, parse)).toBeNull(); // 删除 → null
708
+ } finally {
709
+ fs.rmSync(dir, { recursive: true, force: true });
710
+ }
711
+ });
712
+
713
+ it("clearFileCache 同时清空解析缓存", () => {
714
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "parsed-cache3-"));
715
+ const f = path.join(dir, "a.md");
716
+ fs.writeFileSync(f, "content", "utf-8");
717
+ try {
718
+ const parse = vi.fn(() => "OK");
719
+ getCachedParsed(f, parse);
720
+ clearFileCache();
721
+ getCachedParsed(f, parse);
722
+ expect(parse).toHaveBeenCalledTimes(2); // 缓存被清 → 重新 parse
723
+ } finally {
724
+ fs.rmSync(dir, { recursive: true, force: true });
725
+ }
726
+ });
727
+
728
+ it("同一 path 的不同 parse 各自独立缓存(缓存键含 parse 身份,防跨 parse 污染)", () => {
729
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "parsed-cache4-"));
730
+ const f = path.join(dir, "a.md");
731
+ fs.writeFileSync(f, "shared-content", "utf-8");
732
+ try {
733
+ // 模拟真实双 parse 场景:parseAgentFrontmatter vs parseWorkflowMeta 对同一
734
+ // path(agent 与 workflow 发现源理论上可命中同一路径)各自解析
735
+ const parseA = (content: string) => ({ kind: "agent" as const, content });
736
+ const parseW = (content: string) => ({ kind: "workflow" as const, len: content.length });
737
+ const a1 = getCachedParsed(f, parseA);
738
+ // 修复前:缓存键只有 path,这里会命中 parseA 的缓存条目并 as T 断言返回
739
+ // {kind:"agent"}——w1 被污染成错误类型
740
+ const w1 = getCachedParsed(f, parseW);
741
+ const a2 = getCachedParsed(f, parseA);
742
+ expect(a1).toEqual({ kind: "agent", content: "shared-content" });
743
+ expect(w1).toEqual({ kind: "workflow", len: 14 });
744
+ expect(a2).toEqual({ kind: "agent", content: "shared-content" });
745
+ } finally {
746
+ fs.rmSync(dir, { recursive: true, force: true });
747
+ }
748
+ });
749
+ });