|
| 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