@zhushanwen/pi-todo 0.1.5 → 0.2.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 +2 -2
- package/src/__tests__/todo.test.ts +199 -594
- package/src/commands.ts +5 -26
- package/src/component.ts +11 -34
- package/src/handlers.ts +72 -108
- package/src/index.ts +2 -3
- package/src/model.ts +25 -137
- package/src/render.ts +101 -66
- package/src/state.ts +7 -1
- package/src/tool.ts +28 -130
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
addTodos,
|
|
@@ -9,179 +10,101 @@ import {
|
|
|
9
10
|
updateTodos,
|
|
10
11
|
VALID_STATUSES,
|
|
11
12
|
} from "../model";
|
|
13
|
+
import { renderWidgetLines } from "../render";
|
|
12
14
|
|
|
13
|
-
// ──
|
|
15
|
+
// ── 数据模型 + 向后兼容 ──────────────────────────────
|
|
14
16
|
|
|
15
|
-
describe("Todo data model
|
|
17
|
+
describe("Todo data model", () => {
|
|
16
18
|
it("should load old data without verifyText/verifyAttempts", () => {
|
|
17
|
-
// 旧格式: 没有 verifyText 和 verifyAttempts 字段
|
|
18
19
|
const oldTodo = { id: 1, text: "test", status: "completed" } as unknown as Todo;
|
|
19
20
|
const migrated = migrateTodo(oldTodo);
|
|
20
21
|
|
|
21
|
-
expect(migrated.verifyText).toBeUndefined();
|
|
22
|
-
expect(migrated.verifyAttempts).toBe(0);
|
|
23
22
|
expect(migrated.status).toBe("completed");
|
|
24
23
|
expect(migrated.text).toBe("test");
|
|
25
24
|
expect(migrated.id).toBe(1);
|
|
26
25
|
});
|
|
27
26
|
|
|
28
|
-
it("should
|
|
29
|
-
|
|
30
|
-
id: 1,
|
|
31
|
-
text: "test",
|
|
32
|
-
status: "failed",
|
|
33
|
-
verifyAttempts: 2,
|
|
34
|
-
};
|
|
35
|
-
const migrated = migrateTodo(todo);
|
|
36
|
-
|
|
37
|
-
expect(migrated.status).toBe("failed");
|
|
38
|
-
expect(migrated.verifyAttempts).toBe(2);
|
|
39
|
-
expect(VALID_STATUSES).toContain("failed");
|
|
27
|
+
it("should include exactly three valid statuses", () => {
|
|
28
|
+
expect(VALID_STATUSES).toEqual(["pending", "in_progress", "completed"]);
|
|
40
29
|
});
|
|
41
30
|
|
|
42
|
-
it("should
|
|
43
|
-
const oldTodo = { id:
|
|
31
|
+
it("should migrate verifying → in_progress", () => {
|
|
32
|
+
const oldTodo = { id: 1, text: "test", status: "verifying" } as unknown as Todo;
|
|
44
33
|
const migrated = migrateTodo(oldTodo);
|
|
45
|
-
|
|
46
|
-
expect(migrated.verifyAttempts).toBe(0);
|
|
34
|
+
expect(migrated.status).toBe("in_progress");
|
|
47
35
|
});
|
|
48
36
|
|
|
49
|
-
it("should
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
status: "completed",
|
|
54
|
-
verifyText: "check output",
|
|
55
|
-
verifyAttempts: 1,
|
|
56
|
-
} as unknown as Todo;
|
|
57
|
-
const migrated = migrateTodo(todo);
|
|
58
|
-
|
|
59
|
-
expect(migrated.verifyText).toBe("check output");
|
|
60
|
-
expect(migrated.verifyAttempts).toBe(1);
|
|
37
|
+
it("should migrate failed → pending", () => {
|
|
38
|
+
const oldTodo = { id: 1, text: "test", status: "failed" } as unknown as Todo;
|
|
39
|
+
const migrated = migrateTodo(oldTodo);
|
|
40
|
+
expect(migrated.status).toBe("pending");
|
|
61
41
|
});
|
|
62
42
|
|
|
63
|
-
it("should migrate done:true to completed
|
|
64
|
-
// 极旧格式: done: boolean
|
|
43
|
+
it("should migrate done:true to completed", () => {
|
|
65
44
|
const veryOldTodo = { id: 3, text: "ancient", done: true } as unknown as Todo;
|
|
66
45
|
const migrated = migrateTodo(veryOldTodo);
|
|
67
|
-
|
|
68
46
|
expect(migrated.status).toBe("completed");
|
|
69
|
-
expect(migrated.verifyText).toBeUndefined();
|
|
70
|
-
expect(migrated.verifyAttempts).toBe(0);
|
|
71
47
|
});
|
|
72
48
|
|
|
73
|
-
it("should migrate done:false to pending
|
|
49
|
+
it("should migrate done:false to pending", () => {
|
|
74
50
|
const veryOldTodo = { id: 4, text: "ancient2", done: false } as unknown as Todo;
|
|
75
51
|
const migrated = migrateTodo(veryOldTodo);
|
|
76
|
-
|
|
77
52
|
expect(migrated.status).toBe("pending");
|
|
78
|
-
expect(migrated.verifyText).toBeUndefined();
|
|
79
|
-
expect(migrated.verifyAttempts).toBe(0);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("should include all five valid statuses", () => {
|
|
83
|
-
expect(VALID_STATUSES).toEqual(["pending", "in_progress", "verifying", "completed", "failed"]);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("should preserve evidence when migrating old data", () => {
|
|
87
|
-
const todo = {
|
|
88
|
-
id: 1,
|
|
89
|
-
text: "task",
|
|
90
|
-
status: "verifying",
|
|
91
|
-
verifyText: "check X",
|
|
92
|
-
evidence: "grep confirmed no residual",
|
|
93
|
-
verifyAttempts: 0,
|
|
94
|
-
} as unknown as Todo;
|
|
95
|
-
const migrated = migrateTodo(todo);
|
|
96
|
-
expect(migrated.evidence).toBe("grep confirmed no residual");
|
|
97
|
-
expect(migrated.status).toBe("verifying");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("should default evidence to undefined when absent", () => {
|
|
101
|
-
const oldTodo = { id: 1, text: "test", status: "pending" } as unknown as Todo;
|
|
102
|
-
const migrated = migrateTodo(oldTodo);
|
|
103
|
-
expect(migrated.evidence).toBeUndefined();
|
|
104
53
|
});
|
|
105
54
|
});
|
|
106
55
|
|
|
107
|
-
// ──
|
|
108
|
-
|
|
109
|
-
describe("todo add verifyTexts - Task 2", () => {
|
|
110
|
-
it("should map verifyTexts to todos at corresponding indices", () => {
|
|
111
|
-
const result = addTodos([], 1, ["A", "B"], ["V1"]);
|
|
56
|
+
// ── todo add ────────────────────────────────────────
|
|
112
57
|
|
|
58
|
+
describe("todo add", () => {
|
|
59
|
+
it("should add todos with sequential IDs", () => {
|
|
60
|
+
const result = addTodos([], 1, ["A", "B"]);
|
|
113
61
|
expect(result.error).toBeUndefined();
|
|
114
62
|
expect(result.newTodos).toHaveLength(2);
|
|
115
|
-
expect(result.newTodos[0].
|
|
116
|
-
expect(result.newTodos[1].
|
|
117
|
-
expect(result.newTodos[0].
|
|
118
|
-
expect(result.newTodos[1].verifyAttempts).toBe(0);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("should reject verifyTexts longer than texts", () => {
|
|
122
|
-
const result = addTodos([], 1, ["A"], ["V1", "V2"]);
|
|
123
|
-
|
|
124
|
-
expect(result.error).toBe("verifyTexts too long");
|
|
125
|
-
expect(result.resultText).toContain("Error");
|
|
126
|
-
expect(result.newTodos).toHaveLength(0);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("should work without verifyTexts (backward compat)", () => {
|
|
130
|
-
const result = addTodos([], 1, ["A"]);
|
|
131
|
-
|
|
132
|
-
expect(result.error).toBeUndefined();
|
|
133
|
-
expect(result.newTodos).toHaveLength(1);
|
|
134
|
-
expect(result.newTodos[0].verifyText).toBeUndefined();
|
|
135
|
-
expect(result.newTodos[0].verifyAttempts).toBe(0);
|
|
63
|
+
expect(result.newTodos[0].id).toBe(1);
|
|
64
|
+
expect(result.newTodos[1].id).toBe(2);
|
|
65
|
+
expect(result.newTodos[0].status).toBe("pending");
|
|
136
66
|
});
|
|
137
67
|
|
|
138
|
-
it("should
|
|
139
|
-
const
|
|
140
|
-
|
|
68
|
+
it("should append to existing todos", () => {
|
|
69
|
+
const existing: Todo[] = [{ id: 1, text: "existing", status: "pending" }];
|
|
70
|
+
const result = addTodos(existing, 2, ["new task"]);
|
|
141
71
|
expect(result.error).toBeUndefined();
|
|
142
|
-
expect(result.newTodos).toHaveLength(
|
|
143
|
-
expect(result.newTodos[
|
|
144
|
-
expect(result.newTodos[1].
|
|
145
|
-
expect(result.
|
|
72
|
+
expect(result.newTodos).toHaveLength(2);
|
|
73
|
+
expect(result.newTodos[1].id).toBe(2);
|
|
74
|
+
expect(result.newTodos[1].text).toBe("new task");
|
|
75
|
+
expect(result.newNextId).toBe(3);
|
|
146
76
|
});
|
|
147
77
|
|
|
148
78
|
it("should return error when texts is empty", () => {
|
|
149
79
|
const result = addTodos([], 1, []);
|
|
150
|
-
|
|
151
80
|
expect(result.error).toBe("texts required");
|
|
152
81
|
});
|
|
153
82
|
|
|
154
83
|
it("should return error when all texts are empty after trim", () => {
|
|
155
84
|
const result = addTodos([], 1, [" ", " "]);
|
|
156
|
-
|
|
157
85
|
expect(result.error).toBe("all texts empty");
|
|
158
86
|
});
|
|
159
87
|
|
|
160
|
-
it("should trim texts
|
|
161
|
-
const
|
|
162
|
-
const result = addTodos(existing, 2, [" new task "]);
|
|
163
|
-
|
|
88
|
+
it("should trim texts", () => {
|
|
89
|
+
const result = addTodos([], 1, [" new task "]);
|
|
164
90
|
expect(result.error).toBeUndefined();
|
|
165
|
-
expect(result.newTodos).
|
|
166
|
-
expect(result.newTodos[1].id).toBe(2);
|
|
167
|
-
expect(result.newTodos[1].text).toBe("new task");
|
|
168
|
-
expect(result.newNextId).toBe(3);
|
|
91
|
+
expect(result.newTodos[0].text).toBe("new task");
|
|
169
92
|
});
|
|
170
93
|
});
|
|
171
94
|
|
|
172
|
-
// ──
|
|
95
|
+
// ── todo update batch ───────────────────────────────
|
|
173
96
|
|
|
174
|
-
describe("todo update batch
|
|
97
|
+
describe("todo update batch", () => {
|
|
175
98
|
it("should update multiple todos with updates[]", () => {
|
|
176
99
|
const todos: Todo[] = [
|
|
177
|
-
{ id: 1, text: "A", status: "pending"
|
|
178
|
-
{ id: 2, text: "B", status: "in_progress"
|
|
179
|
-
{ id: 3, text: "C", status: "pending"
|
|
100
|
+
{ id: 1, text: "A", status: "pending" },
|
|
101
|
+
{ id: 2, text: "B", status: "in_progress" },
|
|
102
|
+
{ id: 3, text: "C", status: "pending" },
|
|
180
103
|
];
|
|
181
104
|
const result = updateTodos(todos, [
|
|
182
105
|
{ id: 1, status: "completed" },
|
|
183
106
|
{ id: 2, text: "B updated" },
|
|
184
|
-
{ id: 3, status: "
|
|
107
|
+
{ id: 3, status: "completed", text: "C done" },
|
|
185
108
|
]);
|
|
186
109
|
|
|
187
110
|
expect(result.error).toBeUndefined();
|
|
@@ -189,222 +112,217 @@ describe("todo update batch - Task 3", () => {
|
|
|
189
112
|
expect(result.updatedTodos[0].status).toBe("completed");
|
|
190
113
|
expect(result.updatedTodos[0].text).toBe("A");
|
|
191
114
|
expect(result.updatedTodos[1].text).toBe("B updated");
|
|
192
|
-
expect(result.updatedTodos[1].status).toBe("in_progress");
|
|
193
|
-
expect(result.updatedTodos[2].status).toBe("
|
|
194
|
-
expect(result.updatedTodos[2].text).toBe("C
|
|
195
|
-
expect(result.resultText).toBe("Updated 3 todo(s)");
|
|
115
|
+
expect(result.updatedTodos[1].status).toBe("in_progress");
|
|
116
|
+
expect(result.updatedTodos[2].status).toBe("completed");
|
|
117
|
+
expect(result.updatedTodos[2].text).toBe("C done");
|
|
196
118
|
});
|
|
197
119
|
|
|
198
120
|
it("should reject duplicate ids in updates[]", () => {
|
|
199
|
-
const todos: Todo[] = [
|
|
200
|
-
{ id: 1, text: "A", status: "pending", verifyAttempts: 0 },
|
|
201
|
-
];
|
|
121
|
+
const todos: Todo[] = [{ id: 1, text: "A", status: "pending" }];
|
|
202
122
|
const result = updateTodos(todos, [
|
|
203
123
|
{ id: 1, status: "completed" },
|
|
204
124
|
{ id: 1, status: "pending" },
|
|
205
125
|
]);
|
|
206
|
-
|
|
207
126
|
expect(result.error).toBe("duplicate ids in updates");
|
|
208
|
-
expect(result.updatedTodos).toEqual(todos);
|
|
127
|
+
expect(result.updatedTodos).toEqual(todos);
|
|
209
128
|
});
|
|
210
129
|
|
|
211
|
-
it("should reject non-existent ids
|
|
212
|
-
const todos: Todo[] = [
|
|
213
|
-
|
|
214
|
-
];
|
|
215
|
-
const result = updateTodos(todos, [
|
|
216
|
-
{ id: 1, status: "completed" },
|
|
217
|
-
{ id: 999, status: "pending" },
|
|
218
|
-
]);
|
|
219
|
-
|
|
130
|
+
it("should reject non-existent ids", () => {
|
|
131
|
+
const todos: Todo[] = [{ id: 1, text: "A", status: "pending" }];
|
|
132
|
+
const result = updateTodos(todos, [{ id: 999, status: "pending" }]);
|
|
220
133
|
expect(result.error).toBe("id 999 not found");
|
|
221
|
-
expect(result.updatedTodos[0].status).toBe("pending"); // #1 not modified
|
|
222
134
|
});
|
|
223
135
|
|
|
224
136
|
it("should reject updates[] item missing both status and text", () => {
|
|
225
|
-
const todos: Todo[] = [
|
|
226
|
-
{ id: 1, text: "A", status: "pending", verifyAttempts: 0 },
|
|
227
|
-
];
|
|
137
|
+
const todos: Todo[] = [{ id: 1, text: "A", status: "pending" }];
|
|
228
138
|
const result = updateTodos(todos, [{ id: 1 }]);
|
|
229
|
-
|
|
230
139
|
expect(result.error).toContain("neither status nor text");
|
|
231
|
-
expect(result.updatedTodos[0].status).toBe("pending"); // unchanged
|
|
232
140
|
});
|
|
233
|
-
});
|
|
234
141
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const todo: Todo = {
|
|
240
|
-
id: 1,
|
|
241
|
-
text: "check output",
|
|
242
|
-
status: "pending",
|
|
243
|
-
verifyText: "check X",
|
|
244
|
-
verifyAttempts: 0,
|
|
245
|
-
};
|
|
246
|
-
const line = formatTodoLine(todo);
|
|
247
|
-
|
|
248
|
-
expect(line).toContain(" | 验证: check X");
|
|
249
|
-
expect(line).toContain("#1");
|
|
250
|
-
expect(line).toContain("check output");
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("should not include verify suffix when verifyText is absent", () => {
|
|
254
|
-
const todo: Todo = {
|
|
255
|
-
id: 2,
|
|
256
|
-
text: "plain task",
|
|
257
|
-
status: "completed",
|
|
258
|
-
verifyAttempts: 0,
|
|
259
|
-
};
|
|
260
|
-
const line = formatTodoLine(todo);
|
|
261
|
-
|
|
262
|
-
expect(line).not.toContain("验证");
|
|
263
|
-
expect(line).toContain("#2");
|
|
264
|
-
expect(line).toContain("plain task");
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("should show verifying status with evidence", () => {
|
|
268
|
-
const todo: Todo = {
|
|
269
|
-
id: 3,
|
|
270
|
-
text: "fix auth",
|
|
271
|
-
status: "verifying",
|
|
272
|
-
verifyText: "check status codes",
|
|
273
|
-
evidence: "grep confirmed no display:true residual",
|
|
274
|
-
verifyAttempts: 0,
|
|
275
|
-
};
|
|
276
|
-
const line = formatTodoLine(todo);
|
|
277
|
-
|
|
278
|
-
expect(line).toContain("[v]");
|
|
279
|
-
expect(line).toContain("验证中: grep confirmed no display:true residual");
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it("should show completed with evidence", () => {
|
|
283
|
-
const todo: Todo = {
|
|
284
|
-
id: 4,
|
|
285
|
-
text: "fix login",
|
|
286
|
-
status: "completed",
|
|
287
|
-
verifyText: "密码错误时返回正确错误码",
|
|
288
|
-
evidence: "42 测试全通过,typecheck 无错误",
|
|
289
|
-
verifyAttempts: 0,
|
|
290
|
-
};
|
|
291
|
-
const line = formatTodoLine(todo);
|
|
292
|
-
|
|
293
|
-
expect(line).toContain("[x]");
|
|
294
|
-
expect(line).toContain("已验证: 42 测试全通过");
|
|
142
|
+
it("should reject invalid status values", () => {
|
|
143
|
+
const todos: Todo[] = [{ id: 1, text: "A", status: "pending" }];
|
|
144
|
+
const result = updateTodos(todos, [{ id: 1, status: "banana" }]);
|
|
145
|
+
expect(result.error).toContain("invalid status");
|
|
295
146
|
});
|
|
296
147
|
});
|
|
297
148
|
|
|
298
|
-
// ──
|
|
149
|
+
// ── completed 无拦截 ────────────────────────────────
|
|
299
150
|
|
|
300
|
-
describe("
|
|
301
|
-
|
|
151
|
+
describe("completed without interception", () => {
|
|
152
|
+
it("should allow in_progress → completed directly", () => {
|
|
153
|
+
const todos: Todo[] = [{ id: 1, text: "simple", status: "in_progress" }];
|
|
154
|
+
const result = updateTodos(todos, [{ id: 1, status: "completed" }]);
|
|
155
|
+
expect(result.error).toBeUndefined();
|
|
156
|
+
expect(result.updatedTodos[0].status).toBe("completed");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should allow pending → completed directly", () => {
|
|
160
|
+
const todos: Todo[] = [{ id: 1, text: "skip", status: "pending" }];
|
|
161
|
+
const result = updateTodos(todos, [{ id: 1, status: "completed" }]);
|
|
162
|
+
expect(result.error).toBeUndefined();
|
|
163
|
+
expect(result.updatedTodos[0].status).toBe("completed");
|
|
164
|
+
});
|
|
302
165
|
|
|
303
|
-
it("should
|
|
166
|
+
it("should allow batch all completed without evidence", () => {
|
|
304
167
|
const todos: Todo[] = [
|
|
305
|
-
{
|
|
306
|
-
|
|
307
|
-
text: "task A",
|
|
308
|
-
status: "completed",
|
|
309
|
-
verifyText: "check output",
|
|
310
|
-
verifyAttempts: 0,
|
|
311
|
-
},
|
|
168
|
+
{ id: 1, text: "A", status: "in_progress" },
|
|
169
|
+
{ id: 2, text: "B", status: "in_progress" },
|
|
312
170
|
];
|
|
171
|
+
const result = updateTodos(todos, [
|
|
172
|
+
{ id: 1, status: "completed" },
|
|
173
|
+
{ id: 2, status: "completed" },
|
|
174
|
+
]);
|
|
175
|
+
expect(result.error).toBeUndefined();
|
|
176
|
+
expect(result.updatedTodos[0].status).toBe("completed");
|
|
177
|
+
expect(result.updatedTodos[1].status).toBe("completed");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
313
180
|
|
|
314
|
-
|
|
315
|
-
(t) =>
|
|
316
|
-
t.status === "completed" &&
|
|
317
|
-
t.verifyText &&
|
|
318
|
-
t.verifyAttempts < MAX_VERIFY_ATTEMPTS,
|
|
319
|
-
);
|
|
181
|
+
// ── formatTodoLine ──────────────────────────────────
|
|
320
182
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
183
|
+
describe("formatTodoLine", () => {
|
|
184
|
+
it("should format pending todo", () => {
|
|
185
|
+
const todo: Todo = { id: 1, text: "task A", status: "pending" };
|
|
186
|
+
expect(formatTodoLine(todo)).toBe("[ ] #1: task A");
|
|
324
187
|
});
|
|
325
188
|
|
|
326
|
-
it("should
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
text: "task A",
|
|
331
|
-
status: "completed",
|
|
332
|
-
verifyAttempts: 0,
|
|
333
|
-
},
|
|
334
|
-
];
|
|
335
|
-
|
|
336
|
-
const needsVerify = todos.find(
|
|
337
|
-
(t) =>
|
|
338
|
-
t.status === "completed" &&
|
|
339
|
-
t.verifyText &&
|
|
340
|
-
t.verifyAttempts < MAX_VERIFY_ATTEMPTS,
|
|
341
|
-
);
|
|
189
|
+
it("should format in_progress todo", () => {
|
|
190
|
+
const todo: Todo = { id: 2, text: "task B", status: "in_progress" };
|
|
191
|
+
expect(formatTodoLine(todo)).toBe("[~] #2: task B");
|
|
192
|
+
});
|
|
342
193
|
|
|
343
|
-
|
|
194
|
+
it("should format completed todo", () => {
|
|
195
|
+
const todo: Todo = { id: 3, text: "task C", status: "completed" };
|
|
196
|
+
expect(formatTodoLine(todo)).toBe("[x] #3: task C");
|
|
344
197
|
});
|
|
198
|
+
});
|
|
345
199
|
|
|
346
|
-
|
|
200
|
+
// ── buildRender ─────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
describe("buildRender", () => {
|
|
203
|
+
it("should calculate summary correctly", () => {
|
|
347
204
|
const todos: Todo[] = [
|
|
348
|
-
{
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
status: "completed",
|
|
352
|
-
verifyText: "check B",
|
|
353
|
-
verifyAttempts: MAX_VERIFY_ATTEMPTS,
|
|
354
|
-
},
|
|
205
|
+
{ id: 1, text: "a", status: "completed" },
|
|
206
|
+
{ id: 2, text: "b", status: "pending" },
|
|
207
|
+
{ id: 3, text: "c", status: "in_progress" },
|
|
355
208
|
];
|
|
209
|
+
const render = buildRender(todos);
|
|
210
|
+
expect(render).toBeDefined();
|
|
211
|
+
expect(render!.summary).toBe("1/3 completed");
|
|
212
|
+
expect(render!.data.items).toHaveLength(3);
|
|
213
|
+
});
|
|
356
214
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
t.verifyText &&
|
|
361
|
-
t.verifyAttempts >= MAX_VERIFY_ATTEMPTS,
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
expect(failed).toBeDefined();
|
|
365
|
-
failed!.status = "failed";
|
|
366
|
-
expect(failed!.status).toBe("failed");
|
|
215
|
+
it("should handle empty list", () => {
|
|
216
|
+
const render = buildRender([]);
|
|
217
|
+
expect(render!.summary).toBe("0/0 completed");
|
|
367
218
|
});
|
|
219
|
+
});
|
|
368
220
|
|
|
369
|
-
|
|
221
|
+
// ── widget 渲染布局 ────────────────────────────────
|
|
222
|
+
|
|
223
|
+
const mockTheme = {
|
|
224
|
+
fg: (_color: string, text: string) => text,
|
|
225
|
+
bg: (_color: string, text: string) => text,
|
|
226
|
+
bold: (text: string) => text,
|
|
227
|
+
italic: (text: string) => text,
|
|
228
|
+
underline: (text: string) => text,
|
|
229
|
+
inverse: (text: string) => text,
|
|
230
|
+
strikethrough: (text: string) => text,
|
|
231
|
+
getFgAnsi: (_color: string) => "",
|
|
232
|
+
getBgAnsi: (_color: string) => "",
|
|
233
|
+
getColorMode: () => "truecolor" as const,
|
|
234
|
+
getThinkingBorderColor: () => (text: string) => text,
|
|
235
|
+
getBashModeBorderColor: () => (text: string) => text,
|
|
236
|
+
} as unknown as Theme;
|
|
237
|
+
|
|
238
|
+
describe("widget rendering", () => {
|
|
239
|
+
it("should render empty list as empty widget", () => {
|
|
240
|
+
expect(renderWidgetLines([], mockTheme, 80)).toEqual([]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should use single column for 3 tasks", () => {
|
|
370
244
|
const todos: Todo[] = [
|
|
371
|
-
{ id: 1, text: "
|
|
245
|
+
{ id: 1, text: "A", status: "pending" },
|
|
246
|
+
{ id: 2, text: "B", status: "in_progress" },
|
|
247
|
+
{ id: 3, text: "C", status: "completed" },
|
|
372
248
|
];
|
|
249
|
+
const lines = renderWidgetLines(todos, mockTheme, 80);
|
|
250
|
+
expect(lines.length).toBe(4); // 1 header + 3 items
|
|
251
|
+
expect(lines[0]).toContain("1/3");
|
|
252
|
+
expect(lines[1]).toContain("#1");
|
|
253
|
+
expect(lines[2]).toContain("#2");
|
|
254
|
+
expect(lines[3]).toContain("#3");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should use single column up to 8 tasks", () => {
|
|
258
|
+
const todos: Todo[] = Array.from({ length: 8 }, (_, i) => ({
|
|
259
|
+
id: i + 1,
|
|
260
|
+
text: `Task ${i + 1}`,
|
|
261
|
+
status: "pending" as const,
|
|
262
|
+
}));
|
|
263
|
+
const lines = renderWidgetLines(todos, mockTheme, 80);
|
|
264
|
+
expect(lines.length).toBe(9); // 1 header + 8 items
|
|
265
|
+
for (let i = 1; i < lines.length; i++) {
|
|
266
|
+
expect(lines[i]).toContain(`#${i}`);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
373
269
|
|
|
374
|
-
|
|
375
|
-
todos[
|
|
376
|
-
|
|
270
|
+
it("should switch to dual column for 9 tasks", () => {
|
|
271
|
+
const todos: Todo[] = Array.from({ length: 9 }, (_, i) => ({
|
|
272
|
+
id: i + 1,
|
|
273
|
+
text: `Task ${i + 1}`,
|
|
274
|
+
status: "pending" as const,
|
|
275
|
+
}));
|
|
276
|
+
const lines = renderWidgetLines(todos, mockTheme, 80);
|
|
277
|
+
expect(lines.length).toBe(6); // 1 header + ceil(9/2)=5 rows
|
|
278
|
+
expect(lines[0]).toContain("0/9");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should keep dual column within Pi widget max lines for 18 tasks", () => {
|
|
282
|
+
const todos: Todo[] = Array.from({ length: 18 }, (_, i) => ({
|
|
283
|
+
id: i + 1,
|
|
284
|
+
text: `Task ${i + 1}`,
|
|
285
|
+
status: "pending" as const,
|
|
286
|
+
}));
|
|
287
|
+
const lines = renderWidgetLines(todos, mockTheme, 80);
|
|
288
|
+
expect(lines.length).toBe(10); // 1 header + 9 rows
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should keep widget lines within Pi max for 19 tasks", () => {
|
|
292
|
+
const todos: Todo[] = Array.from({ length: 19 }, (_, i) => ({
|
|
293
|
+
id: i + 1,
|
|
294
|
+
text: `Task ${i + 1}`,
|
|
295
|
+
status: "pending" as const,
|
|
296
|
+
}));
|
|
297
|
+
const lines = renderWidgetLines(todos, mockTheme, 80);
|
|
298
|
+
expect(lines.length).toBe(11); // 1 header + ceil(19/2)=10 rows; Pi truncates at 10
|
|
377
299
|
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ── agent_end logic (pure data) ─────────────────────
|
|
378
303
|
|
|
304
|
+
describe("agent_end logic", () => {
|
|
379
305
|
it("should detect stall when no todo activity for threshold rounds", () => {
|
|
380
306
|
const STALL_THRESHOLD = 5;
|
|
381
307
|
const userMessageCount = 10;
|
|
382
308
|
const lastTodoCallCount = 3;
|
|
383
|
-
const todos: Todo[] = [
|
|
384
|
-
{ id: 1, text: "pending task", status: "pending", verifyAttempts: 0 },
|
|
385
|
-
];
|
|
386
|
-
const allCompletedAtCount = null;
|
|
309
|
+
const todos: Todo[] = [{ id: 1, text: "pending task", status: "pending" }];
|
|
387
310
|
|
|
388
311
|
const isStalled =
|
|
389
312
|
todos.length > 0 &&
|
|
390
|
-
allCompletedAtCount === null &&
|
|
391
313
|
userMessageCount - lastTodoCallCount >= STALL_THRESHOLD;
|
|
392
314
|
|
|
393
315
|
expect(isStalled).toBe(true);
|
|
394
316
|
});
|
|
395
317
|
|
|
396
318
|
it("should detect reminder when interval elapsed", () => {
|
|
397
|
-
const REMINDER_INTERVAL =
|
|
398
|
-
const userMessageCount =
|
|
399
|
-
const lastTodoCallCount =
|
|
400
|
-
const todos: Todo[] = [
|
|
401
|
-
{ id: 1, text: "task", status: "pending", verifyAttempts: 0 },
|
|
402
|
-
];
|
|
403
|
-
const allCompletedAtCount = null;
|
|
319
|
+
const REMINDER_INTERVAL = 2;
|
|
320
|
+
const userMessageCount = 5;
|
|
321
|
+
const lastTodoCallCount = 3;
|
|
322
|
+
const todos: Todo[] = [{ id: 1, text: "task", status: "pending" }];
|
|
404
323
|
|
|
405
324
|
const needsReminder =
|
|
406
325
|
todos.length > 0 &&
|
|
407
|
-
allCompletedAtCount === null &&
|
|
408
326
|
userMessageCount - lastTodoCallCount >= REMINDER_INTERVAL;
|
|
409
327
|
|
|
410
328
|
expect(needsReminder).toBe(true);
|
|
@@ -434,327 +352,14 @@ describe("agent_end loop logic - Task 5", () => {
|
|
|
434
352
|
expect(shouldClear).toBe(false);
|
|
435
353
|
});
|
|
436
354
|
|
|
437
|
-
it("should
|
|
438
|
-
const todos: Todo[] = [
|
|
439
|
-
{
|
|
440
|
-
id: 1,
|
|
441
|
-
text: "task A",
|
|
442
|
-
status: "pending",
|
|
443
|
-
verifyText: "check A",
|
|
444
|
-
verifyAttempts: 0,
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
id: 2,
|
|
448
|
-
text: "task B",
|
|
449
|
-
status: "in_progress",
|
|
450
|
-
verifyAttempts: 0,
|
|
451
|
-
},
|
|
452
|
-
];
|
|
453
|
-
|
|
454
|
-
const pendingTodos = todos.filter((t) => t.status !== "completed");
|
|
455
|
-
const lines = pendingTodos.map((t) => {
|
|
456
|
-
const verifyTag = t.verifyText
|
|
457
|
-
? ` [待验证: ${t.verifyText}]`
|
|
458
|
-
: "";
|
|
459
|
-
return `#${t.id}: ${t.text}${verifyTag}`;
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
expect(lines).toHaveLength(2);
|
|
463
|
-
expect(lines[0]).toBe("#1: task A [待验证: check A]");
|
|
464
|
-
expect(lines[1]).toBe("#2: task B");
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it("should include verifying tasks as pending (not completed)", () => {
|
|
468
|
-
const todos: Todo[] = [
|
|
469
|
-
{ id: 1, text: "A", status: "verifying", verifyText: "check A", verifyAttempts: 0, evidence: "testing..." },
|
|
470
|
-
{ id: 2, text: "B", status: "completed", verifyAttempts: 0 },
|
|
471
|
-
];
|
|
472
|
-
|
|
473
|
-
const pendingTodos = todos.filter((t) => t.status !== "completed");
|
|
474
|
-
expect(pendingTodos).toHaveLength(1);
|
|
475
|
-
expect(pendingTodos[0].status).toBe("verifying");
|
|
476
|
-
});
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
// ── batch update validation - Task 3b ─────────────────
|
|
480
|
-
|
|
481
|
-
describe("batch update validation - Task 3b", () => {
|
|
482
|
-
it("should reject invalid status values in updates[]", () => {
|
|
483
|
-
const todos: Todo[] = [{ id: 1, text: "A", status: "pending", verifyAttempts: 0 }];
|
|
484
|
-
const result = updateTodos(todos, [{ id: 1, status: "banana" }]);
|
|
485
|
-
expect(result.error).toContain("invalid status");
|
|
486
|
-
expect(result.updatedTodos[0].status).toBe("pending"); // unchanged
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
it("should accept valid status values in updates[]", () => {
|
|
490
|
-
const todos: Todo[] = [{ id: 1, text: "A", status: "pending", verifyAttempts: 0 }];
|
|
491
|
-
const result = updateTodos(todos, [{ id: 1, status: "completed" }]);
|
|
492
|
-
expect(result.error).toBeUndefined();
|
|
493
|
-
expect(result.updatedTodos[0].status).toBe("completed");
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
// ── verifyAttempts increment on re-open ─────────────
|
|
498
|
-
|
|
499
|
-
describe("verifyAttempts increment on re-open", () => {
|
|
500
|
-
it("should increment verifyAttempts when status changes from completed to in_progress with verifyText", () => {
|
|
501
|
-
const todo: Todo = { id: 1, text: "fix auth", status: "completed", verifyText: "check status codes", verifyAttempts: 0 };
|
|
502
|
-
|
|
503
|
-
const oldStatus = todo.status;
|
|
504
|
-
todo.status = "in_progress";
|
|
505
|
-
if (oldStatus === "completed" && todo.verifyText && todo.verifyAttempts < 2) {
|
|
506
|
-
todo.verifyAttempts++;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
expect(todo.verifyAttempts).toBe(1);
|
|
510
|
-
expect(todo.status).toBe("in_progress");
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
it("should NOT increment verifyAttempts when re-opening task without verifyText", () => {
|
|
514
|
-
const todo: Todo = { id: 1, text: "simple task", status: "completed", verifyAttempts: 0 };
|
|
515
|
-
|
|
516
|
-
const oldStatus = todo.status;
|
|
517
|
-
todo.status = "in_progress";
|
|
518
|
-
if (oldStatus === "completed" && todo.verifyText && todo.verifyAttempts < 2) {
|
|
519
|
-
todo.verifyAttempts++;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
expect(todo.verifyAttempts).toBe(0); // no verifyText → no increment
|
|
523
|
-
});
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
// ── verifying state transitions ────────────────────
|
|
527
|
-
|
|
528
|
-
describe("verifying state transitions", () => {
|
|
529
|
-
it("should block verifying on task without verifyText", () => {
|
|
530
|
-
const todos: Todo[] = [
|
|
531
|
-
{ id: 1, text: "simple", status: "in_progress", verifyAttempts: 0 },
|
|
532
|
-
];
|
|
533
|
-
const result = updateTodos(todos, [{ id: 1, status: "verifying", evidence: "this should be blocked" }]);
|
|
534
|
-
|
|
535
|
-
expect(result.blocked).toBeDefined();
|
|
536
|
-
expect(result.blocked!.length).toBe(1);
|
|
537
|
-
expect(result.blocked![0].reason).toContain("verifyText");
|
|
538
|
-
expect(result.updatedTodos[0].status).toBe("in_progress"); // unchanged
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
it("should block verifying without evidence", () => {
|
|
542
|
-
const todos: Todo[] = [
|
|
543
|
-
{ id: 1, text: "fix auth", status: "in_progress", verifyText: "check codes", verifyAttempts: 0 },
|
|
544
|
-
];
|
|
545
|
-
const result = updateTodos(todos, [{ id: 1, status: "verifying" }]);
|
|
546
|
-
|
|
547
|
-
expect(result.blocked).toBeDefined();
|
|
548
|
-
expect(result.blocked![0].reason).toContain("evidence");
|
|
549
|
-
expect(result.updatedTodos[0].status).toBe("in_progress");
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
it("should block verifying with evidence shorter than 10 chars", () => {
|
|
553
|
-
const todos: Todo[] = [
|
|
554
|
-
{ id: 1, text: "fix auth", status: "in_progress", verifyText: "check codes", verifyAttempts: 0 },
|
|
555
|
-
];
|
|
556
|
-
const result = updateTodos(todos, [{ id: 1, status: "verifying", evidence: "short" }]);
|
|
557
|
-
|
|
558
|
-
expect(result.blocked).toBeDefined();
|
|
559
|
-
expect(result.blocked![0].reason).toContain("10");
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
it("should allow verifying with valid evidence", () => {
|
|
563
|
-
const todos: Todo[] = [
|
|
564
|
-
{ id: 1, text: "fix auth", status: "in_progress", verifyText: "check codes", verifyAttempts: 0 },
|
|
565
|
-
];
|
|
566
|
-
const result = updateTodos(todos, [{ id: 1, status: "verifying", evidence: "grep confirmed no residual code" }]);
|
|
567
|
-
|
|
568
|
-
expect(result.error).toBeUndefined();
|
|
569
|
-
expect(result.blocked).toBeUndefined();
|
|
570
|
-
expect(result.updatedTodos[0].status).toBe("verifying");
|
|
571
|
-
expect(result.updatedTodos[0].evidence).toBe("grep confirmed no residual code");
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it("should block verifying → completed without evidence", () => {
|
|
575
|
-
const todos: Todo[] = [
|
|
576
|
-
{ id: 1, text: "fix auth", status: "verifying", verifyText: "check codes", verifyAttempts: 0, evidence: "testing" },
|
|
577
|
-
];
|
|
578
|
-
const result = updateTodos(todos, [{ id: 1, status: "completed" }]);
|
|
579
|
-
|
|
580
|
-
expect(result.blocked).toBeDefined();
|
|
581
|
-
expect(result.blocked![0].reason).toContain("evidence");
|
|
582
|
-
expect(result.updatedTodos[0].status).toBe("verifying"); // unchanged
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
it("should allow verifying → completed with evidence", () => {
|
|
586
|
-
const todos: Todo[] = [
|
|
587
|
-
{ id: 1, text: "fix auth", status: "verifying", verifyText: "check codes", verifyAttempts: 0, evidence: "testing in progress" },
|
|
588
|
-
];
|
|
589
|
-
const result = updateTodos(todos, [{ id: 1, status: "completed", evidence: "all 42 tests passed, typecheck clean" }]);
|
|
590
|
-
|
|
591
|
-
expect(result.error).toBeUndefined();
|
|
592
|
-
expect(result.updatedTodos[0].status).toBe("completed");
|
|
593
|
-
expect(result.updatedTodos[0].evidence).toBe("all 42 tests passed, typecheck clean");
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
it("should allow in_progress → completed directly for tasks without verifyText", () => {
|
|
597
|
-
const todos: Todo[] = [
|
|
598
|
-
{ id: 1, text: "simple", status: "in_progress", verifyAttempts: 0 },
|
|
599
|
-
];
|
|
600
|
-
const result = updateTodos(todos, [{ id: 1, status: "completed" }]);
|
|
601
|
-
|
|
602
|
-
expect(result.error).toBeUndefined();
|
|
603
|
-
expect(result.blocked).toBeUndefined();
|
|
604
|
-
expect(result.updatedTodos[0].status).toBe("completed");
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
it("should block in_progress → completed on verifyText task without verified+evidence", () => {
|
|
608
|
-
const todos: Todo[] = [
|
|
609
|
-
{ id: 1, text: "fix auth", status: "in_progress", verifyText: "check codes", verifyAttempts: 0 },
|
|
610
|
-
];
|
|
611
|
-
const result = updateTodos(todos, [{ id: 1, status: "completed" }]);
|
|
612
|
-
|
|
613
|
-
expect(result.blocked).toBeDefined();
|
|
614
|
-
expect(result.blocked![0].reason).toContain("verifying");
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
it("should allow in_progress → completed with verified=true + evidence (skip verifying)", () => {
|
|
618
|
-
const todos: Todo[] = [
|
|
619
|
-
{ id: 1, text: "fix auth", status: "in_progress", verifyText: "check codes", verifyAttempts: 0 },
|
|
620
|
-
];
|
|
621
|
-
const result = updateTodos(todos, [{ id: 1, status: "completed", verified: true, evidence: "all tests passed confirmed" }]);
|
|
622
|
-
|
|
623
|
-
expect(result.error).toBeUndefined();
|
|
624
|
-
expect(result.updatedTodos[0].status).toBe("completed");
|
|
625
|
-
expect(result.updatedTodos[0].evidence).toBe("all tests passed confirmed");
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
it("should block in_progress → completed with verified=true but no evidence", () => {
|
|
629
|
-
const todos: Todo[] = [
|
|
630
|
-
{ id: 1, text: "fix auth", status: "in_progress", verifyText: "check codes", verifyAttempts: 0 },
|
|
631
|
-
];
|
|
632
|
-
const result = updateTodos(todos, [{ id: 1, status: "completed", verified: true }]);
|
|
633
|
-
|
|
634
|
-
expect(result.blocked).toBeDefined();
|
|
635
|
-
expect(result.blocked![0].reason).toContain("evidence");
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
it("should block batch completed when mixed verifyText tasks lack evidence", () => {
|
|
639
|
-
const todos: Todo[] = [
|
|
640
|
-
{ id: 1, text: "A", status: "in_progress", verifyText: "check A", verifyAttempts: 0 },
|
|
641
|
-
{ id: 2, text: "B", status: "in_progress", verifyAttempts: 0 },
|
|
642
|
-
];
|
|
643
|
-
const result = updateTodos(todos, [
|
|
644
|
-
{ id: 1, status: "completed" },
|
|
645
|
-
{ id: 2, status: "completed" },
|
|
646
|
-
]);
|
|
647
|
-
|
|
648
|
-
expect(result.blocked).toBeDefined();
|
|
649
|
-
expect(result.blocked!.length).toBe(1); // only #1 blocked
|
|
650
|
-
expect(result.blocked![0].id).toBe(1);
|
|
651
|
-
// both unchanged (all-or-nothing)
|
|
652
|
-
expect(result.updatedTodos[0].status).toBe("in_progress");
|
|
653
|
-
expect(result.updatedTodos[1].status).toBe("in_progress");
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
it("should allow batch when verifyText task has verified+evidence and non-verifyText task is plain", () => {
|
|
657
|
-
const todos: Todo[] = [
|
|
658
|
-
{ id: 1, text: "A", status: "in_progress", verifyText: "check A", verifyAttempts: 0 },
|
|
659
|
-
{ id: 2, text: "B", status: "in_progress", verifyAttempts: 0 },
|
|
660
|
-
];
|
|
661
|
-
const result = updateTodos(todos, [
|
|
662
|
-
{ id: 1, status: "completed", verified: true, evidence: "confirmed via grep" },
|
|
663
|
-
{ id: 2, status: "completed" },
|
|
664
|
-
]);
|
|
665
|
-
|
|
666
|
-
expect(result.error).toBeUndefined();
|
|
667
|
-
expect(result.updatedTodos[0].status).toBe("completed");
|
|
668
|
-
expect(result.updatedTodos[1].status).toBe("completed");
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
it("should allow non-completed status changes without evidence", () => {
|
|
672
|
-
const todos: Todo[] = [
|
|
673
|
-
{ id: 1, text: "A", status: "pending", verifyText: "check A", verifyAttempts: 0 },
|
|
674
|
-
];
|
|
675
|
-
const result = updateTodos(todos, [{ id: 1, status: "in_progress" }]);
|
|
676
|
-
|
|
677
|
-
expect(result.error).toBeUndefined();
|
|
678
|
-
expect(result.blocked).toBeUndefined();
|
|
679
|
-
expect(result.updatedTodos[0].status).toBe("in_progress");
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
it("should allow verifying → in_progress (verification failed, rework)", () => {
|
|
355
|
+
it("should pick first pending todo as next recommended", () => {
|
|
683
356
|
const todos: Todo[] = [
|
|
684
|
-
{ id: 1, text: "
|
|
357
|
+
{ id: 1, text: "A", status: "completed" },
|
|
358
|
+
{ id: 2, text: "B", status: "pending" },
|
|
359
|
+
{ id: 3, text: "C", status: "pending" },
|
|
685
360
|
];
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
expect(
|
|
689
|
-
expect(result.updatedTodos[0].status).toBe("in_progress");
|
|
690
|
-
});
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
// ── buildRender ───────────────────────────────────────
|
|
694
|
-
|
|
695
|
-
describe("buildRender", () => {
|
|
696
|
-
it("should calculate summary correctly", () => {
|
|
697
|
-
const todos: Todo[] = [
|
|
698
|
-
{ id: 1, text: "a", status: "completed", verifyAttempts: 0 },
|
|
699
|
-
{ id: 2, text: "b", status: "pending", verifyAttempts: 0 },
|
|
700
|
-
{ id: 3, text: "c", status: "failed", verifyAttempts: 2 },
|
|
701
|
-
];
|
|
702
|
-
const render = buildRender(todos);
|
|
703
|
-
|
|
704
|
-
expect(render).toBeDefined();
|
|
705
|
-
expect(render!.summary).toBe("1/3 completed");
|
|
706
|
-
expect(render!.data.items).toHaveLength(3);
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
it("should handle empty list", () => {
|
|
710
|
-
const render = buildRender([]);
|
|
711
|
-
|
|
712
|
-
expect(render).toBeDefined();
|
|
713
|
-
expect(render!.summary).toBe("0/0 completed");
|
|
714
|
-
expect(render!.data.items).toHaveLength(0);
|
|
715
|
-
});
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
// ── batch update verifyAttempts increment ───────────
|
|
719
|
-
|
|
720
|
-
describe("batch update verifyAttempts increment", () => {
|
|
721
|
-
it("should increment verifyAttempts when batch reverts completed → in_progress with verifyText", () => {
|
|
722
|
-
const todos: Todo[] = [
|
|
723
|
-
{ id: 1, text: "fix auth", status: "completed", verifyText: "check codes", verifyAttempts: 0 },
|
|
724
|
-
];
|
|
725
|
-
const result = updateTodos(todos, [{ id: 1, status: "in_progress" }]);
|
|
726
|
-
|
|
727
|
-
expect(result.error).toBeUndefined();
|
|
728
|
-
expect(result.updatedTodos[0].verifyAttempts).toBe(1);
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
it("should increment verifyAttempts when batch reverts verifying → in_progress with verifyText", () => {
|
|
732
|
-
const todos: Todo[] = [
|
|
733
|
-
{ id: 1, text: "fix auth", status: "verifying", verifyText: "check codes", verifyAttempts: 1, evidence: "testing" },
|
|
734
|
-
];
|
|
735
|
-
const result = updateTodos(todos, [{ id: 1, status: "in_progress" }]);
|
|
736
|
-
|
|
737
|
-
expect(result.error).toBeUndefined();
|
|
738
|
-
expect(result.updatedTodos[0].verifyAttempts).toBe(2);
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
it("should NOT increment verifyAttempts for tasks without verifyText", () => {
|
|
742
|
-
const todos: Todo[] = [
|
|
743
|
-
{ id: 1, text: "simple", status: "completed", verifyAttempts: 0 },
|
|
744
|
-
];
|
|
745
|
-
const result = updateTodos(todos, [{ id: 1, status: "in_progress" }]);
|
|
746
|
-
|
|
747
|
-
expect(result.error).toBeUndefined();
|
|
748
|
-
expect(result.updatedTodos[0].verifyAttempts).toBe(0);
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
it("should NOT increment verifyAttempts when already at MAX (2)", () => {
|
|
752
|
-
const todos: Todo[] = [
|
|
753
|
-
{ id: 1, text: "fix auth", status: "completed", verifyText: "check codes", verifyAttempts: 2 },
|
|
754
|
-
];
|
|
755
|
-
const result = updateTodos(todos, [{ id: 1, status: "in_progress" }]);
|
|
756
|
-
|
|
757
|
-
expect(result.error).toBeUndefined();
|
|
758
|
-
expect(result.updatedTodos[0].verifyAttempts).toBe(2); // capped
|
|
361
|
+
const next = todos.find((t) => t.status !== "completed");
|
|
362
|
+
expect(next!.id).toBe(2);
|
|
363
|
+
expect(next!.text).toBe("B");
|
|
759
364
|
});
|
|
760
365
|
});
|