@zhushanwen/pi-goal 0.14.15 → 0.16.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 +5 -4
- package/src/__tests__/active-path.test.ts +2 -2
- package/src/__tests__/circuit-breaker.test.ts +10 -10
- package/src/__tests__/criteria-array-prompt-guidance.test.ts +2 -2
- package/src/__tests__/deserialize-state.test.ts +2 -2
- package/src/__tests__/event-adapter.test.ts +8 -8
- package/src/__tests__/goal-control-adapter.test.ts +6 -6
- package/src/__tests__/goal-control-rpc.test.ts +4 -4
- package/src/__tests__/goal-control-summary.test.ts +200 -0
- package/src/__tests__/index.test.ts +1 -1
- package/src/__tests__/liveness.test.ts +1 -1
- package/src/__tests__/message-end.test.ts +3 -3
- package/src/__tests__/ports.test.ts +1 -1
- package/src/__tests__/service.test.ts +2 -2
- package/src/adapters/command-adapter.ts +1 -1
- package/src/adapters/event-handlers/agent-end.ts +12 -12
- package/src/adapters/event-handlers/before-agent-start.ts +1 -1
- package/src/adapters/event-handlers/message-end.ts +3 -3
- package/src/adapters/event-handlers/session-shutdown.ts +1 -1
- package/src/adapters/event-handlers/turn-end.ts +3 -3
- package/src/adapters/goal-control-adapter.ts +83 -24
- package/src/adapters/ports.ts +2 -2
- package/src/constants.ts +1 -1
- package/src/engine/__tests__/budget.test.ts +1 -1
- package/src/engine/__tests__/goal.test.ts +64 -10
- package/src/engine/goal.ts +82 -6
- package/src/engine/liveness.ts +2 -2
- package/src/engine/types.ts +0 -13
- package/src/index.ts +3 -3
- package/src/persistence.ts +1 -1
- package/src/ports.ts +2 -2
- package/src/projection/__tests__/widget.test.ts +3 -3
- package/src/projection/widget.ts +2 -2
- package/src/service.ts +4 -4
- package/src/session.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-goal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token budget, agent-reported blocking, and steering templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -29,12 +29,13 @@
|
|
|
29
29
|
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
30
30
|
"@earendil-works/pi-tui": "^1.0.0",
|
|
31
31
|
"typebox": "1.3.27",
|
|
32
|
-
"@zhushanwen/pi-pending-notifications": "0.
|
|
32
|
+
"@zhushanwen/pi-pending-notifications": "0.10.1"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@zhushanwen/extension-protocol": "0.
|
|
35
|
+
"@zhushanwen/extension-protocol": "0.23.0",
|
|
36
36
|
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
37
|
-
"@zhushanwen/pi-session-path": "0.1.0"
|
|
37
|
+
"@zhushanwen/pi-session-path": "0.1.0",
|
|
38
|
+
"@zhushanwen/state-machine": "0.2.0"
|
|
38
39
|
},
|
|
39
40
|
"peerDependenciesMeta": {
|
|
40
41
|
"@earendil-works/pi-tui": {
|
|
@@ -127,7 +127,7 @@ interface HandlerFixture {
|
|
|
127
127
|
appendEntry: ReturnType<typeof vi.fn>;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/** 实例化 goal 工厂并捕获事件 handler(最小 pi
|
|
130
|
+
/** 实例化 goal 工厂并捕获事件 handler(最小 pi:注册点 vi.fn + 副作用侧 vi.fn) */
|
|
131
131
|
function makeHandlerFixture(): HandlerFixture {
|
|
132
132
|
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<unknown>>();
|
|
133
133
|
const pi = {
|
|
@@ -145,7 +145,7 @@ function makeHandlerFixture(): HandlerFixture {
|
|
|
145
145
|
return { handlers, sendMessage: pi.sendMessage, appendEntry: pi.appendEntry };
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/** 最小 ctx:sessionManager(分支树)+ ui
|
|
148
|
+
/** 最小 ctx:sessionManager(分支树)+ ui 记录侧 + getContextUsage(before_agent_start 用) */
|
|
149
149
|
function makeCtx(entries: SessionEntryLike[], leafId: string | null) {
|
|
150
150
|
const setWidget = vi.fn();
|
|
151
151
|
const setStatus = vi.fn();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 轮次活性熔断行为测试(chat-domain-v1x 设计 §3.2 D4 / W5,验收 §4 A10
|
|
2
|
+
* 轮次活性熔断行为测试(chat-domain-v1x 设计 §3.2 D4 / W5,验收 §4 A10 的单测)
|
|
3
3
|
*
|
|
4
4
|
* 四断言(对齐 impl-plan W5 验收条款②):
|
|
5
5
|
* a. 主判据 50 次封顶必停发——含「每轮调一次工具」序列同样被封顶(正交性:
|
|
@@ -43,7 +43,7 @@ interface FakeHarness {
|
|
|
43
43
|
entries: unknown[];
|
|
44
44
|
piCalls: RecordedCall[];
|
|
45
45
|
ctxCalls: RecordedCall[];
|
|
46
|
-
/** 可控 isIdle(MF-6①
|
|
46
|
+
/** 可控 isIdle(MF-6① 检查断言用:延迟窗内有活动 turn 时置 false) */
|
|
47
47
|
isIdle: { value: boolean };
|
|
48
48
|
/**
|
|
49
49
|
* 可控 appendEntry 抛错(MF-R2-1 测试前提修正):pi.appendEntry 与 ctx 同生命周期
|
|
@@ -336,11 +336,11 @@ async function enterBackoff(): Promise<{ h: FakeHarness; session: GoalSession }>
|
|
|
336
336
|
return { h, session };
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
describe("MF-6① idle
|
|
339
|
+
describe("MF-6① idle 检查:ctx.signal 实时求值失效场景由 isIdle 拦截", () => {
|
|
340
340
|
it("延迟窗内有活动 turn(isIdle=false、signal=undefined)→ 到期不误发", async () => {
|
|
341
341
|
const { h, session } = await enterBackoff();
|
|
342
342
|
|
|
343
|
-
// agent idle 后 SDK get signal 实时求值为 undefined(旧 signal?.aborted
|
|
343
|
+
// agent idle 后 SDK get signal 实时求值为 undefined(旧 signal?.aborted 检查恒放行);
|
|
344
344
|
// 用户在延迟窗内发新消息 → 活动 turn 占用 → isIdle=false
|
|
345
345
|
h.ctx.signal = undefined;
|
|
346
346
|
h.isIdle.value = false;
|
|
@@ -350,7 +350,7 @@ describe("MF-6① idle 守卫:ctx.signal 实时求值失效场景由 isIdle
|
|
|
350
350
|
expect(session.state!.continuationsSent).toBe(4);
|
|
351
351
|
});
|
|
352
352
|
|
|
353
|
-
it("延迟窗内 idle(isIdle=true、signal=undefined)→
|
|
353
|
+
it("延迟窗内 idle(isIdle=true、signal=undefined)→ 正常发出(检查不误伤退避路径)", async () => {
|
|
354
354
|
const { h, session } = await enterBackoff();
|
|
355
355
|
|
|
356
356
|
h.ctx.signal = undefined;
|
|
@@ -403,7 +403,7 @@ describe("MF-6② stale 保护:timer 回调内异常不冒泡(catch 块自
|
|
|
403
403
|
});
|
|
404
404
|
});
|
|
405
405
|
|
|
406
|
-
describe("MF-R2-1
|
|
406
|
+
describe("MF-R2-1 从根源修复:session_shutdown 取消旧退避 timer", () => {
|
|
407
407
|
it("session_shutdown(invalidate 前触发)后旧 timer 到期不发;全量 stale 下不崩、不冒泡", async () => {
|
|
408
408
|
const { h, session } = await enterBackoff();
|
|
409
409
|
|
|
@@ -430,7 +430,7 @@ describe("MF-R2-1 根修:session_shutdown 取消旧退避 timer", () => {
|
|
|
430
430
|
});
|
|
431
431
|
});
|
|
432
432
|
|
|
433
|
-
describe("MF-6③ timer
|
|
433
|
+
describe("MF-6③ timer 清理侧:session_start / before_agent_start 取消旧 timer", () => {
|
|
434
434
|
it("session_start 重建 → 旧退避 timer 被取消,到期不发", async () => {
|
|
435
435
|
const { h, session } = await enterBackoff();
|
|
436
436
|
|
|
@@ -459,13 +459,13 @@ describe("MF-6③ timer 清理面:session_start / before_agent_start 取消旧
|
|
|
459
459
|
// 端口改造前 backoff timer 到期伪装用户消息投递;改造后
|
|
460
460
|
// deliverContinuation 经端口发 custom message 且 options 含 triggerTurn: true——
|
|
461
461
|
// 非 streaming idle 场景真实开轮(改造前 append 不开轮 = 死消息)。
|
|
462
|
-
// isIdle=false 拦截见 MF-6
|
|
462
|
+
// isIdle=false 拦截见 MF-6①;封顶记账检查见「主判据」describe;此处锁定到期
|
|
463
463
|
// 发出路径的 options 契约与记账恰好 +1。
|
|
464
464
|
describe("A8:退避 timer 到期 → triggerTurn: true 开轮发出", () => {
|
|
465
|
-
it("isIdle
|
|
465
|
+
it("isIdle 检查通过后发出:custom message 四要素齐备,记账恰好 +1", async () => {
|
|
466
466
|
const { h, session } = await enterBackoff();
|
|
467
467
|
|
|
468
|
-
vi.advanceTimersByTime(20_000); // 2×base 到期,isIdle 默认 true →
|
|
468
|
+
vi.advanceTimersByTime(20_000); // 2×base 到期,isIdle 默认 true → 检查放行
|
|
469
469
|
|
|
470
470
|
const sends = followUpSends(h);
|
|
471
471
|
expect(sends).toHaveLength(5); // 4 立即 + 1 到期发出
|
|
@@ -48,12 +48,12 @@ const successCriteriaSchema = GoalControlParams.properties.successCriteria;
|
|
|
48
48
|
const PARAM_DESCRIPTION =
|
|
49
49
|
typeof successCriteriaSchema.description === "string" ? successCriteriaSchema.description : "";
|
|
50
50
|
|
|
51
|
-
// ── 第 2 层切片:工具 description 模板字面量("description: `"
|
|
51
|
+
// ── 第 2 层切片:工具 description 模板字面量("description: `" 到结束反引号)──
|
|
52
52
|
|
|
53
53
|
const TOOL_DESCRIPTION = sliceBetween(ADAPTER_SRC, "description: `", "`");
|
|
54
54
|
|
|
55
55
|
// ── 第 3 层切片:promptGuidelines 赋值块 ─────────────────────
|
|
56
|
-
//
|
|
56
|
+
// 结束 "]" 不能用 indexOf 找——块内 guideline 文本自身含 "string[]" 的 "]",
|
|
57
57
|
// 故按行扫描 trim() === "]," 的行作块尾(缩进无关)。
|
|
58
58
|
|
|
59
59
|
function extractPromptGuidelinesBlock(src: string): string {
|
|
@@ -181,9 +181,9 @@ describe("deserializeState — 旧数据迁移(time_limited → budget_limited
|
|
|
181
181
|
expect((state as unknown as { timeWarning90Sent?: unknown }).timeWarning90Sent).toBeUndefined();
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
it("归一化后是合法终态:/goal clear
|
|
184
|
+
it("归一化后是合法终态:/goal clear 检查(!isTerminalStatus)跳过 transitionStatus,不 throw", () => {
|
|
185
185
|
const state = deserializeState(LEGACY_DATA);
|
|
186
|
-
// handleClear(command-adapter.ts
|
|
186
|
+
// handleClear(command-adapter.ts)的检查:!isTerminalStatus 为 false → 直接 clearSession,
|
|
187
187
|
// 不再走 finalizeAndPersist("cancelled") 的 transitionStatus 查表(旧值会 throw)。
|
|
188
188
|
expect(isTerminalStatus(state.status)).toBe(true);
|
|
189
189
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* event-handlers 行为测试 — agent_end / before_agent_start 核心逻辑
|
|
3
3
|
*
|
|
4
|
-
* 覆盖 agent_end 的 continuation + ESC
|
|
4
|
+
* 覆盖 agent_end 的 continuation + ESC 检查(FR-6.7)+
|
|
5
5
|
* before_agent_start 的 AUTO_CLEAR/context wrap-up。
|
|
6
6
|
*
|
|
7
7
|
* 全解耦后不再测 allTasksDone followUp(原依赖 pi.__todoGetList,已删)。
|
|
@@ -100,9 +100,9 @@ function makeRunningState(overrides?: Partial<GoalRuntimeState>): GoalRuntimeSta
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// ── handleAgentEnd:ESC
|
|
103
|
+
// ── handleAgentEnd:ESC 检查 ──────────────────────────
|
|
104
104
|
|
|
105
|
-
describe("handleAgentEnd — FR-6.7 ESC
|
|
105
|
+
describe("handleAgentEnd — FR-6.7 ESC 检查", () => {
|
|
106
106
|
it("aborted=true:不发 continuation、不做 budget 检查、goal 保持 active", async () => {
|
|
107
107
|
const { pi, calls: piCalls, states } = makeFakePi();
|
|
108
108
|
const { ctx, calls: ctxCalls } = makeFakeCtx({ aborted: true });
|
|
@@ -174,9 +174,9 @@ describe("handleAgentEnd — continuation", () => {
|
|
|
174
174
|
expect(sendContext[0]!.content).toContain("[GOAL]");
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
// ── pending
|
|
177
|
+
// ── pending 检查:background subagent/workflow 运行时不死循环 ──
|
|
178
178
|
|
|
179
|
-
it("有活跃 pending → 不发 continuation(pending
|
|
179
|
+
it("有活跃 pending → 不发 continuation(pending 检查生效)", async () => {
|
|
180
180
|
const { pi, calls: piCalls } = makeFakePi();
|
|
181
181
|
const { ctx, calls: ctxCalls } = makeFakeCtx({
|
|
182
182
|
entries: [
|
|
@@ -258,7 +258,7 @@ describe("handleAgentEnd — continuation", () => {
|
|
|
258
258
|
expect(data?.ids).toEqual(["bg-1", "bg-2"]); // ids 透出
|
|
259
259
|
});
|
|
260
260
|
|
|
261
|
-
it("重复 register 同 id → 去重计为 1(seen.has
|
|
261
|
+
it("重复 register 同 id → 去重计为 1(seen.has 检查)", async () => {
|
|
262
262
|
const { pi, calls: piCalls, states } = makeFakePi();
|
|
263
263
|
const { ctx, calls: ctxCalls } = makeFakeCtx({
|
|
264
264
|
entries: [
|
|
@@ -272,7 +272,7 @@ describe("handleAgentEnd — continuation", () => {
|
|
|
272
272
|
await handleAgentEnd(pi, session, ctx);
|
|
273
273
|
const all = allCalls(piCalls, ctxCalls);
|
|
274
274
|
|
|
275
|
-
//
|
|
275
|
+
// 检查触发:count>0 不发 continuation
|
|
276
276
|
const followUp = all.filter(
|
|
277
277
|
(c) => c.kind === "sendContext" && (c.payload as { deliverAs?: string } | undefined)?.deliverAs === "followUp",
|
|
278
278
|
);
|
|
@@ -298,7 +298,7 @@ describe("handleAgentEnd — continuation", () => {
|
|
|
298
298
|
await handleAgentEnd(pi, session, ctx);
|
|
299
299
|
const all = allCalls(piCalls, ctxCalls);
|
|
300
300
|
|
|
301
|
-
// count=0
|
|
301
|
+
// count=0,检查不触发,走正常 continuation
|
|
302
302
|
const followUp = all.filter(
|
|
303
303
|
(c) => c.kind === "sendContext" && (c.payload as { deliverAs?: string } | undefined)?.deliverAs === "followUp",
|
|
304
304
|
);
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* goal_control adapter 测试 — handler 用 fake ports
|
|
3
3
|
*
|
|
4
4
|
* 覆盖 #3 验收:
|
|
5
|
-
* - complete: active
|
|
6
|
-
* - report_blocked: active
|
|
5
|
+
* - complete: active 检查 + evidence 必填 + finalizeAndPersist(status→complete + history)
|
|
6
|
+
* - report_blocked: active 检查 + reason 必填 + tickState + transitionStatus(active→blocked) + persistState
|
|
7
7
|
*
|
|
8
8
|
* 全解耦后不再测 todo 前置检查(checkCompletePrerequisites / buildProgressInput / findIncompleteTodos 已删)。
|
|
9
9
|
* complete 不做 todo 完成硬检查——AI 自行判断(prompt 软建议)。
|
|
@@ -74,7 +74,7 @@ const activeState = (overrides: Partial<GoalRuntimeState> = {}): GoalRuntimeStat
|
|
|
74
74
|
|
|
75
75
|
// ── handleComplete ───────────────────────────────────
|
|
76
76
|
|
|
77
|
-
describe("handleComplete — active
|
|
77
|
+
describe("handleComplete — active 检查 + evidence + finalizeAndPersist", () => {
|
|
78
78
|
it("active + evidence → status complete + 写 history", () => {
|
|
79
79
|
const session = createGoalSession();
|
|
80
80
|
session.state = activeState();
|
|
@@ -117,7 +117,7 @@ describe("handleComplete — active 守卫 + evidence + finalizeAndPersist", ()
|
|
|
117
117
|
|
|
118
118
|
// ── handleCreate ─────────────────────────────────────
|
|
119
119
|
|
|
120
|
-
describe("handleCreate — objective/successCriteria 必填(handler 运行时校验)+ slug 真 optional +
|
|
120
|
+
describe("handleCreate — objective/successCriteria 必填(handler 运行时校验)+ slug 真 optional + 非终态检查 + createGoal", () => {
|
|
121
121
|
it("无旧 goal + slug + objective → status active + slug 写入 + 持久化 + notify", () => {
|
|
122
122
|
const session = createGoalSession();
|
|
123
123
|
const ports = makeFakePorts();
|
|
@@ -175,7 +175,7 @@ describe("handleCreate — objective/successCriteria 必填(handler 运行时
|
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
const NON_TERMINAL_STATUSES: GoalRuntimeState["status"][] = ["active", "paused", "blocked"];
|
|
178
|
-
it.each(NON_TERMINAL_STATUSES)("已有 %s goal → throw(D25
|
|
178
|
+
it.each(NON_TERMINAL_STATUSES)("已有 %s goal → throw(D25 非终态检查)", (status) => {
|
|
179
179
|
const session = createGoalSession();
|
|
180
180
|
session.state = activeState({ status });
|
|
181
181
|
expect(() =>
|
|
@@ -232,7 +232,7 @@ describe("handleCreate — objective/successCriteria 必填(handler 运行时
|
|
|
232
232
|
|
|
233
233
|
// ── handleReportBlocked ──────────────────────────────
|
|
234
234
|
|
|
235
|
-
describe("handleReportBlocked — active
|
|
235
|
+
describe("handleReportBlocked — active 检查 + tick + transition + persist", () => {
|
|
236
236
|
it("active + reason → status blocked + reason 记录 + tick 累加", () => {
|
|
237
237
|
const session = createGoalSession();
|
|
238
238
|
const past = Date.now() - 4000;
|
|
@@ -17,7 +17,7 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
17
17
|
import { registerGoalControlTool, type GoalControlDetails } from "../adapters/goal-control-adapter";
|
|
18
18
|
import { createGoalSession } from "../session";
|
|
19
19
|
|
|
20
|
-
// ── mock:logger + updateWidget(UI
|
|
20
|
+
// ── mock:logger + updateWidget(UI 通道降级用例断言范围;其余用例下两者为 no-op,
|
|
21
21
|
// 与真实路径行为等价——fixture 的 setWidget/notify 本就是 no-op)──
|
|
22
22
|
const loggerWarnSpy = vi.hoisted(() => vi.fn());
|
|
23
23
|
vi.mock("@zhushanwen/pi-extension-logger", () => ({
|
|
@@ -150,9 +150,9 @@ describe("goal_control execute — RPC 模式 tool result 无 __gui__", () => {
|
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
it("RPC + session.state = null → 前置 handler throw(分支不可达)", async () => {
|
|
153
|
-
// execute 路径上,create 总会 set session.state;complete/report_blocked
|
|
153
|
+
// execute 路径上,create 总会 set session.state;complete/report_blocked 前置检查
|
|
154
154
|
// 要求 session.state 非 null 否则 throw——所以 session.state=null 时不会走到返回路径。
|
|
155
|
-
// 验证:全新 session(state=null)下,complete
|
|
155
|
+
// 验证:全新 session(state=null)下,complete 前置检查先 throw。
|
|
156
156
|
const { pi, ctx } = makeFixture("rpc");
|
|
157
157
|
let captured: CapturedTool | undefined;
|
|
158
158
|
const capturePi = {
|
|
@@ -163,7 +163,7 @@ describe("goal_control execute — RPC 模式 tool result 无 __gui__", () => {
|
|
|
163
163
|
} as unknown as ExtensionAPI;
|
|
164
164
|
registerGoalControlTool(capturePi, createGoalSession());
|
|
165
165
|
const tool2 = captured!;
|
|
166
|
-
// complete 在 state=null 时 throw(handleComplete
|
|
166
|
+
// complete 在 state=null 时 throw(handleComplete 前置检查)
|
|
167
167
|
await expect(
|
|
168
168
|
tool2.execute(
|
|
169
169
|
"id",
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goal_control 收起态摘要测试:buildGoalControlSummary 三分支文案 + execute 补齐
|
|
3
|
+
* details.summary + renderResult 折叠/展开/旧形态回退。
|
|
4
|
+
*
|
|
5
|
+
* mock 形态对齐 goal-control-rpc.test.ts(捕获 registerTool 后直接调 execute/renderResult)。
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { describe, expect, it, vi } from "vitest";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
buildGoalControlSummary,
|
|
12
|
+
registerGoalControlTool,
|
|
13
|
+
type GoalControlDetails,
|
|
14
|
+
} from "../adapters/goal-control-adapter";
|
|
15
|
+
import { createGoalSession } from "../session";
|
|
16
|
+
|
|
17
|
+
// ── mock:logger + updateWidget(与 rpc 测试同款 no-op)──
|
|
18
|
+
vi.mock("@zhushanwen/pi-extension-logger", () => ({
|
|
19
|
+
getLogger: () => ({ warn: vi.fn(), debug: vi.fn(), error: vi.fn() }),
|
|
20
|
+
}));
|
|
21
|
+
vi.mock("../projection/widget", () => ({ updateWidget: vi.fn() }));
|
|
22
|
+
|
|
23
|
+
// ── buildGoalControlSummary ──────────────────────────
|
|
24
|
+
|
|
25
|
+
describe("buildGoalControlSummary", () => {
|
|
26
|
+
it("create → Goal created: <slug>", () => {
|
|
27
|
+
const details = { action: "create", goalId: "g1", status: "active", slug: "refactor-auth" } as GoalControlDetails;
|
|
28
|
+
expect(buildGoalControlSummary({ action: "create" } as never, details)).toBe(
|
|
29
|
+
"Goal created: refactor-auth",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("create 无 slug → 省略后缀", () => {
|
|
34
|
+
const details = { action: "create", goalId: "g1", status: "active" } as GoalControlDetails;
|
|
35
|
+
expect(buildGoalControlSummary({ action: "create" } as never, details)).toBe("Goal created");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("complete → Goal completed: <goalId>(complete 的 details 无 slug)", () => {
|
|
39
|
+
const details = { action: "complete", goalId: "abc123", status: "complete" } as GoalControlDetails;
|
|
40
|
+
expect(buildGoalControlSummary({ action: "complete" } as never, details)).toBe(
|
|
41
|
+
"Goal completed: abc123",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("report_blocked → Blocked: <reason 首 40 字>(超长加省略号)", () => {
|
|
46
|
+
const details = { action: "report_blocked", goalId: "g1", status: "blocked" } as GoalControlDetails;
|
|
47
|
+
const params = { action: "report_blocked", reason: "等待上游 API 修复" } as never;
|
|
48
|
+
expect(buildGoalControlSummary(params, details)).toBe("Blocked: 等待上游 API 修复");
|
|
49
|
+
|
|
50
|
+
const long = "x".repeat(50);
|
|
51
|
+
expect(buildGoalControlSummary({ action: "report_blocked", reason: long } as never, details)).toBe(
|
|
52
|
+
`Blocked: ${"x".repeat(40)}…`,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("report_blocked 无 reason → 省略后缀", () => {
|
|
57
|
+
const details = { action: "report_blocked", goalId: "g1", status: "blocked" } as GoalControlDetails;
|
|
58
|
+
expect(buildGoalControlSummary({ action: "report_blocked" } as never, details)).toBe("Blocked");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ── execute 补齐 details.summary + renderResult ──────
|
|
63
|
+
|
|
64
|
+
interface CapturedTool {
|
|
65
|
+
name: string;
|
|
66
|
+
execute: (
|
|
67
|
+
toolCallId: string,
|
|
68
|
+
params: Record<string, unknown>,
|
|
69
|
+
signal: AbortSignal | undefined,
|
|
70
|
+
onUpdate: unknown,
|
|
71
|
+
ctx: ExtensionContext,
|
|
72
|
+
) => Promise<{ content: Array<{ type: "text"; text: string }>; details: GoalControlDetails }>;
|
|
73
|
+
renderResult: (result: unknown, options: { expanded: boolean }, theme: Theme) => { content: unknown };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function captureTool(): CapturedTool {
|
|
77
|
+
let captured: CapturedTool | undefined;
|
|
78
|
+
const pi = {
|
|
79
|
+
registerTool(tool: CapturedTool): void {
|
|
80
|
+
captured = tool;
|
|
81
|
+
},
|
|
82
|
+
on: () => {},
|
|
83
|
+
appendEntry: () => {},
|
|
84
|
+
notify: () => {},
|
|
85
|
+
} as unknown as ExtensionAPI;
|
|
86
|
+
registerGoalControlTool(pi, createGoalSession());
|
|
87
|
+
if (!captured) throw new Error("registerGoalControlTool did not register a tool");
|
|
88
|
+
return captured;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ctx = {
|
|
92
|
+
mode: "rpc",
|
|
93
|
+
hasUI: true,
|
|
94
|
+
ui: {
|
|
95
|
+
theme: { fg: (_c: string, t: string) => t, bold: (t: string) => t },
|
|
96
|
+
setWidget: () => {},
|
|
97
|
+
setStatus: () => {},
|
|
98
|
+
notify: () => {},
|
|
99
|
+
},
|
|
100
|
+
sessionManager: { getEntries: () => [] },
|
|
101
|
+
} as unknown as ExtensionContext;
|
|
102
|
+
|
|
103
|
+
describe("goal_control execute details.summary", () => {
|
|
104
|
+
it("create → details 带现有字段 + summary", async () => {
|
|
105
|
+
const tool = captureTool();
|
|
106
|
+
const res = await tool.execute(
|
|
107
|
+
"id",
|
|
108
|
+
{
|
|
109
|
+
action: "create",
|
|
110
|
+
slug: "refactor-auth",
|
|
111
|
+
objective: "refactor the auth module",
|
|
112
|
+
successCriteria: ["tests pass"],
|
|
113
|
+
},
|
|
114
|
+
undefined,
|
|
115
|
+
undefined,
|
|
116
|
+
ctx,
|
|
117
|
+
);
|
|
118
|
+
expect(res.details).toMatchObject({
|
|
119
|
+
action: "create",
|
|
120
|
+
status: "active",
|
|
121
|
+
slug: "refactor-auth",
|
|
122
|
+
summary: "Goal created: refactor-auth",
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("report_blocked → details.summary 带 reason 首段", async () => {
|
|
127
|
+
const tool = captureTool();
|
|
128
|
+
const create = await tool.execute(
|
|
129
|
+
"id",
|
|
130
|
+
{ action: "create", slug: "g", objective: "obj", successCriteria: ["c"] },
|
|
131
|
+
undefined,
|
|
132
|
+
undefined,
|
|
133
|
+
ctx,
|
|
134
|
+
);
|
|
135
|
+
void create;
|
|
136
|
+
const res = await tool.execute(
|
|
137
|
+
"id",
|
|
138
|
+
{ action: "report_blocked", reason: "blocked by missing credentials" },
|
|
139
|
+
undefined,
|
|
140
|
+
undefined,
|
|
141
|
+
ctx,
|
|
142
|
+
);
|
|
143
|
+
expect(res.details).toMatchObject({
|
|
144
|
+
action: "report_blocked",
|
|
145
|
+
status: "blocked",
|
|
146
|
+
summary: "Blocked: blocked by missing credentials",
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ── renderResult 分态 ────────────────────────────────
|
|
152
|
+
|
|
153
|
+
const mockTheme = {
|
|
154
|
+
fg: (_color: string, text: string) => text,
|
|
155
|
+
bg: (_color: string, text: string) => text,
|
|
156
|
+
bold: (text: string) => text,
|
|
157
|
+
italic: (text: string) => text,
|
|
158
|
+
underline: (text: string) => text,
|
|
159
|
+
inverse: (text: string) => text,
|
|
160
|
+
strikethrough: (text: string) => text,
|
|
161
|
+
getFgAnsi: (_color: string) => "",
|
|
162
|
+
getBgAnsi: (_color: string) => "",
|
|
163
|
+
getColorMode: () => "truecolor" as const,
|
|
164
|
+
getThinkingBorderColor: () => (text: string) => text,
|
|
165
|
+
getBashModeBorderColor: () => (text: string) => text,
|
|
166
|
+
} as unknown as Theme;
|
|
167
|
+
|
|
168
|
+
describe("goal_control renderResult", () => {
|
|
169
|
+
const tool = captureTool();
|
|
170
|
+
|
|
171
|
+
it("折叠态 → details.summary 一行(dim)", () => {
|
|
172
|
+
const result = {
|
|
173
|
+
content: [{ type: "text", text: "Goal created.\nGoal ID: g1" }],
|
|
174
|
+
details: { action: "create", goalId: "g1", status: "active", slug: "refactor-auth", summary: "Goal created: refactor-auth" },
|
|
175
|
+
};
|
|
176
|
+
expect(tool.renderResult(result, { expanded: false }, mockTheme).content).toBe(
|
|
177
|
+
"Goal created: refactor-auth",
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("展开态 → content 纯文本(多行)", () => {
|
|
182
|
+
const result = {
|
|
183
|
+
content: [{ type: "text", text: "Goal created.\nGoal ID: g1" }],
|
|
184
|
+
details: { action: "create", goalId: "g1", status: "active", slug: "refactor-auth", summary: "Goal created: refactor-auth" },
|
|
185
|
+
};
|
|
186
|
+
expect(tool.renderResult(result, { expanded: true }, mockTheme).content).toBe(
|
|
187
|
+
"Goal created.\nGoal ID: g1",
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("旧 entry 无 summary → 折叠态回退 ◆ 状态行", () => {
|
|
192
|
+
const result = {
|
|
193
|
+
content: [{ type: "text", text: "Goal created." }],
|
|
194
|
+
details: { action: "complete", goalId: "g1", status: "complete" },
|
|
195
|
+
};
|
|
196
|
+
expect(tool.renderResult(result, { expanded: false }, mockTheme).content).toBe(
|
|
197
|
+
"◆ Goal Completed",
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* index.ts 测试 — 工厂入口 + 跨扩展 API(goalInit slot)
|
|
3
3
|
*
|
|
4
4
|
* 覆盖 T1.8 (NFR-AC-8):goalInit 签名 (objective, budget, ctx, slug?, successCriteria?),
|
|
5
|
-
* 结构上无法接收 tasks(task CRUD 已删除,D-16/FR-4 双轨消除)。active
|
|
5
|
+
* 结构上无法接收 tasks(task CRUD 已删除,D-16/FR-4 双轨消除)。active 检查由 service.test.ts 覆盖。
|
|
6
6
|
*
|
|
7
7
|
* 用最小 fake pi + fake ctx 实例化 goalExtension 工厂,再从 globalThis slot 读取 goalInit——
|
|
8
8
|
* mock 形态与真实通道同构(goal-bridge-cross-extension.md §3.4:桥的元教训是 mock 世界
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* engine/liveness.ts 纯函数测试 — 轮次活性熔断判定单元(chat-domain-v1x D4 / W5)
|
|
3
3
|
*
|
|
4
4
|
* 覆盖:env 配置解析(合法/非法/缺省)、退避等级数学(第 N 轮起 ×2、指数天花板)、
|
|
5
|
-
* 进展信号边界、toolCall 块计数(duck-typed
|
|
5
|
+
* 进展信号边界、toolCall 块计数(duck-typed 检查)、pending 集合变化判定。
|
|
6
6
|
* 行为级四断言(封顶/退避/恢复/defer 去重经 handleAgentEnd)见 circuit-breaker.test.ts。
|
|
7
7
|
*/
|
|
8
8
|
import { describe, expect, it } from "vitest";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* message_end handler 测试(D13① role 过滤 + FR-6.7 ESC
|
|
2
|
+
* message_end handler 测试(D13① role 过滤 + FR-6.7 ESC 检查)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 断言范围:仅 user/assistant 的 message_end 进入 service 消费;system(pi 1.0
|
|
5
5
|
* 新形态)/ toolResult / 缺 role 在 handler 层显式拒绝;assistant 正常透传。
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -73,7 +73,7 @@ describe("handleMessageEnd(D13① role 过滤)", () => {
|
|
|
73
73
|
expect(spy).not.toHaveBeenCalled();
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
it("ESC
|
|
76
|
+
it("ESC 检查先于 role 过滤(aborted 直接返回)", async () => {
|
|
77
77
|
const session = makeSession();
|
|
78
78
|
const spy = vi.mocked(applyEvent);
|
|
79
79
|
await handleMessageEnd(session, ctx(true), messageEndEvent("assistant"));
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - tui 模式:text 臂透传(pi 原生文本行,无 marker)
|
|
7
7
|
* - undefined:清除语义(模式无关)
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 模式分派检查单点在 protocol helpers.ts(检查单点化),本测试只验证委托接线。
|
|
10
10
|
*
|
|
11
11
|
* 运行:cd extensions/universal/goal && npx vitest run src/__tests__/ports.test.ts
|
|
12
12
|
*/
|
|
@@ -123,7 +123,7 @@ describe("finalizeGoal — history 写入矩阵", () => {
|
|
|
123
123
|
const ports = makeFakePorts();
|
|
124
124
|
const state = makeState();
|
|
125
125
|
state.status = "complete";
|
|
126
|
-
// transitionStatus 查表:终态不可转,调用方须先 isTerminalStatus
|
|
126
|
+
// transitionStatus 查表:终态不可转,调用方须先 isTerminalStatus 检查
|
|
127
127
|
expect(() => finalizeGoal(state, "cancelled", ports)).toThrow();
|
|
128
128
|
expect(state.status).toBe("complete");
|
|
129
129
|
});
|
|
@@ -175,7 +175,7 @@ describe("applyEvent — 简单事件", () => {
|
|
|
175
175
|
usage: { input: 100, output: 50, cacheRead: 20 },
|
|
176
176
|
},
|
|
177
177
|
});
|
|
178
|
-
// 回归修复:blocked 状态不累加 token(原 bug:缺 isActiveStatus
|
|
178
|
+
// 回归修复:blocked 状态不累加 token(原 bug:缺 isActiveStatus 检查)
|
|
179
179
|
expect(session.state.tokensUsed).toBe(before);
|
|
180
180
|
});
|
|
181
181
|
|
|
@@ -353,7 +353,7 @@ function handleUpdate(
|
|
|
353
353
|
* 这样 goal 创建的唯一路径是 goal_control toolcall(统一入口),
|
|
354
354
|
* slug/objective/budget 都由 AI 在 toolcall 时决定。
|
|
355
355
|
*
|
|
356
|
-
* D25
|
|
356
|
+
* D25 检查仍在此处预检:非终态旧 goal(active/paused/blocked)→ 拒绝,
|
|
357
357
|
* 提示 /goal resume 或 /goal clear(避免 AI 在已有未完成 goal 时重复创建)。
|
|
358
358
|
*
|
|
359
359
|
* budget flag(--tokens)写入消息体,让 AI 原样传给 create。
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 事件 6: agent_end(FR-8.7 + ESC
|
|
2
|
+
* 事件 6: agent_end(FR-8.7 + ESC 检查 + 并发保护)。
|
|
3
3
|
*
|
|
4
4
|
* 关键约定:
|
|
5
5
|
* - FR-8.2 G-021 防重入:session.isProcessing 入口加锁,finally 释放
|
|
6
6
|
* - FR-8.2 G-020 stale 快照:入口 makeStaleChecker snapshot goalId,每个副作用前 checkStale
|
|
7
|
-
* - FR-6.7 ESC
|
|
7
|
+
* - FR-6.7 ESC 检查(最关键):ctx.signal?.aborted → 不发 continuation、不做 budget 检查、
|
|
8
8
|
* 不做任何状态变更,goal 保持 active,等用户下次输入恢复
|
|
9
9
|
*
|
|
10
10
|
* #8 后流程:budget 预警/steering → continuation 去抖 + 轮次活性熔断(W5:
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 全解耦:不再做 allTasksDone followUp(原依赖 pi.__todoGetList,跨 ext 失效)。
|
|
16
16
|
* todo 是否全完成由 AI 自行判断(prompt 软建议)。
|
|
17
17
|
*
|
|
18
|
-
* ESC 路径:aborted 时直接 return(goal 保持 active)。注意 ESC
|
|
18
|
+
* ESC 路径:aborted 时直接 return(goal 保持 active)。注意 ESC 检查在终态/非 active
|
|
19
19
|
* 检查之后——终态 goal 仍走终态 notify(不被 ESC 影响),非 active 状态直接返回。
|
|
20
20
|
*/
|
|
21
21
|
|
|
@@ -63,7 +63,7 @@ export async function handleAgentEnd(
|
|
|
63
63
|
}
|
|
64
64
|
if (!isActiveStatus(session.state.status)) return;
|
|
65
65
|
|
|
66
|
-
// FR-6.7 ESC
|
|
66
|
+
// FR-6.7 ESC 检查(最关键):aborted 时 goal 保持 active,不做任何副作用
|
|
67
67
|
if (ctx.signal?.aborted) {
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
@@ -152,7 +152,7 @@ async function handleBudgetChecks(
|
|
|
152
152
|
* - W5 主判据:continuation 连发总次数封顶(默认 50,PI_GOAL_CONTINUATION_CAP)——
|
|
153
153
|
* 封顶必停发 + notify 用户 + goal 保持 active(不擅自终态)。与「是否调工具」
|
|
154
154
|
* 正交:计数只在 deliverContinuation 实发时 +1,绝不被工具调用清零
|
|
155
|
-
* (2026-09-08 事故:pending
|
|
155
|
+
* (2026-09-08 事故:pending 检查击穿后 64 分钟 ~320 turn 空转,无任何熔断)。
|
|
156
156
|
* - W5 辅判据:连续 N 轮(默认 5)无工具调用且 tokenDelta 低于阈值 → continuation
|
|
157
157
|
* 延迟 base×2^level 发出(间隔 ×2 递增);真实进展清零退避计数(不动总次数)。
|
|
158
158
|
* - W5 defer 通知去重:pending 集合不变的连续 defer 轮不重复 notify。
|
|
@@ -188,7 +188,7 @@ async function handleContinuation(
|
|
|
188
188
|
isProgressSignal(toolDelta, tokenDelta, cfg),
|
|
189
189
|
);
|
|
190
190
|
|
|
191
|
-
// pending
|
|
191
|
+
// pending 检查:有活跃 background subagent/workflow 时不发 continuation。
|
|
192
192
|
// 靠 subagent/workflow 完成时 sendMessage({triggerTurn:true},缺省 deliverAs 即 steer) 自然唤醒主 agent;
|
|
193
193
|
// goal 抢先催会与异步任务完成通知叠加,形成“停不下来”的死循环——continuation 排入
|
|
194
194
|
// _followUpMessages 队列 → _handlePostAgentRun 的 hasQueuedMessages() 返回 true → agent.continue()
|
|
@@ -196,8 +196,8 @@ async function handleContinuation(
|
|
|
196
196
|
// 刻意不校验 expiresAt:长任务 subagent(>1h TTL)完成时仍 triggerTurn 唤醒主 agent,
|
|
197
197
|
// 按 TTL 判非活跃会让死循环在长任务场景复现。
|
|
198
198
|
// [W4 读侧过滤①消费口] 传当前 session 基准:fork 继承的父级注册残留不进差集
|
|
199
|
-
//
|
|
200
|
-
// registry-fork-filter.test.ts 证不传则虚增,A9②
|
|
199
|
+
// (检查不幻 defer);设计 D4「读侧过滤一刀」在本消费口的落地(W6 探针实测
|
|
200
|
+
// registry-fork-filter.test.ts 证不传则虚增,A9② 验收标准)。
|
|
201
201
|
const pendingOps = countActiveFromEntries(entries, {
|
|
202
202
|
currentSessionId: ctx.sessionManager.getSessionId(),
|
|
203
203
|
});
|
|
@@ -239,7 +239,7 @@ async function handleContinuation(
|
|
|
239
239
|
// 空 turn:已 persist,不发 continuation
|
|
240
240
|
return;
|
|
241
241
|
}
|
|
242
|
-
//
|
|
242
|
+
// 终态检查:persistAndUpdate 可能把 goal 转为 budget_limited 终态。
|
|
243
243
|
// 此时不应发 continuation(deliverAs:"followUp" 会触发新 turn,让已耗尽预算的 agent 再跑一轮)。
|
|
244
244
|
if (isTerminalStatus(state.status)) return;
|
|
245
245
|
|
|
@@ -319,11 +319,11 @@ async function handleContinuation(
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
/**
|
|
322
|
-
* 退避 timer
|
|
322
|
+
* 退避 timer 到期后的发射前检查 + 实发(延迟期间状态可能已变):
|
|
323
323
|
* - goal 未被覆盖/清除(goalId 比对)、仍 active
|
|
324
|
-
* - MF-6① idle
|
|
324
|
+
* - MF-6① idle 检查:ctx.signal 为实时求值(pi-agent-core agent.js get signal 返回
|
|
325
325
|
* activeRun?.abortController.signal),agent idle 后恒 undefined,`signal?.aborted`
|
|
326
|
-
*
|
|
326
|
+
* 恒放行、不可作检查;改用 ctx.isIdle()(SDK types.d.ts L232)——延迟窗内有活动
|
|
327
327
|
* turn 时不发,该轮 agent_end 会按最新状态重新决策
|
|
328
328
|
* - 未封顶、无活跃 pending(等待期新出现的后台任务由其完成通知唤醒,goal 不抢先催)
|
|
329
329
|
*/
|