@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,20 +1,18 @@
|
|
|
1
1
|
// src/execution/__tests__/subagent-service-recovery-bounds.test.ts
|
|
2
2
|
//
|
|
3
3
|
// [u-svc / T2] 服务侧回收上界与 kill 收敛:
|
|
4
|
-
// - T2④/LC-2:closeChatIdle /
|
|
5
|
-
//
|
|
6
|
-
// 意图(协议 interact cancel/close fire-and-forget);
|
|
4
|
+
// - T2④/LC-2:closeChatIdle / cancelBackground 终止路径收敛到
|
|
5
|
+
// killRecordChildWithEscalation(spy 断言调用点与参数);
|
|
7
6
|
// - T2⑥/PS-1:disposeAllRecords 补三回收面(controller.abort + kill + disarm idle timer
|
|
8
7
|
// + disarm settled watchdog);
|
|
9
|
-
// - T2③/LC-1 + D9
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// - T2③/LC-1 + D9:Continuation 轮 armMidRoundNoProgress(挂载点在 kickOffChatRound
|
|
9
|
+
// 轮开跑;中段无进展检测 + 到期处置经 Continuation.onWatchdogFire → run 收敛失败
|
|
10
|
+
// 分支统一收口,error 含 'settled watchdog' 标记与恢复指引)。
|
|
12
11
|
//
|
|
13
|
-
// [W3 改写]
|
|
14
|
-
//(
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// 可泄漏的本地方案进程),core 侧不再可等价改写。
|
|
12
|
+
// [W3 改写 → H1 U6] 替身 = 协议 seam(registerFakePiEngine)。[H1 U6] interact 断言
|
|
13
|
+
//(cancel/close 终止意图受理)随 interact 面退役删除:真实杀链 = 轮级 abort signal →
|
|
14
|
+
// 协议 cancel 帧(引擎侧杀链)+ 镜像置死记账 + 引擎退出链收割(红线①);
|
|
15
|
+
// closeAfterRoundSettled 用例随载体退役删除(T2⑧ 段同前批登记归 pi 包)。
|
|
18
16
|
//
|
|
19
17
|
// mock 形态:真实 SubagentService + ServiceInternals cast 暴露 store + logger +
|
|
20
18
|
// spawned-children 的 kill 入口换 spy(importOriginal 保留镜像真实现)。
|
|
@@ -47,7 +45,9 @@ import type { PiLike } from "../subagent-service.ts";
|
|
|
47
45
|
import { armSettledWatchdog, hasSettledWatchdog, SETTLED_MID_ROUND_NO_PROGRESS_MS, _resetSettledWatchdogsForTest } from "../settled-watchdog.ts";
|
|
48
46
|
import { armIdleTimer, hasIdleTimer, _resetLifecycleState } from "../lifecycle-manager.ts";
|
|
49
47
|
import { _resetCoreSpawnedChildrenMirrorForTest } from "../engine/host/spawned-children.ts";
|
|
50
|
-
|
|
48
|
+
// AgentResult = execution 层那份(execution/types.ts:259,主字段 text/turns/sessionId),
|
|
49
|
+
// 与 acquirePoolOrFinalize 签名一致;engine/types.ts 只有消歧注释无此导出。
|
|
50
|
+
import type { AgentResult, ExecutionRecord } from "../types.ts";
|
|
51
51
|
|
|
52
52
|
function makeTmpAgentDir(): string {
|
|
53
53
|
return fs.mkdtempSync(path.join(os.tmpdir(), "svc-recovery-bounds-"));
|
|
@@ -102,23 +102,21 @@ function privateFn<K extends string>(service: SubagentService, key: K): (...args
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
* message 投递入口([
|
|
106
|
-
*
|
|
105
|
+
* message 投递入口([H1 U6] Continuation 编排:deliverChatMessage → onMessage →
|
|
106
|
+
* 新 run + resume 锚点)。
|
|
107
107
|
*/
|
|
108
108
|
async function deliverChat(
|
|
109
109
|
service: SubagentService,
|
|
110
110
|
record: ExecutionRecord,
|
|
111
111
|
text: string,
|
|
112
|
-
interrupt: boolean,
|
|
113
112
|
): Promise<void> {
|
|
114
113
|
return (
|
|
115
114
|
privateFn(service, "deliverChatMessage") as (
|
|
116
115
|
this: SubagentService,
|
|
117
116
|
r: ExecutionRecord,
|
|
118
117
|
t: string,
|
|
119
|
-
i: boolean,
|
|
120
118
|
) => Promise<void>
|
|
121
|
-
).call(service, record, text
|
|
119
|
+
).call(service, record, text);
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
describe("T2④ service-side kill convergence", () => {
|
|
@@ -149,10 +147,8 @@ describe("T2④ service-side kill convergence", () => {
|
|
|
149
147
|
const ok = service.cancel(record.id);
|
|
150
148
|
expect(ok).toBe(true);
|
|
151
149
|
expect(killChildSpy).toHaveBeenCalledWith(record.id, "cancelBackground");
|
|
152
|
-
// [
|
|
153
|
-
|
|
154
|
-
expect(fake.interacts.some((c) => c.action.kind === "cancel")).toBe(true);
|
|
155
|
-
});
|
|
150
|
+
// [H1 U6] 在途 run 的实际终止经 controller.abort → 轮级 signal(cancel 帧 + 杀链);
|
|
151
|
+
// 旧 interact cancel 受理断言随 interact 面退役。
|
|
156
152
|
});
|
|
157
153
|
|
|
158
154
|
it("closeChatIdle (via closeSubagent force:false on idle) routes through killRecordChildWithEscalation and disarms timers", async () => {
|
|
@@ -164,19 +160,8 @@ describe("T2④ service-side kill convergence", () => {
|
|
|
164
160
|
expect(killChildSpy).toHaveBeenCalledWith(record.id, "closeChatIdle");
|
|
165
161
|
expect(hasIdleTimer(record.id)).toBe(false);
|
|
166
162
|
expect(hasSettledWatchdog(record.id)).toBe(false);
|
|
167
|
-
// [
|
|
168
|
-
|
|
169
|
-
const close = fake.interacts.find((c) => c.action.kind === "close");
|
|
170
|
-
expect(close?.action).toMatchObject({ kind: "close", payload: { force: true } });
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("closeAfterRoundSettled routes through killRecordChildWithEscalation", async () => {
|
|
175
|
-
const record = makeRecord({ id: "sa-close-round" });
|
|
176
|
-
await (
|
|
177
|
-
privateFn(service, "closeAfterRoundSettled") as (this: SubagentService, r: ExecutionRecord) => Promise<void>
|
|
178
|
-
).call(service, record);
|
|
179
|
-
expect(killChildSpy).toHaveBeenCalledWith(record.id, "closeAfterRoundSettled");
|
|
163
|
+
// [H1 U6] 旧引擎侧 close force 受理断言随 interact 面退役(无在跑轮无需进程回收,
|
|
164
|
+
// Path A 保活进程由镜像记账 + reaper 兜底回收)。
|
|
180
165
|
});
|
|
181
166
|
|
|
182
167
|
it("disposeAllRecords applies all recovery surfaces: abort + escalation kill + disarm idle/settled timers", () => {
|
|
@@ -230,42 +215,134 @@ describe("T2③ hot-path settled watchdog", () => {
|
|
|
230
215
|
});
|
|
231
216
|
|
|
232
217
|
it("arms the settled watchdog after a successful hot-path prompt", async () => {
|
|
233
|
-
|
|
218
|
+
// [H1 U2] deliverChatMessage = Continuation 派发(续聊轮需 sessionFile 锚点)
|
|
219
|
+
const record = makeRecord({ id: "sa-hot-arm", sessionFile: path.join(agentDir, "sa-hot-arm.jsonl") });
|
|
220
|
+
fs.writeFileSync(record.sessionFile!, "{}\n", "utf-8");
|
|
234
221
|
store.register(record);
|
|
235
|
-
await deliverChat(service, record, "hello"
|
|
236
|
-
expect(fake.
|
|
222
|
+
await deliverChat(service, record, "hello");
|
|
223
|
+
await vi.waitFor(() => expect(fake.runs.length).toBe(1));
|
|
237
224
|
expect(hasSettledWatchdog(record.id)).toBe(true);
|
|
238
225
|
});
|
|
239
226
|
|
|
240
|
-
it("onMidTimeout: kills the child (engine cancel), fails the round, error carries 'settled watchdog' marker and recovery hint", async () => {
|
|
227
|
+
it("onMidTimeout: kills the child (engine cancel + round abort), fails the round via run settlement, error carries 'settled watchdog' marker and recovery hint", async () => {
|
|
241
228
|
// fake timers 必须先于 arm 生效(useFakeTimers 不接管已存在的真实 timer)
|
|
242
229
|
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
243
|
-
|
|
230
|
+
// [H1 U2] Continuation 轮需要 sessionFile 锚点
|
|
231
|
+
const record = makeRecord({ id: "sa-hot-timeout", sessionFile: path.join(agentDir, "sa-hot-timeout.jsonl") });
|
|
232
|
+
fs.writeFileSync(record.sessionFile!, "{}\n", "utf-8");
|
|
244
233
|
store.register(record);
|
|
245
|
-
await deliverChat(service, record, "hello"
|
|
234
|
+
await deliverChat(service, record, "hello");
|
|
235
|
+
await vi.waitFor(() => expect(fake.runs.length).toBe(1));
|
|
246
236
|
expect(hasSettledWatchdog(record.id)).toBe(true);
|
|
247
237
|
|
|
248
|
-
// [D9 两段式]
|
|
249
|
-
//
|
|
238
|
+
// [D9 两段式] 轮开跑后挂中段:静默满中段窗长触发(本测试替身不驱动协议事件,
|
|
239
|
+
// 中段静默形态直达)
|
|
250
240
|
await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS + 1);
|
|
251
241
|
|
|
252
|
-
// kill 收敛入口被触发(
|
|
253
|
-
|
|
242
|
+
// kill 收敛入口被触发([H1 U2] Continuation watchdog fire → killRoundChildForWatchdog,
|
|
243
|
+
// source 含 phase 名——旧 onHotPathSettledWatchdogTimeout 的 "(hot path)" 标记退役)
|
|
244
|
+
expect(killChildSpy).toHaveBeenCalledWith(record.id, "settled watchdog (mid-round)");
|
|
254
245
|
// watchdog 到期自清(armedTimers 先删条目再执行回调)
|
|
255
246
|
expect(hasSettledWatchdog(record.id)).toBe(false);
|
|
256
|
-
//
|
|
257
|
-
|
|
247
|
+
// [H1 U2] fire abort 轮 signal → 在途 run 收敛(替身模拟 abort 合成失败终态)→
|
|
248
|
+
// onRunSettled 失败分支簿记(chatMode → MF-6 回退 resumable)+ 失败通知
|
|
249
|
+
fake.runs[0]!.settle({
|
|
250
|
+
content: "",
|
|
251
|
+
error: "engine_run_failed: run aborted (settled watchdog mid-round no-progress); the process was terminated to bound the wait. Recovery: check state with subagents action:'list', then re-send your message to continue.",
|
|
252
|
+
exitCode: null,
|
|
253
|
+
});
|
|
254
|
+
await vi.waitFor(() => expect(record.resumable).toBe(true));
|
|
258
255
|
expect(record.status).toBe("running");
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
expect(record.
|
|
262
|
-
|
|
256
|
+
// [H1 U2 / D7] 失败轮 lastError 写失败原因(result = 前值 ?? 失败摘要——失败摘要
|
|
257
|
+
// 由 Continuation 失败通知独立承载)
|
|
258
|
+
expect(record.lastError).toContain("settled watchdog");
|
|
259
|
+
expect(record.lastError).toContain("Recovery");
|
|
260
|
+
expect(record.lastError).toContain("action:'list'");
|
|
261
|
+
// 失败通知送达(Continuation 独立载荷——正文带失败摘要与恢复指引)
|
|
263
262
|
expect(pi.sendMessage).toHaveBeenCalled();
|
|
264
263
|
const sendMessageCalls = (pi.sendMessage as unknown as ReturnType<typeof vi.fn>).mock.calls as Array<[{ content?: string }]>;
|
|
265
264
|
const notifyContent = sendMessageCalls[0]?.[0]?.content ?? "";
|
|
266
265
|
expect(notifyContent).toContain("settled watchdog");
|
|
267
|
-
// [
|
|
268
|
-
|
|
269
|
-
|
|
266
|
+
// [H1 U6] 引擎侧终止意图(watchdog fire 的 cancel 受理断言)随 interact 面退役——
|
|
267
|
+
// fire 的真实 kill = killRoundChildForWatchdog(镜像置死)+ abort 轮 signal → cancel 帧。
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ── [S11] 残余守卫分支:cancel tombstone 幂等 / 池排队 abort / record-access 越界 ──
|
|
272
|
+
|
|
273
|
+
describe("S11 守卫分支组", () => {
|
|
274
|
+
let agentDir: string;
|
|
275
|
+
let service: SubagentService;
|
|
276
|
+
let store: RecordStore;
|
|
277
|
+
let pi: MockPi;
|
|
278
|
+
let fake: FakePiEnginePort;
|
|
279
|
+
|
|
280
|
+
beforeEach(() => {
|
|
281
|
+
vi.restoreAllMocks();
|
|
282
|
+
killChildSpy.mockClear();
|
|
283
|
+
({ agentDir, service, store, pi, fake } = setup());
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
afterEach(() => {
|
|
287
|
+
service.dispose();
|
|
288
|
+
clearEngines();
|
|
289
|
+
_resetLifecycleState();
|
|
290
|
+
_resetSettledWatchdogsForTest();
|
|
291
|
+
_resetCoreSpawnedChildrenMirrorForTest();
|
|
292
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("cancel tombstone 幂等:已终态 record 再 cancel → CAS 未抢到返回 false(stop 手段仍执行,零收尾副作用)", async () => {
|
|
296
|
+
const record = makeRecord({ id: "sa-tombstone" });
|
|
297
|
+
record.status = "closed";
|
|
298
|
+
record.closedReason = "cancelled";
|
|
299
|
+
store.register(record);
|
|
300
|
+
const emitSpy = vi.fn();
|
|
301
|
+
store.onChange(emitSpy);
|
|
302
|
+
|
|
303
|
+
const ok = service.cancel(record.id);
|
|
304
|
+
|
|
305
|
+
expect(ok).toBe(false); // CAS 未抢到:覆写终态 + tombstone 双标 + notify 双发不可执行
|
|
306
|
+
expect(record.status).toBe("closed"); // 终态不被覆写
|
|
307
|
+
expect(record.closedReason).toBe("cancelled");
|
|
308
|
+
// stop 手段无条件先执行(cancel 语义 = 进程必死,幂等无害)
|
|
309
|
+
expect(killChildSpy).toHaveBeenCalledWith(record.id, "cancelBackground");
|
|
310
|
+
// 收尾副作用归 CAS 赢家:无 markCancelled 落盘 entry(state-write 面)
|
|
311
|
+
const stateWrites = (pi.appendEntry as unknown as ReturnType<typeof vi.fn>).mock.calls.filter(
|
|
312
|
+
(c: unknown[]) => String(c[0]).includes("state-write-failed"),
|
|
313
|
+
);
|
|
314
|
+
expect(stateWrites).toHaveLength(0);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("排队被 abort 池分支:池满排队 + signal 已 aborted → finalizeAborted(cancelled 终态)", async () => {
|
|
318
|
+
const record = makeRecord({ id: "sa-pool-abort" });
|
|
319
|
+
store.register(record);
|
|
320
|
+
const ro = privateFn(service, "runOrchestration") as unknown as {
|
|
321
|
+
acquirePoolOrFinalize: (r: ExecutionRecord, s: AbortSignal | undefined, p: number) => Promise<AgentResult | undefined>;
|
|
322
|
+
};
|
|
323
|
+
// 先占满池(占满 effective = max(1, maxConcurrent - depth) 个槽位),迫使目标 record 排队
|
|
324
|
+
const pool = privateFn(service, "pool") as unknown as {
|
|
325
|
+
maxConcurrent: number;
|
|
326
|
+
acquire: (p: number, max?: number, s?: AbortSignal) => Promise<void>;
|
|
327
|
+
release: () => void;
|
|
328
|
+
};
|
|
329
|
+
const slots = Math.max(1, pool.maxConcurrent - record.depth);
|
|
330
|
+
for (let i = 0; i < slots; i++) await pool.acquire(0);
|
|
331
|
+
const controller = new AbortController();
|
|
332
|
+
controller.abort(); // 排队窗内 abort(pre-aborted signal → acquire reject AbortError)
|
|
333
|
+
|
|
334
|
+
const result = await ro.acquirePoolOrFinalize(record, controller.signal, 0);
|
|
335
|
+
for (let i = 0; i < slots; i++) pool.release();
|
|
336
|
+
|
|
337
|
+
// S1:排队中被 abort 走 cancelled(与已运行被 abort 一致),返回终态 result 供 early-return
|
|
338
|
+
expect(result).toBeDefined();
|
|
339
|
+
expect(record.status).toBe("closed");
|
|
340
|
+
expect(record.closedReason).toBe("cancelled");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("record-access 越界守卫:disposed 后 lookupRecordAnyState → undefined(不抛)", async () => {
|
|
344
|
+
service.dispose();
|
|
345
|
+
const record = service.queries.lookupRecordAnyState("any-id");
|
|
346
|
+
expect(record).toBeUndefined();
|
|
270
347
|
});
|
|
271
348
|
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// src/__tests__/subagent-service.test.ts
|
|
2
2
|
//
|
|
3
|
-
// SubagentService 生命周期 + 公共 API
|
|
3
|
+
// SubagentService 生命周期 + 公共 API 边界测试([H3/R5 测试归位] 本文件为壳级测试:
|
|
4
|
+
// 装配 / dispose 编排 / 转发面 / 跨域 emit 记账。run 域 execute 入口用例已迁
|
|
5
|
+
// run-orchestration.test.ts;ModelConfigService ctxModel 缓存已迁
|
|
6
|
+
// model-config-service.test.ts)。
|
|
4
7
|
//
|
|
5
8
|
// 范围:initSession / dispose / query / cancel / listRunning / collectRecords /
|
|
6
9
|
// onChange / assertReady -- 这些不依赖动态 import Pi SDK(getSdk)。
|
|
@@ -19,13 +22,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
19
22
|
|
|
20
23
|
import { createRecord } from "../execution-record.ts";
|
|
21
24
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
22
|
-
import type { ModelInfo
|
|
25
|
+
import type { ModelInfo } from "../model-resolver.ts";
|
|
23
26
|
import type { RecordStore } from "../record-store.ts";
|
|
24
27
|
import type { UiRequest, UiRequestHandler } from "../dialog-queue.ts";
|
|
25
28
|
import type { PiLike } from "../subagent-service.ts";
|
|
26
|
-
import {
|
|
29
|
+
import { SubagentService } from "../subagent-service.ts";
|
|
30
|
+
// [H3/R6] 单例访问器外移支撑文件 service/service-bootstrap.ts(壳不再导出)。
|
|
31
|
+
import { getSubagentService, setSubagentService } from "../service/service-bootstrap.ts";
|
|
27
32
|
import type { ExecutionRecord } from "../types.ts";
|
|
28
|
-
import type { WorktreeManager } from "../worktree-manager.ts";
|
|
29
33
|
|
|
30
34
|
// ── 工具:建临时 agentDir + 真实 ModelConfigService ──
|
|
31
35
|
|
|
@@ -331,9 +335,11 @@ describe("SubagentService", () => {
|
|
|
331
335
|
// private 字段,用 Reflect.get 取(与 R0-D 块访问 store 同模式)。
|
|
332
336
|
|
|
333
337
|
describe("dispose uiRequestHandler stub (dispose-cleanup)", () => {
|
|
334
|
-
/** 从 service 取出 private uiRequestHandler(trailing ui_request 实际调用入口)。
|
|
338
|
+
/** 从 service 取出 private uiRequestHandler(trailing ui_request 实际调用入口)。
|
|
339
|
+
* [R1 深绑改写] uiRequestHandler 随域 #2 聚合迁入 SessionBaselines,深绑路径改为
|
|
340
|
+
* service → baselines 聚合实例(断言对象与强度不变,路径对齐终态结构)。 */
|
|
335
341
|
function getHandler(service: SubagentService): UiRequestHandler | undefined {
|
|
336
|
-
return Reflect.get(service, "
|
|
342
|
+
return (Reflect.get(service, "baselines") as { uiRequestHandler: UiRequestHandler | undefined }).uiRequestHandler;
|
|
337
343
|
}
|
|
338
344
|
|
|
339
345
|
/** 最小 UiRequest(method 无关紧要——stub 不论 method 一律返回 cancelled)。 */
|
|
@@ -387,136 +393,11 @@ describe("SubagentService", () => {
|
|
|
387
393
|
});
|
|
388
394
|
|
|
389
395
|
// ============================================================
|
|
390
|
-
// execute() worktree 路径(worktree 与 fork
|
|
396
|
+
// execute() worktree 路径(worktree 与 fork 解耦)
|
|
397
|
+
// → [H3/R5 测试归位] 整块迁至 run-orchestration.test.ts(run 域聚合同名域测试
|
|
398
|
+
// 文件;用例名与断言零改动)。此处不留转发桩——vitest 按文件发现,无注册面。
|
|
391
399
|
// ============================================================
|
|
392
|
-
//
|
|
393
|
-
// worktree(文件隔离)与 fork(上下文继承)已解耦:worktree:true 可独立于 fork 工作
|
|
394
|
-
// (worktreeManager.create 只看 opts.worktree,不读 fork)。此组验证三种 fork/worktree
|
|
395
|
-
// 组合下 worktree 路径的行为(均不应抛 'requires fork'——该 guard 已移除):
|
|
396
|
-
// 1. worktree:true + fork:false → 解耦后正常(创建 worktree 路径,不抛 requires fork)
|
|
397
|
-
// 2. worktree:true + fork:true → 创建 worktree 路径(测试环境 git 失败,抛非 requires fork 错)
|
|
398
|
-
// 3. worktree:false + fork:false → 默认路径(不创建 worktree)
|
|
399
|
-
//
|
|
400
|
-
// 本文件不 mock spawn(保持与文件头声明一致——execute 集成测试在 execute-nesting /
|
|
401
|
-
// run-spawn-integration),因此 case 验证「不抛 requires fork」而非「执行完成」:
|
|
402
|
-
// 执行越过 worktree 创建后在后续步骤(worktreeManager.create 调 git / runSpawn 调
|
|
403
|
-
// spawn)抛与 fork/worktree 无关的错。用 try/catch 断言抛出的不是 requires fork。
|
|
404
|
-
|
|
405
|
-
describe("execute() worktree 路径(worktree 与 fork 解耦)", () => {
|
|
406
|
-
/** 构造已就绪的 service(initSession + initModel 注入 ctxModel,使 resolveIdentity 不因 model 拗错)。 */
|
|
407
|
-
function makeReadyService(): SubagentService {
|
|
408
|
-
const service = new SubagentService({ cwd: agentDir, modelService });
|
|
409
|
-
service.initSession({ pi: makePi(), sessionId: "s1" });
|
|
410
|
-
// 注入 modelRegistry + ctxModel:让 resolveIdentity 越过 resolveModel,
|
|
411
|
-
// 使 guard 之后的失败点稳定在 worktreeManager.create(git)或 runSpawn(spawn),
|
|
412
|
-
// 而非 modelService.resolveModel——避免与 guard 无关的 model 错误掩盖被测点。
|
|
413
|
-
modelService.initModel({
|
|
414
|
-
modelRegistry: {
|
|
415
|
-
getAvailable: () => [],
|
|
416
|
-
find: () => undefined,
|
|
417
|
-
hasConfiguredAuth: () => false,
|
|
418
|
-
},
|
|
419
|
-
sessionId: "s1",
|
|
420
|
-
ctxModel: { id: "ctx-model", name: "Ctx", provider: "p", reasoning: false },
|
|
421
|
-
});
|
|
422
|
-
return service;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
it("worktree:true + fork:false → 解耦后不抛 'requires fork'(worktree 独立于 fork)", async () => {
|
|
426
|
-
const service = makeReadyService();
|
|
427
|
-
// 解耦后 worktree:true+fork:false 不再 throw requires fork(worktreeManager.create 只看 worktree)
|
|
428
|
-
try {
|
|
429
|
-
await service.execute({
|
|
430
|
-
task: "worktree without fork (decoupled)",
|
|
431
|
-
slug: "test",
|
|
432
|
-
worktree: true,
|
|
433
|
-
fork: false,
|
|
434
|
-
ctxModel: { id: "ctx-model", name: "Ctx", provider: "p", reasoning: false },
|
|
435
|
-
});
|
|
436
|
-
} catch (err) {
|
|
437
|
-
// 解耦后绝不抛 requires fork(执行继续到 worktreeManager.create/spawn 才可能抛其他错)
|
|
438
|
-
expect((err as Error).message).not.toMatch(/requires fork/);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
it("worktree:true + fork:true → 创建 worktree 路径(不抛 'requires fork')", async () => {
|
|
443
|
-
const service = makeReadyService();
|
|
444
|
-
// 执行继续:先创建 record,然后 worktreeManager.create 调 git(测试环境无 repo → 抛与 fork 无关的错)
|
|
445
|
-
try {
|
|
446
|
-
await service.execute({
|
|
447
|
-
task: "worktree with fork",
|
|
448
|
-
slug: "test",
|
|
449
|
-
worktree: true,
|
|
450
|
-
fork: true,
|
|
451
|
-
ctxModel: { id: "ctx-model", name: "Ctx", provider: "p", reasoning: false },
|
|
452
|
-
});
|
|
453
|
-
// 若未抛(理论上 worktreeManager.create 在某些环境成功)也 OK——重点是没命中 guard
|
|
454
|
-
} catch (err) {
|
|
455
|
-
// guard 放行:抛出的错误绝不能是 "requires fork"
|
|
456
|
-
expect((err as Error).message).not.toMatch(/requires fork/);
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
it("worktree:false + fork:false → 默认路径(不创建 worktree,不抛 'requires fork')", async () => {
|
|
461
|
-
const service = makeReadyService();
|
|
462
|
-
// 默认路径:runSpawn 调 child_process.spawn(测试环境无真实 pi → 抛与 fork 无关的错)
|
|
463
|
-
try {
|
|
464
|
-
await service.execute({
|
|
465
|
-
task: "default path",
|
|
466
|
-
slug: "test",
|
|
467
|
-
worktree: false,
|
|
468
|
-
fork: false,
|
|
469
|
-
ctxModel: { id: "ctx-model", name: "Ctx", provider: "p", reasoning: false },
|
|
470
|
-
});
|
|
471
|
-
} catch (err) {
|
|
472
|
-
// guard 放行:抛出的错误绝不能是 "requires fork"
|
|
473
|
-
expect((err as Error).message).not.toMatch(/requires fork/);
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
400
|
|
|
477
|
-
// ============================================================
|
|
478
|
-
// create-await 竞态守卫(Phase 2):create 的 await 窗口内 dispose/cancel
|
|
479
|
-
// 可把 record CAS 成 closed——守卫须主动 cleanup + early-failed 返回,不 kickOff。
|
|
480
|
-
// 实现约束固化:「赋值 record.worktreeHandle → 终态检查 → 轮次 kick-off」
|
|
481
|
-
// 必须同一同步段(中间禁止 await),本用例即该不变量的回归锚点。
|
|
482
|
-
// ============================================================
|
|
483
|
-
it("守卫:create await 窗口内 dispose 抢先 → cleanup 被调 + early-failed 返回(不 kickOff)", async () => {
|
|
484
|
-
const service = makeReadyService();
|
|
485
|
-
const wtm = Reflect.get(service, "worktreeManager") as WorktreeManager;
|
|
486
|
-
const handle = Object.freeze({
|
|
487
|
-
path: "/tmp/wt-guard",
|
|
488
|
-
branch: "pi-sub-guard",
|
|
489
|
-
baseCommit: "abc123",
|
|
490
|
-
mainCwd: "/repo",
|
|
491
|
-
}) as Parameters<WorktreeManager["cleanup"]>[0];
|
|
492
|
-
let resolveCreate!: (h: unknown) => void;
|
|
493
|
-
vi.spyOn(wtm, "create").mockImplementation(
|
|
494
|
-
() => new Promise<unknown>((r) => { resolveCreate = r; }) as ReturnType<WorktreeManager["create"]>,
|
|
495
|
-
);
|
|
496
|
-
const cleanupSpy = vi.spyOn(wtm, "cleanup").mockResolvedValue(undefined);
|
|
497
|
-
|
|
498
|
-
const execP = service.execute({
|
|
499
|
-
task: "guard test",
|
|
500
|
-
slug: "test",
|
|
501
|
-
worktree: true,
|
|
502
|
-
fork: false,
|
|
503
|
-
ctxModel: { id: "ctx-model", name: "Ctx", provider: "p", reasoning: false },
|
|
504
|
-
});
|
|
505
|
-
// 微任务推进:record 已建 + execute 挂在 pending create 上
|
|
506
|
-
await new Promise((r) => { setTimeout(r, 0); });
|
|
507
|
-
// dispose 抢先(无需 record id):CAS running → closed,此时 worktreeHandle 仍
|
|
508
|
-
// undefined(dispose 的 fire-and-forget cleanup 跳过)——守卫是唯一的清理点
|
|
509
|
-
service.disposeAllRecords("parent-shutdown");
|
|
510
|
-
resolveCreate(handle);
|
|
511
|
-
|
|
512
|
-
const ret = await execP;
|
|
513
|
-
// 守卫生效:handle 被主动清理(不等 60s reaper)
|
|
514
|
-
expect(cleanupSpy).toHaveBeenCalledWith(handle);
|
|
515
|
-
// 返回 early-failed 形态(details.status 已 closed),而非 kickOff 的 running 形态
|
|
516
|
-
expect(ret.mode).toBe("background");
|
|
517
|
-
expect(ret.details).toMatchObject({ status: "closed" });
|
|
518
|
-
});
|
|
519
|
-
});
|
|
520
401
|
//
|
|
521
402
|
// [背景] PR #82 在 subagent-service.ts 新增 emitPendingRegister/Unregister 调用,
|
|
522
403
|
// 5 个 emit 点:register(execute L309)、unregister(failed)(finalizeFailed 经
|
|
@@ -698,88 +579,9 @@ describe("SubagentService", () => {
|
|
|
698
579
|
|
|
699
580
|
// ============================================================
|
|
700
581
|
// ModelConfigService ctxModel 缓存(renderCall 标题行 model 显示的核心)
|
|
582
|
+
// → [H3/R5 测试归位] 整块迁至 model-config-service.test.ts(被测主体 =
|
|
583
|
+
// ModelConfigService 模块,非 SubagentService;用例名与断言零改动)。
|
|
701
584
|
// ============================================================
|
|
702
|
-
//
|
|
703
|
-
// [HISTORICAL] 99f20da1e 后 renderCall 拿不到主 agent model(ToolRenderContext 无 model),
|
|
704
|
-
// resolveModel 第三层拗错→降级不显示 model。修复:session_start 缓存 ctxModel,
|
|
705
|
-
// resolveModel 第三参默认用缓存。此测试验证该透传链路。
|
|
706
|
-
|
|
707
|
-
describe("ModelConfigService ctxModel 缓存", () => {
|
|
708
|
-
let agentDir: string;
|
|
709
|
-
|
|
710
|
-
beforeEach(() => {
|
|
711
|
-
agentDir = makeTmpAgentDir();
|
|
712
|
-
});
|
|
713
|
-
afterEach(() => {
|
|
714
|
-
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
/** 最小 mock registry:空可用列表(ctxModel 路径不需要 lookup)。 */
|
|
718
|
-
function makeEmptyRegistry(): ModelRegistryLike {
|
|
719
|
-
return {
|
|
720
|
-
getAvailable: () => [],
|
|
721
|
-
find: () => undefined,
|
|
722
|
-
hasConfiguredAuth: () => false,
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
it("initModel 传 ctxModel 后,resolveModel 不传第三参返回缓存 model", () => {
|
|
727
|
-
const svc = makeModelService(agentDir);
|
|
728
|
-
const mainModel: ModelInfo = {
|
|
729
|
-
id: "main-model",
|
|
730
|
-
name: "Main",
|
|
731
|
-
provider: "anthropic",
|
|
732
|
-
reasoning: false,
|
|
733
|
-
};
|
|
734
|
-
svc.initModel({
|
|
735
|
-
modelRegistry: makeEmptyRegistry(),
|
|
736
|
-
sessionId: "sess-1",
|
|
737
|
-
ctxModel: mainModel,
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
// agent 无 model 声明 + 无 override → 走第三层 ctxModel 缓存
|
|
741
|
-
const r = svc.resolveModel("general-purpose");
|
|
742
|
-
expect(r.model).toBe(mainModel);
|
|
743
|
-
expect(r.model.provider).toBe("anthropic");
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
it("显式 ctxModel 参数优先于缓存(execute 路径覆盖 renderCall 缓存)", () => {
|
|
747
|
-
const svc = makeModelService(agentDir);
|
|
748
|
-
const cached: ModelInfo = { id: "cached", name: "C", provider: "p1", reasoning: false };
|
|
749
|
-
const explicit: ModelInfo = { id: "explicit", name: "E", provider: "p2", reasoning: false };
|
|
750
|
-
svc.initModel({
|
|
751
|
-
modelRegistry: makeEmptyRegistry(),
|
|
752
|
-
sessionId: "sess-1",
|
|
753
|
-
ctxModel: cached,
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
// execute 传显式 ctxModel → 用显式,不用缓存
|
|
757
|
-
const r = svc.resolveModel("general-purpose", undefined, explicit);
|
|
758
|
-
expect(r.model).toBe(explicit);
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
it("setCtxModel 刷新缓存(model_select 后 renderCall 能看到新 model)", () => {
|
|
762
|
-
const svc = makeModelService(agentDir);
|
|
763
|
-
const m1: ModelInfo = { id: "m1", name: "1", provider: "p", reasoning: false };
|
|
764
|
-
const m2: ModelInfo = { id: "m2", name: "2", provider: "p", reasoning: false };
|
|
765
|
-
svc.initModel({ modelRegistry: makeEmptyRegistry(), sessionId: "s", ctxModel: m1 });
|
|
766
|
-
|
|
767
|
-
expect(svc.resolveModel("general-purpose").model).toBe(m1);
|
|
768
|
-
svc.setCtxModel(m2); // 模拟 model_select 刷新
|
|
769
|
-
expect(svc.resolveModel("general-purpose").model).toBe(m2);
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
it("缓存为空且无 override/agentConfig.model → 拗错(不静默降级)", () => {
|
|
773
|
-
const svc = makeModelService(agentDir);
|
|
774
|
-
svc.initModel({
|
|
775
|
-
modelRegistry: makeEmptyRegistry(),
|
|
776
|
-
sessionId: "s",
|
|
777
|
-
// ctxModel 不传 → 缓存为空
|
|
778
|
-
});
|
|
779
|
-
// 空 registry + 空 ctxModel → 第三层不可用 → 拗错
|
|
780
|
-
expect(() => svc.resolveModel("general-purpose")).toThrow(/No available model/);
|
|
781
|
-
});
|
|
782
|
-
});
|
|
783
585
|
|
|
784
586
|
// ============================================================
|
|
785
587
|
// execute() 集成测试 — 已由 execute-integration.test.ts 覆盖
|