@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,255 +0,0 @@
|
|
|
1
|
-
// src/execution/__tests__/chat-round-first-round-watchdog.test.ts
|
|
2
|
-
//
|
|
3
|
-
// [F-2 修复] 首轮/冷续轮 settled-watchdog arm 重接 + [F-5 修复] chat 轮路由终态化注销。
|
|
4
|
-
//
|
|
5
|
-
// 背景:armMidRoundNoProgress 此前唯一调用点在 deliverChatMessage(热路径续聊轮);
|
|
6
|
-
// kickOffChatRound(spawn 首轮 + 冷续 resume 轮)内无任何 arm——被删的 inproc
|
|
7
|
-
// stdout-pump 是首轮唯一中段守护,协议化后引擎侧 spawn-runner 仅 turn 计数无墙钟,
|
|
8
|
-
// 首轮 wedged(LC-1 场景①:无事件行输出)无任何熔断。修复 = run 派发前补 arm
|
|
9
|
-
//(refresh 源 = runId 键 ctx.onEvent 事件行 + ctx.onRoundLifecycle settled 交棒——
|
|
10
|
-
// 引擎侧 spawn-runner 对 text_delta 同时走 onEvent 与 onDelta,刷新面与热路径等效)。
|
|
11
|
-
//
|
|
12
|
-
// [F-5] chat 轮路由(chatRoundRoutes)此前仅 cancelBackground 注销;finalize 链终态化
|
|
13
|
-
// (closeChatIdle / closeAfterRoundSettled / finalizeRecord)不注销 → 路由闭包持
|
|
14
|
-
// record/stream/守护引用泄漏。修复 = doFinalizeRecord 的 onFinalized 钩子单一汇聚点。
|
|
15
|
-
//
|
|
16
|
-
// 替身形态与 subagent-service-recovery-bounds.test.ts 同源(fake-engine-port 协议 seam)。
|
|
17
|
-
|
|
18
|
-
import * as fs from "node:fs";
|
|
19
|
-
import * as os from "node:os";
|
|
20
|
-
import * as path from "node:path";
|
|
21
|
-
|
|
22
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
23
|
-
|
|
24
|
-
const { loggerMock } = vi.hoisted(() => ({
|
|
25
|
-
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
26
|
-
}));
|
|
27
|
-
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
28
|
-
|
|
29
|
-
const { killChildSpy } = vi.hoisted(() => ({ killChildSpy: vi.fn() }));
|
|
30
|
-
vi.mock("../engine/host/spawned-children.ts", async (importOriginal) => {
|
|
31
|
-
const actual = await importOriginal<typeof import("../engine/host/spawned-children.ts")>();
|
|
32
|
-
return { ...actual, killRecordChildWithEscalation: killChildSpy };
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
import { clearEngines } from "../engine/registry.ts";
|
|
36
|
-
import { registerFakePiEngine, type FakePiEnginePort } from "./helpers/fake-engine-port.ts";
|
|
37
|
-
import { createRecord } from "../execution-record.ts";
|
|
38
|
-
import { ModelConfigService } from "../model-config-service.ts";
|
|
39
|
-
import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
|
|
40
|
-
import type { RecordStore } from "../record-store.ts";
|
|
41
|
-
import { SubagentService } from "../subagent-service.ts";
|
|
42
|
-
import type { PiLike } from "../subagent-service.ts";
|
|
43
|
-
import {
|
|
44
|
-
getSettledWatchdogPhase,
|
|
45
|
-
hasSettledWatchdog,
|
|
46
|
-
SETTLED_MID_ROUND_NO_PROGRESS_MS,
|
|
47
|
-
SETTLED_WATCHDOG_TIMEOUT_MS,
|
|
48
|
-
_resetSettledWatchdogsForTest,
|
|
49
|
-
} from "../settled-watchdog.ts";
|
|
50
|
-
import { _resetLifecycleState } from "../lifecycle-manager.ts";
|
|
51
|
-
import { _resetCoreSpawnedChildrenMirrorForTest } from "../engine/host/spawned-children.ts";
|
|
52
|
-
import type { ExecutionRecord } from "../types.ts";
|
|
53
|
-
|
|
54
|
-
function makePi(): PiLike {
|
|
55
|
-
return {
|
|
56
|
-
appendEntry: vi.fn(),
|
|
57
|
-
events: { emit: vi.fn() },
|
|
58
|
-
sendMessage: vi.fn(),
|
|
59
|
-
} as unknown as PiLike;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface ServiceInternals {
|
|
63
|
-
store: RecordStore;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const CTX_MODEL: ModelInfo = { id: "m", name: "M", provider: "p", reasoning: false };
|
|
67
|
-
|
|
68
|
-
function makeEmptyRegistry(): ModelRegistryLike {
|
|
69
|
-
return { getAvailable: () => [], find: () => undefined, hasConfiguredAuth: () => true };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function setup(): { agentDir: string; service: SubagentService; store: RecordStore; pi: PiLike; fake: FakePiEnginePort } {
|
|
73
|
-
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "chat-round-watchdog-"));
|
|
74
|
-
clearEngines();
|
|
75
|
-
const fake = registerFakePiEngine();
|
|
76
|
-
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
77
|
-
modelService.initModel({ modelRegistry: makeEmptyRegistry(), sessionId: "root-session", ctxModel: CTX_MODEL });
|
|
78
|
-
const service = new SubagentService({ cwd: agentDir, modelService });
|
|
79
|
-
const pi = makePi();
|
|
80
|
-
service.initSession({ pi, sessionId: "root-session" });
|
|
81
|
-
const store = (service as unknown as ServiceInternals).store;
|
|
82
|
-
return { agentDir, service, store, pi, fake };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** 模拟 kickOffChatRound 的路由注册面(Service map + fake 引擎两端同挂)。 */
|
|
86
|
-
function registerRoute(service: SubagentService, fake: FakePiEnginePort, recordId: string): void {
|
|
87
|
-
const unregister = fake.registerChatRoundRoute(recordId, {
|
|
88
|
-
onStreamDelta: () => {},
|
|
89
|
-
onRoundLifecycle: () => {},
|
|
90
|
-
});
|
|
91
|
-
(
|
|
92
|
-
service as unknown as { chatRoundRoutes: Map<string, () => void> }
|
|
93
|
-
).chatRoundRoutes.set(recordId, unregister);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function makeChatRecord(id: string): ExecutionRecord {
|
|
97
|
-
return createRecord(id, {
|
|
98
|
-
agent: "general-purpose",
|
|
99
|
-
model: "test/model",
|
|
100
|
-
mode: "background",
|
|
101
|
-
task: "test",
|
|
102
|
-
slug: "test",
|
|
103
|
-
startedAt: 1000,
|
|
104
|
-
rootSessionId: "root-session",
|
|
105
|
-
chatMode: true,
|
|
106
|
-
controller: new AbortController(),
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
describe("[F-2] 首轮/冷续轮 settled-watchdog arm(kickOffChatRound run 派发点)", () => {
|
|
111
|
-
let agentDir: string;
|
|
112
|
-
let service: SubagentService;
|
|
113
|
-
let store: RecordStore;
|
|
114
|
-
let pi: PiLike;
|
|
115
|
-
let fake: FakePiEnginePort;
|
|
116
|
-
|
|
117
|
-
beforeEach(() => {
|
|
118
|
-
vi.restoreAllMocks();
|
|
119
|
-
killChildSpy.mockClear();
|
|
120
|
-
({ agentDir, service, store, pi, fake } = setup());
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
afterEach(() => {
|
|
124
|
-
service.dispose();
|
|
125
|
-
clearEngines();
|
|
126
|
-
vi.useRealTimers();
|
|
127
|
-
_resetLifecycleState();
|
|
128
|
-
_resetSettledWatchdogsForTest();
|
|
129
|
-
_resetCoreSpawnedChildrenMirrorForTest();
|
|
130
|
-
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("首轮 run 派发后中段守护 armed(修复前首轮无任何守护)", async () => {
|
|
134
|
-
const handle = await service.execute({ task: "first round", slug: "t", ctxModel: CTX_MODEL, conversation: true });
|
|
135
|
-
expect(fake.runs).toHaveLength(1);
|
|
136
|
-
expect(hasSettledWatchdog(handle.subagentId)).toBe(true);
|
|
137
|
-
expect(getSettledWatchdogPhase(handle.subagentId)).toBe("mid-round");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("冷续轮进行中无产出 → 中段守护按既有超时语义触发:kill + 引擎 cancel + 回退 idle-resumable(fake timers)", async () => {
|
|
141
|
-
// fake timers 必须先于 arm 生效(useFakeTimers 不接管已存在的真实 timer)
|
|
142
|
-
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
143
|
-
const record = makeChatRecord("sa-first");
|
|
144
|
-
record.sessionFile = path.join(agentDir, "prior-session.jsonl");
|
|
145
|
-
fs.writeFileSync(record.sessionFile, "{}\n", "utf-8");
|
|
146
|
-
store.register(record);
|
|
147
|
-
await (
|
|
148
|
-
service as unknown as { resumeColdRound: (r: ExecutionRecord, t: string) => void }
|
|
149
|
-
).resumeColdRound(record, "cold resume text");
|
|
150
|
-
expect(fake.runs).toHaveLength(1); // 冷续轮 run 已派发
|
|
151
|
-
expect(hasSettledWatchdog(record.id)).toBe(true);
|
|
152
|
-
|
|
153
|
-
// 冷续轮全程静默(无协议事件行)满中段窗长
|
|
154
|
-
await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS + 1);
|
|
155
|
-
|
|
156
|
-
expect(killChildSpy).toHaveBeenCalledWith(record.id, "settled watchdog (hot path)");
|
|
157
|
-
expect(hasSettledWatchdog(record.id)).toBe(false); // 到期自清
|
|
158
|
-
// chatMode MF-6:回退 idle-resumable(不销毁对话)
|
|
159
|
-
expect(record.resumable).toBe(true);
|
|
160
|
-
expect(record.status).toBe("running");
|
|
161
|
-
expect(record.result).toContain("settled watchdog");
|
|
162
|
-
expect(record.result).toContain("Recovery");
|
|
163
|
-
// 引擎侧终止意图(协议 interact cancel)
|
|
164
|
-
const cancel = fake.interacts.find((c) => c.action.kind === "cancel");
|
|
165
|
-
expect(cancel).toBeDefined();
|
|
166
|
-
void pi;
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("中段事件行(text_delta 经 onEvent)刷新静默计时:半窗 + 刷新 + 半窗不触发,再满窗才触发", async () => {
|
|
170
|
-
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
171
|
-
const handle = await service.execute({ task: "streaming round", slug: "t", ctxModel: CTX_MODEL, conversation: true });
|
|
172
|
-
const run = fake.runs[0]!;
|
|
173
|
-
|
|
174
|
-
await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS / 2);
|
|
175
|
-
// 首轮 runId 键协议事件行到达(text_delta——引擎侧同时走 onEvent 通道)
|
|
176
|
-
run.emitEvent({ type: "text_delta", delta: "chunk" });
|
|
177
|
-
await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS / 2 + 1);
|
|
178
|
-
expect(hasSettledWatchdog(handle.subagentId)).toBe(true); // 刷新后未到期
|
|
179
|
-
expect(killChildSpy).not.toHaveBeenCalled();
|
|
180
|
-
|
|
181
|
-
// 刷新后再满整个静默窗才触发
|
|
182
|
-
await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS + 1);
|
|
183
|
-
expect(killChildSpy).toHaveBeenCalledWith(handle.subagentId, "settled watchdog (hot path)");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("首轮 settled 相位交棒:中段窗内相位到达 → 切收尾段(中段窗不再触发,收尾段硬顶触发)", async () => {
|
|
187
|
-
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
188
|
-
const handle = await service.execute({ task: "stuck after settle", slug: "t", ctxModel: CTX_MODEL, conversation: true });
|
|
189
|
-
const run = fake.runs[0]!;
|
|
190
|
-
|
|
191
|
-
await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS / 2);
|
|
192
|
-
// 首轮 runId 键 roundLifecycle settled 相位(W3 报告称已接——本用例核实链路生效)
|
|
193
|
-
run.emitLifecycle({ phase: "settled", usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0 } });
|
|
194
|
-
expect(getSettledWatchdogPhase(handle.subagentId)).toBe("settled"); // 已交棒收尾段
|
|
195
|
-
|
|
196
|
-
// 中段不再计时(交棒清掉,不继承——30min 静默判据对收尾段失效);收尾段未满(<600s)不触发
|
|
197
|
-
await vi.advanceTimersByTimeAsync(SETTLED_WATCHDOG_TIMEOUT_MS - 1);
|
|
198
|
-
expect(killChildSpy).not.toHaveBeenCalled();
|
|
199
|
-
expect(hasSettledWatchdog(handle.subagentId)).toBe(true); // 收尾段仍在计时
|
|
200
|
-
|
|
201
|
-
// 收尾段硬顶(600s 默认)到期触发
|
|
202
|
-
await vi.advanceTimersByTimeAsync(2);
|
|
203
|
-
expect(killChildSpy).toHaveBeenCalledWith(handle.subagentId, "settled watchdog (hot path)");
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe("[F-5] chat 轮路由终态化注销(doFinalizeRecord onFinalized 汇聚点)", () => {
|
|
208
|
-
let agentDir: string;
|
|
209
|
-
let service: SubagentService;
|
|
210
|
-
let store: RecordStore;
|
|
211
|
-
let fake: FakePiEnginePort;
|
|
212
|
-
|
|
213
|
-
beforeEach(() => {
|
|
214
|
-
vi.restoreAllMocks();
|
|
215
|
-
killChildSpy.mockClear();
|
|
216
|
-
({ agentDir, service, store, fake } = setup());
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
afterEach(() => {
|
|
220
|
-
service.dispose();
|
|
221
|
-
clearEngines();
|
|
222
|
-
_resetLifecycleState();
|
|
223
|
-
_resetSettledWatchdogsForTest();
|
|
224
|
-
_resetCoreSpawnedChildrenMirrorForTest();
|
|
225
|
-
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("closeChatIdle 终态化路径注销 chat 轮路由(修复前仅 cancelBackground 注销 → 泄漏)", async () => {
|
|
229
|
-
const record = makeChatRecord("sa-route-leak");
|
|
230
|
-
store.register(record);
|
|
231
|
-
// 模拟首轮已注册的 recordId 键路由(kickOffChatRound 注册面:Service map + fake 引擎两端)
|
|
232
|
-
registerRoute(service, fake, record.id);
|
|
233
|
-
// Path A:idle timer armed(进程保活等待续聊)→ closeSubagent(false) 走 closeChatIdle
|
|
234
|
-
const { armIdleTimer } = (await import("../lifecycle-manager.ts")) as typeof import("../lifecycle-manager.ts");
|
|
235
|
-
armIdleTimer(record.id, () => {}, 60_000);
|
|
236
|
-
|
|
237
|
-
await service.chatActions.closeSubagent(record, false);
|
|
238
|
-
|
|
239
|
-
expect(fake.chatRouteUnregisters).toContain(record.id);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("closeAfterRoundSettled(优雅关闭消费)路径注销 chat 轮路由", async () => {
|
|
243
|
-
const record = makeChatRecord("sa-route-close-round");
|
|
244
|
-
store.register(record);
|
|
245
|
-
registerRoute(service, fake, record.id);
|
|
246
|
-
record.closeAfterRound = true;
|
|
247
|
-
|
|
248
|
-
await (
|
|
249
|
-
service as unknown as { closeAfterRoundSettled: (r: ExecutionRecord) => Promise<void> }
|
|
250
|
-
).closeAfterRoundSettled(record);
|
|
251
|
-
|
|
252
|
-
expect(fake.chatRouteUnregisters).toContain(record.id);
|
|
253
|
-
expect(record.status).toBe("closed");
|
|
254
|
-
});
|
|
255
|
-
});
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// src/__tests__/finalized-marker.test.ts
|
|
2
|
-
//
|
|
3
|
-
// finalized-marker 专属测试。
|
|
4
|
-
// 覆盖:write→read 往返 / 缺 sidecar → false / best-effort IO 错静默 / .cancelled 互斥。
|
|
5
|
-
|
|
6
|
-
import * as fs from "node:fs";
|
|
7
|
-
import * as os from "node:os";
|
|
8
|
-
import * as path from "node:path";
|
|
9
|
-
|
|
10
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
11
|
-
|
|
12
|
-
import { readFinalized, readFinalizedReason, writeFinalized } from "../finalized-marker.ts";
|
|
13
|
-
|
|
14
|
-
describe("finalized-marker", () => {
|
|
15
|
-
let tmpDir: string;
|
|
16
|
-
let sessionFile: string;
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "fm-test-"));
|
|
20
|
-
sessionFile = path.join(tmpDir, "2026-01-01_uuid.jsonl");
|
|
21
|
-
});
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe("write → read 往返", () => {
|
|
27
|
-
it("写入后 readFinalized 返回 true", () => {
|
|
28
|
-
writeFinalized(sessionFile);
|
|
29
|
-
expect(readFinalized(sessionFile)).toBe(true);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("sidecar 路径 = sessionFile + '.finalized'", () => {
|
|
33
|
-
writeFinalized(sessionFile);
|
|
34
|
-
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("sidecar 内容为空(无 reason 时保持旧格式兼容)", () => {
|
|
38
|
-
writeFinalized(sessionFile);
|
|
39
|
-
const content = fs.readFileSync(`${sessionFile}.finalized`, "utf-8");
|
|
40
|
-
expect(content).toBe("");
|
|
41
|
-
expect(readFinalizedReason(sessionFile)).toBe("");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// [v8.5 A2] 携带真实 closedReason 写入 → 读取返回原值(trim 后)
|
|
45
|
-
it("带 reason 写入 → readFinalizedReason 返回原值", () => {
|
|
46
|
-
writeFinalized(sessionFile, "user-close");
|
|
47
|
-
expect(readFinalized(sessionFile)).toBe(true);
|
|
48
|
-
expect(readFinalizedReason(sessionFile)).toBe("user-close");
|
|
49
|
-
|
|
50
|
-
writeFinalized(sessionFile, "parent-shutdown");
|
|
51
|
-
expect(readFinalizedReason(sessionFile)).toBe("parent-shutdown");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("带 reason 的内容含首尾空白时 trim 后返回", () => {
|
|
55
|
-
fs.writeFileSync(`${sessionFile}.finalized`, " gc \n", "utf-8");
|
|
56
|
-
expect(readFinalizedReason(sessionFile)).toBe("gc");
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe("readFinalizedReason 无 sidecar", () => {
|
|
61
|
-
it("不存在 → undefined(与「存在但空串」区分)", () => {
|
|
62
|
-
expect(readFinalizedReason(sessionFile)).toBeUndefined();
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe("readFinalized 无 sidecar", () => {
|
|
67
|
-
it("不存在 → false", () => {
|
|
68
|
-
expect(readFinalized(sessionFile)).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe("best-effort IO 错静默", () => {
|
|
73
|
-
it("writeFileSync 抛错时不抛出", () => {
|
|
74
|
-
// 目标路径不存在(不存在的父目录),writeFileSync 会抛
|
|
75
|
-
const badPath = path.join(tmpDir, "nonexistent-sub", "session.jsonl");
|
|
76
|
-
expect(() => writeFinalized(badPath)).not.toThrow();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("写失败后 readFinalized 返回 false(sidecar 未写入)", () => {
|
|
80
|
-
const badPath = path.join(tmpDir, "nonexistent-sub", "session.jsonl");
|
|
81
|
-
writeFinalized(badPath); // 静默失败
|
|
82
|
-
expect(readFinalized(badPath)).toBe(false);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe("与 .cancelled 互斥(BC-4)", () => {
|
|
87
|
-
it("写 finalized 时删除已有的 .cancelled", () => {
|
|
88
|
-
// 先写一个 .cancelled sidecar
|
|
89
|
-
fs.writeFileSync(`${sessionFile}.cancelled`, `{"id":"bg-1","status":"cancelled","agent":"w","startedAt":1,"endedAt":2}\n`, "utf-8");
|
|
90
|
-
expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(true);
|
|
91
|
-
|
|
92
|
-
// 写 finalized → .cancelled 应被删除
|
|
93
|
-
writeFinalized(sessionFile);
|
|
94
|
-
expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(false);
|
|
95
|
-
expect(readFinalized(sessionFile)).toBe(true);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("无 .cancelled 时写 finalized 正常(不报错)", () => {
|
|
99
|
-
expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(false);
|
|
100
|
-
expect(() => writeFinalized(sessionFile)).not.toThrow();
|
|
101
|
-
expect(readFinalized(sessionFile)).toBe(true);
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
});
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
// acquireActivateLock 30s 超时兜底单测 —— v4-lifecycle-convergence.md §3.3 A-2。
|
|
2
|
-
//
|
|
3
|
-
// 验证:前序 holder 持锁不 release(崩溃/死锁模拟)时,waiter
|
|
4
|
-
// - 等待 29999ms 仍 pending(未误伤正常排队)
|
|
5
|
-
// - 到 30001ms 抛含恢复指引的超时错误('activation timed out' + 'retry action: message')
|
|
6
|
-
//
|
|
7
|
-
// 测试策略:纯 Promise 链 + setTimeout 超时,用 vi.useFakeTimers() 控时,
|
|
8
|
-
// advanceTimersByTimeAsync 同时推进 fake timer 与 microtask flush。
|
|
9
|
-
|
|
10
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
_getActivateLockTailCountForTest,
|
|
14
|
-
_resetLifecycleState,
|
|
15
|
-
acquireActivateLock,
|
|
16
|
-
} from "../lifecycle-manager.ts";
|
|
17
|
-
|
|
18
|
-
describe("acquireActivateLock 30s 超时兜底 (v4 A-2)", () => {
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
vi.useFakeTimers();
|
|
21
|
-
_resetLifecycleState();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
_resetLifecycleState();
|
|
26
|
-
vi.useRealTimers();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("waiter 等待 29999ms 仍 pending;到 30001ms 抛含恢复指引的超时错误", async () => {
|
|
30
|
-
// 第一个 holder 持锁后不 release(模拟崩溃/死锁)
|
|
31
|
-
await acquireActivateLock("sa-stuck");
|
|
32
|
-
|
|
33
|
-
// 第二个 waiter 必然排队(前序未 release)
|
|
34
|
-
let errMsg: string | null = null;
|
|
35
|
-
let gotRelease: (() => void) | null = null;
|
|
36
|
-
const waiter = acquireActivateLock("sa-stuck")
|
|
37
|
-
.then((release) => {
|
|
38
|
-
gotRelease = release;
|
|
39
|
-
})
|
|
40
|
-
.catch((err: unknown) => {
|
|
41
|
-
errMsg = err instanceof Error ? err.message : String(err);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// flush microtask:waiter 进入 prev.then 等待链
|
|
45
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
46
|
-
expect(gotRelease).toBe(null); // 尚未拿到锁
|
|
47
|
-
expect(errMsg).toBe(null);
|
|
48
|
-
|
|
49
|
-
// 29999ms:未满 30s,waiter 仍 pending
|
|
50
|
-
await vi.advanceTimersByTimeAsync(29999);
|
|
51
|
-
expect(gotRelease).toBe(null);
|
|
52
|
-
expect(errMsg).toBe(null);
|
|
53
|
-
|
|
54
|
-
// 再推进 2ms(累计 30001ms):超时 timer 触发 → reject
|
|
55
|
-
await vi.advanceTimersByTimeAsync(2);
|
|
56
|
-
await waiter;
|
|
57
|
-
|
|
58
|
-
// 超时:未拿到锁,抛含恢复指引的错误
|
|
59
|
-
expect(gotRelease).toBe(null);
|
|
60
|
-
expect(errMsg).not.toBe(null);
|
|
61
|
-
expect(errMsg).toContain("activation timed out");
|
|
62
|
-
expect(errMsg).toContain("retry action: message");
|
|
63
|
-
expect(errMsg).toContain("sa-stuck");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("首次 acquire(无前序 holder)立即返回 release,不触发超时", async () => {
|
|
67
|
-
// 首次 acquire:prev=Promise.resolve(),立即 resolve,timer 被 clearTimeout 取消
|
|
68
|
-
const release = await acquireActivateLock("sa-fresh");
|
|
69
|
-
expect(typeof release).toBe("function");
|
|
70
|
-
|
|
71
|
-
// 推进远超 30s,不应有任何未捕获的超时 reject(timer 已清除)
|
|
72
|
-
await vi.advanceTimersByTimeAsync(60000);
|
|
73
|
-
release();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("[review 修复] 超时后前序 release → 链可推进:二次 acquire 立即获锁(锁链不瘫痪)", async () => {
|
|
77
|
-
// 瘫痪路径回归:waiter 超时 reject 后,若其 current 永久 pending,tail 永久
|
|
78
|
-
// pending → 前序 release 后后续 acquire 也只能 30s 超时(只能重启恢复)。
|
|
79
|
-
// 修复后超时回调 resolve 自身 current 放行链尾:release1 后 tail settle,
|
|
80
|
-
// 二次 acquire 的 prev 已 settle → 立即拿到 releaseFn。
|
|
81
|
-
const release1 = await acquireActivateLock("sa-recover");
|
|
82
|
-
|
|
83
|
-
let waiterErr: string | null = null;
|
|
84
|
-
const waiter = acquireActivateLock("sa-recover").catch((err: unknown) => {
|
|
85
|
-
waiterErr = err instanceof Error ? err.message : String(err);
|
|
86
|
-
});
|
|
87
|
-
await vi.advanceTimersByTimeAsync(0); // waiter 挂链
|
|
88
|
-
await vi.advanceTimersByTimeAsync(30001); // 超时 reject(+ 超时放行链尾)
|
|
89
|
-
await waiter;
|
|
90
|
-
expect(waiterErr).toContain("activation timed out");
|
|
91
|
-
|
|
92
|
-
release1(); // 前序释放 → tail settle(current 已被超时放行 resolve)
|
|
93
|
-
await vi.advanceTimersByTimeAsync(0); // flush microtask 链
|
|
94
|
-
|
|
95
|
-
// 二次 acquire:prev 是已 settle 的 tail → 立即拿到锁,不等 30s
|
|
96
|
-
const release2 = await acquireActivateLock("sa-recover");
|
|
97
|
-
expect(typeof release2).toBe("function");
|
|
98
|
-
release2();
|
|
99
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("[review 修复] 前序持续持锁时超时不放行后续 waiter 提前获锁(互斥保持)", async () => {
|
|
103
|
-
// 超时放行只 resolve 自身 current(tail 仍等 prev);前序未 release 时,
|
|
104
|
-
// 新 acquire 依然排队等待,不会与前序 holder 形成双写者。
|
|
105
|
-
const release1 = await acquireActivateLock("sa-mutex");
|
|
106
|
-
|
|
107
|
-
const waiter1 = acquireActivateLock("sa-mutex").catch(() => {});
|
|
108
|
-
await vi.advanceTimersByTimeAsync(30001); // waiter1 超时放行自身 current
|
|
109
|
-
await waiter1;
|
|
110
|
-
|
|
111
|
-
// 前序仍持锁:新 acquire 的 prev(= waiter1 的 tail)等待 release1,不提前获锁
|
|
112
|
-
let gotRelease2 = false;
|
|
113
|
-
const waiter2 = acquireActivateLock("sa-mutex").then(() => {
|
|
114
|
-
gotRelease2 = true;
|
|
115
|
-
});
|
|
116
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
117
|
-
expect(gotRelease2).toBe(false); // 未提前获锁
|
|
118
|
-
|
|
119
|
-
release1();
|
|
120
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
121
|
-
await waiter2; // release1 后链推进,waiter2 获锁
|
|
122
|
-
expect(gotRelease2).toBe(true);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("[review 修复] 超时者对称自清:前序 release 推进链尾后 Map 条目回收;前序持锁期间不提前删", async () => {
|
|
126
|
-
// round2 INFO 残留:超时者 releaseFn 永不被调用,原实现条目滞留 Map 直到下次
|
|
127
|
-
// 同 recordId acquire 覆盖。修复后挂 tail 尾部自清——tail settle(前序已
|
|
128
|
-
// release)时回收;tail pending(前序仍持锁)时回调不执行,条目保留(后续
|
|
129
|
-
// acquire 仍排队等待,互斥不被破坏)。
|
|
130
|
-
const release1 = await acquireActivateLock("sa-timeout-gc");
|
|
131
|
-
|
|
132
|
-
const waiter = acquireActivateLock("sa-timeout-gc").catch(() => {});
|
|
133
|
-
await vi.advanceTimersByTimeAsync(0); // waiter 挂链,Map 链尾已覆盖为 waiter 的 tail
|
|
134
|
-
expect(_getActivateLockTailCountForTest()).toBe(1);
|
|
135
|
-
|
|
136
|
-
await vi.advanceTimersByTimeAsync(30001); // 超时:reject + settleCurrent + tail.then 自清排队
|
|
137
|
-
await waiter;
|
|
138
|
-
// 前序仍持锁 → tail 仍 pending → 自清回调未执行 → 条目仍在
|
|
139
|
-
expect(_getActivateLockTailCountForTest()).toBe(1);
|
|
140
|
-
|
|
141
|
-
release1(); // 前序释放 → tail settle → 自清回调执行(identity 匹配,无新 waiter)
|
|
142
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
143
|
-
expect(_getActivateLockTailCountForTest()).toBe(0);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe("activateLockTails tail-identity 自清(release 后回收)", () => {
|
|
148
|
-
beforeEach(() => {
|
|
149
|
-
vi.useFakeTimers();
|
|
150
|
-
_resetLifecycleState();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
afterEach(() => {
|
|
154
|
-
_resetLifecycleState();
|
|
155
|
-
vi.useRealTimers();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("W3TC14: 无 waiter 时 release 后 Map 空 + release 后可再次 acquire", async () => {
|
|
159
|
-
const release = await acquireActivateLock("sa-gc");
|
|
160
|
-
// 自清新增的 test-only 观察点:acquire 后 Map 有 1 条链尾
|
|
161
|
-
expect(_getActivateLockTailCountForTest()).toBe(1);
|
|
162
|
-
|
|
163
|
-
release();
|
|
164
|
-
// 自清经 queueMicrotask 异步执行——advanceTimersByTimeAsync(0) flush microtask
|
|
165
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
166
|
-
// 无 waiter:Map 中链尾即本链尾,identity 匹配 → delete 回收
|
|
167
|
-
expect(_getActivateLockTailCountForTest()).toBe(0);
|
|
168
|
-
|
|
169
|
-
// release 后可再次 acquire:prev 回落 Promise.resolve(),立即 resolve 返回 release
|
|
170
|
-
const release2 = await acquireActivateLock("sa-gc");
|
|
171
|
-
expect(typeof release2).toBe("function");
|
|
172
|
-
release2();
|
|
173
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
174
|
-
expect(_getActivateLockTailCountForTest()).toBe(0);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("W3TC15: 有 waiter 排队不删、最后释放者回收;不同 recordId 链独立", async () => {
|
|
178
|
-
const release1 = await acquireActivateLock("sa-q");
|
|
179
|
-
|
|
180
|
-
// 发起 acquire#2 同 recordId(不 await,.then 捕获 release2)
|
|
181
|
-
let release2: (() => void) | null = null;
|
|
182
|
-
const waiter = acquireActivateLock("sa-q").then((r) => {
|
|
183
|
-
release2 = r;
|
|
184
|
-
});
|
|
185
|
-
// #2 进入等待链——此刻 Map 链尾已被 #2 覆盖
|
|
186
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
187
|
-
|
|
188
|
-
release1();
|
|
189
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
190
|
-
// ES7 核心:waiter 的链尾仍在——identity 不匹配不删
|
|
191
|
-
expect(_getActivateLockTailCountForTest()).toBe(1);
|
|
192
|
-
|
|
193
|
-
// acquire#2 在 release1 后正常 resolve(等待链不被 delete 破坏——waiter 持
|
|
194
|
-
// acquire 时刻捕获的 prev Promise 引用而非 Map 查询)
|
|
195
|
-
await waiter;
|
|
196
|
-
expect(typeof release2).toBe("function");
|
|
197
|
-
|
|
198
|
-
// 另一 recordId 独立链:同时持有时各自独立计数
|
|
199
|
-
const releaseA = await acquireActivateLock("sa-a");
|
|
200
|
-
expect(_getActivateLockTailCountForTest()).toBe(2); // sa-q + sa-a
|
|
201
|
-
|
|
202
|
-
release2!();
|
|
203
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
204
|
-
// 最后释放者回收:release2 的 identity 匹配 → sa-q 条目回收,sa-a 仍在
|
|
205
|
-
expect(_getActivateLockTailCountForTest()).toBe(1);
|
|
206
|
-
|
|
207
|
-
releaseA();
|
|
208
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
209
|
-
expect(_getActivateLockTailCountForTest()).toBe(0);
|
|
210
|
-
});
|
|
211
|
-
});
|