@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.
- package/package.json +13 -16
- package/relay/relay.mjs +1 -1
- package/src/host/__tests__/pi-host.test.ts +214 -0
- package/src/host/pi-host.ts +109 -0
- package/src/index.ts +142 -32
- package/src/injectors/__tests__/engine-awareness.test.ts +292 -0
- package/src/injectors/__tests__/engine-section-stability.test.ts +341 -0
- package/src/injectors/__tests__/subagent-list-injector.test.ts +5 -5
- package/src/injectors/__tests__/workflow-list-injector.test.ts +5 -5
- package/src/injectors/engine-awareness.ts +123 -0
- package/src/injectors/model-list-injector.ts +1 -1
- package/src/injectors/subagent-list-injector.ts +8 -8
- package/src/injectors/workflow-list-injector.ts +8 -8
- package/src/interface/__tests__/detectors.test.ts +3 -3
- package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +2 -2
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +1 -1
- package/src/interface/bg-notify-render.ts +34 -13
- package/src/interface/commands.ts +3 -3
- package/src/interface/format.ts +2 -2
- package/src/interface/helpers.ts +3 -3
- package/src/interface/list-component.ts +4 -4
- package/src/interface/list-shared.ts +2 -2
- package/src/interface/list-view.ts +2 -2
- package/src/interface/subagent-actions.ts +288 -19
- package/src/interface/subagent-tool-schema.ts +163 -0
- package/src/interface/subagent-tool.ts +65 -129
- package/src/interface/subagents.ts +5 -5
- package/src/interface/tool-render.ts +34 -2
- package/src/interface/tool-workflow-script.ts +4 -4
- package/src/interface/tool-workflow.ts +6 -6
- package/src/interface/views/WorkflowsView.ts +5 -5
- 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 +4 -4
- package/src/{orchestration/jsonl-run-store.ts → jsonl-run-store.ts} +132 -18
- 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 -256
- package/src/execution/__tests__/channel-registry-handshake.test.ts +0 -243
- package/src/execution/__tests__/chat-engine-routing.test.ts +0 -597
- 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 -385
- 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 -189
- package/src/execution/__tests__/execution-record.test.ts +0 -1284
- 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 -138
- 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 -238
- 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 -593
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
- package/src/execution/__tests__/lifecycle-manager.test.ts +0 -337
- 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 -109
- package/src/execution/__tests__/manifest-parentid.test.ts +0 -117
- package/src/execution/__tests__/model-resolver.test.ts +0 -413
- 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 -574
- package/src/execution/__tests__/notifier-golden-snapshot.test.ts +0 -261
- package/src/execution/__tests__/one-shot-upgrade.test.ts +0 -205
- package/src/execution/__tests__/output-collector.test.ts +0 -61
- 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 -644
- 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__/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 -311
- package/src/execution/__tests__/session-start-reaper.test.ts +0 -233
- package/src/execution/__tests__/spawn-args.test.ts +0 -434
- 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__/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 -506
- package/src/execution/__tests__/temp-prompt.test.ts +0 -53
- package/src/execution/__tests__/timeout-integration.test.ts +0 -397
- package/src/execution/__tests__/tombstone-store.test.ts +0 -73
- package/src/execution/__tests__/tool-action.test.ts +0 -385
- 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 -193
- package/src/execution/agent-result-mapper.ts +0 -90
- package/src/execution/alive-store.ts +0 -92
- package/src/execution/argv-mirror.ts +0 -109
- 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 -567
- 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 -648
- 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 -304
- package/src/execution/execute-options-mapper.ts +0 -111
- package/src/execution/execution-record.ts +0 -906
- 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 -491
- package/src/execution/lifecycle-predicates.ts +0 -65
- package/src/execution/manifest-store.ts +0 -256
- package/src/execution/model-config-service.ts +0 -204
- package/src/execution/model-resolver.ts +0 -276
- package/src/execution/notifier.ts +0 -260
- package/src/execution/output-collector.ts +0 -88
- 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 -1548
- 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 -2119
- package/src/execution/subprocess-agent-runner.ts +0 -303
- 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 -910
- 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 -174
- 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 -336
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -208
- package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -157
- package/src/orchestration/__tests__/args-validator.test.ts +0 -143
- package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
- package/src/orchestration/__tests__/config-loader.test.ts +0 -503
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +0 -724
- 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 -314
- package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +0 -171
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +0 -1030
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -277
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +0 -96
- package/src/orchestration/__tests__/lifecycle.test.ts +0 -625
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +0 -1971
- package/src/orchestration/__tests__/script-lint.test.ts +0 -831
- package/src/orchestration/__tests__/skill-discovery.test.ts +0 -157
- package/src/orchestration/__tests__/test-mocks.ts +0 -197
- 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 -520
- package/src/orchestration/__tests__/worker-script-builder.test.ts +0 -309
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +0 -110
- 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 -93
- package/src/orchestration/args-validator.ts +0 -127
- package/src/orchestration/concurrency-gate.ts +0 -69
- package/src/orchestration/config-loader.ts +0 -315
- package/src/orchestration/error-recovery.ts +0 -862
- package/src/orchestration/execute-agent-call.ts +0 -217
- package/src/orchestration/launcher.ts +0 -410
- package/src/orchestration/lifecycle.ts +0 -396
- package/src/orchestration/models/__tests__/budget.test.ts +0 -367
- 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 -148
- package/src/orchestration/models/ports.ts +0 -164
- package/src/orchestration/models/run-runtime.ts +0 -89
- 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 -292
- 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 -92
- package/src/orchestration/worker-handle.ts +0 -115
- package/src/orchestration/worker-host.ts +0 -98
- package/src/orchestration/worker-script-builder.ts +0 -408
- 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__/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/agent-event.ts +0 -13
- package/src/shared/agent-ref.ts +0 -57
- package/src/shared/meta-parser.ts +0 -261
- package/src/shared/resource-discovery.ts +0 -811
- package/src/shared/resource-meta.ts +0 -65
- package/src/shared/schema-jsonify.ts +0 -56
- 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 -1307
- package/workflows/review-fix-loop.js +0 -1396
- package/workflows/scatter-gather.js +0 -178
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
// doFinalizeRecord — manifest status 透传测试(M3: 4 态方案)。
|
|
2
|
-
//
|
|
3
|
-
// 验证 finalize-record.ts 的 status 映射:done→completed, failed→failed, cancelled→cancelled
|
|
4
|
-
// (cancelled 不再归并 failed)。crashed 不进 finalize 入参(TS 签名锁定 done/failed/cancelled)。
|
|
5
|
-
//
|
|
6
|
-
// 测试策略:record 不带 sessionFile/worktreeHandle,跳过 Step 0 (collectPatch) 和 Step 3
|
|
7
|
-
// (finalized/tombstone/aliveMarker/worktree cleanup) 的文件操作,聚焦 Step 4 writeManifest
|
|
8
|
-
// 的 status 产出。FinalizeDeps 用 stub 注入(manifestStore 为真实实例,指向 tmpDir)。
|
|
9
|
-
|
|
10
|
-
import * as fs from "node:fs";
|
|
11
|
-
import * as os from "node:os";
|
|
12
|
-
import * as path from "node:path";
|
|
13
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
14
|
-
|
|
15
|
-
// Mock 共享 logger,让 logger.error 可被 spy(源码已从 console.error 改为 logger.error)
|
|
16
|
-
const { loggerMock } = vi.hoisted(() => ({
|
|
17
|
-
loggerMock: {
|
|
18
|
-
debug: vi.fn(),
|
|
19
|
-
warn: vi.fn(),
|
|
20
|
-
error: vi.fn(),
|
|
21
|
-
info: vi.fn(),
|
|
22
|
-
},
|
|
23
|
-
}));
|
|
24
|
-
vi.mock("@zhushanwen/pi-extension-logger", () => ({
|
|
25
|
-
getLogger: () => loggerMock,
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
import { doFinalizeRecord, doFinalizeRoundToIdle } from "../finalize-record.ts";
|
|
29
|
-
import { ManifestStore } from "../manifest-store.ts";
|
|
30
|
-
import type { AgentResult, ExecutionRecord } from "../types.ts";
|
|
31
|
-
|
|
32
|
-
function makeMinimalRecord(overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
|
|
33
|
-
return {
|
|
34
|
-
id: "finalize-test",
|
|
35
|
-
agent: "worker",
|
|
36
|
-
model: "test/model",
|
|
37
|
-
thinkingLevel: undefined,
|
|
38
|
-
mode: "background",
|
|
39
|
-
task: "test",
|
|
40
|
-
slug: "test",
|
|
41
|
-
startedAt: 1000,
|
|
42
|
-
rootSessionId: "session-main",
|
|
43
|
-
parentRecordId: undefined,
|
|
44
|
-
depth: 0,
|
|
45
|
-
status: "running",
|
|
46
|
-
turns: [],
|
|
47
|
-
turnCount: 0,
|
|
48
|
-
totalTokens: 0,
|
|
49
|
-
lastError: undefined,
|
|
50
|
-
endedAt: undefined,
|
|
51
|
-
result: undefined,
|
|
52
|
-
error: undefined,
|
|
53
|
-
agentResult: undefined,
|
|
54
|
-
sessionFile: undefined,
|
|
55
|
-
controller: undefined,
|
|
56
|
-
...overrides,
|
|
57
|
-
} as ExecutionRecord;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function makeMinimalResult(): AgentResult {
|
|
61
|
-
return {
|
|
62
|
-
text: "done",
|
|
63
|
-
turns: 1,
|
|
64
|
-
durationMs: 100,
|
|
65
|
-
success: true,
|
|
66
|
-
sessionId: "sess-1",
|
|
67
|
-
toolCalls: [],
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
72
|
-
let tmpDir: string;
|
|
73
|
-
let manifestStore: ManifestStore;
|
|
74
|
-
|
|
75
|
-
beforeEach(() => {
|
|
76
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "finalize-test-"));
|
|
77
|
-
manifestStore = new ManifestStore(tmpDir);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
afterEach(() => {
|
|
81
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
/** 构造最小 FinalizeDeps:record 无 sessionFile/worktreeHandle,跳过 Step 0/3 文件操作。 */
|
|
85
|
-
function makeDeps() {
|
|
86
|
-
return {
|
|
87
|
-
manifestStore,
|
|
88
|
-
worktreeManager: {} as never,
|
|
89
|
-
store: { archive: vi.fn(), reportRecordTransition: vi.fn() } as never,
|
|
90
|
-
modelService: {} as never,
|
|
91
|
-
pi: { appendEntry: vi.fn() },
|
|
92
|
-
emitUnregister: vi.fn(),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
it("status=closed + cancelled reason → manifest 写 closed(v4 B-1:cancelled 折入 closed)", async () => {
|
|
97
|
-
const record = makeMinimalRecord({ id: "rec-cancelled" });
|
|
98
|
-
await doFinalizeRecord(makeDeps(), record, makeMinimalResult(), "closed", "cancelled");
|
|
99
|
-
|
|
100
|
-
const manifest = await manifestStore.readManifest("rec-cancelled");
|
|
101
|
-
expect(manifest).not.toBeNull();
|
|
102
|
-
// 关键断言:v4 B-1 manifest status 恒写 closed(cancelled 区分靠 tombstone sidecar)
|
|
103
|
-
expect(manifest?.status).toBe("closed");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("status=closed + user-close → manifest 写 closed", async () => {
|
|
107
|
-
const record = makeMinimalRecord({ id: "rec-done" });
|
|
108
|
-
await doFinalizeRecord(makeDeps(), record, makeMinimalResult(), "closed", "user-close");
|
|
109
|
-
|
|
110
|
-
const manifest = await manifestStore.readManifest("rec-done");
|
|
111
|
-
expect(manifest?.status).toBe("closed");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("status=closed + gc → manifest 写 closed", async () => {
|
|
115
|
-
const record = makeMinimalRecord({ id: "rec-failed" });
|
|
116
|
-
await doFinalizeRecord(makeDeps(), record, makeMinimalResult(), "closed", "gc");
|
|
117
|
-
|
|
118
|
-
const manifest = await manifestStore.readManifest("rec-failed");
|
|
119
|
-
expect(manifest?.status).toBe("closed");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("manifest write 抛错时 cleanup-first 顺序仍执行(Step 3 before Step 4 throw)", async () => {
|
|
123
|
-
// record 带 sessionFile 让 Step 3 finalized/aliveMarker 走真实路径;
|
|
124
|
-
// 不设 worktreeHandle → Step 0 (collectPatch) 和 Step 3 worktree cleanup 都跳过。
|
|
125
|
-
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
126
|
-
const record = makeMinimalRecord({ id: "rec-cleanup-first", sessionFile });
|
|
127
|
-
|
|
128
|
-
// 预写 .alive marker(让 removeAliveMarker 真实生效;不预写因 ENOENT 静默也 OK,
|
|
129
|
-
// 但预写后用 fs.existsSync 验证更直观)。
|
|
130
|
-
fs.writeFileSync(
|
|
131
|
-
`${sessionFile}.alive`,
|
|
132
|
-
`${JSON.stringify({ pid: 99999, id: "rec-cleanup-first", startedAt: 1000 })}\n`,
|
|
133
|
-
"utf-8",
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// mock writeManifest 抛错(模拟 disk full)。在 mock 内捕获「writeManifest 被调用时
|
|
137
|
-
// finalized marker 是否已存在」——这是 cleanup-first 顺序的关键断言:若有人把
|
|
138
|
-
// manifest write 前移到 cleanup 之前,本标志会是 false([Critical #1] 反例)。
|
|
139
|
-
const finalizedBeforeManifestWrite = { value: false };
|
|
140
|
-
vi.spyOn(manifestStore, "writeManifest").mockImplementation(async () => {
|
|
141
|
-
finalizedBeforeManifestWrite.value = fs.existsSync(`${sessionFile}.finalized`);
|
|
142
|
-
throw new Error("disk full");
|
|
143
|
-
});
|
|
144
|
-
loggerMock.error.mockClear();
|
|
145
|
-
|
|
146
|
-
const deps = makeDeps();
|
|
147
|
-
|
|
148
|
-
// ── 核心 claim 1:不抛错(cleanup-first → manifest write 失败不 throw)──
|
|
149
|
-
await expect(
|
|
150
|
-
doFinalizeRecord(deps, record, makeMinimalResult(), "closed"),
|
|
151
|
-
).resolves.toBeUndefined();
|
|
152
|
-
|
|
153
|
-
// ── 核心 claim 2:Step 3 cleanup 先执行 —— finalized marker 真实写入 ──
|
|
154
|
-
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
|
|
155
|
-
|
|
156
|
-
// ── 核心 claim 3:Step 3 aliveMarker 被移除(预写的 .alive 不再存在)──
|
|
157
|
-
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
|
|
158
|
-
|
|
159
|
-
// ── 核心 claim 4:pending-notifications 注销仍触发(emitUnregister)──
|
|
160
|
-
expect(deps.emitUnregister).toHaveBeenCalledWith("rec-cleanup-first", "closed");
|
|
161
|
-
|
|
162
|
-
// ── 核心 claim 5:manifest 写失败被 logger.error 记录(含 record id + error)──
|
|
163
|
-
expect(loggerMock.error).toHaveBeenCalledWith(expect.stringContaining("manifest 写入失败"));
|
|
164
|
-
const errMsg = loggerMock.error.mock.calls[0]?.[0];
|
|
165
|
-
expect(errMsg).toContain("rec-cleanup-first");
|
|
166
|
-
expect(errMsg).toContain("disk full");
|
|
167
|
-
|
|
168
|
-
// ── 核心 claim 6:pi.appendEntry 记录 "subagent:manifest-write-failed" 事件 ──
|
|
169
|
-
expect(deps.pi.appendEntry).toHaveBeenCalledWith(
|
|
170
|
-
"subagent:manifest-write-failed",
|
|
171
|
-
expect.objectContaining({ id: "rec-cleanup-first", error: "disk full" }),
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
// ── 核心 claim 7:manifest 实际未写入(writeManifest 抛错被吞咽)──
|
|
175
|
-
expect(await manifestStore.readManifest("rec-cleanup-first")).toBeNull();
|
|
176
|
-
|
|
177
|
-
// ── 核心 claim 8:[Critical #1] cleanup 在 manifest 写之前 —— 顺序锁定 ──
|
|
178
|
-
// 若有人把 manifest write 前移到 cleanup 之前,本标志会是 false(mock 捕获时刻
|
|
179
|
-
// .finalized 尚未被 Step 3 写入),保护 Critical #1 时序不变量。
|
|
180
|
-
expect(finalizedBeforeManifestWrite.value).toBe(true);
|
|
181
|
-
|
|
182
|
-
// 清理 mock 调用记录防污染
|
|
183
|
-
loggerMock.error.mockClear();
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ============================================================
|
|
188
|
-
// doFinalizeRoundToIdle — chatMode 轮次完成进 idle(M2-A)
|
|
189
|
-
// ============================================================
|
|
190
|
-
|
|
191
|
-
describe("doFinalizeRoundToIdle — chatMode 轮次完成进 idle (M2-A)", () => {
|
|
192
|
-
let tmpDir: string;
|
|
193
|
-
let manifestStore: ManifestStore;
|
|
194
|
-
|
|
195
|
-
beforeEach(() => {
|
|
196
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "finalize-idle-test-"));
|
|
197
|
-
manifestStore = new ManifestStore(tmpDir);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
afterEach(() => {
|
|
201
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
/** 构造 FinalizeDeps:worktreeManager.cleanup / store.archive 为 vi.fn 以断言「不调」。 */
|
|
205
|
-
function makeDeps() {
|
|
206
|
-
return {
|
|
207
|
-
manifestStore,
|
|
208
|
-
worktreeManager: { cleanup: vi.fn(), collectPatch: vi.fn() } as never,
|
|
209
|
-
store: { archive: vi.fn(), reportRecordTransition: vi.fn() } as never,
|
|
210
|
-
modelService: {} as never,
|
|
211
|
-
pi: { appendEntry: vi.fn() } as never,
|
|
212
|
-
emitUnregister: vi.fn(),
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
it("record 带 sessionFile → 删 .alive + record.status=running + round 0→1", async () => {
|
|
217
|
-
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
218
|
-
// 预写 .alive marker(验证 doFinalizeRoundToIdle 删除它——进程已回收)
|
|
219
|
-
fs.writeFileSync(
|
|
220
|
-
`${sessionFile}.alive`,
|
|
221
|
-
`${JSON.stringify({ pid: 99999, id: "rec-idle", startedAt: 1000 })}\n`,
|
|
222
|
-
"utf-8",
|
|
223
|
-
);
|
|
224
|
-
const record = makeMinimalRecord({ id: "rec-idle", sessionFile, chatMode: true, round: 0 });
|
|
225
|
-
// tryTransition 已把 status 设为 done,模拟 runAndFinalize 调用前的状态
|
|
226
|
-
record.status = "closed";
|
|
227
|
-
|
|
228
|
-
await doFinalizeRoundToIdle(makeDeps(), record, makeMinimalResult());
|
|
229
|
-
|
|
230
|
-
// 状态机(v4 B-1:旧 idle 折入 running,finalizeRoundToIdle 设 running)
|
|
231
|
-
expect(record.status).toBe("running");
|
|
232
|
-
expect(record.round).toBe(1);
|
|
233
|
-
// .alive marker 被删(进程已 SIGTERM 回收)
|
|
234
|
-
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("W16: 轮终上报 reportRecordTransition(record-store 类外恢复写点迁移落 entry)", async () => {
|
|
238
|
-
const deps = makeDeps();
|
|
239
|
-
const record = makeMinimalRecord({ id: "rec-report", chatMode: true, round: 2 });
|
|
240
|
-
record.status = "closed";
|
|
241
|
-
await doFinalizeRoundToIdle(deps, record, makeMinimalResult());
|
|
242
|
-
expect(deps.store.reportRecordTransition).toHaveBeenCalledTimes(1);
|
|
243
|
-
// 上报发生在 round 推进之后(entry 携带新轮计数,重建源不滞后)
|
|
244
|
-
expect(record.round).toBe(3);
|
|
245
|
-
expect(record.status).toBe("running");
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("record.round 已为 N → round 变 N+1", async () => {
|
|
249
|
-
const record = makeMinimalRecord({ id: "rec-round", round: 3 });
|
|
250
|
-
record.status = "closed";
|
|
251
|
-
await doFinalizeRoundToIdle(makeDeps(), record, makeMinimalResult());
|
|
252
|
-
expect(record.round).toBe(4);
|
|
253
|
-
expect(record.status).toBe("running");
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it("不调 store.archive(record 留内存,getMutable 仍可查)", async () => {
|
|
257
|
-
const deps = makeDeps();
|
|
258
|
-
const record = makeMinimalRecord({ id: "rec-noarchive" });
|
|
259
|
-
record.status = "closed";
|
|
260
|
-
await doFinalizeRoundToIdle(deps, record, makeMinimalResult());
|
|
261
|
-
expect(deps.store.archive).not.toHaveBeenCalled();
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("不 cleanup worktree(即使 record 带 worktreeHandle——保留对话模式工作目录)", async () => {
|
|
265
|
-
const deps = makeDeps();
|
|
266
|
-
const record = makeMinimalRecord({
|
|
267
|
-
id: "rec-noworktree",
|
|
268
|
-
worktreeHandle: { path: "/tmp/x", branch: "b", baseCommit: "c", mainCwd: "/tmp" } as never,
|
|
269
|
-
});
|
|
270
|
-
record.status = "closed";
|
|
271
|
-
await doFinalizeRoundToIdle(deps, record, makeMinimalResult());
|
|
272
|
-
expect(deps.worktreeManager.cleanup).not.toHaveBeenCalled();
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("emitUnregister 被调(status=running,进程已死从 pending 活跃差集移除)", async () => {
|
|
276
|
-
const deps = makeDeps();
|
|
277
|
-
const record = makeMinimalRecord({ id: "rec-emit" });
|
|
278
|
-
record.status = "closed";
|
|
279
|
-
await doFinalizeRoundToIdle(deps, record, makeMinimalResult());
|
|
280
|
-
expect(deps.emitUnregister).toHaveBeenCalledWith("rec-emit", "running");
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("不调 completeRecord:record 不冻结(endedAt / agentResult 仍 undefined)", async () => {
|
|
284
|
-
const record = makeMinimalRecord({ id: "rec-nofreeze" });
|
|
285
|
-
record.status = "closed";
|
|
286
|
-
await doFinalizeRoundToIdle(makeDeps(), record, makeMinimalResult());
|
|
287
|
-
expect(record.endedAt).toBeUndefined();
|
|
288
|
-
expect(record.agentResult).toBeUndefined();
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("不写 manifest(idle 非终态,manifest 是终态诊断辅助)", async () => {
|
|
292
|
-
const record = makeMinimalRecord({ id: "rec-nomanifest" });
|
|
293
|
-
record.status = "closed";
|
|
294
|
-
await doFinalizeRoundToIdle(makeDeps(), record, makeMinimalResult());
|
|
295
|
-
const manifest = await manifestStore.readManifest("rec-nomanifest");
|
|
296
|
-
expect(manifest).toBeNull();
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("MF-2: record.result 设为 result.text(否则 notifier idle 回复恒为 (empty),G1/G2 不成立)", async () => {
|
|
300
|
-
const record = makeMinimalRecord({ id: "rec-result" });
|
|
301
|
-
record.status = "closed";
|
|
302
|
-
const result = makeMinimalResult();
|
|
303
|
-
result.text = "review done, found 3 issues";
|
|
304
|
-
await doFinalizeRoundToIdle(makeDeps(), record, result);
|
|
305
|
-
// MF-2:record.result 被 result.text 填充,notifier idle 分支读取后携带回复正文
|
|
306
|
-
expect(record.result).toBe("review done, found 3 issues");
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("MF-2 兑底:失败轮次 result.text 为空时用 result.error 填 record.result(MF-6 回退 idle 路径)", async () => {
|
|
310
|
-
const record = makeMinimalRecord({ id: "rec-result-err" });
|
|
311
|
-
record.status = "closed";
|
|
312
|
-
const result = makeMinimalResult();
|
|
313
|
-
result.text = "";
|
|
314
|
-
result.success = false;
|
|
315
|
-
result.error = "spawn timeout";
|
|
316
|
-
await doFinalizeRoundToIdle(makeDeps(), record, result);
|
|
317
|
-
// 失败轮次的 notify 需可读:result.text 空 → 兑底用 error
|
|
318
|
-
expect(record.result).toBe("round did not complete: spawn timeout");
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it("C1TC10: chatMode 空增量轮占位——record.result 固定 (no output this round),不含上一轮文本(D5)", async () => {
|
|
322
|
-
const record = makeMinimalRecord({ id: "rec-increment-empty", chatMode: true });
|
|
323
|
-
record.status = "closed";
|
|
324
|
-
// 预置上一轮通知文本(模拟增量语义前的 record.result 残留)
|
|
325
|
-
record.result = "PREV-ROUND-TEXT";
|
|
326
|
-
const result = makeMinimalResult();
|
|
327
|
-
result.text = "";
|
|
328
|
-
result.success = true;
|
|
329
|
-
result.error = undefined;
|
|
330
|
-
await doFinalizeRoundToIdle(makeDeps(), record, result);
|
|
331
|
-
// 空增量轮通知不含上一轮文本:沿用旧值会让父 agent 误读为原样重复回复(D5 判定)
|
|
332
|
-
expect(record.result).toBe("(no output this round)");
|
|
333
|
-
expect(record.result).not.toContain("PREV-ROUND-TEXT");
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it("C1TC11 [R2-1]: one-shot 首轮空文本成功完成 → record.result 补占位「(empty)」(非 undefined)", async () => {
|
|
337
|
-
const record = makeMinimalRecord({ id: "rec-oneshot-empty", chatMode: false });
|
|
338
|
-
record.status = "closed";
|
|
339
|
-
// one-shot 成功空文本完成路径(collectResult getFullText 返回 ""、success=true):
|
|
340
|
-
// result 前值 undefined
|
|
341
|
-
record.result = undefined;
|
|
342
|
-
const result = makeMinimalResult();
|
|
343
|
-
result.text = "";
|
|
344
|
-
result.success = true;
|
|
345
|
-
result.error = undefined;
|
|
346
|
-
await doFinalizeRoundToIdle(makeDeps(), record, result);
|
|
347
|
-
// [R2-1] 轮终信号优先:占位非 undefined 是 renderer hasRunning 排除轮终 running 的
|
|
348
|
-
// 判据——保持 undefined 会让完成注入后末位 turn 永久「工作中」
|
|
349
|
-
expect(record.result).toBe("(empty)");
|
|
350
|
-
// notifier buildLlmContent 的 record.result ?? "(empty)" 兜底同款措辞:
|
|
351
|
-
// 通知文案逐字节产出 "completed. Result:\n(empty)"(G4 保持)
|
|
352
|
-
expect(record.result ?? "(empty)").toBe("(empty)");
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("C1TC11b [R2-1]: one-shot 续轮空文本 → 沿用前值(one-shot 无增量语义,不覆盖为占位)", async () => {
|
|
356
|
-
const record = makeMinimalRecord({ id: "rec-oneshot-cont", chatMode: false });
|
|
357
|
-
record.status = "closed";
|
|
358
|
-
// 上一轮真实产出(续轮 record.result 前值)
|
|
359
|
-
record.result = "first round output";
|
|
360
|
-
const result = makeMinimalResult();
|
|
361
|
-
result.text = "";
|
|
362
|
-
result.success = true;
|
|
363
|
-
result.error = undefined;
|
|
364
|
-
await doFinalizeRoundToIdle(makeDeps(), record, result);
|
|
365
|
-
// one-shot record.result = 该 subagent 最终输出:空文本续轮沿用前值,不被 "(empty)" 覆盖
|
|
366
|
-
expect(record.result).toBe("first round output");
|
|
367
|
-
});
|
|
368
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// src/__tests__/finalized-marker.test.ts
|
|
2
|
-
//
|
|
3
|
-
// finalized-marker 专属测试。
|
|
4
|
-
// 覆盖:write→read 往返 / 缺 sidecar → false / best-effort IO 错静默 / .cancelled 互斥。
|
|
5
|
-
|
|
6
|
-
import * as fs from "node:fs";
|
|
7
|
-
import * as os from "node:os";
|
|
8
|
-
import * as path from "node:path";
|
|
9
|
-
|
|
10
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
11
|
-
|
|
12
|
-
import { readFinalized, writeFinalized } from "../finalized-marker.ts";
|
|
13
|
-
|
|
14
|
-
describe("finalized-marker", () => {
|
|
15
|
-
let tmpDir: string;
|
|
16
|
-
let sessionFile: string;
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "fm-test-"));
|
|
20
|
-
sessionFile = path.join(tmpDir, "2026-01-01_uuid.jsonl");
|
|
21
|
-
});
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe("write → read 往返", () => {
|
|
27
|
-
it("写入后 readFinalized 返回 true", () => {
|
|
28
|
-
writeFinalized(sessionFile);
|
|
29
|
-
expect(readFinalized(sessionFile)).toBe(true);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("sidecar 路径 = sessionFile + '.finalized'", () => {
|
|
33
|
-
writeFinalized(sessionFile);
|
|
34
|
-
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("sidecar 内容为空(存在性即信号)", () => {
|
|
38
|
-
writeFinalized(sessionFile);
|
|
39
|
-
const content = fs.readFileSync(`${sessionFile}.finalized`, "utf-8");
|
|
40
|
-
expect(content).toBe("");
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe("readFinalized 无 sidecar", () => {
|
|
45
|
-
it("不存在 → false", () => {
|
|
46
|
-
expect(readFinalized(sessionFile)).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("best-effort IO 错静默", () => {
|
|
51
|
-
it("writeFileSync 抛错时不抛出", () => {
|
|
52
|
-
// 目标路径不存在(不存在的父目录),writeFileSync 会抛
|
|
53
|
-
const badPath = path.join(tmpDir, "nonexistent-sub", "session.jsonl");
|
|
54
|
-
expect(() => writeFinalized(badPath)).not.toThrow();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("写失败后 readFinalized 返回 false(sidecar 未写入)", () => {
|
|
58
|
-
const badPath = path.join(tmpDir, "nonexistent-sub", "session.jsonl");
|
|
59
|
-
writeFinalized(badPath); // 静默失败
|
|
60
|
-
expect(readFinalized(badPath)).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe("与 .cancelled 互斥(BC-4)", () => {
|
|
65
|
-
it("写 finalized 时删除已有的 .cancelled", () => {
|
|
66
|
-
// 先写一个 .cancelled sidecar
|
|
67
|
-
fs.writeFileSync(`${sessionFile}.cancelled`, `{"id":"bg-1","status":"cancelled","agent":"w","startedAt":1,"endedAt":2}\n`, "utf-8");
|
|
68
|
-
expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(true);
|
|
69
|
-
|
|
70
|
-
// 写 finalized → .cancelled 应被删除
|
|
71
|
-
writeFinalized(sessionFile);
|
|
72
|
-
expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(false);
|
|
73
|
-
expect(readFinalized(sessionFile)).toBe(true);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("无 .cancelled 时写 finalized 正常(不报错)", () => {
|
|
77
|
-
expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(false);
|
|
78
|
-
expect(() => writeFinalized(sessionFile)).not.toThrow();
|
|
79
|
-
expect(readFinalized(sessionFile)).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
});
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
// src/__tests__/format-schema-instruction.test.ts
|
|
2
|
-
//
|
|
3
|
-
// 锁定 formatSchemaInstruction 契约:构造 schema enforcement 的 MANDATORY 指令。
|
|
4
|
-
// 该字符串被拼入 task 末尾(runSpawn 的 fullTask = task + instruction),且 steer
|
|
5
|
-
// reminder 复用同一文本。一旦漏掉 "MUST call structured-output" 关键词或 JSON
|
|
6
|
-
// 序列化漂移,schema 模式会静默失效——agent 可能直接把 JSON 写进文本响应。
|
|
7
|
-
//
|
|
8
|
-
// 纯函数测试:不依赖 Pi 运行时、不 spawn 进程、不 mock。只 import 被测函数。
|
|
9
|
-
import { describe, expect, it } from "vitest";
|
|
10
|
-
|
|
11
|
-
import { formatSchemaInstruction } from "../session-runner.ts";
|
|
12
|
-
|
|
13
|
-
describe("formatSchemaInstruction", () => {
|
|
14
|
-
// ── 指令文本契约 ──────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
it("contains the structured-output tool keyword", () => {
|
|
17
|
-
const out = formatSchemaInstruction({ type: "object" });
|
|
18
|
-
expect(out).toContain("structured-output");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("emits a MANDATORY structured-output directive (not free-form JSON)", () => {
|
|
22
|
-
const out = formatSchemaInstruction({ type: "object" });
|
|
23
|
-
expect(out).toContain("MANDATORY");
|
|
24
|
-
expect(out).toContain("MUST");
|
|
25
|
-
expect(out).toContain("MUST call the `structured-output` tool");
|
|
26
|
-
expect(out).toContain("Do NOT output the JSON directly");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// ── schema 序列化 ─────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
it("embeds the schema as pretty-printed JSON (indent=2) inside a fenced block", () => {
|
|
32
|
-
const schema: Record<string, unknown> = {
|
|
33
|
-
type: "object",
|
|
34
|
-
properties: { name: { type: "string" } },
|
|
35
|
-
};
|
|
36
|
-
const out = formatSchemaInstruction(schema);
|
|
37
|
-
// 必须包含 JSON.stringify(schema, null, 2) 的完整结果
|
|
38
|
-
expect(out).toContain(JSON.stringify(schema, null, 2));
|
|
39
|
-
expect(out).toContain("```json");
|
|
40
|
-
expect(out).toContain("```");
|
|
41
|
-
// indent=2 的可观察证据:属性键前有恰好两个空格(object 第一层缩进)
|
|
42
|
-
expect(out).toContain('\n "type": "object"');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("locks the full output structure for a minimal schema", () => {
|
|
46
|
-
const out = formatSchemaInstruction({ type: "object" });
|
|
47
|
-
// 完整结构快照——任何指令措辞/顺序/缩进漂移都会被捕获。
|
|
48
|
-
// 注意第三行末尾的 em-dash(—),防止有人把它替换成普通连字符。
|
|
49
|
-
// [HISTORICAL] 方案 A 后文案更新:告知 LLM schema 由系统注入,只需传 data。
|
|
50
|
-
expect(out).toBe(
|
|
51
|
-
[
|
|
52
|
-
"MANDATORY: Structured Output Requirement",
|
|
53
|
-
"You MUST call the `structured-output` tool with your final answer.",
|
|
54
|
-
"Do NOT output the JSON directly in your text response — you MUST use the structured-output tool.",
|
|
55
|
-
"The schema is enforced by the system — call structured-output with ONLY the `data` parameter.",
|
|
56
|
-
"Do NOT pass a `schema` parameter; the system validates `data` against the authoritative schema automatically.",
|
|
57
|
-
"The schema for your `data` is:",
|
|
58
|
-
"```json",
|
|
59
|
-
'{',
|
|
60
|
-
' "type": "object"',
|
|
61
|
-
'}',
|
|
62
|
-
"```",
|
|
63
|
-
].join("\n"),
|
|
64
|
-
);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// ── 特殊字符转义(注入风险路径)──────────────────────────────
|
|
68
|
-
|
|
69
|
-
it("escapes double quotes inside schema string values", () => {
|
|
70
|
-
const schema: Record<string, unknown> = { prompt: 'say "hi"' };
|
|
71
|
-
const out = formatSchemaInstruction(schema);
|
|
72
|
-
// JSON.stringify 会把内层 " 转义为 \"
|
|
73
|
-
expect(out).toContain('say \\"hi\\"');
|
|
74
|
-
// 原始未转义形式(含成对字面双引号)绝不能回流进 JSON 体内
|
|
75
|
-
expect(out).not.toContain('say "hi"');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("escapes newlines inside schema string values", () => {
|
|
79
|
-
const schema: Record<string, unknown> = { text: "line1\nline2" };
|
|
80
|
-
const out = formatSchemaInstruction(schema);
|
|
81
|
-
// 换行被序列化为字面反斜杠-n,不能是真实换行符
|
|
82
|
-
expect(out).toContain("line1\\nline2");
|
|
83
|
-
expect(out).not.toContain("line1\nline2");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("escapes backslashes inside schema string values", () => {
|
|
87
|
-
const schema: Record<string, unknown> = { path: "C:\\Users\\x" };
|
|
88
|
-
const out = formatSchemaInstruction(schema);
|
|
89
|
-
// 单反斜杠被序列化为 \\,避免后续解析误把转义序列当指令
|
|
90
|
-
expect(out).toContain("C:\\\\Users\\\\x");
|
|
91
|
-
expect(out).not.toContain("C:\\Users\\x");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// ── 边界值 ────────────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
it("handles empty schema object", () => {
|
|
97
|
-
const out = formatSchemaInstruction({});
|
|
98
|
-
expect(out).toContain("structured-output");
|
|
99
|
-
expect(out).toContain("{}");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("preserves null values in schema", () => {
|
|
103
|
-
const schema: Record<string, unknown> = { default: null };
|
|
104
|
-
const out = formatSchemaInstruction(schema);
|
|
105
|
-
// JSON.stringify 对 null 保留字面 "null"(不会 omit 键,也不会变字符串)
|
|
106
|
-
expect(out).toContain('"default": null');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("serializes nested objects and arrays", () => {
|
|
110
|
-
const schema: Record<string, unknown> = {
|
|
111
|
-
type: "object",
|
|
112
|
-
required: ["name", "age"],
|
|
113
|
-
properties: {
|
|
114
|
-
name: { type: "string" },
|
|
115
|
-
age: { type: "integer", minimum: 0 },
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
const out = formatSchemaInstruction(schema);
|
|
119
|
-
expect(out).toContain(JSON.stringify(schema, null, 2));
|
|
120
|
-
// 嵌套结构 indent 正确(第二层 4 空格)
|
|
121
|
-
expect(out).toContain(' "name": {');
|
|
122
|
-
expect(out).toContain(' "type": "string"');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// ── 确定性 ────────────────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
it("is deterministic — same schema produces identical output", () => {
|
|
128
|
-
const schema: Record<string, unknown> = { a: 1, b: [2, 3] };
|
|
129
|
-
expect(formatSchemaInstruction(schema)).toBe(formatSchemaInstruction(schema));
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("is deterministic across different object key insertion (value-equal schemas)", () => {
|
|
133
|
-
// JSON.stringify 按对象自身属性顺序序列化;同序构造的等价 schema 应产出相同指令
|
|
134
|
-
const a: Record<string, unknown> = { x: 1, y: 2 };
|
|
135
|
-
const b: Record<string, unknown> = { x: 1, y: 2 };
|
|
136
|
-
expect(formatSchemaInstruction(a)).toBe(formatSchemaInstruction(b));
|
|
137
|
-
});
|
|
138
|
-
});
|