@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,445 @@
1
+ // src/__tests__/spawn-args.test.ts
2
+ import { spawnSync } from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+
7
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
8
+
9
+ import { mirrorMainProcessFlags } from "../argv-mirror.ts";
10
+ import { MAX_FORK_DEPTH } from "../session-context-resolver.ts";
11
+ import { buildEnvBlock, buildSpawnArgs } from "../session-runner.ts";
12
+
13
+ describe("buildSpawnArgs", () => {
14
+ // [U1 D2] modelRef 为必填(spawn 前置守卫:未经裁决的裸字符串类型层面不可达)。
15
+ const baseParams = {
16
+ modelRef: { provider: "openai", id: "gpt-4o" },
17
+ thinkingLevel: undefined as string | undefined,
18
+ agentTools: undefined as string[] | undefined,
19
+ appendSystemPromptPath: undefined as string | undefined,
20
+ sessionDir: "/sessions/dir",
21
+ forkSource: undefined as string | undefined,
22
+ skillPaths: undefined as string[] | undefined,
23
+ };
24
+
25
+ it("基础参数:--mode rpc --session-dir + --model provider/id,不含 -p 也不含 task(task 经 stdin 传)", () => {
26
+ const args = buildSpawnArgs(baseParams);
27
+ expect(args).toEqual([
28
+ "--mode", "rpc", "--session-dir", "/sessions/dir", "--model", "openai/gpt-4o",
29
+ ]);
30
+ });
31
+
32
+ it("不含 -p / --print(rpc mode 下 -p 被 resolveAppMode 无视,是死代码)", () => {
33
+ const args = buildSpawnArgs(baseParams);
34
+ expect(args).not.toContain("-p");
35
+ expect(args).not.toContain("--print");
36
+ });
37
+
38
+ it("modelRef → --model 值恒为 `${provider}/${id}`(全等拼接,不重写输入)", () => {
39
+ // 含大小写差异的 provider/id 原样拼接(大小写敏感规则:放行即全等回显)
40
+ const args = buildSpawnArgs({
41
+ ...baseParams,
42
+ modelRef: { provider: "zai-coding-cn", id: "GLM-5.3-Flash" },
43
+ });
44
+ const idx = args.indexOf("--model");
45
+ expect(args[idx + 1]).toBe("zai-coding-cn/GLM-5.3-Flash");
46
+ });
47
+
48
+ it("modelRef + thinkingLevel → model 后缀 :level", () => {
49
+ const args = buildSpawnArgs(
50
+ { ...baseParams, thinkingLevel: "high" },
51
+ );
52
+ const idx = args.indexOf("--model");
53
+ expect(args[idx + 1]).toBe("openai/gpt-4o:high");
54
+ });
55
+
56
+ it("agentTools → --tools 逗号分隔", () => {
57
+ const args = buildSpawnArgs(
58
+ { ...baseParams, agentTools: ["read", "bash", "edit"] },
59
+ );
60
+ const idx = args.indexOf("--tools");
61
+ expect(args[idx + 1]).toBe("read,bash,edit");
62
+ });
63
+
64
+ it("appendSystemPromptPath → --append-system-prompt <path>", () => {
65
+ const args = buildSpawnArgs(
66
+ { ...baseParams, appendSystemPromptPath: "/tmp/prompt.md" },
67
+ );
68
+ const idx = args.indexOf("--append-system-prompt");
69
+ expect(args[idx + 1]).toBe("/tmp/prompt.md");
70
+ });
71
+
72
+ it("forkSource → --fork <path>", () => {
73
+ const args = buildSpawnArgs(
74
+ { ...baseParams, forkSource: "/sessions/parent.jsonl" },
75
+ );
76
+ const idx = args.indexOf("--fork");
77
+ expect(args[idx + 1]).toBe("/sessions/parent.jsonl");
78
+ });
79
+
80
+ it("skillPaths 多个 → 每个 push --skill <path>", () => {
81
+ const args = buildSpawnArgs(
82
+ { ...baseParams, skillPaths: ["/skills/a", "/skills/b", "/skills/c"] },
83
+ );
84
+ // 三个 --skill token,后跟各自路径,顺序保留
85
+ const skillIdxs = args
86
+ .map((a, i) => (a === "--skill" ? i : -1))
87
+ .filter((i) => i >= 0);
88
+ expect(skillIdxs).toHaveLength(3);
89
+ expect(args[skillIdxs[0] + 1]).toBe("/skills/a");
90
+ expect(args[skillIdxs[1] + 1]).toBe("/skills/b");
91
+ expect(args[skillIdxs[2] + 1]).toBe("/skills/c");
92
+ });
93
+
94
+ it("skillPaths 空数组 → 不含 --skill", () => {
95
+ const args = buildSpawnArgs(
96
+ { ...baseParams, skillPaths: [] },
97
+ );
98
+ expect(args).not.toContain("--skill");
99
+ });
100
+
101
+ it("skillPaths undefined → 不含 --skill", () => {
102
+ const args = buildSpawnArgs(baseParams);
103
+ expect(args).not.toContain("--skill");
104
+ });
105
+
106
+ it("全参数组合:所有 flag 存在,不含 -p 也不含 positional task", () => {
107
+ const args = buildSpawnArgs(
108
+ {
109
+ ...baseParams,
110
+ thinkingLevel: "low",
111
+ agentTools: ["read"],
112
+ appendSystemPromptPath: "/tmp/p.md",
113
+ sessionDir: "/s",
114
+ forkSource: "/parent.jsonl",
115
+ skillPaths: ["/skills/x"],
116
+ },
117
+ );
118
+ // 末尾应是最后一个 --skill 的路径(task 不再作为 positional arg 出现)
119
+ expect(args[args.length - 1]).toBe("/skills/x");
120
+ expect(args).toContain("--fork");
121
+ expect(args).toContain("--tools");
122
+ expect(args).toContain("--skill");
123
+ expect(args).not.toContain("-p");
124
+ });
125
+
126
+ it("空 tools 数组不追加 --tools", () => {
127
+ const args = buildSpawnArgs(
128
+ { ...baseParams, agentTools: [] },
129
+ );
130
+ expect(args).not.toContain("--tools");
131
+ });
132
+
133
+ it("含未注册扩展工具名(如 ask_user)正常透传 — 子进程 Pi 静默忽略未注册的 allowlist 项", () => {
134
+ // 场景:orchestrator 模板声明了 ask_user,但用户环境未装 pi-ask-user 扩展。
135
+ // 期望:subagent-workflow 不做特殊处理,仅原样透传给 pi CLI;
136
+ // 静默兼容的责任在 Pi(args.ts 不校验、agent-session 的 _rebuildSystemPrompt
137
+ // 对未注册工具名静默过滤)。本测试钉死这个透传行为,避免有人擅自加
138
+ // 「未注册工具检测」导致原本兼容的场景崩溃。
139
+ const args = buildSpawnArgs({
140
+ ...baseParams,
141
+ agentTools: ["todo", "goal_control", "workflow", "subagent", "ask_user"],
142
+ });
143
+ const idx = args.indexOf("--tools");
144
+ expect(idx).toBeGreaterThanOrEqual(0);
145
+ expect(args[idx + 1]).toBe(
146
+ "todo,goal_control,workflow,subagent,ask_user",
147
+ );
148
+ });
149
+
150
+ // ============================================================
151
+ // mirrorFlags 透传:子进程镜像主进程 extension/approve flag
152
+ // ============================================================
153
+
154
+ it("mirrorFlags 透传:noExtensions+approve+noContextFiles+extensionPaths 全量 push(TC5)", () => {
155
+ const args = buildSpawnArgs({
156
+ ...baseParams,
157
+ mirrorFlags: { noExtensions: true, approve: true, noContextFiles: true, extensionPaths: ["/e1", "/e2"] },
158
+ });
159
+ expect(args).toContain("--no-extensions");
160
+ expect(args).toContain("--approve");
161
+ expect(args).toContain("--no-context-files");
162
+ // 每个 extension 独立 token,顺序保留
163
+ const extIdxs = args.map((a, i) => (a === "--extension" ? i : -1)).filter((i) => i >= 0);
164
+ expect(extIdxs).toHaveLength(2);
165
+ expect(args[extIdxs[0] + 1]).toBe("/e1");
166
+ expect(args[extIdxs[1] + 1]).toBe("/e2");
167
+ });
168
+
169
+ it("mirrorFlags 全 false/空 → 不追加任何目标 flag(TC6)", () => {
170
+ const args = buildSpawnArgs({
171
+ ...baseParams,
172
+ mirrorFlags: { noExtensions: false, approve: false, noContextFiles: false, extensionPaths: [] },
173
+ });
174
+ expect(args).not.toContain("--no-extensions");
175
+ expect(args).not.toContain("--approve");
176
+ expect(args).not.toContain("--extension");
177
+ expect(args).not.toContain("--no-context-files");
178
+ // 仅基础参数
179
+ expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir", "--model", "openai/gpt-4o"]);
180
+ });
181
+
182
+ it("mirrorFlags undefined → 行为等同旧版(TC7)", () => {
183
+ const args = buildSpawnArgs(baseParams);
184
+ expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir", "--model", "openai/gpt-4o"]);
185
+ expect(args).not.toContain("--extension");
186
+ expect(args).not.toContain("--no-extensions");
187
+ expect(args).not.toContain("--approve");
188
+ expect(args).not.toContain("--no-context-files");
189
+ });
190
+
191
+ it("noContextFiles 镜像:主进程 argv 含 --no-context-files → 子进程 args 含该 flag", () => {
192
+ // 端到端链路断言:主 pi 进程以 --no-context-files 启动(用户 opt-out context files)
193
+ // → mirrorMainProcessFlags 解析 → buildSpawnArgs 产出同 flag 给每个 subagent。
194
+ // 缺此镜像时,--extension 镜像带入的 @zhushanwen/pi-system-prompt 检查子进程
195
+ // 自己的 argv(无 flag)→ 全局 AGENTS.md 注入在 subagent system prompt 照常生效,
196
+ // 用户 opt-out 被绕过。
197
+ const mainArgv = [
198
+ "bun", "/pi", "--mode", "rpc", "--no-context-files",
199
+ "--extension", "/staged/@zhushanwen/pi-system-prompt/index.js",
200
+ ];
201
+ const args = buildSpawnArgs({ ...baseParams, mirrorFlags: mirrorMainProcessFlags(mainArgv) });
202
+ expect(args).toContain("--no-context-files");
203
+ // extension 镜像照常(两者共存才构成完整契约)
204
+ const extIdx = args.indexOf("--extension");
205
+ expect(args[extIdx + 1]).toBe("/staged/@zhushanwen/pi-system-prompt/index.js");
206
+ });
207
+
208
+ it("noContextFiles 不镜像:主进程 argv 不含 flag → 子进程 args 不含(默认行为不变)", () => {
209
+ const mainArgv = ["bun", "/pi", "--mode", "rpc"];
210
+ const args = buildSpawnArgs({ ...baseParams, mirrorFlags: mirrorMainProcessFlags(mainArgv) });
211
+ expect(args).not.toContain("--no-context-files");
212
+ });
213
+
214
+ // ============================================================
215
+ // sessionFile(M1 resume 基建):--session <file> 紧跟 --session-dir
216
+ // ============================================================
217
+
218
+ it("sessionFile 存在 → 紧跟 --session-dir 追加 --session <file>", () => {
219
+ const args = buildSpawnArgs({
220
+ ...baseParams,
221
+ sessionFile: "/sessions/sub/abc.jsonl",
222
+ });
223
+ const idx = args.indexOf("--session");
224
+ expect(idx).toBeGreaterThan(-1);
225
+ expect(args[idx + 1]).toBe("/sessions/sub/abc.jsonl");
226
+ // 位置紧跟 --session-dir <dir> 之后(--model 之前)
227
+ const sessionDirIdx = args.indexOf("--session-dir");
228
+ expect(idx).toBe(sessionDirIdx + 2);
229
+ });
230
+
231
+ it("sessionFile undefined → 不含 --session(向后兼容)", () => {
232
+ const args = buildSpawnArgs(baseParams);
233
+ expect(args).not.toContain("--session");
234
+ expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir", "--model", "openai/gpt-4o"]);
235
+ });
236
+
237
+ it("sessionFile + modelRef + thinkingLevel → 三者都进 args(resume 全参数)", () => {
238
+ const args = buildSpawnArgs({
239
+ ...baseParams,
240
+ sessionFile: "/sessions/sub/resume.jsonl",
241
+ thinkingLevel: "high",
242
+ });
243
+ // --session <file>
244
+ const sessionIdx = args.indexOf("--session");
245
+ expect(sessionIdx).toBeGreaterThan(-1);
246
+ expect(args[sessionIdx + 1]).toBe("/sessions/sub/resume.jsonl");
247
+ // --model provider/id:level(thinkingLevel 作 model 后缀)
248
+ const modelIdx = args.indexOf("--model");
249
+ expect(modelIdx).toBeGreaterThan(-1);
250
+ expect(args[modelIdx + 1]).toBe("openai/gpt-4o:high");
251
+ });
252
+ });
253
+
254
+ // ============================================================
255
+ // mirrorMainProcessFlags:从主进程 argv 解析可镜像的 flag
256
+ // ============================================================
257
+
258
+ describe("mirrorMainProcessFlags", () => {
259
+ it("--extension 多次出现(空格分隔)+ 布尔 flag(TC1)", () => {
260
+ const r = mirrorMainProcessFlags([
261
+ "bun", "/pi", "--mode", "rpc", "--no-extensions", "--approve",
262
+ "--extension", "/a", "--extension", "/b",
263
+ ]);
264
+ expect(r).toEqual({ noExtensions: true, approve: true, extensionPaths: ["/a", "/b"], noContextFiles: false });
265
+ });
266
+
267
+ it("--extension=path 等号形式 + 短形式 -e/-ne/-a(TC2)", () => {
268
+ const r = mirrorMainProcessFlags([
269
+ "bun", "/pi", "--extension=/x", "-ne", "-a",
270
+ ]);
271
+ expect(r).toEqual({ noExtensions: true, approve: true, extensionPaths: ["/x"], noContextFiles: false });
272
+ });
273
+
274
+ it("混合形式(空格 + 等号),顺序保留(TC3)", () => {
275
+ const r = mirrorMainProcessFlags([
276
+ "bun", "/pi", "--extension", "/a", "--extension=/b", "--extension", "/c",
277
+ ]);
278
+ expect(r.extensionPaths).toEqual(["/a", "/b", "/c"]);
279
+ expect(r.noExtensions).toBe(false);
280
+ expect(r.approve).toBe(false);
281
+ expect(r.noContextFiles).toBe(false);
282
+ });
283
+
284
+ it("无目标 flag → 全空/全 false(向后兼容,TC4)", () => {
285
+ const r = mirrorMainProcessFlags(["bun", "/pi", "--mode", "rpc"]);
286
+ expect(r).toEqual({ noExtensions: false, approve: false, extensionPaths: [], noContextFiles: false });
287
+ });
288
+
289
+ it("--no-context-files 长形式解析 → noContextFiles: true", () => {
290
+ const r = mirrorMainProcessFlags([
291
+ "bun", "/pi", "--mode", "rpc", "--no-context-files",
292
+ "--extension", "/staged/@zhushanwen/pi-system-prompt/index.js",
293
+ ]);
294
+ expect(r.noContextFiles).toBe(true);
295
+ // 与 extension 镜像共存(实际 xyz-agent 启动形态:两 flag 同时出现)
296
+ expect(r.extensionPaths).toEqual(["/staged/@zhushanwen/pi-system-prompt/index.js"]);
297
+ expect(r.noExtensions).toBe(false);
298
+ expect(r.approve).toBe(false);
299
+ });
300
+
301
+ it("-nc 短形式解析 → noContextFiles: true(pi CLI 等价短形式)", () => {
302
+ const r = mirrorMainProcessFlags(["bun", "/pi", "--mode", "rpc", "-nc"]);
303
+ expect(r.noContextFiles).toBe(true);
304
+ });
305
+
306
+ it("不误吃其他 flag 值与 positional 参数(TC8)", () => {
307
+ const r = mirrorMainProcessFlags([
308
+ "bun", "/pi", "--no-extensions", "--skill", "/sk", "some prompt text",
309
+ ]);
310
+ expect(r.noExtensions).toBe(true);
311
+ expect(r.extensionPaths).toEqual([]);
312
+ // --skill 的 /sk 不混入 extensionPaths;positional prompt 被忽略
313
+ });
314
+
315
+ it("空 argv / 仅前导两项 → 全空", () => {
316
+ expect(mirrorMainProcessFlags([])).toEqual({ noExtensions: false, approve: false, extensionPaths: [], noContextFiles: false });
317
+ expect(mirrorMainProcessFlags(["bun", "/pi"])).toEqual({
318
+ noExtensions: false, approve: false, extensionPaths: [], noContextFiles: false,
319
+ });
320
+ });
321
+
322
+ it("--extension 末尾无值 → 跳过(不越界、不误吃下一个 token)", () => {
323
+ const r = mirrorMainProcessFlags(["bun", "/pi", "--extension"]);
324
+ expect(r.extensionPaths).toEqual([]);
325
+ });
326
+
327
+ it("-e 短形式多次 + 等号混用", () => {
328
+ const r = mirrorMainProcessFlags(["bun", "/pi", "-e", "/a", "-e=/b", "-e", "/c"]);
329
+ expect(r.extensionPaths).toEqual(["/a", "/b", "/c"]);
330
+ });
331
+
332
+ // [MF-7a] flag 判定收窄为 startsWith("--"):单 - 开头的合法路径不被误判为 flag。
333
+ it("[MF-7a] - 开头的路径被当值镜像(不误判 flag)", () => {
334
+ const r = mirrorMainProcessFlags(["bun", "/pi", "--extension", "-weird-dir/ext.js"]);
335
+ expect(r.extensionPaths).toEqual(["-weird-dir/ext.js"]);
336
+ });
337
+
338
+ // [MF-7a] -- 开头的真 flag 仍正确跳过:--extension 后跟 --flag 时不吃值、不越界。
339
+ it("[MF-7a] --extension 后跟 -- 开头真 flag → 不吃值", () => {
340
+ const r = mirrorMainProcessFlags([
341
+ "bun", "/pi", "--extension", "--approve", "--extension", "/real.js",
342
+ ]);
343
+ expect(r.extensionPaths).toEqual(["/real.js"]);
344
+ expect(r.approve).toBe(true);
345
+ });
346
+ });
347
+
348
+ // ============================================================
349
+ // buildEnvBlock(M1 恢复)
350
+ // ============================================================
351
+
352
+ describe("buildEnvBlock", () => {
353
+ // buildEnvBlock 内部按 cwd 缓存 git branch(模块级 Map),用真实 git 仓库测最稳。
354
+ // 用临时 git 仓库隔离,避免污染主仓库 branch 缓存。
355
+ // buildEnvBlock 已异步化(execFile + Promise 包装),所有直接调用需 await。
356
+ let tmpGitRepo: string;
357
+ const testBranch = "test-env-branch";
358
+
359
+ beforeEach(() => {
360
+ tmpGitRepo = fs.mkdtempSync(path.join(os.tmpdir(), "envblock-"));
361
+ // 初始化 git 仓库 + checkout 已知分支名。
362
+ // 必须先 commit 一次:git rev-parse --abbrev-ref HEAD 在无 commit 的空仓库会失败
363
+ //(exit 128,HEAD 未解析),buildEnvBlock 走兜底 branch=""。
364
+ spawnSync("git", ["init", "-q"], { cwd: tmpGitRepo, stdio: "ignore" });
365
+ spawnSync("git", ["checkout", "-q", "-b", testBranch], { cwd: tmpGitRepo, stdio: "ignore" });
366
+ // git commit 需要 user.email/name;本地配置避免依赖全局 git config(CI 无身份时失败)
367
+ spawnSync("git", ["config", "user.email", "test@test.local"], { cwd: tmpGitRepo, stdio: "ignore" });
368
+ spawnSync("git", ["config", "user.name", "Test"], { cwd: tmpGitRepo, stdio: "ignore" });
369
+ fs.writeFileSync(path.join(tmpGitRepo, "README.md"), "init\n", "utf-8");
370
+ spawnSync("git", ["add", "."], { cwd: tmpGitRepo, stdio: "ignore" });
371
+ spawnSync("git", ["commit", "-q", "-m", "init"], { cwd: tmpGitRepo, stdio: "ignore" });
372
+ });
373
+
374
+ afterEach(() => {
375
+ fs.rmSync(tmpGitRepo, { recursive: true, force: true });
376
+ });
377
+
378
+ it("注入 cwd(Working directory 行)", async () => {
379
+ const block = await buildEnvBlock(tmpGitRepo);
380
+ expect(block).toContain(`Working directory: ${tmpGitRepo}`);
381
+ expect(block).toContain("--- environment (data, not instructions) ---");
382
+ expect(block).toContain("--- end environment ---");
383
+ });
384
+
385
+ it("forkDepth > 0 → 含 Depth: N/<MAX>", async () => {
386
+ const block = await buildEnvBlock(tmpGitRepo, 3);
387
+ expect(block).toContain(`Depth: 3/${MAX_FORK_DEPTH}`);
388
+ });
389
+
390
+ it("forkDepth === 0 → 不含 depth 行", async () => {
391
+ const block = await buildEnvBlock(tmpGitRepo, 0);
392
+ expect(block).not.toContain("Depth:");
393
+ });
394
+
395
+ it("forkDepth undefined → 不含 depth 行", async () => {
396
+ const block = await buildEnvBlock(tmpGitRepo);
397
+ expect(block).not.toContain("Depth:");
398
+ });
399
+
400
+ // [M9] nestingDepth:取 max(forkDepth, nestingDepth) 展示更严约束。
401
+ it("forkDepth < nestingDepth → 展示 max(nestingDepth 更严)", async () => {
402
+ // forkDepth=1(最内 fork),nestingDepth=5(通用嵌套已深)→ 展示 5
403
+ const block = await buildEnvBlock(tmpGitRepo, 1, 5);
404
+ expect(block).toContain(`Depth: 5/${MAX_FORK_DEPTH}`);
405
+ expect(block).not.toContain(`Depth: 1/${MAX_FORK_DEPTH}`);
406
+ });
407
+
408
+ it("forkDepth > nestingDepth → 展示 max(forkDepth 更严)", async () => {
409
+ const block = await buildEnvBlock(tmpGitRepo, 7, 2);
410
+ expect(block).toContain(`Depth: 7/${MAX_FORK_DEPTH}`);
411
+ });
412
+
413
+ it("forkDepth=0 + nestingDepth>0 → 展示 nestingDepth(非 fork 嵌套也计入)", async () => {
414
+ // 非 fork 但有嵌套(如顶层 → 子 → 孙),nestingDepth=2 应展示
415
+ const block = await buildEnvBlock(tmpGitRepo, undefined, 2);
416
+ expect(block).toContain(`Depth: 2/${MAX_FORK_DEPTH}`);
417
+ });
418
+
419
+ it("git branch 存在 → 含 Git branch 行", async () => {
420
+ const block = await buildEnvBlock(tmpGitRepo);
421
+ expect(block).toContain(`Git branch: ${testBranch}`);
422
+ });
423
+
424
+ it("非 git 目录 → 不含 Git branch 行(git 失败兜底空串)", async () => {
425
+ const nonGitDir = fs.mkdtempSync(path.join(os.tmpdir(), "envblock-nogit-"));
426
+ try {
427
+ const block = await buildEnvBlock(nonGitDir);
428
+ expect(block).not.toContain("Git branch:");
429
+ // 但仍含 working directory(环境块始终输出)
430
+ expect(block).toContain(`Working directory: ${nonGitDir}`);
431
+ } finally {
432
+ fs.rmSync(nonGitDir, { recursive: true, force: true });
433
+ }
434
+ });
435
+
436
+ it("git 失败(execFile err 回调)→ 不崩,静默省略 branch", async () => {
437
+ // 旧版 spy 挂在测试本地对象字面量上(同名同步 git API 的对象包装),从未真正拦截
438
+ // session-runner 的模块绑定——此前通过是靠 /some/cwd 不存在使真实 git 失败的副作用。
439
+ // 异步化后改为显式用不存在的 cwd 触发 execFile err 回调(err → reject → catch →
440
+ // branch=""),覆盖同一条失败路径,断言不变。
441
+ const block = await buildEnvBlock("/some/cwd");
442
+ expect(block).not.toContain("Git branch:");
443
+ expect(block).toContain("Working directory: /some/cwd");
444
+ });
445
+ });
@@ -0,0 +1,189 @@
1
+ // src/__tests__/spawn-event-adapter-rpc.test.ts
2
+ //
3
+ // W1 红灯测试:spawn-event-adapter.ts 协议层重写 — Pi 原生格式 + method 平铺。
4
+ //
5
+ // 测试对象:extensions/universal/subagent-workflow/src/execution/spawn-event-adapter.ts(改造)
6
+ // 契约来源:.fix-plans/00-master-summary.md §二(统一接口契约)+ §一冲突 2/判定顺序
7
+ //
8
+ // 核心改动:
9
+ // 1. isExtensionUiRequest 重写:判 type==="extension_ui_request" + id:string + method:string
10
+ // (删 jsonrpc==="2.0" 守卫 + params 守卫 — Pi 原生格式是平铺字段)
11
+ // 2. extension_ui_request 分支:从 {id, params:Record} 改为 {id, request: ExtensionUiRequest}
12
+ // request 按 method 平铺(title/options/message/... 与 Pi rpc-types.ts 1:1)
13
+ // 3. isRpcResponse 重写:判 type==="response" + command:string + success:boolean(SR-1 根因 1b)
14
+ // 4. 判定顺序:isExtensionUiRequest 必须在 event 分支之前(否则被 typeof obj.type===string 吞)
15
+ //
16
+ // 红灯原因:spawn-event-adapter.ts 当前用旧 JSON-RPC 2.0 守卫,对 Pi 原生格式样本
17
+ // 无法识别为 extension_ui_request(会落到 event 分支)。改造后这些断言才绿。
18
+
19
+ import { describe, expect, it } from "vitest";
20
+
21
+ import { parseSpawnLine } from "../spawn-event-adapter.ts";
22
+
23
+ // ── Pi 原生协议样本构造(来源:rpc-types.ts L230-265 真实类型) ──────────
24
+ // 10 种 method,全部 {type:"extension_ui_request", id, method, ...平铺字段}。
25
+ // 注意:无 jsonrpc 字段、无 params 包裹、method 在顶层(非 method:"extension_ui_request")。
26
+
27
+ function piLine(methodFields: Record<string, unknown>): string {
28
+ return JSON.stringify({ type: "extension_ui_request", id: "req-001", ...methodFields });
29
+ }
30
+
31
+ describe("parseSpawnLine — 10 种 method 真实样本分类(Pi 原生格式)", () => {
32
+ describe("dialog 类(select/confirm/input/editor)", () => {
33
+ it("select → kind=extension_ui_request + request.method=select + title/options 平铺", () => {
34
+ const result = parseSpawnLine(piLine({
35
+ method: "select", title: "Pick one", options: ["a", "b"], timeout: 30000,
36
+ }));
37
+ expect(result?.kind).toBe("extension_ui_request");
38
+ if (result?.kind !== "extension_ui_request") return;
39
+ expect(result.id).toBe("req-001");
40
+ expect(result.request.method).toBe("select");
41
+ expect(result.request.title).toBe("Pick one");
42
+ expect(result.request.options).toEqual(["a", "b"]);
43
+ });
44
+
45
+ it("confirm → request.method=confirm + title/message 平铺", () => {
46
+ const result = parseSpawnLine(piLine({
47
+ method: "confirm", title: "Sure?", message: "Proceed?", timeout: 10000,
48
+ }));
49
+ expect(result?.kind).toBe("extension_ui_request");
50
+ if (result?.kind !== "extension_ui_request") return;
51
+ expect(result.request.method).toBe("confirm");
52
+ expect(result.request.title).toBe("Sure?");
53
+ expect(result.request.message).toBe("Proceed?");
54
+ });
55
+
56
+ it("input → request.method=input + title + placeholder", () => {
57
+ const result = parseSpawnLine(piLine({
58
+ method: "input", title: "Name", placeholder: "type here",
59
+ }));
60
+ expect(result?.kind).toBe("extension_ui_request");
61
+ if (result?.kind !== "extension_ui_request") return;
62
+ expect(result.request.method).toBe("input");
63
+ expect(result.request.placeholder).toBe("type here");
64
+ });
65
+
66
+ it("editor → request.method=editor + title + prefill", () => {
67
+ const result = parseSpawnLine(piLine({
68
+ method: "editor", title: "Edit", prefill: "initial text",
69
+ }));
70
+ expect(result?.kind).toBe("extension_ui_request");
71
+ if (result?.kind !== "extension_ui_request") return;
72
+ expect(result.request.method).toBe("editor");
73
+ expect(result.request.prefill).toBe("initial text");
74
+ });
75
+ });
76
+
77
+ describe("fire-and-forget 类(notify/setStatus/setWidget/setTitle/set_editor_text)", () => {
78
+ it("notify → request.method=notify + message + notifyType", () => {
79
+ const result = parseSpawnLine(piLine({
80
+ method: "notify", message: "hi", notifyType: "info",
81
+ }));
82
+ expect(result?.kind).toBe("extension_ui_request");
83
+ if (result?.kind !== "extension_ui_request") return;
84
+ expect(result.request.method).toBe("notify");
85
+ expect(result.request.message).toBe("hi");
86
+ });
87
+
88
+ it("setStatus → request.method=setStatus + statusKey/statusText", () => {
89
+ const result = parseSpawnLine(piLine({
90
+ method: "setStatus", statusKey: "k", statusText: "running",
91
+ }));
92
+ expect(result?.kind).toBe("extension_ui_request");
93
+ if (result?.kind !== "extension_ui_request") return;
94
+ expect(result.request.method).toBe("setStatus");
95
+ expect(result.request.statusKey).toBe("k");
96
+ });
97
+
98
+ it("setWidget → request.method=setWidget + widgetKey/widgetLines/widgetPlacement", () => {
99
+ const result = parseSpawnLine(piLine({
100
+ method: "setWidget", widgetKey: "w1",
101
+ widgetLines: ["line1"], widgetPlacement: "aboveEditor",
102
+ }));
103
+ expect(result?.kind).toBe("extension_ui_request");
104
+ if (result?.kind !== "extension_ui_request") return;
105
+ expect(result.request.method).toBe("setWidget");
106
+ expect(result.request.widgetKey).toBe("w1");
107
+ expect(result.request.widgetPlacement).toBe("aboveEditor");
108
+ });
109
+
110
+ it("setTitle → request.method=setTitle + title", () => {
111
+ const result = parseSpawnLine(piLine({ method: "setTitle", title: "My Title" }));
112
+ expect(result?.kind).toBe("extension_ui_request");
113
+ if (result?.kind !== "extension_ui_request") return;
114
+ expect(result.request.method).toBe("setTitle");
115
+ expect(result.request.title).toBe("My Title");
116
+ });
117
+
118
+ it("set_editor_text → request.method=set_editor_text + text", () => {
119
+ const result = parseSpawnLine(piLine({ method: "set_editor_text", text: "body" }));
120
+ expect(result?.kind).toBe("extension_ui_request");
121
+ if (result?.kind !== "extension_ui_request") return;
122
+ expect(result.request.method).toBe("set_editor_text");
123
+ expect(result.request.text).toBe("body");
124
+ });
125
+ });
126
+ });
127
+
128
+ describe("parseSpawnLine — 旧 JSON-RPC 2.0 格式不再被识别(守卫已删)", () => {
129
+ it("jsonrpc+method:extension_ui_request+params 旧格式 → 不归类为 extension_ui_request", () => {
130
+ // 旧格式(错误假设):会被识别为 event 或 invalid,因为新守卫不认 jsonrpc
131
+ const oldFormat = JSON.stringify({
132
+ jsonrpc: "2.0", id: "x", method: "extension_ui_request", params: { foo: "bar" },
133
+ });
134
+ const result = parseSpawnLine(oldFormat);
135
+ // 旧格式的 method 字段值是 "extension_ui_request",无 type 字段 → invalid
136
+ // (这是有意的:强制对齐 Pi 原生格式)
137
+ expect(result?.kind).not.toBe("extension_ui_request");
138
+ });
139
+ });
140
+
141
+ describe("parseSpawnLine — 判定顺序:isExtensionUiRequest 先于 event 分支", () => {
142
+ it("extension_ui_request 不被 typeof obj.type===string 吞为 event", () => {
143
+ // 关键 bug:extension_ui_request 有 type 字段,若 event 分支在前会被当 event
144
+ const result = parseSpawnLine(piLine({ method: "notify", message: "x" }));
145
+ // 必须是 extension_ui_request,不能是 event
146
+ expect(result?.kind).toBe("extension_ui_request");
147
+ expect(result?.kind).not.toBe("event");
148
+ });
149
+ });
150
+
151
+ describe("parseSpawnLine — RPC response(isRpcResponse 重写,SR-1 根因 1b)", () => {
152
+ it("Pi response 格式 {type:response, command, success} → kind=response", () => {
153
+ const result = parseSpawnLine(JSON.stringify({
154
+ type: "response", id: "r1", command: "run_tool", success: true,
155
+ }));
156
+ expect(result?.kind).toBe("response");
157
+ if (result?.kind !== "response") return;
158
+ expect(result.id).toBe("r1");
159
+ expect(result.command).toBe("run_tool");
160
+ expect(result.success).toBe(true);
161
+ });
162
+
163
+ it("Pi response 失败 {type:response, command, success:false, error}", () => {
164
+ const result = parseSpawnLine(JSON.stringify({
165
+ type: "response", command: "run_tool", success: false, error: "boom",
166
+ }));
167
+ expect(result?.kind).toBe("response");
168
+ if (result?.kind !== "response") return;
169
+ expect(result.success).toBe(false);
170
+ expect(result.error).toBe("boom");
171
+ });
172
+
173
+ it("response 含 data 字段 → 保留", () => {
174
+ const result = parseSpawnLine(JSON.stringify({
175
+ type: "response", command: "read", success: true, data: { out: 42 },
176
+ }));
177
+ expect(result?.kind).toBe("response");
178
+ if (result?.kind !== "response") return;
179
+ expect(result.data).toEqual({ out: 42 });
180
+ });
181
+
182
+ it("旧 JSON-RPC 2.0 response(jsonrpc+id+result)→ 不再归类为 response", () => {
183
+ const oldRpc = JSON.stringify({
184
+ jsonrpc: "2.0", id: "x", result: { answers: [] },
185
+ });
186
+ const result = parseSpawnLine(oldRpc);
187
+ expect(result?.kind).not.toBe("response");
188
+ });
189
+ });