@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
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
// lifecycle-manager 单测 —— V2 §5.2 模块 1
|
|
1
|
+
// lifecycle-manager 单测 —— V2 §5.2 模块 1 现存唯一职责(idle timer)。原职责 2/3/4
|
|
2
|
+
// 骨架未接线、职责 5 activate 互斥无生产调用方,已随简化清扫删除。
|
|
2
3
|
//
|
|
3
4
|
// 测试策略:
|
|
4
|
-
// - idle timer
|
|
5
|
-
// Date.now(),让 lastTouched 时间戳可控)。
|
|
6
|
-
// - activate 互斥锁是纯 Promise 链(无 timer),用 advanceTimersByTimeAsync(0)
|
|
7
|
-
// flush microtask 验证串行化。
|
|
5
|
+
// - idle timer 用 vi.useFakeTimers() + advanceTimersByTime。
|
|
8
6
|
// - 每个用例 beforeEach 调 _resetLifecycleState() 隔离模块级单例状态。
|
|
9
7
|
|
|
10
8
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
@@ -20,19 +18,10 @@ vi.mock("../../core/logger.ts", () => ({
|
|
|
20
18
|
|
|
21
19
|
import {
|
|
22
20
|
DEFAULT_IDLE_TIMEOUT_MS,
|
|
23
|
-
DEFAULT_MAX_ALIVE_PROCESSES,
|
|
24
21
|
_resetLifecycleState,
|
|
25
|
-
acquireActivateLock,
|
|
26
22
|
armIdleTimer,
|
|
27
23
|
disarmIdleTimer,
|
|
28
|
-
evictIfOverCeiling,
|
|
29
|
-
getActiveProcessCount,
|
|
30
24
|
hasIdleTimer,
|
|
31
|
-
reapAllAliveProcesses,
|
|
32
|
-
registerActiveProcess,
|
|
33
|
-
scanOrphanProcesses,
|
|
34
|
-
touchActiveProcess,
|
|
35
|
-
unregisterActiveProcess,
|
|
36
25
|
} from "../lifecycle-manager.ts";
|
|
37
26
|
|
|
38
27
|
describe("lifecycle-manager — V2 §5.2 模块 1", () => {
|
|
@@ -179,244 +168,4 @@ describe("lifecycle-manager — V2 §5.2 模块 1", () => {
|
|
|
179
168
|
});
|
|
180
169
|
});
|
|
181
170
|
|
|
182
|
-
// ============================================================
|
|
183
|
-
// 职责 2:全局 ceiling(LRU 挤出)
|
|
184
|
-
// ============================================================
|
|
185
|
-
describe("职责2 全局 ceiling", () => {
|
|
186
|
-
it("register 超过上限时 evictIfOverCeiling 挤出最久空闲", () => {
|
|
187
|
-
registerActiveProcess("sa-1"); // 最早
|
|
188
|
-
vi.advanceTimersByTime(10);
|
|
189
|
-
registerActiveProcess("sa-2");
|
|
190
|
-
vi.advanceTimersByTime(10);
|
|
191
|
-
registerActiveProcess("sa-3"); // 超限(size=3 > ceiling=2)
|
|
192
|
-
expect(getActiveProcessCount()).toBe(3);
|
|
193
|
-
|
|
194
|
-
const evicted: string[] = [];
|
|
195
|
-
evictIfOverCeiling((id) => evicted.push(id), 2);
|
|
196
|
-
|
|
197
|
-
expect(evicted).toEqual(["sa-1"]); // lastTouched 最老的被挤出
|
|
198
|
-
expect(getActiveProcessCount()).toBe(2);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("不超过上限时 evictIfOverCeiling 不挤出", () => {
|
|
202
|
-
registerActiveProcess("sa-1");
|
|
203
|
-
registerActiveProcess("sa-2");
|
|
204
|
-
|
|
205
|
-
const evicted: string[] = [];
|
|
206
|
-
evictIfOverCeiling((id) => evicted.push(id), 5);
|
|
207
|
-
|
|
208
|
-
expect(evicted).toEqual([]);
|
|
209
|
-
expect(getActiveProcessCount()).toBe(2);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("touch 更新 LRU:被 touch 的不再是挤出候选", () => {
|
|
213
|
-
registerActiveProcess("sa-1");
|
|
214
|
-
vi.advanceTimersByTime(10);
|
|
215
|
-
registerActiveProcess("sa-2");
|
|
216
|
-
vi.advanceTimersByTime(10);
|
|
217
|
-
touchActiveProcess("sa-1"); // sa-1 变最新 → sa-2 变最老
|
|
218
|
-
|
|
219
|
-
const evicted: string[] = [];
|
|
220
|
-
evictIfOverCeiling((id) => evicted.push(id), 1);
|
|
221
|
-
|
|
222
|
-
expect(evicted).toEqual(["sa-2"]); // sa-2 最久未 touch,被挤出
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("touch 不存在的 record 为 no-op(不隐式创建)", () => {
|
|
226
|
-
expect(() => touchActiveProcess("never-registered")).not.toThrow();
|
|
227
|
-
expect(getActiveProcessCount()).toBe(0);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("unregister 清理活进程集合", () => {
|
|
231
|
-
registerActiveProcess("sa-1");
|
|
232
|
-
registerActiveProcess("sa-2");
|
|
233
|
-
unregisterActiveProcess("sa-1");
|
|
234
|
-
expect(getActiveProcessCount()).toBe(1);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("挤出时连带 disarm 被挤 record 的 idle timer", () => {
|
|
238
|
-
registerActiveProcess("sa-1");
|
|
239
|
-
armIdleTimer("sa-1", vi.fn(), 1000);
|
|
240
|
-
registerActiveProcess("sa-2");
|
|
241
|
-
vi.advanceTimersByTime(10);
|
|
242
|
-
registerActiveProcess("sa-3"); // sa-1 最老,将被挤出
|
|
243
|
-
|
|
244
|
-
evictIfOverCeiling(() => {
|
|
245
|
-
/* kill */
|
|
246
|
-
}, 2);
|
|
247
|
-
|
|
248
|
-
expect(hasIdleTimer("sa-1")).toBe(false); // 挤出 cascade disarm
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("unregister 连带 disarm idle timer(进程终态 timer 不残留)", () => {
|
|
252
|
-
registerActiveProcess("sa-1");
|
|
253
|
-
armIdleTimer("sa-1", vi.fn(), 1000);
|
|
254
|
-
unregisterActiveProcess("sa-1");
|
|
255
|
-
expect(hasIdleTimer("sa-1")).toBe(false);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it("默认上限 = DEFAULT_MAX_ALIVE_PROCESSES(不传 maxAlive)", () => {
|
|
259
|
-
for (let i = 0; i < DEFAULT_MAX_ALIVE_PROCESSES; i++) {
|
|
260
|
-
registerActiveProcess(`sa-${i}`);
|
|
261
|
-
vi.advanceTimersByTime(1); // 保证 lastTouched 严格递增
|
|
262
|
-
}
|
|
263
|
-
registerActiveProcess("sa-over"); // 超限
|
|
264
|
-
const evicted: string[] = [];
|
|
265
|
-
evictIfOverCeiling((id) => evicted.push(id)); // 用默认上限
|
|
266
|
-
|
|
267
|
-
expect(evicted).toEqual(["sa-0"]); // 默认上限下挤出最早
|
|
268
|
-
expect(getActiveProcessCount()).toBe(DEFAULT_MAX_ALIVE_PROCESSES);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("大幅超限时 while 循环挤出多个直到 ≤ ceiling", () => {
|
|
272
|
-
for (let i = 0; i < 5; i++) {
|
|
273
|
-
registerActiveProcess(`sa-${i}`);
|
|
274
|
-
vi.advanceTimersByTime(1);
|
|
275
|
-
}
|
|
276
|
-
const evicted: string[] = [];
|
|
277
|
-
evictIfOverCeiling((id) => evicted.push(id), 2);
|
|
278
|
-
|
|
279
|
-
expect(evicted).toEqual(["sa-0", "sa-1", "sa-2"]); // 挤出 3 个(5-2),按 LRU
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// ============================================================
|
|
284
|
-
// 职责 3:shutdown 收割
|
|
285
|
-
// ============================================================
|
|
286
|
-
describe("职责3 shutdown 收割", () => {
|
|
287
|
-
it("reapAllAliveProcesses 遍历活进程调 killFn 并返回列表", () => {
|
|
288
|
-
registerActiveProcess("sa-1");
|
|
289
|
-
registerActiveProcess("sa-2");
|
|
290
|
-
|
|
291
|
-
const killed: string[] = [];
|
|
292
|
-
const reaped = reapAllAliveProcesses((id) => killed.push(id));
|
|
293
|
-
|
|
294
|
-
expect(killed).toHaveLength(2);
|
|
295
|
-
expect(killed).toEqual(expect.arrayContaining(["sa-1", "sa-2"]));
|
|
296
|
-
expect(reaped).toEqual(expect.arrayContaining(["sa-1", "sa-2"]));
|
|
297
|
-
expect(getActiveProcessCount()).toBe(0);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it("reap 后 idle timer 已 disarm(advance 不再触发)", () => {
|
|
301
|
-
registerActiveProcess("sa-1");
|
|
302
|
-
const onTimeout = vi.fn();
|
|
303
|
-
armIdleTimer("sa-1", onTimeout, 1000);
|
|
304
|
-
|
|
305
|
-
reapAllAliveProcesses(() => {
|
|
306
|
-
/* kill */
|
|
307
|
-
});
|
|
308
|
-
vi.advanceTimersByTime(10000);
|
|
309
|
-
|
|
310
|
-
expect(onTimeout).not.toHaveBeenCalled(); // timer 已被 reap 清理
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it("无活进程时 reapAll 返回空列表", () => {
|
|
314
|
-
const reaped = reapAllAliveProcesses(() => {
|
|
315
|
-
/* kill */
|
|
316
|
-
});
|
|
317
|
-
expect(reaped).toEqual([]);
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// ============================================================
|
|
322
|
-
// 职责 4:孤儿扫描
|
|
323
|
-
// ============================================================
|
|
324
|
-
describe("职责4 孤儿扫描", () => {
|
|
325
|
-
it("pid 存活 + 不在活进程集合 = 孤儿", () => {
|
|
326
|
-
registerActiveProcess("sa-active"); // 在集合内
|
|
327
|
-
const records = [
|
|
328
|
-
{ id: "sa-active", pid: 100 },
|
|
329
|
-
{ id: "sa-orphan", pid: 200 }, // pid 活但不在集合 → 孤儿
|
|
330
|
-
{ id: "sa-dead", pid: 300 }, // pid 不活 → 非孤儿
|
|
331
|
-
{ id: "sa-nopid" }, // 无 pid → 非孤儿
|
|
332
|
-
];
|
|
333
|
-
|
|
334
|
-
const orphans = scanOrphanProcesses(records, (pid) => pid === 100 || pid === 200);
|
|
335
|
-
|
|
336
|
-
expect(orphans).toEqual(["sa-orphan"]);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it("pid 不存活 = 非孤儿", () => {
|
|
340
|
-
const records = [{ id: "sa-1", pid: 999 }];
|
|
341
|
-
const orphans = scanOrphanProcesses(records, () => false); // 全不活
|
|
342
|
-
expect(orphans).toEqual([]);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it("在活进程集合内 = 非孤儿(即使 pid 活)", () => {
|
|
346
|
-
registerActiveProcess("sa-1");
|
|
347
|
-
const records = [{ id: "sa-1", pid: 100 }];
|
|
348
|
-
const orphans = scanOrphanProcesses(records, () => true);
|
|
349
|
-
expect(orphans).toEqual([]);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it("无 pid 的 record 不视为孤儿", () => {
|
|
353
|
-
const records = [
|
|
354
|
-
{ id: "sa-1" },
|
|
355
|
-
{ id: "sa-2", sessionFile: "/x.jsonl" },
|
|
356
|
-
];
|
|
357
|
-
const orphans = scanOrphanProcesses(records, () => true);
|
|
358
|
-
expect(orphans).toEqual([]);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it("扫描是只读(不改活进程集合)", () => {
|
|
362
|
-
registerActiveProcess("sa-1");
|
|
363
|
-
scanOrphanProcesses([{ id: "sa-2", pid: 200 }], () => true);
|
|
364
|
-
expect(getActiveProcessCount()).toBe(1); // 集合不变
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
// ============================================================
|
|
369
|
-
// 职责 5:activate 互斥锁
|
|
370
|
-
// ============================================================
|
|
371
|
-
describe("职责5 activate 互斥锁", () => {
|
|
372
|
-
it("首次 acquire 立即 resolve 返回 release 函数", async () => {
|
|
373
|
-
const release = await acquireActivateLock("sa-1");
|
|
374
|
-
expect(typeof release).toBe("function");
|
|
375
|
-
release();
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it("并发 acquire 同一 recordId 串行化:第二次等首次 release", async () => {
|
|
379
|
-
const release1 = await acquireActivateLock("sa-1");
|
|
380
|
-
|
|
381
|
-
let secondResolved = false;
|
|
382
|
-
const secondPromise = acquireActivateLock("sa-1").then((r) => {
|
|
383
|
-
secondResolved = true;
|
|
384
|
-
return r;
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
await vi.advanceTimersByTimeAsync(0); // flush microtask
|
|
388
|
-
expect(secondResolved).toBe(false); // 还在等首次 release
|
|
389
|
-
|
|
390
|
-
release1();
|
|
391
|
-
await vi.advanceTimersByTimeAsync(0); // flush microtask
|
|
392
|
-
expect(secondResolved).toBe(true);
|
|
393
|
-
|
|
394
|
-
const release2 = await secondPromise;
|
|
395
|
-
release2();
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
it("不同 recordId 不互斥(各自独立链)", async () => {
|
|
399
|
-
const release1 = await acquireActivateLock("sa-1");
|
|
400
|
-
|
|
401
|
-
let secondResolved = false;
|
|
402
|
-
acquireActivateLock("sa-2").then((r) => {
|
|
403
|
-
secondResolved = true;
|
|
404
|
-
r();
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
await vi.advanceTimersByTimeAsync(0); // flush microtask
|
|
408
|
-
expect(secondResolved).toBe(true); // sa-2 不被 sa-1 阻塞
|
|
409
|
-
|
|
410
|
-
release1();
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
it("release 后同 recordId 可再次 acquire", async () => {
|
|
414
|
-
const r1 = await acquireActivateLock("sa-1");
|
|
415
|
-
r1();
|
|
416
|
-
|
|
417
|
-
const r2 = await acquireActivateLock("sa-1");
|
|
418
|
-
expect(typeof r2).toBe("function");
|
|
419
|
-
r2();
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
171
|
});
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
// src/execution/__tests__/manifest-store-tmp-recovery.test.ts
|
|
2
2
|
//
|
|
3
|
-
// [
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// [U4c / G3] tmp 恢复退役(D6):sweepTmpFiles 语义 = 全部 tmp **静默删除**——
|
|
4
|
+
// manifest 已是可丢可重建缓存(权威 = `.state`,重建 = RecordStore.rebuildIndexes),
|
|
5
|
+
// promote 半写 tmp 的恢复语义失效。断言面:
|
|
6
|
+
// - 合法 JSON 的 tmp(旧 promote 分支 2 形态)也删——promote 退役的核心锚点;
|
|
7
|
+
// - manifest 已存在时的陈旧 tmp(旧分支 1 形态)删;
|
|
8
|
+
// - 非法 JSON 的 tmp(旧分支 3 形态)删;
|
|
9
|
+
// - 返回删除计数(promote 退役后无恢复形态;公开转发链 recoverManifestTmpFiles 签名不变);
|
|
10
|
+
// - [T5④ / PS-13] per-file 容错保留:单个 tmp 删除失败(ENOENT——并发回收/外部
|
|
11
|
+
// 清理抢先)只 warn + 跳过,不再中断整轮。
|
|
6
12
|
|
|
7
13
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
14
|
|
|
@@ -36,7 +42,7 @@ const VALID_MANIFEST = {
|
|
|
36
42
|
status: "running",
|
|
37
43
|
};
|
|
38
44
|
|
|
39
|
-
describe("
|
|
45
|
+
describe("[U4c/G3] sweepTmpFiles 静默删除(promote 语义失效;H4/U5 更名收口)", () => {
|
|
40
46
|
let dir: string;
|
|
41
47
|
let store: ManifestStore;
|
|
42
48
|
|
|
@@ -57,15 +63,32 @@ describe("T5④ recoverTmpFiles per-file tolerance", () => {
|
|
|
57
63
|
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
58
64
|
});
|
|
59
65
|
|
|
60
|
-
it("
|
|
61
|
-
const actualFs = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
62
|
-
// 三个 tmp:good(合法 → promote)、stale(manifest sa-stale.json 已存在 → 删)、
|
|
63
|
-
// doomed(unlink ENOENT)
|
|
66
|
+
it("合法 tmp + manifest 缺失(旧 promote 形态)→ 删而非提升(promote 退役锚点)", async () => {
|
|
64
67
|
fs.writeFileSync(path.join(dir, "sa-good.json.tmp.111"), JSON.stringify(VALID_MANIFEST));
|
|
68
|
+
const result = await store.sweepTmpFiles();
|
|
69
|
+
// 重建源 = `.state` + rebuildIndexes——半写 tmp 不再被复活成「看似权威」的索引
|
|
70
|
+
expect(result).toBe(1);
|
|
71
|
+
expect(fs.existsSync(path.join(dir, "sa-good.json"))).toBe(false);
|
|
72
|
+
expect(fs.existsSync(path.join(dir, "sa-good.json.tmp.111"))).toBe(false);
|
|
73
|
+
expect(loggerMock.warn).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("manifest 已存在 + 非法 JSON tmp → 全删,幸存 manifest 不动", async () => {
|
|
65
77
|
fs.writeFileSync(path.join(dir, "sa-stale.json"), JSON.stringify(VALID_MANIFEST));
|
|
66
78
|
fs.writeFileSync(path.join(dir, "sa-stale.json.tmp.222"), "{}");
|
|
67
|
-
fs.writeFileSync(path.join(dir, "sa-
|
|
68
|
-
const
|
|
79
|
+
fs.writeFileSync(path.join(dir, "sa-junk.json.tmp.333"), "not json");
|
|
80
|
+
const result = await store.sweepTmpFiles();
|
|
81
|
+
expect(result).toBe(2);
|
|
82
|
+
expect(fs.existsSync(path.join(dir, "sa-stale.json"))).toBe(true);
|
|
83
|
+
expect(fs.existsSync(path.join(dir, "sa-stale.json.tmp.222"))).toBe(false);
|
|
84
|
+
expect(fs.existsSync(path.join(dir, "sa-junk.json.tmp.333"))).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("per-file 容错保留:单个 unlink ENOENT 只 warn 跳过,不中断整轮", async () => {
|
|
88
|
+
const actualFs = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
89
|
+
fs.writeFileSync(path.join(dir, "sa-a.json.tmp.444"), "not json");
|
|
90
|
+
fs.writeFileSync(path.join(dir, "sa-doomed.json.tmp.555"), "not json");
|
|
91
|
+
const doomedPath = path.join(dir, "sa-doomed.json.tmp.555");
|
|
69
92
|
unlinkSyncMock.mockImplementation((p: fs.PathLike) => {
|
|
70
93
|
if (String(p) === doomedPath) {
|
|
71
94
|
const err = new Error("ENOENT: file vanished") as NodeJS.ErrnoException;
|
|
@@ -75,25 +98,23 @@ describe("T5④ recoverTmpFiles per-file tolerance", () => {
|
|
|
75
98
|
return actualFs.unlinkSync(p);
|
|
76
99
|
});
|
|
77
100
|
|
|
78
|
-
const result = await store.
|
|
101
|
+
const result = await store.sweepTmpFiles();
|
|
79
102
|
|
|
80
|
-
// 整轮不中断:doomed 之外的 tmp
|
|
81
|
-
expect(result
|
|
82
|
-
expect(
|
|
83
|
-
// good 被提升为正式 manifest
|
|
84
|
-
expect(fs.existsSync(path.join(dir, "sa-good.json"))).toBe(true);
|
|
103
|
+
// 整轮不中断:doomed 之外的 tmp 处理完
|
|
104
|
+
expect(result).toBe(1);
|
|
105
|
+
expect(fs.existsSync(path.join(dir, "sa-a.json.tmp.444"))).toBe(false);
|
|
85
106
|
// 失败留痕(warn 级,含文件名)
|
|
86
107
|
expect(loggerMock.warn).toHaveBeenCalledWith(
|
|
87
|
-
expect.stringContaining("sa-doomed.json.tmp.
|
|
108
|
+
expect.stringContaining("sa-doomed.json.tmp.555"),
|
|
88
109
|
expect.objectContaining({ detail: expect.stringContaining("ENOENT") }),
|
|
89
110
|
);
|
|
90
|
-
expect(loggerMock.warn).toHaveBeenCalledWith(expect.stringContaining("1 of
|
|
111
|
+
expect(loggerMock.warn).toHaveBeenCalledWith(expect.stringContaining("1 of 2 tmp file(s)"));
|
|
91
112
|
});
|
|
92
113
|
|
|
93
|
-
it("
|
|
94
|
-
fs.writeFileSync(path.join(dir, "sa-
|
|
95
|
-
const result = await store.
|
|
96
|
-
expect(result).
|
|
114
|
+
it("无 tmp 残留 → 零副作用零告警", async () => {
|
|
115
|
+
fs.writeFileSync(path.join(dir, "sa-quiescent.json"), JSON.stringify(VALID_MANIFEST));
|
|
116
|
+
const result = await store.sweepTmpFiles();
|
|
117
|
+
expect(result).toBe(0);
|
|
97
118
|
expect(loggerMock.warn).not.toHaveBeenCalled();
|
|
98
119
|
});
|
|
99
120
|
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/execution/__tests__/model-config-service.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [H3/R5 测试归位] ModelConfigService 自有域测试文件。本文件用例自
|
|
4
|
+
// subagent-service.test.ts「ModelConfigService ctxModel 缓存」describe 整块迁入
|
|
5
|
+
//(用例名与断言零改动,纯移动——被测主体是 ModelConfigService 模块而非
|
|
6
|
+
// SubagentService,按被测主体归属原则从 God-class 级测试文件拆出)。
|
|
7
|
+
//
|
|
8
|
+
// ============================================================
|
|
9
|
+
// ModelConfigService ctxModel 缓存(renderCall 标题行 model 显示的核心)
|
|
10
|
+
// ============================================================
|
|
11
|
+
//
|
|
12
|
+
// [HISTORICAL] 99f20da1e 后 renderCall 拿不到主 agent model(ToolRenderContext 无 model),
|
|
13
|
+
// resolveModel 第三层拗错→降级不显示 model。修复:session_start 缓存 ctxModel,
|
|
14
|
+
// resolveModel 第三参默认用缓存。此测试验证该透传链路。
|
|
15
|
+
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as os from "node:os";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
21
|
+
|
|
22
|
+
import { ModelConfigService } from "../model-config-service.ts";
|
|
23
|
+
import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
|
|
24
|
+
|
|
25
|
+
// ── 工具:建临时 agentDir + 真实 ModelConfigService(自 subagent-service.test.ts 迁移段自持副本)──
|
|
26
|
+
|
|
27
|
+
function makeTmpAgentDir(): string {
|
|
28
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subagents-test-"));
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeModelService(agentDir: string): ModelConfigService {
|
|
33
|
+
return new ModelConfigService({ agentDir, cwd: agentDir });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("ModelConfigService ctxModel 缓存", () => {
|
|
37
|
+
let agentDir: string;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
agentDir = makeTmpAgentDir();
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** 最小 mock registry:空可用列表(ctxModel 路径不需要 lookup)。 */
|
|
47
|
+
function makeEmptyRegistry(): ModelRegistryLike {
|
|
48
|
+
return {
|
|
49
|
+
getAvailable: () => [],
|
|
50
|
+
find: () => undefined,
|
|
51
|
+
hasConfiguredAuth: () => false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
it("initModel 传 ctxModel 后,resolveModel 不传第三参返回缓存 model", () => {
|
|
56
|
+
const svc = makeModelService(agentDir);
|
|
57
|
+
const mainModel: ModelInfo = {
|
|
58
|
+
id: "main-model",
|
|
59
|
+
name: "Main",
|
|
60
|
+
provider: "anthropic",
|
|
61
|
+
reasoning: false,
|
|
62
|
+
};
|
|
63
|
+
svc.initModel({
|
|
64
|
+
modelRegistry: makeEmptyRegistry(),
|
|
65
|
+
sessionId: "sess-1",
|
|
66
|
+
ctxModel: mainModel,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// agent 无 model 声明 + 无 override → 走第三层 ctxModel 缓存
|
|
70
|
+
const r = svc.resolveModel("general-purpose");
|
|
71
|
+
expect(r.model).toBe(mainModel);
|
|
72
|
+
expect(r.model.provider).toBe("anthropic");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("显式 ctxModel 参数优先于缓存(execute 路径覆盖 renderCall 缓存)", () => {
|
|
76
|
+
const svc = makeModelService(agentDir);
|
|
77
|
+
const cached: ModelInfo = { id: "cached", name: "C", provider: "p1", reasoning: false };
|
|
78
|
+
const explicit: ModelInfo = { id: "explicit", name: "E", provider: "p2", reasoning: false };
|
|
79
|
+
svc.initModel({
|
|
80
|
+
modelRegistry: makeEmptyRegistry(),
|
|
81
|
+
sessionId: "sess-1",
|
|
82
|
+
ctxModel: cached,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// execute 传显式 ctxModel → 用显式,不用缓存
|
|
86
|
+
const r = svc.resolveModel("general-purpose", undefined, explicit);
|
|
87
|
+
expect(r.model).toBe(explicit);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("setCtxModel 刷新缓存(model_select 后 renderCall 能看到新 model)", () => {
|
|
91
|
+
const svc = makeModelService(agentDir);
|
|
92
|
+
const m1: ModelInfo = { id: "m1", name: "1", provider: "p", reasoning: false };
|
|
93
|
+
const m2: ModelInfo = { id: "m2", name: "2", provider: "p", reasoning: false };
|
|
94
|
+
svc.initModel({ modelRegistry: makeEmptyRegistry(), sessionId: "s", ctxModel: m1 });
|
|
95
|
+
|
|
96
|
+
expect(svc.resolveModel("general-purpose").model).toBe(m1);
|
|
97
|
+
svc.setCtxModel(m2); // 模拟 model_select 刷新
|
|
98
|
+
expect(svc.resolveModel("general-purpose").model).toBe(m2);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("缓存为空且无 override/agentConfig.model → 拗错(不静默降级)", () => {
|
|
102
|
+
const svc = makeModelService(agentDir);
|
|
103
|
+
svc.initModel({
|
|
104
|
+
modelRegistry: makeEmptyRegistry(),
|
|
105
|
+
sessionId: "s",
|
|
106
|
+
// ctxModel 不传 → 缓存为空
|
|
107
|
+
});
|
|
108
|
+
// 空 registry + 空 ctxModel → 第三层不可用 → 拗错
|
|
109
|
+
expect(() => svc.resolveModel("general-purpose")).toThrow(/No available model/);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -17,7 +17,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
17
17
|
import {
|
|
18
18
|
aliveStoreModule,
|
|
19
19
|
childProcessModule,
|
|
20
|
-
|
|
20
|
+
stateMarkerModule,
|
|
21
21
|
fsSyncModule,
|
|
22
22
|
manifestStoreModule,
|
|
23
23
|
} from "./helpers/subagent-service-mocks.ts";
|
|
@@ -29,7 +29,7 @@ import { clearEngines } from "../engine/registry.ts";
|
|
|
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 { spawn } from "node:child_process";
|
|
@@ -101,7 +101,9 @@ describe("TC-3: PI_SUBAGENT_ROOT_SESSION_ID env 贯穿正确", () => {
|
|
|
101
101
|
|
|
102
102
|
it("TC-3a: 根进程 initSession:无 env → sessionRootId = sessionId(自己是 ROOT)", () => {
|
|
103
103
|
const service = setupService("root-it");
|
|
104
|
-
|
|
104
|
+
// [R1 深绑改写] sessionRootId 随域 #2 聚合迁入 SessionBaselines,深绑路径改为
|
|
105
|
+
// service → baselines 聚合实例(断言对象与强度不变,路径对齐终态结构)。
|
|
106
|
+
const sessionRootId = (Reflect.get(service, "baselines") as { sessionRootId: string | null }).sessionRootId as string;
|
|
105
107
|
expect(sessionRootId).toBe("root-it");
|
|
106
108
|
});
|
|
107
109
|
|
|
@@ -109,7 +111,8 @@ describe("TC-3: PI_SUBAGENT_ROOT_SESSION_ID env 贯穿正确", () => {
|
|
|
109
111
|
process.env[ENV_ROOT_SESSION_ID] = "real-root-session";
|
|
110
112
|
const service = setupService("child-session");
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
// [R1 深绑改写] 同上:sessionRootId 经 baselines 聚合路径读取。
|
|
115
|
+
const sessionRootId = (Reflect.get(service, "baselines") as { sessionRootId: string | null }).sessionRootId as string;
|
|
113
116
|
expect(sessionRootId).toBe("real-root-session"); // env 贯穿,不是 child-session
|
|
114
117
|
});
|
|
115
118
|
|
|
@@ -122,11 +125,12 @@ describe("TC-3: PI_SUBAGENT_ROOT_SESSION_ID env 贯穿正确", () => {
|
|
|
122
125
|
const service = setupService("b-session");
|
|
123
126
|
|
|
124
127
|
// B 的 sessionRootId 仍为 ROOT(env 贯穿,不被 B 自己的 sessionId 覆盖)
|
|
125
|
-
|
|
128
|
+
// [R1 深绑改写] 同上:sessionRootId / execNesting 经 baselines 聚合路径读取。
|
|
129
|
+
const sessionRootId = (Reflect.get(service, "baselines") as { sessionRootId: string | null }).sessionRootId as string;
|
|
126
130
|
expect(sessionRootId).toBe("root-main-session");
|
|
127
131
|
|
|
128
132
|
// B 的嵌套基线记录了 B 自己的身份(recordId + depth;[D3-⑤] execNesting 公共层)
|
|
129
|
-
const baseline = (Reflect.get(service, "
|
|
133
|
+
const baseline = ((Reflect.get(service, "baselines") as { execNesting: { baseline(): { recordId: string; depth: number } | null } }).execNesting).baseline()!;
|
|
130
134
|
expect(baseline.recordId).toBe("sa-a-record");
|
|
131
135
|
expect(baseline.depth).toBe(1);
|
|
132
136
|
});
|
|
@@ -13,11 +13,23 @@ import * as fs from "node:fs";
|
|
|
13
13
|
import * as os from "node:os";
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
|
|
16
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
17
17
|
|
|
18
18
|
import { createRecord } from "../execution-record.ts";
|
|
19
19
|
import { RecordStore } from "../record-store.ts";
|
|
20
20
|
|
|
21
|
+
// [Gate A teardown 稳定性] no-op 掉索引落盘。根因:collectRecords 扫描尾的
|
|
22
|
+
// flushIndexAfterScan(record-store.ts)fire-and-forget saveIndex(tmp+fsync+rename
|
|
23
|
+
// 异步 fs),满载下该 promise 可能在本文件 teardown 之后才 settle,其失败分支经
|
|
24
|
+
// logger.warn → console.warn 上报 vitest(rpc onUserConsoleLog 在途)——worker 关闭
|
|
25
|
+
// rpc 时在途调用被 reject 为 EnvironmentTeardownError(0 断言失败,纯 teardown 时序)。
|
|
26
|
+
// 本文件用一次性 tmpdir、断言不观察索引文件,落盘与否无观察者——no-op 消除在途 IO
|
|
27
|
+
// 链,任何负载下确定;loadIndex 等其余导出保留原实现(磁盘重建语义不变)。
|
|
28
|
+
vi.mock("../sessions-index.ts", async (importOriginal) => ({
|
|
29
|
+
...(await importOriginal<typeof import("../sessions-index.ts")>()),
|
|
30
|
+
saveIndex: async () => {},
|
|
31
|
+
}));
|
|
32
|
+
|
|
21
33
|
// ── helpers ──
|
|
22
34
|
|
|
23
35
|
/**
|
|
@@ -917,6 +917,47 @@ describe("createNotifier — ledger 四步接线(U2)", () => {
|
|
|
917
917
|
notifier.dispose();
|
|
918
918
|
});
|
|
919
919
|
|
|
920
|
+
it("dedupKey 显式覆盖 notifyId(drain 丢弃通知独立身份):同轮失败通知 key 不互吞,同 key 重放仍幂等;缺省回退 `id` / `id:round` 不变", () => {
|
|
921
|
+
const ledgerMock = makeLedgerHost();
|
|
922
|
+
const notifierHost = makeNotifierHost();
|
|
923
|
+
// 桥接:ledger sendDelivery → notifier host.sendMessage(模拟 index.ts 装配)
|
|
924
|
+
ledgerMock.host.sendDelivery = (message) => {
|
|
925
|
+
notifierHost.sendMessage(message, { triggerTurn: true });
|
|
926
|
+
if (ledgerMock.deliverPersists.value) {
|
|
927
|
+
ledgerMock.sessionEntries.push({
|
|
928
|
+
type: "custom_message",
|
|
929
|
+
customType: message.customType,
|
|
930
|
+
content: message.content,
|
|
931
|
+
display: message.display,
|
|
932
|
+
details: message.details,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
bindNotifyLedgerHost(ledgerMock.host);
|
|
937
|
+
|
|
938
|
+
const notifier = createNotifier(notifierHost);
|
|
939
|
+
// 场景复刻(conversation-continuation drain 丢弃修复):同 id 同轮——失败单发缺省
|
|
940
|
+
// key `id:round`,丢弃通知 dedupKey `id:round:drain-drop` → 两条都写账送达(修复
|
|
941
|
+
// 前丢弃通知沿用缺省 key 被幂等吞,「队列消息被丢」反馈永不可达)。
|
|
942
|
+
const failedRound = { id: "sa-drain", status: "closed" as const, outcome: "failed" as const, agent: "w", round: 1, startedAt: 1, endedAt: 2 };
|
|
943
|
+
notifier.notify({ ...failedRound, error: "round did not complete: crash" });
|
|
944
|
+
notifier.notify({ ...failedRound, error: "queued message could not be dispatched", dedupKey: "sa-drain:1:drain-drop" });
|
|
945
|
+
// 同 dedupKey 重放 = 同一显式反馈 → 幂等吞(去重语义保留,非全量放行)
|
|
946
|
+
notifier.notify({ ...failedRound, error: "dropped again", dedupKey: "sa-drain:1:drain-drop" });
|
|
947
|
+
|
|
948
|
+
expect(notifierHost.sentMessages).toHaveLength(2);
|
|
949
|
+
expect(notifierHost.sentMessages[0]?.details).toMatchObject({ notifyId: "sa-drain:1" });
|
|
950
|
+
expect(notifierHost.sentMessages[1]?.details).toMatchObject({ notifyId: "sa-drain:1:drain-drop" });
|
|
951
|
+
|
|
952
|
+
// 缺省回退(dedupKey 未设):无 round → 裸 id(notifyClosed 终态通知消费形态),
|
|
953
|
+
// 与 `id:round` / `id:round:drain-drop` 互不吞——key 空间三形态互斥
|
|
954
|
+
notifier.notify({ ...failedRound, round: undefined, error: "closed" });
|
|
955
|
+
expect(notifierHost.sentMessages).toHaveLength(3);
|
|
956
|
+
expect(notifierHost.sentMessages[2]?.details).toMatchObject({ notifyId: "sa-drain" });
|
|
957
|
+
|
|
958
|
+
notifier.dispose();
|
|
959
|
+
});
|
|
960
|
+
|
|
920
961
|
it("ledger 未 bind 时退回内核路径(向后兼容,options 仍单通道 triggerTurn)", () => {
|
|
921
962
|
const notifierHost = makeNotifierHost();
|
|
922
963
|
const notifier = createNotifier(notifierHost);
|
|
@@ -982,7 +1023,7 @@ describe("createNotifier — ledger 四步接线(U2)", () => {
|
|
|
982
1023
|
// ─── MF-5: settled 监听单例化 ──────────────────────────────
|
|
983
1024
|
//
|
|
984
1025
|
// 回归背景:index.ts:428-430 每次 session_start 注册新 pi.on("agent_settled")
|
|
985
|
-
// handler,旧实例 dispose 只置标志不移除物理监听(pi 0.84.
|
|
1026
|
+
// handler,旧实例 dispose 只置标志不移除物理监听(pi 0.84.4 on() 无 off)——
|
|
986
1027
|
// 长会话 N 次切换累积 N 个死 handler。修复:bind 路径首次注册模块级单例
|
|
987
1028
|
// handler(settledEdgeDispatch),后续 bind 只换 boundLedger 引用。
|
|
988
1029
|
|