@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,815 @@
|
|
|
1
|
+
// zcode-engine-appserver.test.ts —— [R4] 引擎接线测试:全部跑 __fixtures__/
|
|
2
|
+
// fake-appserver.mjs 子进程(scenario 注入),绝不 spawn 真 zcode.cjs。覆盖:
|
|
3
|
+
// 模式分派骨架 / 事件时序前移(text_delta 流式 + 终态 message_end/turn_end +
|
|
4
|
+
// resolve 严格晚于末事件——不变量 2)/ usage 映射(与 parser.mapZcodeUsage 同源)/
|
|
5
|
+
// per-session model 透传 / 常驻 HOME 锚定(poolKey='home-appserver' + allProviders
|
|
6
|
+
// config + fake 侧 HOME/env 断言)/ onHandleReady 回调时点(create 应答后)与
|
|
7
|
+
// onPoolResolved 时点(prepare 期,先于首事件)/ abort 链 D3(stop 帧先发 → stop
|
|
8
|
+
// 优雅生效不杀进程 / stop 无效 grace 耗尽 → killChain 连坐)/ 凭据刷新(内容 hash
|
|
9
|
+
// 不一致 → config 重写 + 连接重建)/ dispose(幂等 / close 帧先于 SIGTERM / dispose
|
|
10
|
+
// 后 run 重建——不变量 4)/ 多会话并发不串线(RA3 地基,FAKE_STAMP_SESSION)/
|
|
11
|
+
// -32603 Model config missing 错误归类 / capabilities 断言(eventGranularity 翻转
|
|
12
|
+
// 无下游破坏的生产零消费方守卫)。
|
|
13
|
+
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
20
|
+
|
|
21
|
+
import type { EngineRunResult, RunContext } from "../../../port.ts";
|
|
22
|
+
import type { AgentEvent, AgentTaskSpec } from "../../../types.ts";
|
|
23
|
+
import { getLogger } from "../../../../../core/logger.ts";
|
|
24
|
+
import { resolvePoolDir } from "../../../paths.ts";
|
|
25
|
+
import { ZCODE_APPSERVER_POOL_KEY, ZCODE_POOL_CONFIG_SUFFIX } from "../constants.ts";
|
|
26
|
+
import { ZCODE_APPSERVER_GOLDEN } from "../golden-sample.ts";
|
|
27
|
+
import { AppServerConnection } from "../connection.ts";
|
|
28
|
+
import { SessionChannel } from "../session-channel.ts";
|
|
29
|
+
import { ZcodeEngine, pinnedZcodeMode, type ZcodeEngineDeps } from "../zcode-engine.ts";
|
|
30
|
+
|
|
31
|
+
const FAKE_CLI = fileURLToPath(new URL("./__fixtures__/fake-appserver.mjs", import.meta.url));
|
|
32
|
+
const PROVIDER = "test-provider";
|
|
33
|
+
|
|
34
|
+
/** 与 zcode-engine.ts 同一 facade 单例引用(spy 它的方法即拦截引擎的 warn 出声)。 */
|
|
35
|
+
const subagentsLogger = getLogger("subagents");
|
|
36
|
+
|
|
37
|
+
const GOLDEN_SESSION_ID = "sess_golden_r3_01";
|
|
38
|
+
const GOLDEN_FULL_TEXT = "你好,任务完成";
|
|
39
|
+
|
|
40
|
+
interface RpcErrorSpec {
|
|
41
|
+
code: number;
|
|
42
|
+
message: string;
|
|
43
|
+
data?: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface EngineHandle_ {
|
|
47
|
+
engine: ZcodeEngine;
|
|
48
|
+
stateFile: string;
|
|
49
|
+
workspace: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** run 返回形态别名(abort 用例的中止终态断言面)。 */
|
|
53
|
+
type EngineRunResult_ = EngineRunResult;
|
|
54
|
+
|
|
55
|
+
const engines: ZcodeEngine[] = [];
|
|
56
|
+
let seq = 0;
|
|
57
|
+
let tmpRoot: string;
|
|
58
|
+
let dataDir: string;
|
|
59
|
+
let v2Path: string;
|
|
60
|
+
|
|
61
|
+
function writeJson(p: string, v: unknown): void {
|
|
62
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
63
|
+
fs.writeFileSync(p, JSON.stringify(v, null, 2));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "zcode-eng-appserver-"));
|
|
68
|
+
dataDir = path.join(tmpRoot, "data");
|
|
69
|
+
v2Path = path.join(tmpRoot, "v2.json");
|
|
70
|
+
writeJson(v2Path, {
|
|
71
|
+
provider: { [PROVIDER]: { options: { apiKey: "k", baseURL: "https://t.example" }, models: { m1: {} } } },
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(async () => {
|
|
76
|
+
for (const engine of engines.splice(0)) await engine.dispose().catch(() => undefined);
|
|
77
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
interface ScenarioOverrides {
|
|
81
|
+
dropTurnTerminal?: boolean;
|
|
82
|
+
replaceSendPushes?: string[];
|
|
83
|
+
createError?: RpcErrorSpec;
|
|
84
|
+
/** send 应答 error 帧(运行中失败注入——-32004 会话 id 留痕用例)。 */
|
|
85
|
+
sendError?: RpcErrorSpec;
|
|
86
|
+
stopBehavior?: "terminal" | "none";
|
|
87
|
+
stampSession?: boolean;
|
|
88
|
+
/** 覆盖 read 应答(read 是全文权威来源——schema 用例须与收尾帧一致)。 */
|
|
89
|
+
readResult?: unknown;
|
|
90
|
+
/** 多会话用例:不覆盖 createResult/readResult(fake 缺省每会话独立 sess_<id> + read {messages:[]})。 */
|
|
91
|
+
distinctSessionIds?: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 建一个连到 fake 的引擎(scenario 文件随 env 固化进子进程;每测试独立 state 文件)。 */
|
|
95
|
+
function makeEngine(overrides: ScenarioOverrides = {}): EngineHandle_ {
|
|
96
|
+
seq += 1;
|
|
97
|
+
const stateFile = path.join(tmpRoot, `state-${seq}.jsonl`);
|
|
98
|
+
const scenarioFile = path.join(tmpRoot, `scenario-${seq}.json`);
|
|
99
|
+
const workspace = path.join(tmpRoot, `ws-${seq}`);
|
|
100
|
+
const pushes = (
|
|
101
|
+
overrides.replaceSendPushes ?? [
|
|
102
|
+
...ZCODE_APPSERVER_GOLDEN.pushStream,
|
|
103
|
+
...(overrides.dropTurnTerminal ? [] : [ZCODE_APPSERVER_GOLDEN.terminal[0]]),
|
|
104
|
+
ZCODE_APPSERVER_GOLDEN.terminal[1],
|
|
105
|
+
]
|
|
106
|
+
).map((l) => (typeof l === "string" ? (JSON.parse(l) as Record<string, unknown>) : l));
|
|
107
|
+
writeJson(scenarioFile, {
|
|
108
|
+
...(overrides.distinctSessionIds === true
|
|
109
|
+
? {}
|
|
110
|
+
: {
|
|
111
|
+
createResult: JSON.parse(ZCODE_APPSERVER_GOLDEN.createResponse),
|
|
112
|
+
readResult: overrides.readResult ?? JSON.parse(ZCODE_APPSERVER_GOLDEN.readResponse),
|
|
113
|
+
}),
|
|
114
|
+
sendPushes: pushes,
|
|
115
|
+
...(overrides.stopBehavior !== undefined ? { stopBehavior: overrides.stopBehavior } : {}),
|
|
116
|
+
...(overrides.createError !== undefined ? { createError: overrides.createError } : {}),
|
|
117
|
+
...(overrides.sendError !== undefined ? { sendError: overrides.sendError } : {}),
|
|
118
|
+
});
|
|
119
|
+
const deps: ZcodeEngineDeps = {
|
|
120
|
+
engineDataDir: () => dataDir,
|
|
121
|
+
cliPath: FAKE_CLI,
|
|
122
|
+
sources: { v2ConfigPath: v2Path },
|
|
123
|
+
processEnv: {
|
|
124
|
+
PATH: process.env.PATH ?? "",
|
|
125
|
+
// [R5] 钉扎 appserver 定向(定向不探不降):本文件测常驻路径本体;缺省路径的
|
|
126
|
+
// 探针门控/降级链见 zcode-engine-degrade.test.ts
|
|
127
|
+
XYZ_ZCODE_MODE: "appserver",
|
|
128
|
+
FAKE_STATE_FILE: stateFile,
|
|
129
|
+
FAKE_SESSION_SCENARIO: scenarioFile,
|
|
130
|
+
...(overrides.stampSession === true ? { FAKE_STAMP_SESSION: "1" } : {}),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const engine = new ZcodeEngine(deps);
|
|
134
|
+
engines.push(engine);
|
|
135
|
+
return { engine, stateFile, workspace };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── 流水读取 helpers(与 session-channel.test.ts 同款) ──
|
|
139
|
+
|
|
140
|
+
interface StateEvent {
|
|
141
|
+
seq: number;
|
|
142
|
+
ev: string;
|
|
143
|
+
[key: string]: unknown;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readState(file: string): StateEvent[] {
|
|
147
|
+
try {
|
|
148
|
+
return fs
|
|
149
|
+
.readFileSync(file, "utf8")
|
|
150
|
+
.trim()
|
|
151
|
+
.split("\n")
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.map((l) => JSON.parse(l) as StateEvent);
|
|
154
|
+
} catch {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
160
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function sentFrames(stateFile: string, method: string): Array<{ id: number; params: Record<string, unknown> }> {
|
|
164
|
+
return readState(stateFile)
|
|
165
|
+
.map((e) => e.frame)
|
|
166
|
+
.filter(
|
|
167
|
+
(f): f is { id: number; params: Record<string, unknown> } =>
|
|
168
|
+
isRecord(f) && f.method === method && isRecord(f.params) && typeof f.id === "number",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function sentMethods(stateFile: string): string[] {
|
|
173
|
+
return readState(stateFile)
|
|
174
|
+
.map((e) => e.frame)
|
|
175
|
+
.filter((f): f is Record<string, unknown> => isRecord(f) && typeof f.method === "string")
|
|
176
|
+
.map((f) => f.method as string);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function bootCount(stateFile: string): number {
|
|
180
|
+
return readState(stateFile).filter((e) => e.ev === "boot").length;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function bootEnv(stateFile: string): Record<string, unknown> {
|
|
184
|
+
const env = readState(stateFile).find((e) => e.ev === "env");
|
|
185
|
+
return isRecord(env) ? env : {};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function makeTask(overrides?: Partial<AgentTaskSpec>): AgentTaskSpec {
|
|
189
|
+
return { task: "做点什么", slug: "s", model: `${PROVIDER}/m1`, ...overrides };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function makeCtx(overrides?: Partial<RunContext>): RunContext {
|
|
193
|
+
return { taskId: "sa-appserver", poolKey: "", ...overrides };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================
|
|
197
|
+
// 模式分派骨架(D2——降级链归 R5,此处只验分派点)
|
|
198
|
+
// ============================================================
|
|
199
|
+
|
|
200
|
+
describe("模式分派(XYZ_ZCODE_MODE)", () => {
|
|
201
|
+
it("pinnedZcodeMode 三态:定向值原样;缺省/未知值 undefined(undefined → 缺省 appserver 路径的探针门控 run 分派由 degrade 套件覆盖)", () => {
|
|
202
|
+
expect(pinnedZcodeMode({ XYZ_ZCODE_MODE: "spawn" })).toBe("spawn");
|
|
203
|
+
expect(pinnedZcodeMode({ XYZ_ZCODE_MODE: "appserver" })).toBe("appserver");
|
|
204
|
+
expect(pinnedZcodeMode({})).toBeUndefined();
|
|
205
|
+
expect(pinnedZcodeMode({ XYZ_ZCODE_MODE: "garbage" })).toBeUndefined();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("spawn 定向:launch 通道走原 spawn 路径(不 spawn app-server——boot 计数 0)", async () => {
|
|
209
|
+
const fakeLaunch = vi.fn(() => {
|
|
210
|
+
throw new Error("spawn 定向下不应走 app-server 连接;launch fake 仅供模式断言");
|
|
211
|
+
});
|
|
212
|
+
// spawn 定向 + 无 launch fake 会真 spawn zcode.cjs(缺省路径不存在)——用
|
|
213
|
+
// launch fake 拦截即可证明走了 spawn 分支
|
|
214
|
+
const { engine } = { engine: new ZcodeEngine({
|
|
215
|
+
engineDataDir: () => dataDir,
|
|
216
|
+
cliPath: FAKE_CLI,
|
|
217
|
+
sources: { v2ConfigPath: v2Path },
|
|
218
|
+
processEnv: { PATH: process.env.PATH ?? "", XYZ_ZCODE_MODE: "spawn" },
|
|
219
|
+
launch: fakeLaunch as unknown as ZcodeEngineDeps["launch"],
|
|
220
|
+
}) };
|
|
221
|
+
engines.push(engine);
|
|
222
|
+
await expect(engine.run(makeTask(), makeCtx())).rejects.toThrow(/无法启动 zcode CLI/);
|
|
223
|
+
expect(fakeLaunch).toHaveBeenCalledTimes(1);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ============================================================
|
|
228
|
+
// 事件时序前移 + usage 映射 + 句柄回调时点(不变量 2/3)
|
|
229
|
+
// ============================================================
|
|
230
|
+
|
|
231
|
+
describe("事件流与回调时点(缺省 appserver 路径)", () => {
|
|
232
|
+
it("text_delta 逐个实时流出 + 终态 message_end(usage)/turn_end + resolve 严格晚于末事件", async () => {
|
|
233
|
+
const { engine, workspace } = makeEngine();
|
|
234
|
+
const order: string[] = [];
|
|
235
|
+
const events: AgentEvent[] = [];
|
|
236
|
+
const { handle, outcome } = await engine.run(
|
|
237
|
+
makeTask({ cwd: workspace }),
|
|
238
|
+
makeCtx({
|
|
239
|
+
onEvent: (e) => {
|
|
240
|
+
events.push(e);
|
|
241
|
+
order.push(e.type);
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
order.push("resolve");
|
|
246
|
+
expect(order).toEqual([
|
|
247
|
+
"text_delta",
|
|
248
|
+
"text_delta",
|
|
249
|
+
"text_delta",
|
|
250
|
+
"message_end",
|
|
251
|
+
"turn_end",
|
|
252
|
+
"resolve",
|
|
253
|
+
]);
|
|
254
|
+
// text_delta 逐字(golden pushStream 的三条 session/event delta)
|
|
255
|
+
expect(events[0]).toEqual({ type: "text_delta", delta: "你好" });
|
|
256
|
+
expect(events[1]).toEqual({ type: "text_delta", delta: "," });
|
|
257
|
+
expect(events[2]).toEqual({ type: "text_delta", delta: "任务完成" });
|
|
258
|
+
// message_end.usage:收尾帧 usage 经 parser.mapZcodeUsage 同源映射
|
|
259
|
+
expect(events[3]).toEqual({
|
|
260
|
+
type: "message_end",
|
|
261
|
+
usage: { input: 12599, output: 17, cacheRead: 0, cacheWrite: 0 },
|
|
262
|
+
});
|
|
263
|
+
expect(events[4]).toEqual({ type: "turn_end" });
|
|
264
|
+
|
|
265
|
+
// outcome:content == 全文;usage(orchestration 版,mapZcodeOutcomeUsage 同源)
|
|
266
|
+
expect(outcome.error).toBeUndefined();
|
|
267
|
+
expect(outcome.content).toBe(GOLDEN_FULL_TEXT);
|
|
268
|
+
expect(outcome.sessionId).toBe(GOLDEN_SESSION_ID);
|
|
269
|
+
expect(outcome.usage).toEqual({ input: 12599, output: 17, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 1 });
|
|
270
|
+
expect(outcome.exitCode).toBe(0);
|
|
271
|
+
|
|
272
|
+
// handle 锚定:poolKey=home-appserver,sessionRef 相对锚 HOME
|
|
273
|
+
expect(handle.data.poolKey).toBe(ZCODE_APPSERVER_POOL_KEY);
|
|
274
|
+
expect(handle.data.sessionRef).toEqual({ sessionId: GOLDEN_SESSION_ID, dbPath: ".zcode/cli/db/db.sqlite" });
|
|
275
|
+
}, 15_000);
|
|
276
|
+
|
|
277
|
+
it("回调时点:onPoolResolved(prepare 期)先于 onHandleReady(create 应答后)先于首事件", async () => {
|
|
278
|
+
const { engine, workspace } = makeEngine();
|
|
279
|
+
const order: string[] = [];
|
|
280
|
+
await engine.run(
|
|
281
|
+
makeTask({ cwd: workspace }),
|
|
282
|
+
makeCtx({
|
|
283
|
+
onPoolResolved: (k) => order.push(`pool:${k}`),
|
|
284
|
+
onHandleReady: (partial) => order.push(`handle:${partial.poolKey}:${partial.sessionRef["sessionId"]}`),
|
|
285
|
+
onEvent: (e) => {
|
|
286
|
+
if (e.type === "text_delta" && order[order.length - 1] !== "event") order.push("event");
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
expect(order[0]).toBe(`pool:${ZCODE_APPSERVER_POOL_KEY}`);
|
|
291
|
+
expect(order[1]).toBe(`handle:${ZCODE_APPSERVER_POOL_KEY}:${GOLDEN_SESSION_ID}`);
|
|
292
|
+
expect(order[2]).toBe("event");
|
|
293
|
+
}, 15_000);
|
|
294
|
+
|
|
295
|
+
it("常驻 HOME 锚定 + allProviders config + fake 侧 env/argv(HOME=池目录、遥测关、argv=app-server --cwd HOME)", async () => {
|
|
296
|
+
const { engine, stateFile, workspace } = makeEngine();
|
|
297
|
+
const { handle } = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
298
|
+
const homeDir = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
299
|
+
expect(handle.data.poolKey).toBe(ZCODE_APPSERVER_POOL_KEY);
|
|
300
|
+
// 常驻 config:allProviders(本测试源只有 test-provider 一个带 key 条目)
|
|
301
|
+
const configPath = path.join(homeDir, ...ZCODE_POOL_CONFIG_SUFFIX);
|
|
302
|
+
const written = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
303
|
+
expect(Object.keys(written["provider"] as object)).toEqual([PROVIDER]);
|
|
304
|
+
// fake 侧 boot env:HOME=常驻 HOME、遥测 false、嵌套标记
|
|
305
|
+
const env = bootEnv(stateFile);
|
|
306
|
+
expect(env["home"]).toBe(homeDir);
|
|
307
|
+
expect(env["telemetry"]).toBe("false");
|
|
308
|
+
expect(env["unifiedNested"]).toBe("1");
|
|
309
|
+
const boot = readState(stateFile).find((e) => e.ev === "boot");
|
|
310
|
+
expect(boot?.["argv"]).toEqual(["app-server", "--cwd", homeDir]);
|
|
311
|
+
// 锁文件落 HOME
|
|
312
|
+
expect(fs.existsSync(path.join(homeDir, "lockfile"))).toBe(true);
|
|
313
|
+
// pidfile 已写(app-server 进程 pid——与 lockfile.pid 分离)
|
|
314
|
+
const pidfile = JSON.parse(fs.readFileSync(path.join(homeDir, "appserver.pid"), "utf8")) as { pid: number };
|
|
315
|
+
const lockfilePid = (JSON.parse(fs.readFileSync(path.join(homeDir, "lockfile"), "utf8")) as { pid: number }).pid;
|
|
316
|
+
expect(typeof pidfile.pid).toBe("number");
|
|
317
|
+
expect(pidfile.pid).not.toBe(lockfilePid); // D7 pid 归属分离:app-server pid ≠ 宿主 pid
|
|
318
|
+
expect(lockfilePid).toBe(process.pid); // lockfile.pid = 持锁宿主(本测试进程)
|
|
319
|
+
}, 15_000);
|
|
320
|
+
|
|
321
|
+
it("per-session model:create 帧 model={providerId,modelId}(task.model 拆分)+ toolDenylist 透传", async () => {
|
|
322
|
+
const { engine, stateFile, workspace } = makeEngine();
|
|
323
|
+
await engine.run(makeTask({ cwd: workspace, denyTools: ["bash", ""] }), makeCtx());
|
|
324
|
+
const create = sentFrames(stateFile, "session/create")[0];
|
|
325
|
+
expect(create.params["model"]).toEqual({ providerId: PROVIDER, modelId: "m1" });
|
|
326
|
+
expect(create.params["toolDenylist"]).toEqual(["bash"]); // 空串过滤
|
|
327
|
+
expect(create.params["workspace"]).toMatchObject({ workspacePath: workspace });
|
|
328
|
+
expect(create.params["mode"]).toBe("yolo");
|
|
329
|
+
}, 15_000);
|
|
330
|
+
|
|
331
|
+
it("effort → create 帧 thoughtLevel 透传(F15a:spec.effort 映射协议通道)", async () => {
|
|
332
|
+
const { engine, stateFile, workspace } = makeEngine();
|
|
333
|
+
await engine.run(makeTask({ cwd: workspace, effort: "high" }), makeCtx());
|
|
334
|
+
const create = sentFrames(stateFile, "session/create")[0];
|
|
335
|
+
expect(create.params["thoughtLevel"]).toBe("high");
|
|
336
|
+
}, 15_000);
|
|
337
|
+
|
|
338
|
+
it("effort 未传/空白 → create 帧无 thoughtLevel 键(A.2 ① strict 对象不设空键,防 -32602)", async () => {
|
|
339
|
+
const { engine, stateFile, workspace } = makeEngine();
|
|
340
|
+
await engine.run(makeTask({ cwd: workspace, effort: " " }), makeCtx());
|
|
341
|
+
const create = sentFrames(stateFile, "session/create")[0];
|
|
342
|
+
expect("thoughtLevel" in create.params).toBe(false);
|
|
343
|
+
}, 15_000);
|
|
344
|
+
|
|
345
|
+
it("effort=medium(非常见档位)→ warn 一行提示且 thoughtLevel 键仍透传(RX2-F1:提示不拦截)", async () => {
|
|
346
|
+
const { engine, stateFile, workspace } = makeEngine();
|
|
347
|
+
const warns: string[] = [];
|
|
348
|
+
const warnSpy = vi.spyOn(subagentsLogger, "warn").mockImplementation(((msg: string) => {
|
|
349
|
+
warns.push(msg);
|
|
350
|
+
}) as typeof subagentsLogger.warn);
|
|
351
|
+
try {
|
|
352
|
+
await engine.run(makeTask({ cwd: workspace, effort: "medium" }), makeCtx());
|
|
353
|
+
} finally {
|
|
354
|
+
warnSpy.mockRestore();
|
|
355
|
+
}
|
|
356
|
+
// 提示恰一行;措辞是「若不支持将被忽略/回落」的或然警告(core 不做权威值域断言)
|
|
357
|
+
const hits = warns.filter((m) => m.includes("thoughtLevel"));
|
|
358
|
+
expect(hits).toHaveLength(1);
|
|
359
|
+
expect(hits[0]).toContain("medium");
|
|
360
|
+
expect(hits[0]).toContain("将被忽略/回落");
|
|
361
|
+
expect(hits[0]).toContain("low/high/max");
|
|
362
|
+
// 透传不拦截:create 帧仍带原值(off/minimal/medium/xhigh 全放行)
|
|
363
|
+
const create = sentFrames(stateFile, "session/create")[0];
|
|
364
|
+
expect(create.params["thoughtLevel"]).toBe("medium");
|
|
365
|
+
}, 15_000);
|
|
366
|
+
|
|
367
|
+
it("effort=high(常见档位)→ 零 thoughtLevel 提示(RX2-F1:常见档位不出声)", async () => {
|
|
368
|
+
const { engine, workspace } = makeEngine();
|
|
369
|
+
const warns: string[] = [];
|
|
370
|
+
const warnSpy = vi.spyOn(subagentsLogger, "warn").mockImplementation(((msg: string) => {
|
|
371
|
+
warns.push(msg);
|
|
372
|
+
}) as typeof subagentsLogger.warn);
|
|
373
|
+
try {
|
|
374
|
+
await engine.run(makeTask({ cwd: workspace, effort: "high" }), makeCtx());
|
|
375
|
+
} finally {
|
|
376
|
+
warnSpy.mockRestore();
|
|
377
|
+
}
|
|
378
|
+
expect(warns.filter((m) => m.includes("thoughtLevel"))).toHaveLength(0);
|
|
379
|
+
}, 15_000);
|
|
380
|
+
|
|
381
|
+
it("task.model 缺省 + ctx.ctxModel 存在 → 出声留痕(F16b:ctxModel 是 pi 链路兜底,zcode 落自身缺省链)", async () => {
|
|
382
|
+
// v2 config 补缺省模型 provider 条目——ZCODE_FALLBACK_DEFAULT_MODEL 的解析链才可校验通过
|
|
383
|
+
writeJson(v2Path, {
|
|
384
|
+
provider: {
|
|
385
|
+
[PROVIDER]: { options: { apiKey: "k", baseURL: "https://t.example" }, models: { m1: {} } },
|
|
386
|
+
"builtin:bigmodel-coding-plan": { options: { apiKey: "k" }, models: { "GLM-5.3": {} } },
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
const { engine, workspace } = makeEngine();
|
|
390
|
+
const warns: Array<{ msg: string; data: unknown }> = [];
|
|
391
|
+
const warnSpy = vi.spyOn(subagentsLogger, "warn").mockImplementation(((msg: string, data: unknown) => {
|
|
392
|
+
warns.push({ msg, data });
|
|
393
|
+
}) as typeof subagentsLogger.warn);
|
|
394
|
+
try {
|
|
395
|
+
const { outcome } = await engine.run(
|
|
396
|
+
makeTask({ cwd: workspace, model: undefined }),
|
|
397
|
+
makeCtx({ ctxModel: { id: "main-model", name: "Main", provider: "p", reasoning: false } }),
|
|
398
|
+
);
|
|
399
|
+
expect(outcome.error).toBeUndefined(); // 信号不阻断任务
|
|
400
|
+
const hit = warns.find((w) => w.msg.includes("ctxModel"));
|
|
401
|
+
expect(hit).toBeDefined();
|
|
402
|
+
expect(hit?.msg).toContain("main-model");
|
|
403
|
+
expect(hit?.msg).toContain("builtin:bigmodel-coding-plan/GLM-5.3");
|
|
404
|
+
} finally {
|
|
405
|
+
warnSpy.mockRestore();
|
|
406
|
+
}
|
|
407
|
+
}, 15_000);
|
|
408
|
+
|
|
409
|
+
it("显式 task.model 或 ctx 无 ctxModel → 零 ctxModel 信号(F16b:只在「ctx 有模型但被忽略」时出声)", async () => {
|
|
410
|
+
// 第二段 run(model 缺省)走引擎缺省链——v2 config 需含 ZCODE_FALLBACK_DEFAULT_MODEL 的 provider
|
|
411
|
+
writeJson(v2Path, {
|
|
412
|
+
provider: {
|
|
413
|
+
[PROVIDER]: { options: { apiKey: "k", baseURL: "https://t.example" }, models: { m1: {} } },
|
|
414
|
+
"builtin:bigmodel-coding-plan": { options: { apiKey: "k" }, models: { "GLM-5.3": {} } },
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
const { engine, workspace } = makeEngine();
|
|
418
|
+
const warns: string[] = [];
|
|
419
|
+
const warnSpy = vi.spyOn(subagentsLogger, "warn").mockImplementation(((msg: string) => {
|
|
420
|
+
warns.push(msg);
|
|
421
|
+
}) as typeof subagentsLogger.warn);
|
|
422
|
+
try {
|
|
423
|
+
// 显式 task.model + ctxModel:走正常解析链,不出声
|
|
424
|
+
await engine.run(makeTask({ cwd: workspace }), makeCtx({ ctxModel: { id: "main-model", name: "Main", provider: "p", reasoning: false } }));
|
|
425
|
+
// model 缺省 + ctx 无 ctxModel:预期缺省链,不出声
|
|
426
|
+
await engine.run(makeTask({ cwd: workspace, model: undefined }), makeCtx());
|
|
427
|
+
expect(warns.filter((m) => m.includes("ctxModel"))).toHaveLength(0);
|
|
428
|
+
} finally {
|
|
429
|
+
warnSpy.mockRestore();
|
|
430
|
+
}
|
|
431
|
+
}, 15_000);
|
|
432
|
+
|
|
433
|
+
it("schema 任务:appserver 路径同接 schema 仿真(合法 JSON → parsedOutput)", async () => {
|
|
434
|
+
const finalText = '{"verdict":"ok"}';
|
|
435
|
+
const { engine, workspace } = makeEngine({
|
|
436
|
+
replaceSendPushes: buildJsonPushes(finalText),
|
|
437
|
+
// read 是全文权威来源——须与收尾帧一致(否则 read 兜底覆盖掉 JSON 全文)
|
|
438
|
+
readResult: { messages: [{ info: { role: "assistant" }, parts: [{ type: "text", text: finalText }] }] },
|
|
439
|
+
});
|
|
440
|
+
const { outcome } = await engine.run(
|
|
441
|
+
makeTask({ cwd: workspace, schema: { type: "object", properties: { verdict: { type: "string" } }, required: ["verdict"] } }),
|
|
442
|
+
makeCtx(),
|
|
443
|
+
);
|
|
444
|
+
expect(outcome.error).toBeUndefined();
|
|
445
|
+
expect(outcome.parsedOutput).toEqual({ verdict: "ok" });
|
|
446
|
+
}, 15_000);
|
|
447
|
+
|
|
448
|
+
it("schema 失败重试一轮:两会话两 send(重试轮独立 create),usage 两轮累计", async () => {
|
|
449
|
+
const { engine, stateFile, workspace } = makeEngine({
|
|
450
|
+
replaceSendPushes: buildJsonPushes("I think everything is fine."),
|
|
451
|
+
});
|
|
452
|
+
// 重试轮仍返回非 JSON(scenario 固化)→ 两轮均失败 → schema_emulation_failed
|
|
453
|
+
const { outcome } = await engine.run(
|
|
454
|
+
makeTask({ cwd: workspace, schema: { type: "object", properties: { verdict: { type: "string" } }, required: ["verdict"] } }),
|
|
455
|
+
makeCtx(),
|
|
456
|
+
);
|
|
457
|
+
expect(outcome.error).toContain("schema_emulation_failed");
|
|
458
|
+
expect(sentFrames(stateFile, "session/create")).toHaveLength(2);
|
|
459
|
+
expect(sentFrames(stateFile, "session/send")).toHaveLength(2);
|
|
460
|
+
// 重试 prompt 带强化指令
|
|
461
|
+
const sends = sentFrames(stateFile, "session/send");
|
|
462
|
+
expect(String(sends[1]?.params["content"])).toContain("Retry: Structured Output Failed");
|
|
463
|
+
}, 15_000);
|
|
464
|
+
|
|
465
|
+
it("-32603 'Model config is missing' → engine_credential_missing 归类(outcome 不 reject)", async () => {
|
|
466
|
+
const { engine, workspace } = makeEngine({
|
|
467
|
+
createError: { code: -32603, message: "Model config is missing" },
|
|
468
|
+
});
|
|
469
|
+
const { outcome } = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
470
|
+
expect(outcome.error).toContain("engine_credential_missing");
|
|
471
|
+
expect(outcome.error).toContain("Model config is missing");
|
|
472
|
+
}, 15_000);
|
|
473
|
+
|
|
474
|
+
it("create 漂移错误(-32602)→ run-failed outcome 透传 code(R5 降级链地基)", async () => {
|
|
475
|
+
const { engine, workspace } = makeEngine({
|
|
476
|
+
createError: { code: -32602, message: "Invalid params", data: [{ path: ["model"] }] },
|
|
477
|
+
});
|
|
478
|
+
const { outcome } = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
479
|
+
expect(outcome.error).toContain("engine_run_failed");
|
|
480
|
+
expect(outcome.error).toContain("-32602");
|
|
481
|
+
}, 15_000);
|
|
482
|
+
|
|
483
|
+
it("-32004 运行中失败 → outcome/handle/错误文案携带会话 id(错误规格表:按任务失败上报含会话 id)", async () => {
|
|
484
|
+
// create 成功(golden 会话)后 send 阶段失败——会话已建立,失败上报必须留痕 id
|
|
485
|
+
const { engine, workspace } = makeEngine({
|
|
486
|
+
sendError: { code: -32004, message: "Session is not active" },
|
|
487
|
+
});
|
|
488
|
+
const { handle, outcome } = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
489
|
+
expect(outcome.error).toContain("engine_run_failed");
|
|
490
|
+
expect(outcome.error).toContain("-32004");
|
|
491
|
+
expect(outcome.error).toContain(GOLDEN_SESSION_ID);
|
|
492
|
+
expect(outcome.sessionId).toBe(GOLDEN_SESSION_ID);
|
|
493
|
+
expect(handle.data.sessionRef).toEqual({ sessionId: GOLDEN_SESSION_ID, dbPath: ".zcode/cli/db/db.sqlite" });
|
|
494
|
+
}, 15_000);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// ============================================================
|
|
498
|
+
// abort 链(D3:stop → grace → killChain;stop 成功不 kill)
|
|
499
|
+
// ============================================================
|
|
500
|
+
|
|
501
|
+
describe("abort 链(D3)", () => {
|
|
502
|
+
/** 无终态推送的挂起场景(仅 state.updated——turn 永不自然落定;scenario 每引擎
|
|
503
|
+
* 固化,故「后续任务」同样挂起——复用形态断言用再次 abort 收口,不 await 自然终态)。 */
|
|
504
|
+
const hangPushes = [ZCODE_APPSERVER_GOLDEN.pushStream[0]];
|
|
505
|
+
|
|
506
|
+
/** 挂起场景下发起第 N 轮并推进到该轮 create+send 已达(轮次按 create 帧计数——
|
|
507
|
+
* 全局 send 计数会被上一轮命中,waitFor 会假通过)。 */
|
|
508
|
+
async function startHangRun(
|
|
509
|
+
engine: ZcodeEngine,
|
|
510
|
+
workspace: string,
|
|
511
|
+
stateFile: string,
|
|
512
|
+
taskId: string,
|
|
513
|
+
): Promise<{ run: Promise<EngineRunResult_>; controller: AbortController }> {
|
|
514
|
+
const controller = new AbortController();
|
|
515
|
+
const before = sentFrames(stateFile, "session/create").length;
|
|
516
|
+
const run = engine.run(makeTask({ cwd: workspace }), makeCtx({ taskId, signal: controller.signal }));
|
|
517
|
+
await vi.waitFor(
|
|
518
|
+
() => expect(sentFrames(stateFile, "session/create").length).toBeGreaterThan(before),
|
|
519
|
+
{ timeout: 5_000 },
|
|
520
|
+
);
|
|
521
|
+
await vi.waitFor(
|
|
522
|
+
() => expect(sentFrames(stateFile, "session/send").length).toBeGreaterThan(before),
|
|
523
|
+
{ timeout: 5_000 },
|
|
524
|
+
);
|
|
525
|
+
return { run, controller };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
it("stop 优雅生效:stop 帧先发(send 之后)→ 终态在 grace 内到达 → 不杀共享进程(第二轮零重启)", async () => {
|
|
529
|
+
// stopBehavior 缺省 'terminal':fake 收到 stop 即推该会话终态
|
|
530
|
+
const { engine, stateFile, workspace } = makeEngine({ replaceSendPushes: [...hangPushes] });
|
|
531
|
+
// 第一轮:挂起 → abort → stop 推终态 → 优雅收口
|
|
532
|
+
const first = await startHangRun(engine, workspace, stateFile, "sa-a1");
|
|
533
|
+
first.controller.abort();
|
|
534
|
+
const r1 = await first.run;
|
|
535
|
+
expect(r1.outcome.exitCode).toBeNull();
|
|
536
|
+
expect(r1.outcome.error).toContain("session/stop");
|
|
537
|
+
// 帧序:send 之后紧跟 stop(stop 帧先于任何杀链动作)
|
|
538
|
+
const methods = sentMethods(stateFile);
|
|
539
|
+
expect(methods.indexOf("session/stop")).toBeGreaterThan(methods.indexOf("session/send"));
|
|
540
|
+
// 不杀共享进程:同引擎第二轮(同挂起场景)再 abort 一次——进程若被杀必 boot 2,
|
|
541
|
+
// 存活则 boot 计数仍 1(常驻进程跨任务复用的机制性证据)
|
|
542
|
+
const second = await startHangRun(engine, workspace, stateFile, "sa-a2");
|
|
543
|
+
second.controller.abort();
|
|
544
|
+
const r2 = await second.run;
|
|
545
|
+
expect(r2.outcome.exitCode).toBeNull();
|
|
546
|
+
expect(bootCount(stateFile)).toBe(1);
|
|
547
|
+
}, 30_000);
|
|
548
|
+
|
|
549
|
+
it("stop 无效(stopBehavior none + 无终态)→ grace 耗尽 → killChain 收割共享进程 → 崩溃路径收口 + 重建", async () => {
|
|
550
|
+
const { engine, stateFile, workspace } = makeEngine({
|
|
551
|
+
replaceSendPushes: [...hangPushes],
|
|
552
|
+
stopBehavior: "none",
|
|
553
|
+
});
|
|
554
|
+
const first = await startHangRun(engine, workspace, stateFile, "sa-k1");
|
|
555
|
+
first.controller.abort();
|
|
556
|
+
const r1 = await first.run;
|
|
557
|
+
expect(r1.outcome.exitCode).toBeNull();
|
|
558
|
+
expect(r1.outcome.error).toContain("session/stop");
|
|
559
|
+
// killChain 生效:第二轮自动重建(boot 2——进程死后重建路径,不变量 4 同源)
|
|
560
|
+
const second = await startHangRun(engine, workspace, stateFile, "sa-k2");
|
|
561
|
+
second.controller.abort();
|
|
562
|
+
const r2 = await second.run;
|
|
563
|
+
expect(r2.outcome.exitCode).toBeNull();
|
|
564
|
+
expect(bootCount(stateFile)).toBe(2);
|
|
565
|
+
}, 40_000);
|
|
566
|
+
|
|
567
|
+
it("pre-aborted signal:短路返回中止终态——不建会话不发任何帧(防误杀共享进程);onPoolResolved 先于 error 事件(不变量 3)", async () => {
|
|
568
|
+
const { engine, stateFile, workspace } = makeEngine();
|
|
569
|
+
const controller = new AbortController();
|
|
570
|
+
controller.abort();
|
|
571
|
+
const order: string[] = [];
|
|
572
|
+
const { handle, outcome } = await engine.run(
|
|
573
|
+
makeTask({ cwd: workspace }),
|
|
574
|
+
makeCtx({
|
|
575
|
+
signal: controller.signal,
|
|
576
|
+
onPoolResolved: (k) => order.push(`pool:${k}`),
|
|
577
|
+
onEvent: (e) => order.push(`event:${e.type}`),
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
580
|
+
expect(outcome.exitCode).toBeNull();
|
|
581
|
+
expect(outcome.error).toContain("session/stop");
|
|
582
|
+
expect(handle.data.sessionRef["sessionId"]).toBeUndefined();
|
|
583
|
+
// 不变量 3:poolKey 声明先于首个事件(error)——journal writer 重定向先于落盘,
|
|
584
|
+
// 落盘池与 handle.poolKey 同值同源(短路分支不满足即落 shared 占位池漂移)
|
|
585
|
+
expect(order).toEqual([`pool:${ZCODE_APPSERVER_POOL_KEY}`, "event:error"]);
|
|
586
|
+
expect(handle.data.poolKey).toBe(ZCODE_APPSERVER_POOL_KEY);
|
|
587
|
+
expect(readState(stateFile)).toHaveLength(0); // 连惰性启动都没触发
|
|
588
|
+
}, 10_000);
|
|
589
|
+
|
|
590
|
+
it("宿主超时 abort(reason 标记)→ engine_timeout 公共合成终态(对齐点④,appserver 同语义)", async () => {
|
|
591
|
+
const { engine, workspace } = makeEngine({ replaceSendPushes: [...hangPushes], stopBehavior: "none" });
|
|
592
|
+
const controller = new AbortController();
|
|
593
|
+
controller.abort("agent-call-timeout");
|
|
594
|
+
const { outcome } = await engine.run(makeTask({ cwd: workspace }), makeCtx({ signal: controller.signal }));
|
|
595
|
+
expect(outcome.exitCode).toBeNull();
|
|
596
|
+
expect(outcome.error).toContain("engine_timeout");
|
|
597
|
+
}, 10_000);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// ============================================================
|
|
601
|
+
// 凭据刷新(D7:内容 hash 不一致 → config 重写 + 连接重建)
|
|
602
|
+
// ============================================================
|
|
603
|
+
|
|
604
|
+
describe("凭据刷新", () => {
|
|
605
|
+
it("同内容源:第二任务复用连接(boot 1)+ config 不重写", async () => {
|
|
606
|
+
const { engine, stateFile, workspace } = makeEngine();
|
|
607
|
+
await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
608
|
+
await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
609
|
+
expect(bootCount(stateFile)).toBe(1);
|
|
610
|
+
expect(sentFrames(stateFile, "session/create")).toHaveLength(2);
|
|
611
|
+
}, 20_000);
|
|
612
|
+
|
|
613
|
+
it("apiKey 变更:config 重写 + 旧连接关闭重建(boot 2)", async () => {
|
|
614
|
+
const { engine, stateFile, workspace } = makeEngine();
|
|
615
|
+
await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
616
|
+
const homeDir = resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
617
|
+
const configPath = path.join(homeDir, ...ZCODE_POOL_CONFIG_SUFFIX);
|
|
618
|
+
const before = fs.readFileSync(configPath, "utf8");
|
|
619
|
+
// 变更凭据内容(同文件重写——mtime 变但内容才是判据;此处两者都变)
|
|
620
|
+
writeJson(v2Path, {
|
|
621
|
+
provider: { [PROVIDER]: { options: { apiKey: "k-v2", baseURL: "https://t.example" }, models: { m1: {} } } },
|
|
622
|
+
});
|
|
623
|
+
await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
624
|
+
const after = fs.readFileSync(configPath, "utf8");
|
|
625
|
+
expect(after).not.toBe(before);
|
|
626
|
+
expect(after).toContain("k-v2");
|
|
627
|
+
expect(bootCount(stateFile)).toBe(2); // 连接重建(旧进程被杀、新进程读新配置)
|
|
628
|
+
}, 20_000);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// ============================================================
|
|
632
|
+
// dispose(D6 主体:幂等 / close 帧先于 SIGTERM / dispose 后 run 重建)
|
|
633
|
+
// ============================================================
|
|
634
|
+
|
|
635
|
+
describe("dispose", () => {
|
|
636
|
+
it("在途会话:close 帧发出(fake 收到且记入流水)→ 进程收割 → 挂起任务走崩溃路径收口", async () => {
|
|
637
|
+
const { engine, stateFile, workspace } = makeEngine({
|
|
638
|
+
replaceSendPushes: [ZCODE_APPSERVER_GOLDEN.pushStream[0]], // 挂起不落定
|
|
639
|
+
});
|
|
640
|
+
const runPromise = engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
641
|
+
await vi.waitFor(() => expect(sentFrames(stateFile, "session/send")).toHaveLength(1), { timeout: 5_000 });
|
|
642
|
+
await engine.dispose();
|
|
643
|
+
const { outcome } = await runPromise;
|
|
644
|
+
// 挂起任务被崩溃路径收割(连接死亡 → failAllTurns → run-failed outcome)
|
|
645
|
+
expect(outcome.error).toContain("engine_run_failed");
|
|
646
|
+
// close 帧确实发出(fake 必须活着才能记流水——SIGTERM 先发则该帧大概率丢失,
|
|
647
|
+
// 帧在流水里即证明 close 先于进程死亡送达)
|
|
648
|
+
const closeFrames = sentFrames(stateFile, "session/close");
|
|
649
|
+
expect(closeFrames).toHaveLength(1);
|
|
650
|
+
expect(closeFrames[0]?.params["sessionId"]).toBe(GOLDEN_SESSION_ID);
|
|
651
|
+
// 帧序:close 在 send 之后(在途会话的收尾)
|
|
652
|
+
const methods = sentMethods(stateFile);
|
|
653
|
+
expect(methods.indexOf("session/close")).toBeGreaterThan(methods.indexOf("session/send"));
|
|
654
|
+
// 预算说明:waitFor ≤5s + killChain grace 5s + fake SIGTERM 100ms 排空窗口,套件
|
|
655
|
+
// 并发负载下 20s 偶发超限(正常完成 ~10s),放宽到 30s 防环境性 flake
|
|
656
|
+
}, 30_000);
|
|
657
|
+
|
|
658
|
+
it("幂等:连续两次 dispose 第二次零副作用;无运行时时 dispose 是 no-op", async () => {
|
|
659
|
+
const { engine } = makeEngine();
|
|
660
|
+
await engine.dispose(); // 从未 run——no-op
|
|
661
|
+
const { engine: e2, stateFile, workspace } = makeEngine();
|
|
662
|
+
await e2.run(makeTask({ cwd: workspace }), makeCtx());
|
|
663
|
+
await e2.dispose();
|
|
664
|
+
const boots = bootCount(stateFile);
|
|
665
|
+
await e2.dispose();
|
|
666
|
+
expect(bootCount(stateFile)).toBe(boots); // 无新 boot、无重复杀
|
|
667
|
+
}, 15_000);
|
|
668
|
+
|
|
669
|
+
it("dispose 后首个 run 自动重建(与进程死后重建同路径——不变量 4)", async () => {
|
|
670
|
+
const { engine, stateFile, workspace } = makeEngine();
|
|
671
|
+
await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
672
|
+
expect(bootCount(stateFile)).toBe(1);
|
|
673
|
+
await engine.dispose();
|
|
674
|
+
const second = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
675
|
+
expect(second.outcome.error).toBeUndefined();
|
|
676
|
+
expect(second.outcome.content).toBe(GOLDEN_FULL_TEXT);
|
|
677
|
+
expect(bootCount(stateFile)).toBe(2);
|
|
678
|
+
}, 15_000);
|
|
679
|
+
|
|
680
|
+
it("窄竞态守卫:进程已死(child=null)但 activeSessions 残留时,dispose 不经 post 惰性拉起新进程(D6 防泄漏)", async () => {
|
|
681
|
+
// 真实微拍窗口(进程死亡收割与 activeSessions 清理之间的 microtask)不可从公开
|
|
682
|
+
// 面同步注入——白盒构造同形态快照:runtime 的 conn 无活进程、activeSessions 非空。
|
|
683
|
+
// 守卫缺失时 dispose 循环 post("session/close") 会 ensureStarted 惰性 spawn 一代新
|
|
684
|
+
// 进程再被同次 dispose 杀掉(onSpawned 同步触发);守卫生效时永不 spawn。
|
|
685
|
+
// 观测面用 conn.onSpawned(spawnGeneration 内同步调用)而非 fake 侧 boot 计数:
|
|
686
|
+
// post 后紧接 SIGTERM,新进程来不及写 boot 即死,写盘时序不可靠。
|
|
687
|
+
const { engine } = makeEngine();
|
|
688
|
+
let spawned = false;
|
|
689
|
+
const conn = new AppServerConnection({
|
|
690
|
+
cliPath: FAKE_CLI,
|
|
691
|
+
cwd: tmpRoot,
|
|
692
|
+
env: {
|
|
693
|
+
PATH: process.env.PATH ?? "",
|
|
694
|
+
FAKE_STATE_FILE: path.join(tmpRoot, "state-dispose-guard.jsonl"),
|
|
695
|
+
FAKE_SESSION_SCENARIO: path.join(tmpRoot, "scenario-dispose-guard.json"),
|
|
696
|
+
},
|
|
697
|
+
stderrLogPath: path.join(dataDir, "logs", "dispose-guard-stderr.log"),
|
|
698
|
+
onSpawned: () => {
|
|
699
|
+
spawned = true;
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
const ghostRt = {
|
|
703
|
+
conn,
|
|
704
|
+
channel: new SessionChannel(conn),
|
|
705
|
+
homePoolKey: ZCODE_APPSERVER_POOL_KEY,
|
|
706
|
+
homeDir: resolvePoolDir(dataDir, "zcode", ZCODE_APPSERVER_POOL_KEY),
|
|
707
|
+
activeSessions: new Set<string>(["sess_ghost"]),
|
|
708
|
+
};
|
|
709
|
+
(engine as unknown as { appserverRuntime: unknown }).appserverRuntime = ghostRt;
|
|
710
|
+
expect(conn.alive).toBe(false); // 快照前置:无活进程
|
|
711
|
+
await engine.dispose();
|
|
712
|
+
expect(spawned).toBe(false); // 守卫生效:close 帧循环跳过,零惰性 spawn
|
|
713
|
+
}, 15_000);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// ============================================================
|
|
717
|
+
// 多会话并发(RA3 双会话地基:FAKE_STAMP_SESSION——推送按会话归因)
|
|
718
|
+
// ============================================================
|
|
719
|
+
|
|
720
|
+
describe("多会话并发(单常驻进程双会话不串线)", () => {
|
|
721
|
+
it("并发两 runTurn:各自 sessionId/事件流/结果独立,boot 计数 1(同进程共享)", async () => {
|
|
722
|
+
// distinctSessionIds:fake 缺省 create 应答每会话独立 sess_<请求id>;stamp 后
|
|
723
|
+
// 推送按目标会话归因(telemetry 帧补 sid——并发下不再依赖「唯一在途」兜底)
|
|
724
|
+
const { engine, stateFile, workspace } = makeEngine({ stampSession: true, distinctSessionIds: true });
|
|
725
|
+
const eventsA: AgentEvent[] = [];
|
|
726
|
+
const eventsB: AgentEvent[] = [];
|
|
727
|
+
const [a, b] = await Promise.all([
|
|
728
|
+
engine.run(makeTask({ cwd: workspace }), makeCtx({ taskId: "sa-a", onEvent: (e) => eventsA.push(e) })),
|
|
729
|
+
engine.run(makeTask({ cwd: workspace }), makeCtx({ taskId: "sa-b", onEvent: (e) => eventsB.push(e) })),
|
|
730
|
+
]);
|
|
731
|
+
expect(a.outcome.error).toBeUndefined();
|
|
732
|
+
expect(b.outcome.error).toBeUndefined();
|
|
733
|
+
expect(a.outcome.content).toBe(GOLDEN_FULL_TEXT);
|
|
734
|
+
expect(b.outcome.content).toBe(GOLDEN_FULL_TEXT);
|
|
735
|
+
// 两会话 sessionId 独立(fake 缺省 sess_<请求id>)——不串线
|
|
736
|
+
expect(a.outcome.sessionId).toMatch(/^sess_\d+$/);
|
|
737
|
+
expect(b.outcome.sessionId).toMatch(/^sess_\d+$/);
|
|
738
|
+
expect(a.outcome.sessionId).not.toBe(b.outcome.sessionId);
|
|
739
|
+
// 各自的 delta 流完整(三条全到,无串扰丢失)
|
|
740
|
+
expect(eventsA.filter((e) => e.type === "text_delta")).toHaveLength(3);
|
|
741
|
+
expect(eventsB.filter((e) => e.type === "text_delta")).toHaveLength(3);
|
|
742
|
+
// 两会话的 handle 各归各
|
|
743
|
+
expect(a.handle.data.sessionRef["sessionId"]).toBe(a.outcome.sessionId);
|
|
744
|
+
expect(b.handle.data.sessionRef["sessionId"]).toBe(b.outcome.sessionId);
|
|
745
|
+
const creates = sentFrames(stateFile, "session/create");
|
|
746
|
+
expect(creates).toHaveLength(2);
|
|
747
|
+
// 两会话的 send/subscribe/read/close 各自指向自己的 sessionId(通道帧不串线)
|
|
748
|
+
const sendSids = sentFrames(stateFile, "session/send").map((f) => f.params["sessionId"]);
|
|
749
|
+
expect(new Set(sendSids).size).toBe(2);
|
|
750
|
+
expect(sendSids.sort()).toEqual([a.outcome.sessionId, b.outcome.sessionId].sort());
|
|
751
|
+
expect(bootCount(stateFile)).toBe(1);
|
|
752
|
+
}, 20_000);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// ============================================================
|
|
756
|
+
// capabilities(D5)+ eventGranularity 生产零消费方守卫
|
|
757
|
+
// ============================================================
|
|
758
|
+
|
|
759
|
+
describe("capabilities(D5:仅 eventGranularity 变)", () => {
|
|
760
|
+
it("appserver 路径声明 eventGranularity='stream',其余能力位与 spawn 钉扎测试一致", () => {
|
|
761
|
+
const { engine } = makeEngine();
|
|
762
|
+
expect(engine.capabilities()).toEqual({
|
|
763
|
+
schemaEnforcement: "emulated",
|
|
764
|
+
steer: "unsupported",
|
|
765
|
+
conversation: "unsupported",
|
|
766
|
+
personaInjection: "prompt",
|
|
767
|
+
eventGranularity: "stream",
|
|
768
|
+
sandbox: "none",
|
|
769
|
+
sessionRead: "full",
|
|
770
|
+
resume: "cold",
|
|
771
|
+
interrupt: "kill-only",
|
|
772
|
+
permissionMode: "native",
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it("eventGranularity 生产零消费方守卫:src 内(测试除外)无任何能力位读取访问(翻转无下游破坏)", () => {
|
|
777
|
+
const srcRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../../..");
|
|
778
|
+
const consumerAccess = /\.(capabilities\(\)|caps|capabilities)\.eventGranularity\b/g;
|
|
779
|
+
const offenders: string[] = [];
|
|
780
|
+
const walk = (dir: string): void => {
|
|
781
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
782
|
+
const full = path.join(dir, entry.name);
|
|
783
|
+
if (entry.isDirectory()) {
|
|
784
|
+
if (entry.name === "__tests__" || entry.name === "node_modules" || entry.name === "dist") continue;
|
|
785
|
+
walk(full);
|
|
786
|
+
} else if (entry.name.endsWith(".ts")) {
|
|
787
|
+
const text = fs.readFileSync(full, "utf8");
|
|
788
|
+
consumerAccess.lastIndex = 0;
|
|
789
|
+
if (consumerAccess.test(text)) offenders.push(path.relative(srcRoot, full));
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
walk(srcRoot);
|
|
794
|
+
expect(offenders).toEqual([]);
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// ── 内部 helper ──
|
|
799
|
+
|
|
800
|
+
/** 造一组「delta 流 + 收尾帧 + read 应答」的 JSON 输出推送(schema 仿真用例)。 */
|
|
801
|
+
function buildJsonPushes(finalText: string): string[] {
|
|
802
|
+
return [
|
|
803
|
+
ZCODE_APPSERVER_GOLDEN.pushStream[0],
|
|
804
|
+
ZCODE_APPSERVER_GOLDEN.pushStream[1],
|
|
805
|
+
ZCODE_APPSERVER_GOLDEN.pushStream[2],
|
|
806
|
+
JSON.stringify({
|
|
807
|
+
method: "v4/telemetry/event",
|
|
808
|
+
params: { kind: "turn.terminal", status: "success" },
|
|
809
|
+
}),
|
|
810
|
+
JSON.stringify({
|
|
811
|
+
method: "session/event",
|
|
812
|
+
params: { sessionId: GOLDEN_SESSION_ID, payload: { response: finalText } },
|
|
813
|
+
}),
|
|
814
|
+
];
|
|
815
|
+
}
|