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