@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,472 @@
|
|
|
1
|
+
// connection.test.ts —— R2 连接层测试:全部跑 __fixtures__/fake-appserver.mjs 子进程
|
|
2
|
+
// (移植自 zsw 仓 84b63a0^ fake-server 测试模式,vitest 形态改造),绝不 spawn 真
|
|
3
|
+
// zcode.cjs。覆盖验收条款:四帧型分发逐型 / 请求-应答 id 关联(并发不串)/
|
|
4
|
+
// requestRuntimePreferences 常量应答逐字段 / 未知反向回空 result / onClose 全部在途
|
|
5
|
+
// reject(含 stderr 尾 400 截断)/ 惰性启动与进程死后重建 / stderr tee 落盘 /
|
|
6
|
+
// protocol 自报忽略 / 无 jsonrpc 字段的帧收发(出站精确键集 = 对面 strict 拒未知键
|
|
7
|
+
// 的镜像义务)/ env 惯例 / 关闭原语幂等。
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdtempSync, readFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
15
|
+
|
|
16
|
+
import { ZCODE_APPSERVER_STDERR_TAIL_CHARS } from "../constants.ts";
|
|
17
|
+
import {
|
|
18
|
+
AppServerConnection,
|
|
19
|
+
RUNTIME_PREFERENCES,
|
|
20
|
+
buildAppServerEnv,
|
|
21
|
+
isAppServerRpcError,
|
|
22
|
+
} from "../connection.ts";
|
|
23
|
+
|
|
24
|
+
const TMP = mkdtempSync(join(tmpdir(), "zcode-conn-"));
|
|
25
|
+
const FAKE_CLI = fileURLToPath(new URL("./__fixtures__/fake-appserver.mjs", import.meta.url));
|
|
26
|
+
|
|
27
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
28
|
+
|
|
29
|
+
/** fake 侧事件流水条目(JSONL:boot/env/recv/reverse-answer)。 */
|
|
30
|
+
interface StateEvent {
|
|
31
|
+
seq: number;
|
|
32
|
+
ev: string;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readState(file: string): StateEvent[] {
|
|
37
|
+
try {
|
|
38
|
+
return readFileSync(file, "utf8")
|
|
39
|
+
.trim()
|
|
40
|
+
.split("\n")
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.map((l) => JSON.parse(l) as StateEvent);
|
|
43
|
+
} catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
49
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 轮询等待 fake 侧异步效果可见(fake 异步处理 stdin,不能假设即时)。 */
|
|
53
|
+
async function waitFor(cond: () => boolean, ms = 5_000, step = 25): Promise<boolean> {
|
|
54
|
+
const end = Date.now() + ms;
|
|
55
|
+
while (Date.now() < end) {
|
|
56
|
+
if (cond()) return true;
|
|
57
|
+
await sleep(step);
|
|
58
|
+
}
|
|
59
|
+
return cond();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface FakeHandle {
|
|
63
|
+
conn: AppServerConnection;
|
|
64
|
+
stateFile: string;
|
|
65
|
+
stderrLog: string;
|
|
66
|
+
homeDir: string;
|
|
67
|
+
cwd: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface FakeOpts {
|
|
71
|
+
/** FAKE_STDERR=1:启动时往 stderr 写两行。 */
|
|
72
|
+
stderrLines?: boolean;
|
|
73
|
+
/** FAKE_EXTRA_KEYS=1:fake 应答/推送帧携带未知键(客户端宽容解析断言)。 */
|
|
74
|
+
extraKeys?: boolean;
|
|
75
|
+
/** FAKE_PROTOCOL_PUSH=1:协议自报走 {method:'protocol'} 推送形态。 */
|
|
76
|
+
protocolPush?: boolean;
|
|
77
|
+
/** 基 env 预置引擎原生嵌套标记(nesting guard 剥离断言)。 */
|
|
78
|
+
polluteNested?: boolean;
|
|
79
|
+
/** 注入故障反向 handler(fire-and-forget catch 断言)。 */
|
|
80
|
+
reverseHandlers?: Readonly<Record<string, (params: unknown) => unknown>>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const CONNECTIONS: AppServerConnection[] = [];
|
|
84
|
+
let connSeq = 0;
|
|
85
|
+
|
|
86
|
+
/** 建一个连到 fake 的连接(env 面向子进程固化,测试间零共享状态)。 */
|
|
87
|
+
function makeConnection(opts: FakeOpts = {}): FakeHandle {
|
|
88
|
+
connSeq += 1;
|
|
89
|
+
const stateFile = join(TMP, `state-${connSeq}.jsonl`);
|
|
90
|
+
const stderrLog = join(TMP, `stderr-${connSeq}.log`);
|
|
91
|
+
const homeDir = join(TMP, `home-${connSeq}`);
|
|
92
|
+
const cwd = join(TMP, `cwd-${connSeq}`);
|
|
93
|
+
const base: NodeJS.ProcessEnv = {
|
|
94
|
+
PATH: process.env.PATH ?? "",
|
|
95
|
+
FAKE_STATE_FILE: stateFile,
|
|
96
|
+
...(opts.stderrLines ? { FAKE_STDERR: "1" } : {}),
|
|
97
|
+
...(opts.extraKeys ? { FAKE_EXTRA_KEYS: "1" } : {}),
|
|
98
|
+
...(opts.protocolPush ? { FAKE_PROTOCOL_PUSH: "1" } : {}),
|
|
99
|
+
...(opts.polluteNested ? { ZSW_NESTED: "1", CLAUDECODE: "1", PI_SUBAGENT_DEPTH: "2" } : {}),
|
|
100
|
+
};
|
|
101
|
+
const conn = new AppServerConnection({
|
|
102
|
+
cliPath: FAKE_CLI,
|
|
103
|
+
cwd,
|
|
104
|
+
env: buildAppServerEnv(homeDir, base),
|
|
105
|
+
stderrLogPath: stderrLog,
|
|
106
|
+
...(opts.reverseHandlers ? { reverseHandlers: opts.reverseHandlers } : {}),
|
|
107
|
+
});
|
|
108
|
+
CONNECTIONS.push(conn);
|
|
109
|
+
return { conn, stateFile, stderrLog, homeDir, cwd };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
afterEach(async () => {
|
|
113
|
+
await Promise.allSettled(CONNECTIONS.splice(0).map((c) => c.shutdown({ graceMs: 1_000 })));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ------------------------------------------- 惰性启动与重建(D1 / 不变量 4)
|
|
117
|
+
|
|
118
|
+
describe("惰性启动与惰性重建", () => {
|
|
119
|
+
it("构造不 spawn:无流水、无 pid、alive=false(连接构建零成本)", async () => {
|
|
120
|
+
const { conn, stateFile } = makeConnection();
|
|
121
|
+
expect(conn.alive).toBe(false);
|
|
122
|
+
expect(conn.pid).toBeUndefined();
|
|
123
|
+
await sleep(80);
|
|
124
|
+
expect(existsSync(stateFile)).toBe(false);
|
|
125
|
+
expect(conn.pid).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("首次请求才 spawn:boot 流水出现,argv 形态 = `app-server --cwd <dir>`(D10 基线,不带 --stdio/--surface)", async () => {
|
|
129
|
+
const { conn, stateFile, cwd } = makeConnection();
|
|
130
|
+
const r = await conn.request("test/echo", { marker: "first" });
|
|
131
|
+
expect(r).toEqual({ marker: "first" });
|
|
132
|
+
const boots = readState(stateFile).filter((e) => e.ev === "boot");
|
|
133
|
+
expect(boots).toHaveLength(1);
|
|
134
|
+
expect(boots[0].argv).toEqual(["app-server", "--cwd", cwd]);
|
|
135
|
+
expect(conn.pid).toBeGreaterThan(0);
|
|
136
|
+
expect(conn.alive).toBe(true);
|
|
137
|
+
}, 10_000);
|
|
138
|
+
|
|
139
|
+
it("进程死后下次请求自动重建(与惰性启动同路径):boot 两次、pid 变化", async () => {
|
|
140
|
+
const { conn, stateFile } = makeConnection();
|
|
141
|
+
await conn.request("test/echo", { n: 1 });
|
|
142
|
+
const pidBefore = conn.pid;
|
|
143
|
+
await expect(conn.request("test/suicide")).rejects.toThrow(/app-server/);
|
|
144
|
+
expect(await waitFor(() => !conn.alive)).toBe(true);
|
|
145
|
+
const r2 = await conn.request("test/echo", { n: 2 });
|
|
146
|
+
expect(r2).toEqual({ n: 2 });
|
|
147
|
+
const boots = readState(stateFile).filter((e) => e.ev === "boot");
|
|
148
|
+
expect(boots).toHaveLength(2);
|
|
149
|
+
expect(boots[1].pid).not.toBe(boots[0].pid);
|
|
150
|
+
expect(conn.pid).not.toBe(pidBefore);
|
|
151
|
+
}, 15_000);
|
|
152
|
+
|
|
153
|
+
it("spawn 失败(nodeBin ENOENT)走同一收割路径:请求 reject 带 spawn 失败语境,连接不悬挂", async () => {
|
|
154
|
+
const conn = new AppServerConnection({
|
|
155
|
+
cliPath: FAKE_CLI,
|
|
156
|
+
cwd: TMP,
|
|
157
|
+
nodeBin: join(TMP, "no-such-node-bin"),
|
|
158
|
+
env: { PATH: process.env.PATH ?? "" },
|
|
159
|
+
stderrLogPath: join(TMP, "enoent.log"),
|
|
160
|
+
});
|
|
161
|
+
CONNECTIONS.push(conn);
|
|
162
|
+
await expect(conn.request("test/echo", {})).rejects.toThrow(/spawn 失败/);
|
|
163
|
+
expect(await waitFor(() => !conn.alive)).toBe(true);
|
|
164
|
+
}, 10_000);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ------------------------------------------- 四帧型分发(附录 A.1)
|
|
168
|
+
|
|
169
|
+
describe("四帧型分发(NDJSON,无 jsonrpc 字段)", () => {
|
|
170
|
+
it("客户端请求帧:出站精确键集 {id, method, params}——无 jsonrpc、无未知键(对面 strict 拒未知键)", async () => {
|
|
171
|
+
const { conn, stateFile } = makeConnection();
|
|
172
|
+
await conn.request("test/echo", { a: 1 });
|
|
173
|
+
const frames = readState(stateFile)
|
|
174
|
+
.map((e) => e.frame)
|
|
175
|
+
.filter((f): f is Record<string, unknown> => isRecord(f) && f.method === "test/echo");
|
|
176
|
+
expect(frames).toHaveLength(1);
|
|
177
|
+
expect(Object.keys(frames[0]).sort()).toEqual(["id", "method", "params"]);
|
|
178
|
+
expect(typeof frames[0].id).toBe("number");
|
|
179
|
+
expect("jsonrpc" in frames[0]).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("应答帧(result):resolve 应答值;帧级未知键宽容解析(我们是客户端,strict 校验是对面)", async () => {
|
|
183
|
+
const { conn } = makeConnection({ extraKeys: true });
|
|
184
|
+
const r = await conn.request<{ marker: string }>("test/echo", { marker: "lenient" });
|
|
185
|
+
expect(r).toEqual({ marker: "lenient" });
|
|
186
|
+
// 推送帧携带未知键同样不破坏分发
|
|
187
|
+
const received: unknown[] = [];
|
|
188
|
+
conn.onNotification("test/notification", (p) => received.push(p));
|
|
189
|
+
await conn.request("test/push", { pushMethod: "test/notification", pushParams: { delta: "hi" } });
|
|
190
|
+
expect(received).toEqual([{ delta: "hi" }]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("应答帧(error):code/message/data 挂到 reject Error(A.3 错误码通道,-32602 zod 诊断随 data 带出)", async () => {
|
|
194
|
+
const { conn } = makeConnection();
|
|
195
|
+
const err = await conn
|
|
196
|
+
.request("test/fail", {
|
|
197
|
+
code: -32602,
|
|
198
|
+
message: "Invalid params",
|
|
199
|
+
data: [{ path: ["model"], message: "not an object" }],
|
|
200
|
+
})
|
|
201
|
+
.then(
|
|
202
|
+
() => {
|
|
203
|
+
throw new Error("should reject");
|
|
204
|
+
},
|
|
205
|
+
(e: unknown) => e,
|
|
206
|
+
);
|
|
207
|
+
expect(isAppServerRpcError(err)).toBe(true);
|
|
208
|
+
if (isAppServerRpcError(err)) {
|
|
209
|
+
expect(err.code).toBe(-32602);
|
|
210
|
+
expect(err.data).toEqual([{ path: ["model"], message: "not an object" }]);
|
|
211
|
+
expect(err.message).toContain("[-32602]");
|
|
212
|
+
expect(err.message).toContain("Invalid params");
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("服务端推送帧:onNotification(method) 收到 params;handler 异常不拖死帧泵;退订生效", async () => {
|
|
217
|
+
const { conn } = makeConnection();
|
|
218
|
+
const received: unknown[] = [];
|
|
219
|
+
const receivedBoom: unknown[] = [];
|
|
220
|
+
const receivedOff: unknown[] = [];
|
|
221
|
+
conn.onNotification("test/notification", (params) => {
|
|
222
|
+
received.push(params);
|
|
223
|
+
});
|
|
224
|
+
conn.onNotification("test/notification", (params) => {
|
|
225
|
+
receivedBoom.push(params);
|
|
226
|
+
throw new Error("handler boom");
|
|
227
|
+
});
|
|
228
|
+
const off = conn.onNotification("test/notification", (params) => {
|
|
229
|
+
receivedOff.push(params);
|
|
230
|
+
});
|
|
231
|
+
off();
|
|
232
|
+
await conn.request("test/push", { pushMethod: "test/notification", pushParams: { delta: "hello" } });
|
|
233
|
+
await conn.request("test/push", { pushMethod: "test/notification", pushParams: { delta: "again" } });
|
|
234
|
+
expect(received).toEqual([{ delta: "hello" }, { delta: "again" }]);
|
|
235
|
+
expect(receivedBoom).toEqual([{ delta: "hello" }, { delta: "again" }]);
|
|
236
|
+
expect(receivedOff).toEqual([]);
|
|
237
|
+
// 抛过异常后连接仍可用
|
|
238
|
+
const r = await conn.request("test/echo", { ok: 1 });
|
|
239
|
+
expect(r).toEqual({ ok: 1 });
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ------------------------------------------- 请求-应答 id 关联
|
|
244
|
+
|
|
245
|
+
describe("请求-应答 id 关联", () => {
|
|
246
|
+
it("并发多请求不串线:3 个在途请求各自 resolve 自己的回显;id 自增互异", async () => {
|
|
247
|
+
const { conn, stateFile } = makeConnection();
|
|
248
|
+
const [a, b, c] = await Promise.all([
|
|
249
|
+
conn.request("test/delay-echo", { who: "a", delayMs: 60 }),
|
|
250
|
+
conn.request("test/delay-echo", { who: "b", delayMs: 30 }),
|
|
251
|
+
conn.request("test/echo", { who: "c" }),
|
|
252
|
+
]);
|
|
253
|
+
expect(a).toEqual({ who: "a", delayMs: 60 });
|
|
254
|
+
expect(b).toEqual({ who: "b", delayMs: 30 });
|
|
255
|
+
expect(c).toEqual({ who: "c" });
|
|
256
|
+
const ids = readState(stateFile)
|
|
257
|
+
.map((e) => e.frame)
|
|
258
|
+
.filter((f): f is { id: number } => isRecord(f) && typeof f.method === "string" && typeof f.id === "number")
|
|
259
|
+
.map((f) => f.id);
|
|
260
|
+
expect(ids).toHaveLength(3);
|
|
261
|
+
expect(new Set(ids).size).toBe(3);
|
|
262
|
+
expect([...ids].sort((x, y) => x - y)).toEqual([1, 2, 3]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("超时预算参数化:小预算 reject 超时,pending 清理后后续请求正常", async () => {
|
|
266
|
+
const { conn } = makeConnection();
|
|
267
|
+
await expect(conn.request("test/delay-echo", { delayMs: 5_000 }, { timeoutMs: 120 })).rejects.toThrow(/超时/);
|
|
268
|
+
const r = await conn.request("test/echo", { after: "timeout" });
|
|
269
|
+
expect(r).toEqual({ after: "timeout" });
|
|
270
|
+
}, 10_000);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ------------------------------------------- 反向请求应答(D9)
|
|
274
|
+
|
|
275
|
+
describe("反向请求应答(D9)", () => {
|
|
276
|
+
it("session/requestRuntimePreferences → 常量表应答逐字段(真实协议:create 先发反向请求并等应答)", async () => {
|
|
277
|
+
const { conn, stateFile } = makeConnection();
|
|
278
|
+
const created = await conn.request<{ session: { sessionId: string } }>("session/create", {
|
|
279
|
+
workspace: { workspacePath: "/w", workspaceKey: "ws-x" },
|
|
280
|
+
mode: "yolo",
|
|
281
|
+
});
|
|
282
|
+
expect(typeof created.session.sessionId).toBe("string");
|
|
283
|
+
const answers = readState(stateFile).filter((e) => e.ev === "reverse-answer");
|
|
284
|
+
expect(answers).toHaveLength(1);
|
|
285
|
+
const result = (answers[0].answer as { result?: Record<string, unknown> }).result;
|
|
286
|
+
// 逐字段断言(D9 常量逐字来自旧实现实测)
|
|
287
|
+
expect(result).toEqual({ ...RUNTIME_PREFERENCES });
|
|
288
|
+
expect(result?.nativeSearchEnhancementsEnabled).toBe(true);
|
|
289
|
+
expect(result?.memoryEnabled).toBe(false);
|
|
290
|
+
expect(result?.askUserQuestionAutoResolutionEnabled).toBe(true);
|
|
291
|
+
expect(result?.modelContextBudgetStrategy).toBe("preflight-v1");
|
|
292
|
+
expect(Object.keys(result ?? {}).sort()).toEqual([
|
|
293
|
+
"askUserQuestionAutoResolutionEnabled",
|
|
294
|
+
"memoryEnabled",
|
|
295
|
+
"modelContextBudgetStrategy",
|
|
296
|
+
"nativeSearchEnhancementsEnabled",
|
|
297
|
+
]);
|
|
298
|
+
// 反向应答帧出站精确键集 {id, result}(无 jsonrpc、无未知键)
|
|
299
|
+
const answerFrame = readState(stateFile)
|
|
300
|
+
.map((e) => e.frame)
|
|
301
|
+
.find((f): f is Record<string, unknown> => isRecord(f) && typeof f.id === "string");
|
|
302
|
+
expect(answerFrame).toBeDefined();
|
|
303
|
+
expect(Object.keys(answerFrame).sort()).toEqual(["id", "result"]);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("未知反向请求 → 回 {id, result:{}}(不答会 15s 超时断连,旧实测 -32022)", async () => {
|
|
307
|
+
const { conn, stateFile } = makeConnection();
|
|
308
|
+
await conn.request("test/reverse", { reverseMethod: "permission/request", reverseParams: { tool: "Bash" } });
|
|
309
|
+
const answers = readState(stateFile).filter((e) => e.ev === "reverse-answer");
|
|
310
|
+
expect(answers).toHaveLength(1);
|
|
311
|
+
const answer = answers[0].answer as { result?: unknown; error?: unknown };
|
|
312
|
+
expect(answer.result).toEqual({});
|
|
313
|
+
expect(answer.error).toBeUndefined();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("应答失败不拖死连接(fire-and-forget + catch):故障 handler → 回 error 帧,连接仍可用", async () => {
|
|
317
|
+
const { conn, stateFile } = makeConnection({
|
|
318
|
+
reverseHandlers: {
|
|
319
|
+
"session/requestRuntimePreferences": () => {
|
|
320
|
+
throw new Error("prefs boom");
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
const created = await conn.request("session/create", {});
|
|
325
|
+
expect(created).toBeTruthy();
|
|
326
|
+
const answers = readState(stateFile).filter((e) => e.ev === "reverse-answer");
|
|
327
|
+
expect(answers).toHaveLength(1);
|
|
328
|
+
const answer = answers[0].answer as { error?: { code?: number; message?: string } };
|
|
329
|
+
expect(answer.error?.code).toBe(-32000);
|
|
330
|
+
expect(answer.error?.message).toContain("prefs boom");
|
|
331
|
+
const r = await conn.request("test/echo", { still: "alive" });
|
|
332
|
+
expect(r).toEqual({ still: "alive" });
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ------------------------------------------- 崩溃收割(onClose)
|
|
337
|
+
|
|
338
|
+
describe("崩溃收割(onClose/exit)", () => {
|
|
339
|
+
it("进程意外退出:全部在途 reject,错误信息附 stderr 尾(400 字符截断)", async () => {
|
|
340
|
+
const { conn } = makeConnection();
|
|
341
|
+
await conn.request("test/echo", { warm: true });
|
|
342
|
+
const errors = await Promise.all(
|
|
343
|
+
[
|
|
344
|
+
conn.request("test/delay-echo", { who: "a", delayMs: 10_000 }),
|
|
345
|
+
conn.request("test/delay-echo", { who: "b", delayMs: 10_000 }),
|
|
346
|
+
conn.request("test/suicide"),
|
|
347
|
+
].map((p) => p.then(
|
|
348
|
+
() => {
|
|
349
|
+
throw new Error("should reject");
|
|
350
|
+
},
|
|
351
|
+
(e: unknown) => e,
|
|
352
|
+
)),
|
|
353
|
+
);
|
|
354
|
+
expect(errors).toHaveLength(3);
|
|
355
|
+
for (const err of errors) {
|
|
356
|
+
expect(err).toBeInstanceOf(Error);
|
|
357
|
+
const msg = (err as Error).message;
|
|
358
|
+
expect(msg).toContain("app-server");
|
|
359
|
+
expect(msg).toContain("code=1");
|
|
360
|
+
expect(msg).toContain("CRASH-MARK-TAIL");
|
|
361
|
+
// 截断到常量值:600 个 A 只保留尾部(< 400),整段噪声不允许进错误信息
|
|
362
|
+
expect(msg).not.toMatch(new RegExp(`A{${ZCODE_APPSERVER_STDERR_TAIL_CHARS}}`));
|
|
363
|
+
}
|
|
364
|
+
expect(await waitFor(() => !conn.alive)).toBe(true);
|
|
365
|
+
}, 15_000);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ------------------------------------------- stderr tee 落盘
|
|
369
|
+
|
|
370
|
+
describe("stderr tee 落盘", () => {
|
|
371
|
+
it("stderr 实时 append 到参数化路径(引擎数据目录注入点;测试用 tmp)", async () => {
|
|
372
|
+
const { conn, stderrLog } = makeConnection({ stderrLines: true });
|
|
373
|
+
await conn.request("test/echo", {});
|
|
374
|
+
const ok = await waitFor(() => {
|
|
375
|
+
try {
|
|
376
|
+
const text = readFileSync(stderrLog, "utf8");
|
|
377
|
+
return text.includes("fake-appserver stderr line A") && text.includes("fake-appserver stderr line B");
|
|
378
|
+
} catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
expect(ok).toBe(true);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ------------------------------------------- env 惯例(构造注入面)
|
|
387
|
+
|
|
388
|
+
describe("env 惯例(沿用 launcher 惯例的参数化注入)", () => {
|
|
389
|
+
it("buildAppServerEnv:HOME 隔离 + 遥测关闭 + nesting guard 剥离/注入(fake 侧 env 快照)", async () => {
|
|
390
|
+
const { conn, stateFile, homeDir } = makeConnection({ polluteNested: true });
|
|
391
|
+
await conn.request("test/echo", {});
|
|
392
|
+
const envEvents = readState(stateFile).filter((e) => e.ev === "env");
|
|
393
|
+
expect(envEvents).toHaveLength(1);
|
|
394
|
+
const snap = envEvents[0] as {
|
|
395
|
+
home?: string;
|
|
396
|
+
telemetry?: string;
|
|
397
|
+
nested?: string;
|
|
398
|
+
unifiedNested?: string;
|
|
399
|
+
};
|
|
400
|
+
expect(snap.home).toBe(homeDir);
|
|
401
|
+
expect(snap.telemetry).toBe("false");
|
|
402
|
+
expect(snap.nested).toBeUndefined();
|
|
403
|
+
expect(snap.unifiedNested).toBe("1");
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// ------------------------------------------- 关闭原语(R4 dispose 的连接侧原语)
|
|
408
|
+
|
|
409
|
+
describe("关闭原语", () => {
|
|
410
|
+
it("close():同步 SIGTERM 面——进程随后死亡;幂等;只起过一代", async () => {
|
|
411
|
+
const { conn, stateFile } = makeConnection();
|
|
412
|
+
await conn.request("test/echo", {});
|
|
413
|
+
expect(conn.alive).toBe(true);
|
|
414
|
+
conn.close();
|
|
415
|
+
conn.close();
|
|
416
|
+
expect(await waitFor(() => !conn.alive)).toBe(true);
|
|
417
|
+
expect(readState(stateFile).filter((e) => e.ev === "boot")).toHaveLength(1);
|
|
418
|
+
}, 10_000);
|
|
419
|
+
|
|
420
|
+
it("shutdown():杀链走完进程退出;幂等;之后首个请求自动重建(不变量 4:与崩溃重建同路径)", async () => {
|
|
421
|
+
const { conn, stateFile } = makeConnection();
|
|
422
|
+
await conn.request("test/echo", {});
|
|
423
|
+
await conn.shutdown({ graceMs: 1_500 });
|
|
424
|
+
await conn.shutdown();
|
|
425
|
+
expect(await waitFor(() => !conn.alive)).toBe(true);
|
|
426
|
+
const r = await conn.request("test/echo", { reborn: true });
|
|
427
|
+
expect(r).toEqual({ reborn: true });
|
|
428
|
+
expect(readState(stateFile).filter((e) => e.ev === "boot")).toHaveLength(2);
|
|
429
|
+
}, 15_000);
|
|
430
|
+
|
|
431
|
+
it("close() 后调 shutdown():await close 发起的同一杀链,resolve 不早于进程退出(幂等——同一 promise)", async () => {
|
|
432
|
+
const { conn, stateFile } = makeConnection();
|
|
433
|
+
await conn.request("test/echo", {});
|
|
434
|
+
const pid = conn.pid;
|
|
435
|
+
conn.close();
|
|
436
|
+
let shutdownSettled = false;
|
|
437
|
+
const shutdownPromise = conn.shutdown().then(() => {
|
|
438
|
+
shutdownSettled = true;
|
|
439
|
+
});
|
|
440
|
+
await shutdownPromise;
|
|
441
|
+
expect(shutdownSettled).toBe(true);
|
|
442
|
+
// killChain 挂 exit 事件 resolve(Node exit 前已收尸)——shutdown resolve 时进程
|
|
443
|
+
// 必已退出:kill(0) 探活抛 ESRCH 即「resolve 不早于杀链完成」的机制性证据
|
|
444
|
+
expect(() => process.kill(pid ?? -1, 0)).toThrow(/ESRCH/);
|
|
445
|
+
// 幂等:再次 shutdown 复用同一 promise,无新杀链、无新进程
|
|
446
|
+
await conn.shutdown();
|
|
447
|
+
expect(readState(stateFile).filter((e) => e.ev === "boot")).toHaveLength(1);
|
|
448
|
+
}, 10_000);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// ------------------------------------------- protocol 自报与宽容解析
|
|
452
|
+
|
|
453
|
+
describe("protocol 自报与宽容解析", () => {
|
|
454
|
+
it("首帧 {protocol:{name,version}} 自报忽略不抛,protocolInfo 记录,连接可用", async () => {
|
|
455
|
+
const { conn } = makeConnection();
|
|
456
|
+
const r = await conn.request("test/echo", { ok: 1 });
|
|
457
|
+
expect(r).toEqual({ ok: 1 });
|
|
458
|
+
expect(conn.protocolInfo).toEqual({ name: "ZCode Protocol", version: 1 });
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("protocol 推送形态 {method:'protocol'} 同样收敛到 protocolInfo", async () => {
|
|
462
|
+
const { conn } = makeConnection({ protocolPush: true });
|
|
463
|
+
await conn.request("test/echo", {});
|
|
464
|
+
expect(conn.protocolInfo).toEqual({ name: "ZCode Protocol", version: 1 });
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("坏行跳过不断流:非 JSON 行之后正常应答", async () => {
|
|
468
|
+
const { conn } = makeConnection();
|
|
469
|
+
const r = await conn.request("test/malformed-first", {});
|
|
470
|
+
expect(r).toEqual({ survived: true });
|
|
471
|
+
});
|
|
472
|
+
});
|