@zhushanwen/pi-goal 0.7.1 → 0.8.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 +4 -2
- package/src/__tests__/command-adapter.test.ts +130 -81
- package/src/__tests__/deserialize-state.test.ts +79 -45
- package/src/__tests__/event-adapter.test.ts +26 -39
- package/src/__tests__/goal-control-adapter.test.ts +7 -69
- package/src/__tests__/goal-control-prompt.test.ts +45 -46
- package/src/__tests__/goal-control-rpc.test.ts +8 -49
- package/src/__tests__/gui.test.ts +32 -93
- package/src/__tests__/index.test.ts +19 -37
- package/src/__tests__/schema.test.ts +38 -0
- package/src/__tests__/service.test.ts +20 -52
- package/src/__tests__/session.test.ts +17 -56
- package/src/__tests__/stale-checker.test.ts +9 -40
- package/src/adapters/command-adapter.ts +46 -47
- package/src/adapters/event-adapter.ts +0 -1
- package/src/adapters/event-handlers/agent-end.ts +28 -31
- package/src/adapters/event-handlers/before-agent-start.ts +7 -8
- package/src/adapters/event-handlers/shared.ts +1 -20
- package/src/adapters/event-handlers/turn-end.ts +5 -9
- package/src/adapters/goal-control-adapter.ts +103 -225
- package/src/adapters/ports.ts +3 -0
- package/src/commands.ts +2 -8
- package/src/constants.ts +0 -1
- package/src/engine/__tests__/budget.test.ts +53 -95
- package/src/engine/__tests__/goal.test.ts +62 -102
- package/src/engine/budget.ts +50 -49
- package/src/engine/goal.ts +0 -2
- package/src/engine/types.ts +3 -10
- package/src/index.ts +3 -16
- package/src/persistence.ts +18 -9
- package/src/ports.ts +0 -2
- package/src/projection/__tests__/format.test.ts +161 -0
- package/src/projection/__tests__/prompts.test.ts +48 -77
- package/src/projection/__tests__/widget.test.ts +34 -50
- package/src/projection/gui.ts +126 -0
- package/src/projection/prompts.ts +32 -101
- package/src/projection/widget.ts +12 -31
- package/src/service.ts +17 -40
- package/src/__tests__/stubs/typebox.ts +0 -18
- package/src/adapters/event-handlers/agent-start.ts +0 -17
|
@@ -260,7 +260,6 @@ describe("handleAgentEnd — continuation", () => {
|
|
|
260
260
|
(s as { component?: string } | undefined)?.component === "goal:agent-end" &&
|
|
261
261
|
/continuation deferred/.test(String((s as { message?: string } | undefined)?.message ?? "")),
|
|
262
262
|
);
|
|
263
|
-
expect(deferredLog).toBeDefined();
|
|
264
263
|
const data = (deferredLog as { data?: { activePending?: number; ids?: string[] } } | undefined)?.data;
|
|
265
264
|
expect(data?.activePending).toBe(2); // 多元素计数(替代原 >=1 弱断言)
|
|
266
265
|
expect(data?.ids).toEqual(["bg-1", "bg-2"]); // ids 透出
|
|
@@ -390,15 +389,13 @@ describe("handleBeforeAgentStart", () => {
|
|
|
390
389
|
|
|
391
390
|
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
392
391
|
|
|
393
|
-
expect(result).toBeDefined();
|
|
394
392
|
expect(result!.message.customType).toBe("goal-context");
|
|
395
393
|
expect(result!.message.content).toContain("[GOAL mode activated]");
|
|
396
394
|
expect(result!.message.display).toBe(false);
|
|
397
395
|
});
|
|
398
396
|
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
it("恒定注入 plan mode 建议段落(全解耦:不再探测 plan extension)", async () => {
|
|
397
|
+
// contextInjection 精简(planAvailable 参数已删,plan 提示收敛到 continuationPrompt)
|
|
398
|
+
it("context injection 不含 plan mode 提示(收敛到 continuation)", async () => {
|
|
402
399
|
const { pi } = makeFakePi();
|
|
403
400
|
const { ctx } = makeFakeCtx();
|
|
404
401
|
const session = createGoalSession();
|
|
@@ -406,9 +403,10 @@ describe("handleBeforeAgentStart", () => {
|
|
|
406
403
|
|
|
407
404
|
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
408
405
|
|
|
409
|
-
expect(result).toBeDefined();
|
|
410
406
|
expect(result!.message.customType).toBe("goal-context");
|
|
411
|
-
|
|
407
|
+
// plan 引导段已删(收敛到 continuationPrompt,agent_end 发)
|
|
408
|
+
expect(result!.message.content).not.toContain("plan mode");
|
|
409
|
+
expect(result!.message.content).not.toContain("__planStart");
|
|
412
410
|
});
|
|
413
411
|
|
|
414
412
|
// FR-8.1 G-007: AUTO_CLEAR_TURNS=2
|
|
@@ -444,7 +442,7 @@ describe("handleBeforeAgentStart", () => {
|
|
|
444
442
|
expect(result).toBeUndefined();
|
|
445
443
|
expect(session.state).not.toBeNull(); // 未清理
|
|
446
444
|
// setStatus 触发(折叠为终态单行)
|
|
447
|
-
expect(all.filter((c) => c.kind === "setStatus" && c.key === "goal")
|
|
445
|
+
expect(all.filter((c) => c.kind === "setStatus" && c.key === "goal")).toHaveLength(1);
|
|
448
446
|
});
|
|
449
447
|
|
|
450
448
|
// ADR-002 context usage 提示(CONTEXT_USAGE_RATIO_LIMIT=0.85,保持 active)
|
|
@@ -456,7 +454,6 @@ describe("handleBeforeAgentStart", () => {
|
|
|
456
454
|
|
|
457
455
|
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
458
456
|
|
|
459
|
-
expect(result).toBeDefined();
|
|
460
457
|
expect(result!.message.customType).toBe("goal-context-exceeded");
|
|
461
458
|
// ADR-002:goal 保持 active(不转 paused)
|
|
462
459
|
expect(session.state!.status).toBe("active");
|
|
@@ -487,34 +484,24 @@ describe("handleBeforeAgentStart", () => {
|
|
|
487
484
|
|
|
488
485
|
// ── pending-notifications(W2 跨扩展集成)───────────────────
|
|
489
486
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const { pi } = makeFakePi();
|
|
511
|
-
const { ctx } = makeFakeCtx(); // 默认 getEntries: () => []
|
|
512
|
-
const session = createGoalSession();
|
|
513
|
-
session.state = makeRunningState();
|
|
514
|
-
|
|
515
|
-
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
516
|
-
|
|
517
|
-
expect(result).toBeDefined();
|
|
518
|
-
expect(result!.message.content).not.toContain("pending async operation");
|
|
519
|
-
});
|
|
487
|
+
// pendingHint 已删除(pending 感知交给 LLM 调 pending_notifications tool),
|
|
488
|
+
// 无论是否有活跃 pending entries,content 都不含 pending 提示——轻量回归保护。
|
|
489
|
+
const PENDING_HINT_CASES: Array<[string, unknown[]]> = [
|
|
490
|
+
["有活跃 pending", [{ type: "custom", customType: "pending:register", data: { id: "wf-1" } }]],
|
|
491
|
+
["无 pending", []],
|
|
492
|
+
];
|
|
493
|
+
it.each(PENDING_HINT_CASES)(
|
|
494
|
+
"pendingHint 已移除:%s → content 不含 pending 提示",
|
|
495
|
+
async (_label, entries) => {
|
|
496
|
+
const { pi } = makeFakePi();
|
|
497
|
+
const { ctx } = makeFakeCtx({ entries });
|
|
498
|
+
const session = createGoalSession();
|
|
499
|
+
session.state = makeRunningState();
|
|
500
|
+
|
|
501
|
+
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
502
|
+
|
|
503
|
+
expect(result!.message.customType).toBe("goal-context");
|
|
504
|
+
expect(result!.message.content).not.toContain("pending async operation");
|
|
505
|
+
},
|
|
506
|
+
);
|
|
520
507
|
});
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
handleCreate,
|
|
18
18
|
handleReportBlocked,
|
|
19
19
|
} from "../adapters/goal-control-adapter";
|
|
20
|
-
import { createGoalState
|
|
20
|
+
import { createGoalState } from "../engine/goal";
|
|
21
21
|
import type { GoalRuntimeState } from "../engine/types";
|
|
22
22
|
import type { UiPort } from "../ports";
|
|
23
23
|
import type { ServicePorts } from "../service";
|
|
@@ -106,14 +106,6 @@ describe("handleComplete — active 守卫 + evidence + finalizeAndPersist", ()
|
|
|
106
106
|
handleComplete({ action: "complete", evidence: " " }, session, makeFakePorts()),
|
|
107
107
|
).toThrow(/evidence/);
|
|
108
108
|
});
|
|
109
|
-
|
|
110
|
-
it("completedTasks 写入 history", () => {
|
|
111
|
-
const session = createGoalSession();
|
|
112
|
-
session.state = activeState();
|
|
113
|
-
const ports = makeFakePorts();
|
|
114
|
-
handleComplete({ action: "complete", evidence: "done", completedTasks: 5 }, session, ports);
|
|
115
|
-
expect((ports.history[0] as { completedTasks?: number }).completedTasks).toBe(5);
|
|
116
|
-
});
|
|
117
109
|
});
|
|
118
110
|
|
|
119
111
|
// ── handleCreate ─────────────────────────────────────
|
|
@@ -143,27 +135,13 @@ describe("handleCreate — slug+objective 必填 + 非终态守卫 + createGoal"
|
|
|
143
135
|
expect(ports.notifications[0]?.text).toContain("ship feature X");
|
|
144
136
|
});
|
|
145
137
|
|
|
146
|
-
it("objective 空 → throw
|
|
138
|
+
it("objective 空 → throw", () => {
|
|
147
139
|
const session = createGoalSession();
|
|
148
140
|
expect(() =>
|
|
149
|
-
handleCreate({ action: "create", slug: "x", objective: " " }, session, makeFakePorts()),
|
|
141
|
+
handleCreate({ action: "create", slug: "x", objective: " ", successCriteria: "done" }, session, makeFakePorts()),
|
|
150
142
|
).toThrow(/objective/);
|
|
151
143
|
});
|
|
152
144
|
|
|
153
|
-
it("slug 空 → throw(即使有 objective)", () => {
|
|
154
|
-
const session = createGoalSession();
|
|
155
|
-
expect(() =>
|
|
156
|
-
handleCreate({ action: "create", slug: " ", objective: "do thing" }, session, makeFakePorts()),
|
|
157
|
-
).toThrow(/slug/);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("slug 缺省 → throw", () => {
|
|
161
|
-
const session = createGoalSession();
|
|
162
|
-
expect(() =>
|
|
163
|
-
handleCreate({ action: "create", objective: "do thing" }, session, makeFakePorts()),
|
|
164
|
-
).toThrow(/slug/);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
145
|
it("successCriteria 空 → throw(即使有 slug + objective)", () => {
|
|
168
146
|
const session = createGoalSession();
|
|
169
147
|
expect(() =>
|
|
@@ -175,33 +153,10 @@ describe("handleCreate — slug+objective 必填 + 非终态守卫 + createGoal"
|
|
|
175
153
|
).toThrow(/successCriteria/);
|
|
176
154
|
});
|
|
177
155
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
session.state = activeState({ status: "active" });
|
|
181
|
-
expect(() =>
|
|
182
|
-
handleCreate(
|
|
183
|
-
{ action: "create", slug: "new", objective: "new obj", successCriteria: "done" },
|
|
184
|
-
session,
|
|
185
|
-
makeFakePorts(),
|
|
186
|
-
),
|
|
187
|
-
).toThrow(/already active/i);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("已有 paused goal → throw(paused 也是非终态)", () => {
|
|
191
|
-
const session = createGoalSession();
|
|
192
|
-
session.state = activeState({ status: "paused" });
|
|
193
|
-
expect(() =>
|
|
194
|
-
handleCreate(
|
|
195
|
-
{ action: "create", slug: "new", objective: "new obj", successCriteria: "done" },
|
|
196
|
-
session,
|
|
197
|
-
makeFakePorts(),
|
|
198
|
-
),
|
|
199
|
-
).toThrow(/already active/i);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("已有 blocked goal → throw(blocked 也是非终态)", () => {
|
|
156
|
+
const NON_TERMINAL_STATUSES: GoalRuntimeState["status"][] = ["active", "paused", "blocked"];
|
|
157
|
+
it.each(NON_TERMINAL_STATUSES)("已有 %s goal → throw(D25 非终态守卫)", (status) => {
|
|
203
158
|
const session = createGoalSession();
|
|
204
|
-
session.state = activeState({ status
|
|
159
|
+
session.state = activeState({ status });
|
|
205
160
|
expect(() =>
|
|
206
161
|
handleCreate(
|
|
207
162
|
{ action: "create", slug: "new", objective: "new obj", successCriteria: "done" },
|
|
@@ -240,29 +195,17 @@ describe("handleCreate — slug+objective 必填 + 非终态守卫 + createGoal"
|
|
|
240
195
|
).toThrow(/tokenBudget/);
|
|
241
196
|
});
|
|
242
197
|
|
|
243
|
-
it("timeBudgetMinutes <= 0 → throw", () => {
|
|
244
|
-
const session = createGoalSession();
|
|
245
|
-
expect(() =>
|
|
246
|
-
handleCreate(
|
|
247
|
-
{ action: "create", slug: "x", objective: "x", successCriteria: "done", timeBudgetMinutes: -5 },
|
|
248
|
-
session,
|
|
249
|
-
makeFakePorts(),
|
|
250
|
-
),
|
|
251
|
-
).toThrow(/timeBudgetMinutes/);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
198
|
it("合法 budget → 合并进新 state", () => {
|
|
255
199
|
const session = createGoalSession();
|
|
256
200
|
const ports = makeFakePorts();
|
|
257
201
|
|
|
258
202
|
handleCreate(
|
|
259
|
-
{ action: "create", slug: "x", objective: "x", successCriteria: "done", tokenBudget: 8000
|
|
203
|
+
{ action: "create", slug: "x", objective: "x", successCriteria: "done", tokenBudget: 8000 },
|
|
260
204
|
session,
|
|
261
205
|
ports,
|
|
262
206
|
);
|
|
263
207
|
|
|
264
208
|
expect(session.state!.budget.tokenBudget).toBe(8000);
|
|
265
|
-
expect(session.state!.budget.timeBudgetMinutes).toBe(30);
|
|
266
209
|
});
|
|
267
210
|
});
|
|
268
211
|
|
|
@@ -307,9 +250,4 @@ describe("handleReportBlocked — active 守卫 + tick + transition + persist",
|
|
|
307
250
|
handleReportBlocked({ action: "report_blocked", reason: "" }, session, makeFakePorts()),
|
|
308
251
|
).toThrow(/reason/);
|
|
309
252
|
});
|
|
310
|
-
|
|
311
|
-
it("active→blocked 是合法转换", () => {
|
|
312
|
-
// transitionStatus 自身已由 engine 测试覆盖,此处验证集成不破坏
|
|
313
|
-
expect(transitionStatus("active", "blocked")).toBe("blocked");
|
|
314
|
-
});
|
|
315
253
|
});
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
// 提示词质量回归:goal_control tool description + runtime 错误文案必须是
|
|
2
2
|
// "可纠正的行为约束器",而非干巴巴的错误。
|
|
3
3
|
//
|
|
4
|
-
// agent(LLM)唯一能看到的 tool 元信息就是 description +
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// 正例,能让弱模型在首轮或报错后无需猜测即可对齐。
|
|
4
|
+
// agent(LLM)唯一能看到的 tool 元信息就是 description + 报错文案。description 用中文
|
|
5
|
+
// 重写(动作 create/complete/report_blocked + 规则 + §2.5 终态语义),删除英文 Examples/Don't
|
|
6
|
+
// 段;runtime throw 保留 .trim() 空串校验 + 'Correct:' 纠正正例(缺失必填已前移到 schema
|
|
7
|
+
// discriminated union 层拒绝,C3)。
|
|
9
8
|
//
|
|
10
|
-
// 本测试用源码断言(读 .ts
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// 模式参考:subagent-workflow/src/interface/__tests__/subagent-tool-prompt.test.ts。
|
|
9
|
+
// 本测试用源码断言(读 .ts 文件文本)锁定这些约束,防止后续重构把中文语义/纠错文案/
|
|
10
|
+
// union 结构删掉。读源码而非 import,避免 mock 链(goal-control-adapter.ts 依赖
|
|
11
|
+
// pi-ai/typebox/pi-tui/ExtensionAPI 等值导入)。
|
|
15
12
|
|
|
16
13
|
import { readFileSync } from "node:fs";
|
|
17
14
|
import { dirname, join } from "node:path";
|
|
@@ -34,62 +31,64 @@ function extractDescription(src: string): string {
|
|
|
34
31
|
|
|
35
32
|
const DESCRIPTION = extractDescription(ADAPTER_SRC);
|
|
36
33
|
|
|
37
|
-
describe("goal_control description —
|
|
38
|
-
it("
|
|
39
|
-
|
|
40
|
-
expect(DESCRIPTION).toContain(
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("含 complete 正例子串", () => {
|
|
44
|
-
// complete 需模型主动生成 evidence,省略动因最强;正例锁定必填字段。
|
|
45
|
-
expect(DESCRIPTION).toContain('{"action":"complete"');
|
|
34
|
+
describe("goal_control description — 中文化 + 动作覆盖", () => {
|
|
35
|
+
it("含管理目标总述 + complete 须满足每条 successCriteria 条件", () => {
|
|
36
|
+
expect(DESCRIPTION).toContain("管理当前会话的目标");
|
|
37
|
+
expect(DESCRIPTION).toContain("满足每条 successCriteria 条件");
|
|
46
38
|
});
|
|
39
|
+
});
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
describe("goal_control description — §2.5 终态语义", () => {
|
|
42
|
+
it("create 失败提示 / complete 报告 token / pause-resume-budget 归用户 / 不反复报告阻塞", () => {
|
|
43
|
+
expect(DESCRIPTION).toContain("请让用户运行 /goal resume 或 /goal clear");
|
|
44
|
+
expect(DESCRIPTION).toContain("报告最终 token 用量");
|
|
45
|
+
expect(DESCRIPTION).toContain("pause/resume 和 budget 变更由用户经 /goal 命令控制");
|
|
46
|
+
expect(DESCRIPTION).toContain("不要反复报告同一阻塞");
|
|
50
47
|
});
|
|
51
48
|
});
|
|
52
49
|
|
|
53
|
-
describe("goal_control description —
|
|
54
|
-
it("
|
|
55
|
-
|
|
50
|
+
describe("goal_control description — 删除 Examples/Don't 英文段", () => {
|
|
51
|
+
it("不含 Examples JSON 正例段 + 不含 Don't 段", () => {
|
|
52
|
+
// Examples 段已删,description 不再含裸 JSON 正例
|
|
53
|
+
expect(DESCRIPTION).not.toContain('{"action":"create"');
|
|
54
|
+
expect(DESCRIPTION).not.toMatch(/Don't/);
|
|
56
55
|
});
|
|
56
|
+
});
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
expect(
|
|
58
|
+
describe("goal_control schema — discriminated union(C3)", () => {
|
|
59
|
+
it("Type.Union + 各分支 additionalProperties:false + 无 timeBudgetMinutes", () => {
|
|
60
|
+
expect(ADAPTER_SRC).toMatch(/Type\.Union\(/);
|
|
61
|
+
expect(ADAPTER_SRC).toContain("additionalProperties: false");
|
|
62
|
+
expect(ADAPTER_SRC).not.toMatch(/timeBudgetMinutes/);
|
|
61
63
|
});
|
|
62
64
|
});
|
|
63
65
|
|
|
64
|
-
describe("goal_control promptGuidelines — complete/report_blocked
|
|
65
|
-
|
|
66
|
-
// 「该主动调但不调」的核心载体。用源码断言锁定两条正向触发引导,防止后续重构删掉。
|
|
67
|
-
it("源码含 promptGuidelines 字段", () => {
|
|
66
|
+
describe("goal_control promptGuidelines — complete/report_blocked 主动触发引导", () => {
|
|
67
|
+
it("含 promptGuidelines 字段 + complete/report_blocked 主动触发信号", () => {
|
|
68
68
|
expect(ADAPTER_SRC).toMatch(/promptGuidelines:\s*\[/);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("promptGuidelines 含 complete 主动触发信号(proactively + objective is actually achieved)", () => {
|
|
72
69
|
expect(ADAPTER_SRC).toMatch(/complete:.*proactively call.*objective is actually achieved/s);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("promptGuidelines 含 report_blocked 主动触发信号(proactively + ≥3 approaches)", () => {
|
|
76
70
|
expect(ADAPTER_SRC).toMatch(/report_blocked:.*proactively call.*≥3 distinct alternative approaches/s);
|
|
71
|
+
expect(ADAPTER_SRC).toContain("do not repeatedly report the same blocker");
|
|
77
72
|
});
|
|
78
73
|
});
|
|
79
74
|
|
|
80
|
-
describe("goal_control runtime 错误文案 — 含 'Correct:'
|
|
81
|
-
it("
|
|
82
|
-
// objective/
|
|
75
|
+
describe("goal_control runtime 错误文案 — 含 'Correct:' 纠正正例", () => {
|
|
76
|
+
it("≥4 处 'Correct:'(4 条空串 throw 各一带正例)+ objective/evidence 报错带正例", () => {
|
|
77
|
+
// objective/successCriteria/evidence/reason 四条空串 throw 各应带完整 JSON 正例,
|
|
83
78
|
// 让弱模型在报错后无需猜测即可纠正。读整份源码统计出现次数。
|
|
84
79
|
const matches = ADAPTER_SRC.match(/Correct:/g) || [];
|
|
85
80
|
expect(matches.length).toBeGreaterThanOrEqual(4);
|
|
81
|
+
expect(ADAPTER_SRC).toMatch(/'objective' must not be empty[\s\S]*?Correct:/);
|
|
82
|
+
expect(ADAPTER_SRC).toMatch(/'evidence' must not be empty[\s\S]*?Correct:/);
|
|
86
83
|
});
|
|
84
|
+
});
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
expect(ADAPTER_SRC).
|
|
86
|
+
describe("goal_control 描述修正(A1/slug optional)", () => {
|
|
87
|
+
it("无 'at least 3 approaches' + reason throw 含 'what you tried' + slug 真 optional", () => {
|
|
88
|
+
// A1: reason schema desc/throw 已删强制 ≥3 声明
|
|
89
|
+
expect(ADAPTER_SRC).not.toMatch(/at least 3 approaches/);
|
|
90
|
+
expect(ADAPTER_SRC).toMatch(/'reason' must not be empty[\s\S]*?what you tried/);
|
|
91
|
+
expect(ADAPTER_SRC).toContain("可选。");
|
|
92
|
+
expect(ADAPTER_SRC).not.toMatch(/'slug' is required/);
|
|
94
93
|
});
|
|
95
94
|
});
|
|
@@ -104,7 +104,7 @@ function createViaHandler(
|
|
|
104
104
|
tool: CapturedTool,
|
|
105
105
|
pi: ExtensionAPI,
|
|
106
106
|
ctx: ExtensionContext,
|
|
107
|
-
params: { slug: string; objective: string; tokenBudget?: number
|
|
107
|
+
params: { slug: string; objective: string; tokenBudget?: number },
|
|
108
108
|
): Promise<ExecuteResult> {
|
|
109
109
|
return tool.execute(
|
|
110
110
|
"call-1",
|
|
@@ -127,7 +127,6 @@ describe("goal_control execute — RPC __gui__ 注入分支", () => {
|
|
|
127
127
|
objective: "rpc card goal",
|
|
128
128
|
tokenBudget: 10000,
|
|
129
129
|
});
|
|
130
|
-
expect(result.details.__gui__).toBeDefined();
|
|
131
130
|
const gui = result.details.__gui__!;
|
|
132
131
|
expect(gui.component.type).toBe("card");
|
|
133
132
|
// card body 含 progress-bar(tokenBudget)+ stats-line
|
|
@@ -145,7 +144,6 @@ describe("goal_control execute — RPC __gui__ 注入分支", () => {
|
|
|
145
144
|
slug: "rpc-stats",
|
|
146
145
|
objective: "rpc stats goal",
|
|
147
146
|
});
|
|
148
|
-
expect(result.details.__gui__).toBeDefined();
|
|
149
147
|
const gui = result.details.__gui__!;
|
|
150
148
|
expect(gui.component.type).toBe("stats-line");
|
|
151
149
|
// stats-line items 含 goal/status/turn/tokens
|
|
@@ -182,20 +180,6 @@ describe("goal_control execute — RPC __gui__ 注入分支", () => {
|
|
|
182
180
|
).rejects.toThrow(/not active/i);
|
|
183
181
|
});
|
|
184
182
|
|
|
185
|
-
it("TUI 模式 → __gui__ 不附加(即使有 state)", async () => {
|
|
186
|
-
const { pi, ctx } = makeFixture("tui");
|
|
187
|
-
const tool = captureTool(pi);
|
|
188
|
-
const result = await createViaHandler(tool, pi, ctx, {
|
|
189
|
-
slug: "tui-no-gui",
|
|
190
|
-
objective: "tui goal",
|
|
191
|
-
tokenBudget: 5000,
|
|
192
|
-
});
|
|
193
|
-
// TUI 模式不注入 __gui__
|
|
194
|
-
expect(result.details.__gui__).toBeUndefined();
|
|
195
|
-
// 但 content 仍正常
|
|
196
|
-
expect(result.content[0]!.text).toContain("Goal created");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
183
|
it("RPC + complete 动作 → __gui__ 也附加(用终态 state 渲染)", async () => {
|
|
200
184
|
// 验证 __gui__ 注入不限于 create:complete 后 session.state 仍非 null(status=complete)
|
|
201
185
|
const { pi, ctx } = makeFixture("rpc");
|
|
@@ -210,7 +194,6 @@ describe("goal_control execute — RPC __gui__ 注入分支", () => {
|
|
|
210
194
|
undefined,
|
|
211
195
|
ctx,
|
|
212
196
|
);
|
|
213
|
-
expect(result.details.__gui__).toBeDefined();
|
|
214
197
|
expect(result.details.status).toBe("complete");
|
|
215
198
|
const gui = result.details.__gui__!;
|
|
216
199
|
// 无 budget → stats-line
|
|
@@ -231,7 +214,6 @@ describe("goal_control execute — RPC __gui__ 注入分支", () => {
|
|
|
231
214
|
undefined,
|
|
232
215
|
ctx,
|
|
233
216
|
);
|
|
234
|
-
expect(result.details.__gui__).toBeDefined();
|
|
235
217
|
expect(result.details.status).toBe("blocked");
|
|
236
218
|
// 无 budget → stats-line;blocked status severity=danger
|
|
237
219
|
const gui = result.details.__gui__!;
|
|
@@ -241,44 +223,21 @@ describe("goal_control execute — RPC __gui__ 注入分支", () => {
|
|
|
241
223
|
expect(statusItem!.severity).toBe("danger");
|
|
242
224
|
});
|
|
243
225
|
|
|
244
|
-
it("RPC + 有 time budget → __gui__ card 含 time progress-bar", async () => {
|
|
245
|
-
const { pi, ctx } = makeFixture("rpc");
|
|
246
|
-
const tool = captureTool(pi);
|
|
247
|
-
const result = await createViaHandler(tool, pi, ctx, {
|
|
248
|
-
slug: "time-budget",
|
|
249
|
-
objective: "time boxed",
|
|
250
|
-
timeBudgetMinutes: 30,
|
|
251
|
-
});
|
|
252
|
-
const gui = result.details.__gui__!;
|
|
253
|
-
expect(gui.component.type).toBe("card");
|
|
254
|
-
const body = gui.component.props.body as Array<{ type: string; props: { label?: string } }>;
|
|
255
|
-
const timeBar = body.find((c) => c.type === "progress-bar" && c.props.label === "time");
|
|
256
|
-
expect(timeBar).toBeDefined();
|
|
257
|
-
});
|
|
258
226
|
});
|
|
259
227
|
|
|
260
|
-
// ── 边界:非 RPC 模式(json/print)也不注入 __gui__ ──
|
|
228
|
+
// ── 边界:非 RPC 模式(tui/json/print)也不注入 __gui__ ──
|
|
261
229
|
|
|
262
230
|
describe("goal_control execute — 非 RPC 模式不注入 __gui__", () => {
|
|
263
|
-
|
|
264
|
-
|
|
231
|
+
const NON_RPC_MODES: Array<"tui" | "json" | "print"> = ["tui", "json", "print"];
|
|
232
|
+
it.each(NON_RPC_MODES)("%s 模式 → __gui__ 不附加(content 仍正常)", async (mode) => {
|
|
233
|
+
const { pi, ctx } = makeFixture(mode);
|
|
265
234
|
const tool = captureTool(pi);
|
|
266
235
|
const result = await createViaHandler(tool, pi, ctx, {
|
|
267
|
-
slug:
|
|
268
|
-
objective:
|
|
269
|
-
tokenBudget: 1000,
|
|
270
|
-
});
|
|
271
|
-
expect(result.details.__gui__).toBeUndefined();
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("print 模式 → __gui__ 不附加", async () => {
|
|
275
|
-
const { pi, ctx } = makeFixture("print");
|
|
276
|
-
const tool = captureTool(pi);
|
|
277
|
-
const result = await createViaHandler(tool, pi, ctx, {
|
|
278
|
-
slug: "print-mode",
|
|
279
|
-
objective: "print goal",
|
|
236
|
+
slug: `${mode}-mode`,
|
|
237
|
+
objective: `${mode} goal`,
|
|
280
238
|
tokenBudget: 1000,
|
|
281
239
|
});
|
|
282
240
|
expect(result.details.__gui__).toBeUndefined();
|
|
241
|
+
expect(result.content[0]!.text).toContain("Goal created");
|
|
283
242
|
});
|
|
284
243
|
});
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { describe, expect, it } from "vitest";
|
|
11
11
|
|
|
12
|
-
import { buildGoalGui } from "../
|
|
12
|
+
import { buildGoalGui } from "../projection/gui";
|
|
13
13
|
import { createGoalState } from "../engine/goal";
|
|
14
|
-
import type { GoalRuntimeState } from "../engine/types";
|
|
14
|
+
import type { GoalRuntimeState, GoalStatus } from "../engine/types";
|
|
15
15
|
|
|
16
16
|
function makeState(overrides: Partial<GoalRuntimeState> = {}): GoalRuntimeState {
|
|
17
17
|
return {
|
|
@@ -20,6 +20,15 @@ function makeState(overrides: Partial<GoalRuntimeState> = {}): GoalRuntimeState
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** 从 card 分支的 body 中按 type 取组件(hasBudget 分支只有一个 progress-bar / 一个 stats-line 组合)。 */
|
|
24
|
+
function findBodyComp(
|
|
25
|
+
gui: ReturnType<typeof buildGoalGui>,
|
|
26
|
+
type: string,
|
|
27
|
+
): { type: string; props: Record<string, unknown> } {
|
|
28
|
+
const body = gui.component.props.body as { type: string; props: Record<string, unknown> }[];
|
|
29
|
+
return body.find((c) => c.type === type)!;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
describe("buildGoalGui", () => {
|
|
24
33
|
it("有 tokenBudget → card 含 progress-bar + stats-line", () => {
|
|
25
34
|
const gui = buildGoalGui(
|
|
@@ -31,36 +40,21 @@ describe("buildGoalGui", () => {
|
|
|
31
40
|
);
|
|
32
41
|
expect(gui.v).toBe(1);
|
|
33
42
|
expect(gui.component.type).toBe("card");
|
|
34
|
-
const
|
|
35
|
-
// body[0] = progress-bar(tokens), body[1] = stats-line
|
|
36
|
-
const tokenBar = body.find((c) => c.type === "progress-bar")!;
|
|
43
|
+
const tokenBar = findBodyComp(gui, "progress-bar");
|
|
37
44
|
expect(tokenBar.props).toMatchObject({ current: 4200, total: 10000, severity: "ok" });
|
|
38
|
-
const stats =
|
|
45
|
+
const stats = findBodyComp(gui, "stats-line");
|
|
39
46
|
expect(stats.props.items).toContainEqual(expect.objectContaining({ label: "status", value: "active" }));
|
|
40
47
|
});
|
|
41
48
|
|
|
42
|
-
it(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
expect(tokenBar.props.severity).toBe("danger");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("token 消耗 ≥70% → severity warn", () => {
|
|
55
|
-
const gui = buildGoalGui(
|
|
56
|
-
makeState({
|
|
57
|
-
tokensUsed: 7500,
|
|
58
|
-
budget: { tokenBudget: 10000 },
|
|
59
|
-
}),
|
|
60
|
-
);
|
|
61
|
-
const body = gui.component.props.body as { type: string; props: Record<string, unknown> }[];
|
|
62
|
-
const tokenBar = body.find((c) => c.type === "progress-bar")!;
|
|
63
|
-
expect(tokenBar.props.severity).toBe("warn");
|
|
49
|
+
it.each([
|
|
50
|
+
["9500/10000 ≥90%", 9500, "danger"],
|
|
51
|
+
["9000/10000 =90%", 9000, "danger"],
|
|
52
|
+
["7500/10000 ≥70%", 7500, "warn"],
|
|
53
|
+
["7000/10000 =70%", 7000, "warn"],
|
|
54
|
+
])("token 消耗 %s → severity %s(S#14 边界 ≥)", (_label, used, severity) => {
|
|
55
|
+
const gui = buildGoalGui(makeState({ tokensUsed: used, budget: { tokenBudget: 10000 } }));
|
|
56
|
+
const tokenBar = findBodyComp(gui, "progress-bar");
|
|
57
|
+
expect(tokenBar.props.severity).toBe(severity);
|
|
64
58
|
});
|
|
65
59
|
|
|
66
60
|
it("无 budget → stats-line 摘要", () => {
|
|
@@ -101,18 +95,6 @@ describe("buildGoalGui", () => {
|
|
|
101
95
|
expect(gui.component.props.variant).toBe("success");
|
|
102
96
|
});
|
|
103
97
|
|
|
104
|
-
it("time 进度条:≥90% → danger", () => {
|
|
105
|
-
const gui = buildGoalGui(
|
|
106
|
-
makeState({
|
|
107
|
-
timeUsedSeconds: 5 * 60 + 1, // 5.01 min / 5 min budget
|
|
108
|
-
budget: { timeBudgetMinutes: 5 },
|
|
109
|
-
}),
|
|
110
|
-
);
|
|
111
|
-
const body = gui.component.props.body as { type: string; props: { label?: string; severity?: string } }[];
|
|
112
|
-
const timeBar = body.find((c) => c.type === "progress-bar" && c.props.label === "time")!;
|
|
113
|
-
expect(timeBar.props.severity).toBe("danger");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
98
|
it("slug 缺省 → header 用 goalId 前 8 字符", () => {
|
|
117
99
|
const state = makeState({ slug: undefined, budget: { tokenBudget: 10000 } });
|
|
118
100
|
const gui = buildGoalGui(state);
|
|
@@ -121,59 +103,16 @@ describe("buildGoalGui", () => {
|
|
|
121
103
|
|
|
122
104
|
// ── S#2: statusSeverity 完整覆盖 ──
|
|
123
105
|
|
|
124
|
-
it(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const body = gui.component.props.body as { type: string; props: { items: Array<{ label: string; severity: string }> } }[];
|
|
135
|
-
const stats = body.find((c) => c.type === "stats-line")!;
|
|
136
|
-
const statusItem = stats.props.items.find((i) => i.label === "status")!;
|
|
137
|
-
expect(statusItem.severity).toBe("danger");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("cancelled 状态 → status severity danger(S#2)", () => {
|
|
141
|
-
const gui = buildGoalGui(makeState({ status: "cancelled", budget: { tokenBudget: 10000 } }));
|
|
142
|
-
const body = gui.component.props.body as { type: string; props: { items: Array<{ label: string; severity: string }> } }[];
|
|
143
|
-
const stats = body.find((c) => c.type === "stats-line")!;
|
|
144
|
-
const statusItem = stats.props.items.find((i) => i.label === "status")!;
|
|
145
|
-
expect(statusItem.severity).toBe("danger");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("paused 状态 → status severity warn(S#2)", () => {
|
|
149
|
-
const gui = buildGoalGui(makeState({ status: "paused", budget: { tokenBudget: 10000 } }));
|
|
150
|
-
const body = gui.component.props.body as { type: string; props: { items: Array<{ label: string; severity: string }> } }[];
|
|
151
|
-
const stats = body.find((c) => c.type === "stats-line")!;
|
|
152
|
-
const statusItem = stats.props.items.find((i) => i.label === "status")!;
|
|
153
|
-
expect(statusItem.severity).toBe("warn");
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// ── S#14: 阈值边界精确值 ──
|
|
157
|
-
|
|
158
|
-
it("token 消耗正好 90% → severity danger(边界 ≥,S#14)", () => {
|
|
159
|
-
const gui = buildGoalGui(makeState({ tokensUsed: 9000, budget: { tokenBudget: 10000 } }));
|
|
160
|
-
const body = gui.component.props.body as { type: string; props: { label?: string; severity?: string } }[];
|
|
161
|
-
const tokenBar = body.find((c) => c.type === "progress-bar" && c.props.label === "tokens")!;
|
|
162
|
-
expect(tokenBar.props.severity).toBe("danger");
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("token 消耗正好 70% → severity warn(边界 ≥,S#14)", () => {
|
|
166
|
-
const gui = buildGoalGui(makeState({ tokensUsed: 7000, budget: { tokenBudget: 10000 } }));
|
|
167
|
-
const body = gui.component.props.body as { type: string; props: { label?: string; severity?: string } }[];
|
|
168
|
-
const tokenBar = body.find((c) => c.type === "progress-bar" && c.props.label === "tokens")!;
|
|
169
|
-
expect(tokenBar.props.severity).toBe("warn");
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("time 消耗正好 70% → severity warn(边界 ≥,S#14)", () => {
|
|
173
|
-
const gui = buildGoalGui(makeState({ timeUsedSeconds: 7 * 60, budget: { timeBudgetMinutes: 10 } }));
|
|
174
|
-
const body = gui.component.props.body as { type: string; props: { label?: string; severity?: string } }[];
|
|
175
|
-
const timeBar = body.find((c) => c.type === "progress-bar" && c.props.label === "time")!;
|
|
176
|
-
expect(timeBar.props.severity).toBe("warn");
|
|
106
|
+
it.each([
|
|
107
|
+
["budget_limited", "danger"],
|
|
108
|
+
["cancelled", "danger"],
|
|
109
|
+
["paused", "warn"],
|
|
110
|
+
])("status %s → stats-line status severity %s(S#2)", (status, severity) => {
|
|
111
|
+
const gui = buildGoalGui(makeState({ status: status as GoalStatus, budget: { tokenBudget: 10000 } }));
|
|
112
|
+
const stats = findBodyComp(gui, "stats-line");
|
|
113
|
+
const items = stats.props.items as Array<{ label: string; severity?: string }>;
|
|
114
|
+
const statusItem = items.find((i) => i.label === "status")!;
|
|
115
|
+
expect(statusItem.severity).toBe(severity);
|
|
177
116
|
});
|
|
178
117
|
|
|
179
118
|
// ── I#1: tokenBudget=0 口径统一 ──
|