Skip to content

Commit f9325c2

Browse files
committed
feat(features): add boulder-state persistence
Add boulder-state feature for persisting workflow state: - storage.ts: File I/O operations for state persistence - types.ts: State interfaces - Includes test coverage 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
1 parent c1fa8d5 commit f9325c2

File tree

5 files changed

+442
-0
lines changed

5 files changed

+442
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Boulder State Constants
3+
*/
4+
5+
export const BOULDER_DIR = ".sisyphus"
6+
export const BOULDER_FILE = "boulder.json"
7+
export const BOULDER_STATE_PATH = `${BOULDER_DIR}/${BOULDER_FILE}`
8+
9+
export const NOTEPAD_DIR = "notepads"
10+
export const NOTEPAD_BASE_PATH = `${BOULDER_DIR}/${NOTEPAD_DIR}`
11+
12+
/** Prometheus plan directory pattern */
13+
export const PROMETHEUS_PLANS_DIR = ".sisyphus/plans"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./types"
2+
export * from "./constants"
3+
export * from "./storage"
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { describe, expect, test, beforeEach, afterEach } from "bun:test"
2+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
3+
import { join } from "node:path"
4+
import { tmpdir } from "node:os"
5+
import {
6+
readBoulderState,
7+
writeBoulderState,
8+
appendSessionId,
9+
clearBoulderState,
10+
getPlanProgress,
11+
getPlanName,
12+
createBoulderState,
13+
findPrometheusPlans,
14+
} from "./storage"
15+
import type { BoulderState } from "./types"
16+
17+
describe("boulder-state", () => {
18+
const TEST_DIR = join(tmpdir(), "boulder-state-test-" + Date.now())
19+
const SISYPHUS_DIR = join(TEST_DIR, ".sisyphus")
20+
21+
beforeEach(() => {
22+
if (!existsSync(TEST_DIR)) {
23+
mkdirSync(TEST_DIR, { recursive: true })
24+
}
25+
if (!existsSync(SISYPHUS_DIR)) {
26+
mkdirSync(SISYPHUS_DIR, { recursive: true })
27+
}
28+
clearBoulderState(TEST_DIR)
29+
})
30+
31+
afterEach(() => {
32+
if (existsSync(TEST_DIR)) {
33+
rmSync(TEST_DIR, { recursive: true, force: true })
34+
}
35+
})
36+
37+
describe("readBoulderState", () => {
38+
test("should return null when no boulder.json exists", () => {
39+
// #given - no boulder.json file
40+
// #when
41+
const result = readBoulderState(TEST_DIR)
42+
// #then
43+
expect(result).toBeNull()
44+
})
45+
46+
test("should read valid boulder state", () => {
47+
// #given - valid boulder.json
48+
const state: BoulderState = {
49+
active_plan: "/path/to/plan.md",
50+
started_at: "2026-01-02T10:00:00Z",
51+
session_ids: ["session-1", "session-2"],
52+
plan_name: "my-plan",
53+
}
54+
writeBoulderState(TEST_DIR, state)
55+
56+
// #when
57+
const result = readBoulderState(TEST_DIR)
58+
59+
// #then
60+
expect(result).not.toBeNull()
61+
expect(result?.active_plan).toBe("/path/to/plan.md")
62+
expect(result?.session_ids).toEqual(["session-1", "session-2"])
63+
expect(result?.plan_name).toBe("my-plan")
64+
})
65+
})
66+
67+
describe("writeBoulderState", () => {
68+
test("should write state and create .sisyphus directory if needed", () => {
69+
// #given - state to write
70+
const state: BoulderState = {
71+
active_plan: "/test/plan.md",
72+
started_at: "2026-01-02T12:00:00Z",
73+
session_ids: ["ses-123"],
74+
plan_name: "test-plan",
75+
}
76+
77+
// #when
78+
const success = writeBoulderState(TEST_DIR, state)
79+
const readBack = readBoulderState(TEST_DIR)
80+
81+
// #then
82+
expect(success).toBe(true)
83+
expect(readBack).not.toBeNull()
84+
expect(readBack?.active_plan).toBe("/test/plan.md")
85+
})
86+
})
87+
88+
describe("appendSessionId", () => {
89+
test("should append new session id to existing state", () => {
90+
// #given - existing state with one session
91+
const state: BoulderState = {
92+
active_plan: "/plan.md",
93+
started_at: "2026-01-02T10:00:00Z",
94+
session_ids: ["session-1"],
95+
plan_name: "plan",
96+
}
97+
writeBoulderState(TEST_DIR, state)
98+
99+
// #when
100+
const result = appendSessionId(TEST_DIR, "session-2")
101+
102+
// #then
103+
expect(result).not.toBeNull()
104+
expect(result?.session_ids).toEqual(["session-1", "session-2"])
105+
})
106+
107+
test("should not duplicate existing session id", () => {
108+
// #given - state with session-1 already
109+
const state: BoulderState = {
110+
active_plan: "/plan.md",
111+
started_at: "2026-01-02T10:00:00Z",
112+
session_ids: ["session-1"],
113+
plan_name: "plan",
114+
}
115+
writeBoulderState(TEST_DIR, state)
116+
117+
// #when
118+
appendSessionId(TEST_DIR, "session-1")
119+
const result = readBoulderState(TEST_DIR)
120+
121+
// #then
122+
expect(result?.session_ids).toEqual(["session-1"])
123+
})
124+
125+
test("should return null when no state exists", () => {
126+
// #given - no boulder.json
127+
// #when
128+
const result = appendSessionId(TEST_DIR, "new-session")
129+
// #then
130+
expect(result).toBeNull()
131+
})
132+
})
133+
134+
describe("clearBoulderState", () => {
135+
test("should remove boulder.json", () => {
136+
// #given - existing state
137+
const state: BoulderState = {
138+
active_plan: "/plan.md",
139+
started_at: "2026-01-02T10:00:00Z",
140+
session_ids: ["session-1"],
141+
plan_name: "plan",
142+
}
143+
writeBoulderState(TEST_DIR, state)
144+
145+
// #when
146+
const success = clearBoulderState(TEST_DIR)
147+
const result = readBoulderState(TEST_DIR)
148+
149+
// #then
150+
expect(success).toBe(true)
151+
expect(result).toBeNull()
152+
})
153+
154+
test("should succeed even when no file exists", () => {
155+
// #given - no boulder.json
156+
// #when
157+
const success = clearBoulderState(TEST_DIR)
158+
// #then
159+
expect(success).toBe(true)
160+
})
161+
})
162+
163+
describe("getPlanProgress", () => {
164+
test("should count completed and uncompleted checkboxes", () => {
165+
// #given - plan file with checkboxes
166+
const planPath = join(TEST_DIR, "test-plan.md")
167+
writeFileSync(planPath, `# Plan
168+
- [ ] Task 1
169+
- [x] Task 2
170+
- [ ] Task 3
171+
- [X] Task 4
172+
`)
173+
174+
// #when
175+
const progress = getPlanProgress(planPath)
176+
177+
// #then
178+
expect(progress.total).toBe(4)
179+
expect(progress.completed).toBe(2)
180+
expect(progress.isComplete).toBe(false)
181+
})
182+
183+
test("should return isComplete true when all checked", () => {
184+
// #given - all tasks completed
185+
const planPath = join(TEST_DIR, "complete-plan.md")
186+
writeFileSync(planPath, `# Plan
187+
- [x] Task 1
188+
- [X] Task 2
189+
`)
190+
191+
// #when
192+
const progress = getPlanProgress(planPath)
193+
194+
// #then
195+
expect(progress.total).toBe(2)
196+
expect(progress.completed).toBe(2)
197+
expect(progress.isComplete).toBe(true)
198+
})
199+
200+
test("should return isComplete true for empty plan", () => {
201+
// #given - plan with no checkboxes
202+
const planPath = join(TEST_DIR, "empty-plan.md")
203+
writeFileSync(planPath, "# Plan\nNo tasks here")
204+
205+
// #when
206+
const progress = getPlanProgress(planPath)
207+
208+
// #then
209+
expect(progress.total).toBe(0)
210+
expect(progress.isComplete).toBe(true)
211+
})
212+
213+
test("should handle non-existent file", () => {
214+
// #given - non-existent file
215+
// #when
216+
const progress = getPlanProgress("/non/existent/file.md")
217+
// #then
218+
expect(progress.total).toBe(0)
219+
expect(progress.isComplete).toBe(true)
220+
})
221+
})
222+
223+
describe("getPlanName", () => {
224+
test("should extract plan name from path", () => {
225+
// #given
226+
const path = "/home/user/.sisyphus/plans/project/my-feature.md"
227+
// #when
228+
const name = getPlanName(path)
229+
// #then
230+
expect(name).toBe("my-feature")
231+
})
232+
})
233+
234+
describe("createBoulderState", () => {
235+
test("should create state with correct fields", () => {
236+
// #given
237+
const planPath = "/path/to/auth-refactor.md"
238+
const sessionId = "ses-abc123"
239+
240+
// #when
241+
const state = createBoulderState(planPath, sessionId)
242+
243+
// #then
244+
expect(state.active_plan).toBe(planPath)
245+
expect(state.session_ids).toEqual([sessionId])
246+
expect(state.plan_name).toBe("auth-refactor")
247+
expect(state.started_at).toBeDefined()
248+
})
249+
})
250+
})

0 commit comments

Comments
 (0)