Files
oh-my-openagent/src/features/boulder-state/storage.test.ts
YeonGyu-Kim ef8f22caba fix(boulder-state): treat plans without checkboxes as incomplete (fixes #2648)
GPT/Gemini Prometheus plans sometimes lack markdown checkboxes.
Previously getPlanProgress() returned isComplete=true for 0/0,
causing /start-work to skip Atlas execution.

Now total=0 correctly returns isComplete=false so start-work
detects the invalid plan format.

🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 12:06:24 +09:00

433 lines
12 KiB
TypeScript

import { describe, expect, test, beforeEach, afterEach } from "bun:test"
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { tmpdir } from "node:os"
import {
readBoulderState,
writeBoulderState,
appendSessionId,
clearBoulderState,
getPlanProgress,
getPlanName,
createBoulderState,
findPrometheusPlans,
} from "./storage"
import type { BoulderState } from "./types"
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 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 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")
})
})
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")
})
})
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("getPlanProgress", () => {
test("should count completed and uncompleted checkboxes", () => {
// given - plan file with checkboxes
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, `# Plan
- [ ] Task 1
- [x] Task 2
- [ ] Task 3
- [X] Task 4
`)
// when
const progress = getPlanProgress(planPath)
// then
expect(progress.total).toBe(4)
expect(progress.completed).toBe(2)
expect(progress.isComplete).toBe(false)
})
test("should count space-indented unchecked checkbox", () => {
// given - plan file with a two-space indented checkbox
const planPath = join(TEST_DIR, "space-indented-plan.md")
writeFileSync(planPath, `# Plan
- [ ] indented task
`)
// when
const progress = getPlanProgress(planPath)
// then
expect(progress.total).toBe(1)
expect(progress.completed).toBe(0)
expect(progress.isComplete).toBe(false)
})
test("should count tab-indented unchecked checkbox", () => {
// given - plan file with a tab-indented checkbox
const planPath = join(TEST_DIR, "tab-indented-plan.md")
writeFileSync(planPath, `# Plan
- [ ] tab-indented task
`)
// when
const progress = getPlanProgress(planPath)
// then
expect(progress.total).toBe(1)
expect(progress.completed).toBe(0)
expect(progress.isComplete).toBe(false)
})
test("should count mixed top-level checked and indented unchecked checkboxes", () => {
// given - plan file with checked top-level and unchecked indented task
const planPath = join(TEST_DIR, "mixed-indented-plan.md")
writeFileSync(planPath, `# Plan
- [x] top-level completed task
- [ ] nested unchecked 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 space-indented completed checkbox", () => {
// given - plan file with a two-space indented completed checkbox
const planPath = join(TEST_DIR, "indented-completed-plan.md")
writeFileSync(planPath, `# Plan
- [x] indented completed task
`)
// when
const progress = getPlanProgress(planPath)
// then
expect(progress.total).toBe(1)
expect(progress.completed).toBe(1)
expect(progress.isComplete).toBe(true)
})
test("should return isComplete true when all checked", () => {
// given - all tasks completed
const planPath = join(TEST_DIR, "complete-plan.md")
writeFileSync(planPath, `# Plan
- [x] Task 1
- [X] Task 2
`)
// when
const progress = getPlanProgress(planPath)
// then
expect(progress.total).toBe(2)
expect(progress.completed).toBe(2)
expect(progress.isComplete).toBe(true)
})
test("should return isComplete false for plan with content but no checkboxes", () => {
// 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.isComplete).toBe(true)
})
})
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 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()
})
})
})