@zhushanwen/pi-subagent-workflow 8.5.0 → 8.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/package.json +13 -16
  2. package/relay/relay.mjs +1 -1
  3. package/src/host/__tests__/pi-host.test.ts +214 -0
  4. package/src/host/pi-host.ts +109 -0
  5. package/src/index.ts +142 -32
  6. package/src/injectors/__tests__/engine-awareness.test.ts +292 -0
  7. package/src/injectors/__tests__/engine-section-stability.test.ts +341 -0
  8. package/src/injectors/__tests__/subagent-list-injector.test.ts +5 -5
  9. package/src/injectors/__tests__/workflow-list-injector.test.ts +5 -5
  10. package/src/injectors/engine-awareness.ts +123 -0
  11. package/src/injectors/model-list-injector.ts +1 -1
  12. package/src/injectors/subagent-list-injector.ts +8 -8
  13. package/src/injectors/workflow-list-injector.ts +8 -8
  14. package/src/interface/__tests__/detectors.test.ts +3 -3
  15. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
  16. package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
  17. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +2 -2
  18. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +1 -1
  19. package/src/interface/bg-notify-render.ts +34 -13
  20. package/src/interface/commands.ts +3 -3
  21. package/src/interface/format.ts +2 -2
  22. package/src/interface/helpers.ts +3 -3
  23. package/src/interface/list-component.ts +4 -4
  24. package/src/interface/list-shared.ts +2 -2
  25. package/src/interface/list-view.ts +2 -2
  26. package/src/interface/subagent-actions.ts +288 -19
  27. package/src/interface/subagent-tool-schema.ts +163 -0
  28. package/src/interface/subagent-tool.ts +65 -129
  29. package/src/interface/subagents.ts +5 -5
  30. package/src/interface/tool-render.ts +34 -2
  31. package/src/interface/tool-workflow-script.ts +4 -4
  32. package/src/interface/tool-workflow.ts +6 -6
  33. package/src/interface/views/WorkflowsView.ts +5 -5
  34. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
  35. package/src/interface/views/__tests__/detail-content-session-file.test.ts +2 -2
  36. package/src/interface/views/detail-content.ts +4 -4
  37. package/src/interface/views/format.ts +4 -4
  38. package/src/{orchestration/jsonl-run-store.ts → jsonl-run-store.ts} +132 -18
  39. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +0 -53
  40. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +0 -1
  41. package/src/execution/__tests__/agent-registry.test.ts +0 -363
  42. package/src/execution/__tests__/agent-result-mapper.test.ts +0 -150
  43. package/src/execution/__tests__/alive-store.test.ts +0 -147
  44. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  45. package/src/execution/__tests__/before-agent-start-injection.test.ts +0 -132
  46. package/src/execution/__tests__/bg-notify-render.test.ts +0 -256
  47. package/src/execution/__tests__/channel-registry-handshake.test.ts +0 -243
  48. package/src/execution/__tests__/chat-engine-routing.test.ts +0 -597
  49. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +0 -365
  50. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -190
  51. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +0 -215
  52. package/src/execution/__tests__/concurrency-pool.test.ts +0 -250
  53. package/src/execution/__tests__/config.test.ts +0 -110
  54. package/src/execution/__tests__/conversation-wiring.test.ts +0 -198
  55. package/src/execution/__tests__/crash-recovery.test.ts +0 -320
  56. package/src/execution/__tests__/delivery-methods.test.ts +0 -385
  57. package/src/execution/__tests__/dialog-queue.test.ts +0 -299
  58. package/src/execution/__tests__/epipe-fallback.test.ts +0 -241
  59. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -243
  60. package/src/execution/__tests__/execute-nesting.test.ts +0 -323
  61. package/src/execution/__tests__/execute-options-mapper.test.ts +0 -189
  62. package/src/execution/__tests__/execution-record.test.ts +0 -1284
  63. package/src/execution/__tests__/finalize-record.test.ts +0 -368
  64. package/src/execution/__tests__/finalized-marker.test.ts +0 -82
  65. package/src/execution/__tests__/format-schema-instruction.test.ts +0 -138
  66. package/src/execution/__tests__/format.test.ts +0 -458
  67. package/src/execution/__tests__/gc-timer.test.ts +0 -184
  68. package/src/execution/__tests__/get-record-for-action-restart.test.ts +0 -254
  69. package/src/execution/__tests__/gui-mode-dispatch.test.ts +0 -59
  70. package/src/execution/__tests__/helpers/mock-extension-api.ts +0 -30
  71. package/src/execution/__tests__/helpers/spawn-mock.ts +0 -238
  72. package/src/execution/__tests__/host-mode.test.ts +0 -87
  73. package/src/execution/__tests__/index-session-start-identity.test.ts +0 -371
  74. package/src/execution/__tests__/index-session-start.test.ts +0 -593
  75. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
  76. package/src/execution/__tests__/lifecycle-manager.test.ts +0 -337
  77. package/src/execution/__tests__/lifecycle-predicates.test.ts +0 -116
  78. package/src/execution/__tests__/list-component.test.ts +0 -401
  79. package/src/execution/__tests__/list-fields.test.ts +0 -109
  80. package/src/execution/__tests__/manifest-parentid.test.ts +0 -117
  81. package/src/execution/__tests__/model-resolver.test.ts +0 -413
  82. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +0 -287
  83. package/src/execution/__tests__/nested-visibility.test.ts +0 -325
  84. package/src/execution/__tests__/notifier-flush.test.ts +0 -574
  85. package/src/execution/__tests__/notifier-golden-snapshot.test.ts +0 -261
  86. package/src/execution/__tests__/one-shot-upgrade.test.ts +0 -205
  87. package/src/execution/__tests__/output-collector.test.ts +0 -61
  88. package/src/execution/__tests__/parent-child-matrix.test.ts +0 -336
  89. package/src/execution/__tests__/path-encoding.test.ts +0 -104
  90. package/src/execution/__tests__/pi-invocation.test.ts +0 -134
  91. package/src/execution/__tests__/record-store.test.ts +0 -1057
  92. package/src/execution/__tests__/records-cwd-isolation.test.ts +0 -91
  93. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +0 -339
  94. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -273
  95. package/src/execution/__tests__/relay-agent.test.ts +0 -448
  96. package/src/execution/__tests__/relay-env.test.ts +0 -42
  97. package/src/execution/__tests__/resource-policy.test.ts +0 -109
  98. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  99. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -267
  100. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -253
  101. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -644
  102. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  103. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  104. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  105. package/src/execution/__tests__/sdk-contract.test.ts +0 -273
  106. package/src/execution/__tests__/session-context-resolver.test.ts +0 -167
  107. package/src/execution/__tests__/session-file-gc.test.ts +0 -293
  108. package/src/execution/__tests__/session-pending.test.ts +0 -197
  109. package/src/execution/__tests__/session-reconstructor.test.ts +0 -379
  110. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -178
  111. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -311
  112. package/src/execution/__tests__/session-start-reaper.test.ts +0 -233
  113. package/src/execution/__tests__/spawn-args.test.ts +0 -434
  114. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -189
  115. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  116. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -207
  117. package/src/execution/__tests__/spawned-children.test.ts +0 -92
  118. package/src/execution/__tests__/startup-config-declaration.test.ts +0 -35
  119. package/src/execution/__tests__/status-refactor.test.ts +0 -345
  120. package/src/execution/__tests__/stdin-writer.test.ts +0 -463
  121. package/src/execution/__tests__/stream-sink-retirement.test.ts +0 -261
  122. package/src/execution/__tests__/subagent-service-abort.test.ts +0 -60
  123. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -629
  124. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +0 -180
  125. package/src/execution/__tests__/subagent-service.test.ts +0 -788
  126. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -310
  127. package/src/execution/__tests__/subprocess-agent-runner.test.ts +0 -506
  128. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  129. package/src/execution/__tests__/timeout-integration.test.ts +0 -397
  130. package/src/execution/__tests__/tombstone-store.test.ts +0 -73
  131. package/src/execution/__tests__/tool-action.test.ts +0 -385
  132. package/src/execution/__tests__/truncline-snapshot.test.ts +0 -81
  133. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  134. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  135. package/src/execution/__tests__/ui-channels.test.ts +0 -187
  136. package/src/execution/__tests__/ui-interaction-model.test.ts +0 -67
  137. package/src/execution/__tests__/ui-request-handler-factory.test.ts +0 -370
  138. package/src/execution/__tests__/ui-request-handler.test.ts +0 -204
  139. package/src/execution/__tests__/ui-request-observability.test.ts +0 -114
  140. package/src/execution/__tests__/ui-request-queue.test.ts +0 -133
  141. package/src/execution/__tests__/worktree-manager.test.ts +0 -633
  142. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -229
  143. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +0 -181
  144. package/src/execution/__tests__/worktree-registry.test.ts +0 -199
  145. package/src/execution/agent-registry.ts +0 -193
  146. package/src/execution/agent-result-mapper.ts +0 -90
  147. package/src/execution/alive-store.ts +0 -92
  148. package/src/execution/argv-mirror.ts +0 -109
  149. package/src/execution/best-effort.ts +0 -38
  150. package/src/execution/channel-registry-access.ts +0 -144
  151. package/src/execution/concurrency-pool.ts +0 -116
  152. package/src/execution/config.ts +0 -96
  153. package/src/execution/dialog-queue.ts +0 -330
  154. package/src/execution/engine/__tests__/common/data-dir.test.ts +0 -53
  155. package/src/execution/engine/__tests__/common/errors.test.ts +0 -132
  156. package/src/execution/engine/__tests__/common/event-journal.test.ts +0 -177
  157. package/src/execution/engine/__tests__/common/kill-chain.test.ts +0 -192
  158. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +0 -81
  159. package/src/execution/engine/__tests__/common/persona-router.test.ts +0 -123
  160. package/src/execution/engine/__tests__/common/pool-manager.test.ts +0 -154
  161. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  162. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  163. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +0 -141
  164. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +0 -109
  165. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +0 -101
  166. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +0 -77
  167. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +0 -104
  168. package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +0 -342
  169. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +0 -201
  170. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +0 -76
  171. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +0 -79
  172. package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -87
  173. package/src/execution/engine/__tests__/engines-declaration.test.ts +0 -36
  174. package/src/execution/engine/__tests__/model-prompt.test.ts +0 -85
  175. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  176. package/src/execution/engine/__tests__/registry.test.ts +0 -120
  177. package/src/execution/engine/__tests__/routing.test.ts +0 -231
  178. package/src/execution/engine/common/data-dir.ts +0 -62
  179. package/src/execution/engine/common/errors.ts +0 -183
  180. package/src/execution/engine/common/event-journal.ts +0 -254
  181. package/src/execution/engine/common/journal-replay.ts +0 -62
  182. package/src/execution/engine/common/kill-chain.ts +0 -221
  183. package/src/execution/engine/common/nesting-guard.ts +0 -50
  184. package/src/execution/engine/common/persona-router.ts +0 -108
  185. package/src/execution/engine/common/pool-manager.ts +0 -226
  186. package/src/execution/engine/common/schema-emulation.ts +0 -189
  187. package/src/execution/engine/common/session-view-projection.ts +0 -51
  188. package/src/execution/engine/engine-discovery.ts +0 -65
  189. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -469
  190. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  191. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
  192. package/src/execution/engine/engines/pi/pi-engine.ts +0 -415
  193. package/src/execution/engine/engines/pi/reader.ts +0 -48
  194. package/src/execution/engine/engines/pi/registration.ts +0 -35
  195. package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
  196. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
  197. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
  198. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -246
  199. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -228
  200. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  201. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -64
  202. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -127
  203. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -567
  204. package/src/execution/engine/engines/zcode/constants.ts +0 -43
  205. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -39
  206. package/src/execution/engine/engines/zcode/launcher.ts +0 -161
  207. package/src/execution/engine/engines/zcode/parser.ts +0 -436
  208. package/src/execution/engine/engines/zcode/preparer.ts +0 -363
  209. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  210. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  211. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -648
  212. package/src/execution/engine/host-task-spec.ts +0 -47
  213. package/src/execution/engine/model-prompt.ts +0 -59
  214. package/src/execution/engine/paths.ts +0 -42
  215. package/src/execution/engine/port.ts +0 -153
  216. package/src/execution/engine/registry.ts +0 -123
  217. package/src/execution/engine/routing.ts +0 -218
  218. package/src/execution/engine/types.ts +0 -304
  219. package/src/execution/execute-options-mapper.ts +0 -111
  220. package/src/execution/execution-record.ts +0 -906
  221. package/src/execution/finalize-record.ts +0 -251
  222. package/src/execution/finalized-marker.ts +0 -51
  223. package/src/execution/get-state-handshake.ts +0 -104
  224. package/src/execution/host-mode.ts +0 -56
  225. package/src/execution/idle-gc.ts +0 -47
  226. package/src/execution/lifecycle-manager.ts +0 -491
  227. package/src/execution/lifecycle-predicates.ts +0 -65
  228. package/src/execution/manifest-store.ts +0 -256
  229. package/src/execution/model-config-service.ts +0 -204
  230. package/src/execution/model-resolver.ts +0 -276
  231. package/src/execution/notifier.ts +0 -260
  232. package/src/execution/output-collector.ts +0 -88
  233. package/src/execution/path-encoding.ts +0 -60
  234. package/src/execution/pi-invocation.ts +0 -120
  235. package/src/execution/record-entry.ts +0 -132
  236. package/src/execution/record-store.ts +0 -1234
  237. package/src/execution/relay-env.ts +0 -37
  238. package/src/execution/session-context-resolver.ts +0 -64
  239. package/src/execution/session-file-gc.ts +0 -120
  240. package/src/execution/session-pending.ts +0 -164
  241. package/src/execution/session-reconstructor.ts +0 -678
  242. package/src/execution/session-runner.ts +0 -1548
  243. package/src/execution/sessions-index.ts +0 -304
  244. package/src/execution/spawn-event-adapter.ts +0 -363
  245. package/src/execution/stdin-writer.ts +0 -198
  246. package/src/execution/stream-sink.ts +0 -126
  247. package/src/execution/subagent-service.ts +0 -2119
  248. package/src/execution/subprocess-agent-runner.ts +0 -303
  249. package/src/execution/temp-prompt.ts +0 -62
  250. package/src/execution/tombstone-store.ts +0 -72
  251. package/src/execution/turn-limiter.ts +0 -102
  252. package/src/execution/types.ts +0 -910
  253. package/src/execution/ui-channels.ts +0 -216
  254. package/src/execution/ui-interaction-model.ts +0 -48
  255. package/src/execution/ui-request-handler-factory.ts +0 -212
  256. package/src/execution/ui-request-observability.ts +0 -81
  257. package/src/execution/ui-request-queue.ts +0 -174
  258. package/src/execution/worktree-manager.ts +0 -688
  259. package/src/execution/worktree-registry.ts +0 -222
  260. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +0 -336
  261. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -208
  262. package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -157
  263. package/src/orchestration/__tests__/args-validator.test.ts +0 -143
  264. package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
  265. package/src/orchestration/__tests__/config-loader.test.ts +0 -503
  266. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +0 -724
  267. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +0 -405
  268. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +0 -56
  269. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +0 -166
  270. package/src/orchestration/__tests__/execute-agent-call.test.ts +0 -314
  271. package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +0 -171
  272. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +0 -1030
  273. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -277
  274. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +0 -96
  275. package/src/orchestration/__tests__/lifecycle.test.ts +0 -625
  276. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +0 -1971
  277. package/src/orchestration/__tests__/script-lint.test.ts +0 -831
  278. package/src/orchestration/__tests__/skill-discovery.test.ts +0 -157
  279. package/src/orchestration/__tests__/test-mocks.ts +0 -197
  280. package/src/orchestration/__tests__/worker-host.test.ts +0 -120
  281. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +0 -164
  282. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +0 -520
  283. package/src/orchestration/__tests__/worker-script-builder.test.ts +0 -309
  284. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +0 -110
  285. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -317
  286. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +0 -110
  287. package/src/orchestration/__tests__/workflows-e2e.test.ts +0 -537
  288. package/src/orchestration/agent-opts-resolver.ts +0 -93
  289. package/src/orchestration/args-validator.ts +0 -127
  290. package/src/orchestration/concurrency-gate.ts +0 -69
  291. package/src/orchestration/config-loader.ts +0 -315
  292. package/src/orchestration/error-recovery.ts +0 -862
  293. package/src/orchestration/execute-agent-call.ts +0 -217
  294. package/src/orchestration/launcher.ts +0 -410
  295. package/src/orchestration/lifecycle.ts +0 -396
  296. package/src/orchestration/models/__tests__/budget.test.ts +0 -367
  297. package/src/orchestration/models/__tests__/trace.test.ts +0 -408
  298. package/src/orchestration/models/agent-call.ts +0 -83
  299. package/src/orchestration/models/budget.ts +0 -148
  300. package/src/orchestration/models/ports.ts +0 -164
  301. package/src/orchestration/models/run-runtime.ts +0 -89
  302. package/src/orchestration/models/run-spec.ts +0 -83
  303. package/src/orchestration/models/run-state.ts +0 -44
  304. package/src/orchestration/models/trace.ts +0 -183
  305. package/src/orchestration/models/types.ts +0 -292
  306. package/src/orchestration/models/workflow-run.ts +0 -254
  307. package/src/orchestration/models/workflow-script-registry.ts +0 -35
  308. package/src/orchestration/models/workflow-script.ts +0 -117
  309. package/src/orchestration/script-lint.ts +0 -766
  310. package/src/orchestration/skill-discovery.ts +0 -92
  311. package/src/orchestration/worker-handle.ts +0 -115
  312. package/src/orchestration/worker-host.ts +0 -98
  313. package/src/orchestration/worker-script-builder.ts +0 -408
  314. package/src/orchestration/workflow-files.ts +0 -85
  315. package/src/orchestration/workflow-script-registry-impl.ts +0 -133
  316. package/src/shared/__tests__/agent-ref.test.ts +0 -34
  317. package/src/shared/__tests__/meta-parser.test.ts +0 -304
  318. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -280
  319. package/src/shared/__tests__/resource-discovery.test.ts +0 -736
  320. package/src/shared/__tests__/resource-meta.test.ts +0 -51
  321. package/src/shared/__tests__/schema-jsonify.test.ts +0 -81
  322. package/src/shared/agent-event.ts +0 -13
  323. package/src/shared/agent-ref.ts +0 -57
  324. package/src/shared/meta-parser.ts +0 -261
  325. package/src/shared/resource-discovery.ts +0 -811
  326. package/src/shared/resource-meta.ts +0 -65
  327. package/src/shared/schema-jsonify.ts +0 -56
  328. package/src/shared/xml-injection.ts +0 -35
  329. package/workflows/README.md +0 -81
  330. package/workflows/_shared/agent-refs.cjs +0 -40
  331. package/workflows/chain.js +0 -131
  332. package/workflows/map-reduce.js +0 -174
  333. package/workflows/parallel.js +0 -148
  334. package/workflows/review-fix-loop-utils.cjs +0 -1307
  335. package/workflows/review-fix-loop.js +0 -1396
  336. package/workflows/scatter-gather.js +0 -178
