import { describe, expect, test, beforeEach, afterEach } from "bun:test" import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" import { dirname, join } from "node:path" import { tmpdir } from "node:os" import { addBoulderWork, appendSessionIdForWork, completeBoulder, endTaskTimer, getActiveWorks, getBoulderWorks, readBoulderState, writeBoulderState, appendSessionId, clearBoulderState, getWorkById, getWorkByPlanName, getWorkForSession, getWorkResumeOptions, getPlanProgress, getPlanName, createBoulderState, findPrometheusPlans, getTaskSessionState, resolveBoulderPlanPath, resolveBoulderPlanPathForWork, selectActiveWork, startTaskTimer, upsertTaskSessionState, upsertTaskSessionStateForWork, } from "./storage" import type { BoulderState } from "./types" import { readCurrentTopLevelTask } from "./top-level-task" describe("boulder-state", () => { const TEST_DIR = join(tmpdir(), "boulder-state-test-" + Date.now()) const SISYPHUS_DIR = join(TEST_DIR, ".sisyphus") beforeEach(() => { if (!existsSync(TEST_DIR)) { mkdirSync(TEST_DIR, { recursive: true }) } if (!existsSync(SISYPHUS_DIR)) { mkdirSync(SISYPHUS_DIR, { recursive: true }) } clearBoulderState(TEST_DIR) }) afterEach(() => { if (existsSync(TEST_DIR)) { rmSync(TEST_DIR, { recursive: true, force: true }) } }) describe("readBoulderState", () => { test("should preserve legacy boulder.json fields during round-trip", () => { // given const boulderFile = join(SISYPHUS_DIR, "boulder.json") const legacyRawState = { active_plan: "/path/to/legacy-plan.md", started_at: "2026-01-01T00:00:00.000Z", session_ids: ["legacy-session"], plan_name: "legacy-plan", } writeFileSync(boulderFile, JSON.stringify(legacyRawState, null, 2), "utf-8") // when const state = readBoulderState(TEST_DIR) expect(state).not.toBeNull() const writeSucceeded = writeBoulderState(TEST_DIR, state!) const roundTripState = readBoulderState(TEST_DIR) // then expect(writeSucceeded).toBe(true) expect(roundTripState?.active_plan).toBe(legacyRawState.active_plan) expect(roundTripState?.started_at).toBe(legacyRawState.started_at) expect(roundTripState?.session_ids).toEqual(legacyRawState.session_ids) expect(roundTripState?.plan_name).toBe(legacyRawState.plan_name) }) test("should return null when no boulder.json exists", () => { // given - no boulder.json file // when const result = readBoulderState(TEST_DIR) // then expect(result).toBeNull() }) test("should return null for JSON null value", () => { //#given - boulder.json containing null const boulderFile = join(SISYPHUS_DIR, "boulder.json") writeFileSync(boulderFile, "null") //#when const result = readBoulderState(TEST_DIR) //#then expect(result).toBeNull() }) test("should return null for JSON primitive value", () => { //#given - boulder.json containing a string const boulderFile = join(SISYPHUS_DIR, "boulder.json") writeFileSync(boulderFile, '"just a string"') //#when const result = readBoulderState(TEST_DIR) //#then expect(result).toBeNull() }) test("should default session_ids to [] when missing from JSON", () => { //#given - boulder.json without session_ids field const boulderFile = join(SISYPHUS_DIR, "boulder.json") writeFileSync(boulderFile, JSON.stringify({ active_plan: "/path/to/plan.md", started_at: "2026-01-01T00:00:00Z", plan_name: "plan", })) //#when const result = readBoulderState(TEST_DIR) //#then expect(result).not.toBeNull() expect(result!.session_ids).toEqual([]) }) test("should default session_ids to [] when not an array", () => { //#given - boulder.json with session_ids as a string const boulderFile = join(SISYPHUS_DIR, "boulder.json") writeFileSync(boulderFile, JSON.stringify({ active_plan: "/path/to/plan.md", started_at: "2026-01-01T00:00:00Z", session_ids: "not-an-array", plan_name: "plan", })) //#when const result = readBoulderState(TEST_DIR) //#then expect(result).not.toBeNull() expect(result!.session_ids).toEqual([]) }) test("should default session_ids to [] for empty object", () => { //#given - boulder.json with empty object const boulderFile = join(SISYPHUS_DIR, "boulder.json") writeFileSync(boulderFile, JSON.stringify({})) //#when const result = readBoulderState(TEST_DIR) //#then expect(result).not.toBeNull() expect(result!.session_ids).toEqual([]) }) test("should backfill missing origin as direct only for a single tracked session", () => { // given const boulderFile = join(SISYPHUS_DIR, "boulder.json") writeFileSync(boulderFile, JSON.stringify({ active_plan: "/path/to/plan.md", started_at: "2026-01-01T00:00:00Z", session_ids: ["session-1"], plan_name: "plan", })) // when const result = readBoulderState(TEST_DIR) // then expect(result?.session_origins).toEqual({ "session-1": "direct" }) }) test("should keep missing origins empty when multiple sessions are tracked", () => { // given const boulderFile = join(SISYPHUS_DIR, "boulder.json") writeFileSync(boulderFile, JSON.stringify({ active_plan: "/path/to/plan.md", started_at: "2026-01-01T00:00:00Z", session_ids: ["session-1", "session-2"], plan_name: "plan", })) // when const result = readBoulderState(TEST_DIR) // then expect(result?.session_origins).toEqual({}) }) test("should read valid boulder state", () => { // given - valid boulder.json const state: BoulderState = { active_plan: "/path/to/plan.md", started_at: "2026-01-02T10:00:00Z", session_ids: ["session-1", "session-2"], plan_name: "my-plan", } writeBoulderState(TEST_DIR, state) // when const result = readBoulderState(TEST_DIR) // then expect(result).not.toBeNull() expect(result?.active_plan).toBe("/path/to/plan.md") expect(result?.session_ids).toEqual(["session-1", "session-2"]) expect(result?.plan_name).toBe("my-plan") }) test("should default task_sessions to empty object when missing from JSON", () => { // given - boulder.json without task_sessions field const boulderFile = join(SISYPHUS_DIR, "boulder.json") writeFileSync(boulderFile, JSON.stringify({ active_plan: "/path/to/plan.md", started_at: "2026-01-01T00:00:00Z", session_ids: ["session-1"], plan_name: "plan", })) // when const result = readBoulderState(TEST_DIR) // then expect(result).not.toBeNull() expect(result!.task_sessions).toEqual({}) }) }) describe("writeBoulderState", () => { test("should write state and create .sisyphus directory if needed", () => { // given - state to write const state: BoulderState = { active_plan: "/test/plan.md", started_at: "2026-01-02T12:00:00Z", session_ids: ["ses-123"], plan_name: "test-plan", } // when const success = writeBoulderState(TEST_DIR, state) const readBack = readBoulderState(TEST_DIR) // then expect(success).toBe(true) expect(readBack).not.toBeNull() expect(readBack?.active_plan).toBe("/test/plan.md") }) }) describe("appendSessionId", () => { test("should append new session id to existing state", () => { // given - existing state with one session const state: BoulderState = { active_plan: "/plan.md", started_at: "2026-01-02T10:00:00Z", session_ids: ["session-1"], plan_name: "plan", } writeBoulderState(TEST_DIR, state) // when const result = appendSessionId(TEST_DIR, "session-2") // then expect(result).not.toBeNull() expect(result?.session_ids).toEqual(["session-1", "session-2"]) }) test("should not duplicate existing session id", () => { // given - state with session-1 already const state: BoulderState = { active_plan: "/plan.md", started_at: "2026-01-02T10:00:00Z", session_ids: ["session-1"], plan_name: "plan", } writeBoulderState(TEST_DIR, state) // when appendSessionId(TEST_DIR, "session-1") const result = readBoulderState(TEST_DIR) // then expect(result?.session_ids).toEqual(["session-1"]) }) test("should return null when no state exists", () => { // given - no boulder.json // when const result = appendSessionId(TEST_DIR, "new-session") // then expect(result).toBeNull() }) test("should not crash when boulder.json has no session_ids field", () => { //#given - boulder.json without session_ids const boulderFile = join(SISYPHUS_DIR, "boulder.json") writeFileSync(boulderFile, JSON.stringify({ active_plan: "/plan.md", started_at: "2026-01-01T00:00:00Z", plan_name: "plan", })) //#when const result = appendSessionId(TEST_DIR, "ses-new") //#then - should not crash and should contain the new session expect(result).not.toBeNull() expect(result!.session_ids).toContain("ses-new") }) test("should persist appended session origin when provided", () => { // given writeBoulderState(TEST_DIR, { active_plan: "/path/to/plan.md", started_at: "2026-01-02T10:00:00Z", session_ids: ["session-1"], session_origins: { "session-1": "direct" }, plan_name: "plan", }) // when const result = appendSessionId(TEST_DIR, "session-2", "appended") // then expect(result?.session_origins).toEqual({ "session-1": "direct", "session-2": "appended", }) }) }) describe("clearBoulderState", () => { test("should remove boulder.json", () => { // given - existing state const state: BoulderState = { active_plan: "/plan.md", started_at: "2026-01-02T10:00:00Z", session_ids: ["session-1"], plan_name: "plan", } writeBoulderState(TEST_DIR, state) // when const success = clearBoulderState(TEST_DIR) const result = readBoulderState(TEST_DIR) // then expect(success).toBe(true) expect(result).toBeNull() }) test("should succeed even when no file exists", () => { // given - no boulder.json // when const success = clearBoulderState(TEST_DIR) // then expect(success).toBe(true) }) }) describe("task session state", () => { test("should persist and read preferred session for a top-level plan task", () => { // given - existing boulder state const state: BoulderState = { active_plan: "/plan.md", started_at: "2026-01-02T10:00:00Z", session_ids: ["session-1"], plan_name: "plan", } writeBoulderState(TEST_DIR, state) // when upsertTaskSessionState(TEST_DIR, { taskKey: "todo:1", taskLabel: "1", taskTitle: "Implement auth flow", sessionId: "ses_task_123", agent: "sisyphus-junior", category: "deep", }) const result = getTaskSessionState(TEST_DIR, "todo:1") // then expect(result).not.toBeNull() expect(result?.session_id).toBe("ses_task_123") expect(result?.task_title).toBe("Implement auth flow") expect(result?.agent).toBe("sisyphus-junior") expect(result?.category).toBe("deep") }) test("should overwrite preferred session for the same top-level plan task", () => { // given - existing boulder state with prior preferred session const state: BoulderState = { active_plan: "/plan.md", started_at: "2026-01-02T10:00:00Z", session_ids: ["session-1"], plan_name: "plan", task_sessions: { "todo:1": { task_key: "todo:1", task_label: "1", task_title: "Implement auth flow", session_id: "ses_old", updated_at: "2026-01-02T10:00:00Z", }, }, } writeBoulderState(TEST_DIR, state) // when upsertTaskSessionState(TEST_DIR, { taskKey: "todo:1", taskLabel: "1", taskTitle: "Implement auth flow", sessionId: "ses_new", }) const result = getTaskSessionState(TEST_DIR, "todo:1") // then expect(result?.session_id).toBe("ses_new") }) }) describe("multi-work helpers", () => { test("should add second work and keep both active works", () => { // given const firstState = createBoulderState( join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a", "atlas", "/worktree-a", ) writeBoulderState(TEST_DIR, firstState) const firstWorkId = firstState.active_work_id // when const updatedState = addBoulderWork(TEST_DIR, { planPath: join(TEST_DIR, ".sisyphus/plans/plan-b.md"), sessionId: "session-b", agent: "atlas", worktreePath: "/worktree-b", }) // then expect(updatedState).not.toBeNull() const works = updatedState?.works ?? {} expect(Object.keys(works).length).toBe(2) expect(firstWorkId).toBeDefined() expect(works[firstWorkId!]).toBeDefined() expect(updatedState?.active_plan).toContain("plan-b.md") expect(getActiveWorks(TEST_DIR).length).toBe(2) }) test("should resolve work for session using updated_at tie-break", () => { // given const baseState = createBoulderState( join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a", ) writeBoulderState(TEST_DIR, baseState) const stateWithSecond = addBoulderWork(TEST_DIR, { planPath: join(TEST_DIR, ".sisyphus/plans/plan-b.md"), sessionId: "session-b", }) expect(stateWithSecond).not.toBeNull() const workIds = Object.keys(stateWithSecond!.works ?? {}) expect(workIds.length).toBe(2) const firstWorkId = workIds.find((workId) => (stateWithSecond!.works?.[workId]?.plan_name ?? "") === "plan-a")! const secondWorkId = workIds.find((workId) => (stateWithSecond!.works?.[workId]?.plan_name ?? "") === "plan-b")! appendSessionIdForWork(TEST_DIR, secondWorkId, "session-a", "appended") appendSessionIdForWork(TEST_DIR, firstWorkId, "session-a", "appended") // when const resolvedWork = getWorkForSession(TEST_DIR, "session-a") // then expect(resolvedWork?.work_id).toBe(firstWorkId) }) test("should support selecting active work and read helpers", () => { // given const initialState = createBoulderState(join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a") writeBoulderState(TEST_DIR, initialState) const added = addBoulderWork(TEST_DIR, { planPath: join(TEST_DIR, ".sisyphus/plans/plan-b.md"), sessionId: "session-b", worktreePath: "/tmp/worktree-b", }) expect(added).not.toBeNull() const firstWork = getWorkByPlanName(TEST_DIR, "plan-a") expect(firstWork).not.toBeNull() // when const selected = selectActiveWork(TEST_DIR, firstWork!.work_id) const selectedById = getWorkById(TEST_DIR, firstWork!.work_id) const byPlanNameWithWorktree = getWorkByPlanName(TEST_DIR, "plan-b", { worktreePath: "/tmp/worktree-b" }) const byPlanPath = resolveBoulderPlanPathForWork(TEST_DIR, firstWork!) const resumeOptions = getWorkResumeOptions(TEST_DIR) const worksFromState = getBoulderWorks(selected!) // then expect(selected?.active_work_id).toBe(firstWork!.work_id) expect(selectedById?.work_id).toBe(firstWork!.work_id) expect(byPlanNameWithWorktree?.plan_name).toBe("plan-b") expect(byPlanPath.endsWith("plan-a.md")).toBe(true) expect(resumeOptions.length).toBe(2) expect(worksFromState.length).toBe(2) }) test("should upsert task session for specific work and keep first started_at", () => { // given const initialState = createBoulderState(join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a") writeBoulderState(TEST_DIR, initialState) const workId = initialState.active_work_id! upsertTaskSessionStateForWork(TEST_DIR, workId, { taskKey: "todo:1", taskLabel: "1", taskTitle: "task one", sessionId: "task-session-a", }) const seededState = readBoulderState(TEST_DIR)! seededState.works![workId]!.task_sessions!["todo:1"]!.started_at = "2026-01-01T00:00:00.000Z" writeBoulderState(TEST_DIR, seededState) // when const updated = upsertTaskSessionStateForWork(TEST_DIR, workId, { taskKey: "todo:1", taskLabel: "1", taskTitle: "task one", sessionId: "task-session-b", }) // then expect(updated).not.toBeNull() const taskSession = updated?.works?.[workId]?.task_sessions?.["todo:1"] expect(taskSession?.session_id).toBe("task-session-b") expect(taskSession?.started_at).toBe("2026-01-01T00:00:00.000Z") }) }) describe("task timer and completion helpers", () => { test("should keep started_at stable when starting timer repeatedly", () => { // given const initialState = createBoulderState(join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a") writeBoulderState(TEST_DIR, initialState) const workId = initialState.active_work_id! // when startTaskTimer(TEST_DIR, workId, { taskKey: "todo:1", taskLabel: "1", taskTitle: "task one", sessionId: "session-a", startedAt: "2026-01-01T00:00:00.000Z", }) startTaskTimer(TEST_DIR, workId, { taskKey: "todo:1", taskLabel: "1", taskTitle: "task one", sessionId: "session-a", startedAt: "2026-01-02T00:00:00.000Z", }) // then const taskSession = readBoulderState(TEST_DIR)?.works?.[workId]?.task_sessions?.["todo:1"] expect(taskSession?.started_at).toBe("2026-01-01T00:00:00.000Z") expect(taskSession?.status).toBe("running") }) test("should compute elapsed_ms when ending task timer", () => { // given const initialState = createBoulderState(join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a") writeBoulderState(TEST_DIR, initialState) const workId = initialState.active_work_id! startTaskTimer(TEST_DIR, workId, { taskKey: "todo:1", taskLabel: "1", taskTitle: "task one", sessionId: "session-a", startedAt: "2026-01-01T00:00:00.000Z", }) // when const endedState = endTaskTimer(TEST_DIR, workId, "todo:1", "2026-01-01T00:00:01.500Z") // then const taskSession = endedState?.works?.[workId]?.task_sessions?.["todo:1"] expect(taskSession?.ended_at).toBe("2026-01-01T00:00:01.500Z") expect(taskSession?.elapsed_ms).toBe(1500) expect(taskSession?.status).toBe("completed") }) test("should complete one work and keep other work untouched", () => { // given const initialState = createBoulderState(join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a") writeBoulderState(TEST_DIR, initialState) const firstWorkId = initialState.active_work_id! const withSecond = addBoulderWork(TEST_DIR, { planPath: join(TEST_DIR, ".sisyphus/plans/plan-b.md"), sessionId: "session-b", }) const secondWorkId = Object.keys(withSecond!.works!).find((workId) => workId !== firstWorkId)! // when const completedState = completeBoulder(TEST_DIR, firstWorkId, "2026-01-01T01:00:00.000Z") // then expect(completedState?.works?.[firstWorkId]?.status).toBe("completed") expect(completedState?.works?.[firstWorkId]?.ended_at).toBe("2026-01-01T01:00:00.000Z") expect(completedState?.works?.[firstWorkId]?.elapsed_ms).toBe( Date.parse("2026-01-01T01:00:00.000Z") - Date.parse(completedState!.works![firstWorkId]!.started_at), ) expect(completedState?.works?.[secondWorkId]?.status).not.toBe("completed") expect(existsSync(join(SISYPHUS_DIR, "boulder.json"))).toBe(true) }) test("should keep first completion timing when completeBoulder is called repeatedly", () => { // given const initialState = createBoulderState( join(TEST_DIR, ".sisyphus/plans/plan-idempotent.md"), "session-a", ) writeBoulderState(TEST_DIR, initialState) const workId = initialState.active_work_id! // when const firstCompletedState = completeBoulder(TEST_DIR, workId, "2026-01-01T00:01:00Z") const secondCompletedState = completeBoulder(TEST_DIR, workId, "2026-01-01T01:00:00Z") // then expect(firstCompletedState?.works?.[workId]?.ended_at).toBe("2026-01-01T00:01:00Z") expect(secondCompletedState?.works?.[workId]?.ended_at).toBe("2026-01-01T00:01:00Z") expect(secondCompletedState?.works?.[workId]?.elapsed_ms).toBe( Date.parse("2026-01-01T00:01:00Z") - Date.parse(secondCompletedState!.works![workId]!.started_at), ) }) }) describe("readCurrentTopLevelTask", () => { test("should return the first unchecked top-level task in TODOs", () => { // given - plan with nested and top-level unchecked tasks const planPath = join(TEST_DIR, "current-task-plan.md") writeFileSync(planPath, `# Plan ## TODOs - [x] 1. Finished task - [ ] nested acceptance checkbox - [ ] 2. Current task ## Final Verification Wave - [ ] F1. Final review `) // when const result = readCurrentTopLevelTask(planPath) // then expect(result).not.toBeNull() expect(result?.key).toBe("todo:2") expect(result?.title).toBe("Current task") }) test("should fall back to final-wave task when implementation tasks are complete", () => { // given - plan with only final-wave work remaining const planPath = join(TEST_DIR, "final-wave-current-task-plan.md") writeFileSync(planPath, `# Plan ## TODOs - [x] 1. Finished task ## Final Verification Wave - [ ] F1. Final review `) // when const result = readCurrentTopLevelTask(planPath) // then expect(result).not.toBeNull() expect(result?.key).toBe("final-wave:f1") expect(result?.title).toBe("Final review") }) }) describe("getPlanProgress", () => { test("should count only top-level tasks under TODOs and Final Verification Wave sections", () => { // given - plan with top-level tasks in tracked sections const planPath = join(TEST_DIR, "test-plan.md") writeFileSync(planPath, `# Plan ## TODOs - [ ] 1. Task 1 - [x] 2. Task 2 - [ ] 3. Task 3 - [X] 4. Task 4 ## Final Verification Wave - [ ] F1. Final review `) // when const progress = getPlanProgress(planPath) // then expect(progress.total).toBe(5) expect(progress.completed).toBe(2) expect(progress.isComplete).toBe(false) }) test("should ignore nested Acceptance Criteria checkboxes under TODOs (issue #3066)", () => { // given - plan with 9 completed top-level tasks and unchecked nested acceptance criteria const planPath = join(TEST_DIR, "issue-3066-plan.md") writeFileSync(planPath, `# Plan ## TODOs - [x] 1. Implement feature A **Acceptance Criteria** - [ ] criterion 1 - [ ] criterion 2 - [x] 2. Implement feature B **Acceptance Criteria** - [ ] criterion 3 - [ ] criterion 4 - [x] 3. Implement feature C - [x] 4. Implement feature D - [x] 5. Implement feature E - [x] 6. Implement feature F - [x] 7. Implement feature G - [x] 8. Implement feature H - [x] 9. Implement feature I ## Final Verification Wave - [ ] F1. Final review `) // when const progress = getPlanProgress(planPath) // then expect(progress.total).toBe(10) expect(progress.completed).toBe(9) expect(progress.isComplete).toBe(false) }) test("should ignore checkboxes outside TODOs and Final Verification Wave sections", () => { // given - plan with checkboxes in Work Objectives, Success Criteria, and other sections const planPath = join(TEST_DIR, "ignore-other-sections-plan.md") writeFileSync(planPath, `# Plan ## Work Objectives ### Definition of Done - [ ] Verifiable condition with command ## TODOs - [x] 1. Real task one - [ ] 2. Real task two ## Success Criteria ### Final Checklist - [ ] All Must Have present - [ ] All Must NOT Have absent - [ ] All tests pass `) // when const progress = getPlanProgress(planPath) // then expect(progress.total).toBe(2) expect(progress.completed).toBe(1) expect(progress.isComplete).toBe(false) }) test("should ignore indented checkboxes under top-level tasks", () => { // given - plan with indented unchecked nested checkboxes const planPath = join(TEST_DIR, "nested-indented-plan.md") writeFileSync(planPath, `# Plan ## TODOs - [x] 1. top-level completed task - [ ] nested unchecked task `) // when const progress = getPlanProgress(planPath) // then expect(progress.total).toBe(1) expect(progress.completed).toBe(1) expect(progress.isComplete).toBe(true) }) test("should require proper task label format in TODOs", () => { // given - plan with malformed labels (no numeric prefix) const planPath = join(TEST_DIR, "malformed-labels-plan.md") writeFileSync(planPath, `# Plan ## TODOs - [ ] no number prefix - [x] 1. Valid numbered task `) // when const progress = getPlanProgress(planPath) // then expect(progress.total).toBe(1) expect(progress.completed).toBe(1) expect(progress.isComplete).toBe(true) }) test("should require F-prefix label format in Final Verification Wave", () => { // given - plan with malformed final-wave labels const planPath = join(TEST_DIR, "malformed-final-plan.md") writeFileSync(planPath, `# Plan ## TODOs - [x] 1. Implementation done ## Final Verification Wave - [ ] missing F-prefix - [ ] F1. Proper final review - [x] F2. Another final review `) // when const progress = getPlanProgress(planPath) // then expect(progress.total).toBe(3) expect(progress.completed).toBe(2) expect(progress.isComplete).toBe(false) }) test("should return isComplete true when all top-level tasks checked", () => { // given - all top-level tasks completed const planPath = join(TEST_DIR, "complete-plan.md") writeFileSync(planPath, `# Plan ## TODOs - [x] 1. Task 1 - [X] 2. Task 2 ## Final Verification Wave - [x] F1. Final review `) // when const progress = getPlanProgress(planPath) // then expect(progress.total).toBe(3) expect(progress.completed).toBe(3) expect(progress.isComplete).toBe(true) }) test("should return isComplete false for empty plan", () => { // given - plan with no checkboxes const planPath = join(TEST_DIR, "empty-plan.md") writeFileSync(planPath, "# Plan\nNo tasks here") // when const progress = getPlanProgress(planPath) // then expect(progress.total).toBe(0) expect(progress.isComplete).toBe(false) }) test("should handle non-existent file", () => { // given - non-existent file // when const progress = getPlanProgress("/non/existent/file.md") // then expect(progress.total).toBe(0) expect(progress.completed).toBe(0) expect(progress.isComplete).toBe(false) }) test("should support asterisk bullet top-level tasks", () => { // given - plan with asterisk bullet tasks const planPath = join(TEST_DIR, "asterisk-bullet-plan.md") writeFileSync(planPath, `# Plan ## TODOs * [x] 1. Task using asterisk bullet * [ ] 2. Another asterisk task `) // when const progress = getPlanProgress(planPath) // then expect(progress.total).toBe(2) expect(progress.completed).toBe(1) expect(progress.isComplete).toBe(false) }) test("should count only top-level checkboxes for simple plans with nested tasks", () => { // given const planPath = join(TEST_DIR, "simple-nested-plan.md") writeFileSync(planPath, `# Plan - [ ] Top-level task 1 - [x] Nested task ignored - [x] Top-level task 2 * [ ] Another nested task ignored `) // when const progress = getPlanProgress(planPath) // then expect(progress.total).toBe(2) expect(progress.completed).toBe(1) expect(progress.isComplete).toBe(false) }) test("should treat final-wave-only plans as structured mode", () => { // given const planPath = join(TEST_DIR, "final-wave-only-plan.md") writeFileSync(planPath, `# Plan ## Final Verification Wave - [ ] F1. Top-level final review - [x] Nested verification detail ignored `) // when const progress = getPlanProgress(planPath) // then expect(progress.total).toBe(1) expect(progress.completed).toBe(0) expect(progress.isComplete).toBe(false) }) test("should ignore mixed indentation levels in simple plans", () => { // given const planPath = join(TEST_DIR, "simple-mixed-indentation-plan.md") writeFileSync(planPath, `# Plan * [x] Top-level star task - [ ] Indented task ignored - [x] Tab-indented task ignored - [ ] Top-level dash task `) // when const progress = getPlanProgress(planPath) // then expect(progress.total).toBe(2) expect(progress.completed).toBe(1) expect(progress.isComplete).toBe(false) }) }) describe("getPlanName", () => { test("should extract plan name from path", () => { // given const path = "/home/user/.sisyphus/plans/project/my-feature.md" // when const name = getPlanName(path) // then expect(name).toBe("my-feature") }) }) describe("createBoulderState", () => { test("should create state with correct fields", () => { // given const planPath = "/path/to/auth-refactor.md" const sessionId = "ses-abc123" // when const state = createBoulderState(planPath, sessionId) // then expect(state.active_plan).toBe(planPath) expect(state.session_ids).toEqual([sessionId]) expect(state.plan_name).toBe("auth-refactor") expect(state.started_at).toBeDefined() }) test("should include agent field when provided", () => { //#given - plan path, session id, and agent type const planPath = "/path/to/feature.md" const sessionId = "ses-xyz789" const agent = "atlas" //#when - createBoulderState is called with agent const state = createBoulderState(planPath, sessionId, agent) //#then - state should include the agent field expect(state.agent).toBe("atlas") expect(state.active_plan).toBe(planPath) expect(state.session_ids).toEqual([sessionId]) expect(state.plan_name).toBe("feature") }) test("should mark the initial session origin as direct", () => { // given const planPath = "/path/to/feature.md" const sessionId = "ses-origin" // when const state = createBoulderState(planPath, sessionId) // then expect(state.session_origins).toEqual({ [sessionId]: "direct" }) }) test("should allow agent to be undefined", () => { //#given - plan path and session id without agent const planPath = "/path/to/legacy.md" const sessionId = "ses-legacy" //#when - createBoulderState is called without agent const state = createBoulderState(planPath, sessionId) //#then - state should not have agent field (backward compatible) expect(state.agent).toBeUndefined() }) }) describe("resolveBoulderPlanPath", () => { test("should prefer the mirrored worktree plan when it exists", () => { // given const planPath = join(TEST_DIR, ".sisyphus", "plans", "worktree-plan.md") const worktreeDir = join(tmpdir(), `boulder-state-worktree-${Date.now()}`) const worktreePlanPath = join(worktreeDir, ".sisyphus", "plans", "worktree-plan.md") mkdirSync(dirname(planPath), { recursive: true }) mkdirSync(dirname(worktreePlanPath), { recursive: true }) writeFileSync(planPath, "# Plan\n- [ ] Main repo task\n") writeFileSync(worktreePlanPath, "# Plan\n- [x] Worktree task\n") try { // when const resolvedPath = resolveBoulderPlanPath(TEST_DIR, { active_plan: planPath, worktree_path: worktreeDir, }) // then expect(resolvedPath).toBe(worktreePlanPath) } finally { rmSync(worktreeDir, { recursive: true, force: true }) } }) test("should fall back to the tracked plan when the mirrored worktree plan is missing", () => { // given const planPath = join(TEST_DIR, ".sisyphus", "plans", "fallback-plan.md") mkdirSync(dirname(planPath), { recursive: true }) writeFileSync(planPath, "# Plan\n- [ ] Main repo task\n") // when const resolvedPath = resolveBoulderPlanPath(TEST_DIR, { active_plan: planPath, worktree_path: join(tmpdir(), `missing-worktree-${Date.now()}`), }) // then expect(resolvedPath).toBe(planPath) }) }) })