@zhushanwen/subagent-core 0.8.0 → 0.9.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/dist/{chunk-OL5BK4VN.js → chunk-LZJOEDGQ.js} +455 -203
- package/dist/engine-discovery-scan-Da4Dkzsi.d.cts +818 -0
- package/dist/engine-discovery-scan-Da4Dkzsi.d.ts +818 -0
- package/dist/execution/engine/engine-discovery-scan.cjs +216 -155
- package/dist/execution/engine/engine-discovery-scan.d.cts +1 -2
- package/dist/execution/engine/engine-discovery-scan.d.ts +1 -2
- package/dist/execution/engine/engine-discovery-scan.js +1 -1
- package/dist/execution/relay-env.js +5 -4
- package/dist/index.cjs +7686 -5894
- package/dist/index.d.cts +1876 -1360
- package/dist/index.d.ts +1876 -1360
- package/dist/index.js +7048 -5493
- package/dist.bundle/index.cjs +7747 -5957
- package/package.json +4 -4
- package/src/__tests__/manifest-store.test.ts +16 -15
- package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
- package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
- package/src/execution/__tests__/alive-store.test.ts +117 -1
- package/src/execution/__tests__/batch-finalized.test.ts +206 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
- package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
- package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
- package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
- package/src/execution/__tests__/delivery-methods.test.ts +90 -176
- package/src/execution/__tests__/dialog-queue.test.ts +70 -3
- package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
- package/src/execution/__tests__/execute-nesting.test.ts +8 -5
- package/src/execution/__tests__/execution-record.test.ts +8 -148
- package/src/execution/__tests__/finalize-record.test.ts +295 -129
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
- package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
- package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
- package/src/execution/__tests__/idle-gc.test.ts +60 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
- package/src/execution/__tests__/model-config-service.test.ts +111 -0
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
- package/src/execution/__tests__/notify-ledger.test.ts +42 -1
- package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
- package/src/execution/__tests__/record-binding.test.ts +613 -0
- package/src/execution/__tests__/record-origin.test.ts +318 -0
- package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
- package/src/execution/__tests__/record-store.test.ts +72 -54
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
- package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
- package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
- package/src/execution/__tests__/run-orchestration.test.ts +199 -0
- package/src/execution/__tests__/session-file-gc.test.ts +35 -4
- package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
- package/src/execution/__tests__/state-marker.test.ts +288 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
- package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
- package/src/execution/__tests__/subagent-service.test.ts +17 -215
- package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
- package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
- package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
- package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
- package/src/execution/alive-store.ts +37 -28
- package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
- package/src/execution/config.ts +1 -1
- package/src/execution/conversation-continuation.ts +541 -0
- package/src/execution/dialog-queue.ts +60 -12
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
- package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
- package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
- package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
- package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
- package/src/execution/engine/__tests__/registry.test.ts +0 -1
- package/src/execution/engine/__tests__/routing.test.ts +0 -1
- package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
- package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
- package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
- package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
- package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
- package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
- package/src/execution/engine/client/engine-client.ts +81 -25
- package/src/execution/engine/client/mirror.ts +9 -0
- package/src/execution/engine/client/remote-engine.ts +52 -34
- package/src/execution/engine/client/reverse-router.ts +6 -27
- package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
- package/src/execution/engine/common/capability-gate.ts +27 -0
- package/src/execution/engine/common/data-dir.ts +3 -2
- package/src/execution/engine/common/errors.ts +1 -1
- package/src/execution/engine/common/journal-wiring.ts +18 -9
- package/src/execution/engine/common/run-signals.ts +83 -0
- package/src/execution/engine/d8-compat.ts +1 -1
- package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
- package/src/execution/engine/host/host-bridge.ts +19 -41
- package/src/execution/engine/host/pi-host-binding.ts +5 -5
- package/src/execution/engine/port.ts +34 -54
- package/src/execution/engine/types.ts +11 -14
- package/src/execution/execution-record.ts +15 -47
- package/src/execution/finalize-record.ts +94 -197
- package/src/execution/idle-gc.ts +9 -3
- package/src/execution/lifecycle-manager.ts +25 -361
- package/src/execution/lifecycle-predicates.ts +22 -19
- package/src/execution/manifest-store.ts +40 -42
- package/src/execution/model-config-service.ts +1 -1
- package/src/execution/notifier.ts +39 -1
- package/src/execution/notify-host.ts +13 -4
- package/src/execution/record-entry.ts +20 -2
- package/src/execution/record-store.ts +925 -160
- package/src/execution/relay-env.ts +8 -3
- package/src/execution/round-supervisor/domain.ts +17 -5
- package/src/execution/round-supervisor/service-binding.test.ts +89 -53
- package/src/execution/round-supervisor/service-binding.ts +71 -21
- package/src/execution/round-supervisor/supervisor.ts +23 -9
- package/src/execution/service/record-access.ts +547 -0
- package/src/execution/service/record-lifecycle.ts +542 -0
- package/src/execution/service/run-orchestration.ts +1647 -0
- package/src/execution/service/service-bootstrap.ts +111 -0
- package/src/execution/service/service-constants.ts +27 -0
- package/src/execution/service/session-baselines.ts +401 -0
- package/src/execution/service/sync-collect-domain.ts +442 -0
- package/src/execution/service/workflow-dispatch.ts +567 -0
- package/src/execution/session-file-gc.ts +11 -8
- package/src/execution/session-reconstructor.ts +44 -1
- package/src/execution/sessions-index.ts +20 -3
- package/src/execution/settled-watchdog.ts +86 -43
- package/src/execution/state-marker.ts +516 -0
- package/src/execution/stream-sink.ts +1 -1
- package/src/execution/subagent-actions-core.ts +53 -31
- package/src/execution/subagent-service.ts +643 -2980
- package/src/execution/subprocess-agent-runner.ts +46 -323
- package/src/execution/types.ts +60 -43
- package/src/execution/ui-request-handler-factory.ts +5 -0
- package/src/execution/worktree-manager.ts +1 -1
- package/src/execution/worktree-registry.ts +1 -1
- package/src/index.ts +6 -5
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
- package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
- package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
- package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
- package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
- package/src/orchestration/execute-agent-call.ts +1 -1
- package/src/orchestration/file-run-store.ts +1 -1
- package/src/orchestration/models/ports.ts +20 -13
- package/src/orchestration/models/types.ts +11 -13
- package/src/orchestration/run-snapshot.ts +18 -22
- package/src/orchestration/worker-message-pump.ts +33 -57
- package/src/shared/atomic-write.ts +10 -8
- package/dist/chunk-T2SYBW3I.js +0 -24
- package/dist/engine-discovery-scan-BXLA8y-z.d.cts +0 -1624
- package/dist/engine-discovery-scan-BXLA8y-z.d.ts +0 -1624
- package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
- package/src/execution/__tests__/finalized-marker.test.ts +0 -104
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
- package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
- package/src/execution/__tests__/tombstone-store.test.ts +0 -73
- package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
- package/src/execution/finalized-marker.ts +0 -70
- package/src/execution/tombstone-store.ts +0 -72
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
// doFinalizeRecord — manifest status
|
|
1
|
+
// doFinalizeRecord — manifest status 透传 + [U2a] 终态四件套归口 store 意图原语测试。
|
|
2
2
|
//
|
|
3
3
|
// 验证 finalize-record.ts 的 status 映射:done→completed, failed→failed, cancelled→cancelled
|
|
4
4
|
// (cancelled 不再归并 failed)。crashed 不进 finalize 入参(TS 签名锁定 done/failed/cancelled)。
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// [U2a/B1] 终态持久化四件套(.state/binding/archive/manifest/.alive 删)归口
|
|
7
|
+
// store.markFinalized / markCancelled——测试面相应切换为真实 RecordStore 承载
|
|
8
|
+
// (manifestDir 接线第 4 构造参数 → 终态 manifest writeSync 同步落盘,D8 v7);
|
|
9
|
+
// manifest 写失败路径走缺省降级(manifestStore 异步 + mock 注入 throw)。
|
|
10
|
+
// [B5] doFinalizeRoundToIdle 簿记归口 store.markRoundIdle:record 须先 register 进
|
|
11
|
+
// store 内存(markRoundIdle 按 id 查内存),轮终 `.alive` 保留(D3a 跨轮延续)。
|
|
12
|
+
//
|
|
13
|
+
// FinalizeDeps 其余 stub 注入(manifestStore 为真实实例,指向 tmpDir)。
|
|
9
14
|
|
|
10
15
|
import * as fs from "node:fs";
|
|
11
16
|
import * as os from "node:os";
|
|
@@ -24,8 +29,23 @@ vi.mock("../../core/logger.ts", () => ({
|
|
|
24
29
|
getLogger: () => loggerMock,
|
|
25
30
|
}));
|
|
26
31
|
|
|
32
|
+
// [B1] state-marker 写面 mock(默认透传真实实现):终态写失败(重试耗尽返回 false)
|
|
33
|
+
// 用例经 vi.mocked(writeFinalizedState).mockReturnValueOnce(false) 注入——绕开真实
|
|
34
|
+
// 重试 3 次 ×100ms 指数退避的墙钟等待。
|
|
35
|
+
vi.mock("../state-marker.ts", async (importOriginal) => {
|
|
36
|
+
const actual = await importOriginal<typeof import("../state-marker.ts")>();
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
writeFinalizedState: vi.fn(actual.writeFinalizedState),
|
|
40
|
+
writeCancelledState: vi.fn(actual.writeCancelledState),
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
import { writeFinalizedState } from "../state-marker.ts";
|
|
45
|
+
|
|
27
46
|
import { doFinalizeRecord, doFinalizeRoundToIdle } from "../finalize-record.ts";
|
|
28
47
|
import { ManifestStore } from "../manifest-store.ts";
|
|
48
|
+
import { RecordStore } from "../record-store.ts";
|
|
29
49
|
import { getSubagentSessionDir } from "../path-encoding.ts";
|
|
30
50
|
import type { AgentResult, ExecutionRecord, WorktreeHandle } from "../types.ts";
|
|
31
51
|
|
|
@@ -80,14 +100,24 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
80
100
|
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
81
101
|
});
|
|
82
102
|
|
|
83
|
-
/** 构造最小 FinalizeDeps
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
/** 构造最小 FinalizeDeps。[U2a/B1] store = 真实 RecordStore(manifestDir 第 4 参数
|
|
104
|
+
* 接线 → markFinalized/markCancelled 的终态 manifest writeSync 同步落盘);pi 经
|
|
105
|
+
* 构造注入(reportRecordTransition/写失败 entry 上报可断言)。
|
|
106
|
+
* @param opts.syncManifest false = 不接 manifestDir(manifest 走 manifestStore 异步
|
|
107
|
+
* 降级——manifest 写失败用例 mock manifestStore.writeManifest 注入 throw 用)。 */
|
|
108
|
+
function makeDeps(opts: { syncManifest?: boolean } = {}): Parameters<typeof doFinalizeRecord>[0] {
|
|
109
|
+
const pi = { appendEntry: vi.fn() };
|
|
110
|
+
const store = new RecordStore(
|
|
111
|
+
tmpDir,
|
|
86
112
|
manifestStore,
|
|
113
|
+
pi,
|
|
114
|
+
opts.syncManifest === false ? undefined : tmpDir,
|
|
115
|
+
);
|
|
116
|
+
return {
|
|
87
117
|
worktreeManager: {},
|
|
88
|
-
store
|
|
118
|
+
store,
|
|
89
119
|
modelService: {},
|
|
90
|
-
pi
|
|
120
|
+
pi,
|
|
91
121
|
emitUnregister: vi.fn(),
|
|
92
122
|
} as unknown as Parameters<typeof doFinalizeRecord>[0];
|
|
93
123
|
}
|
|
@@ -118,9 +148,27 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
118
148
|
expect(manifest?.status).toBe("closed");
|
|
119
149
|
});
|
|
120
150
|
|
|
121
|
-
it("
|
|
122
|
-
//
|
|
123
|
-
//
|
|
151
|
+
it("[M2 Gate B] closedReason 随 manifest 持久化(manifest 源快照三分流唯一依据)", async () => {
|
|
152
|
+
// 曾不携带 closedReason:sessionFile 未回填 / sidecar 缺席形态下,重启后 manifest
|
|
153
|
+
// 源重建的快照丢死因,endedMessageGuard 把 user-close/cancelled 误分流进
|
|
154
|
+
// 「reconnectable/fork-from」分支。
|
|
155
|
+
for (const closedReason of ["user-close", "cancelled", "parent-shutdown", "gc"] as const) {
|
|
156
|
+
const id = `rec-reason-${closedReason}`;
|
|
157
|
+
await doFinalizeRecord(makeDeps(), makeMinimalRecord({ id }), makeMinimalResult(), "closed", closedReason);
|
|
158
|
+
const manifest = await manifestStore.readManifest(id);
|
|
159
|
+
expect(manifest?.status).toBe("closed");
|
|
160
|
+
expect(manifest?.closedReason).toBe(closedReason);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("manifest write 抛错时 cleanup-first 顺序仍执行(终态原语内 .state 先于 manifest)", async () => {
|
|
165
|
+
// record 带 sessionFile 让终态原语走真实 .state/.alive 路径;
|
|
166
|
+
// 不设 worktreeHandle → Step 0 (collectPatch) 和 worktree cleanup 都跳过。
|
|
167
|
+
// [U2a] manifest 写失败经 store 内部 fire-and-forget catch(record-store
|
|
168
|
+
// reportManifestWriteFailure:logger.error + subagent:manifest-write-failed entry),
|
|
169
|
+
// 不 throw 不跳过编排层后续步骤——对齐 [Critical #1](manifest 是反查索引非正确性
|
|
170
|
+
// 依赖)。走异步降级(syncManifest:false——同步 writeAtomicFileSync 路径不经
|
|
171
|
+
// manifestStore.writeManifest,mock 无法注入)。
|
|
124
172
|
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
125
173
|
const record = makeMinimalRecord({ id: "rec-cleanup-first", sessionFile });
|
|
126
174
|
|
|
@@ -133,33 +181,35 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
133
181
|
);
|
|
134
182
|
|
|
135
183
|
// mock writeManifest 抛错(模拟 disk full)。在 mock 内捕获「writeManifest 被调用时
|
|
136
|
-
// finalized marker
|
|
137
|
-
//
|
|
184
|
+
// finalized marker 是否已存在」——这是终态原语内部写序的关键断言:D8 v7 要求
|
|
185
|
+
// `.state` writeSync 先、manifest 后(若有人反转写序,本标志会是 false)。
|
|
138
186
|
const finalizedBeforeManifestWrite = { value: false };
|
|
139
187
|
vi.spyOn(manifestStore, "writeManifest").mockImplementation(async () => {
|
|
140
|
-
finalizedBeforeManifestWrite.value = fs.existsSync(`${sessionFile}.
|
|
188
|
+
finalizedBeforeManifestWrite.value = fs.existsSync(`${sessionFile}.state`);
|
|
141
189
|
throw new Error("disk full");
|
|
142
190
|
});
|
|
143
191
|
loggerMock.error.mockClear();
|
|
144
192
|
|
|
145
|
-
const deps = makeDeps();
|
|
193
|
+
const deps = makeDeps({ syncManifest: false });
|
|
146
194
|
|
|
147
|
-
// ── 核心 claim 1:不抛错(
|
|
195
|
+
// ── 核心 claim 1:不抛错(manifest 写失败在 store 内部被吞,不 throw)──
|
|
148
196
|
await expect(
|
|
149
197
|
doFinalizeRecord(deps, record, makeMinimalResult(), "closed"),
|
|
150
198
|
).resolves.toBeUndefined();
|
|
151
199
|
|
|
152
|
-
// ── 核心 claim 2
|
|
153
|
-
expect(fs.existsSync(`${sessionFile}.
|
|
200
|
+
// ── 核心 claim 2:终态权威 .state 真实写入 ──
|
|
201
|
+
expect(fs.existsSync(`${sessionFile}.state`)).toBe(true);
|
|
154
202
|
|
|
155
|
-
// ── 核心 claim 3
|
|
203
|
+
// ── 核心 claim 3:.alive 被移除(release 出口①——预写的 .alive 不再存在)──
|
|
156
204
|
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
|
|
157
205
|
|
|
158
206
|
// ── 核心 claim 4:pending-notifications 注销仍触发(emitUnregister)──
|
|
159
207
|
expect(deps.emitUnregister).toHaveBeenCalledWith("rec-cleanup-first", "closed");
|
|
160
208
|
|
|
161
|
-
// ── 核心 claim 5:manifest 写失败被 logger.error
|
|
162
|
-
|
|
209
|
+
// ── 核心 claim 5:manifest 写失败被 logger.error 记录(store 内 reportManifestWriteFailure)──
|
|
210
|
+
await vi.waitFor(() => {
|
|
211
|
+
expect(loggerMock.error).toHaveBeenCalledWith(expect.stringContaining("manifest write failed"));
|
|
212
|
+
});
|
|
163
213
|
const errMsg = loggerMock.error.mock.calls[0]?.[0];
|
|
164
214
|
expect(errMsg).toContain("rec-cleanup-first");
|
|
165
215
|
expect(errMsg).toContain("disk full");
|
|
@@ -173,12 +223,57 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
173
223
|
// ── 核心 claim 7:manifest 实际未写入(writeManifest 抛错被吞咽)──
|
|
174
224
|
expect(await manifestStore.readManifest("rec-cleanup-first")).toBeNull();
|
|
175
225
|
|
|
176
|
-
// ── 核心 claim 8:[
|
|
177
|
-
// 若有人把 manifest
|
|
178
|
-
//
|
|
226
|
+
// ── 核心 claim 8:[D8 v7] 终态原语内部写序 —— .state 先于 manifest ──
|
|
227
|
+
// 若有人把 manifest 写前移到 .state 之前,mock 捕获时刻 .state 尚未写入,
|
|
228
|
+
// 本标志会是 false(D8 写序不变量:终态权威优先落)。
|
|
179
229
|
expect(finalizedBeforeManifestWrite.value).toBe(true);
|
|
180
230
|
|
|
181
231
|
// 清理 mock 调用记录防污染
|
|
232
|
+
vi.restoreAllMocks();
|
|
233
|
+
loggerMock.error.mockClear();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("[B1/§3.4] markFinalized 返回 false(.state 重试耗尽)→ 响亮 entry 上报 + 零持久化副作用 + cleanup 编排不跳过", async () => {
|
|
237
|
+
// 终态写失败(磁盘满/权限)语义:record 留 running(磁盘无终态位、不 archive、
|
|
238
|
+
// 不删 .alive),GUI 通知面腿 = subagent:state-write-failed entry(日志 error 腿
|
|
239
|
+
// 由 state-marker/store 层完成,此处断言编排层接线);worktree cleanup 等
|
|
240
|
+
// 副作用继续(幂等,不可被写失败跳过——否则 worktree 泄漏)。
|
|
241
|
+
vi.mocked(writeFinalizedState).mockReturnValueOnce(false);
|
|
242
|
+
const sessionFile = path.join(tmpDir, "session-fail.jsonl");
|
|
243
|
+
fs.writeFileSync(sessionFile, "{}\n", "utf-8");
|
|
244
|
+
fs.writeFileSync(
|
|
245
|
+
`${sessionFile}.alive`,
|
|
246
|
+
`${JSON.stringify({ pid: process.pid, id: "rec-write-fail", startedAt: 1000 })}\n`,
|
|
247
|
+
"utf-8",
|
|
248
|
+
);
|
|
249
|
+
const record = makeMinimalRecord({ id: "rec-write-fail", sessionFile });
|
|
250
|
+
const deps = makeDeps();
|
|
251
|
+
const cleanup = vi.fn().mockResolvedValue(undefined);
|
|
252
|
+
(deps as { worktreeManager: unknown }).worktreeManager = { cleanup };
|
|
253
|
+
// 「record 留 running」口径(U1 偏差 5)= 磁盘面(无 .state)+ 不 archive:
|
|
254
|
+
// record 未 register 进内存(doFinalizeRecord 不 register),未 archive 以 spy 钉。
|
|
255
|
+
const archiveSpy = vi.spyOn(deps.store as RecordStore, "archive");
|
|
256
|
+
|
|
257
|
+
await expect(
|
|
258
|
+
doFinalizeRecord(deps, record, makeMinimalResult(), "closed", "gc"),
|
|
259
|
+
).resolves.toBeUndefined();
|
|
260
|
+
|
|
261
|
+
// 响亮 entry 上报(§3.4 GUI 通知面——U1 偏差 6 的接线点)
|
|
262
|
+
expect(deps.pi!.appendEntry).toHaveBeenCalledWith(
|
|
263
|
+
"subagent:state-write-failed",
|
|
264
|
+
expect.objectContaining({ id: "rec-write-fail", status: "closed", closedReason: "gc" }),
|
|
265
|
+
);
|
|
266
|
+
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
267
|
+
expect.stringContaining("terminal state write failed"),
|
|
268
|
+
);
|
|
269
|
+
// 零持久化副作用(D8 失败语义):无 .state / 无 manifest / .alive 未删 / 未 archive
|
|
270
|
+
expect(fs.existsSync(`${sessionFile}.state`)).toBe(false);
|
|
271
|
+
expect(await manifestStore.readManifest("rec-write-fail")).toBeNull();
|
|
272
|
+
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(true);
|
|
273
|
+
expect(archiveSpy).not.toHaveBeenCalled();
|
|
274
|
+
// 副作用编排继续:worktree cleanup / emitUnregister 不被写失败跳过
|
|
275
|
+
expect(cleanup).toHaveBeenCalledTimes(0); // 本用例 record 无 worktreeHandle
|
|
276
|
+
expect(deps.emitUnregister).toHaveBeenCalledWith("rec-write-fail", "closed");
|
|
182
277
|
loggerMock.error.mockClear();
|
|
183
278
|
});
|
|
184
279
|
|
|
@@ -213,7 +308,7 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
213
308
|
return sessionFile;
|
|
214
309
|
}
|
|
215
310
|
|
|
216
|
-
it("sessionFile 缺失 + 反查命中 → .
|
|
311
|
+
it("sessionFile 缺失 + 反查命中 → .state 落盘 + .alive 清理 + record/manifest 回填", async () => {
|
|
217
312
|
const sessionFile = writeSessionFileWithIdentity("20260901T12000000_ps9.jsonl", "rec-ps9");
|
|
218
313
|
// 预写 .alive 残留(模拟 running 期崩溃恢复窗口的 marker)
|
|
219
314
|
fs.writeFileSync(
|
|
@@ -228,9 +323,11 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
228
323
|
|
|
229
324
|
// 反查回填 record.sessionFile
|
|
230
325
|
expect(record.sessionFile).toBe(sessionFile);
|
|
231
|
-
// 终态原因持久化不再丢失(closedReason gc 写入 .
|
|
232
|
-
expect(fs.
|
|
233
|
-
|
|
326
|
+
// 终态原因持久化不再丢失(closedReason gc 写入 .state 的 reason 字段)
|
|
327
|
+
expect(JSON.parse(fs.readFileSync(`${sessionFile}.state`, "utf-8"))).toEqual({
|
|
328
|
+
status: "finalized",
|
|
329
|
+
reason: "gc",
|
|
330
|
+
});
|
|
234
331
|
// alive marker 不再残留
|
|
235
332
|
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
|
|
236
333
|
// manifest 拿到真实 sessionFile(诊断/重建源不再失真)
|
|
@@ -238,7 +335,7 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
238
335
|
expect(manifest?.sessionFile).toBe(sessionFile);
|
|
239
336
|
});
|
|
240
337
|
|
|
241
|
-
it("cancelled + 反查命中 →
|
|
338
|
+
it("cancelled + 反查命中 → cancelled 终态写反查路径(语义不因 sessionFile 缺失丢失)", async () => {
|
|
242
339
|
const sessionFile = writeSessionFileWithIdentity("20260901T12000001_ps9c.jsonl", "rec-ps9c");
|
|
243
340
|
const record = makeMinimalRecord({ id: "rec-ps9c", sessionFile: undefined });
|
|
244
341
|
const deps = { ...makeDeps(), sessionDir };
|
|
@@ -246,12 +343,12 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
246
343
|
await doFinalizeRecord(deps, record, makeMinimalResult(), "closed", "cancelled");
|
|
247
344
|
|
|
248
345
|
expect(record.sessionFile).toBe(sessionFile);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
expect(
|
|
254
|
-
expect(
|
|
346
|
+
const marker = JSON.parse(fs.readFileSync(`${sessionFile}.state`, "utf-8")) as {
|
|
347
|
+
status: string;
|
|
348
|
+
endedAt: number;
|
|
349
|
+
};
|
|
350
|
+
expect(marker.status).toBe("cancelled");
|
|
351
|
+
expect(typeof marker.endedAt).toBe("number");
|
|
255
352
|
});
|
|
256
353
|
|
|
257
354
|
it("反查未命中(目录无 identity 匹配文件)→ 不抛、sessionFile 保持缺失、无 marker(行为退回修复前)", async () => {
|
|
@@ -272,14 +369,14 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
272
369
|
});
|
|
273
370
|
});
|
|
274
371
|
|
|
275
|
-
// ── Step 3a 判别 wiring:closedReason →
|
|
372
|
+
// ── Step 3a 判别 wiring:closedReason → .state 单一终态 sidecar(L4 合并后)──
|
|
276
373
|
//
|
|
277
|
-
// record 带 sessionFile 直接命中
|
|
278
|
-
// cancelled →
|
|
279
|
-
// 其余 reason →
|
|
280
|
-
//
|
|
281
|
-
describe("
|
|
282
|
-
it("closedReason=cancelled →
|
|
374
|
+
// record 带 sessionFile 直接命中 writeTerminalState 的判别分支:
|
|
375
|
+
// cancelled → .state {status:"cancelled", endedAt}(重建还原 cancelled 语义 + 精确结束时间)
|
|
376
|
+
// 其余 reason → .state {status:"finalized", reason}(真实 reason 进 sidecar,重建还原 closedReason)
|
|
377
|
+
// 单文件单 status 字段 → 互斥构造性成立;兼容期旧名(.finalized/.cancelled)不再被写。
|
|
378
|
+
describe("writeTerminalState 判别 wiring(closedReason → .state status)", () => {
|
|
379
|
+
it("closedReason=cancelled → .state 携带 status/endedAt 且不写旧名 sidecar", async () => {
|
|
283
380
|
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
284
381
|
const record = makeMinimalRecord({
|
|
285
382
|
id: "rec-wire-cancelled",
|
|
@@ -290,42 +387,41 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
290
387
|
|
|
291
388
|
await doFinalizeRecord(makeDeps(), record, makeMinimalResult(), "closed", "cancelled");
|
|
292
389
|
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
expect(fs.existsSync(`${sessionFile}.
|
|
296
|
-
const
|
|
297
|
-
id: string;
|
|
390
|
+
// .state 写出且内容为终态 marker(字段逐项断言,不用 objectContaining 放宽——
|
|
391
|
+
// 重建链路靠这些字段还原)
|
|
392
|
+
expect(fs.existsSync(`${sessionFile}.state`)).toBe(true);
|
|
393
|
+
const marker = JSON.parse(fs.readFileSync(`${sessionFile}.state`, "utf-8")) as {
|
|
298
394
|
status: string;
|
|
299
|
-
agent: string;
|
|
300
|
-
startedAt: number;
|
|
301
395
|
endedAt: number;
|
|
302
396
|
};
|
|
303
|
-
expect(
|
|
304
|
-
expect(tomb.status).toBe("cancelled");
|
|
305
|
-
expect(tomb.agent).toBe("worker");
|
|
306
|
-
expect(tomb.startedAt).toBe(1000);
|
|
397
|
+
expect(marker.status).toBe("cancelled");
|
|
307
398
|
// endedAt 来自 completeRecord 冻结后的 record.endedAt(Step 1 先于 Step 3a,
|
|
308
|
-
// record 预设的 endedAt 会被冻结值覆盖)——
|
|
309
|
-
expect(
|
|
310
|
-
expect(typeof
|
|
311
|
-
//
|
|
399
|
+
// record 预设的 endedAt 会被冻结值覆盖)——sidecar 携带真实收尾时间戳
|
|
400
|
+
expect(marker.endedAt).toBe(record.endedAt);
|
|
401
|
+
expect(typeof marker.endedAt).toBe("number");
|
|
402
|
+
// 兼容期旧名不再被写(写侧单点收敛)
|
|
312
403
|
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
|
|
404
|
+
expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(false);
|
|
313
405
|
});
|
|
314
406
|
|
|
315
407
|
it.each(["user-close", "gc"] as const)(
|
|
316
|
-
"closedReason=%s → .
|
|
408
|
+
"closedReason=%s → .state 内容携带真实 reason 且不写旧名 sidecar",
|
|
317
409
|
async (reason) => {
|
|
318
410
|
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
319
411
|
const record = makeMinimalRecord({ id: `rec-wire-${reason}`, sessionFile });
|
|
320
412
|
|
|
321
413
|
await doFinalizeRecord(makeDeps(), record, makeMinimalResult(), "closed", reason);
|
|
322
414
|
|
|
323
|
-
//
|
|
415
|
+
// .state 写出且 reason = 真实关因(磁盘重建用它还原 closedReason,
|
|
324
416
|
// 不再一律硬编码 gc)
|
|
325
|
-
expect(fs.existsSync(`${sessionFile}.
|
|
326
|
-
expect(fs.readFileSync(`${sessionFile}.
|
|
327
|
-
|
|
417
|
+
expect(fs.existsSync(`${sessionFile}.state`)).toBe(true);
|
|
418
|
+
expect(JSON.parse(fs.readFileSync(`${sessionFile}.state`, "utf-8"))).toEqual({
|
|
419
|
+
status: "finalized",
|
|
420
|
+
reason,
|
|
421
|
+
});
|
|
422
|
+
// 兼容期旧名不再被写
|
|
328
423
|
expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(false);
|
|
424
|
+
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
|
|
329
425
|
},
|
|
330
426
|
);
|
|
331
427
|
});
|
|
@@ -419,7 +515,7 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
419
515
|
).resolves.toBeUndefined();
|
|
420
516
|
|
|
421
517
|
// Step 0 失败不影响后续步骤:finalized sidecar 与 manifest 照常落地
|
|
422
|
-
expect(fs.existsSync(`${sessionFile}.
|
|
518
|
+
expect(fs.existsSync(`${sessionFile}.state`)).toBe(true);
|
|
423
519
|
const manifest = await manifestStore.readManifest("rec-patch-err");
|
|
424
520
|
expect(manifest?.status).toBe("closed");
|
|
425
521
|
expect(deps.worktreeManager.cleanup).toHaveBeenCalledTimes(1);
|
|
@@ -446,165 +542,237 @@ describe("doFinalizeRoundToIdle — chatMode 轮次完成进 idle (M2-A)", () =>
|
|
|
446
542
|
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
447
543
|
});
|
|
448
544
|
|
|
449
|
-
/** 构造 FinalizeDeps
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
545
|
+
/** 构造 FinalizeDeps + 真实 RecordStore(markRoundIdle 按 id 查内存——record 须先
|
|
546
|
+
* register;返回 store 供 register/断言用)。worktreeManager.cleanup / store.archive
|
|
547
|
+
* 为 spy 以断言「不调」。 */
|
|
548
|
+
function makeDeps(): { deps: Parameters<typeof doFinalizeRoundToIdle>[0]; store: RecordStore } {
|
|
549
|
+
const pi = { appendEntry: vi.fn() };
|
|
550
|
+
const store = new RecordStore(tmpDir, manifestStore, pi, tmpDir);
|
|
551
|
+
const deps = {
|
|
453
552
|
worktreeManager: { cleanup: vi.fn(), collectPatch: vi.fn() },
|
|
454
|
-
store
|
|
553
|
+
store,
|
|
455
554
|
modelService: {},
|
|
456
|
-
pi
|
|
555
|
+
pi,
|
|
457
556
|
emitUnregister: vi.fn(),
|
|
458
557
|
} as unknown as Parameters<typeof doFinalizeRoundToIdle>[0];
|
|
558
|
+
return { deps, store };
|
|
459
559
|
}
|
|
460
560
|
|
|
461
|
-
it("record 带 sessionFile →
|
|
561
|
+
it("[B5/D3a] record 带 sessionFile → 轮终后 .alive marker 仍在(写权声明跨轮延续)+ record.status=running + round 0→1", async () => {
|
|
462
562
|
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
463
|
-
// 预写 .alive marker
|
|
563
|
+
// 预写 .alive marker([B5] 行为变化锚点:轮终**保留** marker——release = 终态原语
|
|
564
|
+
// 或 idle-GC 归档两出口,轮终 record 仍 resumable 随时续聊 spawn 写同一
|
|
565
|
+
// sessionFile,删则轮后跨进程防御空窗,D3a)
|
|
464
566
|
fs.writeFileSync(
|
|
465
567
|
`${sessionFile}.alive`,
|
|
466
568
|
`${JSON.stringify({ pid: 99999, id: "rec-idle", startedAt: 1000 })}\n`,
|
|
467
569
|
"utf-8",
|
|
468
570
|
);
|
|
571
|
+
const { deps, store } = makeDeps();
|
|
469
572
|
const record = makeMinimalRecord({ id: "rec-idle", sessionFile, chatMode: true, round: 0 });
|
|
470
573
|
// tryTransition 已把 status 设为 done,模拟 runAndFinalize 调用前的状态
|
|
471
574
|
record.status = "closed";
|
|
575
|
+
store.register(record);
|
|
472
576
|
|
|
473
|
-
await doFinalizeRoundToIdle(
|
|
577
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "done" });
|
|
474
578
|
|
|
475
|
-
// 状态机(v4 B-1:旧 idle 折入 running,
|
|
579
|
+
// 状态机(v4 B-1:旧 idle 折入 running,markRoundIdle 设 running)
|
|
476
580
|
expect(record.status).toBe("running");
|
|
477
581
|
expect(record.round).toBe(1);
|
|
478
|
-
// .alive marker
|
|
479
|
-
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(
|
|
582
|
+
// [B5] .alive marker 轮终保留(旧行为「删 marker」随 D3a 跨轮延续退役)
|
|
583
|
+
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(true);
|
|
480
584
|
});
|
|
481
585
|
|
|
482
|
-
it("
|
|
483
|
-
const deps = makeDeps();
|
|
586
|
+
it("[A3] 终态簿记已冻结(endedAt 已设 = completeRecord 已跑)→ store 内硬断言 throw(S7:禁复活已终态化 record)", async () => {
|
|
587
|
+
const { deps, store } = makeDeps();
|
|
588
|
+
const record = makeMinimalRecord({ id: "rec-frozen", chatMode: true });
|
|
589
|
+
// closeChatIdle / disposeAllRecords 等完整终态化路径的产物:completeRecord 已跑
|
|
590
|
+
//(endedAt 冻结)+ status=closed。迟到的轮末分流调用必须被断言拒绝。
|
|
591
|
+
record.status = "closed";
|
|
592
|
+
record.closedReason = "user-close";
|
|
593
|
+
record.endedAt = 12345;
|
|
594
|
+
store.register(record);
|
|
595
|
+
const transitionSpy = vi.spyOn(store, "reportRecordTransition");
|
|
596
|
+
|
|
597
|
+
await expect(doFinalizeRoundToIdle(deps, record, { kind: "success", content: "late" })).rejects.toThrow(
|
|
598
|
+
/terminal bookkeeping already frozen/,
|
|
599
|
+
);
|
|
600
|
+
// 断言先于任何簿记副作用:round 不推进、状态不回滚、无迁移上报
|
|
601
|
+
expect(record.round).toBeUndefined();
|
|
602
|
+
expect(record.status).toBe("closed");
|
|
603
|
+
expect(transitionSpy).not.toHaveBeenCalled();
|
|
604
|
+
expect(deps.emitUnregister).not.toHaveBeenCalled();
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("W16: 轮终上报 reportRecordTransition(markRoundIdle 簿记⑨,entry 携带新轮计数)", async () => {
|
|
608
|
+
const { deps, store } = makeDeps();
|
|
609
|
+
const transitionSpy = vi.spyOn(store, "reportRecordTransition");
|
|
484
610
|
const record = makeMinimalRecord({ id: "rec-report", chatMode: true, round: 2 });
|
|
485
611
|
record.status = "closed";
|
|
486
|
-
|
|
487
|
-
|
|
612
|
+
store.register(record);
|
|
613
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "done" });
|
|
614
|
+
expect(transitionSpy).toHaveBeenCalledTimes(1);
|
|
488
615
|
// 上报发生在 round 推进之后(entry 携带新轮计数,重建源不滞后)
|
|
489
616
|
expect(record.round).toBe(3);
|
|
490
617
|
expect(record.status).toBe("running");
|
|
491
618
|
});
|
|
492
619
|
|
|
493
620
|
it("record.round 已为 N → round 变 N+1", async () => {
|
|
621
|
+
const { deps, store } = makeDeps();
|
|
494
622
|
const record = makeMinimalRecord({ id: "rec-round", round: 3 });
|
|
495
623
|
record.status = "closed";
|
|
496
|
-
|
|
624
|
+
store.register(record);
|
|
625
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "done" });
|
|
497
626
|
expect(record.round).toBe(4);
|
|
498
627
|
expect(record.status).toBe("running");
|
|
499
628
|
});
|
|
500
629
|
|
|
501
630
|
it("不调 store.archive(record 留内存,getMutable 仍可查)", async () => {
|
|
502
|
-
const deps = makeDeps();
|
|
631
|
+
const { deps, store } = makeDeps();
|
|
632
|
+
const archiveSpy = vi.spyOn(store, "archive");
|
|
503
633
|
const record = makeMinimalRecord({ id: "rec-noarchive" });
|
|
504
634
|
record.status = "closed";
|
|
505
|
-
|
|
506
|
-
|
|
635
|
+
store.register(record);
|
|
636
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "done" });
|
|
637
|
+
expect(archiveSpy).not.toHaveBeenCalled();
|
|
638
|
+
expect(store.getMutable("rec-noarchive")).toBeDefined();
|
|
507
639
|
});
|
|
508
640
|
|
|
509
641
|
it("不 cleanup worktree(即使 record 带 worktreeHandle——保留对话模式工作目录)", async () => {
|
|
510
|
-
const deps = makeDeps();
|
|
642
|
+
const { deps, store } = makeDeps();
|
|
511
643
|
const record = makeMinimalRecord({
|
|
512
644
|
id: "rec-noworktree",
|
|
513
645
|
worktreeHandle: { path: "/tmp/x", branch: "b", baseCommit: "c", mainCwd: "/tmp" } as never,
|
|
514
646
|
});
|
|
515
647
|
record.status = "closed";
|
|
516
|
-
|
|
648
|
+
store.register(record);
|
|
649
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "done" });
|
|
517
650
|
expect(deps.worktreeManager.cleanup).not.toHaveBeenCalled();
|
|
518
651
|
});
|
|
519
652
|
|
|
520
|
-
it("
|
|
521
|
-
const deps = makeDeps();
|
|
653
|
+
it("轮终注销经 store 簿记⑧发射(setPendingUnregister 注入闭包,status=running)", async () => {
|
|
654
|
+
const { deps, store } = makeDeps();
|
|
655
|
+
// [B5/U5 收口] 注销由 store.markRoundIdle 簿记⑧统一发射(SubagentService 构造点
|
|
656
|
+
// 注入 emitPendingUnregister;调用方薄壳不再双轨重复发)。
|
|
657
|
+
const unregisterSpy = vi.fn();
|
|
658
|
+
store.setPendingUnregister(unregisterSpy);
|
|
522
659
|
const record = makeMinimalRecord({ id: "rec-emit" });
|
|
523
660
|
record.status = "closed";
|
|
524
|
-
|
|
525
|
-
|
|
661
|
+
store.register(record);
|
|
662
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "done" });
|
|
663
|
+
expect(unregisterSpy).toHaveBeenCalledWith("rec-emit", "running");
|
|
526
664
|
});
|
|
527
665
|
|
|
528
666
|
it("不调 completeRecord:record 不冻结(endedAt / agentResult 仍 undefined)", async () => {
|
|
667
|
+
const { deps, store } = makeDeps();
|
|
529
668
|
const record = makeMinimalRecord({ id: "rec-nofreeze" });
|
|
530
669
|
record.status = "closed";
|
|
531
|
-
|
|
670
|
+
store.register(record);
|
|
671
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "done" });
|
|
532
672
|
expect(record.endedAt).toBeUndefined();
|
|
533
673
|
expect(record.agentResult).toBeUndefined();
|
|
534
674
|
});
|
|
535
675
|
|
|
536
676
|
it("不写 manifest(idle 非终态,manifest 是终态诊断辅助)", async () => {
|
|
677
|
+
const { deps, store } = makeDeps();
|
|
537
678
|
const record = makeMinimalRecord({ id: "rec-nomanifest" });
|
|
538
679
|
record.status = "closed";
|
|
539
|
-
|
|
680
|
+
store.register(record);
|
|
681
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "done" });
|
|
540
682
|
const manifest = await manifestStore.readManifest("rec-nomanifest");
|
|
541
683
|
expect(manifest).toBeNull();
|
|
542
684
|
});
|
|
543
685
|
|
|
544
|
-
it("MF-2: record.result
|
|
686
|
+
it("MF-2: record.result 设为轮终 content(否则 notifier idle 回复恒为 (empty),G1/G2 不成立)", async () => {
|
|
687
|
+
const { deps, store } = makeDeps();
|
|
545
688
|
const record = makeMinimalRecord({ id: "rec-result" });
|
|
546
689
|
record.status = "closed";
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
await doFinalizeRoundToIdle(
|
|
550
|
-
// MF-2:record.result 被
|
|
690
|
+
store.register(record);
|
|
691
|
+
// [H1 U2 / D7] outcome 入参适配:成功轮正文 = content
|
|
692
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "review done, found 3 issues" });
|
|
693
|
+
// MF-2:record.result 被 content 填充,notifier idle 分支读取后携带回复正文
|
|
551
694
|
expect(record.result).toBe("review done, found 3 issues");
|
|
552
695
|
});
|
|
553
696
|
|
|
554
|
-
it("MF-2
|
|
697
|
+
it("MF-2 兑底:失败轮次(无前值)record.result 用失败摘要填充(D7 outcome 入参:前值 ?? 失败摘要)", async () => {
|
|
698
|
+
const { deps, store } = makeDeps();
|
|
555
699
|
const record = makeMinimalRecord({ id: "rec-result-err" });
|
|
556
700
|
record.status = "closed";
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
result.error = "spawn timeout";
|
|
561
|
-
await doFinalizeRoundToIdle(makeDeps(), record, result);
|
|
562
|
-
// 失败轮次的 notify 需可读:result.text 空 → 兑底用 error
|
|
701
|
+
store.register(record);
|
|
702
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "failed", reason: "spawn timeout" });
|
|
703
|
+
// 失败轮次的 notify 需可读:无前值可保 → 兜底失败摘要
|
|
563
704
|
expect(record.result).toBe("round did not complete: spawn timeout");
|
|
564
705
|
});
|
|
565
706
|
|
|
566
|
-
it("
|
|
707
|
+
it("D7 失败轮前值保留:有最后成功正文时 result 不被失败摘要覆盖(GUI record 视图不被失败污染)+ lastError 写失败原因", async () => {
|
|
708
|
+
const { deps, store } = makeDeps();
|
|
709
|
+
const record = makeMinimalRecord({ id: "rec-fail-keep-prev", chatMode: true });
|
|
710
|
+
record.status = "closed";
|
|
711
|
+
record.result = "last successful round output"; // 前值(有最后成功正文则保留)
|
|
712
|
+
store.register(record);
|
|
713
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "failed", reason: "watchdog kill" });
|
|
714
|
+
// 前值 ?? 失败摘要 → 前值保留;失败原因归 lastError 字段与失败通知(可达性迁移)
|
|
715
|
+
expect(record.result).toBe("last successful round output");
|
|
716
|
+
expect(record.lastError).toBe("watchdog kill");
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("D7 失败轮 lastError 写失败原因(排障面——renderer 在 result 非 undefined 时不显示 error,无视觉影响)", async () => {
|
|
720
|
+
const { deps, store } = makeDeps();
|
|
721
|
+
const record = makeMinimalRecord({ id: "rec-fail-last-error", chatMode: true });
|
|
722
|
+
record.status = "closed";
|
|
723
|
+
record.lastError = undefined;
|
|
724
|
+
store.register(record);
|
|
725
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "failed", reason: "engine_round_crashed: x" });
|
|
726
|
+
expect(record.lastError).toBe("engine_round_crashed: x");
|
|
727
|
+
expect(record.result).toBe("round did not complete: engine_round_crashed: x");
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it("D7 成功轮 lastError 不混入正文:空 content 成功轮兜底 '(no output this round)'(旧 lastError 混入形态退役)", async () => {
|
|
731
|
+
const { deps, store } = makeDeps();
|
|
732
|
+
const record = makeMinimalRecord({ id: "rec-success-no-mix", chatMode: true });
|
|
733
|
+
record.status = "closed";
|
|
734
|
+
record.lastError = "stale error from previous round";
|
|
735
|
+
store.register(record);
|
|
736
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "" });
|
|
737
|
+
expect(record.result).toBe("(no output this round)");
|
|
738
|
+
expect(record.result).not.toContain("stale error");
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it("T2-③/LC-1: 失败轮 record.result 写失败摘要、不回显轮内旧正文(D7:可达性从 result 字段迁移到通知 outcome——失败通知由 Continuation 独立承载失败原因 + 恢复指引)", async () => {
|
|
742
|
+
const { deps, store } = makeDeps();
|
|
567
743
|
const record = makeMinimalRecord({ id: "rec-result-err-text", chatMode: true });
|
|
568
744
|
record.status = "closed";
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
// watchdog 错误并存
|
|
572
|
-
result.text = "DONE";
|
|
573
|
-
result.success = false;
|
|
574
|
-
result.error =
|
|
745
|
+
store.register(record);
|
|
746
|
+
const failureReason =
|
|
575
747
|
"subagent did not reach agent_settled within 10 min (settled watchdog); the process was terminated to bound the wait. Recovery: check state with subagents action:'list', then re-send your message to continue.";
|
|
576
|
-
await doFinalizeRoundToIdle(
|
|
577
|
-
//
|
|
578
|
-
expect(record.result).toBe(`round did not complete: ${
|
|
748
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "failed", reason: failureReason });
|
|
749
|
+
// 失败轮 record.result 携带失败摘要(前值 undefined → 失败摘要),不构成成功谎报
|
|
750
|
+
expect(record.result).toBe(`round did not complete: ${failureReason}`);
|
|
579
751
|
expect(record.result).not.toBe("DONE");
|
|
580
752
|
});
|
|
581
753
|
|
|
582
754
|
it("C1TC10: chatMode 空增量轮占位——record.result 固定 (no output this round),不含上一轮文本(D5)", async () => {
|
|
755
|
+
const { deps, store } = makeDeps();
|
|
583
756
|
const record = makeMinimalRecord({ id: "rec-increment-empty", chatMode: true });
|
|
584
757
|
record.status = "closed";
|
|
758
|
+
store.register(record);
|
|
585
759
|
// 预置上一轮通知文本(模拟增量语义前的 record.result 残留)
|
|
586
760
|
record.result = "PREV-ROUND-TEXT";
|
|
587
|
-
|
|
588
|
-
result.text = "";
|
|
589
|
-
result.success = true;
|
|
590
|
-
result.error = undefined;
|
|
591
|
-
await doFinalizeRoundToIdle(makeDeps(), record, result);
|
|
761
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "" });
|
|
592
762
|
// 空增量轮通知不含上一轮文本:沿用旧值会让父 agent 误读为原样重复回复(D5 判定)
|
|
593
763
|
expect(record.result).toBe("(no output this round)");
|
|
594
764
|
expect(record.result).not.toContain("PREV-ROUND-TEXT");
|
|
595
765
|
});
|
|
596
766
|
|
|
597
767
|
it("C1TC11 [R2-1]: one-shot 首轮空文本成功完成 → record.result 补占位「(empty)」(非 undefined)", async () => {
|
|
768
|
+
const { deps, store } = makeDeps();
|
|
598
769
|
const record = makeMinimalRecord({ id: "rec-oneshot-empty", chatMode: false });
|
|
599
770
|
record.status = "closed";
|
|
771
|
+
store.register(record);
|
|
600
772
|
// one-shot 成功空文本完成路径(collectResult getFullText 返回 ""、success=true):
|
|
601
773
|
// result 前值 undefined
|
|
602
774
|
record.result = undefined;
|
|
603
|
-
|
|
604
|
-
result.text = "";
|
|
605
|
-
result.success = true;
|
|
606
|
-
result.error = undefined;
|
|
607
|
-
await doFinalizeRoundToIdle(makeDeps(), record, result);
|
|
775
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "" });
|
|
608
776
|
// [R2-1] 轮终信号优先:占位非 undefined 是 renderer hasRunning 排除轮终 running 的
|
|
609
777
|
// 判据——保持 undefined 会让完成注入后末位 turn 永久「工作中」
|
|
610
778
|
expect(record.result).toBe("(empty)");
|
|
@@ -614,15 +782,13 @@ describe("doFinalizeRoundToIdle — chatMode 轮次完成进 idle (M2-A)", () =>
|
|
|
614
782
|
});
|
|
615
783
|
|
|
616
784
|
it("C1TC11b [R2-1]: one-shot 续轮空文本 → 沿用前值(one-shot 无增量语义,不覆盖为占位)", async () => {
|
|
785
|
+
const { deps, store } = makeDeps();
|
|
617
786
|
const record = makeMinimalRecord({ id: "rec-oneshot-cont", chatMode: false });
|
|
618
787
|
record.status = "closed";
|
|
788
|
+
store.register(record);
|
|
619
789
|
// 上一轮真实产出(续轮 record.result 前值)
|
|
620
790
|
record.result = "first round output";
|
|
621
|
-
|
|
622
|
-
result.text = "";
|
|
623
|
-
result.success = true;
|
|
624
|
-
result.error = undefined;
|
|
625
|
-
await doFinalizeRoundToIdle(makeDeps(), record, result);
|
|
791
|
+
await doFinalizeRoundToIdle(deps, record, { kind: "success", content: "" });
|
|
626
792
|
// one-shot record.result = 该 subagent 最终输出:空文本续轮沿用前值,不被 "(empty)" 覆盖
|
|
627
793
|
expect(record.result).toBe("first round output");
|
|
628
794
|
});
|