@@ -1,1030 +0,0 @@
1
- // src/orchestration/__tests__/jsonl-run-store-session-file.test.ts
2
- //
3
- // W1: jsonl-run-store 序列化/反序列化 sessionFile round-trip 测试
4
- //
5
- // 防的 bug:sessionFile 加入 AgentCall + ExecutionTraceNode 后,序列化时必须写入快照,
6
- // 反序列化时必须恢复——否则跨 session 重水合后 agent 的 session jsonl
7
- // 路径丢失,overlay 无法定位。
8
-
9
- import * as fs from "node:fs";
10
- import * as os from "node:os";
11
- import * as path from "node:path";
12
-
13
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
14
-
15
- // logger mock(文件级):jsonl-run-store.ts 模块级 getLogger 拿到此 mock,
16
- // W2TC10 断言 dispose 后 save no-op 的 debug 留痕;其余 describe 不消费 logger。
17
- const loggerMock = vi.hoisted(() => ({
18
- debug: vi.fn(),
19
- warn: vi.fn(),
20
- error: vi.fn(),
21
- }));
22
- vi.mock("@zhushanwen/pi-extension-logger", () => ({
23
- getLogger: () => loggerMock,
24
- }));
25
-
26
- import type { CustomEntry } from "@earendil-works/pi-coding-agent";
27
-
28
- import { AgentCall } from "../models/agent-call.ts";
29
- import { Budget } from "../models/budget.ts";
30
- import { Trace } from "../models/trace.ts";
31
- import type { ExecutionTraceNode } from "../models/types.ts";
32
- import type { RunSpec } from "../models/run-spec.ts";
33
- import { WorkflowRun } from "../models/workflow-run.ts";
34
- import { JsonlRunStore, WORKFLOW_RECORD_CUSTOM_TYPE } from "../jsonl-run-store.ts";
35
- import { mkCtx, mkPi } from "./test-mocks.ts";
36
-
37
- function makeSpec(): RunSpec {
38
- return {
39
- scriptSource: "module.exports = async () => {};",
40
- args: {},
41
- scriptName: "test-script",
42
- scriptPath: "/tmp/test.js",
43
- description: "test",
44
- };
45
- }
46
-
47
- function makeTraceNode(stepIndex: number): ExecutionTraceNode {
48
- return {
49
- stepIndex,
50
- agent: "worker",
51
- task: "do thing",
52
- model: "default",
53
- status: "pending",
54
- };
55
- }
56
-
57
- function makeRunWithDoneCall(): WorkflowRun {
58
- const trace = new Trace();
59
- const node = makeTraceNode(0);
60
- trace.append(node);
61
- const call = new AgentCall(
62
- 0,
63
- {
64
- prompt: "task",
65
- agent: "worker",
66
- cwd: "/tmp",
67
- },
68
- node,
69
- );
70
- // 模拟已完成 agent call:带 sessionId + sessionFile
71
- call.markRunning();
72
- call.markDone({
73
- content: "done",
74
- sessionId: "session-abc",
75
- sessionFile: "/abs/.pi/agent/subagents/enc/sessions/2026-07-15T_session-abc.jsonl",
76
- });
77
- call.setSessionId("session-abc");
78
- call.setSessionFile("/abs/.pi/agent/subagents/enc/sessions/2026-07-15T_session-abc.jsonl");
79
- trace.update(0, {
80
- status: "completed",
81
- result: call.result,
82
- completedAt: new Date().toISOString(),
83
- sessionId: "session-abc",
84
- sessionFile: "/abs/.pi/agent/subagents/enc/sessions/2026-07-15T_session-abc.jsonl",
85
- });
86
-
87
- return new WorkflowRun(
88
- "run-test-001",
89
- makeSpec(),
90
- {
91
- status: "done",
92
- reason: "completed",
93
- budget: new Budget(),
94
- calls: new Map([[0, call]]),
95
- trace,
96
- errorLogs: [],
97
- },
98
- { startedAt: new Date().toISOString(), completedAt: new Date().toISOString() },
99
- );
100
- }
101
-
102
- /**
103
- * W4+ fixture:running 状态 run(热路径去抖测试的前提)。
104
- *
105
- * 用 WorkflowRun.reconstruct 构造——running 且 runtime undefined 违反 I1
106
- * (持久化的 running 快照无 worker),constructor 会抛错;reconstruct 跳过 I1
107
- * 校验(可信快照重水合语义)。serializeRun 不读 runtime 字段,序列化合法。
108
- * 热路径「中间态演进」直接对同一 run 引用 trace.append 后再次 save(latestRun 语义)。
109
- */
110
- function makeRunningRun(runId: string): WorkflowRun {
111
- const trace = new Trace();
112
- trace.append(makeTraceNode(0));
113
- return WorkflowRun.reconstruct(
114
- runId,
115
- makeSpec(),
116
- {
117
- status: "running",
118
- budget: new Budget(),
119
- calls: new Map(),
120
- trace,
121
- errorLogs: [],
122
- },
123
- { startedAt: new Date().toISOString() },
124
- );
125
- }
126
-
127
- /** 读 run 状态文件并解析快照(W4+ 断言「磁盘真实内容」用)。 */
128
- function readStateFile(
129
- tmpDir: string,
130
- runId: string,
131
- ): { state: { status: string; trace: Array<{ stepIndex: number }> } } {
132
- const raw = fs.readFileSync(path.join(tmpDir, "workflow-state", `${runId}.jsonl`), "utf8");
133
- return JSON.parse(raw.trim()) as { state: { status: string; trace: Array<{ stepIndex: number }> } };
134
- }
135
-
136
- /** unknown → workflow-record entry data 的运行时收窄(taste/no-unsafe-cast:断言前先收窄,
137
- * 对齐 record-store.test.ts 的 asEntryData 模式)。 */
138
- function asRecordData(
139
- d: unknown,
140
- ): { v: number; snapshot: { runId: string; state: { status: string } } } {
141
- if (typeof d !== "object" || d === null) throw new Error("entry data is not an object");
142
- return d as { v: number; snapshot: { runId: string; state: { status: string } } };
143
- }
144
-
145
- describe("W1: JsonlRunStore sessionFile 序列化 round-trip", () => {
146
- let tmpDir: string;
147
- let store: JsonlRunStore;
148
-
149
- beforeEach(() => {
150
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-store-test-"));
151
- store = new JsonlRunStore({ sessionDir: tmpDir });
152
- });
153
-
154
- afterEach(() => {
155
- fs.rmSync(tmpDir, { recursive: true, force: true });
156
- });
157
-
158
- it("save + loadAll round-trip: AgentCall.sessionFile 保留", async () => {
159
- const run = makeRunWithDoneCall();
160
- await store.save(run);
161
-
162
- // 从磁盘直接读快照验证 sessionFile 写入了序列化
163
- const stateDir = path.join(tmpDir, "workflow-state");
164
- const files = fs.readdirSync(stateDir).filter((f) => f.endsWith(".jsonl"));
165
- expect(files).toHaveLength(1);
166
- const raw = fs.readFileSync(path.join(stateDir, files[0]!), "utf8");
167
- const snapshot = JSON.parse(raw.trim());
168
- const serializedCall = snapshot.state.calls[0];
169
- expect(serializedCall.sessionFile).toBe(
170
- "/abs/.pi/agent/subagents/enc/sessions/2026-07-15T_session-abc.jsonl",
171
- );
172
- });
173
-
174
- it("save + loadAll round-trip: ExecutionTraceNode.sessionFile 保留", async () => {
175
- const run = makeRunWithDoneCall();
176
- await store.save(run);
177
-
178
- const raw = fs.readFileSync(
179
- path.join(tmpDir, "workflow-state", "run-test-001.jsonl"),
180
- "utf8",
181
- );
182
- const snapshot = JSON.parse(raw.trim());
183
- const traceNode = snapshot.state.trace[0];
184
- expect(traceNode.sessionFile).toBe(
185
- "/abs/.pi/agent/subagents/enc/sessions/2026-07-15T_session-abc.jsonl",
186
- );
187
- });
188
-
189
- it("save → loadAll 完整 round-trip: 反序列化后 AgentCall.sessionFile 可读", async () => {
190
- // 闭环测试:serialize(save)→ deserialize(loadAll)→ 验证 run.state.calls 的 AgentCall.sessionFile
191
- const sessionFilePath = "/abs/.pi/agent/subagents/enc/sessions/2026-07-15T_session-abc.jsonl";
192
-
193
- // mock pi + ctx 共享同一 entries 数组:save 写 pointer entry,loadAll 读同一组 entries
194
- const entries: CustomEntry[] = [];
195
- const mockPi = mkPi(entries);
196
- const mockCtx = mkCtx(entries);
197
-
198
- const storeWithCtx = new JsonlRunStore({
199
- sessionDir: tmpDir,
200
- pi: mockPi,
201
- ctx: mockCtx,
202
- });
203
-
204
- const run = makeRunWithDoneCall();
205
- await storeWithCtx.save(run);
206
-
207
- const loaded = await storeWithCtx.loadAll();
208
- expect(loaded).toHaveLength(1);
209
- const restoredCall = loaded[0]!.state.calls.get(0);
210
- expect(restoredCall).toBeDefined();
211
- expect(restoredCall!.sessionFile).toBe(sessionFilePath);
212
- });
213
- });
214
-
215
- describe("W2: RunStore.stateFilePath 暴露 run 状态文件路径", () => {
216
- let tmpDir: string;
217
- let store: JsonlRunStore;
218
-
219
- beforeEach(() => {
220
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-store-test-"));
221
- store = new JsonlRunStore({ sessionDir: tmpDir });
222
- });
223
-
224
- afterEach(() => {
225
- fs.rmSync(tmpDir, { recursive: true, force: true });
226
- });
227
-
228
- it("stateFilePath(runId) 返回 <sessionDir>/workflow-state/<runId>.jsonl", () => {
229
- const result = store.stateFilePath("run-xyz");
230
- expect(result).toBe(path.join(tmpDir, "workflow-state", "run-xyz.jsonl"));
231
- });
232
- });
233
-
234
- // ── W9: 快照版本守卫(v2 当前 / v1 跳过)──────────────────────────────
235
- //
236
- // U2 快照格式 v1→v2(status 两态、无 pausedAt)。v1 遗留文件经 loadAll 静默跳过
237
- // (D-5 边界声明:旧 run 历史价值低,不做兼容迁移)。
238
-
239
- describe("W9: 快照版本守卫(v2 当前 / v1 跳过)", () => {
240
- let tmpDir: string;
241
-
242
- beforeEach(() => {
243
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-store-ver-"));
244
- });
245
-
246
- afterEach(() => {
247
- fs.rmSync(tmpDir, { recursive: true, force: true });
248
- });
249
-
250
- it("v1 头快照(升级前遗留)→ loadAll 静默跳过:不崩、不显示", async () => {
251
- // 模拟 v1 时代的遗留快照(版本头 wf-run-v1)。版本守卫只比对 v 字段,
252
- // status 值不影响跳过判定——v1 running 残留同样静默消失不显示(D-5 边界声明)。
253
- const stateDir = path.join(tmpDir, "workflow-state");
254
- fs.mkdirSync(stateDir, { recursive: true });
255
- const filePath = path.join(stateDir, "run-legacy-v1.jsonl");
256
- const legacySnapshot = {
257
- v: "wf-run-v1",
258
- runId: "run-legacy-v1",
259
- state: { status: "running", calls: [], trace: [], errorLogs: [] },
260
- meta: { startedAt: new Date().toISOString() },
261
- };
262
- fs.writeFileSync(filePath, JSON.stringify(legacySnapshot) + "\n", "utf8");
263
-
264
- const entries: CustomEntry[] = [
265
- {
266
- type: "custom",
267
- customType: "workflow-state-link",
268
- data: { runId: "run-legacy-v1", path: filePath },
269
- id: "seed-pointer",
270
- parentId: null,
271
- timestamp: new Date().toISOString(),
272
- },
273
- ];
274
- const store = new JsonlRunStore({ sessionDir: tmpDir, ctx: mkCtx(entries) });
275
-
276
- // 版本不匹配 → deserializeRun 返回 null → loadAll 跳过(空数组),不崩
277
- await expect(store.loadAll()).resolves.toEqual([]);
278
- });
279
-
280
- it("新快照 version === wf-run-v2:status 两态、meta 投影无 pausedAt", async () => {
281
- const store = new JsonlRunStore({ sessionDir: tmpDir });
282
- const run = makeRunningRun("run-v2-check");
283
- await store.save(run);
284
-
285
- const raw = fs.readFileSync(
286
- path.join(tmpDir, "workflow-state", "run-v2-check.jsonl"),
287
- "utf8",
288
- );
289
- const snapshot = JSON.parse(raw.trim()) as {
290
- v: string;
291
- state: { status: string };
292
- meta: Record<string, unknown>;
293
- };
294
- // 版本头 v2(持久化契约锚定字面量)
295
- expect(snapshot.v).toBe("wf-run-v2");
296
- // status 两态(running/done)
297
- expect(snapshot.state.status).toBe("running");
298
- // F6:meta 投影不再含 pausedAt 字段
299
- expect(snapshot.meta).not.toHaveProperty("pausedAt");
300
- });
301
- });
302
-
303
- // W3: save 兜底容错——run 工作目录被并发清理时 mkdir 抛 ENOENT,save 静默返回。
304
- //
305
- // 防的 bug(PR #166 CI 回归):review-fix-loop-e2e 等 runAndWait 测试中,
306
- // handleReturn 的 run.transition("done") 同步改 status 后,runAndWait 轮询发现 done
307
- // 并 resolve,测试 afterEach 随即 rmSync 删除 sessionDir;此时 handleReturn 内 in-flight
308
- // 的 await save 尚未完成,mkdir 遇到目录链被并发删除 → ENOENT。原实现 await save 让错误
309
- // 冒泡为 unhandled promise rejection(worker-host onMessage 无 catch),CI exit 1。
310
- // 修复:save 仅容错 ENOENT(run 已终态,状态不再变化,持久化无意义也无法完成)→ silent return;
311
- // 非 ENOENT 错误(EACCES/ENOSPC 等真实磁盘问题)仍重新抛出,不掩盖。
312
- //
313
- // 注:真正的 ENOENT 只在 rmSync 与 mkdir 并发时出现(串行 rmSync 后 mkdir {recursive:true}
314
- // 会重建目录而非抛 ENOENT),故用 mock 直接锁定 save 的容错判定逻辑,不依赖竞态时序复现。
315
- describe("W3: JsonlRunStore.save 兜底容错(run 工作目录被并发清理时的竞态)", () => {
316
- let tmpDir: string;
317
- let store: JsonlRunStore;
318
-
319
- beforeEach(() => {
320
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-store-enoent-"));
321
- store = new JsonlRunStore({ sessionDir: tmpDir });
322
- });
323
-
324
- afterEach(() => {
325
- fs.rmSync(tmpDir, { recursive: true, force: true });
326
- vi.restoreAllMocks();
327
- });
328
-
329
- it("mkdir 抛 ENOENT(sessionDir 被并发清理)→ save 静默返回,不抛 unhandled rejection", async () => {
330
- const run = makeRunWithDoneCall();
331
- const spy = vi
332
- .spyOn(fs.promises, "mkdir")
333
- .mockRejectedValueOnce(
334
- Object.assign(new Error("ENOENT: no such file or directory, mkdir"), {
335
- code: "ENOENT",
336
- }),
337
- );
338
- // run 已终态 + 工作目录消失 → save 放弃持久化,resolve undefined(不抛)
339
- await expect(store.save(run)).resolves.toBeUndefined();
340
- expect(spy).toHaveBeenCalled();
341
- });
342
-
343
- it("mkdir 抛非 ENOENT 错误(EACCES)→ save 重新抛出,不掩盖真实磁盘问题", async () => {
344
- const run = makeRunWithDoneCall();
345
- const spy = vi
346
- .spyOn(fs.promises, "mkdir")
347
- .mockRejectedValueOnce(
348
- Object.assign(new Error("permission denied"), { code: "EACCES" }),
349
- );
350
- await expect(store.save(run)).rejects.toThrow("permission denied");
351
- expect(spy).toHaveBeenCalled();
352
- });
353
- });
354
-
355
- // ── W4-W8: save 去抖(cw swf-perf wave2,W2TC1-15)──────────────────────
356
- //
357
- // 状态机前提:热路径 = running 中间态且本实例已首写(writtenOnce 已记)→ 进去抖批;
358
- // 冷路径 = 本实例首写(任何 status)或 status !== "running"(done)→ 同步
359
- // flush 绕过 timer。所有用例先做一次冷路径首写 + await 落盘,再进热路径。
360
- //
361
- // fake timers 惯例对齐 lifecycle.test.ts(vi.useFakeTimers + advanceTimersByTimeAsync);
362
- // writeFile/mkdir 计数用 vi.spyOn 保留原实现(真实落盘,读磁盘断言内容)。
363
-
364
- describe("W4: save 去抖(热路径合并 / 冷路径同步 flush)", () => {
365
- let tmpDir: string;
366
- let store: JsonlRunStore;
367
-
368
- beforeEach(() => {
369
- vi.useFakeTimers();
370
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-store-debounce-"));
371
- store = new JsonlRunStore({ sessionDir: tmpDir });
372
- });
373
-
374
- afterEach(() => {
375
- vi.useRealTimers();
376
- vi.restoreAllMocks();
377
- fs.rmSync(tmpDir, { recursive: true, force: true });
378
- });
379
-
380
- it("W2TC1: 热路径去抖合并:3 次 save 合并 1 次写盘、内容为 flush 时刻最新状态", async () => {
381
- const run = makeRunningRun("run-w2tc1");
382
- // 首写冷路径立即落盘(writtenOnce 记录,热路径前提)
383
- await store.save(run);
384
- const wfSpy = vi.spyOn(fs.promises, "writeFile");
385
-
386
- // 热路径 3 次 save(同一 run 引用 mutate,中间态演进)
387
- run.state.trace.append(makeTraceNode(1));
388
- const p1 = store.save(run);
389
- run.state.trace.append(makeTraceNode(2));
390
- const p2 = store.save(run);
391
- const p3 = store.save(run);
392
-
393
- // advance 前:无写盘,文件停留首写状态(1 个节点)
394
- expect(wfSpy).not.toHaveBeenCalled();
395
- expect(readStateFile(tmpDir, "run-w2tc1").state.trace).toHaveLength(1);
396
-
397
- await vi.advanceTimersByTimeAsync(200);
398
- // 3 个 save() Promise 全部 resolved(await 落盘完成——fake timers 不等真实 IO)
399
- await Promise.all([p1, p2, p3]);
400
- // N=3 合并 1 次写盘
401
- expect(wfSpy).toHaveBeenCalledTimes(1);
402
- // latestRun 语义:写的是 flush 时刻最新聚合状态(node1 + node2 都在)
403
- const snap = readStateFile(tmpDir, "run-w2tc1");
404
- const steps = snap.state.trace.map((n) => n.stepIndex);
405
- expect(steps).toContain(1);
406
- expect(steps).toContain(2);
407
- });
408
-
409
- it("W2TC2: 终态 save 绕过 timer 立即落盘:合并 pending 批 + 取消 timer 无二次写", async () => {
410
- const run = makeRunningRun("run-w2tc2");
411
- await store.save(run); // 首写
412
- const wfSpy = vi.spyOn(fs.promises, "writeFile");
413
-
414
- const p1 = store.save(run); // 热路径批 pending(1 个未 settle Promise)
415
- run.transition("done", "completed");
416
- const p2 = store.save(run); // 终态冷路径
417
- // pending 批(save #1)settlers 并入终态批合并 settle(await 落盘完成)
418
- await p1;
419
- await p2;
420
-
421
- // 不 advance(timer 未走):终态已落盘
422
- expect(readStateFile(tmpDir, "run-w2tc2").state.status).toBe("done");
423
-
424
- // timer 已取消:advance 后无第二次写,终态内容不被中间态覆盖
425
- await vi.advanceTimersByTimeAsync(1000);
426
- expect(wfSpy).toHaveBeenCalledTimes(1);
427
- expect(readStateFile(tmpDir, "run-w2tc2").state.status).toBe("done");
428
- });
429
-
430
- it("W2TC3: 同批多次调用共享同一 flush Promise——timer 触发路径全 resolved", async () => {
431
- const run = makeRunningRun("run-w2tc3a");
432
- await store.save(run);
433
- const p1 = store.save(run);
434
- const p2 = store.save(run);
435
- const p3 = store.save(run);
436
- await vi.advanceTimersByTimeAsync(200);
437
- // 同批全部 resolved(成功场景无 rejected)
438
- await Promise.all([p1, p2, p3]);
439
- });
440
-
441
- it("W2TC3: 冷路径合并 pending 批 settlers 共同 settle——await p4 后 p1-p3 全部 resolved", async () => {
442
- const run = makeRunningRun("run-w2tc3b");
443
- await store.save(run);
444
- const p1 = store.save(run);
445
- const p2 = store.save(run);
446
- const p3 = store.save(run);
447
- run.transition("done", "completed");
448
- const p4 = store.save(run); // 冷路径合并 p1-p3 的批
449
- await p4; // 终态写盘完成后 resolve
450
- // settlers 数组统一 settle,无悬挂 Promise(防 unhandled rejection)
451
- await Promise.all([p1, p2, p3]);
452
- expect(readStateFile(tmpDir, "run-w2tc3b").state.status).toBe("done");
453
- });
454
-
455
- it("W2TC7: 终态冷路径同步 flush:立即落盘(绕过 timer)+ 终态 workflow-record entry", async () => {
456
- const mockPi = mkPi();
457
- const store7 = new JsonlRunStore({ sessionDir: tmpDir, pi: mockPi });
458
- const run = makeRunningRun("run-w2tc7");
459
- await store7.save(run); // 首写 + entry
460
- expect(mockPi.appendEntry).toHaveBeenCalledTimes(1);
461
-
462
- const pHot = store7.save(run); // 热路径批 pending
463
- run.transition("done", "completed");
464
- await store7.save(run); // 终态冷路径
465
-
466
- // 不 advance 立即读磁盘:终态优先持久化(去抖窗口内的崩溃不吞终态)
467
- expect(readStateFile(tmpDir, "run-w2tc7").state.status).toBe("done");
468
- // 合并的热路径批 Promise 一并 resolved
469
- await pHot;
470
- // 终态冷路径合并批 1 次 flush → 1 条终态 entry(首写 + 终态 = 2 条)
471
- expect(mockPi.appendEntry).toHaveBeenCalledTimes(2);
472
- // advance 后无追加写
473
- await vi.advanceTimersByTimeAsync(1000);
474
- expect(mockPi.appendEntry).toHaveBeenCalledTimes(2);
475
- });
476
-
477
- it("W2TC8: 首写冷路径(跨 session resume):本 store 实例首 save 立即落盘 + entry", async () => {
478
- const mockPi = mkPi();
479
- const storeA = new JsonlRunStore({ sessionDir: tmpDir, pi: mockPi });
480
- const run = makeRunningRun("run-w2tc8");
481
-
482
- // 实例 A 首写:status 是 running 也立即落盘(首写判定优先于 status)
483
- await storeA.save(run);
484
- expect(readStateFile(tmpDir, "run-w2tc8").state.status).toBe("running");
485
- // 首写即写 entry(即使 status 是 running——新 store 实例对该 runId 的首写就落 entry)
486
- expect(mockPi.appendEntry).toHaveBeenCalledTimes(1);
487
-
488
- // 实例 B(另一 session 的 store)对同一 runId 再 save:又是一次实例首写
489
- const storeB = new JsonlRunStore({ sessionDir: tmpDir, pi: mockPi });
490
- run.state.trace.append(makeTraceNode(9));
491
- await storeB.save(run);
492
- // 跨实例各 1 条 entry(实例级 writtenOnce 判定)
493
- expect(mockPi.appendEntry).toHaveBeenCalledTimes(2);
494
- expect(readStateFile(tmpDir, "run-w2tc8").state.trace).toHaveLength(2);
495
- });
496
-
497
- it("W2TC15: saveDebounceMs 构造参数可调:短参数窗口生效", async () => {
498
- const store50 = new JsonlRunStore({ sessionDir: tmpDir, saveDebounceMs: 50 });
499
- const run = makeRunningRun("run-w2tc15");
500
- await store50.save(run); // 首写
501
- const wfSpy = vi.spyOn(fs.promises, "writeFile");
502
-
503
- run.state.trace.append(makeTraceNode(1));
504
- const p = store50.save(run);
505
- await vi.advanceTimersByTimeAsync(49);
506
- expect(wfSpy).not.toHaveBeenCalled(); // 窗口未到
507
- await vi.advanceTimersByTimeAsync(1);
508
- await p; // flush 落盘完成
509
- expect(wfSpy).toHaveBeenCalledTimes(1); // 50ms 参数生效
510
- });
511
- });
512
-
513
- describe("W5: 批 settle 与 IO 错误语义", () => {
514
- let tmpDir: string;
515
- let store: JsonlRunStore;
516
-
517
- beforeEach(() => {
518
- vi.useFakeTimers();
519
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-store-ioerr-"));
520
- store = new JsonlRunStore({ sessionDir: tmpDir });
521
- });
522
-
523
- afterEach(() => {
524
- vi.useRealTimers();
525
- vi.restoreAllMocks();
526
- fs.rmSync(tmpDir, { recursive: true, force: true });
527
- });
528
-
529
- it("W2TC4: flush IO 错误 reject 批内全部调用 + 失败不粘滞(下一 save 开新批)", async () => {
530
- const run = makeRunningRun("run-w2tc4");
531
- await store.save(run); // 首写
532
- vi.spyOn(fs.promises, "writeFile").mockRejectedValueOnce(
533
- Object.assign(new Error("permission denied"), { code: "EACCES" }),
534
- );
535
-
536
- const p1 = store.save(run);
537
- const p2 = store.save(run); // 同批两次调用
538
- await vi.advanceTimersByTimeAsync(200);
539
- // 批内全部 Promise rejects 同一错误(EACCES 消息传播)
540
- await expect(p1).rejects.toThrow("permission denied");
541
- await expect(p2).rejects.toThrow("permission denied");
542
-
543
- // 失败不粘滞:pending Map 条目已清除,下一 save 开新批正常 flush 成功
544
- const p3 = store.save(run);
545
- await vi.advanceTimersByTimeAsync(200);
546
- await p3;
547
- expect(readStateFile(tmpDir, "run-w2tc4").state.status).toBe("running");
548
- });
549
-
550
- it("W2TC4(ES9): 首写失败回滚 writtenOnce——running 中间态 save 重走冷路径补写指针", async () => {
551
- const mockPi = mkPi();
552
- const store4 = new JsonlRunStore({ sessionDir: tmpDir, pi: mockPi });
553
- const run = makeRunningRun("run-w2tc4b");
554
-
555
- // 首写冷路径遇 writeFile EACCES reject
556
- vi.spyOn(fs.promises, "writeFile").mockRejectedValueOnce(
557
- Object.assign(new Error("permission denied"), { code: "EACCES" }),
558
- );
559
- await expect(store4.save(run)).rejects.toThrow("permission denied");
560
- expect(mockPi.appendEntry).not.toHaveBeenCalled(); // entry 未写(写在 writeFile 成功之后)
561
-
562
- // 恢复 IO 后 running 中间态 save:不经 timer 立即落盘(首写资格已回滚,冷路径重试 entry)
563
- const p = store4.save(run);
564
- await p; // 冷路径同步 flush 完成
565
- expect(readStateFile(tmpDir, "run-w2tc4b").state.status).toBe("running");
566
- expect(mockPi.appendEntry).toHaveBeenCalledTimes(1); // entry 经冷路径补写
567
- });
568
-
569
- it("W2TC5: ENOENT 静默语义保留——热路径去抖批 mkdir ENOENT resolve 全部批 Promise", async () => {
570
- const run = makeRunningRun("run-w2tc5");
571
- await store.save(run); // 首写
572
- vi.spyOn(fs.promises, "mkdir").mockRejectedValueOnce(
573
- Object.assign(new Error("ENOENT: no such file or directory, mkdir"), {
574
- code: "ENOENT",
575
- }),
576
- );
577
-
578
- const p1 = store.save(run); // 热路径批 pending
579
- await vi.advanceTimersByTimeAsync(200);
580
- await p1; // resolved(不抛 unhandled rejection)
581
- // 无新写入:内容停留首写状态
582
- expect(readStateFile(tmpDir, "run-w2tc5").state.trace).toHaveLength(1);
583
- });
584
-
585
- it("W2TC5: 冷路径 mkdir ENOENT → save Promise resolves(对齐 W3 既有语义)", async () => {
586
- const run = makeRunningRun("run-w2tc5b");
587
- // running 首写冷路径(与 W3 的 done 用例互补):mkdir ENOENT → 静默 resolve
588
- vi.spyOn(fs.promises, "mkdir").mockRejectedValueOnce(
589
- Object.assign(new Error("ENOENT: no such file or directory, mkdir"), {
590
- code: "ENOENT",
591
- }),
592
- );
593
- await expect(store.save(run)).resolves.toBeUndefined();
594
- });
595
- });
596
-
597
- describe("W6: workflow-record entry 计数(= flush 次数;save 级不放大)", () => {
598
- let tmpDir: string;
599
-
600
- beforeEach(() => {
601
- vi.useFakeTimers();
602
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-store-ptr-"));
603
- });
604
-
605
- afterEach(() => {
606
- vi.useRealTimers();
607
- vi.restoreAllMocks();
608
- fs.rmSync(tmpDir, { recursive: true, force: true });
609
- });
610
-
611
- it("W2TC6(W17): entry 计数 = flush 次数:首写+终态各 1、中间去抖批合并(N save → 1 entry)、终态 entry 含 done", async () => {
612
- const mockPi = mkPi();
613
- const store6 = new JsonlRunStore({ sessionDir: tmpDir, pi: mockPi });
614
- const run = makeRunningRun("run-w2tc6");
615
-
616
- // 创建首写 flush → 1 条 entry
617
- await store6.save(run);
618
- expect(mockPi.appendEntry).toHaveBeenCalledTimes(1);
619
-
620
- // 3 轮 running 中间态:每轮窗口内 2 次 save(热路径批合并)→ 各 1 次 flush → 各 1 条 entry
621
- for (let i = 1; i <= 3; i++) {
622
- run.state.trace.append(makeTraceNode(i));
623
- const p1 = store6.save(run);
624
- const p2 = store6.save(run);
625
- await vi.advanceTimersByTimeAsync(200);
626
- await Promise.all([p1, p2]);
627
- }
628
- expect(mockPi.appendEntry).toHaveBeenCalledTimes(4); // save 级不放大(2 save → 1 flush → 1 entry)
629
-
630
- // done 终态 flush → 1 条 entry,快照携带终态(验收「entry 序列含终态」)
631
- run.transition("done", "completed");
632
- await store6.save(run);
633
- expect(mockPi.appendEntry).toHaveBeenCalledTimes(5);
634
- const calls = mockPi.appendEntry.mock.calls;
635
- expect(calls[4]![0]).toBe(WORKFLOW_RECORD_CUSTOM_TYPE);
636
- expect(asRecordData(calls[4]![1]).snapshot.state.status).toBe("done");
637
- });
638
- });
639
-
640
- describe("W7: flushPendingSaves / dispose / 串行链", () => {
641
- let tmpDir: string;
642
- let store: JsonlRunStore;
643
-
644
- beforeEach(() => {
645
- vi.useFakeTimers();
646
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-store-dispose-"));
647
- store = new JsonlRunStore({ sessionDir: tmpDir });
648
- loggerMock.debug.mockClear();
649
- });
650
-
651
- afterEach(() => {
652
- vi.useRealTimers();
653
- vi.restoreAllMocks();
654
- fs.rmSync(tmpDir, { recursive: true, force: true });
655
- });
656
-
657
- it("W2TC9: flushPendingSaves 立即刷全部 pending 批且 store 保持可用", async () => {
658
- const runA = makeRunningRun("run-w2tc9a");
659
- const runB = makeRunningRun("run-w2tc9b");
660
- await store.save(runA);
661
- await store.save(runB); // 两 runId 首写
662
-
663
- const pa = store.save(runA); // 两批独立 pending
664
- runB.state.trace.append(makeTraceNode(1));
665
- const pb = store.save(runB);
666
-
667
- await store.flushPendingSaves(); // 不 advance 直接刷
668
- expect(readStateFile(tmpDir, "run-w2tc9a").state.status).toBe("running");
669
- expect(readStateFile(tmpDir, "run-w2tc9b").state.trace).toHaveLength(2);
670
- await Promise.all([pa, pb]); // 两批 save Promise 全部 resolved
671
-
672
- // store 保持可用:后续 save 正常进入新去抖批
673
- const pa2 = store.save(runA);
674
- await vi.advanceTimersByTimeAsync(200);
675
- await pa2;
676
- });
677
-
678
- it("W2TC10: dispose 刷 pending + 停 timer + 幂等 + dispose 后 save 静默 no-op + debug 日志", async () => {
679
- const run = makeRunningRun("run-w2tc10");
680
- await store.save(run); // 首写
681
- const wfSpy = vi.spyOn(fs.promises, "writeFile");
682
- const pHot = store.save(run); // 热路径批 pending
683
- wfSpy.mockClear(); // 清零基准
684
-
685
- const d1 = store.dispose();
686
- const d2 = store.dispose(); // 并发交叠第二次
687
- expect(d1).toBe(d2); // 同一 Promise 引用(dispose 缓存自身 Promise,幂等)
688
- await Promise.all([d1, d2]);
689
- await pHot; // pending 批已刷、settled
690
-
691
- expect(wfSpy).toHaveBeenCalledTimes(1); // 第二次 dispose 不重复刷(清零基准 === 1)
692
- const d3 = store.dispose(); // 串行第三次
693
- expect(d3).toBe(d1); // 返回同一已 resolve 的 Promise
694
- await d3;
695
-
696
- // timer 清除:advance 后无追加写
697
- await vi.advanceTimersByTimeAsync(1000);
698
- expect(wfSpy).toHaveBeenCalledTimes(1);
699
-
700
- // dispose 后 save 返回 resolved Promise 且不写盘(静默 no-op)
701
- run.transition("done", "completed");
702
- await expect(store.save(run)).resolves.toBeUndefined();
703
- expect(wfSpy).toHaveBeenCalledTimes(1);
704
-
705
- // debug 日志留痕(R5 断言锚点:含 runId,不外抛)
706
- expect(loggerMock.debug).toHaveBeenCalled();
707
- const debugDump = loggerMock.debug.mock.calls.map((c) => String(c[0])).join("\n");
708
- expect(debugDump).toContain("run-w2tc10");
709
- });
710
-
711
- it("W2TC11: per-runId 串行 flush 链——前一 flush in-flight 时后续 flush 排队,无并发 writeFile", async () => {
712
- const run = makeRunningRun("run-w2tc11");
713
- await store.save(run); // 首写
714
- // mkdir 立即 resolve:时序控制点收敛到 writeFile(真实 mkdir 的 IO 完成时机
715
- // 不受 fake timers 管,会挡住 writeFile 的调用观察)
716
- vi.spyOn(fs.promises, "mkdir").mockResolvedValue(undefined);
717
- const wfSpy = vi.spyOn(fs.promises, "writeFile");
718
-
719
- // 第一次 writeFile 调用返回手动 gate 控制的 pending Promise,之后恢复原实现
720
- let gateResolve!: () => void;
721
- const gate = new Promise<void>((r) => {
722
- gateResolve = r;
723
- });
724
- wfSpy.mockImplementationOnce(() => gate);
725
-
726
- run.state.trace.append(makeTraceNode(1));
727
- const p1 = store.save(run); // save#1
728
- await vi.advanceTimersByTimeAsync(200); // flush#1 触发,writeFile#1 挂起
729
- expect(wfSpy).toHaveBeenCalledTimes(1);
730
-
731
- run.state.trace.append(makeTraceNode(2));
732
- const p2 = store.save(run); // save#2 新批
733
- await vi.advanceTimersByTimeAsync(200); // flush#2 timer 到点,排队等待 flush#1
734
- expect(wfSpy).toHaveBeenCalledTimes(1); // writeFile 仅 1 次(无并发)
735
-
736
- gateResolve(); // 释放 flush#1
737
- await p1;
738
- await p2; // flush#2 在 flush#1 完成后顺序执行
739
- expect(wfSpy).toHaveBeenCalledTimes(2);
740
- // flush#2 落盘 save#2 时刻的最新状态
741
- expect(readStateFile(tmpDir, "run-w2tc11").state.trace).toHaveLength(3);
742
- });
743
-
744
- it("W2TC11: 链上前序 flush 失败不传染后续批(链尾吞链错误,错误只经批 Promise 传播)", async () => {
745
- const run = makeRunningRun("run-w2tc11b");
746
- await store.save(run); // 首写
747
- vi.spyOn(fs.promises, "writeFile").mockRejectedValueOnce(
748
- Object.assign(new Error("permission denied"), { code: "EACCES" }),
749
- );
750
-
751
- const p1 = store.save(run);
752
- await vi.advanceTimersByTimeAsync(200);
753
- await expect(p1).rejects.toThrow("permission denied"); // flush#1 失败经批 Promise 传播
754
-
755
- // 链未断:flush#2 正常执行并落盘
756
- run.state.trace.append(makeTraceNode(1));
757
- const p2 = store.save(run);
758
- await vi.advanceTimersByTimeAsync(200);
759
- await p2;
760
- expect(readStateFile(tmpDir, "run-w2tc11b").state.trace).toHaveLength(2);
761
- });
762
-
763
- it("W2TC14: 不同 runId 批互不阻塞:并发 workflow 各自独立去抖", async () => {
764
- const runA = makeRunningRun("run-w2tc14a");
765
- const runB = makeRunningRun("run-w2tc14b");
766
- await store.save(runA);
767
- await store.save(runB); // 两 runId 首写
768
-
769
- // runA 的 writeFile 挂 gate(慢 IO),其他路径(runB)正常;mkdir 立即 resolve
770
- //(时序控制点收敛到 writeFile,真实 mkdir 的 IO 完成不受 fake timers 管)
771
- vi.spyOn(fs.promises, "mkdir").mockResolvedValue(undefined);
772
- const realWriteFile = fs.promises.writeFile.bind(fs.promises);
773
- const pathA = path.join(tmpDir, "workflow-state", "run-w2tc14a.jsonl");
774
- let gateResolve!: () => void;
775
- const gate = new Promise<void>((r) => {
776
- gateResolve = r;
777
- });
778
- vi.spyOn(fs.promises, "writeFile").mockImplementation(
779
- (p: unknown, ...rest: unknown[]) =>
780
- String(p) === pathA
781
- ? gate.then(() =>
782
- (realWriteFile as (p: unknown, ...r: unknown[]) => Promise<void>)(p, ...rest),
783
- )
784
- : (realWriteFile as (p: unknown, ...r: unknown[]) => Promise<void>)(p, ...rest),
785
- );
786
-
787
- const pa = store.save(runA); // runA 批
788
- runA.state.trace.append(makeTraceNode(1)); // 中间态演进(serialize-at-flush)
789
- runB.state.trace.append(makeTraceNode(1));
790
- const pb = store.save(runB); // runB 批
791
- await vi.advanceTimersByTimeAsync(200);
792
- await pb; // runB 真实写盘完成
793
-
794
- // runA flush 挂起中,runB 已落盘(无全局锁,per-runId 独立链)
795
- expect(readStateFile(tmpDir, "run-w2tc14b").state.trace).toHaveLength(2);
796
- // runA 文件停留首写状态(热批被 gate 挂起未写入)
797
- expect(readStateFile(tmpDir, "run-w2tc14a").state.trace).toHaveLength(1);
798
-
799
- gateResolve(); // 释放 runA
800
- await pa;
801
- expect(readStateFile(tmpDir, "run-w2tc14a").state.trace).toHaveLength(2);
802
- });
803
- });
804
-
805
- describe("W8: 去抖窗口崩溃语义与 timer unref", () => {
806
- let tmpDir: string;
807
- let store: JsonlRunStore;
808
-
809
- beforeEach(() => {
810
- vi.useFakeTimers();
811
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-store-crash-"));
812
- store = new JsonlRunStore({ sessionDir: tmpDir });
813
- });
814
-
815
- afterEach(() => {
816
- vi.useRealTimers();
817
- vi.restoreAllMocks();
818
- vi.unstubAllGlobals();
819
- fs.rmSync(tmpDir, { recursive: true, force: true });
820
- });
821
-
822
- it("W2TC12: 去抖 timer 必须 unref(不钉住 extension 进程)", async () => {
823
- const fakeTimer = { unref: vi.fn(), ref: vi.fn() };
824
- const setTimeoutSpy = vi.fn(() => fakeTimer);
825
- const clearTimeoutSpy = vi.fn();
826
- vi.stubGlobal("setTimeout", setTimeoutSpy);
827
- vi.stubGlobal("clearTimeout", clearTimeoutSpy);
828
-
829
- const run = makeRunningRun("run-w2tc12");
830
- // 全程用同一 store50 实例:首写必须落在 store50 上(writtenOnce 是 per-instance,
831
- // 用另一个实例首写会让本实例的 save 走冷路径,测不到建批 timer)
832
- const store50 = new JsonlRunStore({ sessionDir: tmpDir, saveDebounceMs: 50 });
833
- await store50.save(run); // 首写冷路径:不经 timer
834
- expect(setTimeoutSpy).not.toHaveBeenCalled();
835
-
836
- const p = store50.save(run); // 热路径建批
837
- // setTimeout 以构造参数 50 被调用,返回的 timer unref() 恰 1 次
838
- expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
839
- expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 50);
840
- expect(fakeTimer.unref).toHaveBeenCalledTimes(1);
841
-
842
- void store50.save(run); // 并入已有批:不重复创建 timer
843
- expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
844
- // 不 advance(timer 是 stub 假对象永不触发);save Promise 由 store 实例持有,
845
- // tmpDir 随 afterEach 清理,无悬挂写盘
846
- void p;
847
- });
848
-
849
- it("W2TC13: 去抖窗口崩溃语义:未 flush 的 run 对 loadAll 不可见(丢失边界 = 最后一次成功 flush)", async () => {
850
- // mockPi + mockCtx entries 数组模式(对齐 W1 round-trip 用例)
851
- const entries: CustomEntry[] = [];
852
- const mockPi = mkPi(entries);
853
- const mockCtx = mkCtx(entries);
854
-
855
- const storeA = new JsonlRunStore({
856
- sessionDir: tmpDir,
857
- pi: mockPi,
858
- ctx: mockCtx,
859
- });
860
- const run = makeRunningRun("run-w2tc13");
861
- await storeA.save(run); // 首写冷路径落盘 + 创建指针
862
-
863
- // 去抖窗口内「崩溃」:中间态批 pending(不 advance;save Promise 留引用供
864
- // 对照阶段 await,防 unhandled rejection)
865
- run.state.trace.append(makeTraceNode(1));
866
- const pHot = storeA.save(run);
867
-
868
- // 重启恢复:新 store 实例 loadAll 只能看到最后一次成功 flush 的状态
869
- const storeB = new JsonlRunStore({ sessionDir: tmpDir, ctx: mkCtx(entries) });
870
- const loaded1 = await storeB.loadAll();
871
- expect(loaded1).toHaveLength(1);
872
- expect(loaded1[0]!.state.trace.toArray()).toHaveLength(1); // 中间态丢失(崩溃窗口 ≤saveDebounceMs 的已接受语义)
873
-
874
- // 对照锚点:advance 后(无崩溃)新 loadAll 返回含中间态的最新快照
875
- await vi.advanceTimersByTimeAsync(200);
876
- await pHot; // 热批 flush 落盘完成
877
- const loaded2 = await storeB.loadAll();
878
- expect(loaded2).toHaveLength(1);
879
- expect(loaded2[0]!.state.trace.toArray()).toHaveLength(2);
880
- });
881
- });
882
-
883
- // ── W17: workflow-record 自描述 entry(D4 收敛:entry > state 文件 > 空)──────
884
- //
885
- // 持久化形态从「state 文件 + workflow-state-link 指针 entry」收敛为自描述完整记录:
886
- // 每次成功 flush append 一条 workflow-record entry(pi 文件 = 持久化权威),state 文件
887
- // 降级纯性能缓存;旧 link entry 兼容读取(优先级低,存量 run 不静默丢失——#9 踩坑)。
888
-
889
- describe("W17: workflow-record 自描述 entry 重建(entry > state 文件 > 空)", () => {
890
- let tmpDir: string;
891
-
892
- beforeEach(() => {
893
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-store-w17-"));
894
- });
895
-
896
- afterEach(() => {
897
- fs.rmSync(tmpDir, { recursive: true, force: true });
898
- });
899
-
900
- it("customType 常量字面量钉住:WORKFLOW_RECORD_CUSTOM_TYPE === 'workflow-record'", () => {
901
- // 写点引用常量(单源);本断言钉住常量与消费方(W18 runtime extractor)约定的
902
- // 字面量拼写,防重命名漂移后静默丢重建。
903
- expect(WORKFLOW_RECORD_CUSTOM_TYPE).toBe("workflow-record");
904
- });
905
-
906
- it("save 落 workflow-record entry 序列:customType + v1 + 完整快照(running → done 终态)", async () => {
907
- const entries: CustomEntry[] = [];
908
- const store = new JsonlRunStore({ sessionDir: tmpDir, pi: mkPi(entries) });
909
- const run = makeRunningRun("run-w17-shape");
910
- await store.save(run); // 首写 flush → entry 1(running)
911
- run.transition("done", "completed");
912
- await store.save(run); // 终态 flush → entry 2(done)
913
-
914
- const wfEntries = entries.filter((e) => e.customType === WORKFLOW_RECORD_CUSTOM_TYPE);
915
- expect(wfEntries).toHaveLength(2);
916
-
917
- const first = asRecordData(wfEntries[0]!.data);
918
- expect(first.v).toBe(1);
919
- expect(first.snapshot.runId).toBe("run-w17-shape");
920
- expect(first.snapshot.state.status).toBe("running");
921
-
922
- const last = asRecordData(wfEntries[1]!.data);
923
- expect(last.v).toBe(1);
924
- expect(last.snapshot.state.status).toBe("done"); // entry 序列含终态
925
- });
926
-
927
- it("新 entry 重建用例:loadAll 优先扫 workflow-record——state 文件删除后仍完整重建(纯性能缓存证明)", async () => {
928
- const entries: CustomEntry[] = [];
929
- const storeA = new JsonlRunStore({
930
- sessionDir: tmpDir,
931
- pi: mkPi(entries),
932
- ctx: mkCtx(entries),
933
- });
934
- const run = makeRunWithDoneCall();
935
- await storeA.save(run);
936
-
937
- // 删除 state 文件(模拟缓存清理/丢失)——entry 是唯一残留源
938
- fs.rmSync(path.join(tmpDir, "workflow-state", "run-test-001.jsonl"));
939
-
940
- const storeB = new JsonlRunStore({ sessionDir: tmpDir, ctx: mkCtx(entries) });
941
- const loaded = await storeB.loadAll();
942
- expect(loaded).toHaveLength(1);
943
- expect(loaded[0]!.runId).toBe("run-test-001");
944
- expect(loaded[0]!.state.status).toBe("done");
945
- // 自描述快照完整:AgentCall.sessionFile 等重水合字段在位(不依赖 state 文件)
946
- expect(loaded[0]!.state.calls.get(0)!.sessionFile).toBe(
947
- "/abs/.pi/agent/subagents/enc/sessions/2026-07-15T_session-abc.jsonl",
948
- );
949
- });
950
-
951
- it("旧 link 兼容用例:存量 session(workflow-state-link + state 文件,无 workflow-record entry)→ loadAll 经 link 重建", async () => {
952
- // 存量形态构造:旧版扩展(无 pi 注入路径)只落 state 文件,session JSONL 里有 link 指针
953
- const storeA = new JsonlRunStore({ sessionDir: tmpDir });
954
- await storeA.save(makeRunWithDoneCall());
955
- const filePath = path.join(tmpDir, "workflow-state", "run-test-001.jsonl");
956
- expect(fs.existsSync(filePath)).toBe(true);
957
-
958
- const entries: CustomEntry[] = [
959
- {
960
- type: "custom",
961
- customType: "workflow-state-link",
962
- data: { runId: "run-test-001", path: filePath },
963
- id: "seed-pointer",
964
- parentId: null,
965
- timestamp: new Date().toISOString(),
966
- },
967
- ];
968
- const storeB = new JsonlRunStore({ sessionDir: tmpDir, ctx: mkCtx(entries) });
969
- const loaded = await storeB.loadAll();
970
- expect(loaded).toHaveLength(1); // 存量 run 不静默丢失(#9)
971
- expect(loaded[0]!.state.calls.get(0)!.sessionFile).toBe(
972
- "/abs/.pi/agent/subagents/enc/sessions/2026-07-15T_session-abc.jsonl",
973
- );
974
- });
975
-
976
- it("读序优先级:同 runId 既有 workflow-record entry 又有旧 link(state 文件为旧 running 快照)→ entry 终态胜出", async () => {
977
- const entries: CustomEntry[] = [];
978
- const storeA = new JsonlRunStore({ sessionDir: tmpDir, pi: mkPi(entries) });
979
- const run = makeRunningRun("run-w17-prio");
980
- await storeA.save(run); // entry 1(running)+ state 文件(running)
981
- run.transition("done", "completed");
982
- await storeA.save(run); // entry 2(done)+ state 文件(done)
983
-
984
- // 把 state 文件回写为旧 running 快照(从 entry 1 提取完整快照),并 seed 旧 link 指针
985
- const filePath = path.join(tmpDir, "workflow-state", "run-w17-prio.jsonl");
986
- fs.writeFileSync(
987
- filePath,
988
- JSON.stringify(asRecordData(entries[0]!.data).snapshot) + "\n",
989
- "utf8",
990
- );
991
- const seedEntries: CustomEntry[] = [
992
- ...entries,
993
- {
994
- type: "custom",
995
- customType: "workflow-state-link",
996
- data: { runId: "run-w17-prio", path: filePath },
997
- id: "seed-pointer",
998
- parentId: null,
999
- timestamp: new Date().toISOString(),
1000
- },
1001
- ];
1002
-
1003
- const storeB = new JsonlRunStore({ sessionDir: tmpDir, ctx: mkCtx(seedEntries) });
1004
- const loaded = await storeB.loadAll();
1005
- expect(loaded).toHaveLength(1);
1006
- // entry 最后一条(done)胜出——不被 link 指向的旧 state 文件(running)回退
1007
- expect(loaded[0]!.state.status).toBe("done");
1008
- });
1009
-
1010
- it("entry v guard:v:2 的 workflow-record entry(未来 schema)→ 跳过不崩(对齐 W16 消费约定)", async () => {
1011
- // snapshot 本身是合法 wf-run-v2 快照,但 entry 层 v=2 ≠ 1 → 整条跳过(不猜测解析)
1012
- const capture: CustomEntry[] = [];
1013
- const storeA = new JsonlRunStore({ sessionDir: tmpDir, pi: mkPi(capture) });
1014
- await storeA.save(makeRunningRun("run-w17-v2"));
1015
- const snapshot = asRecordData(capture[0]!.data).snapshot;
1016
-
1017
- const entries: CustomEntry[] = [
1018
- {
1019
- type: "custom",
1020
- customType: WORKFLOW_RECORD_CUSTOM_TYPE,
1021
- data: { v: 2, snapshot },
1022
- id: "seed-future-entry",
1023
- parentId: null,
1024
- timestamp: new Date().toISOString(),
1025
- },
1026
- ];
1027
- const storeB = new JsonlRunStore({ sessionDir: tmpDir, ctx: mkCtx(entries) });
1028
- await expect(storeB.loadAll()).resolves.toEqual([]);
1029
- });
1030
- });