fix: enforce continuation-aware completion gating

This commit is contained in:
YeonGyu-Kim 2026-02-17 14:52:40 +09:00
parent 47a8c3e4a9
commit 7b2c2529fe
3 changed files with 193 additions and 0 deletions

View File

@ -0,0 +1,138 @@
import { describe, it, expect, mock, spyOn, afterEach } from "bun:test"
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { tmpdir } from "node:os"
import type { RunContext } from "./types"
import { writeState as writeRalphLoopState } from "../../hooks/ralph-loop/storage"
const testDirs: string[] = []
afterEach(() => {
while (testDirs.length > 0) {
const dir = testDirs.pop()
if (dir) {
rmSync(dir, { recursive: true, force: true })
}
}
})
function createTempDir(): string {
const dir = mkdtempSync(join(tmpdir(), "omo-run-continuation-"))
testDirs.push(dir)
return dir
}
function createMockContext(directory: string): RunContext {
return {
client: {
session: {
todo: mock(() => Promise.resolve({ data: [] })),
children: mock(() => Promise.resolve({ data: [] })),
status: mock(() => Promise.resolve({ data: {} })),
},
} as unknown as RunContext["client"],
sessionID: "test-session",
directory,
abortController: new AbortController(),
}
}
function writeBoulderStateFile(directory: string, activePlanPath: string, sessionIDs: string[]): void {
const sisyphusDir = join(directory, ".sisyphus")
mkdirSync(sisyphusDir, { recursive: true })
writeFileSync(
join(sisyphusDir, "boulder.json"),
JSON.stringify({
active_plan: activePlanPath,
started_at: new Date().toISOString(),
session_ids: sessionIDs,
plan_name: "test-plan",
agent: "atlas",
}),
"utf-8",
)
}
describe("checkCompletionConditions continuation coverage", () => {
it("returns false when active boulder continuation exists for this session", async () => {
// given
spyOn(console, "log").mockImplementation(() => {})
const directory = createTempDir()
const planPath = join(directory, ".sisyphus", "plans", "active-plan.md")
mkdirSync(join(directory, ".sisyphus", "plans"), { recursive: true })
writeFileSync(planPath, "- [ ] incomplete task\n", "utf-8")
writeBoulderStateFile(directory, planPath, ["test-session"])
const ctx = createMockContext(directory)
const { checkCompletionConditions } = await import("./completion")
// when
const result = await checkCompletionConditions(ctx)
// then
expect(result).toBe(false)
})
it("returns true when boulder exists but is complete", async () => {
// given
spyOn(console, "log").mockImplementation(() => {})
const directory = createTempDir()
const planPath = join(directory, ".sisyphus", "plans", "done-plan.md")
mkdirSync(join(directory, ".sisyphus", "plans"), { recursive: true })
writeFileSync(planPath, "- [x] completed task\n", "utf-8")
writeBoulderStateFile(directory, planPath, ["test-session"])
const ctx = createMockContext(directory)
const { checkCompletionConditions } = await import("./completion")
// when
const result = await checkCompletionConditions(ctx)
// then
expect(result).toBe(true)
})
it("returns false when active ralph-loop continuation exists for this session", async () => {
// given
spyOn(console, "log").mockImplementation(() => {})
const directory = createTempDir()
writeRalphLoopState(directory, {
active: true,
iteration: 2,
max_iterations: 10,
completion_promise: "DONE",
started_at: new Date().toISOString(),
prompt: "keep going",
session_id: "test-session",
})
const ctx = createMockContext(directory)
const { checkCompletionConditions } = await import("./completion")
// when
const result = await checkCompletionConditions(ctx)
// then
expect(result).toBe(false)
})
it("returns true when active ralph-loop is bound to another session", async () => {
// given
spyOn(console, "log").mockImplementation(() => {})
const directory = createTempDir()
writeRalphLoopState(directory, {
active: true,
iteration: 2,
max_iterations: 10,
completion_promise: "DONE",
started_at: new Date().toISOString(),
prompt: "keep going",
session_id: "other-session",
})
const ctx = createMockContext(directory)
const { checkCompletionConditions } = await import("./completion")
// when
const result = await checkCompletionConditions(ctx)
// then
expect(result).toBe(true)
})
})

View File

@ -1,6 +1,7 @@
import pc from "picocolors"
import type { RunContext, Todo, ChildSession, SessionStatus } from "./types"
import { normalizeSDKResponse } from "../../shared"
import { getContinuationState } from "./continuation-state"
export async function checkCompletionConditions(ctx: RunContext): Promise<boolean> {
try {
@ -12,6 +13,10 @@ export async function checkCompletionConditions(ctx: RunContext): Promise<boolea
return false
}
if (!areContinuationHooksIdle(ctx)) {
return false
}
return true
} catch (err) {
console.error(pc.red(`[completion] API error: ${err}`))
@ -19,6 +24,22 @@ export async function checkCompletionConditions(ctx: RunContext): Promise<boolea
}
}
function areContinuationHooksIdle(ctx: RunContext): boolean {
const continuationState = getContinuationState(ctx.directory, ctx.sessionID)
if (continuationState.hasActiveBoulder) {
console.log(pc.dim(" Waiting: boulder continuation is active"))
return false
}
if (continuationState.hasActiveRalphLoop) {
console.log(pc.dim(" Waiting: ralph-loop continuation is active"))
return false
}
return true
}
async function areAllTodosComplete(ctx: RunContext): Promise<boolean> {
const todosRes = await ctx.client.session.todo({
path: { id: ctx.sessionID },

View File

@ -0,0 +1,34 @@
import { getPlanProgress, readBoulderState } from "../../features/boulder-state"
import { readState as readRalphLoopState } from "../../hooks/ralph-loop/storage"
export interface ContinuationState {
hasActiveBoulder: boolean
hasActiveRalphLoop: boolean
}
export function getContinuationState(directory: string, sessionID: string): ContinuationState {
return {
hasActiveBoulder: hasActiveBoulderContinuation(directory, sessionID),
hasActiveRalphLoop: hasActiveRalphLoopContinuation(directory, sessionID),
}
}
function hasActiveBoulderContinuation(directory: string, sessionID: string): boolean {
const boulder = readBoulderState(directory)
if (!boulder) return false
if (!boulder.session_ids.includes(sessionID)) return false
const progress = getPlanProgress(boulder.active_plan)
return !progress.isComplete
}
function hasActiveRalphLoopContinuation(directory: string, sessionID: string): boolean {
const state = readRalphLoopState(directory)
if (!state || !state.active) return false
if (state.session_id && state.session_id !== sessionID) {
return false
}
return true
}