@zhushanwen/subagent-core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -0
- package/dist/chunk-3VOERJPJ.js +22 -0
- package/dist/chunk-A4IVZWVX.js +31 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
- package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
- package/dist/execution/engine/engines/zcode/constants.js +22 -0
- package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
- package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
- package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
- package/dist/execution/engine/engines/zcode/reader.js +239 -0
- package/dist/execution/engine/paths.cjs +55 -0
- package/dist/execution/engine/paths.d.cts +8 -0
- package/dist/execution/engine/paths.d.ts +8 -0
- package/dist/execution/engine/paths.js +26 -0
- package/dist/execution/relay-env.cjs +62 -0
- package/dist/execution/relay-env.d.cts +33 -0
- package/dist/execution/relay-env.d.ts +33 -0
- package/dist/execution/relay-env.js +20 -0
- package/dist/index.cjs +1852 -0
- package/dist/index.d.cts +1184 -0
- package/dist/index.d.ts +1184 -0
- package/dist/index.js +1810 -0
- package/dist/types-BxyAidGf.d.cts +875 -0
- package/dist/types-BxyAidGf.d.ts +875 -0
- package/package.json +99 -0
- package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
- package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
- package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
- package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
- package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
- package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
- package/src/__tests__/m2-append-content-probe.test.ts +73 -0
- package/src/__tests__/manifest-store.test.ts +368 -0
- package/src/__tests__/record-store-cache.test.ts +335 -0
- package/src/__tests__/record-store-index.test.ts +560 -0
- package/src/__tests__/record-store-last-line.test.ts +129 -0
- package/src/__tests__/review-fix-loop-script.test.ts +288 -0
- package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
- package/src/__tests__/robustness-low-batch1.test.ts +286 -0
- package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
- package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
- package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
- package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
- package/src/__tests__/session-runner.test.ts +69 -0
- package/src/__tests__/smoke.test.ts +9 -0
- package/src/__tests__/stream-sink.test.ts +203 -0
- package/src/core/__tests__/host-services.test.ts +144 -0
- package/src/core/__tests__/logger.test.ts +78 -0
- package/src/core/__tests__/notify-ports.test.ts +104 -0
- package/src/core/host-services.ts +97 -0
- package/src/core/logger.ts +47 -0
- package/src/core/notify-ports.ts +126 -0
- package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
- package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
- package/src/execution/__tests__/config.test.ts +302 -0
- package/src/execution/__tests__/delivery-methods.test.ts +424 -0
- package/src/execution/__tests__/dialog-queue.test.ts +299 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
- package/src/execution/__tests__/execute-nesting.test.ts +323 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
- package/src/execution/__tests__/execution-record.test.ts +1394 -0
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
- package/src/execution/__tests__/finalize-record.test.ts +367 -0
- package/src/execution/__tests__/finalized-marker.test.ts +104 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
- package/src/execution/__tests__/host-mode.test.ts +87 -0
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
- package/src/execution/__tests__/model-resolver.test.ts +465 -0
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/output-collector.test.ts +358 -0
- package/src/execution/__tests__/path-encoding.test.ts +104 -0
- package/src/execution/__tests__/pi-invocation.test.ts +134 -0
- package/src/execution/__tests__/record-store.test.ts +1057 -0
- package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
- package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
- package/src/execution/__tests__/relay-env.test.ts +42 -0
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/rpc-mode.test.ts +89 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +293 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
- package/src/execution/__tests__/spawn-args.test.ts +445 -0
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
- package/src/execution/__tests__/stdin-writer.test.ts +462 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
- package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +788 -0
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +615 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/ui-channels.test.ts +187 -0
- package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
- package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
- package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
- package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
- package/src/execution/__tests__/worktree-manager.test.ts +633 -0
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
- package/src/execution/__tests__/worktree-registry.test.ts +199 -0
- package/src/execution/agent-registry.ts +199 -0
- package/src/execution/agent-result-mapper.ts +90 -0
- package/src/execution/alive-store.ts +119 -0
- package/src/execution/argv-mirror.ts +113 -0
- package/src/execution/best-effort.ts +38 -0
- package/src/execution/channel-registry-access.ts +144 -0
- package/src/execution/concurrency-pool.ts +116 -0
- package/src/execution/config.ts +165 -0
- package/src/execution/dialog-queue.ts +330 -0
- package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
- package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
- package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
- package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
- package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
- package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
- package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
- package/src/execution/engine/__tests__/paths.test.ts +39 -0
- package/src/execution/engine/__tests__/registry.test.ts +120 -0
- package/src/execution/engine/__tests__/routing.test.ts +231 -0
- package/src/execution/engine/common/data-dir.ts +66 -0
- package/src/execution/engine/common/errors.ts +183 -0
- package/src/execution/engine/common/event-journal.ts +254 -0
- package/src/execution/engine/common/journal-replay.ts +62 -0
- package/src/execution/engine/common/kill-chain.ts +221 -0
- package/src/execution/engine/common/nesting-guard.ts +50 -0
- package/src/execution/engine/common/persona-router.ts +108 -0
- package/src/execution/engine/common/pool-manager.ts +226 -0
- package/src/execution/engine/common/schema-emulation.ts +189 -0
- package/src/execution/engine/common/session-view-projection.ts +51 -0
- package/src/execution/engine/engine-discovery.ts +65 -0
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
- package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
- package/src/execution/engine/engines/pi/reader.ts +48 -0
- package/src/execution/engine/engines/pi/registration.ts +35 -0
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
- package/src/execution/engine/engines/zcode/constants.ts +43 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
- package/src/execution/engine/engines/zcode/launcher.ts +161 -0
- package/src/execution/engine/engines/zcode/parser.ts +436 -0
- package/src/execution/engine/engines/zcode/preparer.ts +363 -0
- package/src/execution/engine/engines/zcode/reader.ts +381 -0
- package/src/execution/engine/engines/zcode/registration.ts +37 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
- package/src/execution/engine/host-task-spec.ts +47 -0
- package/src/execution/engine/model-prompt.ts +158 -0
- package/src/execution/engine/paths.ts +42 -0
- package/src/execution/engine/port.ts +153 -0
- package/src/execution/engine/registry.ts +133 -0
- package/src/execution/engine/routing.ts +218 -0
- package/src/execution/engine/types.ts +309 -0
- package/src/execution/execute-options-mapper.ts +115 -0
- package/src/execution/execution-record.ts +984 -0
- package/src/execution/finalize-record.ts +254 -0
- package/src/execution/finalized-marker.ts +70 -0
- package/src/execution/get-state-handshake.ts +104 -0
- package/src/execution/host-mode.ts +56 -0
- package/src/execution/idle-gc.ts +47 -0
- package/src/execution/lifecycle-manager.ts +513 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +256 -0
- package/src/execution/model-config-service.ts +250 -0
- package/src/execution/model-resolver.ts +248 -0
- package/src/execution/notifier.ts +372 -0
- package/src/execution/notify-ledger.ts +580 -0
- package/src/execution/output-collector.ts +228 -0
- package/src/execution/path-encoding.ts +60 -0
- package/src/execution/pi-invocation.ts +120 -0
- package/src/execution/record-entry.ts +132 -0
- package/src/execution/record-store.ts +1265 -0
- package/src/execution/relay-env.ts +37 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +120 -0
- package/src/execution/session-pending.ts +170 -0
- package/src/execution/session-reconstructor.ts +678 -0
- package/src/execution/session-runner.ts +1789 -0
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/spawn-event-adapter.ts +363 -0
- package/src/execution/stdin-writer.ts +198 -0
- package/src/execution/stream-sink.ts +126 -0
- package/src/execution/subagent-service.ts +2268 -0
- package/src/execution/subprocess-agent-runner.ts +317 -0
- package/src/execution/temp-prompt.ts +62 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +102 -0
- package/src/execution/types.ts +1023 -0
- package/src/execution/ui-channels.ts +216 -0
- package/src/execution/ui-interaction-model.ts +48 -0
- package/src/execution/ui-request-handler-factory.ts +243 -0
- package/src/execution/ui-request-observability.ts +81 -0
- package/src/execution/ui-request-queue.ts +184 -0
- package/src/execution/worktree-manager.ts +688 -0
- package/src/execution/worktree-registry.ts +279 -0
- package/src/index.ts +87 -0
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
- package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
- package/src/orchestration/__tests__/args-validator.test.ts +143 -0
- package/src/orchestration/__tests__/config-loader.test.ts +503 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
- package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
- package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
- package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
- package/src/orchestration/__tests__/script-lint.test.ts +831 -0
- package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
- package/src/orchestration/__tests__/test-mocks.ts +197 -0
- package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
- package/src/orchestration/__tests__/worker-host.test.ts +120 -0
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/agent-opts-resolver.ts +174 -0
- package/src/orchestration/args-validator.ts +127 -0
- package/src/orchestration/config-loader.ts +316 -0
- package/src/orchestration/error-recovery.ts +1018 -0
- package/src/orchestration/execute-agent-call.ts +256 -0
- package/src/orchestration/launcher.ts +455 -0
- package/src/orchestration/lifecycle.ts +399 -0
- package/src/orchestration/models/__tests__/budget.test.ts +307 -0
- package/src/orchestration/models/agent-call.ts +83 -0
- package/src/orchestration/models/budget.ts +118 -0
- package/src/orchestration/models/ports.ts +164 -0
- package/src/orchestration/models/run-runtime.ts +104 -0
- package/src/orchestration/models/run-spec.ts +83 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +183 -0
- package/src/orchestration/models/types.ts +301 -0
- package/src/orchestration/models/workflow-run.ts +254 -0
- package/src/orchestration/models/workflow-script-registry.ts +35 -0
- package/src/orchestration/models/workflow-script.ts +117 -0
- package/src/orchestration/script-lint.ts +766 -0
- package/src/orchestration/skill-discovery.ts +135 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +98 -0
- package/src/orchestration/worker-script-builder.ts +421 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +133 -0
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/meta-parser.test.ts +304 -0
- package/src/shared/__tests__/model-ref.test.ts +306 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
- package/src/shared/__tests__/resource-discovery.test.ts +749 -0
- package/src/shared/__tests__/resource-meta.test.ts +51 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/__tests__/timer-delay.test.ts +61 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/agent-ref.ts +57 -0
- package/src/shared/meta-parser.ts +261 -0
- package/src/shared/model-ref.ts +286 -0
- package/src/shared/resource-discovery.ts +835 -0
- package/src/shared/resource-meta.ts +65 -0
- package/src/shared/schema-env.ts +44 -0
- package/src/shared/schema-jsonify.ts +58 -0
- package/src/shared/timer-delay.ts +54 -0
- package/src/shared/xml-injection.ts +35 -0
- package/workflows/README.md +81 -0
- package/workflows/_shared/agent-refs.cjs +40 -0
- package/workflows/chain.js +137 -0
- package/workflows/map-reduce.js +180 -0
- package/workflows/parallel.js +154 -0
- package/workflows/review-fix-loop-utils.cjs +1542 -0
- package/workflows/review-fix-loop.js +1605 -0
- package/workflows/scatter-gather.js +184 -0
|
@@ -0,0 +1,1057 @@
|
|
|
1
|
+
// src/__tests__/record-store.test.ts
|
|
2
|
+
//
|
|
3
|
+
// RecordStore 专属测试。
|
|
4
|
+
// 覆盖:
|
|
5
|
+
// - archive 立即移除(终态 record 不留内存,读时从 session.jsonl 重建)
|
|
6
|
+
// - collectRecords 合并内存(running) + 磁盘(session.jsonl 重建)
|
|
7
|
+
// - collectRecords statusFilter("running" vs "all")
|
|
8
|
+
// - cancelled tombstone override
|
|
9
|
+
// - compareRecords 排序(status priority + startedAt desc)
|
|
10
|
+
// - 重建缓存(notifyChange 失效)
|
|
11
|
+
//
|
|
12
|
+
// 用 tmpdir + 真实 .jsonl fixture(隔离真实文件系统,同 session-reconstructor.test.ts 模式)。
|
|
13
|
+
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
19
|
+
|
|
20
|
+
import { writeAliveMarker } from "../alive-store.ts";
|
|
21
|
+
import { completeRecord, createRecord, tryTransition } from "../execution-record.ts";
|
|
22
|
+
import { writeFinalized } from "../finalized-marker.ts";
|
|
23
|
+
import type { ManifestRecord } from "../manifest-store.ts";
|
|
24
|
+
import { ManifestStore } from "../manifest-store.ts";
|
|
25
|
+
import { getSubagentRecordsDir, getSubagentSessionDir } from "../path-encoding.ts";
|
|
26
|
+
import { SUBAGENT_RECORD_CUSTOM_TYPE } from "../record-entry.ts";
|
|
27
|
+
import type { StatusFilter } from "../record-store.ts";
|
|
28
|
+
import { RecordStore } from "../record-store.ts";
|
|
29
|
+
import { writeCancelledTombstone } from "../tombstone-store.ts";
|
|
30
|
+
import type { AliveMarker, ExecutionRecord } from "../types.ts";
|
|
31
|
+
|
|
32
|
+
/** 构造 ExecutionRecord(base 默认 running,over 覆盖任意字段)。 */
|
|
33
|
+
function makeRecord(over: Partial<ExecutionRecord> = {}): ExecutionRecord {
|
|
34
|
+
const base = createRecord("r1", {
|
|
35
|
+
agent: "worker",
|
|
36
|
+
model: "m",
|
|
37
|
+
mode: "sync",
|
|
38
|
+
task: "t",
|
|
39
|
+
startedAt: 1000,
|
|
40
|
+
rootSessionId: "sess-current",
|
|
41
|
+
// 对齐生产 register 路径(subagent-service createRecord:one-shot 显式 false)
|
|
42
|
+
chatMode: false,
|
|
43
|
+
});
|
|
44
|
+
return { ...base, ...over };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 写一个最小合法的 session.jsonl(含 identity custom entry + 1 个 assistant message)。
|
|
49
|
+
* 用于 collectRecords 磁盘源重建测试。
|
|
50
|
+
*/
|
|
51
|
+
function writeSessionJsonl(
|
|
52
|
+
filePath: string,
|
|
53
|
+
identity: { id: string; agent: string; mode: "sync" | "background"; task: string; startedAt: number; rootSessionId?: string; parentRecordId?: string; depth?: number; lastTs?: number; chatMode?: boolean },
|
|
54
|
+
assistantText = "result text",
|
|
55
|
+
): void {
|
|
56
|
+
const lastTs = identity.lastTs ?? identity.startedAt + 1000;
|
|
57
|
+
const header = JSON.stringify({
|
|
58
|
+
type: "session", version: 3, id: "sess-uuid", timestamp: new Date(identity.startedAt).toISOString(), cwd: "/tmp",
|
|
59
|
+
});
|
|
60
|
+
const identityData: Record<string, unknown> = {
|
|
61
|
+
id: identity.id,
|
|
62
|
+
agent: identity.agent,
|
|
63
|
+
mode: identity.mode,
|
|
64
|
+
task: identity.task,
|
|
65
|
+
startedAt: identity.startedAt,
|
|
66
|
+
};
|
|
67
|
+
if (identity.rootSessionId !== undefined) identityData.rootSessionId = identity.rootSessionId;
|
|
68
|
+
if (identity.parentRecordId !== undefined) identityData.parentRecordId = identity.parentRecordId;
|
|
69
|
+
if (identity.depth !== undefined) identityData.depth = identity.depth;
|
|
70
|
+
if (identity.chatMode !== undefined) identityData.chatMode = identity.chatMode;
|
|
71
|
+
const identityEntry = JSON.stringify({
|
|
72
|
+
type: "custom",
|
|
73
|
+
id: "id-1",
|
|
74
|
+
parentId: null,
|
|
75
|
+
timestamp: new Date(identity.startedAt).toISOString(),
|
|
76
|
+
customType: "subagent-identity",
|
|
77
|
+
data: identityData,
|
|
78
|
+
});
|
|
79
|
+
const assistantMsg = JSON.stringify({
|
|
80
|
+
type: "message",
|
|
81
|
+
id: "msg-1",
|
|
82
|
+
parentId: "id-1",
|
|
83
|
+
timestamp: new Date(lastTs).toISOString(),
|
|
84
|
+
message: {
|
|
85
|
+
role: "assistant",
|
|
86
|
+
content: [{ type: "text", text: assistantText }],
|
|
87
|
+
usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 30, cost: { total: 0 } },
|
|
88
|
+
stopReason: "stop",
|
|
89
|
+
timestamp: lastTs,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
fs.writeFileSync(filePath, `${header}\n${identityEntry}\n${assistantMsg}\n`, "utf-8");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe("RecordStore", () => {
|
|
96
|
+
let rootDir: string;
|
|
97
|
+
let tmpDir: string;
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
// [DS3] 两层布局:tmpDir 深一层(对齐生产 <enc> 段结构),sessions-index.json 落
|
|
101
|
+
// dirname(tmpDir)=rootDir 内随 afterEach 清理(含 MF-3/S-20 的嵌套 <enc> 段索引)。
|
|
102
|
+
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "rs-test-"));
|
|
103
|
+
tmpDir = path.join(rootDir, "sessions");
|
|
104
|
+
fs.mkdirSync(tmpDir);
|
|
105
|
+
});
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
// maxRetries:fire-and-forget 的 sessions-index 写可能与删除并发(ENOTEMPTY 竞态)
|
|
108
|
+
fs.rmSync(rootDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ============================================================
|
|
112
|
+
// archive 立即移除
|
|
113
|
+
// ============================================================
|
|
114
|
+
describe("archive 立即移除", () => {
|
|
115
|
+
it("archive 后 record 立即从内存移除(不再 linger)", () => {
|
|
116
|
+
const store = new RecordStore(tmpDir);
|
|
117
|
+
const r = makeRecord({ id: "sync-1", mode: "sync", status: "closed" });
|
|
118
|
+
store.register(r);
|
|
119
|
+
expect(store.getMutable("sync-1")).toBeDefined();
|
|
120
|
+
store.archive(r);
|
|
121
|
+
expect(store.getMutable("sync-1")).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("background record 同样立即移除(不再 FIFO)", () => {
|
|
125
|
+
const store = new RecordStore(tmpDir);
|
|
126
|
+
const r = makeRecord({ id: "bg-1", mode: "background", status: "closed" });
|
|
127
|
+
store.register(r);
|
|
128
|
+
store.archive(r);
|
|
129
|
+
expect(store.getMutable("bg-1")).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ============================================================
|
|
134
|
+
// collectRecords:内存(running) + 磁盘(重建) 合并
|
|
135
|
+
// ============================================================
|
|
136
|
+
describe("collectRecords 合并", () => {
|
|
137
|
+
it("内存 running record 出现在结果中", () => {
|
|
138
|
+
const store = new RecordStore(tmpDir);
|
|
139
|
+
store.register(makeRecord({ id: "run-1", mode: "background", startedAt: 1000 }));
|
|
140
|
+
const ids = store.collectRecords(100).map((r) => r.id);
|
|
141
|
+
expect(ids).toContain("run-1");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("磁盘 session.jsonl 重建的 record 出现在结果中(无 sidecar → running, SP-2 跨重启可恢复)", () => {
|
|
145
|
+
const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
|
|
146
|
+
writeSessionJsonl(sessionFile, {
|
|
147
|
+
id: "bg-1", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
|
|
148
|
+
});
|
|
149
|
+
// 无 sidecar → 四分支兜底 running(v4 B-1:旧 idle 折入 running,SP-2 跨重启可恢复)
|
|
150
|
+
const store = new RecordStore(tmpDir);
|
|
151
|
+
const found = store.collectRecords(100).find((r) => r.id === "bg-1");
|
|
152
|
+
expect(found).toBeDefined();
|
|
153
|
+
expect(found?.status).toBe("running");
|
|
154
|
+
expect(found?.agent).toBe("worker");
|
|
155
|
+
// [perf] 列表是 light(头部 identity,详情字段缺省)——turns/tokens/result 走 getFullRecord 懒加载
|
|
156
|
+
expect(found?.turns).toBe(0);
|
|
157
|
+
expect(found?.result).toBeUndefined();
|
|
158
|
+
const full = store.getFullRecord("bg-1");
|
|
159
|
+
expect(full?.turns).toBe(1);
|
|
160
|
+
expect(full?.totalTokens).toBe(30);
|
|
161
|
+
expect(full?.result).toBe("result text");
|
|
162
|
+
expect(full?.status).toBe("running"); // 状态矩阵在全量路径同样套用
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("磁盘 session.jsonl + .finalized sidecar → done", () => {
|
|
166
|
+
const sessionFile = path.join(tmpDir, "2026-01-01-uuid-b.jsonl");
|
|
167
|
+
writeSessionJsonl(sessionFile, {
|
|
168
|
+
id: "bg-2", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
|
|
169
|
+
});
|
|
170
|
+
writeFinalized(sessionFile);
|
|
171
|
+
const store = new RecordStore(tmpDir);
|
|
172
|
+
const found = store.collectRecords(100).find((r) => r.id === "bg-2");
|
|
173
|
+
expect(found).toBeDefined();
|
|
174
|
+
expect(found?.status).toBe("closed");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("statusFilter='running' 含内存 + 磁盘跨重启 running(v4 B-1:旧 idle 折入 running)", () => {
|
|
178
|
+
const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
|
|
179
|
+
writeSessionJsonl(sessionFile, {
|
|
180
|
+
id: "bg-1", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
|
|
181
|
+
});
|
|
182
|
+
const store = new RecordStore(tmpDir);
|
|
183
|
+
store.register(makeRecord({ id: "run-1", mode: "background", startedAt: 1000 }));
|
|
184
|
+
const filter: StatusFilter = "running";
|
|
185
|
+
const ids = store.collectRecords(100, filter).map((r) => r.id);
|
|
186
|
+
// 内存 run-1 + 磁盘跨重启 bg-1 都是 running(旧 idle 折入);startedAt desc → bg-1(5000) 先
|
|
187
|
+
expect(ids).toEqual(["bg-1", "run-1"]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("statusFilter='all'(默认)返回内存 + 磁盘", () => {
|
|
191
|
+
const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
|
|
192
|
+
writeSessionJsonl(sessionFile, {
|
|
193
|
+
id: "bg-1", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
|
|
194
|
+
});
|
|
195
|
+
const store = new RecordStore(tmpDir);
|
|
196
|
+
store.register(makeRecord({ id: "run-1", mode: "background", startedAt: 1000 }));
|
|
197
|
+
const ids = store.collectRecords(100).map((r) => r.id);
|
|
198
|
+
expect(ids).toContain("run-1");
|
|
199
|
+
expect(ids).toContain("bg-1");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("内存 running 优先于磁盘同 id(内存覆盖)", () => {
|
|
203
|
+
const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
|
|
204
|
+
writeSessionJsonl(sessionFile, {
|
|
205
|
+
id: "dup-1", agent: "worker", mode: "background", task: "from disk", startedAt: 5000,
|
|
206
|
+
});
|
|
207
|
+
const store = new RecordStore(tmpDir);
|
|
208
|
+
store.register(makeRecord({ id: "dup-1", mode: "background", status: "running", startedAt: 5000 }));
|
|
209
|
+
const found = store.collectRecords(100).find((r) => r.id === "dup-1");
|
|
210
|
+
expect(found?.status).toBe("running"); // 内存 running 覆盖磁盘 idle(SP-2)
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("[MF-3/S-20] 跨进程组合:子进程(worktree)写入 enc(ROOT) 的深层 record 被 ROOT store 重建,同 rootSessionId 全树可见、他树被排除", () => {
|
|
214
|
+
// MF-3 修复后:worktree 子进程把其子 record 的 session 文件写到统一 enc(ROOT cwd) 段,
|
|
215
|
+
// ROOT 进程的 store 扫同一目录重建(旧实现子进程写到 enc(checkout) 段,此处为空)。
|
|
216
|
+
const rootCwd = "/Users/x/root-proj";
|
|
217
|
+
const sessionsDir = getSubagentSessionDir(tmpDir, rootCwd);
|
|
218
|
+
const recordsDir = getSubagentRecordsDir(tmpDir, rootCwd);
|
|
219
|
+
|
|
220
|
+
// 父 record A(ROOT 自己 spawn,parentRecordId 缺省=顶层)+ 孙 record C(B 子进程写入,
|
|
221
|
+
// 同 rootSessionId、parentRecordId=A、depth=2)+ 他树 record X(rootSessionId 不同)
|
|
222
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
223
|
+
writeSessionJsonl(path.join(sessionsDir, "2026-01-01-uuid-a.jsonl"), {
|
|
224
|
+
id: "sa-A", agent: "worker", mode: "background", task: "level1", startedAt: 1000, rootSessionId: "root-main",
|
|
225
|
+
});
|
|
226
|
+
writeSessionJsonl(path.join(sessionsDir, "2026-01-01-uuid-c.jsonl"), {
|
|
227
|
+
id: "sa-C", agent: "worker", mode: "background", task: "level3", startedAt: 3000,
|
|
228
|
+
rootSessionId: "root-main", parentRecordId: "sa-A", depth: 2,
|
|
229
|
+
});
|
|
230
|
+
writeSessionJsonl(path.join(sessionsDir, "2026-01-01-uuid-x.jsonl"), {
|
|
231
|
+
id: "sa-X", agent: "worker", mode: "background", task: "other root", startedAt: 4000, rootSessionId: "root-other",
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ROOT 进程的 store:sessionsDir/recordsDir 与子进程写盘目录同段(getSubagentSessionDir 同源)
|
|
235
|
+
const store = new RecordStore(sessionsDir, new ManifestStore(recordsDir), undefined);
|
|
236
|
+
const recs = store.collectRecords(10, "all", "root-main");
|
|
237
|
+
const ids = recs.map((r) => r.id);
|
|
238
|
+
|
|
239
|
+
// 全树可见:A(顶层)与 C(深度 2,跨进程写入)都在列表,身份字段正确
|
|
240
|
+
expect(ids).toContain("sa-A");
|
|
241
|
+
expect(ids).toContain("sa-C");
|
|
242
|
+
const c = recs.find((r) => r.id === "sa-C");
|
|
243
|
+
expect(c?.parentRecordId).toBe("sa-A");
|
|
244
|
+
expect(c?.depth).toBe(2);
|
|
245
|
+
// 他树 record 被 rootSessionFilter 排除(隔离不破坏)
|
|
246
|
+
expect(ids).not.toContain("sa-X");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ============================================================
|
|
251
|
+
// cancelled tombstone override
|
|
252
|
+
// ============================================================
|
|
253
|
+
describe("cancelled tombstone", () => {
|
|
254
|
+
it("有 .cancelled sidecar → status override 为 closed + closedReason=cancelled", () => {
|
|
255
|
+
const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
|
|
256
|
+
writeSessionJsonl(sessionFile, {
|
|
257
|
+
id: "bg-1", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
|
|
258
|
+
});
|
|
259
|
+
writeCancelledTombstone(sessionFile, {
|
|
260
|
+
id: "bg-1", status: "cancelled", agent: "worker", startedAt: 5000, endedAt: 6000,
|
|
261
|
+
});
|
|
262
|
+
const store = new RecordStore(tmpDir);
|
|
263
|
+
const found = store.collectRecords(100).find((r) => r.id === "bg-1");
|
|
264
|
+
// v4 B-1:cancelled 折入 closed,closedReason='cancelled' 保留用户取消语义
|
|
265
|
+
expect(found?.status).toBe("closed");
|
|
266
|
+
expect(found?.closedReason).toBe("cancelled");
|
|
267
|
+
expect(found?.error).toBe("cancelled by user");
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ============================================================
|
|
272
|
+
// compareRecords 排序稳定性(内存 running record)
|
|
273
|
+
// ============================================================
|
|
274
|
+
describe("compareRecords 排序", () => {
|
|
275
|
+
it("status priority(running < closed)", () => {
|
|
276
|
+
const store = new RecordStore(tmpDir);
|
|
277
|
+
// 内存 running record
|
|
278
|
+
const running = makeRecord({ id: "run-1", mode: "background", startedAt: 3000, status: "running" });
|
|
279
|
+
store.register(running);
|
|
280
|
+
// 磁盘 closed record(.finalized sidecar → closed,v4 B-1 统一终态)
|
|
281
|
+
const doneFile = path.join(tmpDir, "a.jsonl");
|
|
282
|
+
writeSessionJsonl(doneFile, {
|
|
283
|
+
id: "done-1", agent: "w", mode: "background", task: "t", startedAt: 5000,
|
|
284
|
+
});
|
|
285
|
+
writeFinalized(doneFile);
|
|
286
|
+
const ids = store.collectRecords(100).map((r) => r.id);
|
|
287
|
+
expect(ids[0]).toBe("run-1"); // running(0) 排在 closed(3) 前
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("同 status 时 startedAt desc(新→旧)", () => {
|
|
291
|
+
const store = new RecordStore(tmpDir);
|
|
292
|
+
writeSessionJsonl(path.join(tmpDir, "old.jsonl"), {
|
|
293
|
+
id: "old", agent: "w", mode: "background", task: "t", startedAt: 1000,
|
|
294
|
+
});
|
|
295
|
+
writeSessionJsonl(path.join(tmpDir, "new.jsonl"), {
|
|
296
|
+
id: "new", agent: "w", mode: "background", task: "t", startedAt: 9000,
|
|
297
|
+
});
|
|
298
|
+
// 两个都是 running(磁盘重建,无 sidecar → SP-2 兜底 running,旧 idle 折入),按 startedAt desc
|
|
299
|
+
const ids = store.collectRecords(100).map((r) => r.id);
|
|
300
|
+
expect(ids).toEqual(["new", "old"]);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ============================================================
|
|
305
|
+
// running 态 record(M2-A chatMode 续聊;v4 B-1:旧 idle 折入 running)
|
|
306
|
+
// ============================================================
|
|
307
|
+
describe("running 态 record(旧 idle 折入,等待续聊)", () => {
|
|
308
|
+
it("getMutable 能查到 running record(不筛 status,running record 留内存不 archive)", () => {
|
|
309
|
+
const store = new RecordStore(tmpDir);
|
|
310
|
+
const r = makeRecord({ id: "idle-1", status: "running" });
|
|
311
|
+
store.register(r);
|
|
312
|
+
// running record 不 archive → getMutable 仍可查(续聊定位依赖此)
|
|
313
|
+
expect(store.getMutable("idle-1")).toBe(r);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("running record 经 collectRecords(statusFilter=all) 可见", () => {
|
|
317
|
+
const store = new RecordStore(tmpDir);
|
|
318
|
+
store.register(makeRecord({ id: "idle-2", status: "running" }));
|
|
319
|
+
const ids = store.collectRecords(100, "all").map((r) => r.id);
|
|
320
|
+
expect(ids).toContain("idle-2");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("running record 经 collectRecords(statusFilter=running) 可见(v4 B-1:旧 idle 折入 running)", () => {
|
|
324
|
+
const store = new RecordStore(tmpDir);
|
|
325
|
+
store.register(makeRecord({ id: "idle-3", status: "running" }));
|
|
326
|
+
const ids = store.collectRecords(100, "running").map((r) => r.id);
|
|
327
|
+
expect(ids).toContain("idle-3");
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
// ============================================================
|
|
333
|
+
// 重建缓存
|
|
334
|
+
// ============================================================
|
|
335
|
+
describe("重建缓存", () => {
|
|
336
|
+
it("新 session.jsonl 无需内存事件即立即可见([perf] stat 校验取代 notifyChange 整体失效)", () => {
|
|
337
|
+
const store = new RecordStore(tmpDir);
|
|
338
|
+
// 首次 collect:空目录
|
|
339
|
+
expect(store.collectRecords(100)).toHaveLength(0);
|
|
340
|
+
// 写新 session.jsonl(模拟外部进程/子进程写入——不经本进程任何内存事件)
|
|
341
|
+
writeSessionJsonl(path.join(tmpDir, "new.jsonl"), {
|
|
342
|
+
id: "bg-1", agent: "w", mode: "background", task: "t", startedAt: 1000,
|
|
343
|
+
});
|
|
344
|
+
// [perf] 旧实现:reconCache 建立后只能靠 register/archive 触发 notifyChange 失效,
|
|
345
|
+
// 否则永远 stale(外部进程写入的文件对本进程不可见)。新实现:每次 collectRecords
|
|
346
|
+
// 都 readdir + stat 校验,新文件立即可见(同时修复跨进程可见性)。
|
|
347
|
+
const ids = store.collectRecords(100).map((r) => r.id);
|
|
348
|
+
expect(ids).toContain("bg-1");
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ============================================================
|
|
353
|
+
// dispose / revive
|
|
354
|
+
// ============================================================
|
|
355
|
+
describe("dispose / revive", () => {
|
|
356
|
+
it("dispose 后 notifyChange 不再触发 listener", () => {
|
|
357
|
+
const store = new RecordStore(tmpDir);
|
|
358
|
+
let count = 0;
|
|
359
|
+
store.onChange(() => { count++; });
|
|
360
|
+
store.register(makeRecord({ id: "r1", startedAt: 1000 }));
|
|
361
|
+
expect(count).toBe(1);
|
|
362
|
+
store.dispose();
|
|
363
|
+
store.register(makeRecord({ id: "r2", startedAt: 2000 }));
|
|
364
|
+
expect(count).toBe(1); // dispose 后不再通知
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ============================================================
|
|
369
|
+
// 四分支 sidecar 矩阵(D-006 + D-021)
|
|
370
|
+
// ============================================================
|
|
371
|
+
describe("四分支 sidecar 矩阵", () => {
|
|
372
|
+
const SESSION_ID = "bg-1";
|
|
373
|
+
const STARTED_AT = 1000;
|
|
374
|
+
|
|
375
|
+
function writeBaseSession(): string {
|
|
376
|
+
const sessionFile = path.join(tmpDir, "2026-01-01-uuid-sidecar.jsonl");
|
|
377
|
+
writeSessionJsonl(sessionFile, {
|
|
378
|
+
id: SESSION_ID, agent: "worker", mode: "background", task: "do it", startedAt: STARTED_AT,
|
|
379
|
+
});
|
|
380
|
+
return sessionFile;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── 分支 1: .cancelled ──
|
|
384
|
+
it(".cancelled sidecar → closed + closedReason=cancelled", () => {
|
|
385
|
+
const sessionFile = writeBaseSession();
|
|
386
|
+
writeCancelledTombstone(sessionFile, {
|
|
387
|
+
id: SESSION_ID, status: "cancelled", agent: "worker", startedAt: STARTED_AT, endedAt: 6000,
|
|
388
|
+
});
|
|
389
|
+
const store = new RecordStore(tmpDir);
|
|
390
|
+
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
391
|
+
// v4 B-1:cancelled 折入 closed(closedReason='cancelled' 区分用户取消)
|
|
392
|
+
expect(found?.status).toBe("closed");
|
|
393
|
+
expect(found?.closedReason).toBe("cancelled");
|
|
394
|
+
expect(found?.error).toBe("cancelled by user");
|
|
395
|
+
expect(found?.endedAt).toBe(6000);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// ── 分支 2: .finalized done ──
|
|
399
|
+
it(".finalized sidecar + stopReason=stop → done", () => {
|
|
400
|
+
const sessionFile = writeBaseSession();
|
|
401
|
+
writeFinalized(sessionFile);
|
|
402
|
+
const store = new RecordStore(tmpDir);
|
|
403
|
+
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
404
|
+
expect(found?.status).toBe("closed");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// ── 分支 2: .finalized failed ──
|
|
408
|
+
it(".finalized sidecar + stopReason=error → failed", () => {
|
|
409
|
+
// 写一个 stopReason=error 的 session.jsonl
|
|
410
|
+
const sessionFile = path.join(tmpDir, "2026-01-01-uuid-fail.jsonl");
|
|
411
|
+
const header = JSON.stringify({
|
|
412
|
+
type: "session", version: 3, id: "sess-uuid", timestamp: new Date(STARTED_AT).toISOString(), cwd: "/tmp",
|
|
413
|
+
});
|
|
414
|
+
const identityEntry = JSON.stringify({
|
|
415
|
+
type: "custom", id: "id-1", parentId: null, timestamp: new Date(STARTED_AT).toISOString(),
|
|
416
|
+
customType: "subagent-identity",
|
|
417
|
+
data: { id: SESSION_ID, agent: "worker", mode: "background", task: "do it", startedAt: STARTED_AT },
|
|
418
|
+
});
|
|
419
|
+
const assistantMsg = JSON.stringify({
|
|
420
|
+
type: "message", id: "msg-1", parentId: "id-1",
|
|
421
|
+
timestamp: new Date(STARTED_AT + 1000).toISOString(),
|
|
422
|
+
message: {
|
|
423
|
+
role: "assistant",
|
|
424
|
+
content: [{ type: "text", text: "error output" }],
|
|
425
|
+
usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 30, cost: { total: 0 } },
|
|
426
|
+
stopReason: "error",
|
|
427
|
+
errorMessage: "something went wrong",
|
|
428
|
+
timestamp: STARTED_AT + 1000,
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
fs.writeFileSync(sessionFile, `${header}\n${identityEntry}\n${assistantMsg}\n`, "utf-8");
|
|
432
|
+
|
|
433
|
+
writeFinalized(sessionFile);
|
|
434
|
+
const store = new RecordStore(tmpDir);
|
|
435
|
+
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
436
|
+
expect(found?.status).toBe("closed");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// ── 分支 3: .alive + 活 pid → running + externalInstance ──
|
|
440
|
+
it(".alive + 存活 pid → running + externalInstance=true", () => {
|
|
441
|
+
const sessionFile = writeBaseSession();
|
|
442
|
+
const recentStartedAt = Date.now() - 1000; // 1 秒前,确保未超 24h
|
|
443
|
+
const marker: AliveMarker = { pid: process.pid, id: SESSION_ID, startedAt: recentStartedAt };
|
|
444
|
+
writeAliveMarker(sessionFile, marker);
|
|
445
|
+
const store = new RecordStore(tmpDir);
|
|
446
|
+
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
447
|
+
expect(found?.status).toBe("running");
|
|
448
|
+
expect(found?.externalInstance).toEqual(marker);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// ── 分支 3→4: .alive + 死 pid → running(SP-2:跨重启可恢复)──
|
|
452
|
+
it(".alive + 死 pid → running(SP-2 跨重启可恢复)", () => {
|
|
453
|
+
const sessionFile = writeBaseSession();
|
|
454
|
+
const marker: AliveMarker = { pid: 9999999, id: SESSION_ID, startedAt: STARTED_AT };
|
|
455
|
+
writeAliveMarker(sessionFile, marker);
|
|
456
|
+
const store = new RecordStore(tmpDir);
|
|
457
|
+
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
458
|
+
expect(found?.status).toBe("running");
|
|
459
|
+
expect(found?.externalInstance).toBeUndefined();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// ── 分支 4: 都无 sidecar → running(SP-2:跨重启可恢复)──
|
|
463
|
+
it("无任何 sidecar → running(SP-2 跨重启可恢复)", () => {
|
|
464
|
+
writeBaseSession();
|
|
465
|
+
const store = new RecordStore(tmpDir);
|
|
466
|
+
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
467
|
+
expect(found?.status).toBe("running");
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// ── 分支 4: >24h 软超时 → running(SP-2:跨重启可恢复,无视探活)──
|
|
471
|
+
it(">24h 软超时 → running(SP-2,即使 pid 存活)", () => {
|
|
472
|
+
const sessionFile = writeBaseSession();
|
|
473
|
+
// startedAt 设为 25 小时前
|
|
474
|
+
const oldStartedAt = Date.now() - 25 * 60 * 60 * 1000;
|
|
475
|
+
const marker: AliveMarker = { pid: process.pid, id: SESSION_ID, startedAt: oldStartedAt };
|
|
476
|
+
writeAliveMarker(sessionFile, marker);
|
|
477
|
+
|
|
478
|
+
// 重写 session.jsonl 使 startedAt 匹配
|
|
479
|
+
fs.unlinkSync(sessionFile);
|
|
480
|
+
writeSessionJsonl(sessionFile, {
|
|
481
|
+
id: SESSION_ID, agent: "worker", mode: "background", task: "do it", startedAt: oldStartedAt,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const store = new RecordStore(tmpDir);
|
|
485
|
+
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
486
|
+
expect(found?.status).toBe("running");
|
|
487
|
+
expect(found?.externalInstance).toBeUndefined();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// ── 回归:.cancelled 优先于 .finalized ──
|
|
491
|
+
it(".cancelled 优先于 .finalized(即使两者共存)", () => {
|
|
492
|
+
const sessionFile = writeBaseSession();
|
|
493
|
+
writeFinalized(sessionFile);
|
|
494
|
+
writeCancelledTombstone(sessionFile, {
|
|
495
|
+
id: SESSION_ID, status: "cancelled", agent: "worker", startedAt: STARTED_AT, endedAt: 6000,
|
|
496
|
+
});
|
|
497
|
+
const store = new RecordStore(tmpDir);
|
|
498
|
+
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
499
|
+
// v4 B-1:.cancelled 优先级不变,但 status 折入 closed(closedReason='cancelled')
|
|
500
|
+
expect(found?.status).toBe("closed");
|
|
501
|
+
expect(found?.closedReason).toBe("cancelled");
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// ── 回归:旧 .cancelled 单分支行为不变 ──
|
|
505
|
+
it("旧 .cancelled 单分支行为不变(回归,status 折入 closed)", () => {
|
|
506
|
+
const sessionFile = writeBaseSession();
|
|
507
|
+
writeCancelledTombstone(sessionFile, {
|
|
508
|
+
id: SESSION_ID, status: "cancelled", agent: "worker", startedAt: STARTED_AT, endedAt: 7000,
|
|
509
|
+
});
|
|
510
|
+
const store = new RecordStore(tmpDir);
|
|
511
|
+
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
512
|
+
expect(found?.status).toBe("closed");
|
|
513
|
+
expect(found?.closedReason).toBe("cancelled");
|
|
514
|
+
expect(found?.error).toBe("cancelled by user");
|
|
515
|
+
expect(found?.endedAt).toBe(7000);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// ============================================================
|
|
520
|
+
// session 隔离(问题 1 修复)
|
|
521
|
+
// ============================================================
|
|
522
|
+
describe("session 隔离(rootSessionId 过滤)", () => {
|
|
523
|
+
it("磁盘源:只返回 rootSessionId 匹配的 record", () => {
|
|
524
|
+
writeSessionJsonl(path.join(tmpDir, "mine.jsonl"), {
|
|
525
|
+
id: "mine", agent: "w", mode: "background", task: "t", startedAt: 1000, rootSessionId: "sess-A",
|
|
526
|
+
});
|
|
527
|
+
writeSessionJsonl(path.join(tmpDir, "other.jsonl"), {
|
|
528
|
+
id: "other", agent: "w", mode: "background", task: "t", startedAt: 2000, rootSessionId: "sess-B",
|
|
529
|
+
});
|
|
530
|
+
const store = new RecordStore(tmpDir);
|
|
531
|
+
const ids = store.collectRecords(100, "all", "sess-A").map((r) => r.id);
|
|
532
|
+
expect(ids).toEqual(["mine"]); // other 属于 sess-B,被隔离
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("磁盘源:rootSessionId 缺失(旧文件)被排除(无法判定归属)", () => {
|
|
536
|
+
writeSessionJsonl(path.join(tmpDir, "legacy.jsonl"), {
|
|
537
|
+
id: "legacy", agent: "w", mode: "background", task: "t", startedAt: 1000, // 无 rootSessionId
|
|
538
|
+
});
|
|
539
|
+
const store = new RecordStore(tmpDir);
|
|
540
|
+
const ids = store.collectRecords(100, "all", "sess-A").map((r) => r.id);
|
|
541
|
+
expect(ids).toEqual([]); // 旧文件被排除
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("磁盘源:不传 filter(undefined)不过滤(向后兼容)", () => {
|
|
545
|
+
writeSessionJsonl(path.join(tmpDir, "legacy.jsonl"), {
|
|
546
|
+
id: "legacy", agent: "w", mode: "background", task: "t", startedAt: 1000,
|
|
547
|
+
});
|
|
548
|
+
writeSessionJsonl(path.join(tmpDir, "tagged.jsonl"), {
|
|
549
|
+
id: "tagged", agent: "w", mode: "background", task: "t", startedAt: 2000, rootSessionId: "sess-A",
|
|
550
|
+
});
|
|
551
|
+
const store = new RecordStore(tmpDir);
|
|
552
|
+
const ids = store.collectRecords(100).map((r) => r.id);
|
|
553
|
+
expect(ids.sort()).toEqual(["legacy", "tagged"]); // 全返回,不过滤
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("内存源:只返回 rootSessionId 匹配的 running record", () => {
|
|
557
|
+
const store = new RecordStore(tmpDir);
|
|
558
|
+
store.register(makeRecord({ id: "mine", startedAt: 1000, rootSessionId: "sess-A" }));
|
|
559
|
+
store.register(makeRecord({ id: "other", startedAt: 2000, rootSessionId: "sess-B" }));
|
|
560
|
+
const ids = store.collectRecords(100, "all", "sess-A").map((r) => r.id);
|
|
561
|
+
expect(ids).toEqual(["mine"]);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("内存+磁盘混合:同时按 session 过滤", () => {
|
|
565
|
+
writeSessionJsonl(path.join(tmpDir, "disk-A.jsonl"), {
|
|
566
|
+
id: "disk-A", agent: "w", mode: "background", task: "t", startedAt: 1000, rootSessionId: "sess-A",
|
|
567
|
+
});
|
|
568
|
+
writeSessionJsonl(path.join(tmpDir, "disk-B.jsonl"), {
|
|
569
|
+
id: "disk-B", agent: "w", mode: "background", task: "t", startedAt: 2000, rootSessionId: "sess-B",
|
|
570
|
+
});
|
|
571
|
+
const store = new RecordStore(tmpDir);
|
|
572
|
+
store.register(makeRecord({ id: "mem-A", startedAt: 3000, rootSessionId: "sess-A" }));
|
|
573
|
+
store.register(makeRecord({ id: "mem-B", startedAt: 4000, rootSessionId: "sess-B" }));
|
|
574
|
+
const ids = store.collectRecords(100, "all", "sess-A").map((r) => r.id).sort();
|
|
575
|
+
expect(ids).toEqual(["disk-A", "mem-A"]);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("重建缓存:不同 filter 共享缓存,不交叉污染", () => {
|
|
579
|
+
writeSessionJsonl(path.join(tmpDir, "a.jsonl"), {
|
|
580
|
+
id: "a", agent: "w", mode: "background", task: "t", startedAt: 1000, rootSessionId: "sess-A",
|
|
581
|
+
});
|
|
582
|
+
writeSessionJsonl(path.join(tmpDir, "b.jsonl"), {
|
|
583
|
+
id: "b", agent: "w", mode: "background", task: "t", startedAt: 2000, rootSessionId: "sess-B",
|
|
584
|
+
});
|
|
585
|
+
const store = new RecordStore(tmpDir);
|
|
586
|
+
// 先查 sess-A(建缓存)
|
|
587
|
+
expect(store.collectRecords(100, "all", "sess-A").map((r) => r.id)).toEqual(["a"]);
|
|
588
|
+
// 再查 sess-B(复用缓存基底,过滤不交叉)
|
|
589
|
+
expect(store.collectRecords(100, "all", "sess-B").map((r) => r.id)).toEqual(["b"]);
|
|
590
|
+
// 不带 filter(复用缓存,全量)
|
|
591
|
+
expect(store.collectRecords(100).map((r) => r.id).sort()).toEqual(["a", "b"]);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// ============================================================
|
|
596
|
+
// endedAt 重建(问题 2 修复:终态耗时不再随墙钟增长)
|
|
597
|
+
// ============================================================
|
|
598
|
+
describe("endedAt 重建(耗时不再无限增长)", () => {
|
|
599
|
+
it(".finalized → light endedAt 用 mtime 近似,全量 endedAt 为最后 entry 时间戳", () => {
|
|
600
|
+
const sessionFile = path.join(tmpDir, "fin.jsonl");
|
|
601
|
+
writeSessionJsonl(sessionFile, {
|
|
602
|
+
id: "bg-1", agent: "w", mode: "background", task: "t",
|
|
603
|
+
startedAt: 5000, lastTs: 9000, rootSessionId: "sess-A",
|
|
604
|
+
});
|
|
605
|
+
writeFinalized(sessionFile);
|
|
606
|
+
const store = new RecordStore(tmpDir);
|
|
607
|
+
const found = store.collectRecords(100, "all", "sess-A").find((r) => r.id === "bg-1");
|
|
608
|
+
// [perf] light 分支 2:endedAt 用 jsonl mtime 近似(finalize 后文件不再变化,
|
|
609
|
+
// 与最后 entry ts 差 <1s)——不是 entry ts 9000,也不是随墙钟无限增长的 now 基准。
|
|
610
|
+
expect(found?.endedAt).toBeDefined();
|
|
611
|
+
expect(found?.endedAt).toBeGreaterThan(5000);
|
|
612
|
+
// 全量(getFullRecord):精确用最后 entry 时间戳(非 now)。
|
|
613
|
+
expect(store.getFullRecord("bg-1")?.endedAt).toBe(9000);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it("无 sidecar(running, SP-2)→ endedAt 保持 undefined(非终态)", () => {
|
|
617
|
+
const sessionFile = path.join(tmpDir, "crash.jsonl");
|
|
618
|
+
writeSessionJsonl(sessionFile, {
|
|
619
|
+
id: "bg-1", agent: "w", mode: "background", task: "t",
|
|
620
|
+
startedAt: 5000, lastTs: 9000, rootSessionId: "sess-A",
|
|
621
|
+
});
|
|
622
|
+
const store = new RecordStore(tmpDir);
|
|
623
|
+
const found = store.collectRecords(100, "all", "sess-A").find((r) => r.id === "bg-1");
|
|
624
|
+
expect(found?.status).toBe("running");
|
|
625
|
+
// SP-2: running 是非终态(旧 idle 折入),endedAt 保持 undefined(待续聊)
|
|
626
|
+
expect(found?.endedAt).toBeUndefined();
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it(".alive running → endedAt 保持 undefined(running 耗时继续增长是正确的)", () => {
|
|
630
|
+
const sessionFile = path.join(tmpDir, "alive.jsonl");
|
|
631
|
+
writeSessionJsonl(sessionFile, {
|
|
632
|
+
id: "bg-1", agent: "w", mode: "background", task: "t",
|
|
633
|
+
startedAt: Date.now() - 1000, lastTs: Date.now(), rootSessionId: "sess-A",
|
|
634
|
+
});
|
|
635
|
+
writeAliveMarker(sessionFile, { pid: process.pid, id: "bg-1", startedAt: Date.now() - 1000 });
|
|
636
|
+
const store = new RecordStore(tmpDir);
|
|
637
|
+
const found = store.collectRecords(100, "all", "sess-A").find((r) => r.id === "bg-1");
|
|
638
|
+
expect(found?.status).toBe("running");
|
|
639
|
+
expect(found?.endedAt).toBeUndefined();
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// ============================================================
|
|
644
|
+
// [BL-M1] 修复:损坏 manifest 上报 pi.appendEntry(用户可见)
|
|
645
|
+
// collectRecords 跳过 mapManifestStatus 返回 null 的 manifest 时,除 console.warn
|
|
646
|
+
// 外还需调 pi.appendEntry("subagent:manifest-invalid-status", ...) 让用户能在 session
|
|
647
|
+
// 内复盘事故——避免磁盘陈旧/损坏 manifest 被静默吞掉。
|
|
648
|
+
// ============================================================
|
|
649
|
+
describe("manifest status 越界(损坏 manifest)", () => {
|
|
650
|
+
/** 构造一个 status 越界的 manifest mock。
|
|
651
|
+
* ManifestRecord.status union 是 4 态,运行时却可能收到陈旧 "error" / 意外 "crashed"
|
|
652
|
+
* 等越界值——isValidManifest 当前挡掉了大部分,但 manifestStore.listAllSync 与
|
|
653
|
+
* mapManifestStatus 之间仍有防御层([BL-M1] 修复记录)。本测试用 unknown-cast 模拟
|
|
654
|
+
* 一个越过 ManifestRecord 静态类型的对象,验证 collectRecords 反应正确。 */
|
|
655
|
+
function brokenManifest(): ManifestRecord {
|
|
656
|
+
// eslint-disable-next-line taste/no-unsafe-cast -- intentional: cast status 越界值模拟磁盘损坏 manifest(测试目的本身)
|
|
657
|
+
return {
|
|
658
|
+
id: "broken-1",
|
|
659
|
+
rootSessionId: "sess-current",
|
|
660
|
+
agentName: "worker",
|
|
661
|
+
status: "crashed", // 越界值(union 不含 "crashed",SP-1 后 valid statuses = running/closed/cancelled)
|
|
662
|
+
createdAt: 1000,
|
|
663
|
+
} as unknown as ManifestRecord;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Mock ManifestStore 让测试不依赖 fs 路径 + isValidManifest 守卫。
|
|
667
|
+
* source/target 类型不兼容是 intentional(ManifestStore 有多个方法,测试仅需 listAllSync),
|
|
668
|
+
* `as unknown as ManifestStore` 是 vitest mock 标准 pattern。*/
|
|
669
|
+
function makeManifestStoreMock(manifests: ManifestRecord[]): ManifestStore {
|
|
670
|
+
// eslint-disable-next-line taste/no-unsafe-cast -- intentional: ManifestStore 有多个方法,测试仅需 listAllSync
|
|
671
|
+
return {
|
|
672
|
+
// 复制 manifests 防调用方意外 mutate 共享数组;vi.fn 每次调用返回独立数组
|
|
673
|
+
listAllSync: vi.fn(() => [...manifests]),
|
|
674
|
+
} as unknown as ManifestStore;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
it("collectRecords 跳过越界 status 时调用 pi.appendEntry('subagent:manifest-invalid-status', ...)", () => {
|
|
678
|
+
const pi = { appendEntry: vi.fn() };
|
|
679
|
+
// Mock ManifestStore:listAllSync 返回损坏 manifest,绕开真实 fs 路径与 isValidManifest 守卫。
|
|
680
|
+
const manifestStore = makeManifestStoreMock([brokenManifest()]);
|
|
681
|
+
const store = new RecordStore(tmpDir, manifestStore, pi);
|
|
682
|
+
|
|
683
|
+
const result = store.collectRecords(100);
|
|
684
|
+
|
|
685
|
+
// 损坏 manifest 不出现在结果中(status 越界被跳过,不降级为 failed——避免误告警)
|
|
686
|
+
expect(result.find((r) => r.id === "broken-1")).toBeUndefined();
|
|
687
|
+
// appendEntry 被调用,customType + data 字段正确
|
|
688
|
+
expect(pi.appendEntry).toHaveBeenCalledTimes(1);
|
|
689
|
+
expect(pi.appendEntry).toHaveBeenCalledWith("subagent:manifest-invalid-status", {
|
|
690
|
+
id: "broken-1",
|
|
691
|
+
status: "crashed",
|
|
692
|
+
rootSessionId: "sess-current",
|
|
693
|
+
agentName: "worker",
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it("pi 为 null/undefined 时不抛错(向后兼容:不注入 pi 的旧调用路径)", () => {
|
|
698
|
+
const manifestStore = makeManifestStoreMock([brokenManifest()]);
|
|
699
|
+
// 不传 pi(undefined)—— 模拟 SubagentService 构造期 session_start 未触发的场景。
|
|
700
|
+
const store = new RecordStore(tmpDir, manifestStore);
|
|
701
|
+
expect(() => store.collectRecords(100)).not.toThrow();
|
|
702
|
+
// 损坏 manifest 仍被跳过(不降级)
|
|
703
|
+
const result = store.collectRecords(100);
|
|
704
|
+
expect(result.find((r) => r.id === "broken-1")).toBeUndefined();
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("setPi() 后 appendEntry 切换到新 pi(覆盖初始 undefined)", () => {
|
|
708
|
+
const pi1 = { appendEntry: vi.fn() };
|
|
709
|
+
const pi2 = { appendEntry: vi.fn() };
|
|
710
|
+
const manifestStore = makeManifestStoreMock([brokenManifest()]);
|
|
711
|
+
const store = new RecordStore(tmpDir, manifestStore, pi1);
|
|
712
|
+
store.setPi(pi2);
|
|
713
|
+
|
|
714
|
+
store.collectRecords(100);
|
|
715
|
+
|
|
716
|
+
// 切换后只调 pi2(pi1 不再被调用)
|
|
717
|
+
expect(pi1.appendEntry).not.toHaveBeenCalled();
|
|
718
|
+
expect(pi2.appendEntry).toHaveBeenCalledTimes(1);
|
|
719
|
+
expect(pi2.appendEntry).toHaveBeenCalledWith(
|
|
720
|
+
"subagent:manifest-invalid-status",
|
|
721
|
+
expect.objectContaining({ id: "broken-1" }),
|
|
722
|
+
);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// ============================================================
|
|
727
|
+
// SP-2: reconstructAll 兜底分支输出 running(非 crashed;v4 两态后旧 idle 折入 running)
|
|
728
|
+
// ============================================================
|
|
729
|
+
describe("SP-2: reconstructAll 兜底 running(跨重启恢复)", () => {
|
|
730
|
+
// TC-1: 兜底分支输出 running(非 crashed)
|
|
731
|
+
it("TC-1: 无 sidecar + 死 pid → reconstructAll 输出 running(可冷路径 resume)", () => {
|
|
732
|
+
const sessionFile = path.join(tmpDir, "sp2-tc1.jsonl");
|
|
733
|
+
writeSessionJsonl(sessionFile, {
|
|
734
|
+
id: "sa-sp2-1", agent: "worker", mode: "background", task: "cross-restart task",
|
|
735
|
+
startedAt: 1000, rootSessionId: "sess-sp2",
|
|
736
|
+
});
|
|
737
|
+
// 无任何 sidecar marker → 分支 4 兜底
|
|
738
|
+
const store = new RecordStore(tmpDir);
|
|
739
|
+
const found = store.collectRecords(100, "all", "sess-sp2").find((r) => r.id === "sa-sp2-1");
|
|
740
|
+
expect(found).toBeDefined();
|
|
741
|
+
expect(found?.status).toBe("running");
|
|
742
|
+
// SP-2: running 是非终态(旧 idle 折入),endedAt 保持 undefined
|
|
743
|
+
expect(found?.endedAt).toBeUndefined();
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// TC-2: 有 .finalized marker 的 record 仍归档为 done(不回归)
|
|
747
|
+
it("TC-2: .finalized marker → done(分支 1/2 不回归)", () => {
|
|
748
|
+
const sessionFile = path.join(tmpDir, "sp2-tc2.jsonl");
|
|
749
|
+
writeSessionJsonl(sessionFile, {
|
|
750
|
+
id: "sa-sp2-2", agent: "worker", mode: "background", task: "finalized task",
|
|
751
|
+
startedAt: 2000, rootSessionId: "sess-sp2",
|
|
752
|
+
});
|
|
753
|
+
writeFinalized(sessionFile);
|
|
754
|
+
const store = new RecordStore(tmpDir);
|
|
755
|
+
const found = store.collectRecords(100, "all", "sess-sp2").find((r) => r.id === "sa-sp2-2");
|
|
756
|
+
expect(found).toBeDefined();
|
|
757
|
+
// 分支 2 仍归档为 done,不受 SP-2 影响
|
|
758
|
+
expect(found?.status).toBe("closed");
|
|
759
|
+
expect(found?.endedAt).toBeDefined();
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// TC-2b: .cancelled marker → closed+cancelled(分支 1 不回归)
|
|
763
|
+
it("TC-2b: .cancelled marker → closed+cancelled reason(分支 1 不回归)", () => {
|
|
764
|
+
const sessionFile = path.join(tmpDir, "sp2-tc2b.jsonl");
|
|
765
|
+
writeSessionJsonl(sessionFile, {
|
|
766
|
+
id: "sa-sp2-2b", agent: "worker", mode: "background", task: "cancelled task",
|
|
767
|
+
startedAt: 3000, rootSessionId: "sess-sp2",
|
|
768
|
+
});
|
|
769
|
+
writeCancelledTombstone(sessionFile, {
|
|
770
|
+
id: "sa-sp2-2b", status: "cancelled", agent: "worker", startedAt: 3000, endedAt: 4000,
|
|
771
|
+
});
|
|
772
|
+
const store = new RecordStore(tmpDir);
|
|
773
|
+
const found = store.collectRecords(100, "all", "sess-sp2").find((r) => r.id === "sa-sp2-2b");
|
|
774
|
+
expect(found).toBeDefined();
|
|
775
|
+
expect(found?.status).toBe("closed");
|
|
776
|
+
expect(found?.closedReason).toBe("cancelled");
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// ============================================================
|
|
781
|
+
// 孤儿终态恢复(residual-fixes):分支 4 兜底 record 的判定与落盘
|
|
782
|
+
// ============================================================
|
|
783
|
+
describe("recoverOrphanRecords 孤儿终态恢复", () => {
|
|
784
|
+
/** 带真实磁盘 fixture + appendEntry 捕获的 store。 */
|
|
785
|
+
function makeRecoveryStore(): { store: RecordStore; appended: Array<{ customType: string; data: Record<string, unknown> }> } {
|
|
786
|
+
const appended: Array<{ customType: string; data: Record<string, unknown> }> = [];
|
|
787
|
+
const store = new RecordStore(tmpDir, undefined, {
|
|
788
|
+
appendEntry: (customType: string, data: unknown) => {
|
|
789
|
+
appended.push({ customType, data: data as Record<string, unknown> });
|
|
790
|
+
},
|
|
791
|
+
} as never);
|
|
792
|
+
return { store, appended };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
it("末行完整 → closed 终态 entry + .finalized sidecar(防重锚)", () => {
|
|
796
|
+
const sessionFile = path.join(tmpDir, "orphan-done.jsonl");
|
|
797
|
+
writeSessionJsonl(sessionFile, {
|
|
798
|
+
id: "sa-orphan-1", agent: "worker", mode: "background", task: "orphan done",
|
|
799
|
+
startedAt: 1000, rootSessionId: "sess-orphan",
|
|
800
|
+
});
|
|
801
|
+
const { store, appended } = makeRecoveryStore();
|
|
802
|
+
store.recoverOrphanRecords("sess-orphan");
|
|
803
|
+
|
|
804
|
+
const entry = appended.find((c) => c.data.id === "sa-orphan-1");
|
|
805
|
+
expect(entry?.customType).toBe("subagent-record");
|
|
806
|
+
expect(entry?.data.status).toBe("closed");
|
|
807
|
+
expect(entry?.data.closedReason).toBe("gc");
|
|
808
|
+
expect(entry?.data.endedAt).toEqual(expect.any(Number));
|
|
809
|
+
expect(entry?.data.error).toBeUndefined();
|
|
810
|
+
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
|
|
811
|
+
// 防重:sidecar 使下次重建走分支 2(closed),不再进判定
|
|
812
|
+
const again = [...appended];
|
|
813
|
+
store.recoverOrphanRecords("sess-orphan");
|
|
814
|
+
expect(appended.length).toBe(again.length);
|
|
815
|
+
const found = store.collectRecords(100, "all", "sess-orphan").find((r) => r.id === "sa-orphan-1");
|
|
816
|
+
expect(found?.status).toBe("closed");
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it("末行截断 → closed + error(保守方向)", () => {
|
|
820
|
+
const sessionFile = path.join(tmpDir, "orphan-truncated.jsonl");
|
|
821
|
+
writeSessionJsonl(sessionFile, {
|
|
822
|
+
id: "sa-orphan-2", agent: "worker", mode: "background", task: "orphan truncated",
|
|
823
|
+
startedAt: 2000, rootSessionId: "sess-orphan",
|
|
824
|
+
});
|
|
825
|
+
// 制造截断:append 半行 JSON(无换行结尾)
|
|
826
|
+
fs.appendFileSync(sessionFile, '{"type":"message","id":"msg-2","pare', "utf-8");
|
|
827
|
+
const { store, appended } = makeRecoveryStore();
|
|
828
|
+
store.recoverOrphanRecords("sess-orphan");
|
|
829
|
+
|
|
830
|
+
const entry = appended.find((c) => c.data.id === "sa-orphan-2");
|
|
831
|
+
expect(entry?.data.status).toBe("closed");
|
|
832
|
+
expect(entry?.data.error).toContain("truncated");
|
|
833
|
+
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it("末行 >64KB 完整 JSON → 不误判截断,判 done(V1 探针实测回归)", () => {
|
|
837
|
+
const sessionFile = path.join(tmpDir, "orphan-longline.jsonl");
|
|
838
|
+
writeSessionJsonl(sessionFile, {
|
|
839
|
+
id: "sa-orphan-5", agent: "worker", mode: "background", task: "orphan long line",
|
|
840
|
+
startedAt: 5000, rootSessionId: "sess-orphan",
|
|
841
|
+
});
|
|
842
|
+
// 真实库实测形态:末行为超长完整 entry(subagent-identity 的 task 内嵌大 payload,
|
|
843
|
+
// 28/4822 个文件末行 65KB-776KB)——固定 64KB 尾窗曾把这类行从中间切开误判截断。
|
|
844
|
+
const bigEntry = JSON.stringify({
|
|
845
|
+
type: "custom", id: "id-big", parentId: null,
|
|
846
|
+
timestamp: new Date(5000).toISOString(), customType: "subagent-identity",
|
|
847
|
+
data: { id: "sa-orphan-5", agent: "worker", mode: "background", task: "x".repeat(70 * 1024), startedAt: 5000 },
|
|
848
|
+
});
|
|
849
|
+
fs.appendFileSync(sessionFile, bigEntry + "\n", "utf-8");
|
|
850
|
+
const { store, appended } = makeRecoveryStore();
|
|
851
|
+
store.recoverOrphanRecords("sess-orphan");
|
|
852
|
+
|
|
853
|
+
const entry = appended.find((c) => c.data.id === "sa-orphan-5");
|
|
854
|
+
expect(entry?.data.status).toBe("closed");
|
|
855
|
+
expect(entry?.data.error).toBeUndefined();
|
|
856
|
+
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it("chatMode 孤儿 → 不终态化,落 resumable entry(可续聊产品语义)", () => {
|
|
860
|
+
const sessionFile = path.join(tmpDir, "orphan-chat.jsonl");
|
|
861
|
+
writeSessionJsonl(sessionFile, {
|
|
862
|
+
id: "sa-orphan-3", agent: "worker", mode: "background", task: "orphan chat",
|
|
863
|
+
startedAt: 3000, rootSessionId: "sess-orphan", chatMode: true,
|
|
864
|
+
});
|
|
865
|
+
const { store, appended } = makeRecoveryStore();
|
|
866
|
+
store.recoverOrphanRecords("sess-orphan");
|
|
867
|
+
|
|
868
|
+
const entry = appended.find((c) => c.data.id === "sa-orphan-3");
|
|
869
|
+
expect(entry?.data.status).toBe("running");
|
|
870
|
+
expect(entry?.data.resumable).toBe(true);
|
|
871
|
+
expect(entry?.data.chatMode).toBe(true);
|
|
872
|
+
// chat 分流不写终态 sidecar
|
|
873
|
+
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
|
|
874
|
+
// orphanJudged 缓存:同 store 重复调用不重复 append
|
|
875
|
+
const count = appended.length;
|
|
876
|
+
store.recoverOrphanRecords("sess-orphan");
|
|
877
|
+
expect(appended.length).toBe(count);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it("非分支 4 record(closed / 活进程)不进判定", () => {
|
|
881
|
+
const closedFile = path.join(tmpDir, "orphan-closed.jsonl");
|
|
882
|
+
writeSessionJsonl(closedFile, {
|
|
883
|
+
id: "sa-orphan-4", agent: "worker", mode: "background", task: "already closed",
|
|
884
|
+
startedAt: 4000, rootSessionId: "sess-orphan",
|
|
885
|
+
});
|
|
886
|
+
writeFinalized(closedFile);
|
|
887
|
+
const { store, appended } = makeRecoveryStore();
|
|
888
|
+
store.recoverOrphanRecords("sess-orphan");
|
|
889
|
+
expect(appended.find((c) => c.data.id === "sa-orphan-4")).toBeUndefined();
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
/** 写主 session fixture(含指定 subagent-record entry 序列)。 */
|
|
893
|
+
function writeMainSession(entries: Array<Record<string, unknown>>): string {
|
|
894
|
+
const mainFile = path.join(tmpDir, "main-session.jsonl");
|
|
895
|
+
const lines = entries.map((d) => JSON.stringify({ type: "custom", id: `e-${Math.random()}`, parentId: null, customType: "subagent-record", data: d }));
|
|
896
|
+
fs.writeFileSync(mainFile, lines.join("\n") + "\n", "utf-8");
|
|
897
|
+
return mainFile;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
it("entry-born 孤儿(无子文件,spawn 窗口期死亡)→ closed+gc+error(E2E 实测回归)", () => {
|
|
901
|
+
const mainFile = writeMainSession([
|
|
902
|
+
{ v: 1, id: "sa-entryonly-1", agent: "worker", task: "spawn interrupted", slug: "s", status: "running", mode: "background", startedAt: 6000, rootSessionId: "sess-orphan", depth: 0, turns: 0, totalTokens: 0, model: "m", eventLog: [], displayItems: [] },
|
|
903
|
+
]);
|
|
904
|
+
const { store, appended } = makeRecoveryStore();
|
|
905
|
+
store.recoverEntryOnlyOrphans(mainFile, "sess-orphan");
|
|
906
|
+
|
|
907
|
+
const entry = appended.find((c) => c.data.id === "sa-entryonly-1");
|
|
908
|
+
expect(entry?.data.status).toBe("closed");
|
|
909
|
+
expect(entry?.data.closedReason).toBe("gc");
|
|
910
|
+
expect(entry?.data.error).toContain("no child session file");
|
|
911
|
+
// 防重:orphanJudged 缓存拦截二次判定
|
|
912
|
+
const count = appended.length;
|
|
913
|
+
store.recoverEntryOnlyOrphans(mainFile, "sess-orphan");
|
|
914
|
+
expect(appended.length).toBe(count);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it("entry-born chatMode 孤儿 → 不终态化,落 resumable entry", () => {
|
|
918
|
+
const mainFile = writeMainSession([
|
|
919
|
+
{ v: 1, id: "sa-entryonly-2", agent: "worker", task: "chat spawn interrupted", slug: "s", status: "running", mode: "background", startedAt: 7000, rootSessionId: "sess-orphan", depth: 0, turns: 0, totalTokens: 0, model: "m", eventLog: [], displayItems: [], chatMode: true },
|
|
920
|
+
]);
|
|
921
|
+
const { store, appended } = makeRecoveryStore();
|
|
922
|
+
store.recoverEntryOnlyOrphans(mainFile, "sess-orphan");
|
|
923
|
+
|
|
924
|
+
const entry = appended.find((c) => c.data.id === "sa-entryonly-2");
|
|
925
|
+
expect(entry?.data.status).toBe("running");
|
|
926
|
+
expect(entry?.data.resumable).toBe(true);
|
|
927
|
+
expect(entry?.data.error).toBeUndefined();
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
it("有子文件锚 / 末条已终态 / 他 session 的 entry-born id 不进判定", () => {
|
|
931
|
+
const anchoredFile = path.join(tmpDir, "orphan-anchored.jsonl");
|
|
932
|
+
writeSessionJsonl(anchoredFile, {
|
|
933
|
+
id: "sa-anchored", agent: "worker", mode: "background", task: "has file",
|
|
934
|
+
startedAt: 8000, rootSessionId: "sess-orphan",
|
|
935
|
+
});
|
|
936
|
+
const mainFile = writeMainSession([
|
|
937
|
+
{ v: 1, id: "sa-anchored", agent: "worker", task: "has file", slug: "s", status: "running", mode: "background", startedAt: 8000, rootSessionId: "sess-orphan", depth: 0, turns: 0, totalTokens: 0, model: "m", eventLog: [], displayItems: [] },
|
|
938
|
+
{ v: 1, id: "sa-settled", agent: "worker", task: "settled", slug: "s", status: "closed", mode: "background", startedAt: 8100, rootSessionId: "sess-orphan", depth: 0, turns: 1, totalTokens: 0, model: "m", eventLog: [], displayItems: [] },
|
|
939
|
+
{ v: 1, id: "sa-foreign", agent: "worker", task: "other session", slug: "s", status: "running", mode: "background", startedAt: 8200, rootSessionId: "sess-other", depth: 0, turns: 0, totalTokens: 0, model: "m", eventLog: [], displayItems: [] },
|
|
940
|
+
]);
|
|
941
|
+
const { store, appended } = makeRecoveryStore();
|
|
942
|
+
store.recoverEntryOnlyOrphans(mainFile, "sess-orphan");
|
|
943
|
+
expect(appended.find((c) => c.data.id === "sa-anchored")).toBeUndefined();
|
|
944
|
+
expect(appended.find((c) => c.data.id === "sa-settled")).toBeUndefined();
|
|
945
|
+
expect(appended.find((c) => c.data.id === "sa-foreign")).toBeUndefined();
|
|
946
|
+
});
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
// ============================================================
|
|
950
|
+
// W16 [D4]:subagent-record 自描述 appendEntry 上报(状态迁移点)
|
|
951
|
+
// ============================================================
|
|
952
|
+
describe("W16 subagent-record 自描述 appendEntry 上报", () => {
|
|
953
|
+
/** appendEntry 捕获(RecordStorePi 最小实现)。 */
|
|
954
|
+
interface AppendedCall {
|
|
955
|
+
customType: string;
|
|
956
|
+
data: unknown;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** 构造带 appendEntry 捕获的 store(pi 注入通道,对齐生产 setPi 后形态)。 */
|
|
960
|
+
function makeStoreWithPi(): { store: RecordStore; appended: AppendedCall[] } {
|
|
961
|
+
const appended: AppendedCall[] = [];
|
|
962
|
+
const store = new RecordStore(tmpDir, undefined, {
|
|
963
|
+
appendEntry: (customType: string, data: unknown) => {
|
|
964
|
+
appended.push({ customType, data });
|
|
965
|
+
},
|
|
966
|
+
});
|
|
967
|
+
return { store, appended };
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/** unknown → 对象的运行时 guard(taste/no-unsafe-cast:断言前先收窄)。 */
|
|
971
|
+
function asEntryData(d: unknown): Record<string, unknown> {
|
|
972
|
+
if (typeof d !== "object" || d === null) throw new Error("entry data is not an object");
|
|
973
|
+
return d as Record<string, unknown>;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
it("register:append subagent-record entry,data = v1 + SubagentRecord 完整快照 schema", () => {
|
|
977
|
+
const { store, appended } = makeStoreWithPi();
|
|
978
|
+
store.register(makeRecord());
|
|
979
|
+
|
|
980
|
+
expect(appended).toHaveLength(1);
|
|
981
|
+
// 写点字面量与 record-entry.ts 常量等值(钉住双源一致性)
|
|
982
|
+
expect(appended[0]?.customType).toBe(SUBAGENT_RECORD_CUSTOM_TYPE);
|
|
983
|
+
// [E2E 实测教训] 必须先 JSON 序列化再断言:appendEntry 捕获的是内存对象,
|
|
984
|
+
// undefined 值的键名仍在(旧 27-key 断言因此漏检 recordToSubagent 丢
|
|
985
|
+
// chatMode 的 bug);真实 JSONL 会丢 undefined 值键,此处对齐生产行为。
|
|
986
|
+
const data = asEntryData(JSON.parse(JSON.stringify(appended[0]?.data)));
|
|
987
|
+
// register 时点序列化存活字段(undefined 值字段按生产序列化丢弃):
|
|
988
|
+
// chatMode 必须显式在场(one-shot=false)——renderer isDone 判据依赖它。
|
|
989
|
+
expect(Object.keys(data).sort()).toEqual([
|
|
990
|
+
"agent", "chatMode", "depth", "displayItems", "eventLog",
|
|
991
|
+
"id", "mode", "model", "rootSessionId", "round", "startedAt",
|
|
992
|
+
"status", "task", "totalTokens", "turns", "v", "worktree",
|
|
993
|
+
]);
|
|
994
|
+
expect(data).toMatchObject({
|
|
995
|
+
v: 1,
|
|
996
|
+
id: "r1",
|
|
997
|
+
agent: "worker",
|
|
998
|
+
task: "t",
|
|
999
|
+
status: "running",
|
|
1000
|
+
mode: "sync",
|
|
1001
|
+
startedAt: 1000,
|
|
1002
|
+
rootSessionId: "sess-current",
|
|
1003
|
+
chatMode: false,
|
|
1004
|
+
turns: 0,
|
|
1005
|
+
totalTokens: 0,
|
|
1006
|
+
model: "m",
|
|
1007
|
+
eventLog: [],
|
|
1008
|
+
displayItems: [],
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it("archive:append 终态完整快照(result/endedAt/closedReason);one-shot 生命周期共 2 次 append", () => {
|
|
1013
|
+
const { store, appended } = makeStoreWithPi();
|
|
1014
|
+
const r = makeRecord();
|
|
1015
|
+
store.register(r);
|
|
1016
|
+
// 模拟正常终态路径:tryTransition CAS → completeRecord 冻结 → archive(D-017 时序)
|
|
1017
|
+
tryTransition(r, "closed", "gc");
|
|
1018
|
+
completeRecord(r, { text: "task done", turns: 1, durationMs: 500, success: true, sessionId: "r1", toolCalls: [] }, "closed", "gc");
|
|
1019
|
+
store.archive(r);
|
|
1020
|
+
|
|
1021
|
+
// 探针基线(单测级):one-shot 生命周期 = register + archive = 2 次 append
|
|
1022
|
+
expect(appended).toHaveLength(2);
|
|
1023
|
+
const data = asEntryData(appended[1]?.data);
|
|
1024
|
+
expect(data).toMatchObject({
|
|
1025
|
+
v: 1,
|
|
1026
|
+
id: "r1",
|
|
1027
|
+
status: "closed",
|
|
1028
|
+
closedReason: "gc",
|
|
1029
|
+
result: "task done",
|
|
1030
|
+
endedAt: expect.any(Number) as number,
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
it("reportRecordTransition:类外恢复写点上报(chatMode 续轮 round 携带)", () => {
|
|
1035
|
+
const { store, appended } = makeStoreWithPi();
|
|
1036
|
+
const r = makeRecord({ chatMode: true, round: 1 });
|
|
1037
|
+
store.reportRecordTransition(r);
|
|
1038
|
+
|
|
1039
|
+
expect(appended).toHaveLength(1);
|
|
1040
|
+
expect(appended[0]?.customType).toBe(SUBAGENT_RECORD_CUSTOM_TYPE);
|
|
1041
|
+
expect(asEntryData(appended[0]?.data)).toMatchObject({
|
|
1042
|
+
v: 1,
|
|
1043
|
+
id: "r1",
|
|
1044
|
+
status: "running",
|
|
1045
|
+
round: 1,
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it("pi 未注入(session_start 前):三写点均安全降级不抛错", () => {
|
|
1050
|
+
const store = new RecordStore(tmpDir);
|
|
1051
|
+
const r = makeRecord();
|
|
1052
|
+
expect(() => store.register(r)).not.toThrow();
|
|
1053
|
+
expect(() => store.archive(r)).not.toThrow();
|
|
1054
|
+
expect(() => store.reportRecordTransition(r)).not.toThrow();
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
});
|