@towles/tool 0.0.41 → 0.0.49

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.
Files changed (53) hide show
  1. package/README.md +67 -109
  2. package/package.json +51 -41
  3. package/src/commands/base.ts +3 -18
  4. package/src/commands/config.ts +9 -8
  5. package/src/commands/doctor.ts +4 -1
  6. package/src/commands/gh/branch-clean.ts +10 -4
  7. package/src/commands/gh/branch.ts +6 -3
  8. package/src/commands/gh/pr.ts +10 -3
  9. package/src/commands/graph-template.html +1214 -0
  10. package/src/commands/graph.test.ts +176 -0
  11. package/src/commands/graph.ts +970 -0
  12. package/src/commands/install.ts +8 -2
  13. package/src/commands/journal/daily-notes.ts +9 -5
  14. package/src/commands/journal/meeting.ts +12 -6
  15. package/src/commands/journal/note.ts +12 -6
  16. package/src/commands/ralph/plan/add.ts +75 -0
  17. package/src/commands/ralph/plan/done.ts +82 -0
  18. package/src/commands/ralph/{task → plan}/list.test.ts +5 -5
  19. package/src/commands/ralph/{task → plan}/list.ts +28 -39
  20. package/src/commands/ralph/plan/remove.ts +71 -0
  21. package/src/commands/ralph/run.test.ts +521 -0
  22. package/src/commands/ralph/run.ts +126 -189
  23. package/src/commands/ralph/show.ts +88 -0
  24. package/src/config/settings.ts +8 -27
  25. package/src/{commands/ralph/lib → lib/ralph}/execution.ts +4 -14
  26. package/src/lib/ralph/formatter.ts +238 -0
  27. package/src/{commands/ralph/lib → lib/ralph}/state.ts +17 -42
  28. package/src/utils/date-utils.test.ts +2 -1
  29. package/src/utils/date-utils.ts +2 -2
  30. package/LICENSE.md +0 -20
  31. package/src/commands/index.ts +0 -55
  32. package/src/commands/observe/graph.test.ts +0 -89
  33. package/src/commands/observe/graph.ts +0 -1640
  34. package/src/commands/observe/report.ts +0 -166
  35. package/src/commands/observe/session.ts +0 -385
  36. package/src/commands/observe/setup.ts +0 -180
  37. package/src/commands/observe/status.ts +0 -146
  38. package/src/commands/ralph/lib/formatter.ts +0 -298
  39. package/src/commands/ralph/lib/marker.ts +0 -108
  40. package/src/commands/ralph/marker/create.ts +0 -23
  41. package/src/commands/ralph/plan.ts +0 -73
  42. package/src/commands/ralph/progress.ts +0 -44
  43. package/src/commands/ralph/ralph.test.ts +0 -673
  44. package/src/commands/ralph/task/add.ts +0 -105
  45. package/src/commands/ralph/task/done.ts +0 -73
  46. package/src/commands/ralph/task/remove.ts +0 -62
  47. package/src/config/context.ts +0 -7
  48. package/src/constants.ts +0 -3
  49. package/src/utils/anthropic/types.ts +0 -158
  50. package/src/utils/exec.ts +0 -8
  51. package/src/utils/git/git.ts +0 -25
  52. /package/src/{commands → lib}/journal/utils.ts +0 -0
  53. /package/src/{commands/ralph/lib → lib/ralph}/index.ts +0 -0
