@zhushanwen/subagent-core 0.7.1 → 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-VURUAGMM.js → chunk-LZJOEDGQ.js} +603 -154
- 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 +325 -154
- 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 +9799 -7954
- package/dist/index.d.cts +1896 -1360
- package/dist/index.d.ts +1896 -1360
- package/dist/index.js +8952 -7513
- package/dist.bundle/index.cjs +7897 -6054
- package/package.json +5 -5
- package/src/__tests__/manifest-store.test.ts +16 -15
- package/src/__tests__/record-store-last-line.test.ts +12 -0
- package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
- package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
- package/src/core/logger.ts +1 -1
- 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__/inflight-production-wiring.test.ts +223 -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__/nested-visibility.test.ts +13 -1
- 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__/conformance/registry-fork-filter.test.ts +55 -53
- 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__/inflight-snapshot.test.ts +180 -0
- 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 +111 -26
- 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 +26 -41
- package/src/execution/engine/host/pi-host-binding.ts +5 -5
- package/src/execution/engine/host/spawned-children.ts +1 -1
- package/src/execution/engine/inflight-snapshot.ts +92 -0
- package/src/execution/engine/port.ts +52 -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/path-encoding.ts +6 -0
- 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 -2955
- 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 +12 -8
- package/src/index.ts +16 -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-ocpM8FmI.d.cts +0 -1608
- package/dist/engine-discovery-scan-ocpM8FmI.d.ts +0 -1608
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/subagent-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Cross-host shared subagent execution layer and workflow orchestration core (dual-form npm package: TS source for workspace, dist ESM+CJS for npm)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"ajv": "^8.20.0",
|
|
74
74
|
"proper-lockfile": "^4.1.2",
|
|
75
75
|
"yaml": "^2.9.0",
|
|
76
|
-
"@xyz-agent/extension-protocol": "0.
|
|
77
|
-
"@zhushanwen/subagent-engine-sdk": "0.
|
|
76
|
+
"@xyz-agent/extension-protocol": "0.9.0",
|
|
77
|
+
"@zhushanwen/subagent-engine-sdk": "0.3.0"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@types/node": "^24.0.0",
|
|
@@ -82,8 +82,8 @@
|
|
|
82
82
|
"tsup": "^8.5.1",
|
|
83
83
|
"typescript": "^5.7.0",
|
|
84
84
|
"vitest": "^4.1.8",
|
|
85
|
-
"@zhushanwen/pi-subagent-cli": "0.
|
|
86
|
-
"@zhushanwen/zcode-subagent-cli": "0.1
|
|
85
|
+
"@zhushanwen/pi-subagent-cli": "0.2.0",
|
|
86
|
+
"@zhushanwen/zcode-subagent-cli": "0.2.1"
|
|
87
87
|
},
|
|
88
88
|
"scripts": {
|
|
89
89
|
"build": "tsup",
|
|
@@ -114,7 +114,10 @@ describe("ManifestStore", () => {
|
|
|
114
114
|
});
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
// [U4c / G3] tmp 恢复退役(D6):manifest 已是可丢可重建缓存(权威 = `.state`,
|
|
118
|
+
// 重建 = RecordStore.rebuildIndexes),promote 半写 tmp 的恢复语义失效——[H4/U5
|
|
119
|
+
// 更名收口] recoverTmpFiles → sweepTmpFiles(名实对齐静默删除),返回删除计数。
|
|
120
|
+
describe("sweepTmpFiles", () => {
|
|
118
121
|
it("should delete tmp when manifest exists", async () => {
|
|
119
122
|
const id = "test-recovery-1";
|
|
120
123
|
const manifestPath = path.join(tmpDir, `${id}.json`);
|
|
@@ -123,20 +126,20 @@ describe("ManifestStore", () => {
|
|
|
123
126
|
fs.writeFileSync(manifestPath, '{"id":"test-recovery-1"}');
|
|
124
127
|
fs.writeFileSync(tmpPath, '{"id":"test-recovery-1"}');
|
|
125
128
|
|
|
126
|
-
const result = await store.
|
|
129
|
+
const result = await store.sweepTmpFiles();
|
|
127
130
|
|
|
128
|
-
expect(result
|
|
131
|
+
expect(result).toBe(1);
|
|
129
132
|
expect(fs.existsSync(tmpPath)).toBe(false);
|
|
130
133
|
expect(fs.existsSync(manifestPath)).toBe(true);
|
|
131
134
|
});
|
|
132
135
|
|
|
133
|
-
it("should
|
|
136
|
+
it("should delete valid tmp when manifest missing (promote retired)", async () => {
|
|
134
137
|
const id = "test-recovery-2";
|
|
135
138
|
const tmpPath = path.join(tmpDir, `${id}.json.tmp.12345`);
|
|
136
139
|
const manifestPath = path.join(tmpDir, `${id}.json`);
|
|
137
140
|
|
|
138
|
-
//
|
|
139
|
-
//
|
|
141
|
+
// 旧 promote 分支形态(合法完整 manifest 的 tmp + 正式文件缺失)也删——
|
|
142
|
+
// 半写 tmp 不再被复活成「看似权威」的索引,缺员由 rebuildIndexes 重建。
|
|
140
143
|
fs.writeFileSync(tmpPath, JSON.stringify({
|
|
141
144
|
id,
|
|
142
145
|
rootSessionId: "session-123",
|
|
@@ -145,10 +148,10 @@ describe("ManifestStore", () => {
|
|
|
145
148
|
createdAt: Date.now(),
|
|
146
149
|
}));
|
|
147
150
|
|
|
148
|
-
const result = await store.
|
|
151
|
+
const result = await store.sweepTmpFiles();
|
|
149
152
|
|
|
150
|
-
expect(result
|
|
151
|
-
expect(fs.existsSync(manifestPath)).toBe(
|
|
153
|
+
expect(result).toBe(1);
|
|
154
|
+
expect(fs.existsSync(manifestPath)).toBe(false);
|
|
152
155
|
expect(fs.existsSync(tmpPath)).toBe(false);
|
|
153
156
|
});
|
|
154
157
|
|
|
@@ -158,20 +161,18 @@ describe("ManifestStore", () => {
|
|
|
158
161
|
|
|
159
162
|
fs.writeFileSync(tmpPath, "invalid json {{{");
|
|
160
163
|
|
|
161
|
-
const result = await store.
|
|
164
|
+
const result = await store.sweepTmpFiles();
|
|
162
165
|
|
|
163
|
-
expect(result
|
|
166
|
+
expect(result).toBe(1);
|
|
164
167
|
expect(fs.existsSync(tmpPath)).toBe(false);
|
|
165
168
|
});
|
|
166
169
|
|
|
167
|
-
// F2:合法 JSON 但缺必填字段(非合法 manifest)应走删除(分支 3b),不能 promote
|
|
168
170
|
it("should delete tmp when JSON valid but not a valid manifest (missing required fields)", async () => {
|
|
169
171
|
const id = "test-recovery-4";
|
|
170
172
|
const tmpPath = path.join(tmpDir, `${id}.json.tmp.12345`);
|
|
171
173
|
fs.writeFileSync(tmpPath, JSON.stringify({ foo: "bar" })); // 合法 JSON,非 manifest
|
|
172
|
-
const result = await store.
|
|
173
|
-
expect(result
|
|
174
|
-
expect(result.recovered).toBe(0);
|
|
174
|
+
const result = await store.sweepTmpFiles();
|
|
175
|
+
expect(result).toBe(1);
|
|
175
176
|
expect(fs.existsSync(tmpPath)).toBe(false);
|
|
176
177
|
});
|
|
177
178
|
});
|
|
@@ -21,6 +21,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
21
21
|
import { ManifestStore } from "../execution/manifest-store";
|
|
22
22
|
import { RecordStore } from "../execution/record-store";
|
|
23
23
|
|
|
24
|
+
// [Gate A teardown 稳定性] no-op 掉索引落盘。根因:recoverOrphanRecords 走的
|
|
25
|
+
// reconstructAll(record-store.ts)扫描尾 fire-and-forget saveIndex(tmp+fsync+rename
|
|
26
|
+
// 异步 fs),满载下该 promise 可能在本文件 teardown 之后才 settle,其失败分支经
|
|
27
|
+
// logger.warn → console.warn 上报 vitest(rpc onUserConsoleLog 在途)——worker 关闭
|
|
28
|
+
// rpc 时在途调用被 reject 为 EnvironmentTeardownError(0 断言失败,纯 teardown 时序)。
|
|
29
|
+
// 本文件用一次性 tmpdir、断言只观察 appendEntry 捕获的判定结果,落盘与否无观察者——
|
|
30
|
+
// no-op 消除在途 IO 链,任何负载下确定;loadIndex 等其余导出保留原实现。
|
|
31
|
+
vi.mock("../execution/sessions-index.ts", async (importOriginal) => ({
|
|
32
|
+
...(await importOriginal<typeof import("../execution/sessions-index.ts")>()),
|
|
33
|
+
saveIndex: async () => {},
|
|
34
|
+
}));
|
|
35
|
+
|
|
24
36
|
interface CapturedEntry {
|
|
25
37
|
type: string;
|
|
26
38
|
data: Record<string, unknown>;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Medium batch 1 robustness fixes verification
|
|
2
2
|
//
|
|
3
|
-
// M4: dispatchAgentCall
|
|
3
|
+
// M4: [H2 W3 改写] dispatchAgentCall 的 node.live 清理已随 trace.live 字段退役
|
|
4
|
+
// (设计 subagent-workflow-record-unification.md D2——旁路 progress record 族
|
|
5
|
+
// 整体删除,节点无运行期附属对象)。原「live 清理先于 stale guard」的顺序
|
|
6
|
+
// 断言改为退役回归:pump 无 node.live 写点。
|
|
4
7
|
// M7: handleWorkerMessage validates msg shape before dereferencing msg.opts
|
|
5
8
|
// M8: session-reconstructor guards Array.isArray(msg.content) before for...of
|
|
6
9
|
|
|
@@ -17,24 +20,17 @@ function readSrc(relPath: string): string {
|
|
|
17
20
|
return readFileSync(join(PKG_ROOT, relPath), "utf-8");
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
// ── M4: node.live
|
|
23
|
+
// ── M4: node.live 写点已退役([H2 W3] 回归锁定) ─────────────
|
|
21
24
|
|
|
22
|
-
describe("M4: dispatchAgentCall
|
|
25
|
+
describe("M4: [H2 W3] dispatchAgentCall 不再写 node.live", () => {
|
|
23
26
|
const src = readSrc(join("src", "orchestration", "worker-message-pump.ts"));
|
|
24
27
|
|
|
25
|
-
it("
|
|
26
|
-
|
|
28
|
+
it("整个 pump 源码无 .live 写点(旁路 record 族退役),stale guard 仍在", () => {
|
|
29
|
+
expect(src).not.toMatch(/\.live\s*=/);
|
|
30
|
+
// .then 的 stale guard 仍在(原顺序断言的对象消失,守卫本身保留)
|
|
27
31
|
const thenMatch = src.match(/\.then\(\(\)\s*=>\s*\{[\s\S]*?\}\)/);
|
|
28
32
|
expect(thenMatch).toBeTruthy();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const liveIdx = thenBlock.indexOf("node.live = undefined");
|
|
32
|
-
const guardIdx = thenBlock.indexOf('run.state.status !== "running"');
|
|
33
|
-
|
|
34
|
-
expect(liveIdx).toBeGreaterThan(-1);
|
|
35
|
-
expect(guardIdx).toBeGreaterThan(-1);
|
|
36
|
-
// node.live 清理必须在 stale guard 之前(代码行号更小)
|
|
37
|
-
expect(liveIdx).toBeLessThan(guardIdx);
|
|
33
|
+
expect(thenMatch![0]).toContain('run.state.status !== "running"');
|
|
38
34
|
});
|
|
39
35
|
});
|
|
40
36
|
|
|
@@ -24,11 +24,16 @@ function readSrc(relPath: string): string {
|
|
|
24
24
|
// ── M6: worktree cleanup decoupled from patchOk ──────────────
|
|
25
25
|
|
|
26
26
|
describe("M6: worktree cleanup not gated by patchOk", () => {
|
|
27
|
-
|
|
27
|
+
// [H3/R3] worktree cleanup 调用点(disposeAllRecords / cancelBackground)已随终态
|
|
28
|
+
// 写面迁 execution/service/record-lifecycle.ts(strangler 抽取,壳只留单行转发)——
|
|
29
|
+
// 源文本守卫随被守卫本体改读聚合文件,断言强度不变(patchOk 解耦锁)。
|
|
30
|
+
const src = readSrc(join("src", "execution", "service", "record-lifecycle.ts"));
|
|
28
31
|
|
|
29
32
|
it("worktree cleanup condition does not reference patchOk", () => {
|
|
30
33
|
// 找到 worktree cleanup 调用前的条件判断
|
|
31
|
-
|
|
34
|
+
// [H3/R3] cleanup 通道字面随依赖注入改造为 deps.getWorktreeManager().cleanup
|
|
35
|
+
//(this.worktreeManager → this.deps.getWorktreeManager(),方法体其余逐字节保留)。
|
|
36
|
+
const cleanupMatch = src.match(/if\s*\([^)]*worktreeHandle[^)]*\)\s*\{[\s\S]*?getWorktreeManager\(\)\.cleanup/);
|
|
32
37
|
expect(cleanupMatch).toBeTruthy();
|
|
33
38
|
const condition = cleanupMatch![0];
|
|
34
39
|
// 条件中不应包含 patchOk(解耦后 worktree cleanup 只依赖 worktreeHandle 存在)
|
package/src/core/logger.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { getHostServices } from "./host-services.ts";
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/** 日志级别。级别集合对齐 pi-extension-logger(其 LogLevel 为包内部类型、含 info 四值;实例 API 仅 debug/warn/error 三方法,core facade 据此收窄为三值)。 */
|
|
17
17
|
export type LogLevel = "debug" | "warn" | "error";
|
|
18
18
|
|
|
19
19
|
/** core logger 接口。与 pi-extension-logger 的 ExtensionLogger 结构兼容——
|
|
@@ -4,9 +4,33 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
|
|
7
|
-
import { afterEach,describe, expect, it } from "vitest";
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
|
|
9
|
+
// [U1 A4] removeAliveMarker 失败留痕断言需要 warn 可观察。
|
|
10
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
11
|
+
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
14
|
+
|
|
15
|
+
// fs partial mock:unlinkSync 可注错(release 失败留痕用例),其余转发真实实现。
|
|
16
|
+
// ESM namespace 不可直接赋值/spyOn,注错必须走模块 mock(state-marker.test.ts 同款)。
|
|
17
|
+
type UnlinkSyncFn = typeof import("node:fs").unlinkSync;
|
|
18
|
+
const { unlinkSyncMock, actualUnlinkRef } = vi.hoisted(() => ({
|
|
19
|
+
unlinkSyncMock: vi.fn(),
|
|
20
|
+
actualUnlinkRef: { current: undefined as UnlinkSyncFn | undefined },
|
|
21
|
+
}));
|
|
22
|
+
vi.mock("node:fs", async (importOriginal) => {
|
|
23
|
+
const actual = await importOriginal<typeof import("node:fs")>();
|
|
24
|
+
actualUnlinkRef.current = actual.unlinkSync;
|
|
25
|
+
return {
|
|
26
|
+
...actual,
|
|
27
|
+
unlinkSync: unlinkSyncMock,
|
|
28
|
+
default: { ...actual, unlinkSync: unlinkSyncMock },
|
|
29
|
+
};
|
|
30
|
+
});
|
|
8
31
|
|
|
9
32
|
import {
|
|
33
|
+
findForeignLiveInstance,
|
|
10
34
|
isProcessAlive,
|
|
11
35
|
readAliveMarker,
|
|
12
36
|
removeAliveMarker,
|
|
@@ -18,6 +42,13 @@ function makeTmpDir(): string {
|
|
|
18
42
|
return fs.mkdtempSync(path.join(os.tmpdir(), "alive-store-test-"));
|
|
19
43
|
}
|
|
20
44
|
|
|
45
|
+
/**
|
|
46
|
+
* [U1 A4] 「异进程且存活」的确定性模拟 pid:1 号进程(launchd/init)必然存在且非
|
|
47
|
+
* 本测试进程——kill(1, 0) 对普通用户返回 EPERM,isProcessAlive 按「存在但无权限」
|
|
48
|
+
* 保守判活。self-pid 排除后不能再以本进程 pid 模拟异进程实例。
|
|
49
|
+
*/
|
|
50
|
+
const FOREIGN_LIVE_PID = 1;
|
|
51
|
+
|
|
21
52
|
describe("alive-store", () => {
|
|
22
53
|
let tmpDir: string;
|
|
23
54
|
|
|
@@ -25,6 +56,11 @@ describe("alive-store", () => {
|
|
|
25
56
|
if (tmpDir) {
|
|
26
57
|
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
27
58
|
}
|
|
59
|
+
// unlinkSync mock 基线复位 = 真实实现(个别用例注错覆盖后恢复)。
|
|
60
|
+
if (actualUnlinkRef.current !== undefined) {
|
|
61
|
+
unlinkSyncMock.mockReset().mockImplementation(actualUnlinkRef.current);
|
|
62
|
+
}
|
|
63
|
+
loggerMock.warn.mockClear();
|
|
28
64
|
});
|
|
29
65
|
|
|
30
66
|
// ── writeAliveMarker + readAliveMarker 往返 ──
|
|
@@ -144,4 +180,84 @@ describe("alive-store", () => {
|
|
|
144
180
|
process.kill = originalKill;
|
|
145
181
|
}
|
|
146
182
|
});
|
|
183
|
+
|
|
184
|
+
// ── [U1 A4] findForeignLiveInstance:self-pid 排除 + pid 单判据三态 ──
|
|
185
|
+
|
|
186
|
+
describe("findForeignLiveInstance(D3c/D3b v7:self-pid 排除 + pid 单判据)", () => {
|
|
187
|
+
it("本进程 pid(marker.pid === process.pid)→ 放行(视同无 foreign,D3c 排除)", () => {
|
|
188
|
+
tmpDir = makeTmpDir();
|
|
189
|
+
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
190
|
+
writeAliveMarker(sessionFile, { pid: process.pid, id: "bg-1", startedAt: Date.now() });
|
|
191
|
+
|
|
192
|
+
expect(findForeignLiveInstance(sessionFile)).toBeUndefined();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("异进程 pid 活 → 拦截(返回 marker,在持声明)", () => {
|
|
196
|
+
tmpDir = makeTmpDir();
|
|
197
|
+
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
198
|
+
const marker: AliveMarker = { pid: FOREIGN_LIVE_PID, id: "bg-1", startedAt: Date.now() };
|
|
199
|
+
writeAliveMarker(sessionFile, marker);
|
|
200
|
+
|
|
201
|
+
expect(findForeignLiveInstance(sessionFile)).toEqual(marker);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("pid 死 → 放行(确死,允许透明重生)", () => {
|
|
205
|
+
tmpDir = makeTmpDir();
|
|
206
|
+
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
207
|
+
// 大 pid 用户空间必然不存在(isProcessAlive ESRCH 判死,与既有用例同款手法)
|
|
208
|
+
writeAliveMarker(sessionFile, { pid: 9999999, id: "bg-1", startedAt: Date.now() });
|
|
209
|
+
|
|
210
|
+
expect(findForeignLiveInstance(sessionFile)).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("软超时不再参与判定:异进程 pid 活 + startedAt 超 1h 仍拦截(防御不随 idle 时长衰减)", () => {
|
|
214
|
+
tmpDir = makeTmpDir();
|
|
215
|
+
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
216
|
+
const stale: AliveMarker = {
|
|
217
|
+
pid: FOREIGN_LIVE_PID,
|
|
218
|
+
id: "bg-1",
|
|
219
|
+
startedAt: Date.now() - 2 * 3_600_000, // 2h 前——旧三判据形态下会超软超时放行
|
|
220
|
+
};
|
|
221
|
+
writeAliveMarker(sessionFile, stale);
|
|
222
|
+
|
|
223
|
+
expect(findForeignLiveInstance(sessionFile)).toEqual(stale);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("marker 缺失/损坏 → 放行(无声明)", () => {
|
|
227
|
+
tmpDir = makeTmpDir();
|
|
228
|
+
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
229
|
+
expect(findForeignLiveInstance(sessionFile)).toBeUndefined();
|
|
230
|
+
fs.writeFileSync(`${sessionFile}.alive`, "not-json", "utf-8");
|
|
231
|
+
expect(findForeignLiveInstance(sessionFile)).toBeUndefined();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ── [U1] removeAliveMarker 失败留痕(D3a:release 失败 bestEffort 留痕)──
|
|
236
|
+
|
|
237
|
+
describe("removeAliveMarker 失败留痕", () => {
|
|
238
|
+
it("删除失败(非 ENOENT)→ warn 留痕不抛出", () => {
|
|
239
|
+
tmpDir = makeTmpDir();
|
|
240
|
+
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
241
|
+
writeAliveMarker(sessionFile, { pid: 1, id: "x", startedAt: 0 });
|
|
242
|
+
unlinkSyncMock.mockImplementation(() => {
|
|
243
|
+
const err = new Error("EACCES") as NodeJS.ErrnoException;
|
|
244
|
+
err.code = "EACCES";
|
|
245
|
+
throw err;
|
|
246
|
+
});
|
|
247
|
+
expect(() => removeAliveMarker(sessionFile)).not.toThrow();
|
|
248
|
+
expect(loggerMock.warn).toHaveBeenCalledTimes(1);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("ENOENT(marker 不存在)→ 静默(幂等终态,无 warn)", () => {
|
|
252
|
+
tmpDir = makeTmpDir();
|
|
253
|
+
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
254
|
+
unlinkSyncMock.mockImplementation(() => {
|
|
255
|
+
const err = new Error("ENOENT") as NodeJS.ErrnoException;
|
|
256
|
+
err.code = "ENOENT";
|
|
257
|
+
throw err;
|
|
258
|
+
});
|
|
259
|
+
expect(() => removeAliveMarker(sessionFile)).not.toThrow();
|
|
260
|
+
expect(loggerMock.warn).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
147
263
|
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// src/execution/__tests__/batch-finalized.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [U3 / D1] 批写归口后的 barrier 语义测试(设计 docs/design/subagent-record-
|
|
4
|
+
// persistence-consolidation.md §3.1 markBatchFinalized 行 + §3.3 D4②/D5):
|
|
5
|
+
//
|
|
6
|
+
// 1. E1 调用面(SyncCollectDomain 直构 + 真 RecordStore):notifyBatch(写账入口)
|
|
7
|
+
// 被调用的时刻,全部成员 manifest 必已在磁盘——「manifest 落盘完成先于批通知
|
|
8
|
+
// 写账」的构造性保证(违反即红;迁移前 fire-and-forget 形态该时点只大概率成立);
|
|
9
|
+
// 2. 原语(manifestDir 接线,构造点同步分支):markBatchFinalized await 返回即
|
|
10
|
+
// manifest 文件可见(无 fire-and-forget)+ 落标 entry 携带 collectMode/batchFinalized
|
|
11
|
+
// 显式覆写 + 落标写出的时点 manifest 已在(原语内部序);
|
|
12
|
+
// 3. 原语(降级分支,manifestDir 缺省——双轨期形态):allSettled 屏障后落标,
|
|
13
|
+
// barrier 语义与同步分支等价。
|
|
14
|
+
//
|
|
15
|
+
// 与 record-store-intent-api.test.ts(U1,store 单体语义)的分工:本文件断言
|
|
16
|
+
// **调用面时序**——批通知写账(notifyBatch)/ 落标 entry 写出的时刻 manifest 必已
|
|
17
|
+
// 就位,即 D1 验收条款的字面断言面。
|
|
18
|
+
//
|
|
19
|
+
// 通路保真:RecordStore / SyncCollectDomain / ManifestStore 走真实实现(tmpdir
|
|
20
|
+
// 自建自删,红线),仅 pi.appendEntry / notifyHost 为 mock(写账与落标的观察点)。
|
|
21
|
+
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as os from "node:os";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
|
|
26
|
+
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
27
|
+
|
|
28
|
+
import { ManifestStore } from "../manifest-store.ts";
|
|
29
|
+
import { getSubagentRecordsDir, getSubagentSessionDir } from "../path-encoding.ts";
|
|
30
|
+
import { RecordStore } from "../record-store.ts";
|
|
31
|
+
import { SyncCollectDomain } from "../service/sync-collect-domain.ts";
|
|
32
|
+
import type { SubagentRecord } from "../types.ts";
|
|
33
|
+
|
|
34
|
+
const ROOT_SESSION = "root-batch-finalized";
|
|
35
|
+
|
|
36
|
+
/** 种子成员 record(对齐 sync-collect-recovery.test.ts memberRecord 形态——
|
|
37
|
+
* rebuildEntryRecord 解析门槛字段齐备)。 */
|
|
38
|
+
function memberRecord(overrides: Partial<SubagentRecord> & { id: string }): SubagentRecord {
|
|
39
|
+
return {
|
|
40
|
+
agent: "/agents/worker.md",
|
|
41
|
+
task: "batch task",
|
|
42
|
+
slug: "batch",
|
|
43
|
+
status: "running",
|
|
44
|
+
mode: "background",
|
|
45
|
+
startedAt: 1000,
|
|
46
|
+
rootSessionId: ROOT_SESSION,
|
|
47
|
+
parentRecordId: undefined,
|
|
48
|
+
depth: 0,
|
|
49
|
+
endedAt: undefined,
|
|
50
|
+
turns: 0,
|
|
51
|
+
totalTokens: 0,
|
|
52
|
+
model: "prov/m1",
|
|
53
|
+
thinkingLevel: undefined,
|
|
54
|
+
eventLog: [],
|
|
55
|
+
displayItems: [],
|
|
56
|
+
result: undefined,
|
|
57
|
+
error: undefined,
|
|
58
|
+
sessionFile: undefined,
|
|
59
|
+
chatMode: false,
|
|
60
|
+
collectMode: "sync",
|
|
61
|
+
...overrides,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("batch-finalized 归口(U3/D1)——barrier:manifest 落盘完成先于批通知写账", () => {
|
|
66
|
+
let tmpDir: string;
|
|
67
|
+
let sessionsDir: string;
|
|
68
|
+
let manifestDir: string;
|
|
69
|
+
let mainFile: string;
|
|
70
|
+
let appendEntryMock: Mock<(customType: string, data: unknown) => void>;
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "core-batch-finalized-"));
|
|
74
|
+
sessionsDir = getSubagentSessionDir(tmpDir, tmpDir);
|
|
75
|
+
manifestDir = getSubagentRecordsDir(tmpDir, tmpDir);
|
|
76
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
77
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
78
|
+
mainFile = path.join(tmpDir, "main-session.jsonl");
|
|
79
|
+
appendEntryMock = vi.fn();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** 写文件 pi(种子用):appendEntry 真写主 session JSONL(pi 落盘形态)。 */
|
|
87
|
+
function makeWritingPi() {
|
|
88
|
+
return {
|
|
89
|
+
appendEntry: (customType: string, data: unknown) => {
|
|
90
|
+
fs.appendFileSync(
|
|
91
|
+
mainFile,
|
|
92
|
+
`${JSON.stringify({
|
|
93
|
+
type: "custom",
|
|
94
|
+
id: `seed-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
95
|
+
parentId: null,
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
customType,
|
|
98
|
+
data,
|
|
99
|
+
})}\n`,
|
|
100
|
+
"utf-8",
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
it("E1 调用面:notifyBatch 写账时点全成员 manifest 已落盘(构造性 barrier,违反即红)", async () => {
|
|
107
|
+
// 种子:两成员(register running+sync → 终态 closed)经真实 reportSubagentRecord
|
|
108
|
+
// 序列化落主文件——崩溃残留(全员终态 + 无标记)形态。
|
|
109
|
+
const seedStore = new RecordStore(sessionsDir, undefined, makeWritingPi());
|
|
110
|
+
for (const id of ["sa-bf-a", "sa-bf-b"]) {
|
|
111
|
+
seedStore.reportSubagentRecord(memberRecord({ id }));
|
|
112
|
+
seedStore.reportSubagentRecord(
|
|
113
|
+
memberRecord({ id, status: "closed", closedReason: "gc", endedAt: 2000, result: `done-${id}` }),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 被测面:真 RecordStore(manifestDir 接线——构造点形态)+ 直构 SyncCollectDomain。
|
|
118
|
+
const store = new RecordStore(sessionsDir, undefined, { appendEntry: appendEntryMock }, manifestDir);
|
|
119
|
+
const notifyBatch = vi.fn(() => true);
|
|
120
|
+
const domain = new SyncCollectDomain({
|
|
121
|
+
getStore: () => store,
|
|
122
|
+
getNotifyHost: () => ({
|
|
123
|
+
toNotifyRecord: () => undefined,
|
|
124
|
+
notify: () => {},
|
|
125
|
+
notifyBatch,
|
|
126
|
+
}),
|
|
127
|
+
getPi: () => null,
|
|
128
|
+
getSessionRootId: () => ROOT_SESSION,
|
|
129
|
+
getMainSessionFile: () => mainFile,
|
|
130
|
+
getCollectSyncSection: () => undefined,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// 写账时点钩子(严格时序门):notifyBatch 被调用的时刻,两成员 manifest 必已
|
|
134
|
+
// 在磁盘——这是 D1 barrier 的字面断言面(「通知可达 ⇒ 索引就位」)。
|
|
135
|
+
const manifestsAtLedgerWrite: boolean[] = [];
|
|
136
|
+
notifyBatch.mockImplementation(() => {
|
|
137
|
+
manifestsAtLedgerWrite.push(
|
|
138
|
+
fs.existsSync(path.join(manifestDir, "sa-bf-a.json")),
|
|
139
|
+
fs.existsSync(path.join(manifestDir, "sa-bf-b.json")),
|
|
140
|
+
);
|
|
141
|
+
return true;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await domain.recoverSyncCollectBatch();
|
|
145
|
+
|
|
146
|
+
expect(notifyBatch).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(manifestsAtLedgerWrite).toEqual([true, true]);
|
|
148
|
+
// 落标随 markBatchFinalized 前置完成(U3 归口):两成员各一笔带标记 entry。
|
|
149
|
+
const marks = appendEntryMock.mock.calls
|
|
150
|
+
.filter((c) => c[0] === "subagent-record")
|
|
151
|
+
.map((c) => c[1] as Record<string, unknown>)
|
|
152
|
+
.filter((d) => d["batchFinalized"] === true);
|
|
153
|
+
expect(marks.map((m) => m["id"]).sort()).toEqual(["sa-bf-a", "sa-bf-b"]);
|
|
154
|
+
for (const mark of marks) expect(mark["collectMode"]).toBe("sync");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("原语(manifestDir 接线 / 同步分支):await 返回即 manifest 可见;落标 entry 写出时点 manifest 已在", async () => {
|
|
158
|
+
const store = new RecordStore(sessionsDir, undefined, { appendEntry: appendEntryMock }, manifestDir);
|
|
159
|
+
const members = [memberRecord({ id: "sa-sync-1" }), memberRecord({ id: "sa-sync-2" })];
|
|
160
|
+
|
|
161
|
+
// 落标时点钩子:appendEntry(落标写面)触发的时刻,该成员 manifest 必已存在
|
|
162
|
+
// (原语内部序 = manifest 落盘完成先于批通知写账/落标 entry)。
|
|
163
|
+
appendEntryMock.mockImplementation((_type: string, data: unknown) => {
|
|
164
|
+
const d = data as { id?: string };
|
|
165
|
+
if (typeof d?.id === "string") {
|
|
166
|
+
expect(fs.existsSync(path.join(manifestDir, `${d.id}.json`)), `manifest of ${d.id} at entry time`).toBe(true);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await store.markBatchFinalized(members);
|
|
171
|
+
|
|
172
|
+
// 写后即刻可见(无 fire-and-forget——D8 writeSync 语义)。
|
|
173
|
+
expect(fs.existsSync(path.join(manifestDir, "sa-sync-1.json"))).toBe(true);
|
|
174
|
+
expect(fs.existsSync(path.join(manifestDir, "sa-sync-2.json"))).toBe(true);
|
|
175
|
+
// 落标 entry:collectMode/batchFinalized 显式覆写(防非 entry 源重建丢标记)。
|
|
176
|
+
expect(appendEntryMock).toHaveBeenCalledTimes(2);
|
|
177
|
+
expect(appendEntryMock).toHaveBeenCalledWith(
|
|
178
|
+
"subagent-record",
|
|
179
|
+
expect.objectContaining({ id: "sa-sync-1", collectMode: "sync", batchFinalized: true }),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("原语(降级分支 / manifestDir 缺省——双轨期形态):allSettled 屏障后落标,barrier 语义等价", async () => {
|
|
184
|
+
// 缺省 manifestDir:store 降级走 manifestStore.writeManifest 异步屏障(双轨期
|
|
185
|
+
// 现行语义,D7)——await 完成后 manifest 照常落盘 + 落标在其后(顺序不变)。
|
|
186
|
+
const store = new RecordStore(
|
|
187
|
+
sessionsDir,
|
|
188
|
+
new ManifestStore(manifestDir),
|
|
189
|
+
{ appendEntry: appendEntryMock },
|
|
190
|
+
);
|
|
191
|
+
appendEntryMock.mockImplementation((_type: string, data: unknown) => {
|
|
192
|
+
const d = data as { id?: string };
|
|
193
|
+
if (typeof d?.id === "string") {
|
|
194
|
+
expect(fs.existsSync(path.join(manifestDir, `${d.id}.json`)), `manifest of ${d.id} at entry time`).toBe(true);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await store.markBatchFinalized([memberRecord({ id: "sa-degraded" })]);
|
|
199
|
+
|
|
200
|
+
expect(fs.existsSync(path.join(manifestDir, "sa-degraded.json"))).toBe(true);
|
|
201
|
+
expect(appendEntryMock).toHaveBeenCalledWith(
|
|
202
|
+
"subagent-record",
|
|
203
|
+
expect.objectContaining({ id: "sa-degraded", collectMode: "sync", batchFinalized: true }),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
});
|