@zhushanwen/pi-todo 0.4.0 → 0.4.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-todo",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "AI-driven todo list for Pi — stateful task management with session persistence and /todos command.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -28,7 +28,7 @@
28
28
  "@xyz-agent/extension-protocol": "^0.2.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@mariozechner/pi-coding-agent": "*",
31
+ "@earendil-works/pi-coding-agent": "*",
32
32
  "@earendil-works/pi-tui": "*",
33
33
  "@earendil-works/pi-ai": "*",
34
34
  "@sinclair/typebox": "*"
@@ -1,4 +1,4 @@
1
- import type { Theme } from "@mariozechner/pi-coding-agent";
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { describe, expect, it } from "vitest";
3
3
 
4
4
  import {
@@ -0,0 +1,56 @@
1
+ // Behavioral tests for todo dual-form traps (text/texts, id/ids).
2
+ //
3
+ // Complements the source-text prompt-quality locks in tool-prompt.test.ts: those
4
+ // verify the Correct/error STRINGS exist; these exercise the actual throw logic of
5
+ // handleAdd/handleDelete, so a refactor cannot silently drop the dual-form detection.
6
+ //
7
+ // handleAdd/handleDelete were exported specifically to enable these tests.
8
+
9
+ import { describe, expect, it } from "vitest";
10
+
11
+ import { createTodoSessionState } from "../state";
12
+ import { handleAdd, handleDelete } from "../tool";
13
+
14
+ describe("handleAdd — text/texts dual-form detection", () => {
15
+ it("triggers dual-form error when singular 'text' used instead of 'texts'", () => {
16
+ const state = createTodoSessionState();
17
+ expect(() => handleAdd(state, { action: "add", text: "write spec" })).toThrow(
18
+ /singular "text"|add needs texts/,
19
+ );
20
+ });
21
+
22
+ it("throws 'requires texts' when neither text nor texts given", () => {
23
+ const state = createTodoSessionState();
24
+ expect(() => handleAdd(state, { action: "add" })).toThrow(/requires texts/);
25
+ });
26
+
27
+ it("throws 'requires texts' on empty array (missing, not dual-form)", () => {
28
+ const state = createTodoSessionState();
29
+ expect(() => handleAdd(state, { action: "add", texts: [] })).toThrow(/requires texts/);
30
+ });
31
+
32
+ it("does NOT throw when correct 'texts' array provided", () => {
33
+ const state = createTodoSessionState();
34
+ expect(() => handleAdd(state, { action: "add", texts: ["write spec"] })).not.toThrow();
35
+ });
36
+ });
37
+
38
+ describe("handleDelete — id/ids dual-form detection", () => {
39
+ it("triggers dual-form error when singular 'id' used instead of 'ids'", () => {
40
+ const state = createTodoSessionState();
41
+ expect(() => handleDelete(state, { action: "delete", id: 5 })).toThrow(
42
+ /singular "id"|delete needs ids/,
43
+ );
44
+ });
45
+
46
+ it("throws 'requires ids' when neither id nor ids given", () => {
47
+ const state = createTodoSessionState();
48
+ expect(() => handleDelete(state, { action: "delete" })).toThrow(/requires ids/);
49
+ });
50
+
51
+ it("does NOT throw when correct 'ids' array provided (after seeding a todo)", () => {
52
+ const state = createTodoSessionState();
53
+ handleAdd(state, { action: "add", texts: ["temp"] }); // seed todo #1
54
+ expect(() => handleDelete(state, { action: "delete", ids: [1] })).not.toThrow();
55
+ });
56
+ });
@@ -0,0 +1,105 @@
1
+ // 提示词质量回归:todo tool 的 description 与 runtime 纠错文案必须是
2
+ // "弱模型友好"的——条件必填字段要给完整 JSON 正例,双形陷阱(text/texts、
3
+ // id/ids)要给消歧反例,失败后 throw 要带 Correct 正例让模型自我纠正。
4
+ //
5
+ // 本测试用源码文本断言锁定这些约束,防止后续重构把正例/反例/纠错文案删掉或
6
+ // 弱化。读源码而非 import,避免 mock 链(tool.ts 依赖 typebox/ExtensionAPI/
7
+ // Theme 等值导入)。参考 subagent-workflow 的 prompt 回归范式。
8
+
9
+ import { readFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ import { describe, expect, it } from "vitest";
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const TOOL_SRC = readFileSync(join(__dirname, "../tool.ts"), "utf-8");
17
+
18
+ /**
19
+ * 提取 description 拼接区(从 `description:` 到下一个 `promptSnippet:`)。
20
+ * tool.ts 的 description 用字符串拼接(非模板字面量),故整段截取后做子串断言。
21
+ */
22
+ function extractDescriptionRegion(src: string): string {
23
+ // 锚定到 registerTodoTool 内的 tool description,而非 schema 字段的 description:
24
+ // (Type.String({ description: "..." }) 等 schema 字段会先被 indexOf 命中,导致
25
+ // 捕获区域含 schema 描述 + tool 描述——虽当前子串唯一能过但 fragile to restructuring)。
26
+ const regIdx = src.indexOf("registerTodoTool");
27
+ if (regIdx === -1) throw new Error("registerTodoTool not found in tool.ts");
28
+ const start = src.indexOf("description:", regIdx);
29
+ if (start === -1) throw new Error("tool description: not found in registerTodoTool");
30
+ const end = src.indexOf("promptSnippet:", start);
31
+ if (end === -1) throw new Error("promptSnippet: not found in tool.ts");
32
+ return src.slice(start, end);
33
+ }
34
+
35
+ const DESCRIPTION_REGION = extractDescriptionRegion(TOOL_SRC);
36
+
37
+ // ── description 必须给完整 JSON 正例 ─────────────────
38
+
39
+ describe("todo description — 给完整 JSON 正例", () => {
40
+ it("add 正例:含 {\"action\":\"add\",\"texts\"", () => {
41
+ // 弱模型 add 时易误用单数 text;正例必须显式 texts 数组。
42
+ expect(DESCRIPTION_REGION).toContain('{"action":"add","texts"');
43
+ });
44
+
45
+ it("add+verification 正例:含 isVerification", () => {
46
+ expect(DESCRIPTION_REGION).toContain('"isVerification":true');
47
+ });
48
+
49
+ it("update single 正例:含 {\"action\":\"update\",\"id\"", () => {
50
+ expect(DESCRIPTION_REGION).toContain('{"action":"update","id"');
51
+ });
52
+
53
+ it("update batch 正例:含 {\"action\":\"update\",\"updates\"", () => {
54
+ // updates[] 批量路径是 [批量优先] 规范的核心,正例不可缺。
55
+ expect(DESCRIPTION_REGION).toContain('{"action":"update","updates"');
56
+ });
57
+
58
+ it("delete 正例:含 {\"action\":\"delete\",\"ids\"", () => {
59
+ // 弱模型 delete 时易误用单数 id;正例必须显式 ids 数组。
60
+ expect(DESCRIPTION_REGION).toContain('{"action":"delete","ids"');
61
+ });
62
+ });
63
+
64
+ // ── description 必须给双形陷阱反例(消歧) ──────────
65
+
66
+ describe("todo description — 双形陷阱反例(消歧单复数)", () => {
67
+ it("Don't 段存在", () => {
68
+ expect(DESCRIPTION_REGION).toMatch(/Don't/);
69
+ });
70
+
71
+ it("反例标注:text 属于 update(add 用 texts)", () => {
72
+ // 反例必须含消歧词,告诉模型 text 属于 update 而非 add。
73
+ expect(DESCRIPTION_REGION).toContain("text is for update");
74
+ expect(DESCRIPTION_REGION).toContain("add uses texts");
75
+ });
76
+
77
+ it("反例标注:id 属于 update(delete 用 ids)", () => {
78
+ expect(DESCRIPTION_REGION).toContain("id is for update");
79
+ expect(DESCRIPTION_REGION).toContain("delete uses ids");
80
+ });
81
+
82
+ it("反例标注:update 缺 id", () => {
83
+ expect(DESCRIPTION_REGION).toContain("missing id");
84
+ });
85
+ });
86
+
87
+ // ── runtime throw 必须带 Correct 纠错正例 ───────────
88
+
89
+ describe("todo runtime — throw 含 Correct 纠错正例", () => {
90
+ it("源码含 ≥4 处 Correct: 纠错文案(覆盖 add/delete/update 路径)", () => {
91
+ // 每个 required throw 必须追加完整 JSON 正例,弱模型失败后能自我纠正。
92
+ const matches = TOOL_SRC.match(/Correct:/g) || [];
93
+ expect(matches.length).toBeGreaterThanOrEqual(4);
94
+ });
95
+
96
+ it('add 双形检测:含 singular "text" 纠错文案', () => {
97
+ expect(TOOL_SRC).toContain('singular "text"');
98
+ expect(TOOL_SRC).toContain('"text" — that field is for update');
99
+ });
100
+
101
+ it('delete 双形检测:含 singular "id" 纠错文案', () => {
102
+ expect(TOOL_SRC).toContain('singular "id"');
103
+ expect(TOOL_SRC).toContain('"id" — that field is for update');
104
+ });
105
+ });
@@ -6,8 +6,8 @@
6
6
  * 已注册 tool,再以不同 ctx.mode 调 execute。每个用例新建 state(隔离),
7
7
  * 无模块级状态需重置。
8
8
  */
9
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
10
- import type { Theme } from "@mariozechner/pi-coding-agent";
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import type { Theme } from "@earendil-works/pi-coding-agent";
11
11
  import { describe, expect, it } from "vitest";
12
12
 
13
13
  import { createTodoSessionState, type TodoSessionState } from "../state";
package/src/commands.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * 用户在 TUI 中不可见。
7
7
  */
8
8
 
9
- import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
9
+ import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
10
10
 
11
11
  import type { TodoSessionState } from "./state";
12
12
  import { TodoListComponent } from "./component";
package/src/component.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * /todos 命令的 TUI 组件 — 独立可关闭的 todo 列表视图(双列布局)。
3
3
  */
4
4
 
5
- import type { Theme } from "@mariozechner/pi-coding-agent";
5
+ import type { Theme } from "@earendil-works/pi-coding-agent";
6
6
  import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
7
7
 
8
8
  import type { Todo } from "./model";
package/src/handlers.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * before_agent_start / agent_end。
4
4
  */
5
5
 
6
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
6
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
7
 
8
8
  import {
9
9
  migrateTodo,
package/src/index.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  * model 层纯函数返回 Result 对象(合法),dispatcher 拿到 error 时 throw。
21
21
  */
22
22
 
23
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
23
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
24
24
 
25
25
  import { registerTodosCommand } from "./commands";
26
26
  import { registerTodoEventHandlers } from "./handlers";
package/src/render.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Todo 渲染函数 — 状态栏、widget(双列)、tool result 渲染。
3
3
  */
4
4
 
5
- import type { Theme } from "@mariozechner/pi-coding-agent";
5
+ import type { Theme } from "@earendil-works/pi-coding-agent";
6
6
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
7
7
 
8
8
  import {
package/src/tool.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { StringEnum } from "@earendil-works/pi-ai";
6
6
  import { Text } from "@earendil-works/pi-tui";
7
- import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
7
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
8
8
  import { type Static, Type } from "typebox";
9
9
 
10
10
  import {
@@ -75,9 +75,18 @@ function handleList(state: TodoSessionState): string {
75
75
  }
76
76
 
77
77
  /** add action — 失败抛错 */
78
- function handleAdd(state: TodoSessionState, params: TodoActionParams): string {
78
+ /** add action 失败抛错。export behavioral 测试(text/texts 双形陷阱检测)。 */
79
+ export function handleAdd(state: TodoSessionState, params: TodoActionParams): string {
79
80
  if (!params.texts || params.texts.length === 0) {
80
- throw new Error("add requires texts parameter (non-empty array)");
81
+ // 双形陷阱:弱模型 add 时误用单数 text(那是 update 的字段)
82
+ if (params.text !== undefined) {
83
+ throw new Error(
84
+ 'add needs texts (array). You passed singular "text" — that field is for update. Correct: {"action":"add","texts":["<your text>"]}',
85
+ );
86
+ }
87
+ throw new Error(
88
+ 'add requires texts parameter (non-empty array). Correct: {"action":"add","texts":["..."]}',
89
+ );
81
90
  }
82
91
  const r = addTodos(state.todos, state.nextId, params.texts, params.isVerification);
83
92
  if (r.error) throw new Error(r.resultText);
@@ -96,9 +105,14 @@ function handleBatchUpdate(state: TodoSessionState, params: TodoActionParams): s
96
105
 
97
106
  /** update action: single — 失败抛错 */
98
107
  export function handleSingleUpdate(state: TodoSessionState, params: TodoActionParams): string {
99
- if (params.id === undefined) throw new Error("update requires id parameter");
108
+ if (params.id === undefined)
109
+ throw new Error(
110
+ 'update requires id parameter. Correct: {"action":"update","id":<n>,"status":"in_progress"}',
111
+ );
100
112
  if (params.status === undefined && params.text === undefined)
101
- throw new Error("update requires at least status or text parameter");
113
+ throw new Error(
114
+ 'update requires at least status or text parameter. Correct: {"action":"update","id":<n>,"status":"in_progress"}',
115
+ );
102
116
  if (params.text !== undefined && params.text === "") throw new Error("text cannot be empty string");
103
117
  if (
104
118
  params.status !== undefined &&
@@ -140,9 +154,18 @@ function handleUpdate(state: TodoSessionState, params: TodoActionParams): string
140
154
  }
141
155
 
142
156
  /** delete action — 失败抛错;部分 id 缺失则整体拒绝(原子性) */
143
- function handleDelete(state: TodoSessionState, params: TodoActionParams): string {
157
+ /** delete action 失败抛错。export behavioral 测试(id/ids 双形陷阱检测)。 */
158
+ export function handleDelete(state: TodoSessionState, params: TodoActionParams): string {
144
159
  if (!params.ids || params.ids.length === 0) {
145
- throw new Error("delete requires ids parameter (non-empty array)");
160
+ // 双形陷阱:弱模型 delete 时误用单数 id(那是 update 的字段)
161
+ if (params.id !== undefined) {
162
+ throw new Error(
163
+ 'delete needs ids (array). You passed singular "id" — that field is for update. Correct: {"action":"delete","ids":[<your id>]}',
164
+ );
165
+ }
166
+ throw new Error(
167
+ 'delete requires ids parameter (non-empty array). Correct: {"action":"delete","ids":[<n>]}',
168
+ );
146
169
  }
147
170
  const uniqueIds = [...new Set(params.ids)];
148
171
  const missing = uniqueIds.filter((id) => !state.todos.some((t) => t.id === id));
@@ -240,7 +263,17 @@ export function registerTodoTool(
240
263
  "\n- add: Batch add todos (requires texts array; optional isVerification marks verification tasks)" +
241
264
  "\n- update: Update todo(s) — single (id + optional status/text) or batch (updates[], takes priority)" +
242
265
  "\n- delete: Batch delete todos (requires ids array)" +
243
- "\n- clear: Clear all todos and reset IDs",
266
+ "\n- clear: Clear all todos and reset IDs" +
267
+ "\n\nExamples:" +
268
+ '\n{"action":"add","texts":["write spec","implement"]}' +
269
+ '\n{"action":"add","texts":["run tests"],"isVerification":true}' +
270
+ '\n{"action":"update","id":1,"status":"in_progress"}' +
271
+ '\n{"action":"update","updates":[{"id":1,"status":"completed"},{"id":2,"status":"in_progress"}]}' +
272
+ '\n{"action":"delete","ids":[3]}' +
273
+ "\n\nDon't:" +
274
+ '\n{"action":"add","text":"x"} ← text is for update; add uses texts:[...]' +
275
+ '\n{"action":"delete","id":3} ← id is for update; delete uses ids:[...]' +
276
+ '\n{"action":"update","status":"x"} ← missing id',
244
277
  promptSnippet: "Use todo when breaking multi-step work into trackable items. Add verification todos (isVerification=true) for checks like running tests.",
245
278
  promptGuidelines: [
246
279
  "[Usage] 多步骤工作(3+步)时使用。AI 自发创建,无需用户触发",