@towles/tool 0.0.53 → 0.0.55
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 +82 -72
- package/package.json +8 -7
- package/src/commands/auto-claude.ts +219 -0
- package/src/commands/doctor.ts +1 -34
- package/src/config/settings.ts +0 -10
- package/src/lib/auto-claude/config.test.ts +53 -0
- package/src/lib/auto-claude/config.ts +68 -0
- package/src/lib/auto-claude/index.ts +14 -0
- package/src/lib/auto-claude/pipeline.test.ts +14 -0
- package/src/lib/auto-claude/pipeline.ts +64 -0
- package/src/lib/auto-claude/prompt-templates/01-prompt-research.md +28 -0
- package/src/lib/auto-claude/prompt-templates/02-prompt-plan.md +28 -0
- package/src/lib/auto-claude/prompt-templates/03-prompt-plan-annotations.md +21 -0
- package/src/lib/auto-claude/prompt-templates/04-prompt-plan-implementation.md +33 -0
- package/src/lib/auto-claude/prompt-templates/05-prompt-implement.md +31 -0
- package/src/lib/auto-claude/prompt-templates/06-prompt-review.md +30 -0
- package/src/lib/auto-claude/prompt-templates/07-prompt-refresh.md +39 -0
- package/src/lib/auto-claude/prompt-templates/index.test.ts +145 -0
- package/src/lib/auto-claude/prompt-templates/index.ts +44 -0
- package/src/lib/auto-claude/steps/create-pr.ts +93 -0
- package/src/lib/auto-claude/steps/fetch-issues.ts +64 -0
- package/src/lib/auto-claude/steps/implement.ts +63 -0
- package/src/lib/auto-claude/steps/plan-annotations.ts +54 -0
- package/src/lib/auto-claude/steps/plan-implementation.ts +14 -0
- package/src/lib/auto-claude/steps/plan.ts +14 -0
- package/src/lib/auto-claude/steps/refresh.ts +114 -0
- package/src/lib/auto-claude/steps/remove-label.ts +22 -0
- package/src/lib/auto-claude/steps/research.ts +21 -0
- package/src/lib/auto-claude/steps/review.ts +14 -0
- package/src/lib/auto-claude/utils.test.ts +136 -0
- package/src/lib/auto-claude/utils.ts +334 -0
- package/src/commands/ralph/plan/add.ts +0 -69
- package/src/commands/ralph/plan/done.ts +0 -82
- package/src/commands/ralph/plan/list.test.ts +0 -48
- package/src/commands/ralph/plan/list.ts +0 -100
- package/src/commands/ralph/plan/remove.ts +0 -71
- package/src/commands/ralph/run.test.ts +0 -607
- package/src/commands/ralph/run.ts +0 -362
- package/src/commands/ralph/show.ts +0 -88
- package/src/lib/ralph/execution.ts +0 -292
- package/src/lib/ralph/formatter.ts +0 -240
- package/src/lib/ralph/index.ts +0 -4
- package/src/lib/ralph/state.ts +0 -201
|
@@ -1,607 +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 { 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
|
-
DEFAULT_TASK_DONE_MARKER,
|
|
26
|
-
CLAUDE_DEFAULT_ARGS,
|
|
27
|
-
} from "../../lib/ralph/index.js";
|
|
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(DEFAULT_TASK_DONE_MARKER).toBe("TASK_DONE");
|
|
50
|
-
expect(CLAUDE_DEFAULT_ARGS).toEqual([
|
|
51
|
-
"--print",
|
|
52
|
-
"--verbose",
|
|
53
|
-
"--output-format",
|
|
54
|
-
"stream-json",
|
|
55
|
-
"--permission-mode",
|
|
56
|
-
"bypassPermissions",
|
|
57
|
-
]);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe("createInitialState", () => {
|
|
62
|
-
it("should create state with correct structure", () => {
|
|
63
|
-
const state = createInitialState();
|
|
64
|
-
|
|
65
|
-
expect(state.version).toBe(1);
|
|
66
|
-
expect(state.status).toBe("running");
|
|
67
|
-
expect(state.plans).toEqual([]);
|
|
68
|
-
expect(state.startedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe("saveState and loadState", () => {
|
|
73
|
-
it("should save and load state correctly", () => {
|
|
74
|
-
const state = createInitialState();
|
|
75
|
-
addPlanToState(state, "/tmp/test-plan.md");
|
|
76
|
-
|
|
77
|
-
saveState(state, testStateFile);
|
|
78
|
-
const loaded = loadState(testStateFile);
|
|
79
|
-
|
|
80
|
-
expect(loaded).not.toBeNull();
|
|
81
|
-
expect(loaded?.plans).toHaveLength(1);
|
|
82
|
-
expect(loaded?.plans[0].planFilePath).toBe("/tmp/test-plan.md");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("should return null for non-existent file", () => {
|
|
86
|
-
const loaded = loadState("/nonexistent/path/file.json");
|
|
87
|
-
expect(loaded).toBeNull();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("should return null for invalid JSON", () => {
|
|
91
|
-
writeFileSync(testStateFile, "invalid json {{{");
|
|
92
|
-
const loaded = loadState(testStateFile);
|
|
93
|
-
expect(loaded).toBeNull();
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe("buildIterationPrompt", () => {
|
|
98
|
-
const testPlan: RalphPlan = {
|
|
99
|
-
id: 1,
|
|
100
|
-
planFilePath: "/tmp/first-plan.md",
|
|
101
|
-
status: "ready",
|
|
102
|
-
addedAt: new Date().toISOString(),
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
it("should include completion marker", () => {
|
|
106
|
-
const prompt = buildIterationPrompt({
|
|
107
|
-
completionMarker: "RALPH_DONE",
|
|
108
|
-
taskDoneMarker: "TASK_DONE",
|
|
109
|
-
plan: testPlan,
|
|
110
|
-
});
|
|
111
|
-
expect(prompt).toContain("RALPH_DONE");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("should include plan file path for reading", () => {
|
|
115
|
-
const prompt = buildIterationPrompt({
|
|
116
|
-
completionMarker: "RALPH_DONE",
|
|
117
|
-
taskDoneMarker: "TASK_DONE",
|
|
118
|
-
plan: testPlan,
|
|
119
|
-
});
|
|
120
|
-
expect(prompt).toContain("/tmp/first-plan.md");
|
|
121
|
-
expect(prompt).toContain("Read Plan:");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("should include instruction to update plan file", () => {
|
|
125
|
-
const prompt = buildIterationPrompt({
|
|
126
|
-
completionMarker: "RALPH_DONE",
|
|
127
|
-
taskDoneMarker: "TASK_DONE",
|
|
128
|
-
plan: testPlan,
|
|
129
|
-
});
|
|
130
|
-
expect(prompt).toContain("Update the plan");
|
|
131
|
-
expect(prompt).toContain("/tmp/first-plan.md");
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("should include custom completion marker", () => {
|
|
135
|
-
const prompt = buildIterationPrompt({
|
|
136
|
-
completionMarker: "CUSTOM_MARKER",
|
|
137
|
-
taskDoneMarker: "TASK_DONE",
|
|
138
|
-
plan: testPlan,
|
|
139
|
-
});
|
|
140
|
-
expect(prompt).toContain("CUSTOM_MARKER");
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("should include TASK_DONE for tasks remaining", () => {
|
|
144
|
-
const prompt = buildIterationPrompt({
|
|
145
|
-
completionMarker: "RALPH_DONE",
|
|
146
|
-
taskDoneMarker: "TASK_DONE",
|
|
147
|
-
plan: testPlan,
|
|
148
|
-
});
|
|
149
|
-
expect(prompt).toContain("TASK_DONE");
|
|
150
|
-
expect(prompt).toContain("tasks remain");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("should include RALPH_DONE for plan complete", () => {
|
|
154
|
-
const prompt = buildIterationPrompt({
|
|
155
|
-
completionMarker: "RALPH_DONE",
|
|
156
|
-
taskDoneMarker: "TASK_DONE",
|
|
157
|
-
plan: testPlan,
|
|
158
|
-
});
|
|
159
|
-
expect(prompt).toContain("RALPH_DONE");
|
|
160
|
-
expect(prompt).toContain("plan is complete");
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("should skip commit step when skipCommit is true", () => {
|
|
164
|
-
const promptWithCommit = buildIterationPrompt({
|
|
165
|
-
completionMarker: "RALPH_DONE",
|
|
166
|
-
taskDoneMarker: "TASK_DONE",
|
|
167
|
-
plan: testPlan,
|
|
168
|
-
skipCommit: false,
|
|
169
|
-
});
|
|
170
|
-
const promptWithoutCommit = buildIterationPrompt({
|
|
171
|
-
completionMarker: "RALPH_DONE",
|
|
172
|
-
taskDoneMarker: "TASK_DONE",
|
|
173
|
-
plan: testPlan,
|
|
174
|
-
skipCommit: true,
|
|
175
|
-
});
|
|
176
|
-
expect(promptWithCommit).toContain("git commit");
|
|
177
|
-
expect(promptWithoutCommit).not.toContain("git commit");
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
describe("extractOutputSummary", () => {
|
|
182
|
-
it("should return last 5 lines joined", () => {
|
|
183
|
-
const output = "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7";
|
|
184
|
-
const summary = extractOutputSummary(output);
|
|
185
|
-
|
|
186
|
-
expect(summary).toContain("line 3");
|
|
187
|
-
expect(summary).toContain("line 7");
|
|
188
|
-
expect(summary).not.toContain("line 1");
|
|
189
|
-
expect(summary).not.toContain("line 2");
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("should filter empty lines", () => {
|
|
193
|
-
const output = "line 1\n\n\nline 2\n\nline 3";
|
|
194
|
-
const summary = extractOutputSummary(output);
|
|
195
|
-
|
|
196
|
-
expect(summary).toBe("line 1 line 2 line 3");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("should truncate long output", () => {
|
|
200
|
-
const longLine = "x".repeat(300);
|
|
201
|
-
const summary = extractOutputSummary(longLine, 200);
|
|
202
|
-
|
|
203
|
-
expect(summary.length).toBe(203); // 200 + '...'
|
|
204
|
-
expect(summary.endsWith("...")).toBe(true);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('should return "(no output)" for empty string', () => {
|
|
208
|
-
expect(extractOutputSummary("")).toBe("(no output)");
|
|
209
|
-
expect(extractOutputSummary(" \n \n ")).toBe("(no output)");
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("should use custom maxLength", () => {
|
|
213
|
-
const output = "a".repeat(100);
|
|
214
|
-
const summary = extractOutputSummary(output, 50);
|
|
215
|
-
|
|
216
|
-
expect(summary.length).toBe(53); // 50 + '...'
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
describe("detectCompletionMarker", () => {
|
|
221
|
-
it("should detect marker in output", () => {
|
|
222
|
-
expect(detectCompletionMarker("Task complete RALPH_DONE", "RALPH_DONE")).toBe(true);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("should return false when marker not present", () => {
|
|
226
|
-
expect(detectCompletionMarker("Task still in progress", "RALPH_DONE")).toBe(false);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("should detect marker anywhere in output", () => {
|
|
230
|
-
expect(detectCompletionMarker("start RALPH_DONE end", "RALPH_DONE")).toBe(true);
|
|
231
|
-
expect(detectCompletionMarker("RALPH_DONE", "RALPH_DONE")).toBe(true);
|
|
232
|
-
expect(detectCompletionMarker("prefix\nRALPH_DONE\nsuffix", "RALPH_DONE")).toBe(true);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("should work with custom markers", () => {
|
|
236
|
-
expect(detectCompletionMarker("CUSTOM_DONE", "CUSTOM_DONE")).toBe(true);
|
|
237
|
-
expect(detectCompletionMarker("<done/>", "<done/>")).toBe(true);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("should be case-sensitive", () => {
|
|
241
|
-
expect(detectCompletionMarker("ralph_done", "RALPH_DONE")).toBe(false);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it("should detect TASK_DONE marker", () => {
|
|
245
|
-
expect(detectCompletionMarker("finished <promise>TASK_DONE</promise>", "TASK_DONE")).toBe(
|
|
246
|
-
true,
|
|
247
|
-
);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("should distinguish TASK_DONE from RALPH_DONE", () => {
|
|
251
|
-
const output = "<promise>TASK_DONE</promise>";
|
|
252
|
-
expect(detectCompletionMarker(output, "TASK_DONE")).toBe(true);
|
|
253
|
-
expect(detectCompletionMarker(output, "RALPH_DONE")).toBe(false);
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
describe("state transitions", () => {
|
|
258
|
-
it("should update status correctly", () => {
|
|
259
|
-
const state = createInitialState();
|
|
260
|
-
|
|
261
|
-
expect(state.status).toBe("running");
|
|
262
|
-
|
|
263
|
-
state.status = "completed";
|
|
264
|
-
expect(state.status).toBe("completed");
|
|
265
|
-
|
|
266
|
-
state.status = "max_iterations_reached";
|
|
267
|
-
expect(state.status).toBe("max_iterations_reached");
|
|
268
|
-
|
|
269
|
-
state.status = "error";
|
|
270
|
-
expect(state.status).toBe("error");
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
describe("appendHistory", () => {
|
|
275
|
-
it("should append history as JSON line", () => {
|
|
276
|
-
const history: IterationHistory = {
|
|
277
|
-
iteration: 1,
|
|
278
|
-
startedAt: "2026-01-08T10:00:00Z",
|
|
279
|
-
completedAt: "2026-01-08T10:01:00Z",
|
|
280
|
-
durationMs: 60000,
|
|
281
|
-
durationHuman: "1m 0s",
|
|
282
|
-
outputSummary: "test output",
|
|
283
|
-
markerFound: false,
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
appendHistory(history, testHistoryFile);
|
|
287
|
-
|
|
288
|
-
const content = require("node:fs").readFileSync(testHistoryFile, "utf-8");
|
|
289
|
-
const lines = content.trim().split("\n");
|
|
290
|
-
expect(lines).toHaveLength(1);
|
|
291
|
-
|
|
292
|
-
const parsed = JSON.parse(lines[0]);
|
|
293
|
-
expect(parsed.iteration).toBe(1);
|
|
294
|
-
expect(parsed.outputSummary).toBe("test output");
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it("should append multiple entries", () => {
|
|
298
|
-
const history1: IterationHistory = {
|
|
299
|
-
iteration: 1,
|
|
300
|
-
startedAt: "2026-01-08T10:00:00Z",
|
|
301
|
-
completedAt: "2026-01-08T10:01:00Z",
|
|
302
|
-
durationMs: 60000,
|
|
303
|
-
durationHuman: "1m 0s",
|
|
304
|
-
outputSummary: "first",
|
|
305
|
-
markerFound: false,
|
|
306
|
-
};
|
|
307
|
-
const history2: IterationHistory = {
|
|
308
|
-
iteration: 2,
|
|
309
|
-
startedAt: "2026-01-08T10:02:00Z",
|
|
310
|
-
completedAt: "2026-01-08T10:03:00Z",
|
|
311
|
-
durationMs: 60000,
|
|
312
|
-
durationHuman: "1m 0s",
|
|
313
|
-
outputSummary: "second",
|
|
314
|
-
markerFound: true,
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
appendHistory(history1, testHistoryFile);
|
|
318
|
-
appendHistory(history2, testHistoryFile);
|
|
319
|
-
|
|
320
|
-
const content = require("node:fs").readFileSync(testHistoryFile, "utf-8");
|
|
321
|
-
const lines = content.trim().split("\n");
|
|
322
|
-
expect(lines).toHaveLength(2);
|
|
323
|
-
|
|
324
|
-
const parsed1 = JSON.parse(lines[0]);
|
|
325
|
-
const parsed2 = JSON.parse(lines[1]);
|
|
326
|
-
expect(parsed1.outputSummary).toBe("first");
|
|
327
|
-
expect(parsed2.outputSummary).toBe("second");
|
|
328
|
-
expect(parsed2.markerFound).toBe(true);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it("should include taskMarkerFound field", () => {
|
|
332
|
-
const history: IterationHistory = {
|
|
333
|
-
iteration: 1,
|
|
334
|
-
startedAt: "2026-01-19T10:00:00Z",
|
|
335
|
-
completedAt: "2026-01-19T10:01:00Z",
|
|
336
|
-
durationMs: 60000,
|
|
337
|
-
durationHuman: "1m 0s",
|
|
338
|
-
outputSummary: "test output",
|
|
339
|
-
markerFound: false,
|
|
340
|
-
taskMarkerFound: true,
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
appendHistory(history, testHistoryFile);
|
|
344
|
-
|
|
345
|
-
const content = require("node:fs").readFileSync(testHistoryFile, "utf-8");
|
|
346
|
-
const parsed = JSON.parse(content.trim());
|
|
347
|
-
expect(parsed.taskMarkerFound).toBe(true);
|
|
348
|
-
expect(parsed.markerFound).toBe(false);
|
|
349
|
-
});
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
describe("addPlanToState", () => {
|
|
353
|
-
it("should add plan with correct structure", () => {
|
|
354
|
-
const state = createInitialState();
|
|
355
|
-
const plan = addPlanToState(state, "/tmp/implement-feature.md");
|
|
356
|
-
|
|
357
|
-
expect(plan.id).toBe(1);
|
|
358
|
-
expect(plan.planFilePath).toBe("/tmp/implement-feature.md");
|
|
359
|
-
expect(plan.status).toBe("ready");
|
|
360
|
-
expect(plan.addedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
361
|
-
expect(plan.completedAt).toBeUndefined();
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it("should increment plan IDs", () => {
|
|
365
|
-
const state = createInitialState();
|
|
366
|
-
|
|
367
|
-
const plan1 = addPlanToState(state, "/tmp/plan-1.md");
|
|
368
|
-
const plan2 = addPlanToState(state, "/tmp/plan-2.md");
|
|
369
|
-
const plan3 = addPlanToState(state, "/tmp/plan-3.md");
|
|
370
|
-
|
|
371
|
-
expect(plan1.id).toBe(1);
|
|
372
|
-
expect(plan2.id).toBe(2);
|
|
373
|
-
expect(plan3.id).toBe(3);
|
|
374
|
-
expect(state.plans).toHaveLength(3);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it("should handle non-sequential IDs", () => {
|
|
378
|
-
const state = createInitialState();
|
|
379
|
-
|
|
380
|
-
// Simulate deleted plan by adding with gap
|
|
381
|
-
state.plans.push({
|
|
382
|
-
id: 5,
|
|
383
|
-
planFilePath: "/tmp/existing-plan.md",
|
|
384
|
-
status: "done",
|
|
385
|
-
addedAt: new Date().toISOString(),
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
const newPlan = addPlanToState(state, "/tmp/new-plan.md");
|
|
389
|
-
expect(newPlan.id).toBe(6);
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
describe("formatPlansAsMarkdown", () => {
|
|
394
|
-
it("should return placeholder for empty plans", () => {
|
|
395
|
-
const formatted = formatPlansAsMarkdown([]);
|
|
396
|
-
expect(formatted).toContain("# Plans");
|
|
397
|
-
expect(formatted).toContain("No plans.");
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
it("should include summary counts", () => {
|
|
401
|
-
const plans: RalphPlan[] = [
|
|
402
|
-
{ id: 1, planFilePath: "/tmp/plan-1.md", status: "done", addedAt: "" },
|
|
403
|
-
{ id: 2, planFilePath: "/tmp/plan-2.md", status: "ready", addedAt: "" },
|
|
404
|
-
];
|
|
405
|
-
|
|
406
|
-
const formatted = formatPlansAsMarkdown(plans);
|
|
407
|
-
|
|
408
|
-
expect(formatted).toContain("**Total:** 2");
|
|
409
|
-
expect(formatted).toContain("**Done:** 1");
|
|
410
|
-
expect(formatted).toContain("**Ready:** 1");
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
it("should format done plans with checked boxes", () => {
|
|
414
|
-
const plans: RalphPlan[] = [
|
|
415
|
-
{ id: 1, planFilePath: "/tmp/completed-plan.md", status: "done", addedAt: "" },
|
|
416
|
-
];
|
|
417
|
-
|
|
418
|
-
const formatted = formatPlansAsMarkdown(plans);
|
|
419
|
-
|
|
420
|
-
expect(formatted).toContain("## Done");
|
|
421
|
-
expect(formatted).toContain("- [x] **#1** /tmp/completed-plan.md");
|
|
422
|
-
expect(formatted).toContain("`✓ done`");
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
it("should format ready plans with unchecked boxes", () => {
|
|
426
|
-
const plans: RalphPlan[] = [
|
|
427
|
-
{ id: 2, planFilePath: "/tmp/ready-plan.md", status: "ready", addedAt: "" },
|
|
428
|
-
];
|
|
429
|
-
|
|
430
|
-
const formatted = formatPlansAsMarkdown(plans);
|
|
431
|
-
|
|
432
|
-
expect(formatted).toContain("## Ready");
|
|
433
|
-
expect(formatted).toContain("- [ ] **#2** /tmp/ready-plan.md");
|
|
434
|
-
expect(formatted).toContain("`○ ready`");
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it("should group plans by status section", () => {
|
|
438
|
-
const plans: RalphPlan[] = [
|
|
439
|
-
{ id: 1, planFilePath: "/tmp/done-plan.md", status: "done", addedAt: "" },
|
|
440
|
-
{ id: 2, planFilePath: "/tmp/ready-plan.md", status: "ready", addedAt: "" },
|
|
441
|
-
];
|
|
442
|
-
|
|
443
|
-
const formatted = formatPlansAsMarkdown(plans);
|
|
444
|
-
|
|
445
|
-
expect(formatted).toContain("## Ready");
|
|
446
|
-
expect(formatted).toContain("## Done");
|
|
447
|
-
});
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
describe("formatPlanAsMarkdown", () => {
|
|
451
|
-
it("should include plan header and summary", () => {
|
|
452
|
-
const state = createInitialState();
|
|
453
|
-
addPlanToState(state, "/tmp/plan-1.md");
|
|
454
|
-
|
|
455
|
-
const formatted = formatPlanAsMarkdown(state.plans, state);
|
|
456
|
-
|
|
457
|
-
expect(formatted).toContain("# Ralph Plan");
|
|
458
|
-
expect(formatted).toContain("## Summary");
|
|
459
|
-
expect(formatted).toContain("**Status:** running");
|
|
460
|
-
expect(formatted).toContain("**Total:** 1");
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it("should include plans section", () => {
|
|
464
|
-
const state = createInitialState();
|
|
465
|
-
addPlanToState(state, "/tmp/implement-feature.md");
|
|
466
|
-
|
|
467
|
-
const formatted = formatPlanAsMarkdown(state.plans, state);
|
|
468
|
-
|
|
469
|
-
expect(formatted).toContain("## Plans");
|
|
470
|
-
expect(formatted).toContain("**#1** /tmp/implement-feature.md");
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
it("should include mermaid graph section", () => {
|
|
474
|
-
const state = createInitialState();
|
|
475
|
-
addPlanToState(state, "/tmp/plan-1.md");
|
|
476
|
-
addPlanToState(state, "/tmp/plan-2.md");
|
|
477
|
-
|
|
478
|
-
const formatted = formatPlanAsMarkdown(state.plans, state);
|
|
479
|
-
|
|
480
|
-
expect(formatted).toContain("## Progress Graph");
|
|
481
|
-
expect(formatted).toContain("```mermaid");
|
|
482
|
-
expect(formatted).toContain("graph LR");
|
|
483
|
-
expect(formatted).toContain("classDef done fill:#22c55e");
|
|
484
|
-
expect(formatted).toContain("classDef ready fill:#94a3b8");
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
it("should format done plans correctly in mermaid", () => {
|
|
488
|
-
const state = createInitialState();
|
|
489
|
-
const plan = addPlanToState(state, "/tmp/done-plan.md");
|
|
490
|
-
plan.status = "done";
|
|
491
|
-
|
|
492
|
-
const formatted = formatPlanAsMarkdown(state.plans, state);
|
|
493
|
-
|
|
494
|
-
expect(formatted).toContain('P1["#1: done-plan.md"]:::done');
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
it("should truncate long filenames in mermaid", () => {
|
|
498
|
-
const state = createInitialState();
|
|
499
|
-
addPlanToState(state, "/tmp/this-is-a-very-long-plan-filename-that-should-be-truncated.md");
|
|
500
|
-
|
|
501
|
-
const formatted = formatPlanAsMarkdown(state.plans, state);
|
|
502
|
-
|
|
503
|
-
// Mermaid section should have truncated filename
|
|
504
|
-
expect(formatted).toContain('P1["#1: this-is-a-very-long-plan-fi..."]');
|
|
505
|
-
// But the Plans section should have full path
|
|
506
|
-
expect(formatted).toContain(
|
|
507
|
-
"**#1** /tmp/this-is-a-very-long-plan-filename-that-should-be-truncated.md",
|
|
508
|
-
);
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
describe("formatPlanAsJson", () => {
|
|
513
|
-
it("should return valid JSON", () => {
|
|
514
|
-
const state = createInitialState();
|
|
515
|
-
addPlanToState(state, "/tmp/plan-1.md");
|
|
516
|
-
|
|
517
|
-
const json = formatPlanAsJson(state.plans, state);
|
|
518
|
-
const parsed = JSON.parse(json);
|
|
519
|
-
|
|
520
|
-
expect(parsed.status).toBe("running");
|
|
521
|
-
expect(parsed.plans).toHaveLength(1);
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
it("should include summary counts", () => {
|
|
525
|
-
const state = createInitialState();
|
|
526
|
-
const plan1 = addPlanToState(state, "/tmp/done-plan.md");
|
|
527
|
-
plan1.status = "done";
|
|
528
|
-
addPlanToState(state, "/tmp/ready-plan.md");
|
|
529
|
-
|
|
530
|
-
const json = formatPlanAsJson(state.plans, state);
|
|
531
|
-
const parsed = JSON.parse(json);
|
|
532
|
-
|
|
533
|
-
expect(parsed.summary.total).toBe(2);
|
|
534
|
-
expect(parsed.summary.done).toBe(1);
|
|
535
|
-
expect(parsed.summary.ready).toBe(1);
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
it("should include all plan fields", () => {
|
|
539
|
-
const state = createInitialState();
|
|
540
|
-
const plan = addPlanToState(state, "/tmp/test-plan.md");
|
|
541
|
-
plan.status = "done";
|
|
542
|
-
plan.completedAt = "2026-01-10T12:00:00Z";
|
|
543
|
-
|
|
544
|
-
const json = formatPlanAsJson(state.plans, state);
|
|
545
|
-
const parsed = JSON.parse(json);
|
|
546
|
-
|
|
547
|
-
expect(parsed.plans[0].id).toBe(1);
|
|
548
|
-
expect(parsed.plans[0].planFilePath).toBe("/tmp/test-plan.md");
|
|
549
|
-
expect(parsed.plans[0].status).toBe("done");
|
|
550
|
-
expect(parsed.plans[0].addedAt).toBeDefined();
|
|
551
|
-
expect(parsed.plans[0].completedAt).toBe("2026-01-10T12:00:00Z");
|
|
552
|
-
});
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
describe("markDone functionality", () => {
|
|
556
|
-
it("should mark plan as done and add completedAt", () => {
|
|
557
|
-
const state = createInitialState();
|
|
558
|
-
addPlanToState(state, "/tmp/plan-1.md");
|
|
559
|
-
addPlanToState(state, "/tmp/plan-2.md");
|
|
560
|
-
|
|
561
|
-
// Simulate marking plan 1 as done
|
|
562
|
-
const plan = state.plans.find((t) => t.id === 1);
|
|
563
|
-
expect(plan).toBeDefined();
|
|
564
|
-
expect(plan?.status).toBe("ready");
|
|
565
|
-
|
|
566
|
-
plan!.status = "done";
|
|
567
|
-
plan!.completedAt = new Date().toISOString();
|
|
568
|
-
|
|
569
|
-
expect(plan?.status).toBe("done");
|
|
570
|
-
expect(plan?.completedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
it("should find plan by ID", () => {
|
|
574
|
-
const state = createInitialState();
|
|
575
|
-
addPlanToState(state, "/tmp/plan-1.md");
|
|
576
|
-
addPlanToState(state, "/tmp/plan-2.md");
|
|
577
|
-
addPlanToState(state, "/tmp/plan-3.md");
|
|
578
|
-
|
|
579
|
-
const plan = state.plans.find((p) => p.id === 2);
|
|
580
|
-
expect(plan).toBeDefined();
|
|
581
|
-
expect(plan?.planFilePath).toBe("/tmp/plan-2.md");
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
it("should return undefined for non-existent plan ID", () => {
|
|
585
|
-
const state = createInitialState();
|
|
586
|
-
addPlanToState(state, "/tmp/plan-1.md");
|
|
587
|
-
|
|
588
|
-
const plan = state.plans.find((t) => t.id === 99);
|
|
589
|
-
expect(plan).toBeUndefined();
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
it("should persist marked-done plan to file", () => {
|
|
593
|
-
const state = createInitialState();
|
|
594
|
-
addPlanToState(state, "/tmp/plan-1.md");
|
|
595
|
-
|
|
596
|
-
const plan = state.plans.find((t) => t.id === 1)!;
|
|
597
|
-
plan.status = "done";
|
|
598
|
-
plan.completedAt = new Date().toISOString();
|
|
599
|
-
|
|
600
|
-
saveState(state, testStateFile);
|
|
601
|
-
const loaded = loadState(testStateFile);
|
|
602
|
-
|
|
603
|
-
expect(loaded?.plans[0].status).toBe("done");
|
|
604
|
-
expect(loaded?.plans[0].completedAt).toBeDefined();
|
|
605
|
-
});
|
|
606
|
-
});
|
|
607
|
-
});
|