@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,306 @@
|
|
|
1
|
+
// src/shared/__tests__/model-ref.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [U1 ModelRef 全等裁决] assertCanonicalModelRef / modelRefFromVerified 单测族。
|
|
4
|
+
// 设计权威:docs/design/subagent-dispatch-reliability.md §3.3 D1(规则①~⑤含孪生守卫)、D2(继承路径豁免口径)。
|
|
5
|
+
//
|
|
6
|
+
// P-A2(实施期门):孪生守卫行为验证——含孪生的 registry 快照实测拒单路径 +
|
|
7
|
+
// 无孪生快照实测放行路径。「有孪生仍放行」= 守卫失效,阻断合入。
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type ModelRefSource,
|
|
13
|
+
assertCanonicalModelRef,
|
|
14
|
+
assertThinkingLevel,
|
|
15
|
+
modelRefFromVerified,
|
|
16
|
+
stripThinkingSuffix,
|
|
17
|
+
THINKING_ORDER,
|
|
18
|
+
} from "../model-ref.ts";
|
|
19
|
+
|
|
20
|
+
// ============================================================
|
|
21
|
+
// helpers
|
|
22
|
+
// ============================================================
|
|
23
|
+
|
|
24
|
+
/** registry 快照构造(getAvailable 顺序即判定顺序)。 */
|
|
25
|
+
function makeSource(entries: ReadonlyArray<{ provider: string; id: string }>): ModelRefSource {
|
|
26
|
+
return { getAvailable: () => entries };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 基线快照:模拟真实 registry 形态(大小写混合 id + 无关条目)。 */
|
|
30
|
+
const BASELINE: ReadonlyArray<{ provider: string; id: string }> = [
|
|
31
|
+
{ provider: "zai-coding-cn", id: "GLM-5.3-Flash" },
|
|
32
|
+
{ provider: "zai-coding-cn", id: "GLM-5.3" },
|
|
33
|
+
{ provider: "xiaomi-token-plan-cn", id: "mimo-v2.5-pro" },
|
|
34
|
+
{ provider: "deepseek", id: "deepseek-v4-pro" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// ============================================================
|
|
38
|
+
// 全等放行(规则③)+ 无孪生快照(P-A2 放行路径)
|
|
39
|
+
// ============================================================
|
|
40
|
+
|
|
41
|
+
describe("assertCanonicalModelRef — exact match passes (rule ③)", () => {
|
|
42
|
+
it("全等命中 → 返回 {provider, id},与 registry 条目逐字段全等", () => {
|
|
43
|
+
const ref = assertCanonicalModelRef("zai-coding-cn/GLM-5.3-Flash", makeSource(BASELINE));
|
|
44
|
+
expect(ref).toEqual({ provider: "zai-coding-cn", id: "GLM-5.3-Flash" });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("P-A2 放行路径:无孪生快照下全等写法放行(含大小写差异的 id 原样保留)", () => {
|
|
48
|
+
// 快照中该 id 仅一种大小写形态(GLM-5.3-Flash ≠ glm-5.3,后者是不同 id 而非孪生)
|
|
49
|
+
const ref = assertCanonicalModelRef("zai-coding-cn/GLM-5.3-Flash", makeSource(BASELINE));
|
|
50
|
+
expect(ref.id).toBe("GLM-5.3-Flash");
|
|
51
|
+
expect(ref.provider).toBe("zai-coding-cn");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("无关大小写的不同 id(GLM-5.3 vs GLM-5.3-Flash)不构成孪生,全等命中各自放行", () => {
|
|
55
|
+
const source = makeSource(BASELINE);
|
|
56
|
+
expect(assertCanonicalModelRef("zai-coding-cn/GLM-5.3", source)).toEqual({
|
|
57
|
+
provider: "zai-coding-cn",
|
|
58
|
+
id: "GLM-5.3",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ============================================================
|
|
64
|
+
// 非全等同步拒单(规则②⑤)+ case variant 首位建议
|
|
65
|
+
// ============================================================
|
|
66
|
+
|
|
67
|
+
describe("assertCanonicalModelRef — non-exact rejected synchronously (rule ⑤)", () => {
|
|
68
|
+
it("小写变体输入 → 同步抛错(不采纳、不改写),错误含原始输入", () => {
|
|
69
|
+
expect(() =>
|
|
70
|
+
assertCanonicalModelRef("zai-coding-cn/glm-5.3-flash", makeSource(BASELINE)),
|
|
71
|
+
).toThrow(/is not a registry entry/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("case variant 是首个建议且标注 ← case variant(S2 负面路径)", () => {
|
|
75
|
+
let msg = "";
|
|
76
|
+
try {
|
|
77
|
+
assertCanonicalModelRef("zai-coding-cn/glm-5.3-flash", makeSource(BASELINE), { source: "paramOverride" });
|
|
78
|
+
} catch (e) {
|
|
79
|
+
msg = (e as Error).message;
|
|
80
|
+
}
|
|
81
|
+
expect(msg).toMatch(/is not a registry entry/);
|
|
82
|
+
expect(msg).toContain('Model "zai-coding-cn/glm-5.3-flash" (paramOverride)');
|
|
83
|
+
expect(msg).toMatch(/Registry match is case-sensitive/);
|
|
84
|
+
expect(msg).toMatch(/Did you mean one of these\?/);
|
|
85
|
+
// case variant 排首位并标注
|
|
86
|
+
const didYouMeanIdx = msg.indexOf("Did you mean one of these?");
|
|
87
|
+
const variantLine = msg.indexOf("zai-coding-cn/GLM-5.3-Flash ← case variant of \"glm-5.3-flash\"");
|
|
88
|
+
expect(variantLine).toBeGreaterThan(didYouMeanIdx);
|
|
89
|
+
// 其他模糊候选在 case variant 之后
|
|
90
|
+
const otherIdx = msg.indexOf("Other models you may have meant");
|
|
91
|
+
expect(otherIdx).toBeGreaterThan(variantLine);
|
|
92
|
+
expect(msg).toContain("zai-coding-cn/GLM-5.3");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("错误末尾附「省略 model 继承主 agent」指引", () => {
|
|
96
|
+
let msg = "";
|
|
97
|
+
try {
|
|
98
|
+
assertCanonicalModelRef("zai-coding-cn/glm-5.3-flash", makeSource(BASELINE));
|
|
99
|
+
} catch (e) {
|
|
100
|
+
msg = (e as Error).message;
|
|
101
|
+
}
|
|
102
|
+
expect(msg).toMatch(/Or omit the `model` param to inherit the main agent model\./);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("provider 大小写不同(Zai-Coding-CN)→ 不命中(provider 精确匹配,规则②)", () => {
|
|
106
|
+
let msg = "";
|
|
107
|
+
try {
|
|
108
|
+
assertCanonicalModelRef("Zai-Coding-CN/GLM-5.3-Flash", makeSource(BASELINE));
|
|
109
|
+
} catch (e) {
|
|
110
|
+
msg = (e as Error).message;
|
|
111
|
+
}
|
|
112
|
+
expect(msg).toMatch(/is not a registry entry/);
|
|
113
|
+
// provider 拼写相近 → general 候选命中
|
|
114
|
+
expect(msg).toMatch(/Other models you may have meant/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("同 provider 但 id 不存在 → provider 相似度候选(同 provider 下其他条目)", () => {
|
|
118
|
+
let msg = "";
|
|
119
|
+
try {
|
|
120
|
+
assertCanonicalModelRef("zai-coding-cn/nonexistent-probe", makeSource(BASELINE), { source: "agentConfig" });
|
|
121
|
+
} catch (e) {
|
|
122
|
+
msg = (e as Error).message;
|
|
123
|
+
}
|
|
124
|
+
expect(msg).toMatch(/is not a registry entry/);
|
|
125
|
+
expect(msg).not.toMatch(/Did you mean/);
|
|
126
|
+
expect(msg).toMatch(/Other models you may have meant/);
|
|
127
|
+
expect(msg).toContain("zai-coding-cn/GLM-5.3-Flash");
|
|
128
|
+
expect(msg).toContain("(agentConfig)");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("provider 与 id 均无相似物 → 列合法串全集 + 继承指引(S2 前者路径)", () => {
|
|
132
|
+
let msg = "";
|
|
133
|
+
try {
|
|
134
|
+
assertCanonicalModelRef("ghost-provider/zzz-unknown", makeSource(BASELINE));
|
|
135
|
+
} catch (e) {
|
|
136
|
+
msg = (e as Error).message;
|
|
137
|
+
}
|
|
138
|
+
expect(msg).toMatch(/is not a registry entry/);
|
|
139
|
+
expect(msg).not.toMatch(/Did you mean/);
|
|
140
|
+
expect(msg).toMatch(/No similar models found\./);
|
|
141
|
+
expect(msg).toMatch(/Available models:/);
|
|
142
|
+
// 合法串全集(canonical 形态,可复制)
|
|
143
|
+
expect(msg).toContain("zai-coding-cn/GLM-5.3-Flash");
|
|
144
|
+
expect(msg).toContain("xiaomi-token-plan-cn/mimo-v2.5-pro");
|
|
145
|
+
expect(msg).toMatch(/Or omit the `model` param to inherit the main agent model\./);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("无 slash / 空 id → 同步拒单(结构非法)", () => {
|
|
149
|
+
const source = makeSource(BASELINE);
|
|
150
|
+
expect(() => assertCanonicalModelRef("no-slash", source)).toThrow(/is not a registry entry/);
|
|
151
|
+
expect(() => assertCanonicalModelRef("only-provider/", source)).toThrow(/is not a registry entry/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("空 registry → 明确提示 + 继承指引", () => {
|
|
155
|
+
let msg = "";
|
|
156
|
+
try {
|
|
157
|
+
assertCanonicalModelRef("p/m", makeSource([]));
|
|
158
|
+
} catch (e) {
|
|
159
|
+
msg = (e as Error).message;
|
|
160
|
+
}
|
|
161
|
+
expect(msg).toMatch(/Registry has no available models\./);
|
|
162
|
+
expect(msg).toMatch(/Or omit the `model` param/);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ============================================================
|
|
167
|
+
// strip thinking 后缀(规则①)
|
|
168
|
+
// ============================================================
|
|
169
|
+
|
|
170
|
+
describe("assertCanonicalModelRef — strips legal thinking suffix (rule ①)", () => {
|
|
171
|
+
it('":xhigh" 后缀剥离后全等命中', () => {
|
|
172
|
+
const ref = assertCanonicalModelRef("deepseek/deepseek-v4-pro:xhigh", makeSource(BASELINE));
|
|
173
|
+
expect(ref).toEqual({ provider: "deepseek", id: "deepseek-v4-pro" });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('":max" 后缀剥离后全等命中(THINKING_ORDER 含 max)', () => {
|
|
177
|
+
const ref = assertCanonicalModelRef("zai-coding-cn/GLM-5.3-Flash:max", makeSource(BASELINE));
|
|
178
|
+
expect(ref.id).toBe("GLM-5.3-Flash");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('非白名单后缀(":foo")不剥离 → 未命中拒单', () => {
|
|
182
|
+
expect(() =>
|
|
183
|
+
assertCanonicalModelRef("zai-coding-cn/GLM-5.3-Flash:foo", makeSource(BASELINE)),
|
|
184
|
+
).toThrow(/is not a registry entry/);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ============================================================
|
|
189
|
+
// 孪生守卫(规则④)—— P-A2 拒单路径
|
|
190
|
+
// ============================================================
|
|
191
|
+
|
|
192
|
+
describe("assertCanonicalModelRef — twin guard (rule ④, P-A2)", () => {
|
|
193
|
+
/** 含大小写孪生的 registry 快照(模拟 models-store 刷新后 GLM-5.3-Flash 与 glm-5.3-flash 并存)。 */
|
|
194
|
+
const TWIN_SNAPSHOT: ReadonlyArray<{ provider: string; id: string }> = [
|
|
195
|
+
{ provider: "zai-coding-cn", id: "GLM-5.3-Flash" },
|
|
196
|
+
{ provider: "zai-coding-cn", id: "glm-5.3-flash" },
|
|
197
|
+
{ provider: "deepseek", id: "deepseek-v4-pro" },
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
it("P-A2 拒单路径:有孪生仍放行 = 守卫失效——全等命中(大写形态)也必须拒绝", () => {
|
|
201
|
+
let msg = "";
|
|
202
|
+
try {
|
|
203
|
+
// 输入与快照条目逐字符全等,但 registry 存在 case-insensitive 相等的另一条目
|
|
204
|
+
assertCanonicalModelRef("zai-coding-cn/GLM-5.3-Flash", makeSource(TWIN_SNAPSHOT));
|
|
205
|
+
} catch (e) {
|
|
206
|
+
msg = (e as Error).message;
|
|
207
|
+
}
|
|
208
|
+
expect(msg).toContain('Model "zai-coding-cn/GLM-5.3-Flash" matches a registry entry exactly');
|
|
209
|
+
expect(msg).toMatch(
|
|
210
|
+
/registry contains ambiguous case variants for zai-coding-cn\/GLM-5\.3-Flash: \[zai-coding-cn\/GLM-5\.3-Flash, zai-coding-cn\/glm-5\.3-flash\]/,
|
|
211
|
+
);
|
|
212
|
+
// 恢复指引:清理重复条目后重试
|
|
213
|
+
expect(msg).toMatch(/Recovery: remove the duplicate case variant from models\.json/);
|
|
214
|
+
expect(msg).toMatch(/then retry with the exact registry string\./);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("P-A2 拒单路径:孪生快照下小写全等形态同样拒绝(两个方向都不放行)", () => {
|
|
218
|
+
expect(() =>
|
|
219
|
+
assertCanonicalModelRef("zai-coding-cn/glm-5.3-flash", makeSource(TWIN_SNAPSHOT)),
|
|
220
|
+
).toThrow(/ambiguous case variants/);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("无孪生快照下同串放行(守卫零误伤)", () => {
|
|
224
|
+
const ref = assertCanonicalModelRef("zai-coding-cn/GLM-5.3-Flash", makeSource(BASELINE));
|
|
225
|
+
expect(ref.id).toBe("GLM-5.3-Flash");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("不同 provider 下的同形 id 不构成孪生(provider 精确作用域)", () => {
|
|
229
|
+
const snapshot = [
|
|
230
|
+
{ provider: "zai-coding-cn", id: "GLM-5.3-Flash" },
|
|
231
|
+
{ provider: "other-cn", id: "glm-5.3-flash" },
|
|
232
|
+
];
|
|
233
|
+
const ref = assertCanonicalModelRef("zai-coding-cn/GLM-5.3-Flash", makeSource(snapshot));
|
|
234
|
+
expect(ref.id).toBe("GLM-5.3-Flash");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ============================================================
|
|
239
|
+
// 继承路径包装(D2 豁免口径):modelRefFromVerified
|
|
240
|
+
// ============================================================
|
|
241
|
+
|
|
242
|
+
describe("modelRefFromVerified — ctxModel inheritance path (D2)", () => {
|
|
243
|
+
it("无孪生 → 包装为 ModelRef({provider, id} 形态,供 ${provider}/${id} 拼接)", () => {
|
|
244
|
+
const ref = modelRefFromVerified(
|
|
245
|
+
{ provider: "zai-coding-cn", id: "GLM-5.3-Flash" },
|
|
246
|
+
makeSource(BASELINE),
|
|
247
|
+
);
|
|
248
|
+
expect(ref).toEqual({ provider: "zai-coding-cn", id: "GLM-5.3-Flash" });
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("豁免存在性复查:ctxModel 不在 registry 快照中仍放行(运行时已验证)", () => {
|
|
252
|
+
// registry 为空(快照过期等)不阻塞继承——缺省继承是输入缺省而非变体放行
|
|
253
|
+
const ref = modelRefFromVerified(
|
|
254
|
+
{ provider: "runtime-only", id: "live-model" },
|
|
255
|
+
makeSource([]),
|
|
256
|
+
);
|
|
257
|
+
expect(ref).toEqual({ provider: "runtime-only", id: "live-model" });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("P-A2 双路径生效:含孪生快照下继承路径同样拒绝(豁免的只是存在性复查,不是孪生守卫)", () => {
|
|
261
|
+
const TWIN_SNAPSHOT = [
|
|
262
|
+
{ provider: "zai-coding-cn", id: "GLM-5.3-Flash" },
|
|
263
|
+
{ provider: "zai-coding-cn", id: "glm-5.3-flash" },
|
|
264
|
+
];
|
|
265
|
+
expect(() =>
|
|
266
|
+
modelRefFromVerified({ provider: "zai-coding-cn", id: "GLM-5.3-Flash" }, makeSource(TWIN_SNAPSHOT)),
|
|
267
|
+
).toThrow(/ambiguous case variants/);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ============================================================
|
|
272
|
+
// stripThinkingSuffix / assertThinkingLevel
|
|
273
|
+
// ============================================================
|
|
274
|
+
|
|
275
|
+
describe("stripThinkingSuffix", () => {
|
|
276
|
+
it("剥白名单后缀", () => {
|
|
277
|
+
expect(stripThinkingSuffix("p/m:high")).toBe("p/m");
|
|
278
|
+
expect(stripThinkingSuffix("p/m:off")).toBe("p/m");
|
|
279
|
+
expect(stripThinkingSuffix("p/m")).toBe("p/m");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("非白名单冒号后缀不剥", () => {
|
|
283
|
+
expect(stripThinkingSuffix("p/m:foo")).toBe("p/m:foo");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("assertThinkingLevel", () => {
|
|
288
|
+
it("undefined 透传", () => {
|
|
289
|
+
expect(assertThinkingLevel(undefined)).toBeUndefined();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("白名单值放行并窄化为字面量联合", () => {
|
|
293
|
+
expect(assertThinkingLevel("high")).toBe("high");
|
|
294
|
+
expect(assertThinkingLevel("max")).toBe("max");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("非白名单值同步抛错(可操作文案:列出合法值)", () => {
|
|
298
|
+
expect(() => assertThinkingLevel("ultra")).toThrow(
|
|
299
|
+
/Invalid thinkingLevel "ultra"\. Allowed values: off, minimal, low, medium, high, xhigh, max\./,
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("THINKING_ORDER SSOT:白名单全集顺序稳定(低→高,含 max)", () => {
|
|
304
|
+
expect(THINKING_ORDER).toEqual(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -58,7 +58,7 @@ describe("stringifySchemaCached — WeakMap 引用级缓存", () => {
|
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
it("pretty 缩进锚定:indent=2 与 formatSchemaInstruction 渲染同源", () => {
|
|
61
|
+
it("pretty 缩进锚定:indent=2 与 resolver formatSchemaInstruction 渲染同源", () => {
|
|
62
62
|
const out = stringifySchemaCached({ type: "object" }, "pretty");
|
|
63
63
|
expect(out).toBe('{\n "type": "object"\n}');
|
|
64
64
|
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/shared/__tests__/timer-delay.test.ts
|
|
2
|
+
//
|
|
3
|
+
// assertSafeTimerDelay 纯函数契约——三个 timer 挂载入口(budgetTimeMs /
|
|
4
|
+
// XYZ_SUBAGENT_SPAWN_WATCHDOG_MS / idleTimeoutMs)统一在值流入 setTimeout 前调
|
|
5
|
+
// 用本函数 fail-fast,契约由本文件锁定。
|
|
6
|
+
//
|
|
7
|
+
// [F-3 NaN 穿透修复] 旧实现只挡 `> MAX`:NaN 的 `NaN > MAX` 为 false 静默放行,
|
|
8
|
+
// setTimeout(fn, NaN) 被 Node 塌缩为 1ms 立即触发(watchdog 刚启动就误杀)。
|
|
9
|
+
// 非有限数(NaN/±Infinity)现在同样 fail-fast,且错误消息区分两种情况。
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
import { MAX_TIMER_DELAY_MS, assertSafeTimerDelay } from "../timer-delay.ts";
|
|
13
|
+
|
|
14
|
+
describe("assertSafeTimerDelay", () => {
|
|
15
|
+
it("安全域内的有限值放行(0 与上限值不抛)", () => {
|
|
16
|
+
expect(() => assertSafeTimerDelay(0, "t")).not.toThrow();
|
|
17
|
+
expect(() => assertSafeTimerDelay(1000, "t")).not.toThrow();
|
|
18
|
+
expect(() => assertSafeTimerDelay(MAX_TIMER_DELAY_MS, "t")).not.toThrow();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("[F-3] NaN → fail-fast throw(旧实现 NaN > MAX 为 false 静默放行)", () => {
|
|
22
|
+
expect(() => assertSafeTimerDelay(Number.NaN, "budgetTimeMs")).toThrowError(/NaN/);
|
|
23
|
+
expect(() => assertSafeTimerDelay(Number.NaN, "budgetTimeMs")).toThrowError(/not a finite number/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("[F-3] ±Infinity → fail-fast throw", () => {
|
|
27
|
+
expect(() => assertSafeTimerDelay(Number.POSITIVE_INFINITY, "t")).toThrowError(/not a finite number/);
|
|
28
|
+
expect(() => assertSafeTimerDelay(Number.NEGATIVE_INFINITY, "t")).toThrowError(/not a finite number/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("非有限值错误消息含来源标识与上游修复指引(非 clamp 指引)", () => {
|
|
32
|
+
expect(() => assertSafeTimerDelay(Number.NaN, "idleTimeoutMs")).toThrowError(/idleTimeoutMs/);
|
|
33
|
+
expect(() => assertSafeTimerDelay(Number.NaN, "idleTimeoutMs")).toThrowError(/fix the upstream computation/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("超出上限 → fail-fast throw,消息含上限值与 clamp 恢复指引(原语义不变)", () => {
|
|
37
|
+
expect(() => assertSafeTimerDelay(MAX_TIMER_DELAY_MS + 1, "budgetTimeMs")).toThrowError(/2147483647/);
|
|
38
|
+
expect(() => assertSafeTimerDelay(3_000_000_000, "budgetTimeMs")).toThrowError(/Recovery: clamp/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("两种错误可区分:非有限值消息不含 clamp 指引,溢出消息不含 upstream 指引", () => {
|
|
42
|
+
const nonFiniteMsg = (() => {
|
|
43
|
+
try {
|
|
44
|
+
assertSafeTimerDelay(Number.NaN, "t");
|
|
45
|
+
return "";
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return (err as Error).message;
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
const overflowMsg = (() => {
|
|
51
|
+
try {
|
|
52
|
+
assertSafeTimerDelay(MAX_TIMER_DELAY_MS + 1, "t");
|
|
53
|
+
return "";
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return (err as Error).message;
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
expect(nonFiniteMsg).not.toContain("clamp");
|
|
59
|
+
expect(overflowMsg).not.toContain("fix the upstream computation");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -88,7 +88,9 @@ function typecheckMeta(raw: unknown, kind: ResourceKind): ResourceMeta | null {
|
|
|
88
88
|
|
|
89
89
|
if (kind === "workflow") {
|
|
90
90
|
// minor-2:agent 专属字段不可出现在 workflow(串类 reject)
|
|
91
|
-
if (o.examples !== undefined || o.tools !== undefined || o.model !== undefined)
|
|
91
|
+
if (o.examples !== undefined || o.tools !== undefined || o.model !== undefined || o.engine !== undefined) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
92
94
|
// phases 必填数组,元素为 string | {title:string, detail?:string}
|
|
93
95
|
if (!Array.isArray(o.phases)) return null;
|
|
94
96
|
const phases: WorkflowMeta["phases"] = [];
|
|
@@ -156,6 +158,7 @@ function typecheckMeta(raw: unknown, kind: ResourceKind): ResourceMeta | null {
|
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
const model = isString(o.model) ? o.model : undefined;
|
|
161
|
+
const engine = isString(o.engine) ? o.engine : undefined;
|
|
159
162
|
|
|
160
163
|
const meta: AgentMeta = {
|
|
161
164
|
kind: "agent",
|
|
@@ -164,6 +167,7 @@ function typecheckMeta(raw: unknown, kind: ResourceKind): ResourceMeta | null {
|
|
|
164
167
|
...(examples !== undefined ? { examples } : {}),
|
|
165
168
|
...(tools !== undefined ? { tools } : {}),
|
|
166
169
|
...(model !== undefined ? { model } : {}),
|
|
170
|
+
...(engine !== undefined ? { engine } : {}),
|
|
167
171
|
...(when !== undefined ? { when } : {}),
|
|
168
172
|
...(notFor !== undefined ? { notFor } : {}),
|
|
169
173
|
};
|