Rishi Vhavle d8137c0c90 fix: track agent in boulder state to fix session continuation (fixes #927)
Add 'agent' field to BoulderState to track which agent (atlas) should
resume on session continuation. Previously, when user typed 'continue'
after interruption, Prometheus (planner) resumed instead of Sisyphus
(executor), causing all delegate_task calls to get READ-ONLY mode.

Changes:
- Add optional 'agent' field to BoulderState interface
- Update createBoulderState() to accept agent parameter
- Set agent='atlas' when /start-work creates boulder.json
- Use stored agent on boulder continuation (defaults to 'atlas')
- Add tests for new agent field functionality
2026-02-04 21:21:57 +05:30

279 lines
7.8 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 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()
})
})
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 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 true 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(true)
})
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()
})
})
})