@towles/tool 0.0.41 → 0.0.48
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/README.md +67 -109
- package/package.json +51 -41
- package/src/commands/base.ts +3 -18
- package/src/commands/config.ts +9 -8
- package/src/commands/doctor.ts +4 -1
- package/src/commands/gh/branch-clean.ts +10 -4
- package/src/commands/gh/branch.ts +6 -3
- package/src/commands/gh/pr.ts +10 -3
- package/src/commands/graph-template.html +1214 -0
- package/src/commands/graph.test.ts +176 -0
- package/src/commands/graph.ts +970 -0
- package/src/commands/install.ts +8 -2
- package/src/commands/journal/daily-notes.ts +9 -5
- package/src/commands/journal/meeting.ts +12 -6
- package/src/commands/journal/note.ts +12 -6
- package/src/commands/ralph/plan/add.ts +75 -0
- package/src/commands/ralph/plan/done.ts +82 -0
- package/src/commands/ralph/{task → plan}/list.test.ts +5 -5
- package/src/commands/ralph/{task → plan}/list.ts +28 -39
- package/src/commands/ralph/plan/remove.ts +71 -0
- package/src/commands/ralph/run.test.ts +521 -0
- package/src/commands/ralph/run.ts +126 -189
- package/src/commands/ralph/show.ts +88 -0
- package/src/config/settings.ts +8 -27
- package/src/{commands/ralph/lib → lib/ralph}/execution.ts +4 -14
- package/src/lib/ralph/formatter.ts +238 -0
- package/src/{commands/ralph/lib → lib/ralph}/state.ts +17 -42
- package/src/utils/date-utils.test.ts +2 -1
- package/src/utils/date-utils.ts +2 -2
- package/LICENSE.md +0 -20
- package/src/commands/index.ts +0 -55
- package/src/commands/observe/graph.test.ts +0 -89
- package/src/commands/observe/graph.ts +0 -1640
- package/src/commands/observe/report.ts +0 -166
- package/src/commands/observe/session.ts +0 -385
- package/src/commands/observe/setup.ts +0 -180
- package/src/commands/observe/status.ts +0 -146
- package/src/commands/ralph/lib/formatter.ts +0 -298
- package/src/commands/ralph/lib/marker.ts +0 -108
- package/src/commands/ralph/marker/create.ts +0 -23
- package/src/commands/ralph/plan.ts +0 -73
- package/src/commands/ralph/progress.ts +0 -44
- package/src/commands/ralph/ralph.test.ts +0 -673
- package/src/commands/ralph/task/add.ts +0 -105
- package/src/commands/ralph/task/done.ts +0 -73
- package/src/commands/ralph/task/remove.ts +0 -62
- package/src/config/context.ts +0 -7
- package/src/constants.ts +0 -3
- package/src/utils/anthropic/types.ts +0 -158
- package/src/utils/exec.ts +0 -8
- package/src/utils/git/git.ts +0 -25
- /package/src/{commands → lib}/journal/utils.ts +0 -0
- /package/src/{commands/ralph/lib → lib/ralph}/index.ts +0 -0
|
@@ -0,0 +1,521 @@
|
|
|
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 { RalphPlan, IterationHistory } from "../../lib/ralph/index.js";
|
|
9
|
+
import {
|
|
10
|
+
createInitialState,
|
|
11
|
+
saveState,
|
|
12
|
+
loadState,
|
|
13
|
+
addPlanToState,
|
|
14
|
+
formatPlansAsMarkdown,
|
|
15
|
+
formatPlanAsMarkdown,
|
|
16
|
+
formatPlanAsJson,
|
|
17
|
+
buildIterationPrompt,
|
|
18
|
+
extractOutputSummary,
|
|
19
|
+
detectCompletionMarker,
|
|
20
|
+
appendHistory,
|
|
21
|
+
DEFAULT_MAX_ITERATIONS,
|
|
22
|
+
DEFAULT_STATE_FILE,
|
|
23
|
+
DEFAULT_HISTORY_FILE,
|
|
24
|
+
DEFAULT_COMPLETION_MARKER,
|
|
25
|
+
CLAUDE_DEFAULT_ARGS,
|
|
26
|
+
} from "../../lib/ralph/index.js";
|
|
27
|
+
|
|
28
|
+
describe("ralph-loop", () => {
|
|
29
|
+
const testStateFile = join(tmpdir(), `ralph-test-${Date.now()}.json`);
|
|
30
|
+
const testHistoryFile = join(tmpdir(), `ralph-history-${Date.now()}.log`);
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
// Cleanup test files
|
|
34
|
+
if (existsSync(testStateFile)) {
|
|
35
|
+
unlinkSync(testStateFile);
|
|
36
|
+
}
|
|
37
|
+
if (existsSync(testHistoryFile)) {
|
|
38
|
+
unlinkSync(testHistoryFile);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("constants", () => {
|
|
43
|
+
it("should have correct default values", () => {
|
|
44
|
+
expect(DEFAULT_MAX_ITERATIONS).toBe(10);
|
|
45
|
+
expect(DEFAULT_STATE_FILE).toBe("./.claude/.ralph/ralph-state.local.json");
|
|
46
|
+
expect(DEFAULT_HISTORY_FILE).toBe("./.claude/.ralph/ralph-history.local.log");
|
|
47
|
+
expect(DEFAULT_COMPLETION_MARKER).toBe("RALPH_DONE");
|
|
48
|
+
expect(CLAUDE_DEFAULT_ARGS).toEqual([
|
|
49
|
+
"--print",
|
|
50
|
+
"--verbose",
|
|
51
|
+
"--output-format",
|
|
52
|
+
"stream-json",
|
|
53
|
+
"--permission-mode",
|
|
54
|
+
"bypassPermissions",
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("createInitialState", () => {
|
|
60
|
+
it("should create state with correct structure", () => {
|
|
61
|
+
const state = createInitialState();
|
|
62
|
+
|
|
63
|
+
expect(state.version).toBe(1);
|
|
64
|
+
expect(state.status).toBe("running");
|
|
65
|
+
expect(state.plans).toEqual([]);
|
|
66
|
+
expect(state.startedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("saveState and loadState", () => {
|
|
71
|
+
it("should save and load state correctly", () => {
|
|
72
|
+
const state = createInitialState();
|
|
73
|
+
addPlanToState(state, "test plan");
|
|
74
|
+
|
|
75
|
+
saveState(state, testStateFile);
|
|
76
|
+
const loaded = loadState(testStateFile);
|
|
77
|
+
|
|
78
|
+
expect(loaded).not.toBeNull();
|
|
79
|
+
expect(loaded?.plans).toHaveLength(1);
|
|
80
|
+
expect(loaded?.plans[0].description).toBe("test plan");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should return null for non-existent file", () => {
|
|
84
|
+
const loaded = loadState("/nonexistent/path/file.json");
|
|
85
|
+
expect(loaded).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should return null for invalid JSON", () => {
|
|
89
|
+
writeFileSync(testStateFile, "invalid json {{{");
|
|
90
|
+
const loaded = loadState(testStateFile);
|
|
91
|
+
expect(loaded).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("buildIterationPrompt", () => {
|
|
96
|
+
const testPlan: RalphPlan = {
|
|
97
|
+
id: 1,
|
|
98
|
+
description: "First plan",
|
|
99
|
+
status: "ready",
|
|
100
|
+
addedAt: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
it("should include completion marker", () => {
|
|
104
|
+
const prompt = buildIterationPrompt({ completionMarker: "RALPH_DONE", plan: testPlan });
|
|
105
|
+
expect(prompt).toContain("RALPH_DONE");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should include plan id and description", () => {
|
|
109
|
+
const prompt = buildIterationPrompt({ completionMarker: "RALPH_DONE", plan: testPlan });
|
|
110
|
+
expect(prompt).toContain("#1: First plan");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should include mark done command with plan id", () => {
|
|
114
|
+
const prompt = buildIterationPrompt({ completionMarker: "RALPH_DONE", plan: testPlan });
|
|
115
|
+
expect(prompt).toContain("tt ralph plan done 1");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should include custom completion marker", () => {
|
|
119
|
+
const prompt = buildIterationPrompt({ completionMarker: "CUSTOM_MARKER", plan: testPlan });
|
|
120
|
+
expect(prompt).toContain("CUSTOM_MARKER");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("extractOutputSummary", () => {
|
|
125
|
+
it("should return last 5 lines joined", () => {
|
|
126
|
+
const output = "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7";
|
|
127
|
+
const summary = extractOutputSummary(output);
|
|
128
|
+
|
|
129
|
+
expect(summary).toContain("line 3");
|
|
130
|
+
expect(summary).toContain("line 7");
|
|
131
|
+
expect(summary).not.toContain("line 1");
|
|
132
|
+
expect(summary).not.toContain("line 2");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should filter empty lines", () => {
|
|
136
|
+
const output = "line 1\n\n\nline 2\n\nline 3";
|
|
137
|
+
const summary = extractOutputSummary(output);
|
|
138
|
+
|
|
139
|
+
expect(summary).toBe("line 1 line 2 line 3");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should truncate long output", () => {
|
|
143
|
+
const longLine = "x".repeat(300);
|
|
144
|
+
const summary = extractOutputSummary(longLine, 200);
|
|
145
|
+
|
|
146
|
+
expect(summary.length).toBe(203); // 200 + '...'
|
|
147
|
+
expect(summary.endsWith("...")).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should return "(no output)" for empty string', () => {
|
|
151
|
+
expect(extractOutputSummary("")).toBe("(no output)");
|
|
152
|
+
expect(extractOutputSummary(" \n \n ")).toBe("(no output)");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should use custom maxLength", () => {
|
|
156
|
+
const output = "a".repeat(100);
|
|
157
|
+
const summary = extractOutputSummary(output, 50);
|
|
158
|
+
|
|
159
|
+
expect(summary.length).toBe(53); // 50 + '...'
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("detectCompletionMarker", () => {
|
|
164
|
+
it("should detect marker in output", () => {
|
|
165
|
+
expect(detectCompletionMarker("Task complete RALPH_DONE", "RALPH_DONE")).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should return false when marker not present", () => {
|
|
169
|
+
expect(detectCompletionMarker("Task still in progress", "RALPH_DONE")).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should detect marker anywhere in output", () => {
|
|
173
|
+
expect(detectCompletionMarker("start RALPH_DONE end", "RALPH_DONE")).toBe(true);
|
|
174
|
+
expect(detectCompletionMarker("RALPH_DONE", "RALPH_DONE")).toBe(true);
|
|
175
|
+
expect(detectCompletionMarker("prefix\nRALPH_DONE\nsuffix", "RALPH_DONE")).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should work with custom markers", () => {
|
|
179
|
+
expect(detectCompletionMarker("CUSTOM_DONE", "CUSTOM_DONE")).toBe(true);
|
|
180
|
+
expect(detectCompletionMarker("<done/>", "<done/>")).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should be case-sensitive", () => {
|
|
184
|
+
expect(detectCompletionMarker("ralph_done", "RALPH_DONE")).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("state transitions", () => {
|
|
189
|
+
it("should update status correctly", () => {
|
|
190
|
+
const state = createInitialState();
|
|
191
|
+
|
|
192
|
+
expect(state.status).toBe("running");
|
|
193
|
+
|
|
194
|
+
state.status = "completed";
|
|
195
|
+
expect(state.status).toBe("completed");
|
|
196
|
+
|
|
197
|
+
state.status = "max_iterations_reached";
|
|
198
|
+
expect(state.status).toBe("max_iterations_reached");
|
|
199
|
+
|
|
200
|
+
state.status = "error";
|
|
201
|
+
expect(state.status).toBe("error");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("appendHistory", () => {
|
|
206
|
+
it("should append history as JSON line", () => {
|
|
207
|
+
const history: IterationHistory = {
|
|
208
|
+
iteration: 1,
|
|
209
|
+
startedAt: "2026-01-08T10:00:00Z",
|
|
210
|
+
completedAt: "2026-01-08T10:01:00Z",
|
|
211
|
+
durationMs: 60000,
|
|
212
|
+
durationHuman: "1m 0s",
|
|
213
|
+
outputSummary: "test output",
|
|
214
|
+
markerFound: false,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
appendHistory(history, testHistoryFile);
|
|
218
|
+
|
|
219
|
+
const content = require("node:fs").readFileSync(testHistoryFile, "utf-8");
|
|
220
|
+
const lines = content.trim().split("\n");
|
|
221
|
+
expect(lines).toHaveLength(1);
|
|
222
|
+
|
|
223
|
+
const parsed = JSON.parse(lines[0]);
|
|
224
|
+
expect(parsed.iteration).toBe(1);
|
|
225
|
+
expect(parsed.outputSummary).toBe("test output");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should append multiple entries", () => {
|
|
229
|
+
const history1: IterationHistory = {
|
|
230
|
+
iteration: 1,
|
|
231
|
+
startedAt: "2026-01-08T10:00:00Z",
|
|
232
|
+
completedAt: "2026-01-08T10:01:00Z",
|
|
233
|
+
durationMs: 60000,
|
|
234
|
+
durationHuman: "1m 0s",
|
|
235
|
+
outputSummary: "first",
|
|
236
|
+
markerFound: false,
|
|
237
|
+
};
|
|
238
|
+
const history2: IterationHistory = {
|
|
239
|
+
iteration: 2,
|
|
240
|
+
startedAt: "2026-01-08T10:02:00Z",
|
|
241
|
+
completedAt: "2026-01-08T10:03:00Z",
|
|
242
|
+
durationMs: 60000,
|
|
243
|
+
durationHuman: "1m 0s",
|
|
244
|
+
outputSummary: "second",
|
|
245
|
+
markerFound: true,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
appendHistory(history1, testHistoryFile);
|
|
249
|
+
appendHistory(history2, testHistoryFile);
|
|
250
|
+
|
|
251
|
+
const content = require("node:fs").readFileSync(testHistoryFile, "utf-8");
|
|
252
|
+
const lines = content.trim().split("\n");
|
|
253
|
+
expect(lines).toHaveLength(2);
|
|
254
|
+
|
|
255
|
+
const parsed1 = JSON.parse(lines[0]);
|
|
256
|
+
const parsed2 = JSON.parse(lines[1]);
|
|
257
|
+
expect(parsed1.outputSummary).toBe("first");
|
|
258
|
+
expect(parsed2.outputSummary).toBe("second");
|
|
259
|
+
expect(parsed2.markerFound).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("addPlanToState", () => {
|
|
264
|
+
it("should add plan with correct structure", () => {
|
|
265
|
+
const state = createInitialState();
|
|
266
|
+
const plan = addPlanToState(state, "implement feature");
|
|
267
|
+
|
|
268
|
+
expect(plan.id).toBe(1);
|
|
269
|
+
expect(plan.description).toBe("implement feature");
|
|
270
|
+
expect(plan.status).toBe("ready");
|
|
271
|
+
expect(plan.addedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
272
|
+
expect(plan.completedAt).toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should increment plan IDs", () => {
|
|
276
|
+
const state = createInitialState();
|
|
277
|
+
|
|
278
|
+
const plan1 = addPlanToState(state, "plan 1");
|
|
279
|
+
const plan2 = addPlanToState(state, "plan 2");
|
|
280
|
+
const plan3 = addPlanToState(state, "plan 3");
|
|
281
|
+
|
|
282
|
+
expect(plan1.id).toBe(1);
|
|
283
|
+
expect(plan2.id).toBe(2);
|
|
284
|
+
expect(plan3.id).toBe(3);
|
|
285
|
+
expect(state.plans).toHaveLength(3);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should handle non-sequential IDs", () => {
|
|
289
|
+
const state = createInitialState();
|
|
290
|
+
|
|
291
|
+
// Simulate deleted plan by adding with gap
|
|
292
|
+
state.plans.push({
|
|
293
|
+
id: 5,
|
|
294
|
+
description: "existing plan",
|
|
295
|
+
status: "done",
|
|
296
|
+
addedAt: new Date().toISOString(),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const newPlan = addPlanToState(state, "new plan");
|
|
300
|
+
expect(newPlan.id).toBe(6);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe("formatPlansAsMarkdown", () => {
|
|
305
|
+
it("should return placeholder for empty plans", () => {
|
|
306
|
+
const formatted = formatPlansAsMarkdown([]);
|
|
307
|
+
expect(formatted).toContain("# Plans");
|
|
308
|
+
expect(formatted).toContain("No plans.");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("should include summary counts", () => {
|
|
312
|
+
const plans: RalphPlan[] = [
|
|
313
|
+
{ id: 1, description: "plan 1", status: "done", addedAt: "" },
|
|
314
|
+
{ id: 2, description: "plan 2", status: "ready", addedAt: "" },
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
const formatted = formatPlansAsMarkdown(plans);
|
|
318
|
+
|
|
319
|
+
expect(formatted).toContain("**Total:** 2");
|
|
320
|
+
expect(formatted).toContain("**Done:** 1");
|
|
321
|
+
expect(formatted).toContain("**Ready:** 1");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("should format done plans with checked boxes", () => {
|
|
325
|
+
const plans: RalphPlan[] = [
|
|
326
|
+
{ id: 1, description: "completed plan", status: "done", addedAt: "" },
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
const formatted = formatPlansAsMarkdown(plans);
|
|
330
|
+
|
|
331
|
+
expect(formatted).toContain("## Done");
|
|
332
|
+
expect(formatted).toContain("- [x] **#1** completed plan");
|
|
333
|
+
expect(formatted).toContain("`✓ done`");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("should format ready plans with unchecked boxes", () => {
|
|
337
|
+
const plans: RalphPlan[] = [
|
|
338
|
+
{ id: 2, description: "ready plan", status: "ready", addedAt: "" },
|
|
339
|
+
];
|
|
340
|
+
|
|
341
|
+
const formatted = formatPlansAsMarkdown(plans);
|
|
342
|
+
|
|
343
|
+
expect(formatted).toContain("## Ready");
|
|
344
|
+
expect(formatted).toContain("- [ ] **#2** ready plan");
|
|
345
|
+
expect(formatted).toContain("`○ ready`");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("should group plans by status section", () => {
|
|
349
|
+
const plans: RalphPlan[] = [
|
|
350
|
+
{ id: 1, description: "done plan", status: "done", addedAt: "" },
|
|
351
|
+
{ id: 2, description: "ready plan", status: "ready", addedAt: "" },
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
const formatted = formatPlansAsMarkdown(plans);
|
|
355
|
+
|
|
356
|
+
expect(formatted).toContain("## Ready");
|
|
357
|
+
expect(formatted).toContain("## Done");
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("formatPlanAsMarkdown", () => {
|
|
362
|
+
it("should include plan header and summary", () => {
|
|
363
|
+
const state = createInitialState();
|
|
364
|
+
addPlanToState(state, "plan 1");
|
|
365
|
+
|
|
366
|
+
const formatted = formatPlanAsMarkdown(state.plans, state);
|
|
367
|
+
|
|
368
|
+
expect(formatted).toContain("# Ralph Plan");
|
|
369
|
+
expect(formatted).toContain("## Summary");
|
|
370
|
+
expect(formatted).toContain("**Status:** running");
|
|
371
|
+
expect(formatted).toContain("**Total:** 1");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("should include plans section", () => {
|
|
375
|
+
const state = createInitialState();
|
|
376
|
+
addPlanToState(state, "implement feature");
|
|
377
|
+
|
|
378
|
+
const formatted = formatPlanAsMarkdown(state.plans, state);
|
|
379
|
+
|
|
380
|
+
expect(formatted).toContain("## Plans");
|
|
381
|
+
expect(formatted).toContain("**#1** implement feature");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("should include mermaid graph section", () => {
|
|
385
|
+
const state = createInitialState();
|
|
386
|
+
addPlanToState(state, "plan 1");
|
|
387
|
+
addPlanToState(state, "plan 2");
|
|
388
|
+
|
|
389
|
+
const formatted = formatPlanAsMarkdown(state.plans, state);
|
|
390
|
+
|
|
391
|
+
expect(formatted).toContain("## Progress Graph");
|
|
392
|
+
expect(formatted).toContain("```mermaid");
|
|
393
|
+
expect(formatted).toContain("graph LR");
|
|
394
|
+
expect(formatted).toContain("classDef done fill:#22c55e");
|
|
395
|
+
expect(formatted).toContain("classDef ready fill:#94a3b8");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("should format done plans correctly in mermaid", () => {
|
|
399
|
+
const state = createInitialState();
|
|
400
|
+
const plan = addPlanToState(state, "done plan");
|
|
401
|
+
plan.status = "done";
|
|
402
|
+
|
|
403
|
+
const formatted = formatPlanAsMarkdown(state.plans, state);
|
|
404
|
+
|
|
405
|
+
expect(formatted).toContain('P1["#1: done plan"]:::done');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("should truncate long descriptions in mermaid", () => {
|
|
409
|
+
const state = createInitialState();
|
|
410
|
+
addPlanToState(
|
|
411
|
+
state,
|
|
412
|
+
"This is a very long plan description that should be truncated for the mermaid graph",
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
const formatted = formatPlanAsMarkdown(state.plans, state);
|
|
416
|
+
|
|
417
|
+
// Mermaid section should have truncated description
|
|
418
|
+
expect(formatted).toContain('P1["#1: This is a very long plan de..."]');
|
|
419
|
+
// But the Plans section should have full description
|
|
420
|
+
expect(formatted).toContain(
|
|
421
|
+
"**#1** This is a very long plan description that should be truncated for the mermaid graph",
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe("formatPlanAsJson", () => {
|
|
427
|
+
it("should return valid JSON", () => {
|
|
428
|
+
const state = createInitialState();
|
|
429
|
+
addPlanToState(state, "plan 1");
|
|
430
|
+
|
|
431
|
+
const json = formatPlanAsJson(state.plans, state);
|
|
432
|
+
const parsed = JSON.parse(json);
|
|
433
|
+
|
|
434
|
+
expect(parsed.status).toBe("running");
|
|
435
|
+
expect(parsed.plans).toHaveLength(1);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("should include summary counts", () => {
|
|
439
|
+
const state = createInitialState();
|
|
440
|
+
const plan1 = addPlanToState(state, "done plan");
|
|
441
|
+
plan1.status = "done";
|
|
442
|
+
addPlanToState(state, "ready plan");
|
|
443
|
+
|
|
444
|
+
const json = formatPlanAsJson(state.plans, state);
|
|
445
|
+
const parsed = JSON.parse(json);
|
|
446
|
+
|
|
447
|
+
expect(parsed.summary.total).toBe(2);
|
|
448
|
+
expect(parsed.summary.done).toBe(1);
|
|
449
|
+
expect(parsed.summary.ready).toBe(1);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("should include all plan fields", () => {
|
|
453
|
+
const state = createInitialState();
|
|
454
|
+
const plan = addPlanToState(state, "test plan");
|
|
455
|
+
plan.status = "done";
|
|
456
|
+
plan.completedAt = "2026-01-10T12:00:00Z";
|
|
457
|
+
|
|
458
|
+
const json = formatPlanAsJson(state.plans, state);
|
|
459
|
+
const parsed = JSON.parse(json);
|
|
460
|
+
|
|
461
|
+
expect(parsed.plans[0].id).toBe(1);
|
|
462
|
+
expect(parsed.plans[0].description).toBe("test plan");
|
|
463
|
+
expect(parsed.plans[0].status).toBe("done");
|
|
464
|
+
expect(parsed.plans[0].addedAt).toBeDefined();
|
|
465
|
+
expect(parsed.plans[0].completedAt).toBe("2026-01-10T12:00:00Z");
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
describe("markDone functionality", () => {
|
|
470
|
+
it("should mark plan as done and add completedAt", () => {
|
|
471
|
+
const state = createInitialState();
|
|
472
|
+
addPlanToState(state, "plan 1");
|
|
473
|
+
addPlanToState(state, "plan 2");
|
|
474
|
+
|
|
475
|
+
// Simulate marking plan 1 as done
|
|
476
|
+
const plan = state.plans.find((t) => t.id === 1);
|
|
477
|
+
expect(plan).toBeDefined();
|
|
478
|
+
expect(plan?.status).toBe("ready");
|
|
479
|
+
|
|
480
|
+
plan!.status = "done";
|
|
481
|
+
plan!.completedAt = new Date().toISOString();
|
|
482
|
+
|
|
483
|
+
expect(plan?.status).toBe("done");
|
|
484
|
+
expect(plan?.completedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("should find plan by ID", () => {
|
|
488
|
+
const state = createInitialState();
|
|
489
|
+
addPlanToState(state, "plan 1");
|
|
490
|
+
addPlanToState(state, "plan 2");
|
|
491
|
+
addPlanToState(state, "plan 3");
|
|
492
|
+
|
|
493
|
+
const plan = state.plans.find((p) => p.id === 2);
|
|
494
|
+
expect(plan).toBeDefined();
|
|
495
|
+
expect(plan?.description).toBe("plan 2");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("should return undefined for non-existent plan ID", () => {
|
|
499
|
+
const state = createInitialState();
|
|
500
|
+
addPlanToState(state, "plan 1");
|
|
501
|
+
|
|
502
|
+
const plan = state.plans.find((t) => t.id === 99);
|
|
503
|
+
expect(plan).toBeUndefined();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("should persist marked-done plan to file", () => {
|
|
507
|
+
const state = createInitialState();
|
|
508
|
+
addPlanToState(state, "plan 1");
|
|
509
|
+
|
|
510
|
+
const plan = state.plans.find((t) => t.id === 1)!;
|
|
511
|
+
plan.status = "done";
|
|
512
|
+
plan.completedAt = new Date().toISOString();
|
|
513
|
+
|
|
514
|
+
saveState(state, testStateFile);
|
|
515
|
+
const loaded = loadState(testStateFile);
|
|
516
|
+
|
|
517
|
+
expect(loaded?.plans[0].status).toBe("done");
|
|
518
|
+
expect(loaded?.plans[0].completedAt).toBeDefined();
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
});
|