@zhushanwen/pi-subagent-workflow 8.4.0 → 8.6.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/package.json +22 -7
- package/relay/relay.mjs +390 -0
- package/skills/subagent-ext-config/SKILL.md +80 -0
- package/src/execution/__tests__/agent-registry.test.ts +110 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +73 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +601 -0
- package/src/execution/__tests__/delivery-methods.test.ts +38 -1
- package/src/execution/__tests__/execute-options-mapper.test.ts +11 -0
- package/src/execution/__tests__/execution-record.test.ts +237 -1
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +63 -32
- package/src/execution/__tests__/helpers/spawn-mock.ts +4 -0
- package/src/execution/__tests__/index-session-start.test.ts +86 -7
- package/src/execution/__tests__/lifecycle-manager.test.ts +46 -0
- package/src/execution/__tests__/list-fields.test.ts +45 -14
- package/src/execution/__tests__/model-resolver.test.ts +57 -5
- package/src/execution/__tests__/notifier-flush.test.ts +64 -26
- package/src/execution/__tests__/notify-ledger.test.ts +826 -0
- package/src/execution/__tests__/output-collector.test.ts +299 -2
- package/src/execution/__tests__/pi-invocation.test.ts +62 -1
- package/src/execution/__tests__/relay-agent.test.ts +448 -0
- package/src/execution/__tests__/relay-env.test.ts +42 -0
- package/src/execution/__tests__/rpc-mode.test.ts +1 -1
- package/src/execution/__tests__/run-spawn-edges.test.ts +44 -1
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +39 -0
- package/src/execution/__tests__/spawn-args.test.ts +37 -26
- package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
- package/src/execution/__tests__/startup-config-declaration.test.ts +35 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +147 -6
- package/src/execution/__tests__/timeout-integration.test.ts +220 -2
- package/src/execution/__tests__/tool-action.test.ts +92 -1
- package/src/execution/agent-registry.ts +16 -0
- package/src/execution/argv-mirror.ts +5 -1
- package/src/execution/concurrency-pool.ts +1 -1
- package/src/execution/config.ts +25 -2
- package/src/execution/engine/__tests__/common/data-dir.test.ts +53 -0
- package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
- package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
- package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
- package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
- package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +342 -0
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
- package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
- package/src/execution/engine/__tests__/engines-declaration.test.ts +36 -0
- package/src/execution/engine/__tests__/model-prompt.test.ts +85 -0
- package/src/execution/engine/__tests__/paths.test.ts +39 -0
- package/src/execution/engine/__tests__/registry.test.ts +120 -0
- package/src/execution/engine/__tests__/routing.test.ts +231 -0
- package/src/execution/engine/common/data-dir.ts +62 -0
- package/src/execution/engine/common/errors.ts +183 -0
- package/src/execution/engine/common/event-journal.ts +254 -0
- package/src/execution/engine/common/journal-replay.ts +62 -0
- package/src/execution/engine/common/kill-chain.ts +221 -0
- package/src/execution/engine/common/nesting-guard.ts +50 -0
- package/src/execution/engine/common/persona-router.ts +108 -0
- package/src/execution/engine/common/pool-manager.ts +226 -0
- package/src/execution/engine/common/schema-emulation.ts +189 -0
- package/src/execution/engine/common/session-view-projection.ts +51 -0
- package/src/execution/engine/engine-discovery.ts +65 -0
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
- package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
- package/src/execution/engine/engines/pi/reader.ts +48 -0
- package/src/execution/engine/engines/pi/registration.ts +35 -0
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +64 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
- package/src/execution/engine/engines/zcode/constants.ts +43 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
- package/src/execution/engine/engines/zcode/launcher.ts +161 -0
- package/src/execution/engine/engines/zcode/parser.ts +436 -0
- package/src/execution/engine/engines/zcode/preparer.ts +363 -0
- package/src/execution/engine/engines/zcode/reader.ts +381 -0
- package/src/execution/engine/engines/zcode/registration.ts +37 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
- package/src/execution/engine/host-task-spec.ts +47 -0
- package/src/execution/engine/model-prompt.ts +59 -0
- package/src/execution/engine/paths.ts +42 -0
- package/src/execution/engine/port.ts +153 -0
- package/src/execution/engine/registry.ts +123 -0
- package/src/execution/engine/routing.ts +218 -0
- package/src/execution/engine/types.ts +309 -0
- package/src/execution/execute-options-mapper.ts +13 -8
- package/src/execution/execution-record.ts +66 -1
- package/src/execution/lifecycle-manager.ts +23 -1
- package/src/execution/model-config-service.ts +16 -1
- package/src/execution/model-resolver.ts +37 -59
- package/src/execution/notifier.ts +105 -35
- package/src/execution/notify-ledger.ts +580 -0
- package/src/execution/output-collector.ts +143 -3
- package/src/execution/pi-invocation.ts +32 -2
- package/src/execution/record-entry.ts +14 -0
- package/src/execution/record-store.ts +34 -0
- package/src/execution/relay-env.ts +37 -0
- package/src/execution/session-runner.ts +328 -71
- package/src/execution/stream-sink.ts +26 -0
- package/src/execution/subagent-service.ts +273 -13
- package/src/execution/subprocess-agent-runner.ts +210 -14
- package/src/execution/types.ts +124 -5
- package/src/execution/ui-request-queue.ts +14 -4
- package/src/index.ts +99 -1
- package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
- package/src/interface/bg-notify-render.ts +33 -12
- package/src/interface/helpers.ts +2 -2
- package/src/interface/subagent-actions.ts +29 -9
- package/src/interface/subagent-tool-schema.ts +156 -0
- package/src/interface/subagent-tool.ts +56 -119
- package/src/interface/subagents.ts +2 -2
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +19 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -6
- package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -5
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +89 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +137 -0
- package/src/orchestration/__tests__/jsonl-run-store-corrupt-entry.test.ts +150 -0
- package/src/orchestration/__tests__/jsonl-run-store-retention.test.ts +202 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +326 -3
- package/src/orchestration/__tests__/lifecycle.test.ts +41 -7
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +57 -3
- package/src/orchestration/__tests__/skill-discovery.test.ts +44 -0
- package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +43 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +22 -3
- package/src/orchestration/agent-opts-resolver.ts +104 -23
- package/src/orchestration/error-recovery.ts +189 -33
- package/src/orchestration/execute-agent-call.ts +39 -0
- package/src/orchestration/jsonl-run-store.ts +121 -7
- package/src/orchestration/launcher.ts +60 -15
- package/src/orchestration/lifecycle.ts +10 -7
- package/src/orchestration/models/__tests__/budget.test.ts +1 -61
- package/src/orchestration/models/budget.ts +5 -35
- package/src/orchestration/models/run-runtime.ts +24 -9
- package/src/orchestration/models/types.ts +16 -0
- package/src/orchestration/script-lint.ts +1 -1
- package/src/orchestration/skill-discovery.ts +31 -8
- package/src/orchestration/worker-script-builder.ts +19 -3
- package/src/shared/__tests__/model-ref.test.ts +306 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +1 -1
- package/src/shared/__tests__/timer-delay.test.ts +61 -0
- package/src/shared/meta-parser.ts +5 -1
- package/src/shared/model-ref.ts +286 -0
- package/src/shared/resource-meta.ts +5 -0
- package/src/shared/schema-env.ts +44 -0
- package/src/shared/schema-jsonify.ts +6 -4
- package/src/shared/timer-delay.ts +54 -0
- package/workflows/review-fix-loop-utils.cjs +9 -7
- package/workflows/review-fix-loop.js +20 -12
- package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
- package/src/orchestration/concurrency-gate.ts +0 -69
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [F-R2] stdout data 同步回调链内 fail-fast throw 不逃逸(不升级为 uncaughtException 崩宿主)。
|
|
4
|
+
//
|
|
5
|
+
// 背景:session-runner 的两处 timer 安全校验调用位于 child.stdout.on("data") 同步回调链内:
|
|
6
|
+
// ① agent_end keep-alive 分支的 resolveSpawnWatchdogMs(→ assertSafeTimerDelay fail-fast)
|
|
7
|
+
// ② chatMode agent_settled 分支的 armIdleTimer(→ assertSafeTimerDelay fail-fast)
|
|
8
|
+
// 旧实现任一处 throw 都会逃出事件回调 = Node uncaughtException 崩宿主进程。
|
|
9
|
+
// 修复:调用点包 try/catch 降级(不挂 timer),错误经 bestEffort("error") 可见。
|
|
10
|
+
//
|
|
11
|
+
// mock 结构与 run-spawn-chatmode-settled.test.ts 一致(FakeChild + lifecycle-manager 真实模块)。
|
|
12
|
+
// logger 整体 mock(loggerMock spy)断言错误可见——「fail-fast 语义保留(错误可见、行为明确)
|
|
13
|
+
// 但不升级为进程崩溃」。
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
|
|
17
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
18
|
+
|
|
19
|
+
// logger mock:bestEffort / session-runner 共享同一 loggerMock(断言错误可见性)。
|
|
20
|
+
// vi.hoisted:vi.mock 工厂被提升到顶层 const 之前执行,loggerMock 必须经 vi.hoisted 创建。
|
|
21
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
22
|
+
loggerMock: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
23
|
+
}));
|
|
24
|
+
vi.mock("@zhushanwen/pi-extension-logger", () => ({ getLogger: () => loggerMock }));
|
|
25
|
+
|
|
26
|
+
vi.mock("node:child_process", async () => {
|
|
27
|
+
const { FakeChild } = await import("./helpers/spawn-mock.ts");
|
|
28
|
+
return {
|
|
29
|
+
spawn: vi.fn(() => new FakeChild()),
|
|
30
|
+
// buildEnvBlock 的 git branch 调用(execFile 异步):默认 err-first 兜底 → catch → branch=""
|
|
31
|
+
execFile: vi.fn(
|
|
32
|
+
(
|
|
33
|
+
_cmd: string,
|
|
34
|
+
_args: readonly string[],
|
|
35
|
+
_opts: unknown,
|
|
36
|
+
cb: (err: Error | null, stdout?: string, stderr?: string) => void,
|
|
37
|
+
) => cb(new Error("execFile not configured in this test")),
|
|
38
|
+
),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
vi.mock("node:fs", async () => {
|
|
43
|
+
const actual = await import("node:fs");
|
|
44
|
+
return {
|
|
45
|
+
default: {
|
|
46
|
+
...actual,
|
|
47
|
+
mkdirSync: vi.fn(),
|
|
48
|
+
existsSync: vi.fn(() => false),
|
|
49
|
+
appendFileSync: vi.fn(),
|
|
50
|
+
writeFileSync: vi.fn(),
|
|
51
|
+
readdirSync: vi.fn(() => []),
|
|
52
|
+
},
|
|
53
|
+
mkdirSync: vi.fn(),
|
|
54
|
+
existsSync: vi.fn(() => false),
|
|
55
|
+
appendFileSync: vi.fn(),
|
|
56
|
+
writeFileSync: vi.fn(),
|
|
57
|
+
readdirSync: vi.fn(() => []),
|
|
58
|
+
promises: actual.promises,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
vi.mock("../alive-store.ts", () => ({
|
|
63
|
+
writeAliveMarker: vi.fn(),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// 场景 ① 依赖 keep-alive 分支命中:count=1(有活跃后代 → 不 kill → 重挂 watchdog)
|
|
67
|
+
vi.mock("../session-pending.ts", () => ({
|
|
68
|
+
readActivePendingFromSessionFile: vi.fn(() => ({ count: 1 })),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
vi.mock("../temp-prompt.ts", () => ({
|
|
72
|
+
writePromptToTempFile: vi.fn(async (agent: string) => {
|
|
73
|
+
const safeName = agent.replace(/[^\w.-]+/g, "_");
|
|
74
|
+
return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
|
|
75
|
+
}),
|
|
76
|
+
cleanupTempPrompt: vi.fn(async () => {}),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
import { runSpawn, SPAWN_WATCHDOG_ENV } from "../session-runner.ts";
|
|
80
|
+
import type { SessionRunnerContext } from "../session-runner.ts";
|
|
81
|
+
import { hasIdleTimer, _resetLifecycleState } from "../lifecycle-manager.ts";
|
|
82
|
+
import { createRecord } from "../execution-record.ts";
|
|
83
|
+
import type { ExecutionRecord } from "../types.ts";
|
|
84
|
+
import {
|
|
85
|
+
emitStdoutLine,
|
|
86
|
+
type FakeChild,
|
|
87
|
+
lastSpawnedChild as lastSpawnedChildOf,
|
|
88
|
+
makeCtx,
|
|
89
|
+
makeOpts,
|
|
90
|
+
makeRecord,
|
|
91
|
+
sessionHeader,
|
|
92
|
+
waitForSpawn as waitForSpawnOf,
|
|
93
|
+
} from "./helpers/spawn-mock.ts";
|
|
94
|
+
|
|
95
|
+
const mockSpawn = vi.mocked(spawn);
|
|
96
|
+
|
|
97
|
+
const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
|
|
98
|
+
const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
|
|
99
|
+
|
|
100
|
+
/** 构造 chatMode record(idleTimeoutMs 设为超出 setTimeout 安全域的值 → assertSafeTimerDelay throw)。 */
|
|
101
|
+
function makeChatModeRecord(id = "sa-f2-idle"): ExecutionRecord {
|
|
102
|
+
return createRecord(id, {
|
|
103
|
+
agent: "general-purpose",
|
|
104
|
+
model: "test-model",
|
|
105
|
+
mode: "sync",
|
|
106
|
+
task: "chat task",
|
|
107
|
+
slug: "chat",
|
|
108
|
+
startedAt: 1_000_000,
|
|
109
|
+
rootSessionId: "root-session",
|
|
110
|
+
parentRecordId: undefined,
|
|
111
|
+
depth: 0,
|
|
112
|
+
chatMode: true,
|
|
113
|
+
// > 2^31-1:Node setTimeout 溢出域,assertSafeTimerDelay fail-fast
|
|
114
|
+
idleTimeoutMs: Number.MAX_SAFE_INTEGER,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe("[F-R2] stdout 回调链内 fail-fast throw 不逃逸", () => {
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
vi.clearAllMocks();
|
|
121
|
+
_resetLifecycleState();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
vi.restoreAllMocks();
|
|
126
|
+
_resetLifecycleState();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("① agent_end keep-alive 分支 resolveSpawnWatchdogMs throw → 降级不 re-arm,run 正常收尾,error 可见", async () => {
|
|
130
|
+
const record = makeRecord();
|
|
131
|
+
const promise = runSpawn(record, "Task: keepalive", makeOpts(), makeCtx());
|
|
132
|
+
|
|
133
|
+
await waitForSpawn();
|
|
134
|
+
const child = lastSpawnedChild();
|
|
135
|
+
|
|
136
|
+
emitStdoutLine(child, sessionHeader("sess-fr2a"));
|
|
137
|
+
// 初始 watchdog 解析在 spawn 后同步块内已完成(env 未设 → 不挂)。
|
|
138
|
+
// 此刻设非法 env:agent_end keep-alive 分支再读 env → assertSafeTimerDelay throw。
|
|
139
|
+
process.env[SPAWN_WATCHDOG_ENV] = String(Number.MAX_SAFE_INTEGER);
|
|
140
|
+
try {
|
|
141
|
+
emitStdoutLine(child, { type: "agent_end", willRetry: false });
|
|
142
|
+
// 给 stream flush 留一个 tick(对齐 chatmode 测试模式):throw 若逃逸回调,
|
|
143
|
+
// vitest 会以 unhandled error 判本测试失败——能走到断言即「未崩宿主」。
|
|
144
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
145
|
+
|
|
146
|
+
// 降级语义:不 re-arm(无 timer 可直接观察的是——子进程未被 kill、run 未被中断)
|
|
147
|
+
expect(child.killed).toBe(false);
|
|
148
|
+
|
|
149
|
+
// 错误可见(fail-fast 语义保留):bestEffort("error") 经 logger.error 落日志
|
|
150
|
+
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
151
|
+
expect.stringContaining("resolveSpawnWatchdogMs"),
|
|
152
|
+
expect.objectContaining({ detail: expect.stringContaining("exceeds the Node setTimeout limit") }),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// 收尾:run 正常完成(未被回调异常打断)
|
|
156
|
+
child.stdout.end();
|
|
157
|
+
child.stderr.end();
|
|
158
|
+
child.emit("close", 0);
|
|
159
|
+
const result = await promise;
|
|
160
|
+
expect(result.success).toBe(true);
|
|
161
|
+
} finally {
|
|
162
|
+
delete process.env[SPAWN_WATCHDOG_ENV];
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("② chatMode agent_settled armIdleTimer throw → 降级不挂 idle timer,onRoundSettled 照常、提前 resolve", async () => {
|
|
167
|
+
const record = makeChatModeRecord("sa-f2-idle");
|
|
168
|
+
const onRoundSettled = vi.fn();
|
|
169
|
+
const ctx: Partial<SessionRunnerContext> = { onRoundSettled };
|
|
170
|
+
const promise = runSpawn(record, "Task: idle", makeOpts(), makeCtx(ctx as SessionRunnerContext));
|
|
171
|
+
|
|
172
|
+
await waitForSpawn();
|
|
173
|
+
const child = lastSpawnedChild();
|
|
174
|
+
|
|
175
|
+
emitStdoutLine(child, sessionHeader("sess-fr2b"));
|
|
176
|
+
emitStdoutLine(child, { type: "agent_settled" });
|
|
177
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
178
|
+
|
|
179
|
+
// 降级语义:idle timer 未挂(throw 发生在 arm 入口校验,先于 setTimeout)
|
|
180
|
+
expect(hasIdleTimer(record.id)).toBe(false);
|
|
181
|
+
expect(child.killed).toBe(false);
|
|
182
|
+
|
|
183
|
+
// catch 后续语句照常执行:本轮完成通知不因 GC timer 故障丢失
|
|
184
|
+
expect(onRoundSettled).toHaveBeenCalledTimes(1);
|
|
185
|
+
|
|
186
|
+
// 错误可见
|
|
187
|
+
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
188
|
+
expect.stringContaining("armIdleTimer"),
|
|
189
|
+
expect.objectContaining({ detail: expect.stringContaining("exceeds the Node setTimeout limit") }),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// 收尾:chatMode 首轮 agent_settled 已 resolveRun(0),close 后 runSpawn 正常返回
|
|
193
|
+
child.stdout.end();
|
|
194
|
+
child.stderr.end();
|
|
195
|
+
child.emit("close", 0);
|
|
196
|
+
const result = await promise;
|
|
197
|
+
expect(result.success).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -94,8 +94,10 @@ import {
|
|
|
94
94
|
applySchemaEnvToChildEnv,
|
|
95
95
|
type RunOptions,
|
|
96
96
|
runSpawn,
|
|
97
|
+
SCHEMA_ENV_MAX_BYTES,
|
|
97
98
|
type SessionRunnerContext,
|
|
98
99
|
} from "../session-runner.ts";
|
|
100
|
+
import { schemaEnvByteLength } from "../../shared/schema-env.ts";
|
|
99
101
|
|
|
100
102
|
const mockSpawn = vi.mocked(spawn);
|
|
101
103
|
const mockExistsSync = vi.mocked(fs.existsSync);
|
|
@@ -225,6 +227,43 @@ describe("applySchemaEnvToChildEnv (T3.9/T3.11/T3.16)", () => {
|
|
|
225
227
|
expect(childEnv.HOME).toBe("/home/user");
|
|
226
228
|
expect(childEnv.PI_WORKFLOW_SCHEMA).toBe('{"x":1}');
|
|
227
229
|
});
|
|
230
|
+
|
|
231
|
+
// [SO-DATA-4] 边界内通过:255KB(< 256KiB 上限)正常注入,行为不变
|
|
232
|
+
it("[SO-DATA-4] 255KB schema 正常注入(上限内不误拒)", () => {
|
|
233
|
+
const childEnv: Record<string, string | undefined> = {};
|
|
234
|
+
// ASCII 串 byteLength === length,构造 255KB 纯 JSON body(外层再包一层合法 JSON)
|
|
235
|
+
const padding = "x".repeat(255 * 1024);
|
|
236
|
+
const schemaJson = JSON.stringify({ type: "object", properties: { pad: { const: padding } } });
|
|
237
|
+
expect(schemaEnvByteLength(schemaJson)).toBeLessThanOrEqual(SCHEMA_ENV_MAX_BYTES);
|
|
238
|
+
applySchemaEnvToChildEnv(childEnv, schemaJson);
|
|
239
|
+
expect(childEnv.PI_WORKFLOW_SCHEMA).toBe(schemaJson);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// [SO-DATA-4] 超限 fail-fast:257KB 拒绝,错误消息含实际大小 + 精简/拆分指引 + E2BIG 归因
|
|
243
|
+
it("[SO-DATA-4] 257KB schema fail-fast 拒绝(错误含实际大小与恢复指引)", () => {
|
|
244
|
+
const childEnv: Record<string, string | undefined> = {};
|
|
245
|
+
const padding = "x".repeat(257 * 1024);
|
|
246
|
+
const schemaJson = JSON.stringify({ type: "object", properties: { pad: { const: padding } } });
|
|
247
|
+
expect(schemaEnvByteLength(schemaJson)).toBeGreaterThan(SCHEMA_ENV_MAX_BYTES);
|
|
248
|
+
expect(() => applySchemaEnvToChildEnv(childEnv, schemaJson)).toThrow(
|
|
249
|
+
expect.objectContaining({
|
|
250
|
+
message: expect.stringContaining("bytes exceeds"),
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
// 错误消息含实际大小、上限、精简/拆分指引与 E2BIG 归因(可操作性验收)
|
|
254
|
+
try {
|
|
255
|
+
applySchemaEnvToChildEnv(childEnv, schemaJson);
|
|
256
|
+
throw new Error("expected applySchemaEnvToChildEnv to throw");
|
|
257
|
+
} catch (err) {
|
|
258
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
259
|
+
expect(msg).toContain(String(schemaEnvByteLength(schemaJson))); // 实际大小
|
|
260
|
+
expect(msg).toContain(String(SCHEMA_ENV_MAX_BYTES)); // 上限值
|
|
261
|
+
expect(msg).toContain("simplify the schema"); // 精简指引
|
|
262
|
+
expect(msg).toContain("E2BIG"); // ARG_MAX/E2BIG 约束说明
|
|
263
|
+
}
|
|
264
|
+
// fail-fast:不写入半截值
|
|
265
|
+
expect(childEnv.PI_WORKFLOW_SCHEMA).toBeUndefined();
|
|
266
|
+
});
|
|
228
267
|
});
|
|
229
268
|
|
|
230
269
|
// ── runSpawn 集成测试:schemaEnv 经 RunOptions → childEnv ──
|
|
@@ -11,8 +11,9 @@ import { MAX_FORK_DEPTH } from "../session-context-resolver.ts";
|
|
|
11
11
|
import { buildEnvBlock, buildSpawnArgs } from "../session-runner.ts";
|
|
12
12
|
|
|
13
13
|
describe("buildSpawnArgs", () => {
|
|
14
|
+
// [U1 D2] modelRef 为必填(spawn 前置守卫:未经裁决的裸字符串类型层面不可达)。
|
|
14
15
|
const baseParams = {
|
|
15
|
-
|
|
16
|
+
modelRef: { provider: "openai", id: "gpt-4o" },
|
|
16
17
|
thinkingLevel: undefined as string | undefined,
|
|
17
18
|
agentTools: undefined as string[] | undefined,
|
|
18
19
|
appendSystemPromptPath: undefined as string | undefined,
|
|
@@ -21,9 +22,11 @@ describe("buildSpawnArgs", () => {
|
|
|
21
22
|
skillPaths: undefined as string[] | undefined,
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
it("基础参数:--mode rpc --session-dir,不含 -p 也不含 task(task 经 stdin 传)", () => {
|
|
25
|
+
it("基础参数:--mode rpc --session-dir + --model provider/id,不含 -p 也不含 task(task 经 stdin 传)", () => {
|
|
25
26
|
const args = buildSpawnArgs(baseParams);
|
|
26
|
-
expect(args).toEqual([
|
|
27
|
+
expect(args).toEqual([
|
|
28
|
+
"--mode", "rpc", "--session-dir", "/sessions/dir", "--model", "openai/gpt-4o",
|
|
29
|
+
]);
|
|
27
30
|
});
|
|
28
31
|
|
|
29
32
|
it("不含 -p / --print(rpc mode 下 -p 被 resolveAppMode 无视,是死代码)", () => {
|
|
@@ -32,28 +35,22 @@ describe("buildSpawnArgs", () => {
|
|
|
32
35
|
expect(args).not.toContain("--print");
|
|
33
36
|
});
|
|
34
37
|
|
|
35
|
-
it("
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
it("modelRef → --model 值恒为 `${provider}/${id}`(全等拼接,不重写输入)", () => {
|
|
39
|
+
// 含大小写差异的 provider/id 原样拼接(大小写敏感规则:放行即全等回显)
|
|
40
|
+
const args = buildSpawnArgs({
|
|
41
|
+
...baseParams,
|
|
42
|
+
modelRef: { provider: "zai-coding-cn", id: "GLM-5.3-Flash" },
|
|
43
|
+
});
|
|
40
44
|
const idx = args.indexOf("--model");
|
|
41
|
-
expect(args[idx + 1]).toBe("
|
|
45
|
+
expect(args[idx + 1]).toBe("zai-coding-cn/GLM-5.3-Flash");
|
|
42
46
|
});
|
|
43
47
|
|
|
44
|
-
it("
|
|
48
|
+
it("modelRef + thinkingLevel → model 后缀 :level", () => {
|
|
45
49
|
const args = buildSpawnArgs(
|
|
46
|
-
{ ...baseParams,
|
|
50
|
+
{ ...baseParams, thinkingLevel: "high" },
|
|
47
51
|
);
|
|
48
52
|
const idx = args.indexOf("--model");
|
|
49
|
-
expect(args[idx + 1]).toBe("
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("thinkingLevel 无 model → 不追加(thinking 依赖 model 后缀)", () => {
|
|
53
|
-
const args = buildSpawnArgs(
|
|
54
|
-
{ ...baseParams, model: undefined, thinkingLevel: "high" },
|
|
55
|
-
);
|
|
56
|
-
expect(args).not.toContain("--model");
|
|
53
|
+
expect(args[idx + 1]).toBe("openai/gpt-4o:high");
|
|
57
54
|
});
|
|
58
55
|
|
|
59
56
|
it("agentTools → --tools 逗号分隔", () => {
|
|
@@ -109,7 +106,7 @@ describe("buildSpawnArgs", () => {
|
|
|
109
106
|
it("全参数组合:所有 flag 存在,不含 -p 也不含 positional task", () => {
|
|
110
107
|
const args = buildSpawnArgs(
|
|
111
108
|
{
|
|
112
|
-
|
|
109
|
+
...baseParams,
|
|
113
110
|
thinkingLevel: "low",
|
|
114
111
|
agentTools: ["read"],
|
|
115
112
|
appendSystemPromptPath: "/tmp/p.md",
|
|
@@ -179,12 +176,12 @@ describe("buildSpawnArgs", () => {
|
|
|
179
176
|
expect(args).not.toContain("--extension");
|
|
180
177
|
expect(args).not.toContain("--no-context-files");
|
|
181
178
|
// 仅基础参数
|
|
182
|
-
expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir"]);
|
|
179
|
+
expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir", "--model", "openai/gpt-4o"]);
|
|
183
180
|
});
|
|
184
181
|
|
|
185
182
|
it("mirrorFlags undefined → 行为等同旧版(TC7)", () => {
|
|
186
183
|
const args = buildSpawnArgs(baseParams);
|
|
187
|
-
expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir"]);
|
|
184
|
+
expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir", "--model", "openai/gpt-4o"]);
|
|
188
185
|
expect(args).not.toContain("--extension");
|
|
189
186
|
expect(args).not.toContain("--no-extensions");
|
|
190
187
|
expect(args).not.toContain("--approve");
|
|
@@ -234,14 +231,13 @@ describe("buildSpawnArgs", () => {
|
|
|
234
231
|
it("sessionFile undefined → 不含 --session(向后兼容)", () => {
|
|
235
232
|
const args = buildSpawnArgs(baseParams);
|
|
236
233
|
expect(args).not.toContain("--session");
|
|
237
|
-
expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir"]);
|
|
234
|
+
expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir", "--model", "openai/gpt-4o"]);
|
|
238
235
|
});
|
|
239
236
|
|
|
240
|
-
it("sessionFile +
|
|
237
|
+
it("sessionFile + modelRef + thinkingLevel → 三者都进 args(resume 全参数)", () => {
|
|
241
238
|
const args = buildSpawnArgs({
|
|
242
239
|
...baseParams,
|
|
243
240
|
sessionFile: "/sessions/sub/resume.jsonl",
|
|
244
|
-
model: "anthropic/claude",
|
|
245
241
|
thinkingLevel: "high",
|
|
246
242
|
});
|
|
247
243
|
// --session <file>
|
|
@@ -251,7 +247,7 @@ describe("buildSpawnArgs", () => {
|
|
|
251
247
|
// --model provider/id:level(thinkingLevel 作 model 后缀)
|
|
252
248
|
const modelIdx = args.indexOf("--model");
|
|
253
249
|
expect(modelIdx).toBeGreaterThan(-1);
|
|
254
|
-
expect(args[modelIdx + 1]).toBe("
|
|
250
|
+
expect(args[modelIdx + 1]).toBe("openai/gpt-4o:high");
|
|
255
251
|
});
|
|
256
252
|
});
|
|
257
253
|
|
|
@@ -332,6 +328,21 @@ describe("mirrorMainProcessFlags", () => {
|
|
|
332
328
|
const r = mirrorMainProcessFlags(["bun", "/pi", "-e", "/a", "-e=/b", "-e", "/c"]);
|
|
333
329
|
expect(r.extensionPaths).toEqual(["/a", "/b", "/c"]);
|
|
334
330
|
});
|
|
331
|
+
|
|
332
|
+
// [MF-7a] flag 判定收窄为 startsWith("--"):单 - 开头的合法路径不被误判为 flag。
|
|
333
|
+
it("[MF-7a] - 开头的路径被当值镜像(不误判 flag)", () => {
|
|
334
|
+
const r = mirrorMainProcessFlags(["bun", "/pi", "--extension", "-weird-dir/ext.js"]);
|
|
335
|
+
expect(r.extensionPaths).toEqual(["-weird-dir/ext.js"]);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// [MF-7a] -- 开头的真 flag 仍正确跳过:--extension 后跟 --flag 时不吃值、不越界。
|
|
339
|
+
it("[MF-7a] --extension 后跟 -- 开头真 flag → 不吃值", () => {
|
|
340
|
+
const r = mirrorMainProcessFlags([
|
|
341
|
+
"bun", "/pi", "--extension", "--approve", "--extension", "/real.js",
|
|
342
|
+
]);
|
|
343
|
+
expect(r.extensionPaths).toEqual(["/real.js"]);
|
|
344
|
+
expect(r.approve).toBe(true);
|
|
345
|
+
});
|
|
335
346
|
});
|
|
336
347
|
|
|
337
348
|
// ============================================================
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/execution/__tests__/start-sync-model-guard.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [U1 验收 3] start 同步期裁决:非全等 model 入参在 start 工具调用同步期 isError——
|
|
4
|
+
// 无 spawn、无子 session、record 不落盘。
|
|
5
|
+
//
|
|
6
|
+
// 用真实 SubagentService(tmpdir + mock pi)走 execute 全链路,断言:
|
|
7
|
+
// 1. execute 在 IDENTITY 解析(resolveModel 裁决)阶段 reject(问句式报错);
|
|
8
|
+
// 2. buildSpawnArgs 从未被调用(vi.mock spy 包装真实实现)——spawn 前置守卫的结构性证据;
|
|
9
|
+
// 3. node:child_process.spawn 从未被调用——无子进程;
|
|
10
|
+
// 4. pi.appendEntry 从未被调用——record 未创建(emitPendingRegister 未发生)。
|
|
11
|
+
//
|
|
12
|
+
// 放行路径(全等 → 裁决通过)由 model-ref.test.ts / model-resolver.test.ts 锁定,
|
|
13
|
+
// 全链路放行的 spawn 行为由 run-spawn-* 契约测试覆盖。
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as os from "node:os";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
21
|
+
|
|
22
|
+
vi.mock("node:child_process", async () => {
|
|
23
|
+
const actual = await import("node:child_process");
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
spawn: vi.fn(() => {
|
|
27
|
+
throw new Error("spawn must not be called on sync-model rejection");
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// buildSpawnArgs 包装为 spy(保留真实实现)——「非全等拒单不触达 spawn 参数组装」的承重断言。
|
|
33
|
+
vi.mock("../session-runner.ts", async (importOriginal) => {
|
|
34
|
+
const actual = await importOriginal<typeof import("../session-runner.ts")>();
|
|
35
|
+
return { ...actual, buildSpawnArgs: vi.fn(actual.buildSpawnArgs) };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
import { ModelConfigService } from "../model-config-service.ts";
|
|
39
|
+
import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
|
|
40
|
+
import { buildSpawnArgs } from "../session-runner.ts";
|
|
41
|
+
import type { PiLike } from "../subagent-service.ts";
|
|
42
|
+
import { SubagentService } from "../subagent-service.ts";
|
|
43
|
+
|
|
44
|
+
const mockSpawn = vi.mocked(spawn);
|
|
45
|
+
const mockBuildSpawnArgs = vi.mocked(buildSpawnArgs);
|
|
46
|
+
|
|
47
|
+
// ── 工具 ──
|
|
48
|
+
|
|
49
|
+
function makeModel(over: Partial<ModelInfo> = {}): ModelInfo {
|
|
50
|
+
return {
|
|
51
|
+
id: over.id ?? "GLM-5.3-Flash",
|
|
52
|
+
name: over.name ?? "GLM 5.3 Flash",
|
|
53
|
+
provider: over.provider ?? "zai-coding-cn",
|
|
54
|
+
reasoning: over.reasoning ?? false,
|
|
55
|
+
...over,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** registry 快照:真实形态(大小写混合 id),无孪生(P-A2 放行快照形态)。 */
|
|
60
|
+
function makeRegistry(models: ModelInfo[]): ModelRegistryLike {
|
|
61
|
+
return {
|
|
62
|
+
getAvailable: () => models,
|
|
63
|
+
find: (provider, modelId) => models.find((m) => m.provider === provider && m.id === modelId),
|
|
64
|
+
hasConfiguredAuth: () => true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function makePi(): PiLike {
|
|
69
|
+
return {
|
|
70
|
+
appendEntry: vi.fn(),
|
|
71
|
+
events: { emit: vi.fn() },
|
|
72
|
+
sendMessage: vi.fn(),
|
|
73
|
+
} as unknown as PiLike;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("start 同步期裁决(U1 验收 3:非全等 → isError 且无 spawn 无子 session)", () => {
|
|
77
|
+
let tmpDir: string;
|
|
78
|
+
let service: SubagentService;
|
|
79
|
+
let pi: PiLike;
|
|
80
|
+
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
vi.clearAllMocks();
|
|
83
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sw-start-guard-"));
|
|
84
|
+
const modelService = new ModelConfigService({ agentDir: tmpDir, cwd: tmpDir });
|
|
85
|
+
modelService.initModel({
|
|
86
|
+
modelRegistry: makeRegistry([makeModel()]),
|
|
87
|
+
sessionId: "guard-session",
|
|
88
|
+
});
|
|
89
|
+
service = new SubagentService({ cwd: tmpDir, modelService });
|
|
90
|
+
pi = makePi();
|
|
91
|
+
service.initSession({ pi, sessionId: "guard-session" });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
vi.restoreAllMocks();
|
|
96
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("小写变体入参 → execute 同步 reject(问句式报错),buildSpawnArgs/spawn/appendEntry 均未被调", async () => {
|
|
100
|
+
await expect(
|
|
101
|
+
service.execute({
|
|
102
|
+
task: "probe task",
|
|
103
|
+
slug: "guard",
|
|
104
|
+
model: "zai-coding-cn/glm-5.3-flash",
|
|
105
|
+
}),
|
|
106
|
+
).rejects.toThrow(/is not a registry entry.*Did you mean one of these\?/s);
|
|
107
|
+
|
|
108
|
+
// spawn 链路零触达:参数组装(buildSpawnArgs)与进程创建(spawn)都未发生
|
|
109
|
+
expect(mockBuildSpawnArgs).not.toHaveBeenCalled();
|
|
110
|
+
expect(mockSpawn).not.toHaveBeenCalled();
|
|
111
|
+
// record 未创建(emitPendingRegister 未发生)→ 无子 session 前置状态
|
|
112
|
+
expect(pi.appendEntry).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("错误含 case variant 首位建议 + 继承指引(可直接重发的合法串)", async () => {
|
|
116
|
+
let msg = "";
|
|
117
|
+
try {
|
|
118
|
+
await service.execute({
|
|
119
|
+
task: "probe task",
|
|
120
|
+
slug: "guard",
|
|
121
|
+
model: "zai-coding-cn/glm-5.3-flash",
|
|
122
|
+
});
|
|
123
|
+
} catch (e) {
|
|
124
|
+
msg = (e as Error).message;
|
|
125
|
+
}
|
|
126
|
+
expect(msg).toContain('zai-coding-cn/GLM-5.3-Flash ← case variant of "glm-5.3-flash"');
|
|
127
|
+
expect(msg).toMatch(/Or omit the `model` param to inherit the main agent model\./);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("全等入参通过裁决(reject 不发生,放行链路触达 spawn 参数组装)——守卫零误伤对照", async () => {
|
|
131
|
+
// 放行路径会进入 kickOffBackground(detached);execute resolve 即视为裁决放行。
|
|
132
|
+
// spawn 本身由 FakeChild 缺失抛错收口——本断言只关心「越过了同步裁决」:
|
|
133
|
+
// execute 不 reject,且 buildSpawnArgs 已被调(或即将被调的 record 已创建)。
|
|
134
|
+
const promise = service.execute({
|
|
135
|
+
task: "probe task",
|
|
136
|
+
slug: "guard-exact",
|
|
137
|
+
model: "zai-coding-cn/GLM-5.3-Flash",
|
|
138
|
+
});
|
|
139
|
+
// 放行路径:裁决不抛(execute 最终可能因 mock 环境子进程收尾而 reject,
|
|
140
|
+
// 但错误不能是模型裁决错误)
|
|
141
|
+
try {
|
|
142
|
+
await promise;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
expect((e as Error).message).not.toMatch(/is not a registry entry/);
|
|
145
|
+
expect((e as Error).message).not.toMatch(/ambiguous case variants/);
|
|
146
|
+
}
|
|
147
|
+
// 全等放行 → record 已创建(emitPendingRegister 发生)——与拒单路径形成对照
|
|
148
|
+
expect(pi.appendEntry).toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/execution/__tests__/startup-config-declaration.test.ts
|
|
2
|
+
//
|
|
3
|
+
// startupConfig 声明守护测试:断言 package.json `xyz-agent.startupConfig` 声明的
|
|
4
|
+
// content 与代码 DEFAULT_CONFIG 常量深相等,防止两侧任一改动未同步(漂移)。
|
|
5
|
+
// 机制:runtime 启动序列统一 ensure(extension-startup-config.ts),声明即首建内容。
|
|
6
|
+
//
|
|
7
|
+
// 运行:cd extensions/universal/subagent-workflow && npx vitest run src/execution/__tests__/startup-config-declaration.test.ts
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
|
|
15
|
+
import { DEFAULT_CONFIG } from "../config.ts";
|
|
16
|
+
|
|
17
|
+
/** package.json `xyz-agent.startupConfig` 声明条目的最小形状(守护断言用)。 */
|
|
18
|
+
interface StartupConfigEntry {
|
|
19
|
+
path: string;
|
|
20
|
+
content: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const pkg = JSON.parse(
|
|
24
|
+
readFileSync(join(fileURLToPath(import.meta.url), "../../../../package.json"), "utf-8"),
|
|
25
|
+
) as { "xyz-agent"?: { startupConfig?: StartupConfigEntry[] } };
|
|
26
|
+
|
|
27
|
+
describe("startupConfig 声明守护", () => {
|
|
28
|
+
it("声明 content 与代码 DEFAULT_CONFIG 深相等", () => {
|
|
29
|
+
const entry = pkg["xyz-agent"]?.startupConfig?.find(
|
|
30
|
+
(e: StartupConfigEntry) => e.path === "subagents/config.json",
|
|
31
|
+
);
|
|
32
|
+
expect(entry).toBeDefined();
|
|
33
|
+
expect(entry?.content).toEqual(DEFAULT_CONFIG);
|
|
34
|
+
});
|
|
35
|
+
});
|