@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,169 @@
1
+ // src/execution/__tests__/format-schema-instruction.test.ts
2
+ //
3
+ // 锁定 formatSchemaInstruction 契约:构造 schema enforcement 的 MANDATORY 指令。
4
+ // [审查项#2] 注入链变更:指令仅经 agent-opts-resolver 进 appendSystemPrompt
5
+ // (ASP 单点),runSpawn 的 task + instruction 后缀拼接已删除——本测试随之改锁
6
+ // resolver 导出的同名函数。一旦漏掉 "structured-output" 关键词或 JSON 序列化漂移,
7
+ // schema 模式会静默失效——agent 可能直接把 JSON 写进文本响应。
8
+ //
9
+ // [审查项#4] 锁定 AP 告知句:注入侧校验用 additionalProperties:false 收窄后的
10
+ // parameters,指令必须前置告知「schema 外字段被拒」,否则拒绝显得凭空。
11
+ //
12
+ // 纯函数测试:不依赖 Pi 运行时、不 spawn 进程、不 mock。只 import 被测函数。
13
+ import { describe, expect, it } from "vitest";
14
+
15
+ import { formatSchemaInstruction } from "../../orchestration/agent-opts-resolver.ts";
16
+
17
+ describe("formatSchemaInstruction", () => {
18
+ // ── 指令文本契约 ──────────────────────────────────────────────
19
+
20
+ it("contains the structured-output tool keyword", () => {
21
+ const out = formatSchemaInstruction({ type: "object" });
22
+ expect(out).toContain("structured-output");
23
+ });
24
+
25
+ it("emits a MANDATORY structured-output directive (not free-form JSON)", () => {
26
+ const out = formatSchemaInstruction({ type: "object" });
27
+ expect(out).toContain("MANDATORY");
28
+ expect(out).toContain("must be calling the `structured-output` tool");
29
+ expect(out).toContain("Do NOT output JSON in your text response");
30
+ });
31
+
32
+ it("announces that fields outside the schema are rejected (AP 告知,审查项#4)", () => {
33
+ const out = formatSchemaInstruction({ type: "object" });
34
+ expect(out).toContain("Fields not defined in this schema are rejected");
35
+ expect(out).toContain("do not add extra fields");
36
+ });
37
+
38
+ it("author-declared additionalProperties → weak wording (AP 条件化:声明时作者 schema 管辖额外字段)", () => {
39
+ // D4 只在「未声明」时注入 false;作者显式声明 true 时额外字段实际放行——
40
+ // 无条件「一律拒绝」强承诺与参数层行为不符,改用弱承诺措辞。
41
+ const out = formatSchemaInstruction({ type: "object", additionalProperties: true });
42
+ expect(out).not.toContain("Fields not defined in this schema are rejected");
43
+ expect(out).toContain("follow this schema's own additionalProperties declaration");
44
+ });
45
+
46
+ // ── schema 序列化(compact:与 schemaEnv 复用同串,IF7 #13)───────
47
+
48
+ it("embeds the schema as compact JSON inside a fenced block", () => {
49
+ const schema: Record<string, unknown> = {
50
+ type: "object",
51
+ properties: { name: { type: "string" } },
52
+ };
53
+ const out = formatSchemaInstruction(schema);
54
+ // 必须包含 JSON.stringify(schema) 的完整结果(compact,无缩进)
55
+ expect(out).toContain(JSON.stringify(schema));
56
+ expect(out).toContain("```json");
57
+ expect(out).toContain("```");
58
+ });
59
+
60
+ it("locks the full output structure for a minimal schema", () => {
61
+ const out = formatSchemaInstruction({ type: "object" });
62
+ // 完整结构快照——任何指令措辞/顺序/序列化漂移都会被捕获。
63
+ // [HISTORICAL] runner task 后缀双重注入删除后,本函数迁至 resolver 成为唯一
64
+ // 文案源;JSON 从 pretty(indent=2)改为 compact(与 schemaEnv 同串复用)。
65
+ expect(out).toBe(
66
+ [
67
+ "## MANDATORY: Structured Output Requirement",
68
+ "",
69
+ "This task requires structured output.",
70
+ "Your FINAL action must be calling the `structured-output` tool.",
71
+ "",
72
+ "Your call arguments ARE the result data itself — the tool's parameter schema IS the required shape of your result.",
73
+ "Your result must conform to this schema:",
74
+ "```json",
75
+ '{"type":"object"}',
76
+ "```",
77
+ "",
78
+ "Rules:",
79
+ "- Call the structured-output tool with your result data as its arguments. The system validates them against the schema above automatically.",
80
+ "- Do NOT output JSON in your text response — use the structured-output tool.",
81
+ "- Do NOT skip this step. The structured-output call IS your result.",
82
+ "- Complete all other work FIRST, then call structured-output as the last action.",
83
+ "- Fields not defined in this schema are rejected — do not add extra fields.",
84
+ ].join("\n"),
85
+ );
86
+ });
87
+
88
+ it("locks the single-parameter wording: arguments ARE the data coexists with the mandatory tool call", () => {
89
+ const out = formatSchemaInstruction({ type: "object" });
90
+ // 新口径双支柱必须共存:① 参数即数据(arguments ARE the data)② 必须调用工具
91
+ expect(out).toContain("arguments ARE the result data itself");
92
+ expect(out).toContain("parameter schema IS the required shape of your result");
93
+ expect(out).toContain("must be calling the `structured-output` tool");
94
+ // 旧双参数警告语义不得回流(工具已是单参数形态,警告失去对象)
95
+ expect(out).not.toMatch(/do NOT pass a .schema. parameter/i);
96
+ expect(out).not.toContain("ONLY the `data` parameter");
97
+ expect(out).not.toContain("enforced by the system");
98
+ });
99
+
100
+ // ── 特殊字符转义(注入风险路径)──────────────────────────────
101
+
102
+ it("escapes double quotes inside schema string values", () => {
103
+ const schema: Record<string, unknown> = { prompt: 'say "hi"' };
104
+ const out = formatSchemaInstruction(schema);
105
+ // JSON.stringify 会把内层 " 转义为 \"
106
+ expect(out).toContain('say \\"hi\\"');
107
+ // 原始未转义形式(含成对字面双引号)绝不能回流进 JSON 体内
108
+ expect(out).not.toContain('say "hi"');
109
+ });
110
+
111
+ it("escapes newlines inside schema string values", () => {
112
+ const schema: Record<string, unknown> = { text: "line1\nline2" };
113
+ const out = formatSchemaInstruction(schema);
114
+ // 换行被序列化为字面反斜杠-n,不能是真实换行符
115
+ expect(out).toContain("line1\\nline2");
116
+ expect(out).not.toContain("line1\nline2");
117
+ });
118
+
119
+ it("escapes backslashes inside schema string values", () => {
120
+ const schema: Record<string, unknown> = { path: "C:\\Users\\x" };
121
+ const out = formatSchemaInstruction(schema);
122
+ // 单反斜杠被序列化为 \\,避免后续解析误把转义序列当指令
123
+ expect(out).toContain("C:\\\\Users\\\\x");
124
+ expect(out).not.toContain("C:\\Users\\x");
125
+ });
126
+
127
+ // ── 边界值 ────────────────────────────────────────────────────
128
+
129
+ it("handles empty schema object", () => {
130
+ const out = formatSchemaInstruction({});
131
+ expect(out).toContain("structured-output");
132
+ expect(out).toContain("{}");
133
+ });
134
+
135
+ it("preserves null values in schema", () => {
136
+ const schema: Record<string, unknown> = { default: null };
137
+ const out = formatSchemaInstruction(schema);
138
+ // JSON.stringify 对 null 保留字面 "null"(不会 omit 键,也不会变字符串)。
139
+ // compact 序列化无空格:'"default":null'
140
+ expect(out).toContain('"default":null');
141
+ });
142
+
143
+ it("serializes nested objects and arrays", () => {
144
+ const schema: Record<string, unknown> = {
145
+ type: "object",
146
+ required: ["name", "age"],
147
+ properties: {
148
+ name: { type: "string" },
149
+ age: { type: "integer", minimum: 0 },
150
+ },
151
+ };
152
+ const out = formatSchemaInstruction(schema);
153
+ expect(out).toContain(JSON.stringify(schema));
154
+ });
155
+
156
+ // ── 确定性 ────────────────────────────────────────────────────
157
+
158
+ it("is deterministic — same schema produces identical output", () => {
159
+ const schema: Record<string, unknown> = { a: 1, b: [2, 3] };
160
+ expect(formatSchemaInstruction(schema)).toBe(formatSchemaInstruction(schema));
161
+ });
162
+
163
+ it("is deterministic across different object key insertion (value-equal schemas)", () => {
164
+ // JSON.stringify 按对象自身属性顺序序列化;同序构造的等价 schema 应产出相同指令
165
+ const a: Record<string, unknown> = { x: 1, y: 2 };
166
+ const b: Record<string, unknown> = { x: 1, y: 2 };
167
+ expect(formatSchemaInstruction(a)).toBe(formatSchemaInstruction(b));
168
+ });
169
+ });
@@ -0,0 +1,184 @@
1
+ // src/execution/__tests__/gc-timer.test.ts
2
+ //
3
+ // [M8] idle record GC 定时器测试(subagent-service.ts startGcTimer,L485-507)。
4
+ //
5
+ // 背景:GC 定时器(1h interval 扫描 + isResumable && idleSince 30 天 TTL + store.archive)
6
+ // 此前零测试覆盖——所有相关测试把 startGcTimer mock 成 no-op(index-session-start /
7
+ // crash-recovery / stream-sink-guard / session-start-reaper / index-session-start-identity)。
8
+ // 误判 isResumable/idleSince 会把可续聊 record 从内存错误归档(用户数据路径),
9
+ // 或永不触发(内存驻留失效)——两个方向都无回归拦截。
10
+ //
11
+ // 本文件用真实 SubagentService + 真实 startGcTimer(不 mock GC 逻辑本身),仅 mock
12
+ // session-runner(import 链依赖 + isResumable 的 hasLiveProcessHandle 查询点)与 logger。
13
+ // timer 全部 vi.useFakeTimers,不真实等待。
14
+
15
+ import type { ChildProcess } from "node:child_process";
16
+ import * as fs from "node:fs";
17
+ import * as os from "node:os";
18
+ import * as path from "node:path";
19
+
20
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
21
+
22
+ const { loggerMock } = vi.hoisted(() => ({
23
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
24
+ }));
25
+ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
26
+
27
+ // mock session-runner:runSpawn/killAllSpawnedChildren 占位(import 链需要),
28
+ // getChildByRecord 默认返回 undefined(无活进程 → isResumable=true),用例内按需覆盖。
29
+ vi.mock("../session-runner.ts", () => ({
30
+ runSpawn: vi.fn(),
31
+ killAllSpawnedChildren: vi.fn(),
32
+ getChildByRecord: vi.fn(() => undefined),
33
+ spawnedChildren: new Map(),
34
+ }));
35
+
36
+ import { getChildByRecord } from "../session-runner.ts";
37
+ import { createRecord } from "../execution-record.ts";
38
+ import { ModelConfigService } from "../model-config-service.ts";
39
+ import { RecordStore } from "../record-store.ts";
40
+ import { SubagentService } from "../subagent-service.ts";
41
+ import type { ExecutionRecord } from "../types.ts";
42
+
43
+ const mockGetChildByRecord = vi.mocked(getChildByRecord);
44
+
45
+ /** 与 startGcTimer 内部常量一致(1h 扫描 / 30 天 TTL)。 */
46
+ const GC_INTERVAL_MS = 60 * 60 * 1000;
47
+ const IDLE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
48
+
49
+ /** initSession 注入的最小 pi duck-type(同 subagent-service PiLike 形状,结构匹配即可)。 */
50
+ interface PiStub {
51
+ appendEntry(customType: string, data?: unknown): void;
52
+ events: { emit(channel: string, data: unknown): void };
53
+ sendMessage(
54
+ message: { customType: string; content: string; display: boolean; details?: unknown },
55
+ options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
56
+ ): void;
57
+ }
58
+
59
+ function makePi(): PiStub {
60
+ return {
61
+ appendEntry: vi.fn(),
62
+ events: { emit: vi.fn() },
63
+ sendMessage: vi.fn(),
64
+ };
65
+ }
66
+
67
+ /** 构造 running + 无活进程 + 指定 idleSince 的 record(GC 扫描的目标态)。
68
+ * listAllActive 只返回 status==="running"(createRecord 初始即 running)。 */
69
+ function makeIdleRecord(id: string, idleSince: number): ExecutionRecord {
70
+ const record = createRecord(id, {
71
+ agent: "general-purpose",
72
+ model: "test/model",
73
+ mode: "background",
74
+ task: "test",
75
+ slug: "test",
76
+ startedAt: idleSince,
77
+ rootSessionId: "root-session",
78
+ chatMode: true,
79
+ controller: new AbortController(),
80
+ });
81
+ record.idleSince = idleSince;
82
+ return record;
83
+ }
84
+
85
+ /** 暴露私有字段供测试读取 store。 */
86
+ interface ServiceInternals {
87
+ store: RecordStore;
88
+ }
89
+
90
+ describe("[M8] idle record GC 定时器(startGcTimer)", () => {
91
+ let agentDir: string;
92
+ let service: SubagentService;
93
+ let store: RecordStore;
94
+
95
+ beforeEach(() => {
96
+ vi.useFakeTimers();
97
+ agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "gc-timer-"));
98
+ const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
99
+ service = new SubagentService({ cwd: agentDir, modelService });
100
+ service.initSession({ pi: makePi(), sessionId: "root-session" });
101
+ store = (service as unknown as ServiceInternals).store;
102
+ mockGetChildByRecord.mockReset();
103
+ // 默认无活进程(isResumable=true)——Path B:进程已回收、可冷路径 resume 的等待续聊态
104
+ mockGetChildByRecord.mockImplementation(() => undefined);
105
+ loggerMock.warn.mockClear();
106
+ });
107
+
108
+ afterEach(() => {
109
+ service.dispose();
110
+ fs.rmSync(agentDir, { recursive: true, force: true });
111
+ vi.useRealTimers();
112
+ });
113
+
114
+ it("TTL 边界:超 1ms 归档、差 1ms 不归档(同一轮扫描)", () => {
115
+ // idleSince 相对 interval 首次触发时刻(fake clock + 1h)构造:
116
+ // over = fireAt - TTL - 1 → 扫描时 age = TTL + 1 > TTL → archive
117
+ // under = fireAt - TTL + 1 → 扫描时 age = TTL - 1 ≤ TTL → 留内存
118
+ const fireAt = Date.now() + GC_INTERVAL_MS;
119
+ store.register(makeIdleRecord("sa-over", fireAt - IDLE_TTL_MS - 1));
120
+ store.register(makeIdleRecord("sa-under", fireAt - IDLE_TTL_MS + 1));
121
+
122
+ service.startGcTimer();
123
+ vi.advanceTimersByTime(GC_INTERVAL_MS);
124
+
125
+ expect(store.getMutable("sa-over")).toBeUndefined(); // archive 从内存移除
126
+ expect(store.getMutable("sa-under")).toBeDefined(); // 未超 TTL 留内存
127
+ expect(loggerMock.warn).toHaveBeenCalledTimes(1);
128
+ expect(loggerMock.warn).toHaveBeenCalledWith(
129
+ expect.stringContaining("GC: archiving idle record sa-over"),
130
+ );
131
+ });
132
+
133
+ it("有活进程的 record 不归档(isResumable=false,即使 idleSince 超 TTL)", () => {
134
+ const fireAt = Date.now() + GC_INTERVAL_MS;
135
+ // Path A:进程保活等待续聊(idle timer armed、child 未 kill)——归档会让活进程失管
136
+ store.register(makeIdleRecord("sa-live", fireAt - IDLE_TTL_MS - 1));
137
+ mockGetChildByRecord.mockImplementation(
138
+ (id: string): ChildProcess | undefined =>
139
+ id === "sa-live" ? ({ killed: false } as unknown as ChildProcess) : undefined,
140
+ );
141
+
142
+ service.startGcTimer();
143
+ vi.advanceTimersByTime(GC_INTERVAL_MS);
144
+
145
+ expect(store.getMutable("sa-live")).toBeDefined(); // 有活进程 → 不归档
146
+ expect(loggerMock.warn).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it("每小时重复扫描:上一轮差 1ms 的 record 下一轮超龄被归档", () => {
150
+ const fireAt = Date.now() + GC_INTERVAL_MS;
151
+ store.register(makeIdleRecord("sa-late", fireAt - IDLE_TTL_MS + 1));
152
+
153
+ service.startGcTimer();
154
+ vi.advanceTimersByTime(GC_INTERVAL_MS);
155
+ expect(store.getMutable("sa-late")).toBeDefined(); // 第一轮:age = TTL - 1,未到
156
+
157
+ vi.advanceTimersByTime(GC_INTERVAL_MS); // 第二轮:age = TTL + 1h - 1 > TTL
158
+ expect(store.getMutable("sa-late")).toBeUndefined();
159
+ });
160
+
161
+ it("startGcTimer 幂等:重复调用不产生第二个 interval", () => {
162
+ const fireAt = Date.now() + GC_INTERVAL_MS;
163
+ store.register(makeIdleRecord("sa-over", fireAt - IDLE_TTL_MS - 1));
164
+
165
+ service.startGcTimer();
166
+ service.startGcTimer(); // 已有 timer 直接 return(防重复 interval)
167
+ vi.advanceTimersByTime(GC_INTERVAL_MS);
168
+
169
+ // 同一 record 一轮只扫一次(重复 interval 会对同一 record 产生第二条 GC warn)
170
+ expect(loggerMock.warn).toHaveBeenCalledTimes(1);
171
+ });
172
+
173
+ it("dispose 后 interval 停止:超 TTL record 不再被归档", () => {
174
+ service.startGcTimer();
175
+ vi.advanceTimersByTime(GC_INTERVAL_MS); // 第一轮正常触发
176
+ service.dispose(); // stopGcTimer + disposeAllRecords
177
+
178
+ // dispose 后塞入超 TTL record,interval 已停 → 无论推进多久都不归档
179
+ store.register(makeIdleRecord("sa-after-dispose", Date.now() - IDLE_TTL_MS - 1));
180
+ vi.advanceTimersByTime(GC_INTERVAL_MS * 10);
181
+
182
+ expect(store.getMutable("sa-after-dispose")).toBeDefined();
183
+ });
184
+ });
@@ -0,0 +1,254 @@
1
+ // src/execution/__tests__/get-record-for-action-restart.test.ts
2
+ //
3
+ // [M10] getRecordForAction 跨重启磁盘重建分支测试(subagent-service.ts L933-951)。
4
+ //
5
+ // 背景:内存 miss → collectRecords(1000,"all",undefined).find(status==="running") →
6
+ // createRecord({chatMode: true}) → register → 回填 sessionFile/round。代码注释
7
+ // (L946-949)自设强制约束「改动此处必须带 S3 回归场景(跨重启 message 续聊验证)」
8
+ // ——该 S3 场景此前不存在,分支零测试。该分支含多个仅此处独有的决策:
9
+ // - 无条件 chatMode: true(v4 A-3 跨重启恢复入口,V3 方案 A 方向)
10
+ // - rootSessionFilter 传 undefined 后置校验(异树仍 throw not owned)
11
+ // - sessionFile/round 从磁盘重建结果回填(round 无磁盘持久化 → undefined)
12
+ //
13
+ // fixture 构造参照 record-store.test.ts 的 writeSessionJsonl(真实 .jsonl 文件,
14
+ // identity custom entry + assistant message;无 .alive sidecar → record-store
15
+ // sidecar 矩阵分支 4 → status="running" 跨重启可续聊态)。
16
+
17
+ import * as fs from "node:fs";
18
+ import * as os from "node:os";
19
+ import * as path from "node:path";
20
+
21
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
+
23
+ const { loggerMock } = vi.hoisted(() => ({
24
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
25
+ }));
26
+ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
27
+
28
+ // mock session-runner:import 链需要(getRecordForAction 本身不调 spawn,仅守卫读
29
+ // hasLiveProcessHandle——默认无活进程,与跨重启场景一致)。
30
+ vi.mock("../session-runner.ts", () => ({
31
+ runSpawn: vi.fn(),
32
+ killAllSpawnedChildren: vi.fn(),
33
+ getChildByRecord: vi.fn(() => undefined),
34
+ spawnedChildren: new Map(),
35
+ }));
36
+
37
+ import { writeFinalized } from "../finalized-marker.ts";
38
+ import { ModelConfigService } from "../model-config-service.ts";
39
+ import { getSubagentSessionDir } from "../path-encoding.ts";
40
+ import { RecordStore } from "../record-store.ts";
41
+ import { SubagentService } from "../subagent-service.ts";
42
+
43
+ // 身份 env 名(与 subagent-service.ts 常量一致;beforeEach/afterEach 清理防泄漏——
44
+ // 测试进程可能继承 subagent env,会污染 rootCwd 编码目录与 sessionRootId 基线)。
45
+ const IDENTITY_ENV_KEYS = [
46
+ "PI_SUBAGENT_ROOT_SESSION_ID",
47
+ "PI_SUBAGENT_SELF_RECORD_ID",
48
+ "PI_SUBAGENT_DEPTH",
49
+ "PI_SUBAGENT_ROOT_CWD",
50
+ "PI_SUBAGENT_FORK_DEPTH",
51
+ ] as const;
52
+
53
+ /** initSession 注入的最小 pi duck-type(同 subagent-service PiLike 形状,结构匹配即可)。 */
54
+ interface PiStub {
55
+ appendEntry(customType: string, data?: unknown): void;
56
+ events: { emit(channel: string, data: unknown): void };
57
+ sendMessage(
58
+ message: { customType: string; content: string; display: boolean; details?: unknown },
59
+ options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
60
+ ): void;
61
+ }
62
+
63
+ function makePi(): PiStub {
64
+ return {
65
+ appendEntry: vi.fn(),
66
+ events: { emit: vi.fn() },
67
+ sendMessage: vi.fn(),
68
+ };
69
+ }
70
+
71
+ /** 写一个最小合法 session.jsonl(session header + identity entry + 1 条 assistant message)。
72
+ * 不写任何 sidecar(.alive/.cancelled/.finalized)→ sidecar 矩阵分支 4 → running。 */
73
+ function writeSessionJsonl(
74
+ sessionsDir: string,
75
+ identity: {
76
+ id: string;
77
+ rootSessionId: string;
78
+ parentRecordId?: string;
79
+ depth?: number;
80
+ chatMode?: boolean;
81
+ worktree?: boolean;
82
+ },
83
+ ): string {
84
+ const file = path.join(sessionsDir, `${identity.id}.jsonl`);
85
+ const startedAt = 1_700_000_000_000;
86
+ const lines = [
87
+ JSON.stringify({
88
+ type: "session",
89
+ version: 3,
90
+ id: "sess-uuid",
91
+ timestamp: new Date(startedAt).toISOString(),
92
+ cwd: "/tmp",
93
+ }),
94
+ JSON.stringify({
95
+ type: "custom",
96
+ id: "id-1",
97
+ parentId: null,
98
+ timestamp: new Date(startedAt).toISOString(),
99
+ customType: "subagent-identity",
100
+ data: {
101
+ id: identity.id,
102
+ agent: "general-purpose",
103
+ mode: "background",
104
+ task: "restart recovery task",
105
+ slug: "restart-test",
106
+ startedAt,
107
+ rootSessionId: identity.rootSessionId,
108
+ ...(identity.parentRecordId !== undefined ? { parentRecordId: identity.parentRecordId } : {}),
109
+ ...(identity.depth !== undefined ? { depth: identity.depth } : {}),
110
+ ...(identity.chatMode !== undefined ? { chatMode: identity.chatMode } : {}),
111
+ ...(identity.worktree !== undefined ? { worktree: identity.worktree } : {}),
112
+ },
113
+ }),
114
+ JSON.stringify({
115
+ type: "message",
116
+ id: "msg-1",
117
+ parentId: "id-1",
118
+ timestamp: new Date(startedAt + 1000).toISOString(),
119
+ message: {
120
+ role: "assistant",
121
+ content: [{ type: "text", text: "first round done" }],
122
+ usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0 },
123
+ stopReason: "stop",
124
+ timestamp: startedAt + 1000,
125
+ },
126
+ }),
127
+ ];
128
+ fs.writeFileSync(file, `${lines.join("\n")}\n`, "utf-8");
129
+ return file;
130
+ }
131
+
132
+ /** 暴露私有 store 供断言 register 副作用。 */
133
+ interface ServiceInternals {
134
+ store: RecordStore;
135
+ }
136
+
137
+ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)", () => {
138
+ let agentDir: string;
139
+ let sessionsDir: string;
140
+ let service: SubagentService;
141
+ let store: RecordStore;
142
+
143
+ beforeEach(() => {
144
+ for (const k of IDENTITY_ENV_KEYS) delete process.env[k];
145
+ agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "restart-recon-"));
146
+ sessionsDir = getSubagentSessionDir(agentDir, agentDir);
147
+ fs.mkdirSync(sessionsDir, { recursive: true });
148
+
149
+ const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
150
+ service = new SubagentService({ cwd: agentDir, modelService });
151
+ // 根进程身份:无 env → sessionRootId = sessionId(自己是 root)、execCtxBaseline = null
152
+ service.initSession({ pi: makePi(), sessionId: "root-session" });
153
+ store = (service as unknown as ServiceInternals).store;
154
+ });
155
+
156
+ afterEach(() => {
157
+ service.dispose();
158
+ // maxRetries:collectRecords 触发的 fire-and-forget sessions-index 写可能与删除
159
+ // 并发(ENOTEMPTY 竞态,全量并行跑时机器负载高会放大窗口)
160
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
161
+ for (const k of IDENTITY_ENV_KEYS) delete process.env[k];
162
+ });
163
+
164
+ it("内存 miss + 磁盘 running(无 .alive)→ 重建 record:chatMode 无条件 true、sessionFile 回填、register 进内存", () => {
165
+ // identity entry 故意不带 chatMode 字段——证明重建分支的无条件 chatMode:true
166
+ //(不依赖磁盘是否记录过对话模式标志)
167
+ const file = writeSessionJsonl(sessionsDir, { id: "sa-restart-1", rootSessionId: "root-session" });
168
+ expect(store.getMutable("sa-restart-1")).toBeUndefined(); // 前置:内存确无
169
+
170
+ const record = service.getRecordForAction("sa-restart-1");
171
+
172
+ // [v4 A-3] 无条件 chatMode=true(跨重启恢复入口)
173
+ expect(record.chatMode).toBe(true);
174
+ // sessionFile 从磁盘重建结果回填
175
+ expect(record.sessionFile).toBe(file);
176
+ // round 从 found 回填:light 磁盘重建无 round(内存态字段,跨重启不恢复)→ undefined
177
+ expect(record.round).toBeUndefined();
178
+ // 身份字段从磁盘 identity entry 回填
179
+ expect(record.agent).toBe("general-purpose");
180
+ expect(record.task).toBe("restart recovery task");
181
+ expect(record.slug).toBe("restart-test");
182
+ expect(record.rootSessionId).toBe("root-session");
183
+ expect(record.parentRecordId).toBeUndefined();
184
+ expect(record.status).toBe("running");
185
+ // register 生效:进内存,二次调用走内存命中(同一引用)
186
+ expect(store.getMutable("sa-restart-1")).toBe(record);
187
+ expect(service.getRecordForAction("sa-restart-1")).toBe(record);
188
+ });
189
+
190
+ it("rootSessionId 校验仍生效:异树 record(other-session)→ throw not found or not owned", () => {
191
+ // rootSessionFilter 传 undefined(扫全量磁盘),异树 record 会被 find 命中并重建,
192
+ // 但后置校验 record.rootSessionId !== this.sessionRootId 必须拦截
193
+ writeSessionJsonl(sessionsDir, { id: "sa-foreign", rootSessionId: "other-session" });
194
+
195
+ expect(() => service.getRecordForAction("sa-foreign")).toThrow(/not found or not owned/);
196
+ });
197
+
198
+ it("直接父校验仍生效:孙级 record(parentRecordId=sa-parent)→ 主进程 throw direct parent", () => {
199
+ writeSessionJsonl(sessionsDir, {
200
+ id: "sa-grand",
201
+ rootSessionId: "root-session",
202
+ parentRecordId: "sa-parent",
203
+ depth: 2,
204
+ });
205
+
206
+ // 重建后 parentRecordId=sa-parent ≠ 主进程 baseline(undefined) → cross-layer 守卫拦截
207
+ expect(() => service.getRecordForAction("sa-grand")).toThrow(/direct parent/);
208
+ });
209
+
210
+ it(".finalized sidecar(closed 终态)不重建 → throw not found or not owned", () => {
211
+ const file = writeSessionJsonl(sessionsDir, { id: "sa-fin", rootSessionId: "root-session" });
212
+ writeFinalized(file); // sidecar 矩阵分支 2 → status=closed → find(status==="running") miss
213
+
214
+ expect(() => service.getRecordForAction("sa-fin")).toThrow(/not found or not owned/);
215
+ expect(store.getMutable("sa-fin")).toBeUndefined(); // 未重建注册
216
+ });
217
+
218
+ // ============================================================
219
+ // [review round2] 跨重启 worktree 绑定丢失防护
220
+ // ============================================================
221
+ // 场景:worktree:true + conversation:true 的 subagent 在父进程重启后续聊。WorktreeHandle
222
+ // 不可序列化、重建后恒缺失,若无守卫 → 冷路径 resume 的 spawn cwd 静默回落主 repo
223
+ //(隔离失效,正是 worktree 要防的并发写冲突场景)。
224
+ it("[review round2] worktree record 跨重启重建 → hadWorktree 标记 + 续聊被拒(行动语言)", async () => {
225
+ writeSessionJsonl(sessionsDir, {
226
+ id: "sa-wt",
227
+ rootSessionId: "root-session",
228
+ worktree: true,
229
+ });
230
+
231
+ const record = service.getRecordForAction("sa-wt");
232
+ // hadWorktree 从磁盘 identity entry 的 worktree 标志恢复
233
+ expect(record.hadWorktree).toBe(true);
234
+ // handle 无法恢复(不可序列化)
235
+ expect(record.worktreeHandle).toBeUndefined();
236
+
237
+ // 冷路径续聊(无活进程)→ resumeRound 守卫拒绝,不 spawn 回落主 repo
238
+ await expect(service.deliverMessage(record, "resume after restart", false)).rejects.toThrow(
239
+ /worktree isolation.*lost when the parent process restarted/,
240
+ );
241
+ });
242
+
243
+ it("[review round2] 非 worktree record 跨重启重建 → 续聊不受 worktree 守卫拦截(向后兼容)", async () => {
244
+ // identity entry 无 worktree 字段(旧文件)→ found.worktree undefined → hadWorktree false
245
+ writeSessionJsonl(sessionsDir, { id: "sa-nowt", rootSessionId: "root-session" });
246
+
247
+ const record = service.getRecordForAction("sa-nowt");
248
+ expect(record.hadWorktree).toBe(false);
249
+
250
+ // deliverMessage 冷路径不被 worktree 守卫拦截(resume 正常发起;后续 spawn 编排
251
+ // 超出本用例关注点——runSpawn 在本文件是 no-op mock)
252
+ await expect(service.deliverMessage(record, "resume normal", false)).resolves.toBeUndefined();
253
+ });
254
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * 创建一个结构兼容的 ExtensionAPI mock,供 sdk-contract / 注册契约测试用。
3
+ *
4
+ * ExtensionAPI 有 ~30 个必需方法(on/registerTool/registerCommand/...),逐个手写
5
+ * 不现实。本 helper 用 Proxy 把所有未显式 override 的方法/属性短路为 no-op 函数,
6
+ * 让对象**结构兼容** ExtensionAPI——避免 `as unknown as ExtensionAPI` 双重断言
7
+ * (taste/no-unsafe-cast 规则禁止)。测试只需 override 关心的方法:
8
+ *
9
+ * const pi = mockExtensionApi({
10
+ * registerCommand: (name) => { capturedName = name; },
11
+ * });
12
+ *
13
+ * Proxy 的 trap 对所有属性访问返回 override 值或 no-op 函数,运行时安全
14
+ * (注册 handler 只调用被 override 的方法,其余方法测试不触及)。
15
+ */
16
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
+
18
+ export function mockExtensionApi(
19
+ overrides: Record<string, unknown> = {},
20
+ ): ExtensionAPI {
21
+ const noop = (): void => { /* test mock: method not invoked by this test */ };
22
+ // Proxy<T> 泛型参数决定返回类型——直接声明为 ExtensionAPI,
23
+ // TS 接受(Proxy handler 对 target 的类型不约束 T)。
24
+ return new Proxy<ExtensionAPI>(overrides as ExtensionAPI, {
25
+ get(target, prop: string | symbol): unknown {
26
+ if (prop in target) return target[prop as keyof ExtensionAPI];
27
+ return noop;
28
+ },
29
+ });
30
+ }