@@ -1,673 +0,0 @@
1
- /**
2
- * Unit tests for ralph-loop script
3
- */
4
- import { describe, it, expect, afterEach } from "vitest";
5
- import { existsSync, unlinkSync, writeFileSync } from "node:fs";
6
- import { join } from "node:path";
7
- import { tmpdir } from "node:os";
8
- import type { RalphTask, IterationHistory } from "./lib/index";
9
- import {
10
- createInitialState,
11
- saveState,
12
- loadState,
13
- addTaskToState,
14
- formatTasksForPrompt,
15
- formatTasksAsMarkdown,
16
- formatPlanAsMarkdown,
17
- formatPlanAsJson,
18
- buildIterationPrompt,
19
- extractOutputSummary,
20
- detectCompletionMarker,
21
- appendHistory,
22
- DEFAULT_MAX_ITERATIONS,
23
- DEFAULT_STATE_FILE,
24
- DEFAULT_HISTORY_FILE,
25
- DEFAULT_COMPLETION_MARKER,
26
- CLAUDE_DEFAULT_ARGS,
27
- } from "./lib/index";
28
-
29
- describe("ralph-loop", () => {
30
- const testStateFile = join(tmpdir(), `ralph-test-${Date.now()}.json`);
31
- const testHistoryFile = join(tmpdir(), `ralph-history-${Date.now()}.log`);
32
-
33
- afterEach(() => {
34
- // Cleanup test files
35
- if (existsSync(testStateFile)) {
36
- unlinkSync(testStateFile);
37
- }
38
- if (existsSync(testHistoryFile)) {
39
- unlinkSync(testHistoryFile);
40
- }
41
- });
42
-
43
- describe("constants", () => {
44
- it("should have correct default values", () => {
45
- expect(DEFAULT_MAX_ITERATIONS).toBe(10);
46
- expect(DEFAULT_STATE_FILE).toBe("./.claude/.ralph/ralph-state.local.json");
47
- expect(DEFAULT_HISTORY_FILE).toBe("./.claude/.ralph/ralph-history.local.log");
48
- expect(DEFAULT_COMPLETION_MARKER).toBe("RALPH_DONE");
49
- expect(CLAUDE_DEFAULT_ARGS).toEqual([
50
- "--print",
51
- "--verbose",
52
- "--output-format",
53
- "stream-json",
54
- "--permission-mode",
55
- "bypassPermissions",
56
- ]);
57
- });
58
- });
59
-
60
- describe("createInitialState", () => {
61
- it("should create state with correct structure", () => {
62
- const state = createInitialState(5);
63
-
64
- expect(state.version).toBe(1);
65
- expect(state.iteration).toBe(0);
66
- expect(state.maxIterations).toBe(5);
67
- expect(state.status).toBe("running");
68
- expect(state.tasks).toEqual([]);
69
- expect(state.startedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
70
- expect(state.sessionId).toBeUndefined();
71
- });
72
-
73
- it("should use provided maxIterations", () => {
74
- const state = createInitialState(20);
75
- expect(state.maxIterations).toBe(20);
76
- });
77
- });
78
-
79
- describe("state sessionId", () => {
80
- it("should allow setting and persisting sessionId", () => {
81
- const state = createInitialState(10);
82
- state.sessionId = "test-session-uuid-123";
83
-
84
- saveState(state, testStateFile);
85
- const loaded = loadState(testStateFile);
86
-
87
- expect(loaded?.sessionId).toBe("test-session-uuid-123");
88
- });
89
-
90
- it("should preserve sessionId as undefined when not set", () => {
91
- const state = createInitialState(10);
92
-
93
- saveState(state, testStateFile);
94
- const loaded = loadState(testStateFile);
95
-
96
- expect(loaded?.sessionId).toBeUndefined();
97
- });
98
- });
99
-
100
- describe("saveState and loadState", () => {
101
- it("should save and load state correctly", () => {
102
- const state = createInitialState(10);
103
- state.iteration = 3;
104
- addTaskToState(state, "test task");
105
-
106
- saveState(state, testStateFile);
107
- const loaded = loadState(testStateFile);
108
-
109
- expect(loaded).not.toBeNull();
110
- expect(loaded?.iteration).toBe(3);
111
- expect(loaded?.tasks).toHaveLength(1);
112
- expect(loaded?.tasks[0].description).toBe("test task");
113
- });
114
-
115
- it("should return null for non-existent file", () => {
116
- const loaded = loadState("/nonexistent/path/file.json");
117
- expect(loaded).toBeNull();
118
- });
119
-
120
- it("should return null for invalid JSON", () => {
121
- writeFileSync(testStateFile, "invalid json {{{");
122
- const loaded = loadState(testStateFile);
123
- expect(loaded).toBeNull();
124
- });
125
- });
126
-
127
- describe("buildIterationPrompt", () => {
128
- const defaultOpts = {
129
- completionMarker: "RALPH_DONE",
130
- progressFile: "ralph-progress.md",
131
- focusedTaskId: null as number | null,
132
- taskList: JSON.stringify([{ id: 1, description: "First task", status: "ready" }], null, 2),
133
- };
134
-
135
- it("should include completion marker", () => {
136
- const prompt = buildIterationPrompt(defaultOpts);
137
- expect(prompt).toContain("RALPH_DONE");
138
- });
139
-
140
- it("should include task list as JSON", () => {
141
- const prompt = buildIterationPrompt(defaultOpts);
142
- expect(prompt).toContain('"description": "First task"');
143
- });
144
-
145
- it("should include progress file reference", () => {
146
- const prompt = buildIterationPrompt(defaultOpts);
147
- expect(prompt).toContain("@ralph-progress.md");
148
- });
149
-
150
- it("should default to choosing task when no focusedTaskId", () => {
151
- const prompt = buildIterationPrompt(defaultOpts);
152
- expect(prompt).toContain("**Choose** which ready task");
153
- });
154
-
155
- it("should focus on specific task when focusedTaskId provided", () => {
156
- const prompt = buildIterationPrompt({ ...defaultOpts, focusedTaskId: 3 });
157
- expect(prompt).toContain("**Work on Task #3**");
158
- expect(prompt).not.toContain("**Choose** which ready task");
159
- });
160
-
161
- it("should include custom completion marker", () => {
162
- const prompt = buildIterationPrompt({ ...defaultOpts, completionMarker: "CUSTOM_MARKER" });
163
- expect(prompt).toContain("CUSTOM_MARKER");
164
- });
165
- });
166
-
167
- describe("extractOutputSummary", () => {
168
- it("should return last 5 lines joined", () => {
169
- const output = "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7";
170
- const summary = extractOutputSummary(output);
171
-
172
- expect(summary).toContain("line 3");
173
- expect(summary).toContain("line 7");
174
- expect(summary).not.toContain("line 1");
175
- expect(summary).not.toContain("line 2");
176
- });
177
-
178
- it("should filter empty lines", () => {
179
- const output = "line 1\n\n\nline 2\n\nline 3";
180
- const summary = extractOutputSummary(output);
181
-
182
- expect(summary).toBe("line 1 line 2 line 3");
183
- });
184
-
185
- it("should truncate long output", () => {
186
- const longLine = "x".repeat(300);
187
- const summary = extractOutputSummary(longLine, 200);
188
-
189
- expect(summary.length).toBe(203); // 200 + '...'
190
- expect(summary.endsWith("...")).toBe(true);
191
- });
192
-
193
- it('should return "(no output)" for empty string', () => {
194
- expect(extractOutputSummary("")).toBe("(no output)");
195
- expect(extractOutputSummary(" \n \n ")).toBe("(no output)");
196
- });
197
-
198
- it("should use custom maxLength", () => {
199
- const output = "a".repeat(100);
200
- const summary = extractOutputSummary(output, 50);
201
-
202
- expect(summary.length).toBe(53); // 50 + '...'
203
- });
204
- });
205
-
206
- describe("detectCompletionMarker", () => {
207
- it("should detect marker in output", () => {
208
- expect(detectCompletionMarker("Task complete RALPH_DONE", "RALPH_DONE")).toBe(true);
209
- });
210
-
211
- it("should return false when marker not present", () => {
212
- expect(detectCompletionMarker("Task still in progress", "RALPH_DONE")).toBe(false);
213
- });
214
-
215
- it("should detect marker anywhere in output", () => {
216
- expect(detectCompletionMarker("start RALPH_DONE end", "RALPH_DONE")).toBe(true);
217
- expect(detectCompletionMarker("RALPH_DONE", "RALPH_DONE")).toBe(true);
218
- expect(detectCompletionMarker("prefix\nRALPH_DONE\nsuffix", "RALPH_DONE")).toBe(true);
219
- });
220
-
221
- it("should work with custom markers", () => {
222
- expect(detectCompletionMarker("CUSTOM_DONE", "CUSTOM_DONE")).toBe(true);
223
- expect(detectCompletionMarker("<done/>", "<done/>")).toBe(true);
224
- });
225
-
226
- it("should be case-sensitive", () => {
227
- expect(detectCompletionMarker("ralph_done", "RALPH_DONE")).toBe(false);
228
- });
229
- });
230
-
231
- describe("state transitions", () => {
232
- it("should track iteration progress", () => {
233
- const state = createInitialState(5);
234
-
235
- expect(state.iteration).toBe(0);
236
-
237
- state.iteration++;
238
- expect(state.iteration).toBe(1);
239
- });
240
-
241
- it("should update status correctly", () => {
242
- const state = createInitialState(5);
243
-
244
- expect(state.status).toBe("running");
245
-
246
- state.status = "completed";
247
- expect(state.status).toBe("completed");
248
-
249
- state.status = "max_iterations_reached";
250
- expect(state.status).toBe("max_iterations_reached");
251
-
252
- state.status = "error";
253
- expect(state.status).toBe("error");
254
- });
255
- });
256
-
257
- describe("appendHistory", () => {
258
- it("should append history as JSON line", () => {
259
- const history: IterationHistory = {
260
- iteration: 1,
261
- startedAt: "2026-01-08T10:00:00Z",
262
- completedAt: "2026-01-08T10:01:00Z",
263
- durationMs: 60000,
264
- durationHuman: "1m 0s",
265
- outputSummary: "test output",
266
- markerFound: false,
267
- };
268
-
269
- appendHistory(history, testHistoryFile);
270
-
271
- const content = require("node:fs").readFileSync(testHistoryFile, "utf-8");
272
- const lines = content.trim().split("\n");
273
- expect(lines).toHaveLength(1);
274
-
275
- const parsed = JSON.parse(lines[0]);
276
- expect(parsed.iteration).toBe(1);
277
- expect(parsed.outputSummary).toBe("test output");
278
- });
279
-
280
- it("should append multiple entries", () => {
281
- const history1: IterationHistory = {
282
- iteration: 1,
283
- startedAt: "2026-01-08T10:00:00Z",
284
- completedAt: "2026-01-08T10:01:00Z",
285
- durationMs: 60000,
286
- durationHuman: "1m 0s",
287
- outputSummary: "first",
288
- markerFound: false,
289
- };
290
- const history2: IterationHistory = {
291
- iteration: 2,
292
- startedAt: "2026-01-08T10:02:00Z",
293
- completedAt: "2026-01-08T10:03:00Z",
294
- durationMs: 60000,
295
- durationHuman: "1m 0s",
296
- outputSummary: "second",
297
- markerFound: true,
298
- };
299
-
300
- appendHistory(history1, testHistoryFile);
301
- appendHistory(history2, testHistoryFile);
302
-
303
- const content = require("node:fs").readFileSync(testHistoryFile, "utf-8");
304
- const lines = content.trim().split("\n");
305
- expect(lines).toHaveLength(2);
306
-
307
- const parsed1 = JSON.parse(lines[0]);
308
- const parsed2 = JSON.parse(lines[1]);
309
- expect(parsed1.outputSummary).toBe("first");
310
- expect(parsed2.outputSummary).toBe("second");
311
- expect(parsed2.markerFound).toBe(true);
312
- });
313
- });
314
-
315
- describe("addTaskToState", () => {
316
- it("should add task with correct structure", () => {
317
- const state = createInitialState(10);
318
- const task = addTaskToState(state, "implement feature");
319
-
320
- expect(task.id).toBe(1);
321
- expect(task.description).toBe("implement feature");
322
- expect(task.status).toBe("ready");
323
- expect(task.addedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
324
- expect(task.completedAt).toBeUndefined();
325
- });
326
-
327
- it("should increment task IDs", () => {
328
- const state = createInitialState(10);
329
-
330
- const task1 = addTaskToState(state, "task 1");
331
- const task2 = addTaskToState(state, "task 2");
332
- const task3 = addTaskToState(state, "task 3");
333
-
334
- expect(task1.id).toBe(1);
335
- expect(task2.id).toBe(2);
336
- expect(task3.id).toBe(3);
337
- expect(state.tasks).toHaveLength(3);
338
- });
339
-
340
- it("should handle non-sequential IDs", () => {
341
- const state = createInitialState(10);
342
-
343
- // Simulate deleted task by adding with gap
344
- state.tasks.push({
345
- id: 5,
346
- description: "existing task",
347
- status: "done",
348
- addedAt: new Date().toISOString(),
349
- });
350
-
351
- const newTask = addTaskToState(state, "new task");
352
- expect(newTask.id).toBe(6);
353
- });
354
- });
355
-
356
- describe("formatTasksForPrompt", () => {
357
- it("should return placeholder for no tasks", () => {
358
- expect(formatTasksForPrompt([])).toBe("No tasks.");
359
- });
360
-
361
- it("should format tasks as markdown", () => {
362
- const tasks: RalphTask[] = [
363
- {
364
- id: 1,
365
- description: "implement feature",
366
- status: "ready",
367
- addedAt: new Date().toISOString(),
368
- },
369
- ];
370
-
371
- const formatted = formatTasksForPrompt(tasks);
372
-
373
- expect(formatted).toContain("- [ ] #1 implement feature");
374
- expect(formatted).toContain("`○ ready`");
375
- });
376
-
377
- it("should format multiple tasks as markdown list", () => {
378
- const tasks: RalphTask[] = [
379
- { id: 1, description: "task 1", status: "done", addedAt: "" },
380
- { id: 2, description: "task 2", status: "ready", addedAt: "" },
381
- { id: 3, description: "task 3", status: "ready", addedAt: "" },
382
- ];
383
-
384
- const formatted = formatTasksForPrompt(tasks);
385
-
386
- expect(formatted).toContain("- [x] #1 task 1 `✓ done`");
387
- expect(formatted).toContain("- [ ] #2 task 2 `○ ready`");
388
- expect(formatted).toContain("- [ ] #3 task 3 `○ ready`");
389
- });
390
- });
391
-
392
- describe("formatTasksAsMarkdown", () => {
393
- it("should return placeholder for empty tasks", () => {
394
- const formatted = formatTasksAsMarkdown([]);
395
- expect(formatted).toContain("# Tasks");
396
- expect(formatted).toContain("No tasks.");
397
- });
398
-
399
- it("should include summary counts", () => {
400
- const tasks: RalphTask[] = [
401
- { id: 1, description: "task 1", status: "done", addedAt: "" },
402
- { id: 2, description: "task 2", status: "ready", addedAt: "" },
403
- ];
404
-
405
- const formatted = formatTasksAsMarkdown(tasks);
406
-
407
- expect(formatted).toContain("**Total:** 2");
408
- expect(formatted).toContain("**Done:** 1");
409
- expect(formatted).toContain("**Ready:** 1");
410
- });
411
-
412
- it("should format done tasks with checked boxes", () => {
413
- const tasks: RalphTask[] = [
414
- { id: 1, description: "completed task", status: "done", addedAt: "" },
415
- ];
416
-
417
- const formatted = formatTasksAsMarkdown(tasks);
418
-
419
- expect(formatted).toContain("## Done");
420
- expect(formatted).toContain("- [x] **#1** completed task");
421
- expect(formatted).toContain("`✓ done`");
422
- });
423
-
424
- it("should format ready tasks with unchecked boxes", () => {
425
- const tasks: RalphTask[] = [
426
- { id: 2, description: "ready task", status: "ready", addedAt: "" },
427
- ];
428
-
429
- const formatted = formatTasksAsMarkdown(tasks);
430
-
431
- expect(formatted).toContain("## Ready");
432
- expect(formatted).toContain("- [ ] **#2** ready task");
433
- expect(formatted).toContain("`○ ready`");
434
- });
435
-
436
- it("should group tasks by status section", () => {
437
- const tasks: RalphTask[] = [
438
- { id: 1, description: "done task", status: "done", addedAt: "" },
439
- { id: 2, description: "ready task", status: "ready", addedAt: "" },
440
- ];
441
-
442
- const formatted = formatTasksAsMarkdown(tasks);
443
-
444
- expect(formatted).toContain("## Ready");
445
- expect(formatted).toContain("## Done");
446
- });
447
- });
448
-
449
- describe("loadState backwards compatibility", () => {
450
- it("should add empty tasks array if missing", () => {
451
- // Simulate old state without tasks
452
- const oldState = {
453
- version: 1,
454
- task: "old task", // legacy field
455
- startedAt: new Date().toISOString(),
456
- iteration: 0,
457
- maxIterations: 10,
458
- status: "running",
459
- history: [],
460
- };
461
- writeFileSync(testStateFile, JSON.stringify(oldState));
462
-
463
- const loaded = loadState(testStateFile);
464
-
465
- expect(loaded).not.toBeNull();
466
- expect(loaded?.tasks).toEqual([]);
467
- });
468
- });
469
-
470
- describe("formatPlanAsMarkdown", () => {
471
- it("should include plan header and summary", () => {
472
- const state = createInitialState(10);
473
- addTaskToState(state, "task 1");
474
-
475
- const formatted = formatPlanAsMarkdown(state.tasks, state);
476
-
477
- expect(formatted).toContain("# Ralph Plan");
478
- expect(formatted).toContain("## Summary");
479
- expect(formatted).toContain("**Status:** running");
480
- expect(formatted).toContain("**Total Tasks:** 1");
481
- });
482
-
483
- it("should include tasks section", () => {
484
- const state = createInitialState(10);
485
- addTaskToState(state, "implement feature");
486
-
487
- const formatted = formatPlanAsMarkdown(state.tasks, state);
488
-
489
- expect(formatted).toContain("## Tasks");
490
- expect(formatted).toContain("**#1** implement feature");
491
- });
492
-
493
- it("should include mermaid graph section", () => {
494
- const state = createInitialState(10);
495
- addTaskToState(state, "task 1");
496
- addTaskToState(state, "task 2");
497
-
498
- const formatted = formatPlanAsMarkdown(state.tasks, state);
499
-
500
- expect(formatted).toContain("## Progress Graph");
501
- expect(formatted).toContain("```mermaid");
502
- expect(formatted).toContain("graph LR");
503
- expect(formatted).toContain("classDef done fill:#22c55e");
504
- expect(formatted).toContain("classDef ready fill:#94a3b8");
505
- });
506
-
507
- it("should format done tasks correctly in mermaid", () => {
508
- const state = createInitialState(10);
509
- const task = addTaskToState(state, "done task");
510
- task.status = "done";
511
-
512
- const formatted = formatPlanAsMarkdown(state.tasks, state);
513
-
514
- expect(formatted).toContain('T1["#1: done task"]:::done');
515
- });
516
-
517
- it("should truncate long descriptions in mermaid", () => {
518
- const state = createInitialState(10);
519
- addTaskToState(
520
- state,
521
- "This is a very long task description that should be truncated for the mermaid graph",
522
- );
523
-
524
- const formatted = formatPlanAsMarkdown(state.tasks, state);
525
-
526
- // Mermaid section should have truncated description
527
- expect(formatted).toContain('T1["#1: This is a very long task de..."]');
528
- // But the Tasks section should have full description
529
- expect(formatted).toContain(
530
- "**#1** This is a very long task description that should be truncated for the mermaid graph",
531
- );
532
- });
533
-
534
- it("should include session ID if present", () => {
535
- const state = createInitialState(10);
536
- state.sessionId = "test-session-id-1234567890";
537
- addTaskToState(state, "task");
538
-
539
- const formatted = formatPlanAsMarkdown(state.tasks, state);
540
-
541
- expect(formatted).toContain("**Session ID:** test-ses...");
542
- });
543
-
544
- it("should show iteration progress", () => {
545
- const state = createInitialState(10);
546
- state.iteration = 3;
547
- addTaskToState(state, "task");
548
-
549
- const formatted = formatPlanAsMarkdown(state.tasks, state);
550
-
551
- expect(formatted).toContain("**Iteration:** 3/10");
552
- });
553
- });
554
-
555
- describe("formatPlanAsJson", () => {
556
- it("should return valid JSON", () => {
557
- const state = createInitialState(10);
558
- addTaskToState(state, "task 1");
559
-
560
- const json = formatPlanAsJson(state.tasks, state);
561
- const parsed = JSON.parse(json);
562
-
563
- expect(parsed.status).toBe("running");
564
- expect(parsed.tasks).toHaveLength(1);
565
- });
566
-
567
- it("should include summary counts", () => {
568
- const state = createInitialState(10);
569
- const task1 = addTaskToState(state, "done task");
570
- task1.status = "done";
571
- addTaskToState(state, "ready task");
572
-
573
- const json = formatPlanAsJson(state.tasks, state);
574
- const parsed = JSON.parse(json);
575
-
576
- expect(parsed.summary.total).toBe(2);
577
- expect(parsed.summary.done).toBe(1);
578
- expect(parsed.summary.ready).toBe(1);
579
- });
580
-
581
- it("should include all task fields", () => {
582
- const state = createInitialState(10);
583
- const task = addTaskToState(state, "test task");
584
- task.status = "done";
585
- task.completedAt = "2026-01-10T12:00:00Z";
586
-
587
- const json = formatPlanAsJson(state.tasks, state);
588
- const parsed = JSON.parse(json);
589
-
590
- expect(parsed.tasks[0].id).toBe(1);
591
- expect(parsed.tasks[0].description).toBe("test task");
592
- expect(parsed.tasks[0].status).toBe("done");
593
- expect(parsed.tasks[0].addedAt).toBeDefined();
594
- expect(parsed.tasks[0].completedAt).toBe("2026-01-10T12:00:00Z");
595
- });
596
-
597
- it("should include iteration and maxIterations", () => {
598
- const state = createInitialState(15);
599
- state.iteration = 5;
600
- addTaskToState(state, "task");
601
-
602
- const json = formatPlanAsJson(state.tasks, state);
603
- const parsed = JSON.parse(json);
604
-
605
- expect(parsed.iteration).toBe(5);
606
- expect(parsed.maxIterations).toBe(15);
607
- });
608
-
609
- it("should include sessionId if present", () => {
610
- const state = createInitialState(10);
611
- state.sessionId = "test-session-uuid";
612
- addTaskToState(state, "task");
613
-
614
- const json = formatPlanAsJson(state.tasks, state);
615
- const parsed = JSON.parse(json);
616
-
617
- expect(parsed.sessionId).toBe("test-session-uuid");
618
- });
619
- });
620
-
621
- describe("markDone functionality", () => {
622
- it("should mark task as done and add completedAt", () => {
623
- const state = createInitialState(10);
624
- addTaskToState(state, "task 1");
625
- addTaskToState(state, "task 2");
626
-
627
- // Simulate marking task 1 as done
628
- const task = state.tasks.find((t) => t.id === 1);
629
- expect(task).toBeDefined();
630
- expect(task?.status).toBe("ready");
631
-
632
- task!.status = "done";
633
- task!.completedAt = new Date().toISOString();
634
-
635
- expect(task?.status).toBe("done");
636
- expect(task?.completedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
637
- });
638
-
639
- it("should find task by ID", () => {
640
- const state = createInitialState(10);
641
- addTaskToState(state, "task 1");
642
- addTaskToState(state, "task 2");
643
- addTaskToState(state, "task 3");
644
-
645
- const task = state.tasks.find((t) => t.id === 2);
646
- expect(task).toBeDefined();
647
- expect(task?.description).toBe("task 2");
648
- });
649
-
650
- it("should return undefined for non-existent task ID", () => {
651
- const state = createInitialState(10);
652
- addTaskToState(state, "task 1");
653
-
654
- const task = state.tasks.find((t) => t.id === 99);
655
- expect(task).toBeUndefined();
656
- });
657
-
658
- it("should persist marked-done task to file", () => {
659
- const state = createInitialState(10);
660
- addTaskToState(state, "task 1");
661
-
662
- const task = state.tasks.find((t) => t.id === 1)!;
663
- task.status = "done";
664
- task.completedAt = new Date().toISOString();
665
-
666
- saveState(state, testStateFile);
667
- const loaded = loadState(testStateFile);
668
-
669
- expect(loaded?.tasks[0].status).toBe("done");
670
- expect(loaded?.tasks[0].completedAt).toBeDefined();
671
- });
672
- });
673
- });