@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
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
// session-channel.test.ts —— R3 会话层测试:全部跑 __fixtures__/fake-appserver.mjs
|
|
2
|
+
// 子进程(scenario 注入会话场景),绝不 spawn 真 zcode.cjs。覆盖验收条款:
|
|
3
|
+
// A.2 帧序列逐字断言(create 参数键集与值形态 / subscribe deliveryKind 必填 /
|
|
4
|
+
// send 字段是 content 不是 text)/ sessionId 从 result.session.sessionId 提取且不用
|
|
5
|
+
// projection / 终态判定(turn.terminal 权威 + 缺失时收尾帧宽松判定 + error status
|
|
6
|
+
// 亦终态)/ read 兜底全文拼接 == delta 拼接(不变量 1,四方双来源)/ usage 收尾帧
|
|
7
|
+
// 提取并随 resolve 返回(step-finish tokens 宽容兜底)/ close 成功与失败路径都被
|
|
8
|
+
// 调用(try/finally)/ -32601/-32602 结构化透传(R5 降级链地基)/ resolve 严格晚于
|
|
9
|
+
// 末事件回调(不变量 2)/ golden 双副本 diff + 帧序列语料四类样本全链回放。
|
|
10
|
+
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
AppServerConnection,
|
|
21
|
+
buildAppServerEnv,
|
|
22
|
+
isAppServerRpcError,
|
|
23
|
+
} from "../connection.ts";
|
|
24
|
+
import { ZCODE_APPSERVER_GOLDEN } from "../golden-sample.ts";
|
|
25
|
+
import {
|
|
26
|
+
SessionChannel,
|
|
27
|
+
SUBSCRIBE_DELIVERY_KIND,
|
|
28
|
+
extractAssistantText,
|
|
29
|
+
extractCreatedSessionId,
|
|
30
|
+
extractReadUsage,
|
|
31
|
+
stableWorkspaceKey,
|
|
32
|
+
} from "../session-channel.ts";
|
|
33
|
+
|
|
34
|
+
const TMP = mkdtempSync(join(tmpdir(), "zcode-sess-"));
|
|
35
|
+
const FAKE_CLI = fileURLToPath(
|
|
36
|
+
new URL("./__fixtures__/fake-appserver.mjs", import.meta.url)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// golden fixture(采集 SSOT)——双副本 diff 断言的数据源
|
|
40
|
+
const GOLDEN_FIXTURE = JSON.parse(
|
|
41
|
+
readFileSync(
|
|
42
|
+
fileURLToPath(
|
|
43
|
+
new URL("./__fixtures__/zcode-golden-appserver.json", import.meta.url)
|
|
44
|
+
),
|
|
45
|
+
"utf8"
|
|
46
|
+
)
|
|
47
|
+
) as { frames: typeof ZCODE_APPSERVER_GOLDEN };
|
|
48
|
+
|
|
49
|
+
const GOLDEN_SESSION_ID = "sess_golden_r3_01";
|
|
50
|
+
const GOLDEN_FULL_TEXT = "你好,任务完成";
|
|
51
|
+
const GOLDEN_FINAL_USAGE = { inputTokens: 12599, outputTokens: 17 };
|
|
52
|
+
const GOLDEN_READ_TOKENS = {
|
|
53
|
+
input: 12599,
|
|
54
|
+
output: 17,
|
|
55
|
+
reasoning: 0,
|
|
56
|
+
cache: 512,
|
|
57
|
+
total: 12616,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** 任意 RPC error 注入形态(fake scenario 的 error 三件套)。 */
|
|
61
|
+
interface RpcErrorSpec {
|
|
62
|
+
code: number;
|
|
63
|
+
message: string;
|
|
64
|
+
data?: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ScenarioOverrides {
|
|
68
|
+
/** 终态帧组去掉 turn.terminal(宽松判定场景)。 */
|
|
69
|
+
dropTurnTerminal?: boolean;
|
|
70
|
+
/** 终态帧组去掉收尾帧(delta 聚合 / read tokens 兜底场景)。 */
|
|
71
|
+
dropFinalFrame?: boolean;
|
|
72
|
+
/** 整组替换推送帧(完全自定义帧序)。 */
|
|
73
|
+
replaceSendPushes?: string[];
|
|
74
|
+
/** 在默认帧序之后追加帧(迟到帧注入)。 */
|
|
75
|
+
extraSendPushes?: string[];
|
|
76
|
+
createResult?: unknown;
|
|
77
|
+
createError?: RpcErrorSpec;
|
|
78
|
+
sendError?: RpcErrorSpec;
|
|
79
|
+
sendResult?: unknown;
|
|
80
|
+
readError?: RpcErrorSpec;
|
|
81
|
+
readResult?: unknown;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ChannelHandle {
|
|
85
|
+
ch: SessionChannel;
|
|
86
|
+
conn: AppServerConnection;
|
|
87
|
+
stateFile: string;
|
|
88
|
+
workspacePath: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const CONNECTIONS: AppServerConnection[] = [];
|
|
92
|
+
let connSeq = 0;
|
|
93
|
+
|
|
94
|
+
/** NDJSON 行 / 帧对象统一规范化为帧对象(golden 语料是 NDJSON 行,fake 需要对象逐字回放)。 */
|
|
95
|
+
function toFrame(f: string | Record<string, unknown>): Record<string, unknown> {
|
|
96
|
+
return typeof f === "string" ? (JSON.parse(f) as Record<string, unknown>) : f;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 建一条连到 fake 的连接 + 会话通道(scenario 文件随 env 固化进子进程)。 */
|
|
100
|
+
function makeChannel(overrides: ScenarioOverrides = {}): ChannelHandle {
|
|
101
|
+
connSeq += 1;
|
|
102
|
+
const stateFile = join(TMP, `state-${connSeq}.jsonl`);
|
|
103
|
+
const scenarioFile = join(TMP, `scenario-${connSeq}.json`);
|
|
104
|
+
const workspacePath = join(TMP, `ws-${connSeq}`);
|
|
105
|
+
const pushes = (
|
|
106
|
+
overrides.replaceSendPushes ?? [
|
|
107
|
+
...ZCODE_APPSERVER_GOLDEN.pushStream,
|
|
108
|
+
...(overrides.dropTurnTerminal
|
|
109
|
+
? []
|
|
110
|
+
: [ZCODE_APPSERVER_GOLDEN.terminal[0]]),
|
|
111
|
+
...(overrides.dropFinalFrame ? [] : [ZCODE_APPSERVER_GOLDEN.terminal[1]]),
|
|
112
|
+
]
|
|
113
|
+
).map(toFrame);
|
|
114
|
+
const scenario: Record<string, unknown> = {
|
|
115
|
+
createResult:
|
|
116
|
+
overrides.createResult ??
|
|
117
|
+
JSON.parse(ZCODE_APPSERVER_GOLDEN.createResponse),
|
|
118
|
+
sendPushes: overrides.extraSendPushes
|
|
119
|
+
? [...pushes, ...overrides.extraSendPushes.map(toFrame)]
|
|
120
|
+
: pushes,
|
|
121
|
+
readResult:
|
|
122
|
+
overrides.readResult ?? JSON.parse(ZCODE_APPSERVER_GOLDEN.readResponse),
|
|
123
|
+
...(overrides.createError !== undefined
|
|
124
|
+
? { createError: overrides.createError }
|
|
125
|
+
: {}),
|
|
126
|
+
...(overrides.sendError !== undefined
|
|
127
|
+
? { sendError: overrides.sendError }
|
|
128
|
+
: {}),
|
|
129
|
+
...(overrides.sendResult !== undefined
|
|
130
|
+
? { sendResult: overrides.sendResult }
|
|
131
|
+
: {}),
|
|
132
|
+
...(overrides.readError !== undefined
|
|
133
|
+
? { readError: overrides.readError }
|
|
134
|
+
: {}),
|
|
135
|
+
...(overrides.readResult !== undefined
|
|
136
|
+
? { readResult: overrides.readResult }
|
|
137
|
+
: {}),
|
|
138
|
+
};
|
|
139
|
+
writeFileSync(scenarioFile, JSON.stringify(scenario));
|
|
140
|
+
const conn = new AppServerConnection({
|
|
141
|
+
cliPath: FAKE_CLI,
|
|
142
|
+
cwd: workspacePath,
|
|
143
|
+
env: buildAppServerEnv(join(TMP, `home-${connSeq}`), {
|
|
144
|
+
PATH: process.env.PATH ?? "",
|
|
145
|
+
FAKE_STATE_FILE: stateFile,
|
|
146
|
+
FAKE_SESSION_SCENARIO: scenarioFile,
|
|
147
|
+
}),
|
|
148
|
+
stderrLogPath: join(TMP, `stderr-${connSeq}.log`),
|
|
149
|
+
});
|
|
150
|
+
CONNECTIONS.push(conn);
|
|
151
|
+
return { ch: new SessionChannel(conn), conn, stateFile, workspacePath };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
afterEach(async () => {
|
|
155
|
+
const conns = CONNECTIONS.splice(0);
|
|
156
|
+
for (const conn of conns) conn.close(); // 同步 SIGTERM 面快速收割;shutdown 幂等收尸
|
|
157
|
+
await Promise.allSettled(conns.map((c) => c.shutdown({ graceMs: 1_000 })));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ------------------------------------------- 流水读取 helpers(fake 侧 recv 帧)
|
|
161
|
+
|
|
162
|
+
interface StateEvent {
|
|
163
|
+
seq: number;
|
|
164
|
+
ev: string;
|
|
165
|
+
[key: string]: unknown;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function readState(file: string): StateEvent[] {
|
|
169
|
+
try {
|
|
170
|
+
return readFileSync(file, "utf8")
|
|
171
|
+
.trim()
|
|
172
|
+
.split("\n")
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.map((l) => JSON.parse(l) as StateEvent);
|
|
175
|
+
} catch {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
181
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface SentFrame {
|
|
185
|
+
id: number;
|
|
186
|
+
method: string;
|
|
187
|
+
params: Record<string, unknown>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function sentFrames(stateFile: string, method: string): SentFrame[] {
|
|
191
|
+
return readState(stateFile)
|
|
192
|
+
.map((e) => e.frame)
|
|
193
|
+
.filter(
|
|
194
|
+
(f): f is SentFrame =>
|
|
195
|
+
isRecord(f) &&
|
|
196
|
+
f.method === method &&
|
|
197
|
+
isRecord(f.params) &&
|
|
198
|
+
typeof f.id === "number"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 客户端请求帧的 method 序列(我方反向应答帧无 method 不计入)。 */
|
|
203
|
+
function sentMethods(stateFile: string): string[] {
|
|
204
|
+
return readState(stateFile)
|
|
205
|
+
.map((e) => e.frame)
|
|
206
|
+
.filter(
|
|
207
|
+
(f): f is Record<string, unknown> =>
|
|
208
|
+
isRecord(f) && typeof f.method === "string"
|
|
209
|
+
)
|
|
210
|
+
.map((f) => f.method as string);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================================
|
|
214
|
+
// A.2 帧序列逐字断言(出站精确键集——对面 zod strict 拒未知键)
|
|
215
|
+
// ============================================================
|
|
216
|
+
|
|
217
|
+
describe("A.2 帧序列逐字断言", () => {
|
|
218
|
+
it("create 帧:参数键集恰 {workspace, mode, persistence};workspaceKey='ws-'+sha256 前 16;persistence:'immediate'", async () => {
|
|
219
|
+
const { ch, stateFile, workspacePath } = makeChannel();
|
|
220
|
+
await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
221
|
+
const frames = sentFrames(stateFile, "session/create");
|
|
222
|
+
expect(frames).toHaveLength(1);
|
|
223
|
+
// 帧级键集 {id, method, params}——无 jsonrpc、无未知键
|
|
224
|
+
expect(Object.keys(frames[0]).sort()).toEqual(["id", "method", "params"]);
|
|
225
|
+
const params = frames[0].params;
|
|
226
|
+
expect(Object.keys(params).sort()).toEqual([
|
|
227
|
+
"mode",
|
|
228
|
+
"persistence",
|
|
229
|
+
"workspace",
|
|
230
|
+
]);
|
|
231
|
+
expect(params.mode).toBe("yolo");
|
|
232
|
+
expect(params.persistence).toBe("immediate");
|
|
233
|
+
expect(Object.keys(params.workspace).sort()).toEqual([
|
|
234
|
+
"workspaceKey",
|
|
235
|
+
"workspacePath",
|
|
236
|
+
]);
|
|
237
|
+
expect(params.workspace).toEqual({
|
|
238
|
+
workspacePath,
|
|
239
|
+
workspaceKey:
|
|
240
|
+
"ws-" +
|
|
241
|
+
createHash("sha256").update(workspacePath).digest("hex").slice(0, 16),
|
|
242
|
+
});
|
|
243
|
+
expect(stableWorkspaceKey(workspacePath)).toBe(
|
|
244
|
+
params.workspace.workspaceKey
|
|
245
|
+
);
|
|
246
|
+
}, 10_000);
|
|
247
|
+
|
|
248
|
+
it("create 可选键:model 是 strict 对象(providerId/modelId/variant);空 toolAllowlist 不设键;toolDenylist 传则带", async () => {
|
|
249
|
+
const { ch, stateFile, workspacePath } = makeChannel();
|
|
250
|
+
await ch.runTurn(
|
|
251
|
+
{
|
|
252
|
+
workspacePath,
|
|
253
|
+
mode: "yolo",
|
|
254
|
+
model: { providerId: "zai", modelId: "glm-5.3", variant: "plan" },
|
|
255
|
+
thoughtLevel: "high",
|
|
256
|
+
toolAllowlist: [],
|
|
257
|
+
toolDenylist: ["Bash", "Write"],
|
|
258
|
+
},
|
|
259
|
+
"做点什么"
|
|
260
|
+
);
|
|
261
|
+
const params = sentFrames(stateFile, "session/create")[0].params;
|
|
262
|
+
expect(Object.keys(params).sort()).toEqual([
|
|
263
|
+
"mode",
|
|
264
|
+
"model",
|
|
265
|
+
"persistence",
|
|
266
|
+
"thoughtLevel",
|
|
267
|
+
"toolDenylist",
|
|
268
|
+
"workspace",
|
|
269
|
+
]);
|
|
270
|
+
// strict 对象逐字段(A.2 ①:字符串 model 会被 -32602 拒收)
|
|
271
|
+
expect(params.model).toEqual({
|
|
272
|
+
providerId: "zai",
|
|
273
|
+
modelId: "glm-5.3",
|
|
274
|
+
variant: "plan",
|
|
275
|
+
});
|
|
276
|
+
expect(params.thoughtLevel).toBe("high");
|
|
277
|
+
expect(params.toolDenylist).toEqual(["Bash", "Write"]);
|
|
278
|
+
}, 10_000);
|
|
279
|
+
|
|
280
|
+
it("subscribe 帧:参数恰 {sessionId, deliveryKind:'desktop-continuous'}(deliveryKind 必填)", async () => {
|
|
281
|
+
const { ch, stateFile, workspacePath } = makeChannel();
|
|
282
|
+
await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
283
|
+
const frames = sentFrames(stateFile, "session/subscribe");
|
|
284
|
+
expect(frames).toHaveLength(1);
|
|
285
|
+
expect(frames[0].params).toEqual({
|
|
286
|
+
sessionId: GOLDEN_SESSION_ID,
|
|
287
|
+
deliveryKind: "desktop-continuous",
|
|
288
|
+
});
|
|
289
|
+
expect(Object.keys(frames[0].params).sort()).toEqual([
|
|
290
|
+
"deliveryKind",
|
|
291
|
+
"sessionId",
|
|
292
|
+
]);
|
|
293
|
+
expect(SUBSCRIBE_DELIVERY_KIND).toBe("desktop-continuous");
|
|
294
|
+
}, 10_000);
|
|
295
|
+
|
|
296
|
+
it("send 帧:字段是 content 不是 text——参数恰 {sessionId, content}", async () => {
|
|
297
|
+
const { ch, stateFile, workspacePath } = makeChannel();
|
|
298
|
+
await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
299
|
+
const frames = sentFrames(stateFile, "session/send");
|
|
300
|
+
expect(frames).toHaveLength(1);
|
|
301
|
+
expect(frames[0].params).toEqual({
|
|
302
|
+
sessionId: GOLDEN_SESSION_ID,
|
|
303
|
+
content: "做点什么",
|
|
304
|
+
});
|
|
305
|
+
expect(Object.keys(frames[0].params).sort()).toEqual([
|
|
306
|
+
"content",
|
|
307
|
+
"sessionId",
|
|
308
|
+
]);
|
|
309
|
+
expect("text" in frames[0].params).toBe(false);
|
|
310
|
+
}, 10_000);
|
|
311
|
+
|
|
312
|
+
it("read/close 帧参数恰 {sessionId}", async () => {
|
|
313
|
+
const { ch, stateFile, workspacePath } = makeChannel();
|
|
314
|
+
await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
315
|
+
expect(sentFrames(stateFile, "session/read")).toHaveLength(1);
|
|
316
|
+
expect(sentFrames(stateFile, "session/read")[0].params).toEqual({
|
|
317
|
+
sessionId: GOLDEN_SESSION_ID,
|
|
318
|
+
});
|
|
319
|
+
expect(sentFrames(stateFile, "session/close")).toHaveLength(1);
|
|
320
|
+
expect(sentFrames(stateFile, "session/close")[0].params).toEqual({
|
|
321
|
+
sessionId: GOLDEN_SESSION_ID,
|
|
322
|
+
});
|
|
323
|
+
}, 10_000);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ============================================================
|
|
327
|
+
// sessionId 提取(projection.sessionId 恒 "unknown" 勿用)
|
|
328
|
+
// ============================================================
|
|
329
|
+
|
|
330
|
+
describe("sessionId 提取", () => {
|
|
331
|
+
it("golden create 应答:从 result.session.sessionId 提取,不用 projection.sessionId(恒 'unknown')", async () => {
|
|
332
|
+
const { ch, workspacePath } = makeChannel();
|
|
333
|
+
const sid = await ch.createSession({ workspacePath, mode: "yolo" });
|
|
334
|
+
expect(sid).toBe(GOLDEN_SESSION_ID);
|
|
335
|
+
expect(sid).not.toBe("unknown");
|
|
336
|
+
// 提取函数对 golden 应答的单点断言(conformance/golden 回放层可复用)
|
|
337
|
+
expect(
|
|
338
|
+
extractCreatedSessionId(JSON.parse(ZCODE_APPSERVER_GOLDEN.createResponse))
|
|
339
|
+
).toBe(GOLDEN_SESSION_ID);
|
|
340
|
+
}, 10_000);
|
|
341
|
+
|
|
342
|
+
it("无 sessionId 的 create 应答 → 显式错误(含应答摘要,不落 'unknown')", async () => {
|
|
343
|
+
const { ch, workspacePath } = makeChannel({
|
|
344
|
+
createResult: { projection: { sessionId: "unknown" } },
|
|
345
|
+
});
|
|
346
|
+
await expect(
|
|
347
|
+
ch.createSession({ workspacePath, mode: "yolo" })
|
|
348
|
+
).rejects.toThrow(/未返回 sessionId/);
|
|
349
|
+
}, 10_000);
|
|
350
|
+
|
|
351
|
+
it("宽松兜底链:session.id / 顶层 sessionId 旧形态可提取", () => {
|
|
352
|
+
expect(extractCreatedSessionId({ session: { id: "sess_alt" } })).toBe(
|
|
353
|
+
"sess_alt"
|
|
354
|
+
);
|
|
355
|
+
expect(extractCreatedSessionId({ sessionId: "sess_top" })).toBe("sess_top");
|
|
356
|
+
expect(
|
|
357
|
+
extractCreatedSessionId({ projection: { sessionId: "unknown" } })
|
|
358
|
+
).toBeUndefined();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// ============================================================
|
|
363
|
+
// 终态判定(D4 + 不变量 1)
|
|
364
|
+
// ============================================================
|
|
365
|
+
|
|
366
|
+
describe("终态判定", () => {
|
|
367
|
+
it("turn.terminal 权威:success → resolve,terminal={status:'success', source:'turn.terminal'}", async () => {
|
|
368
|
+
const { ch, workspacePath } = makeChannel();
|
|
369
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
370
|
+
expect(r.terminal).toEqual({ status: "success", source: "turn.terminal" });
|
|
371
|
+
}, 10_000);
|
|
372
|
+
|
|
373
|
+
it("turn.terminal status:'error' 亦终态(不归类会挂到超时);read 兜底照常", async () => {
|
|
374
|
+
const errorTerminal = JSON.stringify({
|
|
375
|
+
method: "v4/telemetry/event",
|
|
376
|
+
params: { kind: "turn.terminal", status: "error" },
|
|
377
|
+
});
|
|
378
|
+
const finalFrame = ZCODE_APPSERVER_GOLDEN.terminal[1];
|
|
379
|
+
const { ch, workspacePath } = makeChannel({
|
|
380
|
+
replaceSendPushes: [
|
|
381
|
+
...ZCODE_APPSERVER_GOLDEN.pushStream,
|
|
382
|
+
errorTerminal,
|
|
383
|
+
finalFrame,
|
|
384
|
+
],
|
|
385
|
+
});
|
|
386
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
387
|
+
expect(r.terminal).toEqual({ status: "error", source: "turn.terminal" });
|
|
388
|
+
expect(r.response).toBe(GOLDEN_FULL_TEXT);
|
|
389
|
+
}, 10_000);
|
|
390
|
+
|
|
391
|
+
it("宽松匹配防洪堤:turn.terminal 缺失时收尾帧(payload.response 存在)判定终态", async () => {
|
|
392
|
+
const { ch, workspacePath } = makeChannel({ dropTurnTerminal: true });
|
|
393
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
394
|
+
expect(r.terminal).toEqual({ status: "success", source: "final-frame" });
|
|
395
|
+
expect(r.response).toBe(GOLDEN_FULL_TEXT);
|
|
396
|
+
}, 10_000);
|
|
397
|
+
|
|
398
|
+
it("迟到帧丢弃:收尾帧判定终态后,迟到的 turn.terminal 不改判、迟到的 delta 不触发回调", async () => {
|
|
399
|
+
const lateDelta = JSON.stringify({
|
|
400
|
+
method: "session/event",
|
|
401
|
+
params: { sessionId: GOLDEN_SESSION_ID, payload: { delta: "迟到文本" } },
|
|
402
|
+
});
|
|
403
|
+
const lateTerminal = JSON.stringify({
|
|
404
|
+
method: "v4/telemetry/event",
|
|
405
|
+
params: { kind: "turn.terminal", status: "success" },
|
|
406
|
+
});
|
|
407
|
+
const deltas: string[] = [];
|
|
408
|
+
const { ch, workspacePath } = makeChannel({
|
|
409
|
+
dropTurnTerminal: true,
|
|
410
|
+
extraSendPushes: [lateDelta, lateTerminal],
|
|
411
|
+
});
|
|
412
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么", {
|
|
413
|
+
onTextDelta: (d) => deltas.push(d),
|
|
414
|
+
});
|
|
415
|
+
expect(deltas).toEqual(["你好", ",", "任务完成"]); // 迟到 delta 未触发(不变量 2)
|
|
416
|
+
expect(r.terminal).toEqual({ status: "success", source: "final-frame" }); // 迟到 turn.terminal 未改判
|
|
417
|
+
}, 10_000);
|
|
418
|
+
|
|
419
|
+
it("他人 sessionId 的推送帧不归因(防串线)", async () => {
|
|
420
|
+
const crossTalk = JSON.stringify({
|
|
421
|
+
method: "session/event",
|
|
422
|
+
params: {
|
|
423
|
+
sessionId: "sess_someone_else",
|
|
424
|
+
payload: { delta: "串线文本" },
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
const deltas: string[] = [];
|
|
428
|
+
const { ch, workspacePath } = makeChannel({ extraSendPushes: [crossTalk] });
|
|
429
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么", {
|
|
430
|
+
onTextDelta: (d) => deltas.push(d),
|
|
431
|
+
});
|
|
432
|
+
expect(deltas).toEqual(["你好", ",", "任务完成"]);
|
|
433
|
+
expect(r.response).not.toContain("串线文本");
|
|
434
|
+
}, 10_000);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// ============================================================
|
|
438
|
+
// read 兜底与不变量 1(text_delta 拼接 == read 全文,双来源)
|
|
439
|
+
// ============================================================
|
|
440
|
+
|
|
441
|
+
describe("read 兜底与不变量 1", () => {
|
|
442
|
+
it("golden 全链:delta 拼接 === resolve.response === read 全文 === 收尾帧 response(四方一致)", async () => {
|
|
443
|
+
const deltas: string[] = [];
|
|
444
|
+
const { ch, workspacePath } = makeChannel();
|
|
445
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么", {
|
|
446
|
+
onTextDelta: (d) => deltas.push(d),
|
|
447
|
+
});
|
|
448
|
+
expect(deltas).toEqual(["你好", ",", "任务完成"]);
|
|
449
|
+
expect(deltas.join("")).toBe(GOLDEN_FULL_TEXT);
|
|
450
|
+
// read 是权威来源:golden readResponse 的 assistant text parts 拼接与 delta 流一致
|
|
451
|
+
expect(
|
|
452
|
+
extractAssistantText(JSON.parse(ZCODE_APPSERVER_GOLDEN.readResponse))
|
|
453
|
+
).toBe(GOLDEN_FULL_TEXT);
|
|
454
|
+
expect(r.response).toBe(deltas.join(""));
|
|
455
|
+
// 收尾帧 response(第二来源)亦一致
|
|
456
|
+
const finalFrame = JSON.parse(ZCODE_APPSERVER_GOLDEN.terminal[1]) as {
|
|
457
|
+
params: { payload: { response: string } };
|
|
458
|
+
};
|
|
459
|
+
expect(finalFrame.params.payload.response).toBe(GOLDEN_FULL_TEXT);
|
|
460
|
+
expect(r.response).toBe(finalFrame.params.payload.response);
|
|
461
|
+
}, 10_000);
|
|
462
|
+
|
|
463
|
+
it("read 失败(-32004)→ 收尾帧 response 兜底,不抛", async () => {
|
|
464
|
+
const { ch, workspacePath } = makeChannel({
|
|
465
|
+
readError: { code: -32004, message: "Session is not active" },
|
|
466
|
+
});
|
|
467
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
468
|
+
expect(r.response).toBe(GOLDEN_FULL_TEXT); // 收尾帧
|
|
469
|
+
expect(r.usage).toEqual(GOLDEN_FINAL_USAGE); // usage 仍从收尾帧提取
|
|
470
|
+
}, 10_000);
|
|
471
|
+
|
|
472
|
+
it("read 失败 + 无收尾帧 → delta 聚合兜底(三层降级链末位),usage 缺席", async () => {
|
|
473
|
+
const deltas: string[] = [];
|
|
474
|
+
const { ch, workspacePath } = makeChannel({
|
|
475
|
+
dropFinalFrame: true,
|
|
476
|
+
readError: { code: -32003, message: "boom" },
|
|
477
|
+
});
|
|
478
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么", {
|
|
479
|
+
onTextDelta: (d) => deltas.push(d),
|
|
480
|
+
});
|
|
481
|
+
expect(r.response).toBe(deltas.join(""));
|
|
482
|
+
expect(r.response).toBe(GOLDEN_FULL_TEXT);
|
|
483
|
+
expect(r.usage).toBeUndefined();
|
|
484
|
+
}, 10_000);
|
|
485
|
+
|
|
486
|
+
it("read 成功但提取不到 assistant 文本(messages 空)→ 收尾帧兜底", async () => {
|
|
487
|
+
const { ch, workspacePath } = makeChannel({ readResult: { messages: [] } });
|
|
488
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
489
|
+
expect(r.response).toBe(GOLDEN_FULL_TEXT);
|
|
490
|
+
}, 10_000);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// ============================================================
|
|
494
|
+
// usage(收尾帧权威 → read step-finish tokens 宽容兜底)
|
|
495
|
+
// ============================================================
|
|
496
|
+
|
|
497
|
+
describe("usage", () => {
|
|
498
|
+
it("usage 从收尾帧提取并随 resolve 返回(A.2 权威形态,精确对象——read tokens 不遮蔽)", async () => {
|
|
499
|
+
const { ch, workspacePath } = makeChannel();
|
|
500
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
501
|
+
expect(r.usage).toEqual(GOLDEN_FINAL_USAGE);
|
|
502
|
+
}, 10_000);
|
|
503
|
+
|
|
504
|
+
it("收尾帧缺失时 read step-finish tokens 宽容兜底(A.5 未确认项——字段缺席不抛)", async () => {
|
|
505
|
+
const { ch, workspacePath } = makeChannel({ dropFinalFrame: true });
|
|
506
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
507
|
+
expect(r.usage).toEqual(GOLDEN_READ_TOKENS);
|
|
508
|
+
// 提取函数单点:golden readResponse 的 tokens 结构(宽容解析面)
|
|
509
|
+
expect(
|
|
510
|
+
extractReadUsage(JSON.parse(ZCODE_APPSERVER_GOLDEN.readResponse))
|
|
511
|
+
).toEqual(GOLDEN_READ_TOKENS);
|
|
512
|
+
}, 10_000);
|
|
513
|
+
|
|
514
|
+
it("收尾帧与 read tokens 皆无 → usage 缺席(不给残缺对象)", async () => {
|
|
515
|
+
const { ch, workspacePath } = makeChannel({
|
|
516
|
+
dropFinalFrame: true,
|
|
517
|
+
readResult: { messages: [] },
|
|
518
|
+
});
|
|
519
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
520
|
+
expect(r.usage).toBeUndefined();
|
|
521
|
+
}, 10_000);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// ============================================================
|
|
525
|
+
// close(D4 用后即毁:成功/失败路径都 close——try/finally)
|
|
526
|
+
// ============================================================
|
|
527
|
+
|
|
528
|
+
describe("close(D4 用后即毁)", () => {
|
|
529
|
+
it("成功路径:请求帧序恰 create→subscribe→send→read→close(close 最后)", async () => {
|
|
530
|
+
const { ch, stateFile, workspacePath } = makeChannel();
|
|
531
|
+
await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
532
|
+
expect(sentMethods(stateFile)).toEqual([
|
|
533
|
+
"session/create",
|
|
534
|
+
"session/subscribe",
|
|
535
|
+
"session/send",
|
|
536
|
+
"session/read",
|
|
537
|
+
"session/close",
|
|
538
|
+
]);
|
|
539
|
+
}, 10_000);
|
|
540
|
+
|
|
541
|
+
it("send 失败(-32010 busy)→ reject 且 close 仍被调用(try/finally)", async () => {
|
|
542
|
+
const { ch, stateFile, workspacePath } = makeChannel({
|
|
543
|
+
sendError: {
|
|
544
|
+
code: -32010,
|
|
545
|
+
message: "A prompt is already running for this session",
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
const err = await ch
|
|
549
|
+
.runTurn({ workspacePath, mode: "yolo" }, "做点什么")
|
|
550
|
+
.then(
|
|
551
|
+
() => {
|
|
552
|
+
throw new Error("should reject");
|
|
553
|
+
},
|
|
554
|
+
(e: unknown) => e
|
|
555
|
+
);
|
|
556
|
+
expect(isAppServerRpcError(err)).toBe(true);
|
|
557
|
+
if (isAppServerRpcError(err)) expect(err.code).toBe(-32010);
|
|
558
|
+
expect(sentFrames(stateFile, "session/close")).toHaveLength(1); // 失败路径也 close
|
|
559
|
+
}, 10_000);
|
|
560
|
+
|
|
561
|
+
it("send 返回 accepted:false → 显式 reject 且 close 已尽力", async () => {
|
|
562
|
+
const { ch, stateFile, workspacePath } = makeChannel({
|
|
563
|
+
sendResult: { accepted: false },
|
|
564
|
+
});
|
|
565
|
+
await expect(
|
|
566
|
+
ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么")
|
|
567
|
+
).rejects.toThrow(/accepted:false/);
|
|
568
|
+
expect(sentFrames(stateFile, "session/close")).toHaveLength(1);
|
|
569
|
+
}, 10_000);
|
|
570
|
+
|
|
571
|
+
it("终态超时(turn.terminal 与收尾帧均未达)→ reject 超时语境且 close 仍被调用", async () => {
|
|
572
|
+
const onlyRunning = [ZCODE_APPSERVER_GOLDEN.pushStream[0]]; // 仅 state.updated,无终态
|
|
573
|
+
const { ch, stateFile, workspacePath } = makeChannel({
|
|
574
|
+
replaceSendPushes: onlyRunning,
|
|
575
|
+
});
|
|
576
|
+
await expect(
|
|
577
|
+
ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么", {
|
|
578
|
+
turnTimeoutMs: 250,
|
|
579
|
+
})
|
|
580
|
+
).rejects.toThrow(/终态/);
|
|
581
|
+
expect(sentFrames(stateFile, "session/close")).toHaveLength(1);
|
|
582
|
+
}, 10_000);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// ============================================================
|
|
586
|
+
// 错误透传(R5 降级链地基:-32601/-32602 结构化透传)
|
|
587
|
+
// ============================================================
|
|
588
|
+
|
|
589
|
+
describe("错误透传(-32601/-32602 漂移类)", () => {
|
|
590
|
+
it("create -32602:reject 形态 isAppServerRpcError,code/message/data 原样(zod 诊断随 data 带出);无会话故无 close 帧", async () => {
|
|
591
|
+
const zodIssues = [{ path: ["model"], message: "not an object" }];
|
|
592
|
+
const { ch, stateFile, workspacePath } = makeChannel({
|
|
593
|
+
createError: { code: -32602, message: "Invalid params", data: zodIssues },
|
|
594
|
+
});
|
|
595
|
+
const err = await ch
|
|
596
|
+
.runTurn({ workspacePath, mode: "yolo" }, "做点什么")
|
|
597
|
+
.then(
|
|
598
|
+
() => {
|
|
599
|
+
throw new Error("should reject");
|
|
600
|
+
},
|
|
601
|
+
(e: unknown) => e
|
|
602
|
+
);
|
|
603
|
+
expect(isAppServerRpcError(err)).toBe(true);
|
|
604
|
+
if (isAppServerRpcError(err)) {
|
|
605
|
+
expect(err.code).toBe(-32602);
|
|
606
|
+
expect(err.data).toEqual(zodIssues);
|
|
607
|
+
expect(err.message).toContain("Invalid params");
|
|
608
|
+
expect(err.message).toContain("[-32602]");
|
|
609
|
+
}
|
|
610
|
+
// sessionId 未建立:无从 close(D4 会话尚未存在)
|
|
611
|
+
expect(sentFrames(stateFile, "session/close")).toHaveLength(0);
|
|
612
|
+
}, 10_000);
|
|
613
|
+
|
|
614
|
+
it("send -32602:透传且会话已建立 → close 已尽力(try/finally 不因透传跳过)", async () => {
|
|
615
|
+
const { ch, stateFile, workspacePath } = makeChannel({
|
|
616
|
+
sendError: {
|
|
617
|
+
code: -32602,
|
|
618
|
+
message: "Invalid params",
|
|
619
|
+
data: [{ path: ["content"] }],
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
const err = await ch
|
|
623
|
+
.runTurn({ workspacePath, mode: "yolo" }, "做点什么")
|
|
624
|
+
.then(
|
|
625
|
+
() => {
|
|
626
|
+
throw new Error("should reject");
|
|
627
|
+
},
|
|
628
|
+
(e: unknown) => e
|
|
629
|
+
);
|
|
630
|
+
expect(isAppServerRpcError(err)).toBe(true);
|
|
631
|
+
if (isAppServerRpcError(err)) expect(err.code).toBe(-32602);
|
|
632
|
+
expect(sentFrames(stateFile, "session/close")).toHaveLength(1);
|
|
633
|
+
}, 10_000);
|
|
634
|
+
|
|
635
|
+
it("-32601(方法不存在)同样结构化透传", async () => {
|
|
636
|
+
const { ch, workspacePath } = makeChannel({
|
|
637
|
+
createError: { code: -32601, message: "Method not found" },
|
|
638
|
+
});
|
|
639
|
+
const err = await ch.createSession({ workspacePath, mode: "yolo" }).then(
|
|
640
|
+
() => {
|
|
641
|
+
throw new Error("should reject");
|
|
642
|
+
},
|
|
643
|
+
(e: unknown) => e
|
|
644
|
+
);
|
|
645
|
+
expect(isAppServerRpcError(err)).toBe(true);
|
|
646
|
+
if (isAppServerRpcError(err)) {
|
|
647
|
+
expect(err.code).toBe(-32601);
|
|
648
|
+
expect(err.message).toContain("Method not found");
|
|
649
|
+
}
|
|
650
|
+
}, 10_000);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// ============================================================
|
|
654
|
+
// resolve 时序(不变量 2:resolve 严格晚于全部事件回调)
|
|
655
|
+
// ============================================================
|
|
656
|
+
|
|
657
|
+
describe("resolve 时序(不变量 2)", () => {
|
|
658
|
+
it("事件回调序在前、resolve 严格最后(终态判定 → read 兜底 → 才 resolve)", async () => {
|
|
659
|
+
const order: string[] = [];
|
|
660
|
+
const { ch, workspacePath } = makeChannel();
|
|
661
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么", {
|
|
662
|
+
onTextDelta: (d) => order.push(`delta:${d}`),
|
|
663
|
+
});
|
|
664
|
+
order.push("resolve");
|
|
665
|
+
expect(order).toEqual([
|
|
666
|
+
"delta:你好",
|
|
667
|
+
"delta:,",
|
|
668
|
+
"delta:任务完成",
|
|
669
|
+
"resolve",
|
|
670
|
+
]);
|
|
671
|
+
expect(r.response).toBe(GOLDEN_FULL_TEXT);
|
|
672
|
+
}, 10_000);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// ============================================================
|
|
676
|
+
// [R4] 连接崩溃收割(onClose 面 → failAllTurns——不再挂到 turnTimeoutMs)
|
|
677
|
+
// ============================================================
|
|
678
|
+
|
|
679
|
+
describe("连接崩溃收割(R4 onClose 面)", () => {
|
|
680
|
+
it("进程崩溃(test/suicide)→ 在途 turn 立即 reject(崩溃 reason 含 stderr 尾)——不等 turnTimeoutMs", async () => {
|
|
681
|
+
// 挂起场景(无终态)+ 长预算:崩溃收割前 turnTimeoutMs 兜底永远不会到
|
|
682
|
+
const onlyRunning = [ZCODE_APPSERVER_GOLDEN.pushStream[0]];
|
|
683
|
+
const { ch, conn, stateFile, workspacePath } = makeChannel({ replaceSendPushes: onlyRunning });
|
|
684
|
+
const turn = ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么", {
|
|
685
|
+
turnTimeoutMs: 60_000,
|
|
686
|
+
});
|
|
687
|
+
// 等 send 已达(fake 流水可观测)后在同一连接上触发自杀(崩溃收割的触发面)
|
|
688
|
+
await vi.waitFor(() => {
|
|
689
|
+
expect(sentFrames(stateFile, "session/send")).toHaveLength(1);
|
|
690
|
+
}, { timeout: 5_000 });
|
|
691
|
+
// suicide 不回应答——崩溃时该请求随 pending 一起 reject(预期,吞掉)
|
|
692
|
+
await conn.request("test/suicide").then(
|
|
693
|
+
() => undefined,
|
|
694
|
+
() => undefined,
|
|
695
|
+
);
|
|
696
|
+
const err = await turn.then(
|
|
697
|
+
() => {
|
|
698
|
+
throw new Error("should reject");
|
|
699
|
+
},
|
|
700
|
+
(e: unknown) => e,
|
|
701
|
+
);
|
|
702
|
+
expect(err).toBeInstanceOf(Error);
|
|
703
|
+
expect(String((err as Error).message)).toContain("app-server");
|
|
704
|
+
expect(String((err as Error).message)).toMatch(/进程退出|spawn 失败|CRASH-MARK-TAIL/);
|
|
705
|
+
}, 10_000);
|
|
706
|
+
|
|
707
|
+
it("post:fire-and-forget 帧不等待应答(活连接 true;返回布尔不抛)", async () => {
|
|
708
|
+
const { conn } = makeChannel();
|
|
709
|
+
expect(typeof conn.post).toBe("function");
|
|
710
|
+
expect(conn.post("session/close", { sessionId: "sess_x" })).toBe(true);
|
|
711
|
+
await conn.shutdown({ graceMs: 1_000 });
|
|
712
|
+
// 死连接:ensureStarted 重建一代(D1 重建语义)→ post 复又可用;不抛即可
|
|
713
|
+
expect(typeof conn.post("session/close", { sessionId: "sess_x" })).toBe("boolean");
|
|
714
|
+
await conn.shutdown({ graceMs: 1_000 });
|
|
715
|
+
}, 10_000);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// ============================================================
|
|
719
|
+
// golden 双副本与帧序列语料(R3 验收 ②)
|
|
720
|
+
// ============================================================
|
|
721
|
+
|
|
722
|
+
describe("golden 双副本与四类样本", () => {
|
|
723
|
+
it("双副本 diff:golden-sample.ts 内嵌副本 === fixture frames(防漂移,更新需同步两处)", () => {
|
|
724
|
+
expect(ZCODE_APPSERVER_GOLDEN).toEqual(GOLDEN_FIXTURE.frames);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("四类样本形态自检:create 可提取 sessionId≠unknown;推送流含三 method 且 stream.chunk 无文本;终态双帧;read 可提取全文与 tokens", () => {
|
|
728
|
+
// ① create 应答
|
|
729
|
+
expect(
|
|
730
|
+
extractCreatedSessionId(JSON.parse(ZCODE_APPSERVER_GOLDEN.createResponse))
|
|
731
|
+
).toBe(GOLDEN_SESSION_ID);
|
|
732
|
+
// ④ 推送流:state.updated + stream.chunk(无文本)+ session/event(文本在此)
|
|
733
|
+
const pushMethods = ZCODE_APPSERVER_GOLDEN.pushStream.map(
|
|
734
|
+
(l) => (JSON.parse(l) as { method: string }).method
|
|
735
|
+
);
|
|
736
|
+
expect(new Set(pushMethods)).toEqual(
|
|
737
|
+
new Set(["state.updated", "v4/telemetry/event", "session/event"])
|
|
738
|
+
);
|
|
739
|
+
const streamChunk = JSON.parse(ZCODE_APPSERVER_GOLDEN.pushStream[1]) as {
|
|
740
|
+
params: Record<string, unknown>;
|
|
741
|
+
};
|
|
742
|
+
expect(streamChunk.params.kind).toBe("stream.chunk");
|
|
743
|
+
for (const textKey of ["chunk", "text", "content", "delta"]) {
|
|
744
|
+
expect(streamChunk.params[textKey]).toBeUndefined(); // A.2:stream.chunk 无文本
|
|
745
|
+
}
|
|
746
|
+
const deltaFrame = JSON.parse(ZCODE_APPSERVER_GOLDEN.pushStream[2]) as {
|
|
747
|
+
params: { payload: { delta: string } };
|
|
748
|
+
};
|
|
749
|
+
expect(deltaFrame.params.payload.delta).toBe("你好");
|
|
750
|
+
// ⑤ 终态双帧
|
|
751
|
+
const terminalFrame = JSON.parse(ZCODE_APPSERVER_GOLDEN.terminal[0]) as {
|
|
752
|
+
params: { kind: string; status: string };
|
|
753
|
+
};
|
|
754
|
+
expect(terminalFrame.params).toEqual({
|
|
755
|
+
kind: "turn.terminal",
|
|
756
|
+
status: "success",
|
|
757
|
+
});
|
|
758
|
+
const finalFrame = JSON.parse(ZCODE_APPSERVER_GOLDEN.terminal[1]) as {
|
|
759
|
+
params: { payload: { response: string; usage: unknown } };
|
|
760
|
+
};
|
|
761
|
+
expect(finalFrame.params.payload.response).toBe(GOLDEN_FULL_TEXT);
|
|
762
|
+
expect(finalFrame.params.payload.usage).toEqual(GOLDEN_FINAL_USAGE);
|
|
763
|
+
// ⑥ read 应答
|
|
764
|
+
expect(
|
|
765
|
+
extractAssistantText(JSON.parse(ZCODE_APPSERVER_GOLDEN.readResponse))
|
|
766
|
+
).toBe(GOLDEN_FULL_TEXT);
|
|
767
|
+
expect(
|
|
768
|
+
extractReadUsage(JSON.parse(ZCODE_APPSERVER_GOLDEN.readResponse))
|
|
769
|
+
).toEqual(GOLDEN_READ_TOKENS);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("golden 帧序列全链回放(fake 逐字回放四类样本):不变量 1 + usage + sessionId 一次收口", async () => {
|
|
773
|
+
const { ch, workspacePath } = makeChannel(); // scenario 即 golden 四类样本
|
|
774
|
+
const r = await ch.runTurn({ workspacePath, mode: "yolo" }, "做点什么");
|
|
775
|
+
expect(r.sessionId).toBe(GOLDEN_SESSION_ID);
|
|
776
|
+
expect(r.response).toBe(GOLDEN_FULL_TEXT); // delta 拼接 == read 全文 == 收尾帧
|
|
777
|
+
expect(r.usage).toEqual(GOLDEN_FINAL_USAGE);
|
|
778
|
+
expect(r.terminal).toEqual({ status: "success", source: "turn.terminal" });
|
|
779
|
+
}, 10_000);
|
|
780
|
+
});
|