@zhushanwen/pi-subagent-workflow 8.6.0 → 8.8.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.
- package/README.md +5 -11
- package/package.json +13 -20
- package/relay/relay.mjs +1 -1
- package/skills/workflow-script-format/SKILL.md +32 -13
- package/src/host/__tests__/pi-host.test.ts +235 -0
- package/src/host/pi-host.ts +163 -0
- package/src/index.ts +141 -138
- package/src/injectors/__tests__/engine-awareness.test.ts +292 -0
- package/src/injectors/__tests__/engine-section-stability.test.ts +343 -0
- package/src/injectors/__tests__/model-list-injector.test.ts +18 -21
- package/src/injectors/__tests__/subagent-list-injector.test.ts +59 -19
- package/src/injectors/__tests__/workflow-list-injector.test.ts +31 -17
- package/src/injectors/engine-awareness.ts +119 -0
- package/src/injectors/model-list-injector.ts +15 -58
- package/src/injectors/subagent-list-injector.ts +56 -114
- package/src/injectors/workflow-list-injector.ts +37 -70
- package/src/interface/__tests__/detectors.test.ts +48 -37
- package/src/interface/__tests__/subagent-tool-path-guard.test.ts +2 -2
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +6 -3
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +216 -0
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +17 -6
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +38 -2
- package/src/interface/bg-notify-render.ts +7 -17
- package/src/interface/command-actions.ts +5 -15
- package/src/interface/commands.ts +4 -4
- package/src/interface/format.ts +17 -6
- package/src/interface/gui-mappers.ts +18 -22
- package/src/interface/helpers.ts +9 -130
- package/src/interface/list-component.ts +5 -5
- package/src/interface/list-shared.ts +3 -3
- package/src/interface/list-view.ts +3 -3
- package/src/interface/subagent-actions.ts +66 -439
- package/src/interface/subagent-tool-schema.ts +21 -17
- package/src/interface/subagent-tool.ts +12 -7
- package/src/interface/subagents.ts +4 -4
- package/src/interface/tool-render.ts +37 -15
- package/src/interface/tool-workflow-script.ts +34 -76
- package/src/interface/tool-workflow.ts +57 -94
- package/src/interface/views/WorkflowsView.ts +8 -16
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +2 -2
- package/src/interface/views/detail-content.ts +4 -4
- package/src/interface/views/format.ts +23 -62
- package/src/{orchestration/jsonl-run-store.ts → jsonl-run-store.ts} +137 -225
- package/agents/analyst.md +0 -61
- package/agents/coder.md +0 -70
- package/agents/debugger.md +0 -67
- package/agents/doc-reviewer.md +0 -50
- package/agents/explorer.md +0 -64
- package/agents/general-purpose.md +0 -32
- package/agents/orchestrator.md +0 -63
- package/agents/planner.md +0 -54
- package/agents/researcher.md +0 -65
- package/agents/reviewer.md +0 -74
- package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +0 -53
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +0 -1
- package/src/execution/__tests__/agent-registry.test.ts +0 -363
- package/src/execution/__tests__/agent-result-mapper.test.ts +0 -150
- package/src/execution/__tests__/alive-store.test.ts +0 -147
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
- package/src/execution/__tests__/before-agent-start-injection.test.ts +0 -132
- package/src/execution/__tests__/bg-notify-render.test.ts +0 -329
- package/src/execution/__tests__/channel-registry-handshake.test.ts +0 -243
- package/src/execution/__tests__/chat-engine-routing.test.ts +0 -601
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +0 -365
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -190
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +0 -215
- package/src/execution/__tests__/concurrency-pool.test.ts +0 -250
- package/src/execution/__tests__/config.test.ts +0 -110
- package/src/execution/__tests__/conversation-wiring.test.ts +0 -198
- package/src/execution/__tests__/crash-recovery.test.ts +0 -320
- package/src/execution/__tests__/delivery-methods.test.ts +0 -422
- package/src/execution/__tests__/dialog-queue.test.ts +0 -299
- package/src/execution/__tests__/epipe-fallback.test.ts +0 -241
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -243
- package/src/execution/__tests__/execute-nesting.test.ts +0 -323
- package/src/execution/__tests__/execute-options-mapper.test.ts +0 -200
- package/src/execution/__tests__/execution-record.test.ts +0 -1394
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +0 -171
- package/src/execution/__tests__/finalize-record.test.ts +0 -368
- package/src/execution/__tests__/finalized-marker.test.ts +0 -82
- package/src/execution/__tests__/format-schema-instruction.test.ts +0 -169
- package/src/execution/__tests__/format.test.ts +0 -458
- package/src/execution/__tests__/gc-timer.test.ts +0 -184
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +0 -254
- package/src/execution/__tests__/gui-mode-dispatch.test.ts +0 -59
- package/src/execution/__tests__/helpers/mock-extension-api.ts +0 -30
- package/src/execution/__tests__/helpers/spawn-mock.ts +0 -242
- package/src/execution/__tests__/host-mode.test.ts +0 -87
- package/src/execution/__tests__/index-session-start-identity.test.ts +0 -371
- package/src/execution/__tests__/index-session-start.test.ts +0 -672
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
- package/src/execution/__tests__/lifecycle-manager.test.ts +0 -383
- package/src/execution/__tests__/lifecycle-predicates.test.ts +0 -116
- package/src/execution/__tests__/list-component.test.ts +0 -401
- package/src/execution/__tests__/list-fields.test.ts +0 -140
- package/src/execution/__tests__/manifest-parentid.test.ts +0 -117
- package/src/execution/__tests__/model-resolver.test.ts +0 -465
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +0 -287
- package/src/execution/__tests__/nested-visibility.test.ts +0 -325
- package/src/execution/__tests__/notifier-flush.test.ts +0 -612
- package/src/execution/__tests__/notifier-golden-snapshot.test.ts +0 -261
- package/src/execution/__tests__/notify-ledger.test.ts +0 -826
- package/src/execution/__tests__/one-shot-upgrade.test.ts +0 -205
- package/src/execution/__tests__/output-collector.test.ts +0 -358
- package/src/execution/__tests__/parent-child-matrix.test.ts +0 -336
- package/src/execution/__tests__/path-encoding.test.ts +0 -104
- package/src/execution/__tests__/pi-invocation.test.ts +0 -134
- package/src/execution/__tests__/record-store.test.ts +0 -1057
- package/src/execution/__tests__/records-cwd-isolation.test.ts +0 -91
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +0 -339
- package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -273
- package/src/execution/__tests__/relay-agent.test.ts +0 -448
- package/src/execution/__tests__/relay-env.test.ts +0 -42
- package/src/execution/__tests__/resource-policy.test.ts +0 -109
- package/src/execution/__tests__/rpc-mode.test.ts +0 -89
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -267
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -253
- package/src/execution/__tests__/run-spawn-edges.test.ts +0 -687
- package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
- package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -199
- package/src/execution/__tests__/sdk-contract.test.ts +0 -273
- package/src/execution/__tests__/session-context-resolver.test.ts +0 -167
- package/src/execution/__tests__/session-file-gc.test.ts +0 -293
- package/src/execution/__tests__/session-pending.test.ts +0 -197
- package/src/execution/__tests__/session-reconstructor.test.ts +0 -379
- package/src/execution/__tests__/session-runner-epipe.test.ts +0 -178
- package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -350
- package/src/execution/__tests__/session-start-reaper.test.ts +0 -233
- package/src/execution/__tests__/spawn-args.test.ts +0 -445
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -189
- package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -207
- package/src/execution/__tests__/spawned-children.test.ts +0 -92
- package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
- package/src/execution/__tests__/startup-config-declaration.test.ts +0 -35
- package/src/execution/__tests__/status-refactor.test.ts +0 -345
- package/src/execution/__tests__/stdin-writer.test.ts +0 -463
- package/src/execution/__tests__/stream-sink-retirement.test.ts +0 -261
- package/src/execution/__tests__/subagent-service-abort.test.ts +0 -60
- package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -629
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +0 -180
- package/src/execution/__tests__/subagent-service.test.ts +0 -788
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -310
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +0 -599
- package/src/execution/__tests__/temp-prompt.test.ts +0 -53
- package/src/execution/__tests__/timeout-integration.test.ts +0 -615
- package/src/execution/__tests__/tombstone-store.test.ts +0 -73
- package/src/execution/__tests__/tool-action.test.ts +0 -476
- package/src/execution/__tests__/truncline-snapshot.test.ts +0 -81
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
- package/src/execution/__tests__/turn-limiter.test.ts +0 -65
- package/src/execution/__tests__/ui-channels.test.ts +0 -187
- package/src/execution/__tests__/ui-interaction-model.test.ts +0 -67
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +0 -370
- package/src/execution/__tests__/ui-request-handler.test.ts +0 -204
- package/src/execution/__tests__/ui-request-observability.test.ts +0 -114
- package/src/execution/__tests__/ui-request-queue.test.ts +0 -133
- package/src/execution/__tests__/worktree-manager.test.ts +0 -633
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -229
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +0 -181
- package/src/execution/__tests__/worktree-registry.test.ts +0 -199
- package/src/execution/agent-registry.ts +0 -199
- package/src/execution/agent-result-mapper.ts +0 -90
- package/src/execution/alive-store.ts +0 -92
- package/src/execution/argv-mirror.ts +0 -113
- package/src/execution/best-effort.ts +0 -38
- package/src/execution/channel-registry-access.ts +0 -144
- package/src/execution/concurrency-pool.ts +0 -116
- package/src/execution/config.ts +0 -96
- package/src/execution/dialog-queue.ts +0 -330
- package/src/execution/engine/__tests__/common/data-dir.test.ts +0 -53
- package/src/execution/engine/__tests__/common/errors.test.ts +0 -132
- package/src/execution/engine/__tests__/common/event-journal.test.ts +0 -177
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +0 -192
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +0 -81
- package/src/execution/engine/__tests__/common/persona-router.test.ts +0 -123
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +0 -154
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
- package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +0 -141
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +0 -109
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +0 -101
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +0 -77
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +0 -104
- package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +0 -342
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +0 -201
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +0 -76
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +0 -79
- package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -87
- package/src/execution/engine/__tests__/engines-declaration.test.ts +0 -36
- package/src/execution/engine/__tests__/model-prompt.test.ts +0 -85
- package/src/execution/engine/__tests__/paths.test.ts +0 -39
- package/src/execution/engine/__tests__/registry.test.ts +0 -120
- package/src/execution/engine/__tests__/routing.test.ts +0 -231
- package/src/execution/engine/common/data-dir.ts +0 -62
- package/src/execution/engine/common/errors.ts +0 -183
- package/src/execution/engine/common/event-journal.ts +0 -254
- package/src/execution/engine/common/journal-replay.ts +0 -62
- package/src/execution/engine/common/kill-chain.ts +0 -221
- package/src/execution/engine/common/nesting-guard.ts +0 -50
- package/src/execution/engine/common/persona-router.ts +0 -108
- package/src/execution/engine/common/pool-manager.ts +0 -226
- package/src/execution/engine/common/schema-emulation.ts +0 -189
- package/src/execution/engine/common/session-view-projection.ts +0 -51
- package/src/execution/engine/engine-discovery.ts +0 -65
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -469
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
- package/src/execution/engine/engines/pi/pi-engine.ts +0 -415
- package/src/execution/engine/engines/pi/reader.ts +0 -48
- package/src/execution/engine/engines/pi/registration.ts +0 -35
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -246
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -228
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -64
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -127
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -580
- package/src/execution/engine/engines/zcode/constants.ts +0 -43
- package/src/execution/engine/engines/zcode/golden-sample.ts +0 -39
- package/src/execution/engine/engines/zcode/launcher.ts +0 -161
- package/src/execution/engine/engines/zcode/parser.ts +0 -436
- package/src/execution/engine/engines/zcode/preparer.ts +0 -363
- package/src/execution/engine/engines/zcode/reader.ts +0 -381
- package/src/execution/engine/engines/zcode/registration.ts +0 -37
- package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -658
- package/src/execution/engine/host-task-spec.ts +0 -47
- package/src/execution/engine/model-prompt.ts +0 -59
- package/src/execution/engine/paths.ts +0 -42
- package/src/execution/engine/port.ts +0 -153
- package/src/execution/engine/registry.ts +0 -123
- package/src/execution/engine/routing.ts +0 -218
- package/src/execution/engine/types.ts +0 -309
- package/src/execution/execute-options-mapper.ts +0 -112
- package/src/execution/execution-record.ts +0 -965
- package/src/execution/finalize-record.ts +0 -251
- package/src/execution/finalized-marker.ts +0 -51
- package/src/execution/get-state-handshake.ts +0 -104
- package/src/execution/host-mode.ts +0 -56
- package/src/execution/idle-gc.ts +0 -47
- package/src/execution/lifecycle-manager.ts +0 -513
- package/src/execution/lifecycle-predicates.ts +0 -65
- package/src/execution/manifest-store.ts +0 -256
- package/src/execution/model-config-service.ts +0 -219
- package/src/execution/model-resolver.ts +0 -248
- package/src/execution/notifier.ts +0 -330
- package/src/execution/notify-ledger.ts +0 -580
- package/src/execution/output-collector.ts +0 -228
- package/src/execution/path-encoding.ts +0 -60
- package/src/execution/pi-invocation.ts +0 -120
- package/src/execution/record-entry.ts +0 -132
- package/src/execution/record-store.ts +0 -1234
- package/src/execution/relay-env.ts +0 -37
- package/src/execution/session-context-resolver.ts +0 -64
- package/src/execution/session-file-gc.ts +0 -120
- package/src/execution/session-pending.ts +0 -164
- package/src/execution/session-reconstructor.ts +0 -678
- package/src/execution/session-runner.ts +0 -1781
- package/src/execution/sessions-index.ts +0 -304
- package/src/execution/spawn-event-adapter.ts +0 -363
- package/src/execution/stdin-writer.ts +0 -198
- package/src/execution/stream-sink.ts +0 -126
- package/src/execution/subagent-service.ts +0 -2141
- package/src/execution/subprocess-agent-runner.ts +0 -317
- package/src/execution/temp-prompt.ts +0 -62
- package/src/execution/tombstone-store.ts +0 -72
- package/src/execution/turn-limiter.ts +0 -102
- package/src/execution/types.ts +0 -973
- package/src/execution/ui-channels.ts +0 -216
- package/src/execution/ui-interaction-model.ts +0 -48
- package/src/execution/ui-request-handler-factory.ts +0 -212
- package/src/execution/ui-request-observability.ts +0 -81
- package/src/execution/ui-request-queue.ts +0 -184
- package/src/execution/worktree-manager.ts +0 -688
- package/src/execution/worktree-registry.ts +0 -222
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +0 -349
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -202
- package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -152
- package/src/orchestration/__tests__/args-validator.test.ts +0 -143
- package/src/orchestration/__tests__/config-loader.test.ts +0 -503
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +0 -809
- package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +0 -405
- package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +0 -56
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +0 -166
- package/src/orchestration/__tests__/execute-agent-call.test.ts +0 -451
- package/src/orchestration/__tests__/jsonl-run-store-corrupt-entry.test.ts +0 -150
- package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +0 -171
- package/src/orchestration/__tests__/jsonl-run-store-retention.test.ts +0 -202
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +0 -1030
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -600
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +0 -96
- package/src/orchestration/__tests__/lifecycle.test.ts +0 -659
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +0 -95
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +0 -2025
- package/src/orchestration/__tests__/script-lint.test.ts +0 -831
- package/src/orchestration/__tests__/skill-discovery.test.ts +0 -201
- package/src/orchestration/__tests__/test-mocks.ts +0 -197
- package/src/orchestration/__tests__/worker-exit-without-result.test.ts +0 -368
- package/src/orchestration/__tests__/worker-host.test.ts +0 -120
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +0 -164
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +0 -563
- package/src/orchestration/__tests__/worker-script-builder.test.ts +0 -309
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +0 -129
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -317
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +0 -110
- package/src/orchestration/__tests__/workflows-e2e.test.ts +0 -537
- package/src/orchestration/agent-opts-resolver.ts +0 -174
- package/src/orchestration/args-validator.ts +0 -127
- package/src/orchestration/config-loader.ts +0 -315
- package/src/orchestration/error-recovery.ts +0 -1018
- package/src/orchestration/execute-agent-call.ts +0 -256
- package/src/orchestration/launcher.ts +0 -455
- package/src/orchestration/lifecycle.ts +0 -399
- package/src/orchestration/models/__tests__/budget.test.ts +0 -307
- package/src/orchestration/models/__tests__/trace.test.ts +0 -408
- package/src/orchestration/models/agent-call.ts +0 -83
- package/src/orchestration/models/budget.ts +0 -118
- package/src/orchestration/models/ports.ts +0 -164
- package/src/orchestration/models/run-runtime.ts +0 -104
- package/src/orchestration/models/run-spec.ts +0 -83
- package/src/orchestration/models/run-state.ts +0 -44
- package/src/orchestration/models/trace.ts +0 -183
- package/src/orchestration/models/types.ts +0 -301
- package/src/orchestration/models/workflow-run.ts +0 -254
- package/src/orchestration/models/workflow-script-registry.ts +0 -35
- package/src/orchestration/models/workflow-script.ts +0 -117
- package/src/orchestration/script-lint.ts +0 -766
- package/src/orchestration/skill-discovery.ts +0 -115
- package/src/orchestration/worker-handle.ts +0 -115
- package/src/orchestration/worker-host.ts +0 -98
- package/src/orchestration/worker-script-builder.ts +0 -421
- package/src/orchestration/workflow-files.ts +0 -85
- package/src/orchestration/workflow-script-registry-impl.ts +0 -133
- package/src/shared/__tests__/agent-ref.test.ts +0 -34
- package/src/shared/__tests__/meta-parser.test.ts +0 -304
- package/src/shared/__tests__/model-ref.test.ts +0 -306
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -280
- package/src/shared/__tests__/resource-discovery.test.ts +0 -736
- package/src/shared/__tests__/resource-meta.test.ts +0 -51
- package/src/shared/__tests__/schema-jsonify.test.ts +0 -81
- package/src/shared/__tests__/timer-delay.test.ts +0 -61
- package/src/shared/agent-event.ts +0 -13
- package/src/shared/agent-ref.ts +0 -57
- package/src/shared/meta-parser.ts +0 -261
- package/src/shared/model-ref.ts +0 -286
- package/src/shared/resource-discovery.ts +0 -811
- package/src/shared/resource-meta.ts +0 -65
- package/src/shared/schema-env.ts +0 -44
- package/src/shared/schema-jsonify.ts +0 -58
- package/src/shared/timer-delay.ts +0 -54
- package/src/shared/xml-injection.ts +0 -35
- package/workflows/README.md +0 -81
- package/workflows/_shared/agent-refs.cjs +0 -40
- package/workflows/chain.js +0 -131
- package/workflows/map-reduce.js +0 -174
- package/workflows/parallel.js +0 -148
- package/workflows/review-fix-loop-utils.cjs +0 -1309
- package/workflows/review-fix-loop.js +0 -1404
- 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
|
-
});
|