@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
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
// src/execution/__tests__/dispose-manifest-recovery.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [M1/M2/M3 Gate B 真机验收回归] 编排性/取消终态化的 manifest 反查索引 + 重启冷查分流:
|
|
4
|
+
// - M1:disposeAllRecords(parent-shutdown 等)终态化补写 records/<id>.json——曾整体
|
|
5
|
+
// 缺席 → 重启后 list 不可见 + message「not found or not owned」(展示层∪动作链双失)。
|
|
6
|
+
// 含 sessionFile 锚点提升(engineHandle.sessionRef.sessionFile → record.sessionFile)。
|
|
7
|
+
// - M1 自愈段:SIGKILL 打进 shutdown 窗口吞掉 fire-and-forget manifest 写时,
|
|
8
|
+
// initSession 的可重连 entry(closed + closedReason ∈ RECONNECTABLE_FINAL_REASONS)
|
|
9
|
+
// 重物化 manifest。
|
|
10
|
+
// - [D8 v7] writeSync 接线后 fire-and-forget 停机窗构造性消灭(头两段描述为
|
|
11
|
+
// 历史形态);自愈段保留为防御纵深。
|
|
12
|
+
// - M1 负向:user-close/cancelled/gc 末条 entry 不被孤儿恢复/重物化(主动终态语义不放大)。
|
|
13
|
+
// - M2:manifest 源快照 closedReason 投影 → endedMessageGuard 三分流正确
|
|
14
|
+
// (user-close/cancelled → 主动关闭专属文案;parent-shutdown → reconnectable 文案)。
|
|
15
|
+
// 曾因 manifest 不携带 closedReason,user-close 误入「closedReason: unknown +
|
|
16
|
+
// reconnectable/fork-from」分支;cancel(无 sessionFile 形态)则完全不可见报原始 not-found。
|
|
17
|
+
//
|
|
18
|
+
// mock 形态:真实 SubagentService + 真实 ManifestStore(tmp agentDir),重启 =
|
|
19
|
+
// 同 agentDir 二次构造 Service + initSession。残余异步写(防御纵深分支/afterEach 拆
|
|
20
|
+
// 目录前)用 flushAsyncWrites 数轮 tick 等待。
|
|
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 } from "vitest";
|
|
27
|
+
|
|
28
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
29
|
+
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
32
|
+
|
|
33
|
+
import { createRecord } from "../execution-record.ts";
|
|
34
|
+
import { ModelConfigService } from "../model-config-service.ts";
|
|
35
|
+
import { toSubagentRecordEntry, SUBAGENT_RECORD_CUSTOM_TYPE } from "../record-entry.ts";
|
|
36
|
+
import { RecordStore } from "../record-store.ts";
|
|
37
|
+
import { getSubagentRecordsDir } from "../path-encoding.ts";
|
|
38
|
+
import { SubagentService, type PiLike } from "../subagent-service.ts";
|
|
39
|
+
import { endedMessageGuard } from "../subagent-actions-core.ts";
|
|
40
|
+
import { isBootReadoptable } from "../round-supervisor/domain.ts";
|
|
41
|
+
import type { ExecutionRecord, SubagentRecord } from "../types.ts";
|
|
42
|
+
import { isReconnectableFinalReason } from "../types.ts";
|
|
43
|
+
|
|
44
|
+
function makeTmpAgentDir(): string {
|
|
45
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "dispose-manifest-"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makePi(): PiLike {
|
|
49
|
+
return {
|
|
50
|
+
appendEntry: vi.fn(),
|
|
51
|
+
events: { emit: vi.fn() },
|
|
52
|
+
sendMessage: vi.fn(),
|
|
53
|
+
} as unknown as PiLike;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeRecord(overrides: Partial<ExecutionRecord> & { id?: string } = {}): ExecutionRecord {
|
|
57
|
+
const { id = "sa-m1", ...rest } = overrides;
|
|
58
|
+
const r = createRecord(id, {
|
|
59
|
+
agent: "general-purpose",
|
|
60
|
+
model: "test/model",
|
|
61
|
+
mode: "background",
|
|
62
|
+
task: "gate b task",
|
|
63
|
+
slug: "gate-b",
|
|
64
|
+
startedAt: 1000,
|
|
65
|
+
rootSessionId: "root-session",
|
|
66
|
+
chatMode: false,
|
|
67
|
+
controller: new AbortController(),
|
|
68
|
+
});
|
|
69
|
+
Object.assign(r, rest);
|
|
70
|
+
return r;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ServiceInternals {
|
|
74
|
+
store: RecordStore;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 同 agentDir 二次构造 + initSession = 重启冷查(新 RecordStore/fileCache/manifest 缓存)。 */
|
|
78
|
+
function makeServiceOn(agentDir: string, init?: { mainSessionFile?: string }): SubagentService {
|
|
79
|
+
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
80
|
+
const service = new SubagentService({ cwd: agentDir, modelService });
|
|
81
|
+
service.initSession({ pi: makePi(), sessionId: "root-session", mainSessionFile: init?.mainSessionFile });
|
|
82
|
+
return service;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 构造一条 closed 终态的 subagent-record entry 行(archive 写点的离线形态)。 */
|
|
86
|
+
function closedEntryLine(rec: Partial<SubagentRecord> & { id: string; closedReason: NonNullable<SubagentRecord["closedReason"]> }): string {
|
|
87
|
+
const full: SubagentRecord = {
|
|
88
|
+
id: rec.id,
|
|
89
|
+
agent: rec.agent ?? "general-purpose",
|
|
90
|
+
task: rec.task ?? "gate b task",
|
|
91
|
+
slug: rec.slug ?? "gate-b",
|
|
92
|
+
status: "closed",
|
|
93
|
+
closedReason: rec.closedReason,
|
|
94
|
+
mode: "background",
|
|
95
|
+
startedAt: 1000,
|
|
96
|
+
rootSessionId: "root-session",
|
|
97
|
+
parentRecordId: undefined,
|
|
98
|
+
depth: 0,
|
|
99
|
+
endedAt: 2000,
|
|
100
|
+
turns: 0,
|
|
101
|
+
totalTokens: 0,
|
|
102
|
+
model: "test/model",
|
|
103
|
+
thinkingLevel: undefined,
|
|
104
|
+
eventLog: [],
|
|
105
|
+
displayItems: [],
|
|
106
|
+
error: "closed due to parent-shutdown",
|
|
107
|
+
sessionFile: rec.sessionFile,
|
|
108
|
+
};
|
|
109
|
+
return JSON.stringify({ type: "custom", customType: SUBAGENT_RECORD_CUSTOM_TYPE, data: toSubagentRecordEntry(full) });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function manifestPath(agentDir: string, id: string): string {
|
|
113
|
+
return path.join(getSubagentRecordsDir(agentDir, agentDir), `${id}.json`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function flushAsyncWrites(): Promise<void> {
|
|
117
|
+
// fire-and-forget manifest 写 = fs.promises 微任务/宏任务链;数轮 tick 留足 flush 窗。
|
|
118
|
+
for (let i = 0; i < 5; i++) {
|
|
119
|
+
await new Promise((r) => { setTimeout(r, 5); });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe("[M1/M2 Gate B] 编排性终态化 manifest 反查索引 + 重启冷查分流", () => {
|
|
124
|
+
let agentDir: string;
|
|
125
|
+
let service: SubagentService;
|
|
126
|
+
const extraServices: SubagentService[] = [];
|
|
127
|
+
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
// 身份/目录贯穿 env 净化(本包 vitest.setup 不覆盖;字面量同 subagent-service 模块私有常量,
|
|
130
|
+
// 残留会让 recoverOrphansIfRootProcess 误判子进程跳过恢复、rootCwd 漂移出 tmp agentDir)。
|
|
131
|
+
delete process.env.PI_SUBAGENT_SELF_RECORD_ID;
|
|
132
|
+
delete process.env.PI_SUBAGENT_ROOT_CWD;
|
|
133
|
+
delete process.env.PI_SUBAGENT_ROOT_SESSION_ID;
|
|
134
|
+
agentDir = makeTmpAgentDir();
|
|
135
|
+
service = makeServiceOn(agentDir);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
afterEach(async () => {
|
|
139
|
+
await flushAsyncWrites(); // 先等 fire-and-forget 写落盘,再拆 tmp 目录(防 ENOENT 竞态噪声)
|
|
140
|
+
for (const s of extraServices.splice(0)) s.dispose();
|
|
141
|
+
service.dispose();
|
|
142
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("M1: disposeAllRecords(parent-shutdown) 补写 manifest(status closed + closedReason 随写)", async () => {
|
|
146
|
+
const record = makeRecord({ id: "sa-m1" });
|
|
147
|
+
(service as unknown as ServiceInternals).store.register(record);
|
|
148
|
+
|
|
149
|
+
const count = service.disposeAllRecords("parent-shutdown");
|
|
150
|
+
expect(count).toBe(1);
|
|
151
|
+
|
|
152
|
+
// [U4c / G4-S5②] 同步写锚点:dispose 终态完成点(同步写返回后)**立即**读
|
|
153
|
+
// manifest——存在且合法 JSON、无 0 字节/半写 tmp 残留(manifestDir writeSync
|
|
154
|
+
// 接线后 fire-and-forget 停机窗构造性消灭;本用例不加 waitFor,等待形态回归
|
|
155
|
+
// = D8 判据破坏)。
|
|
156
|
+
const manifestFile = manifestPath(agentDir, "sa-m1");
|
|
157
|
+
expect(fs.existsSync(manifestFile)).toBe(true);
|
|
158
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf-8")) as Record<string, unknown>;
|
|
159
|
+
expect(manifest.status).toBe("closed");
|
|
160
|
+
expect(manifest.closedReason).toBe("parent-shutdown");
|
|
161
|
+
expect(manifest.rootSessionId).toBe("root-session");
|
|
162
|
+
expect(manifest.agentName).toBe("general-purpose");
|
|
163
|
+
expect(manifest.createdAt).toBe(1000);
|
|
164
|
+
// [U4c / G2] 词汇双写过渡字段随终态写面落盘(session-reader 前向兼容锚)
|
|
165
|
+
expect(manifest.executionStatus).toBe("closed");
|
|
166
|
+
// 原子写无 tmp 残留(0 字节/半写 tmp 只在崩溃打断 writeAtomicFile 时出现)
|
|
167
|
+
const residue = fs.readdirSync(path.dirname(manifestFile)).filter((f) => f.includes(".tmp."));
|
|
168
|
+
expect(residue).toEqual([]);
|
|
169
|
+
await flushAsyncWrites(); // 拆 tmp 目录前等尽潜在异步写(防御既有 afterEach 契约)
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("M1: 在途 record 的 engineHandle.sessionRef.sessionFile 提升为终态锚点(entry+manifest 带真实路径)", async () => {
|
|
173
|
+
const promoted = path.join(agentDir, "enc", "20260910T000000_sess-e1.jsonl");
|
|
174
|
+
// [U2a/B3] dispose 终态化归口 markFinalized 后 .state/binding 随锚点真实落盘——
|
|
175
|
+
// 父目录须真实存在(旧路径不写 .state,promoted 仅作 manifest 字符串投影)。
|
|
176
|
+
fs.mkdirSync(path.dirname(promoted), { recursive: true });
|
|
177
|
+
fs.writeFileSync(promoted, "{}\n", "utf-8");
|
|
178
|
+
const record = makeRecord({
|
|
179
|
+
id: "sa-promote",
|
|
180
|
+
engineHandle: { sessionRef: { sessionId: "sess-e1", sessionFile: promoted }, poolKey: "shared" },
|
|
181
|
+
});
|
|
182
|
+
(service as unknown as ServiceInternals).store.register(record);
|
|
183
|
+
|
|
184
|
+
// 预写 .alive(断言 dispose 的 release 出口①——D8 终态原语内部删)
|
|
185
|
+
fs.writeFileSync(
|
|
186
|
+
`${promoted}.alive`,
|
|
187
|
+
`${JSON.stringify({ pid: 99999, id: "sa-promote", startedAt: 1000 })}\n`,
|
|
188
|
+
"utf-8",
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
service.disposeAllRecords("parent-shutdown");
|
|
192
|
+
|
|
193
|
+
// record 本体提升(archive entry 投影随之携带)
|
|
194
|
+
expect(record.sessionFile).toBe(promoted);
|
|
195
|
+
// [B3/D8] .state 权威写随归口生效(reason = 真实 parent-shutdown,非重建兜底 gc)
|
|
196
|
+
expect(JSON.parse(fs.readFileSync(`${promoted}.state`, "utf-8"))).toEqual({
|
|
197
|
+
status: "finalized",
|
|
198
|
+
reason: "parent-shutdown",
|
|
199
|
+
});
|
|
200
|
+
// .alive release(终态原语内部删——旧 dispose 不删 marker 的形态随归口消灭)
|
|
201
|
+
expect(fs.existsSync(`${promoted}.alive`)).toBe(false);
|
|
202
|
+
// manifest 落真实锚点(重启 revive/fork-from 可直接定位子文件)
|
|
203
|
+
await vi.waitFor(() => expect(fs.existsSync(manifestPath(agentDir, "sa-promote"))).toBe(true));
|
|
204
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath(agentDir, "sa-promote"), "utf-8")) as Record<string, unknown>;
|
|
205
|
+
expect(manifest.sessionFile).toBe(promoted);
|
|
206
|
+
expect(manifest.closedReason).toBe("parent-shutdown");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("M1+M2: 重启冷查 manifest 源可见,endedMessageGuard 走 parent-shutdown 可重连分流", async () => {
|
|
210
|
+
const record = makeRecord({ id: "sa-m1" });
|
|
211
|
+
(service as unknown as ServiceInternals).store.register(record);
|
|
212
|
+
service.disposeAllRecords("parent-shutdown");
|
|
213
|
+
await vi.waitFor(() => expect(fs.existsSync(manifestPath(agentDir, "sa-m1"))).toBe(true));
|
|
214
|
+
service.dispose();
|
|
215
|
+
|
|
216
|
+
// 重启:新 Service 实例(内存空、fileCache 空、manifest 缓存空)+ initSession
|
|
217
|
+
const restarted = makeServiceOn(agentDir);
|
|
218
|
+
extraServices.push(restarted);
|
|
219
|
+
|
|
220
|
+
const snap = restarted.queries.lookupRecordAnyState("sa-m1");
|
|
221
|
+
expect(snap).toBeDefined();
|
|
222
|
+
expect(snap?.status).toBe("closed");
|
|
223
|
+
expect(snap?.closedReason).toBe("parent-shutdown");
|
|
224
|
+
|
|
225
|
+
const err = endedMessageGuard(restarted, "sa-m1", new Error("subagent not found or not owned: sa-m1"));
|
|
226
|
+
expect(err.message).toContain("ended but reconnectable");
|
|
227
|
+
expect(err.message).toContain("closedReason: parent-shutdown");
|
|
228
|
+
expect(err.message).toContain("it was disconnected when the previous parent session exited");
|
|
229
|
+
expect(err.message).not.toContain("deliberately closed by user");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("M1 自愈: manifest 被 SIGKILL 竞态吞掉时,boot 从可重连 entry 重物化反查索引", async () => {
|
|
233
|
+
// 离线形态:archive entry 已随 pi flush 落盘,但 dispose 的 manifest 写未及落盘
|
|
234
|
+
//(无 records/<id>.json、无子文件、无绑定 sidecar——重物化的唯一恢复源是 entry)。
|
|
235
|
+
const mainSessionFile = path.join(agentDir, "main-session.jsonl");
|
|
236
|
+
fs.writeFileSync(
|
|
237
|
+
mainSessionFile,
|
|
238
|
+
`${closedEntryLine({ id: "sa-remat", closedReason: "parent-shutdown" })}\n`,
|
|
239
|
+
"utf-8",
|
|
240
|
+
);
|
|
241
|
+
expect(fs.existsSync(manifestPath(agentDir, "sa-remat"))).toBe(false);
|
|
242
|
+
|
|
243
|
+
const restarted = makeServiceOn(agentDir, { mainSessionFile });
|
|
244
|
+
extraServices.push(restarted);
|
|
245
|
+
|
|
246
|
+
await vi.waitFor(() => expect(fs.existsSync(manifestPath(agentDir, "sa-remat"))).toBe(true));
|
|
247
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath(agentDir, "sa-remat"), "utf-8")) as Record<string, unknown>;
|
|
248
|
+
expect(manifest.status).toBe("closed");
|
|
249
|
+
expect(manifest.closedReason).toBe("parent-shutdown");
|
|
250
|
+
expect(manifest.rootSessionId).toBe("root-session");
|
|
251
|
+
// 动作链同步恢复:message 分流可查(不再 not found)
|
|
252
|
+
const snap = restarted.queries.lookupRecordAnyState("sa-remat");
|
|
253
|
+
expect(snap?.status).toBe("closed");
|
|
254
|
+
expect(snap?.closedReason).toBe("parent-shutdown");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("M1 负向: user-close/cancelled/gc 末条 entry 不被重物化、不被孤儿恢复改写", async () => {
|
|
258
|
+
const mainSessionFile = path.join(agentDir, "main-session.jsonl");
|
|
259
|
+
const lines = [
|
|
260
|
+
closedEntryLine({ id: "sa-uc", closedReason: "user-close" }),
|
|
261
|
+
closedEntryLine({ id: "sa-cc", closedReason: "cancelled" }),
|
|
262
|
+
closedEntryLine({ id: "sa-gc", closedReason: "gc" }),
|
|
263
|
+
];
|
|
264
|
+
fs.writeFileSync(mainSessionFile, `${lines.join("\n")}\n`, "utf-8");
|
|
265
|
+
|
|
266
|
+
const restarted = makeServiceOn(agentDir, { mainSessionFile });
|
|
267
|
+
extraServices.push(restarted);
|
|
268
|
+
await flushAsyncWrites();
|
|
269
|
+
|
|
270
|
+
// 三类主动/自洽终态均不重物化 manifest(可见性语义 = 「记录真没了/无需恢复」)
|
|
271
|
+
expect(fs.existsSync(manifestPath(agentDir, "sa-uc"))).toBe(false);
|
|
272
|
+
expect(fs.existsSync(manifestPath(agentDir, "sa-cc"))).toBe(false);
|
|
273
|
+
expect(fs.existsSync(manifestPath(agentDir, "sa-gc"))).toBe(false);
|
|
274
|
+
// 查询面不可见(保持现状语义),且孤儿恢复未追加任何 subagent-record entry(无改写)
|
|
275
|
+
expect(restarted.queries.lookupRecordAnyState("sa-uc")).toBeUndefined();
|
|
276
|
+
expect(restarted.queries.lookupRecordAnyState("sa-cc")).toBeUndefined();
|
|
277
|
+
expect(restarted.queries.lookupRecordAnyState("sa-gc")).toBeUndefined();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("M1 负向(store 侧): recoverEntryOnlyOrphans 对非 running 末条零 entry 追加", () => {
|
|
281
|
+
// 与上一用例互补:直接钉 store 层候选守卫(isEntryOrphanCandidate 只认 running 末条),
|
|
282
|
+
// 防未来放宽时把主动终态误回收(closed+parent-shutdown 交由 service 重物化段治理)。
|
|
283
|
+
const sessionsDir = path.join(agentDir, "sessions");
|
|
284
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
285
|
+
const mainSessionFile = path.join(agentDir, "main-session.jsonl");
|
|
286
|
+
fs.writeFileSync(
|
|
287
|
+
mainSessionFile,
|
|
288
|
+
`${closedEntryLine({ id: "sa-uc2", closedReason: "user-close" })}\n`,
|
|
289
|
+
"utf-8",
|
|
290
|
+
);
|
|
291
|
+
const appended: Array<{ customType: string; data: Record<string, unknown> }> = [];
|
|
292
|
+
const store = new RecordStore(sessionsDir, undefined, {
|
|
293
|
+
appendEntry: (customType: string, data: unknown) => {
|
|
294
|
+
appended.push({ customType, data: data as Record<string, unknown> });
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
store.recoverEntryOnlyOrphans(mainSessionFile, "root-session");
|
|
298
|
+
expect(appended).toHaveLength(0);
|
|
299
|
+
store.dispose();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("M2: cancel 终态化补写 manifest——无 sessionFile 形态重启可见 + 主动关闭分流", async () => {
|
|
303
|
+
const record = makeRecord({ id: "sa-cx" });
|
|
304
|
+
(service as unknown as ServiceInternals).store.register(record);
|
|
305
|
+
expect(service.cancel("sa-cx")).toBe(true);
|
|
306
|
+
|
|
307
|
+
await vi.waitFor(() => expect(fs.existsSync(manifestPath(agentDir, "sa-cx"))).toBe(true));
|
|
308
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath(agentDir, "sa-cx"), "utf-8")) as Record<string, unknown>;
|
|
309
|
+
expect(manifest.status).toBe("closed");
|
|
310
|
+
expect(manifest.closedReason).toBe("cancelled");
|
|
311
|
+
service.dispose();
|
|
312
|
+
|
|
313
|
+
const restarted = makeServiceOn(agentDir);
|
|
314
|
+
extraServices.push(restarted);
|
|
315
|
+
const snap = restarted.queries.lookupRecordAnyState("sa-cx");
|
|
316
|
+
expect(snap?.status).toBe("closed");
|
|
317
|
+
expect(snap?.closedReason).toBe("cancelled");
|
|
318
|
+
|
|
319
|
+
// 曾报原始 not-found(Gate B sq-c)——现走「主动关闭」专属文案
|
|
320
|
+
const err = endedMessageGuard(restarted, "sa-cx", new Error("subagent not found or not owned: sa-cx"));
|
|
321
|
+
expect(err.message).toContain("deliberately closed by user (closedReason: cancelled)");
|
|
322
|
+
expect(err.message).toContain("cannot be messaged or resumed");
|
|
323
|
+
expect(err.message).not.toContain("fork-from");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("M2: manifest 源 user-close 快照走「主动关闭」专属文案(读侧投影三分流收口)", async () => {
|
|
327
|
+
// user-close 终态正常路径走 doFinalizeRecord(store.markFinalized 携 closedReason,
|
|
328
|
+
// 写侧由 finalize-record.test.ts 钉);此处钉读侧:离线构造 user-close manifest,
|
|
329
|
+
// 重启后 endedMessageGuard 不再误入 reconnectable/fork-from 分支(Gate B 实测错分流形态)。
|
|
330
|
+
const restarted = makeServiceOn(agentDir);
|
|
331
|
+
extraServices.push(restarted);
|
|
332
|
+
fs.writeFileSync(
|
|
333
|
+
manifestPath(agentDir, "sa-ucx"),
|
|
334
|
+
JSON.stringify({
|
|
335
|
+
id: "sa-ucx",
|
|
336
|
+
rootSessionId: "root-session",
|
|
337
|
+
agentName: "general-purpose",
|
|
338
|
+
status: "closed",
|
|
339
|
+
closedReason: "user-close",
|
|
340
|
+
createdAt: 1000,
|
|
341
|
+
completedAt: 2000,
|
|
342
|
+
task: "gate b task",
|
|
343
|
+
slug: "gate-b",
|
|
344
|
+
}),
|
|
345
|
+
"utf-8",
|
|
346
|
+
);
|
|
347
|
+
// manifest 缓存按 stat 戳读取,写入后直接查询即可(listAllSync 每次重 stat)
|
|
348
|
+
const err = endedMessageGuard(restarted, "sa-ucx", new Error("subagent not found or not owned: sa-ucx"));
|
|
349
|
+
expect(err.message).toContain("deliberately closed by user (closedReason: user-close)");
|
|
350
|
+
expect(err.message).toContain("cannot be messaged or resumed");
|
|
351
|
+
expect(err.message).not.toContain("ended but reconnectable");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// ── [B3/D8] disposeAllRecords 终态化归口 markFinalized——行为变化矩阵五行 ──
|
|
355
|
+
//
|
|
356
|
+
// 归口前 dispose 不写 .state 不删 .alive(重启恒分支 4 兜底 running + 孤儿恢复
|
|
357
|
+
// 分流);归口后 .state 携真实 reason(parent-*),重启判定链 = buildRecord 读
|
|
358
|
+
// .state 还原 closedReason → isReconnectableFinalReason 过滤(message resurrect
|
|
359
|
+
// 准入)/ isBootReadoptable(supervisor 自动重认领,只收 running)。五行断言以
|
|
360
|
+
// .state 磁盘产物 + 两谓词直测判定链(与重启重建同源)。
|
|
361
|
+
describe("[B3/D8] disposeAllRecords 终态化归口 markFinalized——行为变化矩阵五行", () => {
|
|
362
|
+
/** 构造带真实 sessionFile + .alive marker 的活跃 record 并注册进 service store。 */
|
|
363
|
+
function registerActiveRecord(opts: {
|
|
364
|
+
id: string;
|
|
365
|
+
chatMode?: boolean;
|
|
366
|
+
resumable?: boolean;
|
|
367
|
+
result?: string;
|
|
368
|
+
}): { record: ExecutionRecord; sessionFile: string } {
|
|
369
|
+
const sessionFile = path.join(agentDir, `sess-${opts.id}.jsonl`);
|
|
370
|
+
fs.writeFileSync(sessionFile, "{}\n", "utf-8");
|
|
371
|
+
fs.writeFileSync(
|
|
372
|
+
`${sessionFile}.alive`,
|
|
373
|
+
`${JSON.stringify({ pid: 99999, id: opts.id, startedAt: 1000 })}\n`,
|
|
374
|
+
"utf-8",
|
|
375
|
+
);
|
|
376
|
+
const record = makeRecord({
|
|
377
|
+
id: opts.id,
|
|
378
|
+
chatMode: opts.chatMode ?? false,
|
|
379
|
+
resumable: opts.resumable,
|
|
380
|
+
result: opts.result,
|
|
381
|
+
sessionFile,
|
|
382
|
+
});
|
|
383
|
+
(service as unknown as ServiceInternals).store.register(record);
|
|
384
|
+
return { record, sessionFile };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** dispose 后的 .state 磁盘产物读取(归口行为变化的核心断言面)。 */
|
|
388
|
+
function readDisposedState(sessionFile: string): { status: string; reason?: string } {
|
|
389
|
+
return JSON.parse(fs.readFileSync(`${sessionFile}.state`, "utf-8")) as { status: string; reason?: string };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
it("行 1 [chat × parent-shutdown 不变]:.state reason=parent-shutdown ∈ 可重连集 → resurrect 可续", () => {
|
|
393
|
+
const { sessionFile } = registerActiveRecord({ id: "sa-d8-chat-shutdown", chatMode: true });
|
|
394
|
+
expect(service.disposeAllRecords("parent-shutdown")).toBe(1);
|
|
395
|
+
|
|
396
|
+
const marker = readDisposedState(sessionFile);
|
|
397
|
+
expect(marker).toEqual({ status: "finalized", reason: "parent-shutdown" });
|
|
398
|
+
// 可续性判定链:closedReason=parent-shutdown ∈ RECONNECTABLE_FINAL_REASONS →
|
|
399
|
+
// message resurrect 准入(closed(parent-shutdown) 可重连,D8 行 1「不变」)
|
|
400
|
+
expect(isReconnectableFinalReason(marker.reason)).toBe(true);
|
|
401
|
+
// release 出口①:.alive 删除
|
|
402
|
+
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("行 2 [chat × parent-fork 收紧,已接受]:.state reason=parent-fork ∉ 可重连集 → message resurrect 硬拒(fork-from 承接)", () => {
|
|
406
|
+
const { sessionFile } = registerActiveRecord({ id: "sa-d8-chat-fork", chatMode: true });
|
|
407
|
+
service.disposeAllRecords("parent-fork");
|
|
408
|
+
|
|
409
|
+
const marker = readDisposedState(sessionFile);
|
|
410
|
+
expect(marker).toEqual({ status: "finalized", reason: "parent-fork" });
|
|
411
|
+
// 收紧裁决(D8 行 2):RECONNECTABLE 注释本就不含 parent-fork/new——现状宽松
|
|
412
|
+
// (可续)是 dispose 不写 .state 的副作用而非有意设计;承接通道 = fork-from 新 id
|
|
413
|
+
expect(isReconnectableFinalReason(marker.reason)).toBe(false);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("行 3 [one-shot × parent-shutdown 放宽,正向]:.state reason=parent-shutdown ∈ 可重连集 → 可续(不再孤儿恢复直断 gc)", () => {
|
|
417
|
+
const { sessionFile } = registerActiveRecord({ id: "sa-d8-oneshot-shutdown" });
|
|
418
|
+
service.disposeAllRecords("parent-shutdown");
|
|
419
|
+
|
|
420
|
+
const marker = readDisposedState(sessionFile);
|
|
421
|
+
expect(marker).toEqual({ status: "finalized", reason: "parent-shutdown" });
|
|
422
|
+
// 放宽裁决(D8 行 3):现状 dispose 不写 .state → one-shot 重启被孤儿恢复直断
|
|
423
|
+
// gc 不可续;归口后真实 reason 落可重连集,与 conversation-continuation D4
|
|
424
|
+
// revive 格意图一致(「session shutdown 时被 disposeAllRecords 关成
|
|
425
|
+
// parent-shutdown 的在途 one-shot 正靠此路径保持可续」)
|
|
426
|
+
expect(isReconnectableFinalReason(marker.reason)).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("行 4 [one-shot × parent-new 一致]:.state reason=parent-new ∉ 可重连集 → 不可续(reason 更真实,不再谎报 gc)", () => {
|
|
430
|
+
const { sessionFile } = registerActiveRecord({ id: "sa-d8-oneshot-new" });
|
|
431
|
+
service.disposeAllRecords("parent-new");
|
|
432
|
+
|
|
433
|
+
const marker = readDisposedState(sessionFile);
|
|
434
|
+
expect(marker).toEqual({ status: "finalized", reason: "parent-new" });
|
|
435
|
+
// 一致裁决(D8 行 4):现状直断 gc 也不可续——行为一致,死因从谎报 gc 变真实
|
|
436
|
+
expect(isReconnectableFinalReason(marker.reason)).toBe(false);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("行 5 [one-shot 纳管态 × parent-shutdown 降级,已接受]:closed(parent-shutdown) → 自动重认领失效,降级手动 resurrect(可重连)", () => {
|
|
440
|
+
// 纳管态形态:resumable=true 且无 result(监督器死亡接管后重启的 W4 形态)
|
|
441
|
+
const { record, sessionFile } = registerActiveRecord({
|
|
442
|
+
id: "sa-d8-adoptable",
|
|
443
|
+
resumable: true,
|
|
444
|
+
result: undefined,
|
|
445
|
+
});
|
|
446
|
+
expect(record.resumable).toBe(true);
|
|
447
|
+
expect(record.result).toBeUndefined();
|
|
448
|
+
service.disposeAllRecords("parent-shutdown");
|
|
449
|
+
|
|
450
|
+
// 归口后内存终态:completeRecord 已跑(result 合成 ""——空串非 undefined)
|
|
451
|
+
expect(record.status).toBe("closed");
|
|
452
|
+
expect(record.closedReason).toBe("parent-shutdown");
|
|
453
|
+
const marker = readDisposedState(sessionFile);
|
|
454
|
+
expect(marker.reason).toBe("parent-shutdown");
|
|
455
|
+
|
|
456
|
+
// 自动重认领失效:isBootReadoptable 只收 running(domain.ts 单一权威谓词)——
|
|
457
|
+
// 归口后重启形态 status=closed → 谓词不命中(现状 SIGKILL 形态分支 4 running
|
|
458
|
+
// → 命中自动重认领)。give-up 的 supervisorNotify 通知面消失(已接受)。
|
|
459
|
+
expect(
|
|
460
|
+
isBootReadoptable({
|
|
461
|
+
status: "closed",
|
|
462
|
+
resumable: true,
|
|
463
|
+
chatMode: false,
|
|
464
|
+
hasResult: record.result !== undefined,
|
|
465
|
+
}),
|
|
466
|
+
).toBe(false);
|
|
467
|
+
// 降级后的兜底通道在:手动 message resurrect 可达(closedReason ∈ 可重连集)
|
|
468
|
+
expect(isReconnectableFinalReason("parent-shutdown")).toBe(true);
|
|
469
|
+
// 对照(现状形态的自动重认领命中——「降级」的差分锚点):分支 4 兜底 running
|
|
470
|
+
expect(
|
|
471
|
+
isBootReadoptable({ status: "running", resumable: true, chatMode: false, hasResult: false }),
|
|
472
|
+
).toBe(true);
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// ── mock 策略 ──
|
|
9
9
|
//
|
|
10
10
|
// 复用 execute-nesting.test.ts 的 spawn / node:fs / manifest-store / temp-prompt / alive-store
|
|
11
|
-
// /
|
|
11
|
+
// / state-marker mock 范式,已收敛到 ./helpers/subagent-service-mocks.ts 单源(四文件
|
|
12
12
|
// 共享)。本文件 **不驱动 FakeChild 完成**——被测的两个分支都在 runSpawn 之前抛/收尾
|
|
13
13
|
// (worktree create 在步骤 2.5,runSpawn 在步骤 5),因此 spawn 即使被调也无人驱动,
|
|
14
14
|
// 测试在抛错后立即断言即可结束。
|
|
@@ -21,7 +21,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
21
21
|
import {
|
|
22
22
|
aliveStoreModule,
|
|
23
23
|
childProcessModule,
|
|
24
|
-
|
|
24
|
+
stateMarkerModule,
|
|
25
25
|
fsSyncModule,
|
|
26
26
|
manifestStoreModule,
|
|
27
27
|
} from "./helpers/subagent-service-mocks.ts";
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
vi.mock("node:child_process", () => childProcessModule());
|
|
30
30
|
vi.mock("node:fs", async (importOriginal) => fsSyncModule(await importOriginal<typeof import("node:fs")>()));
|
|
31
31
|
vi.mock("../alive-store.ts", async (importOriginal) => aliveStoreModule(await importOriginal<typeof import("../alive-store.ts")>()));
|
|
32
|
-
vi.mock("../
|
|
32
|
+
vi.mock("../state-marker.ts", () => stateMarkerModule());
|
|
33
33
|
vi.mock("../manifest-store.ts", () => manifestStoreModule());
|
|
34
34
|
|
|
35
35
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// (那是对 in-process run() 的旧 mock,在 spawn 改造后是死代码)。
|
|
17
17
|
//
|
|
18
18
|
// mock 模块工厂已收敛 ./helpers/subagent-service-mocks.ts(四文件共享单源,含
|
|
19
|
-
// spawn → FakeChild / fs 同步方法 / temp-prompt / alive-store /
|
|
19
|
+
// spawn → FakeChild / fs 同步方法 / temp-prompt / alive-store / state-marker /
|
|
20
20
|
// manifest-store 的完整桩形与动机注释)。
|
|
21
21
|
//
|
|
22
22
|
// 所有断言语义不变:它们测的是 SubagentService 的 **编排逻辑**
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
aliveStoreModule,
|
|
30
30
|
childProcessModule,
|
|
31
31
|
driveChildToCompletion,
|
|
32
|
-
|
|
32
|
+
stateMarkerModule,
|
|
33
33
|
fsSyncModule,
|
|
34
34
|
manifestStoreModule,
|
|
35
35
|
} from "./helpers/subagent-service-mocks.ts";
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
vi.mock("node:child_process", () => childProcessModule());
|
|
38
38
|
vi.mock("node:fs", async (importOriginal) => fsSyncModule(await importOriginal<typeof import("node:fs")>()));
|
|
39
39
|
vi.mock("../alive-store.ts", async (importOriginal) => aliveStoreModule(await importOriginal<typeof import("../alive-store.ts")>()));
|
|
40
|
-
vi.mock("../
|
|
40
|
+
vi.mock("../state-marker.ts", () => stateMarkerModule());
|
|
41
41
|
vi.mock("../manifest-store.ts", () => manifestStoreModule());
|
|
42
42
|
|
|
43
43
|
import { spawn } from "node:child_process";
|
|
@@ -138,7 +138,9 @@ describe("嵌套护栏 / 并发池 / 节流(D-030~D-033 回归锁)", () => {
|
|
|
138
138
|
it("[D-033] execCtxAls depth=MAX 时 execute 抛错(nestingDepth=MAX+1 被拒)", async () => {
|
|
139
139
|
const { service } = setup();
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
// [R1 深绑改写] execNesting 随域 #2 聚合迁入 SessionBaselines(壳转发 getter 透传),
|
|
142
|
+
// 深绑路径改为 service → baselines 聚合实例(断言对象与强度不变,路径对齐终态结构)。
|
|
143
|
+
const execNesting = (Reflect.get(service, "baselines") as { execNesting: ExecCtxAls }).execNesting;
|
|
142
144
|
|
|
143
145
|
await expect(
|
|
144
146
|
execNesting.run({ recordId: "parent", depth: MAX_FORK_DEPTH }, () =>
|
|
@@ -155,7 +157,8 @@ describe("嵌套护栏 / 并发池 / 节流(D-030~D-033 回归锁)", () => {
|
|
|
155
157
|
it("[D-033] execCtxAls depth=MAX-1 时 execute 不抛(nestingDepth=MAX 允许)", async () => {
|
|
156
158
|
const { service, fake } = setupWithFakeEngine();
|
|
157
159
|
|
|
158
|
-
|
|
160
|
+
// [R1 深绑改写] 同上:execNesting 经 baselines 聚合路径取用。
|
|
161
|
+
const execNesting = (Reflect.get(service, "baselines") as { execNesting: ExecCtxAls }).execNesting;
|
|
159
162
|
|
|
160
163
|
const execPromise = execNesting.run({ recordId: "parent", depth: MAX_FORK_DEPTH - 1 }, () =>
|
|
161
164
|
service.execute({ task: "at limit", slug: "test", ctxModel }),
|