@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,462 @@
|
|
|
1
|
+
// zcode-engine-degrade.test.ts —— [R5] 降级链四步(D2)+ probe 冒烟(D8)单测。
|
|
2
|
+
// 全部跑 __fixtures__/fake-appserver.mjs 子进程(scenario 注入),绝不 spawn 真
|
|
3
|
+
// zcode.cjs。覆盖(impl-plan R5 验收条款 ①-④):
|
|
4
|
+
// ① RA5-① 回归门:fixture 注入 -32602(create)/ -32601(send)→ 首任务降级
|
|
5
|
+
// spawn 重跑成功 + outcome.engineFallback 标注 degraded: spawn + 第二任务直走
|
|
6
|
+
// spawn(断言不再尝试 app-server 连接:boot/create 帧计数冻结);
|
|
7
|
+
// ② probe 冒烟:探针连接帧序(create→close、无 send 帧——不发模型请求)、
|
|
8
|
+
// mtime 未变不重探、mtime 变化(fake 触碰)后重探、预算超时(hangCreate)失败降级、
|
|
9
|
+
// 探针 create error 失败降级;
|
|
10
|
+
// ③ env 三态:缺省(探+降)/ appserver 定向(不探不降,失败直接报)/ spawn 定向
|
|
11
|
+
// (不探不降直 spawn);
|
|
12
|
+
// ④ 错误分类表:-32603 → engine_credential_missing 不降级;-32004/-32010 →
|
|
13
|
+
// engine_run_failed 上报不降级(非漂移类)。
|
|
14
|
+
// 探针/主连接区分判据:探针 env 带 ZCODE_APPSERVER_PROBE_CONN=1(fake env 快照记
|
|
15
|
+
// probe 标志;探针 scenario 只读 FAKE_PROBE_SCENARIO——故障注入互不串扰)。
|
|
16
|
+
|
|
17
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as os from "node:os";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
import { PassThrough } from "node:stream";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
|
|
24
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
25
|
+
|
|
26
|
+
import type { RunContext } from "../../../port.ts";
|
|
27
|
+
import type { AgentTaskSpec } from "../../../types.ts";
|
|
28
|
+
import { getLogger } from "../../../../../core/logger.ts";
|
|
29
|
+
import { resolvePoolDir } from "../../../paths.ts";
|
|
30
|
+
import { ZCODE_APPSERVER_GOLDEN, ZCODE_GOLDEN_STDOUT } from "../golden-sample.ts";
|
|
31
|
+
import type { ZcodeLaunchedProcess } from "../launcher.ts";
|
|
32
|
+
import { ZcodeEngine, pinnedZcodeMode, type ZcodeEngineDeps } from "../zcode-engine.ts";
|
|
33
|
+
|
|
34
|
+
const FAKE_CLI = fileURLToPath(new URL("./__fixtures__/fake-appserver.mjs", import.meta.url));
|
|
35
|
+
const PROVIDER = "test-provider";
|
|
36
|
+
|
|
37
|
+
/** 与 zcode-engine.ts 同一 facade 单例引用(spy 它的方法即拦截引擎的 warn 出声)。 */
|
|
38
|
+
const subagentsLogger = getLogger("subagents");
|
|
39
|
+
|
|
40
|
+
const GOLDEN_SESSION_ID = "sess_golden_r3_01";
|
|
41
|
+
|
|
42
|
+
const engines: ZcodeEngine[] = [];
|
|
43
|
+
let seq = 0;
|
|
44
|
+
let tmpRoot: string;
|
|
45
|
+
let dataDir: string;
|
|
46
|
+
let v2Path: string;
|
|
47
|
+
|
|
48
|
+
function writeJson(p: string, v: unknown): void {
|
|
49
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
50
|
+
fs.writeFileSync(p, JSON.stringify(v, null, 2));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "zcode-eng-degrade-"));
|
|
55
|
+
dataDir = path.join(tmpRoot, "data");
|
|
56
|
+
v2Path = path.join(tmpRoot, "v2.json");
|
|
57
|
+
writeJson(v2Path, {
|
|
58
|
+
provider: { [PROVIDER]: { options: { apiKey: "k", baseURL: "https://t.example" }, models: { m1: {} } } },
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(async () => {
|
|
63
|
+
for (const engine of engines.splice(0)) await engine.dispose().catch(() => undefined);
|
|
64
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ── fake launcher(降级 spawn 重跑的成功通道——golden stdout 单轮) ──
|
|
68
|
+
|
|
69
|
+
function makeFakeLaunch(stdout = ZCODE_GOLDEN_STDOUT) {
|
|
70
|
+
const calls: Array<{ cliPath: string; args: string[]; env: NodeJS.ProcessEnv }> = [];
|
|
71
|
+
const launch = (o: { cliPath: string; args: string[]; env: NodeJS.ProcessEnv }): ZcodeLaunchedProcess => {
|
|
72
|
+
calls.push(o);
|
|
73
|
+
const child: ChildProcess = spawn(process.execPath, ["-e", ""]);
|
|
74
|
+
const out = new PassThrough();
|
|
75
|
+
out.end(stdout);
|
|
76
|
+
const stderr = new PassThrough();
|
|
77
|
+
stderr.end("");
|
|
78
|
+
let killed = false;
|
|
79
|
+
return {
|
|
80
|
+
child,
|
|
81
|
+
pid: 4242,
|
|
82
|
+
stdout: out,
|
|
83
|
+
stderr,
|
|
84
|
+
abort: async () => {
|
|
85
|
+
killed = true;
|
|
86
|
+
},
|
|
87
|
+
exited: Promise.resolve({ code: 0, signal: undefined }),
|
|
88
|
+
killedByUs: () => killed,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
return { launch, calls };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface DegradeScenario {
|
|
95
|
+
/** 主连接(常驻)scenario 覆盖(缺省 golden 成功形态)。 */
|
|
96
|
+
createError?: { code: number; message: string; data?: unknown };
|
|
97
|
+
sendError?: { code: number; message: string; data?: unknown };
|
|
98
|
+
/** 探针 scenario(只作用于探针连接)。 */
|
|
99
|
+
probeScenario?: Record<string, unknown>;
|
|
100
|
+
/** 探针预算(缺省走 10s 常量——测试注入短值验证超时路径)。 */
|
|
101
|
+
probeBudgetMs?: number;
|
|
102
|
+
/** env 定向(XYZ_ZCODE_MODE)。 */
|
|
103
|
+
mode?: "appserver" | "spawn";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 建一个缺省(探+降)或定向模式的引擎 + 配套 launch fake。 */
|
|
107
|
+
function makeDegradeEngine(s: DegradeScenario = {}) {
|
|
108
|
+
seq += 1;
|
|
109
|
+
const stateFile = path.join(tmpRoot, `state-${seq}.jsonl`);
|
|
110
|
+
const scenarioFile = path.join(tmpRoot, `scenario-${seq}.json`);
|
|
111
|
+
const workspace = path.join(tmpRoot, `ws-${seq}`);
|
|
112
|
+
const pushes = [...ZCODE_APPSERVER_GOLDEN.pushStream, ...ZCODE_APPSERVER_GOLDEN.terminal].map((l) =>
|
|
113
|
+
typeof l === "string" ? (JSON.parse(l) as Record<string, unknown>) : l,
|
|
114
|
+
);
|
|
115
|
+
writeJson(scenarioFile, {
|
|
116
|
+
createResult: JSON.parse(ZCODE_APPSERVER_GOLDEN.createResponse),
|
|
117
|
+
readResult: JSON.parse(ZCODE_APPSERVER_GOLDEN.readResponse),
|
|
118
|
+
sendPushes: pushes,
|
|
119
|
+
...(s.createError !== undefined ? { createError: s.createError } : {}),
|
|
120
|
+
...(s.sendError !== undefined ? { sendError: s.sendError } : {}),
|
|
121
|
+
});
|
|
122
|
+
let probeScenarioFile: string | undefined;
|
|
123
|
+
if (s.probeScenario !== undefined) {
|
|
124
|
+
probeScenarioFile = path.join(tmpRoot, `probe-scenario-${seq}.json`);
|
|
125
|
+
writeJson(probeScenarioFile, s.probeScenario);
|
|
126
|
+
}
|
|
127
|
+
const fakeLaunch = makeFakeLaunch();
|
|
128
|
+
const deps: ZcodeEngineDeps = {
|
|
129
|
+
engineDataDir: () => dataDir,
|
|
130
|
+
cliPath: FAKE_CLI,
|
|
131
|
+
sources: { v2ConfigPath: v2Path },
|
|
132
|
+
processEnv: {
|
|
133
|
+
PATH: process.env.PATH ?? "",
|
|
134
|
+
...(s.mode !== undefined ? { XYZ_ZCODE_MODE: s.mode } : {}),
|
|
135
|
+
FAKE_STATE_FILE: stateFile,
|
|
136
|
+
FAKE_SESSION_SCENARIO: scenarioFile,
|
|
137
|
+
...(probeScenarioFile !== undefined ? { FAKE_PROBE_SCENARIO: probeScenarioFile } : {}),
|
|
138
|
+
},
|
|
139
|
+
...(s.probeBudgetMs !== undefined ? { probeBudgetMs: s.probeBudgetMs } : {}),
|
|
140
|
+
launch: fakeLaunch.launch as unknown as ZcodeEngineDeps["launch"],
|
|
141
|
+
};
|
|
142
|
+
const engine = new ZcodeEngine(deps);
|
|
143
|
+
engines.push(engine);
|
|
144
|
+
return { engine, stateFile, workspace, fakeLaunch };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── 流水读取 helpers(与 zcode-engine-appserver.test.ts 同款 + 探针判据) ──
|
|
148
|
+
|
|
149
|
+
interface StateEvent {
|
|
150
|
+
seq: number;
|
|
151
|
+
ev: string;
|
|
152
|
+
[key: string]: unknown;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readState(file: string): StateEvent[] {
|
|
156
|
+
try {
|
|
157
|
+
return fs
|
|
158
|
+
.readFileSync(file, "utf8")
|
|
159
|
+
.trim()
|
|
160
|
+
.split("\n")
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.map((l) => JSON.parse(l) as StateEvent);
|
|
163
|
+
} catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
169
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function recvFrames(stateFile: string, method: string): Array<{ id: number; params: Record<string, unknown> }> {
|
|
173
|
+
return readState(stateFile)
|
|
174
|
+
.filter((e) => e.ev === "recv")
|
|
175
|
+
.map((e) => e.frame)
|
|
176
|
+
.filter(
|
|
177
|
+
(f): f is { id: number; params: Record<string, unknown> } =>
|
|
178
|
+
isRecord(f) && f.method === method && isRecord(f.params) && typeof f.id === "number",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function recvMethods(stateFile: string): string[] {
|
|
183
|
+
return readState(stateFile)
|
|
184
|
+
.filter((e) => e.ev === "recv")
|
|
185
|
+
.map((e) => e.frame)
|
|
186
|
+
.filter((f): f is Record<string, unknown> => isRecord(f) && typeof f.method === "string")
|
|
187
|
+
.map((f) => f.method as string);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 探针进程 boot 数(env 快照带 probe='1'——探针连接标记)。 */
|
|
191
|
+
function probeBootCount(stateFile: string): number {
|
|
192
|
+
return readState(stateFile).filter((e) => e.ev === "env" && e["probe"] === "1").length;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 全部 boot 数(探针 + 常驻主连接)。 */
|
|
196
|
+
function bootCount(stateFile: string): number {
|
|
197
|
+
return readState(stateFile).filter((e) => e.ev === "boot").length;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function makeTask(overrides?: Partial<AgentTaskSpec>): AgentTaskSpec {
|
|
201
|
+
return { task: "做点什么", slug: "s", model: `${PROVIDER}/m1`, ...overrides };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function makeCtx(overrides?: Partial<RunContext>): RunContext {
|
|
205
|
+
return { taskId: "sa-degrade", poolKey: "", ...overrides };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ============================================================
|
|
209
|
+
// ① RA5-① 回归门:首败漂移降级(D2②)
|
|
210
|
+
// ============================================================
|
|
211
|
+
|
|
212
|
+
describe("首败漂移降级(RA5-① 回归门)", () => {
|
|
213
|
+
it("-32602(create 阶段):首任务降级 spawn 重跑成功 + 标注 degraded: spawn + 第二任务直走 spawn", async () => {
|
|
214
|
+
const { engine, stateFile, workspace, fakeLaunch } = makeDegradeEngine({
|
|
215
|
+
createError: { code: -32602, message: "Invalid params", data: [{ path: ["model"] }] },
|
|
216
|
+
});
|
|
217
|
+
// 首任务:探针过(探针连接不受主 scenario 影响)→ 主连接 create -32602 → 漂移
|
|
218
|
+
// → 同一任务 spawn 重跑(fake launch golden)成功
|
|
219
|
+
const r1 = await engine.run(makeTask({ cwd: workspace }), makeCtx({ taskId: "d1" }));
|
|
220
|
+
expect(r1.outcome.error).toBeUndefined();
|
|
221
|
+
expect(r1.outcome.content).toBe("ok"); // spawn golden 单轮回复(ZCODE_GOLDEN_STDOUT)
|
|
222
|
+
expect(r1.outcome.engineFallback).toBeDefined();
|
|
223
|
+
expect(r1.outcome.engineFallback?.from).toBe("zcode:appserver");
|
|
224
|
+
expect(r1.outcome.engineFallback?.reason).toContain("degraded: spawn");
|
|
225
|
+
expect(r1.outcome.engineFallback?.reason).toContain("protocol-drift");
|
|
226
|
+
expect(r1.outcome.engineFallback?.reason).toContain("-32602");
|
|
227
|
+
expect(fakeLaunch.calls).toHaveLength(1); // 重跑走的是 spawn 通道
|
|
228
|
+
const bootsAfter1 = bootCount(stateFile);
|
|
229
|
+
const createsAfter1 = recvFrames(stateFile, "session/create").length;
|
|
230
|
+
|
|
231
|
+
// 第二任务:直走 spawn——不再尝试 app-server 连接(boot/create 帧计数冻结)
|
|
232
|
+
const r2 = await engine.run(makeTask({ cwd: workspace }), makeCtx({ taskId: "d2" }));
|
|
233
|
+
expect(r2.outcome.error).toBeUndefined();
|
|
234
|
+
expect(r2.outcome.engineFallback?.reason).toContain("degraded: spawn");
|
|
235
|
+
expect(fakeLaunch.calls).toHaveLength(2);
|
|
236
|
+
expect(bootCount(stateFile)).toBe(bootsAfter1);
|
|
237
|
+
expect(recvFrames(stateFile, "session/create").length).toBe(createsAfter1);
|
|
238
|
+
}, 20_000);
|
|
239
|
+
|
|
240
|
+
it("-32601(send 阶段)同归类:首任务漂移降级 + 后续直走 spawn", async () => {
|
|
241
|
+
const { engine, stateFile, workspace, fakeLaunch } = makeDegradeEngine({
|
|
242
|
+
sendError: { code: -32601, message: "method not found: session/send" },
|
|
243
|
+
});
|
|
244
|
+
const r1 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
245
|
+
expect(r1.outcome.error).toBeUndefined();
|
|
246
|
+
expect(r1.outcome.content).toBe("ok"); // spawn golden 单轮回复(ZCODE_GOLDEN_STDOUT)
|
|
247
|
+
expect(r1.outcome.engineFallback?.reason).toContain("-32601");
|
|
248
|
+
expect(fakeLaunch.calls).toHaveLength(1);
|
|
249
|
+
const createsAfter1 = recvFrames(stateFile, "session/create").length;
|
|
250
|
+
const r2 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
251
|
+
expect(r2.outcome.error).toBeUndefined();
|
|
252
|
+
expect(recvFrames(stateFile, "session/create").length).toBe(createsAfter1);
|
|
253
|
+
expect(fakeLaunch.calls).toHaveLength(2);
|
|
254
|
+
}, 20_000);
|
|
255
|
+
|
|
256
|
+
it("RX2-F3:ctxModel 忽略 warn 在降级链仅一次(appserver 首跑出声,spawn 重跑不重复)", async () => {
|
|
257
|
+
// task.model 缺省走 ZCODE_FALLBACK_DEFAULT_MODEL 解析链——v2 config 需含其 provider
|
|
258
|
+
writeJson(v2Path, {
|
|
259
|
+
provider: {
|
|
260
|
+
[PROVIDER]: { options: { apiKey: "k", baseURL: "https://t.example" }, models: { m1: {} } },
|
|
261
|
+
"builtin:bigmodel-coding-plan": { options: { apiKey: "k" }, models: { "GLM-5.3": {} } },
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
const { engine, fakeLaunch } = makeDegradeEngine({
|
|
265
|
+
createError: { code: -32602, message: "Invalid params", data: [{ path: ["model"] }] },
|
|
266
|
+
});
|
|
267
|
+
const warns: string[] = [];
|
|
268
|
+
const warnSpy = vi.spyOn(subagentsLogger, "warn").mockImplementation(((msg: string) => {
|
|
269
|
+
warns.push(msg);
|
|
270
|
+
}) as typeof subagentsLogger.warn);
|
|
271
|
+
let r1: Awaited<ReturnType<typeof engine.run>>;
|
|
272
|
+
try {
|
|
273
|
+
r1 = await engine.run(
|
|
274
|
+
makeTask({ model: undefined }),
|
|
275
|
+
makeCtx({ taskId: "d-ctxmodel", ctxModel: { id: "main-model", name: "Main", provider: "p", reasoning: false } }),
|
|
276
|
+
);
|
|
277
|
+
} finally {
|
|
278
|
+
warnSpy.mockRestore();
|
|
279
|
+
}
|
|
280
|
+
// 降级重跑成功(spawn golden 单轮)
|
|
281
|
+
expect(r1.outcome.error).toBeUndefined();
|
|
282
|
+
expect(fakeLaunch.calls).toHaveLength(1);
|
|
283
|
+
// 旧实现:appserver 首跑 + spawn 重跑各 warn 一次 = 同 taskId 双份相同 warn(本断言会红)
|
|
284
|
+
const ctxModelWarns = warns.filter((m) => m.includes("ctxModel"));
|
|
285
|
+
expect(ctxModelWarns).toHaveLength(1);
|
|
286
|
+
expect(ctxModelWarns[0]).toContain("main-model");
|
|
287
|
+
// 降级宣告 warn 不受抑制(漂移降级本身仍出声)
|
|
288
|
+
expect(warns.some((m) => m.includes("漂移类错误"))).toBe(true);
|
|
289
|
+
}, 20_000);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ============================================================
|
|
293
|
+
// ② probe 冒烟(D8:帧序 / 无模型请求 / mtime 缓存与重探 / 预算)
|
|
294
|
+
// ============================================================
|
|
295
|
+
|
|
296
|
+
describe("probe 冒烟(D8)", () => {
|
|
297
|
+
it("探针通过 → 走 app-server;帧序 create→close、无 send 帧(不发模型请求);结论记 CLI mtime", async () => {
|
|
298
|
+
const { engine, stateFile, workspace, fakeLaunch } = makeDegradeEngine();
|
|
299
|
+
const r1 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
300
|
+
expect(r1.outcome.error).toBeUndefined();
|
|
301
|
+
expect(r1.outcome.sessionId).toBe(GOLDEN_SESSION_ID); // 走的是 app-server 常驻路径
|
|
302
|
+
expect(fakeLaunch.calls).toHaveLength(0);
|
|
303
|
+
// 探针连接恰好 1 个 boot,且先于常驻主连接(首帧 env 快照带 probe 标志)
|
|
304
|
+
expect(probeBootCount(stateFile)).toBe(1);
|
|
305
|
+
const envs = readState(stateFile).filter((e) => e.ev === "env");
|
|
306
|
+
expect(envs[0]?.["probe"]).toBe("1");
|
|
307
|
+
expect(envs[1]?.["probe"]).toBeUndefined();
|
|
308
|
+
// 探针帧序:探针 create(workspace=常驻 HOME)→ 探针 close,均在主连接 create 之前
|
|
309
|
+
const creates = recvFrames(stateFile, "session/create");
|
|
310
|
+
const homeDir = resolvePoolDir(dataDir, "zcode", "home-appserver");
|
|
311
|
+
expect(creates[0]?.params["workspace"]).toMatchObject({ workspacePath: homeDir });
|
|
312
|
+
const methods = recvMethods(stateFile);
|
|
313
|
+
const firstCreate = methods.indexOf("session/create");
|
|
314
|
+
const firstClose = methods.indexOf("session/close");
|
|
315
|
+
const secondCreate = methods.indexOf("session/create", firstCreate + 1);
|
|
316
|
+
expect(firstClose).toBeGreaterThan(firstCreate);
|
|
317
|
+
expect(secondCreate).toBeGreaterThan(firstClose);
|
|
318
|
+
// 不发模型请求:探针会话(首条 close 帧的 sessionId)从未出现 send 帧
|
|
319
|
+
const probeSessionId = recvFrames(stateFile, "session/close")[0]?.params["sessionId"];
|
|
320
|
+
expect(typeof probeSessionId).toBe("string");
|
|
321
|
+
expect(probeSessionId).not.toBe(GOLDEN_SESSION_ID);
|
|
322
|
+
const sendTargets = recvFrames(stateFile, "session/send").map((f) => f.params["sessionId"]);
|
|
323
|
+
expect(sendTargets).not.toContain(probeSessionId);
|
|
324
|
+
}, 20_000);
|
|
325
|
+
|
|
326
|
+
it("mtime 未变不重探:第二任务探针 boot 计数不增;mtime 变化(触碰 CLI)后重探", async () => {
|
|
327
|
+
const { engine, stateFile, workspace } = makeDegradeEngine();
|
|
328
|
+
await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
329
|
+
await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
330
|
+
expect(probeBootCount(stateFile)).toBe(1); // 缓存命中
|
|
331
|
+
// 触碰 CLI 伪造 zcode 升级(mtime 前移 2s——避开 fs 时间精度)
|
|
332
|
+
const now = new Date();
|
|
333
|
+
fs.utimesSync(FAKE_CLI, new Date(now.getTime() + 2000), new Date(now.getTime() + 2000));
|
|
334
|
+
const r3 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
335
|
+
expect(r3.outcome.error).toBeUndefined();
|
|
336
|
+
expect(probeBootCount(stateFile)).toBe(2); // mtime 变化 → 首个任务前重探
|
|
337
|
+
}, 20_000);
|
|
338
|
+
|
|
339
|
+
it("探针预算超时(hangCreate + 500ms 预算)→ 探针失败 → 直接 spawn + 标注 probe-failed", async () => {
|
|
340
|
+
const { engine, stateFile, workspace, fakeLaunch } = makeDegradeEngine({
|
|
341
|
+
probeScenario: { hangCreate: true },
|
|
342
|
+
probeBudgetMs: 500,
|
|
343
|
+
});
|
|
344
|
+
const started = Date.now();
|
|
345
|
+
const r1 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
346
|
+
// 500ms 预算内收割(不真等 10s 常量),随后 spawn 重跑成功
|
|
347
|
+
expect(Date.now() - started).toBeLessThan(8_000);
|
|
348
|
+
expect(r1.outcome.error).toBeUndefined();
|
|
349
|
+
expect(r1.outcome.engineFallback?.reason).toContain("probe-failed");
|
|
350
|
+
expect(r1.outcome.engineFallback?.reason).toContain("超时");
|
|
351
|
+
expect(fakeLaunch.calls).toHaveLength(1);
|
|
352
|
+
// 探针连接未发出 close(create 挂死)——但也没有任何 send 帧
|
|
353
|
+
expect(recvMethods(stateFile)).not.toContain("session/send");
|
|
354
|
+
}, 20_000);
|
|
355
|
+
|
|
356
|
+
it("探针 create error(-32602)→ 探针失败 → 直接 spawn(主连接 scenario 不受影响也未被触碰)", async () => {
|
|
357
|
+
const { engine, stateFile, workspace, fakeLaunch } = makeDegradeEngine({
|
|
358
|
+
probeScenario: { createError: { code: -32602, message: "Invalid params" } },
|
|
359
|
+
});
|
|
360
|
+
const r1 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
361
|
+
expect(r1.outcome.error).toBeUndefined();
|
|
362
|
+
expect(r1.outcome.engineFallback?.reason).toContain("probe-failed");
|
|
363
|
+
expect(fakeLaunch.calls).toHaveLength(1);
|
|
364
|
+
// 探针失败短路:常驻主连接从未建立(boot 全部来自探针)
|
|
365
|
+
expect(bootCount(stateFile)).toBe(probeBootCount(stateFile));
|
|
366
|
+
// 失败结论同样绑 mtime 缓存(smokeConclusion 缓存命中不分成败):同一 CLI mtime 下
|
|
367
|
+
// 第二任务不重探(探针 boot 冻结)且仍直走 spawn,不再触碰主连接
|
|
368
|
+
const probeBootsAfter1 = probeBootCount(stateFile);
|
|
369
|
+
const bootsAfter1 = bootCount(stateFile);
|
|
370
|
+
const r2 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
371
|
+
expect(r2.outcome.error).toBeUndefined();
|
|
372
|
+
expect(r2.outcome.engineFallback?.reason).toContain("probe-failed");
|
|
373
|
+
expect(fakeLaunch.calls).toHaveLength(2);
|
|
374
|
+
expect(probeBootCount(stateFile)).toBe(probeBootsAfter1);
|
|
375
|
+
expect(bootCount(stateFile)).toBe(bootsAfter1);
|
|
376
|
+
}, 20_000);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// ============================================================
|
|
380
|
+
// ③ env 三态(D2④ 定向不探不降)
|
|
381
|
+
// ============================================================
|
|
382
|
+
|
|
383
|
+
describe("env 三态(XYZ_ZCODE_MODE)", () => {
|
|
384
|
+
it("pinnedZcodeMode 三态判定:定向值原样 / 缺省与未知值 undefined", () => {
|
|
385
|
+
expect(pinnedZcodeMode({ XYZ_ZCODE_MODE: "spawn" })).toBe("spawn");
|
|
386
|
+
expect(pinnedZcodeMode({ XYZ_ZCODE_MODE: "appserver" })).toBe("appserver");
|
|
387
|
+
expect(pinnedZcodeMode({})).toBeUndefined();
|
|
388
|
+
expect(pinnedZcodeMode({ XYZ_ZCODE_MODE: "garbage" })).toBeUndefined();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("appserver 定向:不探(探针 boot 0)不降(-32602 直接上报,spawn 通道不启用)", async () => {
|
|
392
|
+
const { engine, stateFile, workspace, fakeLaunch } = makeDegradeEngine({
|
|
393
|
+
mode: "appserver",
|
|
394
|
+
createError: { code: -32602, message: "Invalid params" },
|
|
395
|
+
});
|
|
396
|
+
const r1 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
397
|
+
expect(r1.outcome.error).toContain("engine_run_failed");
|
|
398
|
+
expect(r1.outcome.error).toContain("-32602");
|
|
399
|
+
expect(r1.outcome.engineFallback).toBeUndefined(); // 不降级 → 无标注
|
|
400
|
+
expect(fakeLaunch.calls).toHaveLength(0);
|
|
401
|
+
expect(probeBootCount(stateFile)).toBe(0);
|
|
402
|
+
// 定向持续失败直接报:第二任务仍走 app-server(create 计数增长,非降级冻结)
|
|
403
|
+
const createsBefore = recvFrames(stateFile, "session/create").length;
|
|
404
|
+
const r2 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
405
|
+
expect(r2.outcome.error).toContain("-32602");
|
|
406
|
+
expect(recvFrames(stateFile, "session/create").length).toBeGreaterThan(createsBefore);
|
|
407
|
+
expect(fakeLaunch.calls).toHaveLength(0);
|
|
408
|
+
}, 20_000);
|
|
409
|
+
|
|
410
|
+
it("spawn 定向:不探不降直 spawn(零 app-server boot、零协议帧)", async () => {
|
|
411
|
+
const { engine, stateFile, workspace, fakeLaunch } = makeDegradeEngine({ mode: "spawn" });
|
|
412
|
+
const r1 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
413
|
+
expect(r1.outcome.error).toBeUndefined();
|
|
414
|
+
expect(r1.outcome.engineFallback).toBeUndefined();
|
|
415
|
+
expect(fakeLaunch.calls).toHaveLength(1);
|
|
416
|
+
expect(bootCount(stateFile)).toBe(0);
|
|
417
|
+
expect(readState(stateFile).filter((e) => e.ev === "recv")).toHaveLength(0);
|
|
418
|
+
}, 15_000);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ============================================================
|
|
422
|
+
// ④ 错误分类表(非漂移类:-32603 / -32004 / -32010 不降级)
|
|
423
|
+
// ============================================================
|
|
424
|
+
|
|
425
|
+
describe("错误分类(错误规格表)", () => {
|
|
426
|
+
it("-32603 'Model config is missing' → engine_credential_missing,不降级(第二任务仍走 app-server)", async () => {
|
|
427
|
+
const { engine, stateFile, workspace, fakeLaunch } = makeDegradeEngine({
|
|
428
|
+
createError: { code: -32603, message: "Model config is missing" },
|
|
429
|
+
});
|
|
430
|
+
const r1 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
431
|
+
expect(r1.outcome.error).toContain("engine_credential_missing");
|
|
432
|
+
expect(r1.outcome.engineFallback).toBeUndefined();
|
|
433
|
+
expect(fakeLaunch.calls).toHaveLength(0);
|
|
434
|
+
const createsBefore = recvFrames(stateFile, "session/create").length;
|
|
435
|
+
const r2 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
436
|
+
expect(r2.outcome.error).toContain("engine_credential_missing");
|
|
437
|
+
expect(recvFrames(stateFile, "session/create").length).toBeGreaterThan(createsBefore);
|
|
438
|
+
expect(fakeLaunch.calls).toHaveLength(0);
|
|
439
|
+
}, 20_000);
|
|
440
|
+
|
|
441
|
+
it("-32004 'Session is not active' → 任务失败上报,不降级", async () => {
|
|
442
|
+
const { engine, workspace, fakeLaunch } = makeDegradeEngine({
|
|
443
|
+
createError: { code: -32004, message: "Session is not active" },
|
|
444
|
+
});
|
|
445
|
+
const r1 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
446
|
+
expect(r1.outcome.error).toContain("engine_run_failed");
|
|
447
|
+
expect(r1.outcome.error).toContain("-32004");
|
|
448
|
+
expect(r1.outcome.engineFallback).toBeUndefined();
|
|
449
|
+
expect(fakeLaunch.calls).toHaveLength(0);
|
|
450
|
+
}, 20_000);
|
|
451
|
+
|
|
452
|
+
it("-32010(send busy)→ 任务失败上报,不降级", async () => {
|
|
453
|
+
const { engine, workspace, fakeLaunch } = makeDegradeEngine({
|
|
454
|
+
sendError: { code: -32010, message: "A turn is already running" },
|
|
455
|
+
});
|
|
456
|
+
const r1 = await engine.run(makeTask({ cwd: workspace }), makeCtx());
|
|
457
|
+
expect(r1.outcome.error).toContain("engine_run_failed");
|
|
458
|
+
expect(r1.outcome.error).toContain("-32010");
|
|
459
|
+
expect(r1.outcome.engineFallback).toBeUndefined();
|
|
460
|
+
expect(fakeLaunch.calls).toHaveLength(0);
|
|
461
|
+
}, 20_000);
|
|
462
|
+
});
|
|
@@ -16,7 +16,10 @@ import * as path from "node:path";
|
|
|
16
16
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
17
17
|
|
|
18
18
|
import { resolvePoolDir } from "../../../paths.ts";
|
|
19
|
+
import type { AgentEvent } from "../../../types.ts";
|
|
20
|
+
import { ZCODE_APPSERVER_POOL_KEY } from "../constants.ts";
|
|
19
21
|
import { ZcodeEngine } from "../zcode-engine.ts";
|
|
22
|
+
import { assertAgentEventInvariants } from "../../../__tests__/conformance/agent-event-invariants.ts";
|
|
20
23
|
|
|
21
24
|
const LIVE = process.env["ENGINE_CONFORMANCE_LIVE"] === "1";
|
|
22
25
|
const E2E_MODEL = process.env["ZCODE_E2E_MODEL"] ?? "e512d53e-0bfc-4915-9081-860d4aa13cd0/mimo-v2.5-pro";
|
|
@@ -28,7 +31,12 @@ describe.skipIf(!LIVE)("ZcodeEngine 端到端真机(真实 LLM 调用)", ()
|
|
|
28
31
|
|
|
29
32
|
beforeAll(() => {
|
|
30
33
|
fs.mkdirSync(WORK_CWD, { recursive: true });
|
|
31
|
-
engine = new ZcodeEngine({
|
|
34
|
+
engine = new ZcodeEngine({
|
|
35
|
+
engineDataDir: () => DATA_ROOT,
|
|
36
|
+
// [R4] 钉扎 spawn 单轮(本门原为 spawn 链路实录;app-server 常驻真机门由 R6
|
|
37
|
+
// live gate 改写接入——两模式各自的真机覆盖不合并)
|
|
38
|
+
processEnv: { ...process.env, XYZ_ZCODE_MODE: "spawn" },
|
|
39
|
+
});
|
|
32
40
|
});
|
|
33
41
|
|
|
34
42
|
afterAll(() => {
|
|
@@ -125,3 +133,113 @@ function listZcodeProcessesForCwd(cwd: string): string[] {
|
|
|
125
133
|
return [];
|
|
126
134
|
}
|
|
127
135
|
}
|
|
136
|
+
|
|
137
|
+
// ============================================================
|
|
138
|
+
// [R6] app-server 常驻真机门(RA1/RA2/RA4 的本仓可测形态):真实 zcode.cjs +
|
|
139
|
+
// 真实凭据 + 真实 app-server 进程,XYZ_ZCODE_MODE=appserver 定向(不探不降)。
|
|
140
|
+
// 与上方 spawn 门分立(两模式各自的真机覆盖不合并);跨仓 live gate(RA8 live /
|
|
141
|
+
// zsw 真机)另由跨仓段承载。skip 条件与 spawn 门一致(ENGINE_CONFORMANCE_LIVE)。
|
|
142
|
+
// ============================================================
|
|
143
|
+
|
|
144
|
+
const AS_DATA_ROOT = path.join(fs.realpathSync(os.tmpdir()), "zcode-r6-appserver-e2e-data");
|
|
145
|
+
const AS_WORK_CWD = path.join(fs.realpathSync(os.tmpdir()), "zcode-r6-appserver-e2e-cwd");
|
|
146
|
+
|
|
147
|
+
describe.skipIf(!LIVE)("ZcodeEngine 端到端真机(app-server 常驻,[R6] live gate 改写)", () => {
|
|
148
|
+
let engine: ZcodeEngine;
|
|
149
|
+
|
|
150
|
+
beforeAll(() => {
|
|
151
|
+
fs.mkdirSync(AS_WORK_CWD, { recursive: true });
|
|
152
|
+
engine = new ZcodeEngine({
|
|
153
|
+
engineDataDir: () => AS_DATA_ROOT,
|
|
154
|
+
// 定向 appserver(D2①):不探不降——常驻路径本体;缺省路径门控真机另由
|
|
155
|
+
// RA5-②③(跨仓段)承载
|
|
156
|
+
processEnv: { ...process.env, XYZ_ZCODE_MODE: "appserver" },
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
afterAll(async () => {
|
|
161
|
+
// dispose 是常驻进程的唯一收割入口(D6)——先 dispose 再清目录
|
|
162
|
+
await engine.dispose().catch(() => undefined);
|
|
163
|
+
for (const dir of [AS_DATA_ROOT, AS_WORK_CWD]) {
|
|
164
|
+
try {
|
|
165
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
166
|
+
} catch {
|
|
167
|
+
// 尽力清理
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("run:常驻通道全链(RA2——stream 事件流出 + read①级 native + poolKey 锚定 home-appserver)", async () => {
|
|
173
|
+
const events: AgentEvent[] = [];
|
|
174
|
+
const { handle, outcome } = await engine.run(
|
|
175
|
+
{
|
|
176
|
+
task: 'Verify this arithmetic check: 2 + 2 = 4. If it is correct the verdict must be "ok", otherwise "bad".',
|
|
177
|
+
slug: "e2e-appserver",
|
|
178
|
+
model: E2E_MODEL,
|
|
179
|
+
cwd: AS_WORK_CWD,
|
|
180
|
+
schema: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: { verdict: { type: "string", enum: ["ok", "bad"] } },
|
|
183
|
+
required: ["verdict"],
|
|
184
|
+
additionalProperties: false,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{ taskId: "sa-live-appserver", poolKey: "", onEvent: (e) => events.push(e) },
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// C2:outcome 正确
|
|
191
|
+
expect(outcome.error).toBeUndefined();
|
|
192
|
+
expect(outcome.exitCode).toBe(0);
|
|
193
|
+
expect(outcome.sessionId).toMatch(/^sess_/);
|
|
194
|
+
expect(outcome.parsedOutput).toEqual({ verdict: "ok" });
|
|
195
|
+
expect(outcome.usage?.input).toBeGreaterThan(0);
|
|
196
|
+
|
|
197
|
+
// C3 stream 口径(RA2-①断言面):text_delta 实时流出(非终态一次性)+ 不变量五条
|
|
198
|
+
// (3a 拼接 == read 全文——content 来自 read 兜底/收尾帧,同源比对)
|
|
199
|
+
expect(events.some((e) => e.type === "text_delta")).toBe(true);
|
|
200
|
+
assertAgentEventInvariants(events, { granularity: "stream", content: outcome.content });
|
|
201
|
+
|
|
202
|
+
// handle 锚定(RA2-②断言面):poolKey=home-appserver,①级读取钥匙随 handle 走
|
|
203
|
+
expect(handle.data.poolKey).toBe(ZCODE_APPSERVER_POOL_KEY);
|
|
204
|
+
|
|
205
|
+
// read 第①级:SQLite 完整重建(非降级)
|
|
206
|
+
const view = await engine.read(handle);
|
|
207
|
+
expect(view.source).toBe("native");
|
|
208
|
+
expect(view.turns.length).toBeGreaterThanOrEqual(1);
|
|
209
|
+
}, 300_000);
|
|
210
|
+
|
|
211
|
+
it("abort:长任务中途取消(RA4)→ stop 链合成终态 exitCode=null + 常驻进程不残留 + 后续任务可用", async () => {
|
|
212
|
+
const controller = new AbortController();
|
|
213
|
+
const runPromise = engine.run(
|
|
214
|
+
{
|
|
215
|
+
task:
|
|
216
|
+
"Write an extremely detailed 5000-word technical essay about distributed systems consistency models. Do not stop early.",
|
|
217
|
+
slug: "e2e-appserver-abort",
|
|
218
|
+
model: E2E_MODEL,
|
|
219
|
+
cwd: AS_WORK_CWD,
|
|
220
|
+
},
|
|
221
|
+
{ taskId: "sa-live-appserver-abort", poolKey: "", signal: controller.signal },
|
|
222
|
+
);
|
|
223
|
+
// 等 send 已发(会话在途),再中途 abort——覆盖 stop 链而非 pre-abort 短路
|
|
224
|
+
await new Promise((r) => setTimeout(r, 4_000));
|
|
225
|
+
controller.abort();
|
|
226
|
+
const { outcome } = await runPromise;
|
|
227
|
+
|
|
228
|
+
expect(outcome.exitCode).toBeNull();
|
|
229
|
+
expect(outcome.error).toContain("session/stop");
|
|
230
|
+
|
|
231
|
+
// 无残留:常驻 HOME 锚定的 app-server 进程在 dispose 前应 ≤1(abort 不误杀共享
|
|
232
|
+
// 进程;杀的是本仓 HOME,与其他 zcode 实例区分靠 --cwd HOME 路径)
|
|
233
|
+
const homeDir = resolvePoolDir(AS_DATA_ROOT, "zcode", ZCODE_APPSERVER_POOL_KEY);
|
|
234
|
+
const leftovers = listZcodeProcessesForCwd(homeDir);
|
|
235
|
+
expect(leftovers.length).toBeLessThanOrEqual(1);
|
|
236
|
+
|
|
237
|
+
// 崩溃/中止后下一任务自动重建或复用(不变量 4 同路径)——abort 不污染常驻进程
|
|
238
|
+
const second = await engine.run(
|
|
239
|
+
{ task: "Reply with the single word: ok", slug: "e2e-appserver-after-abort", model: E2E_MODEL, cwd: AS_WORK_CWD },
|
|
240
|
+
{ taskId: "sa-live-appserver-after-abort", poolKey: "" },
|
|
241
|
+
);
|
|
242
|
+
expect(second.outcome.error).toBeUndefined();
|
|
243
|
+
expect(second.outcome.content.trim().length).toBeGreaterThan(0);
|
|
244
|
+
}, 300_000);
|
|
245
|
+
});
|