@zhushanwen/subagent-core 0.2.0 → 0.4.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/README.md +45 -2
- package/agents/analyst.md +60 -0
- package/agents/coder.md +69 -0
- package/agents/debugger.md +66 -0
- package/agents/doc-reviewer.md +49 -0
- package/agents/explorer.md +63 -0
- package/agents/general-purpose.md +32 -0
- package/agents/orchestrator.md +61 -0
- package/agents/planner.md +53 -0
- package/agents/researcher.md +67 -0
- package/agents/reviewer.md +73 -0
- package/dist/chunk-4KN4TTG7.js +240 -0
- package/dist/chunk-APZY4IME.js +27 -0
- package/dist/chunk-X7SZ5HLQ.js +66 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
- package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
- package/dist/execution/engine/engines/zcode/constants.js +43 -1
- package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
- package/dist/execution/engine/engines/zcode/reader.js +4 -234
- package/dist/execution/engine/paths.js +7 -19
- package/dist/index.cjs +14709 -100
- package/dist/index.d.cts +4053 -200
- package/dist/index.d.ts +4053 -200
- package/dist/index.js +14358 -102
- package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
- package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
- package/package.json +6 -3
- package/src/__tests__/review-fix-loop-script.test.ts +90 -2
- package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
- package/src/__tests__/smoke.test.ts +9 -2
- package/src/execution/__tests__/agent-profile.test.ts +232 -0
- package/src/execution/__tests__/agents-assembly.test.ts +219 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
- package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
- package/src/execution/__tests__/delivery-methods.test.ts +2 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
- package/src/execution/__tests__/dialog-queue.test.ts +199 -1
- package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
- package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
- package/src/execution/__tests__/finalize-record.test.ts +106 -0
- package/src/execution/__tests__/gc-timer.test.ts +2 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
- package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
- package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
- package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
- package/src/execution/__tests__/notify-ledger.test.ts +294 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
- package/src/execution/__tests__/record-store.test.ts +83 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
- package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
- package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
- package/src/execution/__tests__/session-pending.test.ts +228 -0
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
- package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
- package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
- package/src/execution/__tests__/spawned-children.test.ts +153 -10
- package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
- package/src/execution/__tests__/timeout-integration.test.ts +7 -7
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
- package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
- package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
- package/src/execution/agent-registry.ts +225 -1
- package/src/execution/agents-assembly.ts +88 -0
- package/src/execution/concurrency-pool.ts +71 -9
- package/src/execution/dialog-queue.ts +101 -3
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
- package/src/execution/engine/__tests__/registry.test.ts +90 -1
- package/src/execution/engine/engine-discovery.ts +12 -16
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
- package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
- package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
- package/src/execution/engine/engines/zcode/connection.ts +585 -0
- package/src/execution/engine/engines/zcode/constants.ts +129 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
- package/src/execution/engine/engines/zcode/preparer.ts +25 -22
- package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
- package/src/execution/engine/host-task-spec.ts +3 -3
- package/src/execution/engine/port.ts +41 -1
- package/src/execution/engine/registry.ts +53 -0
- package/src/execution/finalize-record.ts +63 -5
- package/src/execution/get-state-handshake.ts +85 -10
- package/src/execution/lifecycle-manager.ts +27 -3
- package/src/execution/manifest-store.ts +53 -62
- package/src/execution/notifier.ts +10 -10
- package/src/execution/notify-ledger.ts +117 -16
- package/src/execution/record-entry.ts +8 -2
- package/src/execution/record-store.ts +29 -15
- package/src/execution/session-pending.ts +213 -62
- package/src/execution/session-runner.ts +974 -128
- package/src/execution/sessions-index.ts +10 -55
- package/src/execution/settled-watchdog.ts +99 -0
- package/src/execution/subagent-actions-core.ts +686 -0
- package/src/execution/subagent-service.ts +396 -49
- package/src/execution/ui-request-handler-factory.ts +27 -4
- package/src/execution/worktree-git-ops.ts +397 -0
- package/src/execution/worktree-manager.ts +72 -10
- package/src/execution/worktree-registry.ts +6 -12
- package/src/index.ts +413 -6
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
- package/src/orchestration/__tests__/args-meta.test.ts +358 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
- package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
- package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
- package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
- package/src/orchestration/__tests__/script-generate.test.ts +314 -0
- package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
- package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
- package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
- package/src/orchestration/agent-opts-resolver.ts +7 -7
- package/src/orchestration/args-meta.ts +198 -0
- package/src/orchestration/error-recovery.ts +416 -146
- package/src/orchestration/execute-agent-call.ts +9 -9
- package/src/orchestration/file-run-store.ts +327 -0
- package/src/orchestration/launcher.ts +35 -26
- package/src/orchestration/lifecycle.ts +355 -79
- package/src/orchestration/models/agent-call.ts +7 -7
- package/src/orchestration/models/budget.ts +5 -5
- package/src/orchestration/models/run-runtime.ts +13 -13
- package/src/orchestration/models/trace.ts +8 -8
- package/src/orchestration/models/workflow-run.ts +27 -27
- package/src/orchestration/models/workflow-script.ts +4 -4
- package/src/orchestration/run-snapshot.ts +266 -0
- package/src/orchestration/script-generate.ts +154 -0
- package/src/orchestration/script-lint.ts +33 -33
- package/src/orchestration/worker-handle.ts +10 -10
- package/src/orchestration/worker-host.ts +10 -10
- package/src/orchestration/worker-script-builder.ts +401 -346
- package/src/orchestration/workflow-files.ts +37 -11
- package/src/orchestration/workflow-run-summary.ts +69 -0
- package/src/orchestration/workflow-script-registry-impl.ts +31 -9
- package/src/shared/__tests__/agent-ref.test.ts +209 -2
- package/src/shared/__tests__/atomic-write.test.ts +267 -0
- package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
- package/src/shared/__tests__/injection-render.test.ts +518 -0
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
- package/src/shared/__tests__/resource-discovery.test.ts +3 -2
- package/src/shared/agent-ref.ts +143 -2
- package/src/shared/atomic-write.ts +320 -0
- package/src/shared/bounded-serialize.ts +154 -0
- package/src/shared/injection-render.ts +279 -0
- package/src/shared/meta-parser.ts +41 -1
- package/src/shared/resource-discovery.ts +104 -37
- package/src/shared/resource-meta.ts +14 -0
- package/src/shared/xml-injection.ts +9 -9
- package/workflows/README.md +9 -9
- package/workflows/chain.js +4 -2
- package/workflows/map-reduce.js +5 -3
- package/workflows/parallel.js +5 -3
- package/workflows/review-fix-loop-utils.cjs +14 -3
- package/workflows/review-fix-loop.js +18 -7
- package/workflows/scatter-gather.js +4 -2
- package/dist/chunk-3VOERJPJ.js +0 -22
|
@@ -27,6 +27,8 @@ vi.mock("../session-runner.ts", () => {
|
|
|
27
27
|
return {
|
|
28
28
|
runSpawn: vi.fn(),
|
|
29
29
|
killAllSpawnedChildren: vi.fn(),
|
|
30
|
+
registerSpawnedChildForRecord: vi.fn(),
|
|
31
|
+
killRecordChildWithEscalation: vi.fn(),
|
|
30
32
|
spawnedChildren,
|
|
31
33
|
getChildByRecord: (id: string): unknown => spawnedChildren.get(id),
|
|
32
34
|
};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// src/execution/__tests__/execution-runtime-face.test.ts
|
|
2
|
+
//
|
|
3
|
+
// U10① execution 运行时面导出形态测试 = 第三宿主最小构造示例(验收证据)。
|
|
4
|
+
//
|
|
5
|
+
// 示例语义(设计 docs/design/subagent-core-sink-design.md §3.3 D6 / §4 S5):第三宿主
|
|
6
|
+
// 仅凭模块导出面(不看 core 内部实现)完成——
|
|
7
|
+
// 1. createSubagentService({cwd, modelService, ...}) 参数注入构造(无全局查找);
|
|
8
|
+
// 2. record 状态查询面调用(SubagentService.lookupRecordAnyState 按 id 全态查询 +
|
|
9
|
+
// RecordStore 按状态枚举 listRunning/collectRecords 与按 id findLightById/getFullRecord);
|
|
10
|
+
// 3. 错误类型族 instanceof 分流。
|
|
11
|
+
//
|
|
12
|
+
// 构造先例同 subagent-service.test.ts:真实 ModelConfigService 指向 tmpdir 空目录
|
|
13
|
+
// (loadGlobalConfig 对不存在文件返回默认配置,AgentRegistry 空目录安全),不 mock
|
|
14
|
+
// service 内核;pi 仅在 initSession 场景以最小 duck-typed mock 注入。
|
|
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, vi } from "vitest";
|
|
21
|
+
|
|
22
|
+
// MF-4:barrel 可达性用例的命名空间导入——「仅凭 src/index.ts 导入」的字面载体
|
|
23
|
+
//(不经深路径),证明第三宿主按文档用法可构造。
|
|
24
|
+
import * as subagentCore from "../../index.ts";
|
|
25
|
+
import { EngineError } from "../engine/common/errors.ts";
|
|
26
|
+
import { EngineNotFoundError } from "../engine/registry.ts";
|
|
27
|
+
import { ZcodePrepareError } from "../engine/engines/zcode/preparer.ts";
|
|
28
|
+
import { ZcodeReaderError } from "../engine/engines/zcode/reader.ts";
|
|
29
|
+
import { ZcodeTaskShapeError } from "../engine/engines/zcode/zcode-engine.ts";
|
|
30
|
+
import { ModelConfigService } from "../model-config-service.ts";
|
|
31
|
+
import {
|
|
32
|
+
SUBAGENT_RECORD_CUSTOM_TYPE,
|
|
33
|
+
toSubagentRecordEntry,
|
|
34
|
+
} from "../record-entry.ts";
|
|
35
|
+
import { RecordStore } from "../record-store.ts";
|
|
36
|
+
import { createSubagentService, getSubagentService } from "../subagent-service.ts";
|
|
37
|
+
import {
|
|
38
|
+
DirtyWorktreeError,
|
|
39
|
+
ForkDepthExceededError,
|
|
40
|
+
ResurrectDeniedError,
|
|
41
|
+
} from "../types.ts";
|
|
42
|
+
import { GitRunError as WorktreeManagerGitRunError } from "../worktree-manager.ts";
|
|
43
|
+
import { GitRunError as WorktreeGitOpsGitRunError } from "../worktree-git-ops.ts";
|
|
44
|
+
|
|
45
|
+
// ── 工具:tmpdir agentDir + 真实 ModelConfigService(第三宿主自备的最小依赖集)──
|
|
46
|
+
|
|
47
|
+
function makeAgentDir(): string {
|
|
48
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "exec-runtime-face-"));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeModelService(agentDir: string): ModelConfigService {
|
|
52
|
+
// cwd 为 ModelConfigServiceInit 必填键(与 agentDir 同指 tmp 目录;构造期仅消费 agentDir)
|
|
53
|
+
return new ModelConfigService({ agentDir, cwd: agentDir });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 最小 PiLike duck-type(initSession 场景用;本文件不触发真实 pi 调用)。 */
|
|
57
|
+
function makePi() {
|
|
58
|
+
return {
|
|
59
|
+
appendEntry: vi.fn(),
|
|
60
|
+
events: { emit: vi.fn() },
|
|
61
|
+
sendMessage: vi.fn(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("execution 运行时面(U10① D6)", () => {
|
|
66
|
+
let agentDir: string;
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
agentDir = makeAgentDir();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ============================================================
|
|
77
|
+
// ① createSubagentService 工厂:参数注入构造(第三宿主最小构造示例主体)
|
|
78
|
+
// ============================================================
|
|
79
|
+
describe("createSubagentService 工厂(参数注入构造)", () => {
|
|
80
|
+
it("仅凭参数注入构造成功,不读写全局单例槽位(无全局查找)", () => {
|
|
81
|
+
// 前置:全局槽位为空(工厂与 getSubagentService/setSubagentService 单例流程解耦)
|
|
82
|
+
expect(getSubagentService()).toBeNull();
|
|
83
|
+
|
|
84
|
+
const service = createSubagentService({
|
|
85
|
+
cwd: agentDir,
|
|
86
|
+
modelService: makeModelService(agentDir),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 构造产物可用(实例方法存在)且未被登记进全局单例槽位——
|
|
90
|
+
// session_start 单例流程行为零改动,宿主自持实例。
|
|
91
|
+
expect(typeof service.lookupRecordAnyState).toBe("function");
|
|
92
|
+
expect(getSubagentService()).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("构造后即可调用按 id 全态查询面:空 store 返回 undefined(未 initSession 按「不存在」处理)", () => {
|
|
96
|
+
const service = createSubagentService({
|
|
97
|
+
cwd: agentDir,
|
|
98
|
+
modelService: makeModelService(agentDir),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// lookupRecordAnyState 契约:未初始化/disposed 时 assertReady 抛错 → 按「不存在」处理返回 undefined
|
|
102
|
+
expect(service.lookupRecordAnyState("no-such-record")).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("initSession(宿主注入最小 pi mock)后查询面同型可用", () => {
|
|
106
|
+
const service = createSubagentService({
|
|
107
|
+
cwd: agentDir,
|
|
108
|
+
modelService: makeModelService(agentDir),
|
|
109
|
+
});
|
|
110
|
+
service.initSession({ pi: makePi(), sessionId: "s1" });
|
|
111
|
+
|
|
112
|
+
expect(service.lookupRecordAnyState("no-such-record")).toBeUndefined();
|
|
113
|
+
service.dispose();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ============================================================
|
|
118
|
+
// ② record 状态查询面:按状态枚举 / 按 id(RecordStore 公共构造)
|
|
119
|
+
// ============================================================
|
|
120
|
+
describe("record 状态查询面(RecordStore)", () => {
|
|
121
|
+
it("按状态枚举查询:空 store 下 listRunning 与 collectRecords 返回空数组", () => {
|
|
122
|
+
const sessionsDir = path.join(agentDir, "sessions");
|
|
123
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
124
|
+
const store = new RecordStore(sessionsDir);
|
|
125
|
+
|
|
126
|
+
expect(store.listRunning()).toEqual([]);
|
|
127
|
+
expect(store.collectRecords(10, "all", undefined)).toEqual([]);
|
|
128
|
+
expect(store.collectRecords(10, "running", undefined)).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("按 id 查询:findLightById / getFullRecord / getMutable 对缺失 id 均返回 undefined", () => {
|
|
132
|
+
const sessionsDir = path.join(agentDir, "sessions");
|
|
133
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
134
|
+
const store = new RecordStore(sessionsDir);
|
|
135
|
+
|
|
136
|
+
expect(store.findLightById("missing-id")).toBeUndefined();
|
|
137
|
+
expect(store.getFullRecord("missing-id")).toBeUndefined();
|
|
138
|
+
expect(store.getMutable("missing-id")).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ============================================================
|
|
143
|
+
// ③ 错误类型族:instanceof 分流(导出形态 = 宿主可程序化判别)
|
|
144
|
+
// ============================================================
|
|
145
|
+
describe("错误类型族 instanceof", () => {
|
|
146
|
+
it("EngineError:code 前缀 message + 结构化投影", () => {
|
|
147
|
+
const err = new EngineError("model_not_available", "no provider", "pick another model");
|
|
148
|
+
expect(err).toBeInstanceOf(Error);
|
|
149
|
+
expect(err).toBeInstanceOf(EngineError);
|
|
150
|
+
expect(err.message).toBe("model_not_available: no provider");
|
|
151
|
+
expect(err.toStructured()).toEqual({
|
|
152
|
+
code: "model_not_available",
|
|
153
|
+
message: "model_not_available: no provider",
|
|
154
|
+
recovery: "pick another model",
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("EngineNotFoundError:携带 engineId / registered / source 供分流", () => {
|
|
159
|
+
const err = new EngineNotFoundError("nope", ["pi", "zcode"], "agent.md");
|
|
160
|
+
expect(err).toBeInstanceOf(Error);
|
|
161
|
+
expect(err).toBeInstanceOf(EngineNotFoundError);
|
|
162
|
+
expect(err.code).toBe("engine_not_found");
|
|
163
|
+
expect(err.engineId).toBe("nope");
|
|
164
|
+
expect(err.registered).toEqual(["pi", "zcode"]);
|
|
165
|
+
expect(err.source).toBe("agent.md");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("zcode 引擎错误族:ZcodePrepareError / ZcodeReaderError / ZcodeTaskShapeError", () => {
|
|
169
|
+
const prepare = new ZcodePrepareError("model_not_available", "model gone");
|
|
170
|
+
expect(prepare).toBeInstanceOf(Error);
|
|
171
|
+
expect(prepare).toBeInstanceOf(ZcodePrepareError);
|
|
172
|
+
expect(prepare.code).toBe("model_not_available");
|
|
173
|
+
|
|
174
|
+
const reader = new ZcodeReaderError("db missing", "check journal");
|
|
175
|
+
expect(reader).toBeInstanceOf(Error);
|
|
176
|
+
expect(reader).toBeInstanceOf(ZcodeReaderError);
|
|
177
|
+
|
|
178
|
+
// [U10①] 原 class 未导出(仅内部 throw),本断言钉住 export 补齐后宿主可 instanceof
|
|
179
|
+
const shape = new ZcodeTaskShapeError("capability_unsupported", "no schema");
|
|
180
|
+
expect(shape).toBeInstanceOf(Error);
|
|
181
|
+
expect(shape).toBeInstanceOf(ZcodeTaskShapeError);
|
|
182
|
+
expect(shape.code).toBe("capability_unsupported");
|
|
183
|
+
expect(shape.name).toBe("ZcodeTaskShapeError");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("执行守卫错误族:ResurrectDeniedError / ForkDepthExceededError / DirtyWorktreeError", () => {
|
|
187
|
+
const resurrect = new ResurrectDeniedError();
|
|
188
|
+
expect(resurrect).toBeInstanceOf(Error);
|
|
189
|
+
expect(resurrect).toBeInstanceOf(ResurrectDeniedError);
|
|
190
|
+
|
|
191
|
+
const forkDepth = new ForkDepthExceededError("depth > max");
|
|
192
|
+
expect(forkDepth).toBeInstanceOf(Error);
|
|
193
|
+
expect(forkDepth).toBeInstanceOf(ForkDepthExceededError);
|
|
194
|
+
|
|
195
|
+
const dirty = new DirtyWorktreeError("uncommitted changes");
|
|
196
|
+
expect(dirty).toBeInstanceOf(Error);
|
|
197
|
+
expect(dirty).toBeInstanceOf(DirtyWorktreeError);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("GitRunError 两处同名列(worktree-manager / worktree-git-ops)各自可 instanceof", () => {
|
|
201
|
+
const fromManager = new WorktreeManagerGitRunError("git fail", { exitCode: 1 });
|
|
202
|
+
expect(fromManager).toBeInstanceOf(Error);
|
|
203
|
+
expect(fromManager).toBeInstanceOf(WorktreeManagerGitRunError);
|
|
204
|
+
expect(fromManager).not.toBeInstanceOf(WorktreeGitOpsGitRunError);
|
|
205
|
+
|
|
206
|
+
const fromGitOps = new WorktreeGitOpsGitRunError("git fail", { exitCode: 128 });
|
|
207
|
+
expect(fromGitOps).toBeInstanceOf(Error);
|
|
208
|
+
expect(fromGitOps).toBeInstanceOf(WorktreeGitOpsGitRunError);
|
|
209
|
+
expect(fromGitOps).not.toBeInstanceOf(WorktreeManagerGitRunError);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ============================================================
|
|
214
|
+
// ④ record-entry 自描述 entry 面(导出形态确认;投影逐字段等值由
|
|
215
|
+
// execution-record.test.ts 钉住,此处不重复)
|
|
216
|
+
// ============================================================
|
|
217
|
+
describe("record-entry 面", () => {
|
|
218
|
+
it("SUBAGENT_RECORD_CUSTOM_TYPE 值契约:'subagent-record'", () => {
|
|
219
|
+
expect(SUBAGENT_RECORD_CUSTOM_TYPE).toBe("subagent-record");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("toSubagentRecordEntry 可达且对最小 SubagentRecord 投影出 v===1", () => {
|
|
223
|
+
const entry = toSubagentRecordEntry({
|
|
224
|
+
id: "r1",
|
|
225
|
+
agent: "a.md",
|
|
226
|
+
task: "t",
|
|
227
|
+
slug: "s",
|
|
228
|
+
status: "running",
|
|
229
|
+
mode: "background",
|
|
230
|
+
startedAt: 1,
|
|
231
|
+
rootSessionId: undefined,
|
|
232
|
+
parentRecordId: undefined,
|
|
233
|
+
depth: 0,
|
|
234
|
+
endedAt: undefined,
|
|
235
|
+
turns: 0,
|
|
236
|
+
totalTokens: 0,
|
|
237
|
+
model: "m",
|
|
238
|
+
thinkingLevel: undefined,
|
|
239
|
+
eventLog: [],
|
|
240
|
+
displayItems: [],
|
|
241
|
+
});
|
|
242
|
+
expect(entry.v).toBe(1);
|
|
243
|
+
expect(entry.id).toBe("r1");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ============================================================
|
|
248
|
+
// ⑤ barrel 可达性(MF-4):仅凭 src/index.ts 导入完成第三宿主构造
|
|
249
|
+
// (SubagentServiceInit.modelService: ModelConfigService——构造依赖
|
|
250
|
+
// 与 jsdoc 示例引用的访问器必须全部从 barrel 可达)
|
|
251
|
+
// ============================================================
|
|
252
|
+
describe("barrel 可达性(仅凭 src/index.ts 导入,MF-4)", () => {
|
|
253
|
+
it("ModelConfigService / getModelConfigService / createSubagentService 从 barrel 可达并可构造", () => {
|
|
254
|
+
// 符号面:类与访问器均为函数(导出存在性)
|
|
255
|
+
expect(typeof subagentCore.ModelConfigService).toBe("function");
|
|
256
|
+
expect(typeof subagentCore.getModelConfigService).toBe("function");
|
|
257
|
+
|
|
258
|
+
// 进程单例槽位未被本文件写入(同 ① 的全局查找断言口径)
|
|
259
|
+
expect(subagentCore.getModelConfigService()).toBeNull();
|
|
260
|
+
|
|
261
|
+
// 运行时可达:仅凭 barrel 符号完成参数注入构造(ModelConfigServiceInit
|
|
262
|
+
// = { cwd, agentDir },指向 tmp 目录——loadGlobalConfig 对不存在文件
|
|
263
|
+
// 返回默认配置,AgentRegistry 空目录安全,同文件头既有 stub 口径)
|
|
264
|
+
const modelService = new subagentCore.ModelConfigService({ cwd: agentDir, agentDir });
|
|
265
|
+
const service = subagentCore.createSubagentService({ cwd: agentDir, modelService });
|
|
266
|
+
|
|
267
|
+
// 构造产物可用(实例方法存在),且不写进程单例槽位(第三宿主自持实例)
|
|
268
|
+
expect(typeof service.lookupRecordAnyState).toBe("function");
|
|
269
|
+
expect(subagentCore.getModelConfigService()).toBeNull();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -181,6 +181,96 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
181
181
|
// 清理 mock 调用记录防污染
|
|
182
182
|
loggerMock.error.mockClear();
|
|
183
183
|
});
|
|
184
|
+
|
|
185
|
+
// ── [T1/PS-9] sessionFile 缺失 → sessionDir 反查后 marker/alive 清理仍落地 ──
|
|
186
|
+
//
|
|
187
|
+
// PS-9:finalize 的 tombstone/finalized sidecar 与 removeAliveMarker 全部 gated on
|
|
188
|
+
// record.sessionFile——RC-1(握手失败)+ LC-4(收尾反查未命中)的残余形态下,
|
|
189
|
+
// 「sessionFile 缺失 → 终态原因丢失 + alive marker 残留」。修复:sessionFile 缺失时
|
|
190
|
+
// 用 deps.sessionDir 按 identity(record.id)反查真实 session 文件作依据。
|
|
191
|
+
// 真实 fs + tmp 目录(与文件既有策略一致):session JSONL 写入 identity custom entry
|
|
192
|
+
//(子进程 session_start hook 的写入形态),readIdentityHeader/readIdentityTail 真实解析。
|
|
193
|
+
describe("[T1/PS-9] sessionFile 缺失 sessionDir 反查", () => {
|
|
194
|
+
let sessionDir: string;
|
|
195
|
+
|
|
196
|
+
beforeEach(() => {
|
|
197
|
+
sessionDir = fs.mkdtempSync(path.join(os.tmpdir(), "finalize-ps9-sessions-"));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
afterEach(() => {
|
|
201
|
+
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/** 在 sessionDir 写入带 identity 的 session 文件(1 行 identity entry,位于头部)。 */
|
|
205
|
+
function writeSessionFileWithIdentity(fileName: string, recordId: string): string {
|
|
206
|
+
const sessionFile = path.join(sessionDir, fileName);
|
|
207
|
+
const identityEntry = JSON.stringify({
|
|
208
|
+
type: "custom",
|
|
209
|
+
customType: "subagent-identity",
|
|
210
|
+
data: { id: recordId, agent: "worker", mode: "background", task: "t", startedAt: 1000 },
|
|
211
|
+
});
|
|
212
|
+
fs.writeFileSync(sessionFile, `${identityEntry}\n`, "utf-8");
|
|
213
|
+
return sessionFile;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
it("sessionFile 缺失 + 反查命中 → .finalized 落盘 + .alive 清理 + record/manifest 回填", async () => {
|
|
217
|
+
const sessionFile = writeSessionFileWithIdentity("20260901T12000000_ps9.jsonl", "rec-ps9");
|
|
218
|
+
// 预写 .alive 残留(模拟 running 期崩溃恢复窗口的 marker)
|
|
219
|
+
fs.writeFileSync(
|
|
220
|
+
`${sessionFile}.alive`,
|
|
221
|
+
`${JSON.stringify({ pid: 99999, id: "rec-ps9", startedAt: 1000 })}\n`,
|
|
222
|
+
"utf-8",
|
|
223
|
+
);
|
|
224
|
+
const record = makeMinimalRecord({ id: "rec-ps9", sessionFile: undefined });
|
|
225
|
+
const deps = { ...makeDeps(), sessionDir };
|
|
226
|
+
|
|
227
|
+
await doFinalizeRecord(deps, record, makeMinimalResult(), "closed", "gc");
|
|
228
|
+
|
|
229
|
+
// 反查回填 record.sessionFile
|
|
230
|
+
expect(record.sessionFile).toBe(sessionFile);
|
|
231
|
+
// 终态原因持久化不再丢失(closedReason gc 写入 .finalized 内容)
|
|
232
|
+
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
|
|
233
|
+
expect(fs.readFileSync(`${sessionFile}.finalized`, "utf-8")).toBe("gc");
|
|
234
|
+
// alive marker 不再残留
|
|
235
|
+
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
|
|
236
|
+
// manifest 拿到真实 sessionFile(诊断/重建源不再失真)
|
|
237
|
+
const manifest = await manifestStore.readManifest("rec-ps9");
|
|
238
|
+
expect(manifest?.sessionFile).toBe(sessionFile);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("cancelled + 反查命中 → tombstone 写到反查路径(cancelled 语义不因 sessionFile 缺失丢失)", async () => {
|
|
242
|
+
const sessionFile = writeSessionFileWithIdentity("20260901T12000001_ps9c.jsonl", "rec-ps9c");
|
|
243
|
+
const record = makeMinimalRecord({ id: "rec-ps9c", sessionFile: undefined });
|
|
244
|
+
const deps = { ...makeDeps(), sessionDir };
|
|
245
|
+
|
|
246
|
+
await doFinalizeRecord(deps, record, makeMinimalResult(), "closed", "cancelled");
|
|
247
|
+
|
|
248
|
+
expect(record.sessionFile).toBe(sessionFile);
|
|
249
|
+
expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(true);
|
|
250
|
+
const tomb = JSON.parse(
|
|
251
|
+
fs.readFileSync(`${sessionFile}.cancelled`, "utf-8"),
|
|
252
|
+
) as { id: string; status: string };
|
|
253
|
+
expect(tomb.id).toBe("rec-ps9c");
|
|
254
|
+
expect(tomb.status).toBe("cancelled");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("反查未命中(目录无 identity 匹配文件)→ 不抛、sessionFile 保持缺失、无 marker(行为退回修复前)", async () => {
|
|
258
|
+
// sessionDir 存在但内容与 record 不匹配
|
|
259
|
+
writeSessionFileWithIdentity("20260901T12000000_other.jsonl", "rec-someone-else");
|
|
260
|
+
const record = makeMinimalRecord({ id: "rec-ps9-miss", sessionFile: undefined });
|
|
261
|
+
const deps = { ...makeDeps(), sessionDir };
|
|
262
|
+
|
|
263
|
+
await expect(
|
|
264
|
+
doFinalizeRecord(deps, record, makeMinimalResult(), "closed", "gc"),
|
|
265
|
+
).resolves.toBeUndefined();
|
|
266
|
+
|
|
267
|
+
expect(record.sessionFile).toBeUndefined();
|
|
268
|
+
// manifest sessionFile 同步保持 undefined
|
|
269
|
+
const manifest = await manifestStore.readManifest("rec-ps9-miss");
|
|
270
|
+
expect(manifest?.sessionFile).toBeUndefined();
|
|
271
|
+
loggerMock.error.mockClear();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
184
274
|
});
|
|
185
275
|
|
|
186
276
|
// ============================================================
|
|
@@ -317,6 +407,22 @@ describe("doFinalizeRoundToIdle — chatMode 轮次完成进 idle (M2-A)", () =>
|
|
|
317
407
|
expect(record.result).toBe("round did not complete: spawn timeout");
|
|
318
408
|
});
|
|
319
409
|
|
|
410
|
+
it("T2-③/LC-1: 失败轮 text 与 error 并存时 error 优先——collectResult 恒带 getFullText 正文,text 优先会覆盖恢复指引(S-B-1 首轮 settled watchdog 实测)", async () => {
|
|
411
|
+
const record = makeMinimalRecord({ id: "rec-result-err-text", chatMode: true });
|
|
412
|
+
record.status = "closed";
|
|
413
|
+
const result = makeMinimalResult();
|
|
414
|
+
// runSpawn settledWatchdogFired 分支的 collectResult 形态:正文(T1 回补采集)与
|
|
415
|
+
// watchdog 错误并存
|
|
416
|
+
result.text = "DONE";
|
|
417
|
+
result.success = false;
|
|
418
|
+
result.error =
|
|
419
|
+
"subagent did not reach agent_settled within 10 min (settled watchdog); the process was terminated to bound the wait. Recovery: check state with subagents action:'list', then re-send your message to continue.";
|
|
420
|
+
await doFinalizeRoundToIdle(makeDeps(), record, result);
|
|
421
|
+
// 失败的 record.result(= 通知正文)必须携带失败原因与恢复指引,不得回显轮内旧正文
|
|
422
|
+
expect(record.result).toBe(`round did not complete: ${result.error}`);
|
|
423
|
+
expect(record.result).not.toBe("DONE");
|
|
424
|
+
});
|
|
425
|
+
|
|
320
426
|
it("C1TC10: chatMode 空增量轮占位——record.result 固定 (no output this round),不含上一轮文本(D5)", async () => {
|
|
321
427
|
const record = makeMinimalRecord({ id: "rec-increment-empty", chatMode: true });
|
|
322
428
|
record.status = "closed";
|
|
@@ -30,6 +30,8 @@ vi.mock("../session-runner.ts", () => ({
|
|
|
30
30
|
runSpawn: vi.fn(),
|
|
31
31
|
killAllSpawnedChildren: vi.fn(),
|
|
32
32
|
getChildByRecord: vi.fn(() => undefined),
|
|
33
|
+
registerSpawnedChildForRecord: vi.fn(),
|
|
34
|
+
killRecordChildWithEscalation: vi.fn(),
|
|
33
35
|
spawnedChildren: new Map(),
|
|
34
36
|
}));
|
|
35
37
|
|
|
@@ -31,6 +31,8 @@ vi.mock("../session-runner.ts", () => ({
|
|
|
31
31
|
runSpawn: vi.fn(),
|
|
32
32
|
killAllSpawnedChildren: vi.fn(),
|
|
33
33
|
getChildByRecord: vi.fn(() => undefined),
|
|
34
|
+
registerSpawnedChildForRecord: vi.fn(),
|
|
35
|
+
killRecordChildWithEscalation: vi.fn(),
|
|
34
36
|
spawnedChildren: new Map(),
|
|
35
37
|
}));
|
|
36
38
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/execution/__tests__/get-state-handshake.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [T1/RC-1] requestGetStateOnce 单测:agent_end 决策点惰性回补用的单次 get_state 请求辅助。
|
|
4
|
+
//
|
|
5
|
+
// 契约锚点(与 performGetStateHandshake 共用消息构造 sendGetStateCommand + 字段提取
|
|
6
|
+
// extractGetStateFields,但不做重试循环):
|
|
7
|
+
// 1. response 到达 → resolve 提取后的 GetStateResult(无条件 finish,单次语义无重试);
|
|
8
|
+
// 2. 超时 → resolve 空对象(调用方走保守分支);
|
|
9
|
+
// 3. stdin 同步写失败(EPIPE 形态)→ resolve 空对象、永不 reject(fire-and-forget 契约);
|
|
10
|
+
// 4. 自清理:finish 后从监听表注销本请求 resolver(消费注册器返回的注销函数);
|
|
11
|
+
// 注册器不返回注销函数时退化为 no-op(与握手同形态)。
|
|
12
|
+
//
|
|
13
|
+
// performGetStateHandshake 既有行为由 run-spawn-rpc-mode.test.ts 集成覆盖,此处不重复。
|
|
14
|
+
|
|
15
|
+
import { describe, expect, it, vi } from "vitest";
|
|
16
|
+
|
|
17
|
+
import { requestGetStateOnce } from "../get-state-handshake.ts";
|
|
18
|
+
import type { ChildProcess } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
/** 最小 FakeChild:只需 stdin.write 行为(成功 / 可注入同步 throw)。 */
|
|
21
|
+
function makeFakeStdin(behavior?: { throwOnWrite?: Error }): { child: ChildProcess; writes: string[] } {
|
|
22
|
+
const writes: string[] = [];
|
|
23
|
+
const child = {
|
|
24
|
+
stdin: {
|
|
25
|
+
write(line: string): boolean {
|
|
26
|
+
if (behavior?.throwOnWrite) throw behavior.throwOnWrite;
|
|
27
|
+
writes.push(line);
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
} as unknown as ChildProcess;
|
|
32
|
+
return { child, writes };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 可控的监听表:模拟 stdout pump 的 get_stateListeners(注册返回注销函数)。 */
|
|
36
|
+
function makeListenerRegistry() {
|
|
37
|
+
const resolvers = new Map<string, (data: unknown) => void>();
|
|
38
|
+
const removed: string[] = [];
|
|
39
|
+
const add = (id: string, resolver: (data: unknown) => void) => {
|
|
40
|
+
resolvers.set(id, resolver);
|
|
41
|
+
return () => {
|
|
42
|
+
if (resolvers.get(id) === resolver) resolvers.delete(id);
|
|
43
|
+
removed.push(id);
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
return { resolvers, removed, add };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("requestGetStateOnce([T1/RC-1] 惰性回补单次请求)", () => {
|
|
50
|
+
it("response 到达 → resolve 提取的 sessionFile/sessionId,并从监听表注销", async () => {
|
|
51
|
+
const { child, writes } = makeFakeStdin();
|
|
52
|
+
const reg = makeListenerRegistry();
|
|
53
|
+
|
|
54
|
+
const promise = requestGetStateOnce(child, reg.add, 1000);
|
|
55
|
+
// 消息构造:单行 JSON {id, type:"get_state"}(与 performGetStateHandshake 同一 sendGetStateCommand)
|
|
56
|
+
expect(writes).toHaveLength(1);
|
|
57
|
+
const sent = JSON.parse(writes[0]!) as { id: string; type: string };
|
|
58
|
+
expect(sent.type).toBe("get_state");
|
|
59
|
+
expect(reg.resolvers.has(sent.id)).toBe(true);
|
|
60
|
+
|
|
61
|
+
// response 到达(含空串 sessionFile 应被过滤的字段形态覆盖提取规则)
|
|
62
|
+
reg.resolvers.get(sent.id)?.({
|
|
63
|
+
sessionFile: "/tmp/sessions/abc.jsonl",
|
|
64
|
+
sessionId: "sess-1",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await expect(promise).resolves.toEqual({
|
|
68
|
+
sessionFile: "/tmp/sessions/abc.jsonl",
|
|
69
|
+
sessionId: "sess-1",
|
|
70
|
+
});
|
|
71
|
+
// 自清理:finish 后从监听表移除本请求 resolver
|
|
72
|
+
expect(reg.resolvers.has(sent.id)).toBe(false);
|
|
73
|
+
expect(reg.removed).toEqual([sent.id]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("response 只含部分字段(仅 sessionId)→ resolve 已提取部分(单次语义:到达即 finish,不等重试)", async () => {
|
|
77
|
+
const { child } = makeFakeStdin();
|
|
78
|
+
const reg = makeListenerRegistry();
|
|
79
|
+
|
|
80
|
+
const promise = requestGetStateOnce(child, reg.add, 1000);
|
|
81
|
+
const id = reg.resolvers.keys().next().value as string;
|
|
82
|
+
reg.resolvers.get(id)?.({ sessionId: "only-id" });
|
|
83
|
+
|
|
84
|
+
await expect(promise).resolves.toEqual({ sessionId: "only-id" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("超时无 response → resolve 空对象(不 reject,不调注销前的 resolver)", async () => {
|
|
88
|
+
vi.useFakeTimers();
|
|
89
|
+
try {
|
|
90
|
+
const { child } = makeFakeStdin();
|
|
91
|
+
const reg = makeListenerRegistry();
|
|
92
|
+
|
|
93
|
+
const promise = requestGetStateOnce(child, reg.add, 1000);
|
|
94
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
95
|
+
|
|
96
|
+
await expect(promise).resolves.toEqual({});
|
|
97
|
+
} finally {
|
|
98
|
+
vi.useRealTimers();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("stdin 同步写失败(EPIPE code,writeStdinLine rethrow 路径)→ 立即 resolve 空对象、永不 reject", async () => {
|
|
103
|
+
// writeStdinLine 只对 code 为 EPIPE / ERR_STREAM_DESTROYED 的错误 rethrow([R3]),
|
|
104
|
+
// requestGetStateOnce 的 catch 捕获后按「回补失败」resolve 空对象(同超时语义)。
|
|
105
|
+
const epipeErr = Object.assign(new Error("write after end"), { code: "EPIPE" });
|
|
106
|
+
const { child } = makeFakeStdin({ throwOnWrite: epipeErr });
|
|
107
|
+
const reg = makeListenerRegistry();
|
|
108
|
+
|
|
109
|
+
await expect(requestGetStateOnce(child, reg.add, 1000)).resolves.toEqual({});
|
|
110
|
+
// 写失败路径不注册监听(未发出请求)
|
|
111
|
+
expect(reg.resolvers.size).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("注册器不返回注销函数(void 形态,对齐旧握手调用方)→ 正常 resolve 不抛", async () => {
|
|
115
|
+
const { child } = makeFakeStdin();
|
|
116
|
+
const resolvers = new Map<string, (data: unknown) => void>();
|
|
117
|
+
const addVoid = (id: string, resolver: (data: unknown) => void): void => {
|
|
118
|
+
resolvers.set(id, resolver);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const promise = requestGetStateOnce(child, addVoid, 1000);
|
|
122
|
+
const id = resolvers.keys().next().value as string;
|
|
123
|
+
resolvers.get(id)?.({ sessionFile: "/tmp/x.jsonl" });
|
|
124
|
+
|
|
125
|
+
await expect(promise).resolves.toEqual({ sessionFile: "/tmp/x.jsonl" });
|
|
126
|
+
});
|
|
127
|
+
});
|