fix(ralph-loop): detect completion promise from session messages API (#413)

* fix(ralph-loop): detect completion promise from session messages API

The completion promise (e.g., <promise>DONE</promise>) was not being detected
because assistant text messages were never recorded to the transcript file.
Only user messages, tool uses, and tool results were recorded.

This fix adds a new detection method that fetches session messages via the
OpenCode API and checks assistant text messages for the completion promise.
The transcript file check is kept as a fallback.

Fixes #412

* refactor(ralph-loop): address review feedback

- Simplify response parsing to use response.data consistently (greptile)
- Add session ID tracking to messages mock for better test coverage (cubic)
- Add assertion to verify correct session ID is passed to API

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
This commit is contained in:
Sisyphus
2026-01-02 16:37:04 +09:00
committed by GitHub
parent 3224c15578
commit 3ba61790ab
2 changed files with 93 additions and 4 deletions

View File

@@ -10,6 +10,8 @@ describe("ralph-loop", () => {
const TEST_DIR = join(tmpdir(), "ralph-loop-test-" + Date.now())
let promptCalls: Array<{ sessionID: string; text: string }>
let toastCalls: Array<{ title: string; message: string; variant: string }>
let messagesCalls: Array<{ sessionID: string }>
let mockSessionMessages: Array<{ info?: { role?: string }; parts?: Array<{ type: string; text?: string }> }>
function createMockPluginInput() {
return {
@@ -22,6 +24,10 @@ describe("ralph-loop", () => {
})
return {}
},
messages: async (opts: { path: { id: string } }) => {
messagesCalls.push({ sessionID: opts.path.id })
return { data: mockSessionMessages }
},
},
tui: {
showToast: async (opts: { body: { title: string; message: string; variant: string } }) => {
@@ -35,12 +41,14 @@ describe("ralph-loop", () => {
},
},
directory: TEST_DIR,
} as Parameters<typeof createRalphLoopHook>[0]
} as unknown as Parameters<typeof createRalphLoopHook>[0]
}
beforeEach(() => {
promptCalls = []
toastCalls = []
messagesCalls = []
mockSessionMessages = []
if (!existsSync(TEST_DIR)) {
mkdirSync(TEST_DIR, { recursive: true })
@@ -351,6 +359,35 @@ describe("ralph-loop", () => {
expect(hook.getState()).toBeNull()
})
test("should detect completion promise via session messages API", async () => {
// #given - active loop with assistant message containing completion promise
mockSessionMessages = [
{ info: { role: "user" }, parts: [{ type: "text", text: "Build something" }] },
{ info: { role: "assistant" }, parts: [{ type: "text", text: "I have completed the task. <promise>API_DONE</promise>" }] },
]
const hook = createRalphLoopHook(createMockPluginInput(), {
getTranscriptPath: () => join(TEST_DIR, "nonexistent.jsonl"),
})
hook.startLoop("session-123", "Build something", { completionPromise: "API_DONE" })
// #when - session goes idle
await hook.event({
event: {
type: "session.idle",
properties: { sessionID: "session-123" },
},
})
// #then - loop completed via API detection, no continuation
expect(promptCalls.length).toBe(0)
expect(toastCalls.some((t) => t.title === "Ralph Loop Complete!")).toBe(true)
expect(hook.getState()).toBeNull()
// #then - messages API was called with correct session ID
expect(messagesCalls.length).toBe(1)
expect(messagesCalls[0].sessionID).toBe("session-123")
})
test("should handle multiple iterations correctly", async () => {
// #given - active loop
const hook = createRalphLoopHook(createMockPluginInput())