refactor: major codebase cleanup - BDD comments, file splitting, bug fixes (#1350)

* style(tests): normalize BDD comments from '// #given' to '// given'

- Replace 4,668 Python-style BDD comments across 107 test files
- Patterns changed: // #given -> // given, // #when -> // when, // #then -> // then
- Also handles no-space variants: //#given -> // given

* fix(rules-injector): prefer output.metadata.filePath over output.title

- Extract file path resolution to dedicated output-path.ts module
- Prefer metadata.filePath which contains actual file path
- Fall back to output.title only when metadata unavailable
- Fixes issue where rules weren't injected when tool output title was a label

* feat(slashcommand): add optional user_message parameter

- Add user_message optional parameter for command arguments
- Model can now call: command='publish' user_message='patch'
- Improves error messages with clearer format guidance
- Helps LLMs understand correct parameter usage

* feat(hooks): restore compaction-context-injector hook

- Restore hook deleted in cbbc7bd0 for session compaction context
- Injects 7 mandatory sections: User Requests, Final Goal, Work Completed,
  Remaining Tasks, Active Working Context, MUST NOT Do, Agent Verification State
- Re-register in hooks/index.ts and main plugin entry

* refactor(background-agent): split manager.ts into focused modules

- Extract constants.ts for TTL values and internal types (52 lines)
- Extract state.ts for TaskStateManager class (204 lines)
- Extract spawner.ts for task creation logic (244 lines)
- Extract result-handler.ts for completion handling (265 lines)
- Reduce manager.ts from 1377 to 755 lines (45% reduction)
- Maintain backward compatible exports

* refactor(agents): split prometheus-prompt.ts into subdirectory

- Move 1196-line prometheus-prompt.ts to prometheus/ subdirectory
- Organize prompt sections into separate files for maintainability
- Update agents/index.ts exports

* refactor(delegate-task): split tools.ts into focused modules

- Extract categories.ts for category definitions and routing
- Extract executor.ts for task execution logic
- Extract helpers.ts for utility functions
- Extract prompt-builder.ts for prompt construction
- Reduce tools.ts complexity with cleaner separation of concerns

* refactor(builtin-skills): split skills.ts into individual skill files

- Move each skill to dedicated file in skills/ subdirectory
- Create barrel export for backward compatibility
- Improve maintainability with focused skill modules

* chore: update import paths and lockfile

- Update prometheus import path after refactor
- Update bun.lock

* fix(tests): complete BDD comment normalization

- Fix remaining #when/#then patterns missed by initial sed
- Affected: state.test.ts, events.test.ts

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
This commit is contained in:
YeonGyu-Kim
2026-02-01 16:47:50 +09:00
committed by GitHub
parent c83150d9ea
commit f146aeff0f
145 changed files with 10307 additions and 9562 deletions
+127 -127
View File
@@ -66,7 +66,7 @@ describe("ralph-loop", () => {
describe("storage", () => {
test("should write and read state correctly", () => {
// #given - a state object
// given - a state object
const state: RalphLoopState = {
active: true,
iteration: 1,
@@ -77,11 +77,11 @@ describe("ralph-loop", () => {
session_id: "test-session-123",
}
// #when - write and read state
// when - write and read state
const writeSuccess = writeState(TEST_DIR, state)
const readResult = readState(TEST_DIR)
// #then - state should match
// then - state should match
expect(writeSuccess).toBe(true)
expect(readResult).not.toBeNull()
expect(readResult?.active).toBe(true)
@@ -93,7 +93,7 @@ describe("ralph-loop", () => {
})
test("should handle ultrawork field", () => {
// #given - a state object with ultrawork enabled
// given - a state object with ultrawork enabled
const state: RalphLoopState = {
active: true,
iteration: 1,
@@ -105,25 +105,25 @@ describe("ralph-loop", () => {
ultrawork: true,
}
// #when - write and read state
// when - write and read state
writeState(TEST_DIR, state)
const readResult = readState(TEST_DIR)
// #then - ultrawork field should be preserved
// then - ultrawork field should be preserved
expect(readResult?.ultrawork).toBe(true)
})
test("should return null for non-existent state", () => {
// #given - no state file exists
// #when - read state
// given - no state file exists
// when - read state
const result = readState(TEST_DIR)
// #then - should return null
// then - should return null
expect(result).toBeNull()
})
test("should clear state correctly", () => {
// #given - existing state
// given - existing state
const state: RalphLoopState = {
active: true,
iteration: 1,
@@ -134,17 +134,17 @@ describe("ralph-loop", () => {
}
writeState(TEST_DIR, state)
// #when - clear state
// when - clear state
const clearSuccess = clearState(TEST_DIR)
const readResult = readState(TEST_DIR)
// #then - state should be cleared
// then - state should be cleared
expect(clearSuccess).toBe(true)
expect(readResult).toBeNull()
})
test("should handle multiline prompts", () => {
// #given - state with multiline prompt
// given - state with multiline prompt
const state: RalphLoopState = {
active: true,
iteration: 1,
@@ -154,27 +154,27 @@ describe("ralph-loop", () => {
prompt: "Build a feature\nwith multiple lines\nand requirements",
}
// #when - write and read
// when - write and read
writeState(TEST_DIR, state)
const readResult = readState(TEST_DIR)
// #then - multiline prompt preserved
// then - multiline prompt preserved
expect(readResult?.prompt).toBe("Build a feature\nwith multiple lines\nand requirements")
})
})
describe("hook", () => {
test("should start loop and write state", () => {
// #given - hook instance
// given - hook instance
const hook = createRalphLoopHook(createMockPluginInput())
// #when - start loop
// when - start loop
const success = hook.startLoop("session-123", "Build something", {
maxIterations: 25,
completionPromise: "FINISHED",
})
// #then - state should be written
// then - state should be written
expect(success).toBe(true)
const state = hook.getState()
expect(state?.active).toBe(true)
@@ -186,35 +186,35 @@ describe("ralph-loop", () => {
})
test("should accept ultrawork option in startLoop", () => {
// #given - hook instance
// given - hook instance
const hook = createRalphLoopHook(createMockPluginInput())
// #when - start loop with ultrawork
// when - start loop with ultrawork
hook.startLoop("session-123", "Build something", { ultrawork: true })
// #then - state should have ultrawork=true
// then - state should have ultrawork=true
const state = hook.getState()
expect(state?.ultrawork).toBe(true)
})
test("should handle missing ultrawork option in startLoop", () => {
// #given - hook instance
// given - hook instance
const hook = createRalphLoopHook(createMockPluginInput())
// #when - start loop without ultrawork
// when - start loop without ultrawork
hook.startLoop("session-123", "Build something")
// #then - state should have ultrawork=undefined
// then - state should have ultrawork=undefined
const state = hook.getState()
expect(state?.ultrawork).toBeUndefined()
})
test("should inject continuation when loop active and no completion detected", async () => {
// #given - active loop state
// given - active loop state
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Build a feature", { maxIterations: 10 })
// #when - session goes idle
// when - session goes idle
await hook.event({
event: {
type: "session.idle",
@@ -222,20 +222,20 @@ describe("ralph-loop", () => {
},
})
// #then - continuation should be injected
// then - continuation should be injected
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].sessionID).toBe("session-123")
expect(promptCalls[0].text).toContain("RALPH LOOP")
expect(promptCalls[0].text).toContain("Build a feature")
expect(promptCalls[0].text).toContain("2/10")
// #then - iteration should be incremented
// then - iteration should be incremented
const state = hook.getState()
expect(state?.iteration).toBe(2)
})
test("should stop loop when max iterations reached", async () => {
// #given - loop at max iteration
// given - loop at max iteration
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Build something", { maxIterations: 2 })
@@ -243,7 +243,7 @@ describe("ralph-loop", () => {
state.iteration = 2
writeState(TEST_DIR, state)
// #when - session goes idle
// when - session goes idle
await hook.event({
event: {
type: "session.idle",
@@ -251,46 +251,46 @@ describe("ralph-loop", () => {
},
})
// #then - no continuation injected
// then - no continuation injected
expect(promptCalls.length).toBe(0)
// #then - warning toast shown
// then - warning toast shown
expect(toastCalls.length).toBe(1)
expect(toastCalls[0].title).toBe("Ralph Loop Stopped")
expect(toastCalls[0].variant).toBe("warning")
// #then - state should be cleared
// then - state should be cleared
expect(hook.getState()).toBeNull()
})
test("should cancel loop via cancelLoop", () => {
// #given - active loop
// given - active loop
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Test task")
// #when - cancel loop
// when - cancel loop
const success = hook.cancelLoop("session-123")
// #then - loop cancelled
// then - loop cancelled
expect(success).toBe(true)
expect(hook.getState()).toBeNull()
})
test("should not cancel loop for different session", () => {
// #given - active loop for session-123
// given - active loop for session-123
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Test task")
// #when - try to cancel for different session
// when - try to cancel for different session
const success = hook.cancelLoop("session-456")
// #then - cancel should fail
// then - cancel should fail
expect(success).toBe(false)
expect(hook.getState()).not.toBeNull()
})
test("should skip injection during recovery", async () => {
// #given - active loop and session in recovery
// given - active loop and session in recovery
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Test task")
@@ -301,7 +301,7 @@ describe("ralph-loop", () => {
},
})
// #when - session goes idle immediately
// when - session goes idle immediately
await hook.event({
event: {
type: "session.idle",
@@ -309,16 +309,16 @@ describe("ralph-loop", () => {
},
})
// #then - no continuation injected
// then - no continuation injected
expect(promptCalls.length).toBe(0)
})
test("should clear state on session deletion", async () => {
// #given - active loop
// given - active loop
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Test task")
// #when - session deleted
// when - session deleted
await hook.event({
event: {
type: "session.deleted",
@@ -326,16 +326,16 @@ describe("ralph-loop", () => {
},
})
// #then - state should be cleared
// then - state should be cleared
expect(hook.getState()).toBeNull()
})
test("should not inject for different session than loop owner", async () => {
// #given - loop owned by session-123
// given - loop owned by session-123
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Test task")
// #when - different session goes idle
// when - different session goes idle
await hook.event({
event: {
type: "session.idle",
@@ -343,12 +343,12 @@ describe("ralph-loop", () => {
},
})
// #then - no continuation injected
// then - no continuation injected
expect(promptCalls.length).toBe(0)
})
test("should clear orphaned state when original session no longer exists", async () => {
// #given - state file exists from a previous session that no longer exists
// given - state file exists from a previous session that no longer exists
const state: RalphLoopState = {
active: true,
iteration: 3,
@@ -368,7 +368,7 @@ describe("ralph-loop", () => {
},
})
// #when - a new session goes idle (different from the orphaned session in state)
// when - a new session goes idle (different from the orphaned session in state)
await hook.event({
event: {
type: "session.idle",
@@ -376,14 +376,14 @@ describe("ralph-loop", () => {
},
})
// #then - orphaned state should be cleared
// then - orphaned state should be cleared
expect(hook.getState()).toBeNull()
// #then - no continuation injected (state was cleared, not resumed)
// then - no continuation injected (state was cleared, not resumed)
expect(promptCalls.length).toBe(0)
})
test("should NOT clear state when original session still exists (different active session)", async () => {
// #given - state file exists from a session that still exists
// given - state file exists from a session that still exists
const state: RalphLoopState = {
active: true,
iteration: 2,
@@ -403,7 +403,7 @@ describe("ralph-loop", () => {
},
})
// #when - a different session goes idle
// when - a different session goes idle
await hook.event({
event: {
type: "session.idle",
@@ -411,15 +411,15 @@ describe("ralph-loop", () => {
},
})
// #then - state should NOT be cleared (original session still active)
// then - state should NOT be cleared (original session still active)
expect(hook.getState()).not.toBeNull()
expect(hook.getState()?.session_id).toBe("active-session-123")
// #then - no continuation injected (it's a different session's loop)
// then - no continuation injected (it's a different session's loop)
expect(promptCalls.length).toBe(0)
})
test("should use default config values", () => {
// #given - hook with config
// given - hook with config
const hook = createRalphLoopHook(createMockPluginInput(), {
config: {
enabled: true,
@@ -427,19 +427,19 @@ describe("ralph-loop", () => {
},
})
// #when - start loop without options
// when - start loop without options
hook.startLoop("session-123", "Test task")
// #then - should use config defaults
// then - should use config defaults
const state = hook.getState()
expect(state?.max_iterations).toBe(200)
})
test("should not inject when no loop is active", async () => {
// #given - no active loop
// given - no active loop
const hook = createRalphLoopHook(createMockPluginInput())
// #when - session goes idle
// when - session goes idle
await hook.event({
event: {
type: "session.idle",
@@ -447,12 +447,12 @@ describe("ralph-loop", () => {
},
})
// #then - no continuation injected
// then - no continuation injected
expect(promptCalls.length).toBe(0)
})
test("should detect completion promise and stop loop", async () => {
// #given - active loop with transcript containing completion
// given - active loop with transcript containing completion
const transcriptPath = join(TEST_DIR, "transcript.jsonl")
const hook = createRalphLoopHook(createMockPluginInput(), {
getTranscriptPath: () => transcriptPath,
@@ -461,7 +461,7 @@ describe("ralph-loop", () => {
writeFileSync(transcriptPath, JSON.stringify({ type: "tool_result", tool_name: "write", tool_output: { output: "Task done <promise>COMPLETE</promise>" } }) + "\n")
// #when - session goes idle (transcriptPath now derived from sessionID via getTranscriptPath)
// when - session goes idle (transcriptPath now derived from sessionID via getTranscriptPath)
await hook.event({
event: {
type: "session.idle",
@@ -469,14 +469,14 @@ describe("ralph-loop", () => {
},
})
// #then - loop completed, no continuation
// then - loop completed, no continuation
expect(promptCalls.length).toBe(0)
expect(toastCalls.some((t) => t.title === "Ralph Loop Complete!")).toBe(true)
expect(hook.getState()).toBeNull()
})
test("should detect completion promise via session messages API", async () => {
// #given - active loop with assistant message containing completion promise
// 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>" }] },
@@ -486,7 +486,7 @@ describe("ralph-loop", () => {
})
hook.startLoop("session-123", "Build something", { completionPromise: "API_DONE" })
// #when - session goes idle
// when - session goes idle
await hook.event({
event: {
type: "session.idle",
@@ -494,22 +494,22 @@ describe("ralph-loop", () => {
},
})
// #then - loop completed via API detection, no continuation
// 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
// 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
// given - active loop
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Build feature", { maxIterations: 5 })
// #when - multiple idle events
// when - multiple idle events
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
@@ -517,36 +517,36 @@ describe("ralph-loop", () => {
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
// #then - iteration incremented correctly
// then - iteration incremented correctly
expect(hook.getState()?.iteration).toBe(3)
expect(promptCalls.length).toBe(2)
})
test("should include prompt and promise in continuation message", async () => {
// #given - loop with specific prompt and promise
// given - loop with specific prompt and promise
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Create a calculator app", {
completionPromise: "CALCULATOR_DONE",
maxIterations: 10,
})
// #when - session goes idle
// when - session goes idle
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
// #then - continuation includes original task and promise
// then - continuation includes original task and promise
expect(promptCalls[0].text).toContain("Create a calculator app")
expect(promptCalls[0].text).toContain("<promise>CALCULATOR_DONE</promise>")
})
test("should clear loop state on user abort (MessageAbortedError)", async () => {
// #given - active loop
// given - active loop
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Build something")
expect(hook.getState()).not.toBeNull()
// #when - user aborts (Ctrl+C)
// when - user aborts (Ctrl+C)
await hook.event({
event: {
type: "session.error",
@@ -557,16 +557,16 @@ describe("ralph-loop", () => {
},
})
// #then - loop state should be cleared immediately
// then - loop state should be cleared immediately
expect(hook.getState()).toBeNull()
})
test("should NOT set recovery mode on user abort", async () => {
// #given - active loop
// given - active loop
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Build something")
// #when - user aborts (Ctrl+C)
// when - user aborts (Ctrl+C)
await hook.event({
event: {
type: "session.error",
@@ -580,17 +580,17 @@ describe("ralph-loop", () => {
// Start a new loop
hook.startLoop("session-123", "New task")
// #when - session goes idle immediately (should work, no recovery mode)
// when - session goes idle immediately (should work, no recovery mode)
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
// #then - continuation should be injected (not blocked by recovery)
// then - continuation should be injected (not blocked by recovery)
expect(promptCalls.length).toBe(1)
})
test("should only check LAST assistant message for completion", async () => {
// #given - multiple assistant messages, only first has completion promise
// given - multiple assistant messages, only first has completion promise
mockSessionMessages = [
{ info: { role: "user" }, parts: [{ type: "text", text: "Start task" }] },
{ info: { role: "assistant" }, parts: [{ type: "text", text: "I'll work on it. <promise>DONE</promise>" }] },
@@ -602,18 +602,18 @@ describe("ralph-loop", () => {
})
hook.startLoop("session-123", "Build something", { completionPromise: "DONE" })
// #when - session goes idle
// when - session goes idle
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
// #then - loop should continue (last message has no completion promise)
// then - loop should continue (last message has no completion promise)
expect(promptCalls.length).toBe(1)
expect(hook.getState()?.iteration).toBe(2)
})
test("should detect completion only in LAST assistant message", async () => {
// #given - last assistant message has completion promise
// given - last assistant message has completion promise
mockSessionMessages = [
{ info: { role: "user" }, parts: [{ type: "text", text: "Start task" }] },
{ info: { role: "assistant" }, parts: [{ type: "text", text: "Starting work..." }] },
@@ -625,50 +625,50 @@ describe("ralph-loop", () => {
})
hook.startLoop("session-123", "Build something", { completionPromise: "DONE" })
// #when - session goes idle
// when - session goes idle
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
// #then - loop should complete (last message has completion promise)
// then - loop should complete (last message has completion promise)
expect(promptCalls.length).toBe(0)
expect(toastCalls.some((t) => t.title === "Ralph Loop Complete!")).toBe(true)
expect(hook.getState()).toBeNull()
})
test("should allow starting new loop while previous loop is active (different session)", async () => {
// #given - active loop in session A
// given - active loop in session A
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-A", "First task", { maxIterations: 10 })
expect(hook.getState()?.session_id).toBe("session-A")
expect(hook.getState()?.prompt).toBe("First task")
// #when - start new loop in session B (without completing A)
// when - start new loop in session B (without completing A)
hook.startLoop("session-B", "Second task", { maxIterations: 20 })
// #then - state should be overwritten with session B's loop
// then - state should be overwritten with session B's loop
expect(hook.getState()?.session_id).toBe("session-B")
expect(hook.getState()?.prompt).toBe("Second task")
expect(hook.getState()?.max_iterations).toBe(20)
expect(hook.getState()?.iteration).toBe(1)
// #when - session B goes idle
// when - session B goes idle
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-B" } },
})
// #then - continuation should be injected for session B
// then - continuation should be injected for session B
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].sessionID).toBe("session-B")
expect(promptCalls[0].text).toContain("Second task")
expect(promptCalls[0].text).toContain("2/20")
// #then - iteration incremented
// then - iteration incremented
expect(hook.getState()?.iteration).toBe(2)
})
test("should allow starting new loop in same session (restart)", async () => {
// #given - active loop in session A at iteration 5
// given - active loop in session A at iteration 5
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-A", "First task", { maxIterations: 10 })
@@ -682,29 +682,29 @@ describe("ralph-loop", () => {
expect(hook.getState()?.iteration).toBe(3)
expect(promptCalls.length).toBe(2)
// #when - start NEW loop in same session (restart)
// when - start NEW loop in same session (restart)
hook.startLoop("session-A", "Restarted task", { maxIterations: 50 })
// #then - state should be reset to iteration 1 with new prompt
// then - state should be reset to iteration 1 with new prompt
expect(hook.getState()?.session_id).toBe("session-A")
expect(hook.getState()?.prompt).toBe("Restarted task")
expect(hook.getState()?.max_iterations).toBe(50)
expect(hook.getState()?.iteration).toBe(1)
// #when - session goes idle
// when - session goes idle
promptCalls = [] // Reset to check new continuation
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-A" } },
})
// #then - continuation should use new task
// then - continuation should use new task
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].text).toContain("Restarted task")
expect(promptCalls[0].text).toContain("2/50")
})
test("should NOT detect completion from user message in transcript (issue #622)", async () => {
// #given - transcript contains user message with template text that includes completion promise
// given - transcript contains user message with template text that includes completion promise
// This reproduces the bug where the RALPH_LOOP_TEMPLATE instructional text
// containing `<promise>DONE</promise>` is recorded as a user message and
// falsely triggers completion detection
@@ -723,7 +723,7 @@ Output <promise>DONE</promise> when fully complete`
})
hook.startLoop("session-123", "Build something", { completionPromise: "DONE" })
// #when - session goes idle
// when - session goes idle
await hook.event({
event: {
type: "session.idle",
@@ -731,13 +731,13 @@ Output <promise>DONE</promise> when fully complete`
},
})
// #then - loop should CONTINUE (user message completion promise is instructional, not actual)
// then - loop should CONTINUE (user message completion promise is instructional, not actual)
expect(promptCalls.length).toBe(1)
expect(hook.getState()?.iteration).toBe(2)
})
test("should NOT detect completion from continuation prompt in transcript (issue #622)", async () => {
// #given - transcript contains continuation prompt (also a user message) with completion promise
// given - transcript contains continuation prompt (also a user message) with completion promise
const transcriptPath = join(TEST_DIR, "transcript.jsonl")
const continuationText = `RALPH LOOP 2/100
When FULLY complete, output: <promise>DONE</promise>
@@ -754,7 +754,7 @@ Original task: Build something`
})
hook.startLoop("session-123", "Build something", { completionPromise: "DONE" })
// #when - session goes idle
// when - session goes idle
await hook.event({
event: {
type: "session.idle",
@@ -762,13 +762,13 @@ Original task: Build something`
},
})
// #then - loop should CONTINUE (continuation prompt text is not actual completion)
// then - loop should CONTINUE (continuation prompt text is not actual completion)
expect(promptCalls.length).toBe(1)
expect(hook.getState()?.iteration).toBe(2)
})
test("should detect completion from tool_result entry in transcript", async () => {
// #given - transcript contains a tool_result with completion promise
// given - transcript contains a tool_result with completion promise
const transcriptPath = join(TEST_DIR, "transcript.jsonl")
const toolResultEntry = JSON.stringify({
type: "tool_result",
@@ -784,7 +784,7 @@ Original task: Build something`
})
hook.startLoop("session-123", "Build something", { completionPromise: "DONE" })
// #when - session goes idle
// when - session goes idle
await hook.event({
event: {
type: "session.idle",
@@ -792,14 +792,14 @@ Original task: Build something`
},
})
// #then - loop should complete (tool_result contains actual completion output)
// then - loop should complete (tool_result contains actual completion output)
expect(promptCalls.length).toBe(0)
expect(toastCalls.some((t) => t.title === "Ralph Loop Complete!")).toBe(true)
expect(hook.getState()).toBeNull()
})
test("should check transcript BEFORE API to optimize performance", async () => {
// #given - transcript has completion promise
// given - transcript has completion promise
const transcriptPath = join(TEST_DIR, "transcript.jsonl")
writeFileSync(transcriptPath, JSON.stringify({ type: "tool_result", tool_name: "write", tool_output: { output: "<promise>DONE</promise>" } }) + "\n")
mockSessionMessages = [
@@ -810,7 +810,7 @@ Original task: Build something`
})
hook.startLoop("session-123", "Build something", { completionPromise: "DONE" })
// #when - session goes idle
// when - session goes idle
await hook.event({
event: {
type: "session.idle",
@@ -818,7 +818,7 @@ Original task: Build something`
},
})
// #then - should complete via transcript (API not called when transcript succeeds)
// then - should complete via transcript (API not called when transcript succeeds)
expect(promptCalls.length).toBe(0)
expect(hook.getState()).toBeNull()
// API should NOT be called since transcript found completion
@@ -826,7 +826,7 @@ Original task: Build something`
})
test("should show ultrawork completion toast", async () => {
// #given - hook with ultrawork mode and completion in transcript
// given - hook with ultrawork mode and completion in transcript
const transcriptPath = join(TEST_DIR, "transcript.jsonl")
const hook = createRalphLoopHook(createMockPluginInput(), {
getTranscriptPath: () => transcriptPath,
@@ -834,17 +834,17 @@ Original task: Build something`
writeFileSync(transcriptPath, JSON.stringify({ type: "tool_result", tool_name: "write", tool_output: { output: "<promise>DONE</promise>" } }) + "\n")
hook.startLoop("test-id", "Build API", { ultrawork: true })
// #when - idle event triggered
// when - idle event triggered
await hook.event({ event: { type: "session.idle", properties: { sessionID: "test-id" } } })
// #then - ultrawork toast shown
// then - ultrawork toast shown
const completionToast = toastCalls.find(t => t.title === "ULTRAWORK LOOP COMPLETE!")
expect(completionToast).toBeDefined()
expect(completionToast!.message).toMatch(/JUST ULW ULW!/)
})
test("should show regular completion toast when ultrawork disabled", async () => {
// #given - hook without ultrawork
// given - hook without ultrawork
const transcriptPath = join(TEST_DIR, "transcript.jsonl")
const hook = createRalphLoopHook(createMockPluginInput(), {
getTranscriptPath: () => transcriptPath,
@@ -852,39 +852,39 @@ Original task: Build something`
writeFileSync(transcriptPath, JSON.stringify({ type: "tool_result", tool_name: "write", tool_output: { output: "<promise>DONE</promise>" } }) + "\n")
hook.startLoop("test-id", "Build API")
// #when - idle event triggered
// when - idle event triggered
await hook.event({ event: { type: "session.idle", properties: { sessionID: "test-id" } } })
// #then - regular toast shown
// then - regular toast shown
expect(toastCalls.some(t => t.title === "Ralph Loop Complete!")).toBe(true)
})
test("should prepend ultrawork to continuation prompt when ultrawork=true", async () => {
// #given - hook with ultrawork mode enabled
// given - hook with ultrawork mode enabled
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Build API", { ultrawork: true })
// #when - session goes idle (continuation triggered)
// when - session goes idle (continuation triggered)
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
// #then - prompt should start with "ultrawork "
// then - prompt should start with "ultrawork "
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].text).toMatch(/^ultrawork /)
})
test("should NOT prepend ultrawork to continuation prompt when ultrawork=false", async () => {
// #given - hook without ultrawork mode
// given - hook without ultrawork mode
const hook = createRalphLoopHook(createMockPluginInput())
hook.startLoop("session-123", "Build API")
// #when - session goes idle (continuation triggered)
// when - session goes idle (continuation triggered)
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
// #then - prompt should NOT start with "ultrawork "
// then - prompt should NOT start with "ultrawork "
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].text).not.toMatch(/^ultrawork /)
})
@@ -892,7 +892,7 @@ Original task: Build something`
describe("API timeout protection", () => {
test("should not hang when session.messages() throws", async () => {
// #given - API that throws (simulates timeout error)
// given - API that throws (simulates timeout error)
let apiCallCount = 0
const errorMock = {
...createMockPluginInput(),
@@ -913,16 +913,16 @@ Original task: Build something`
})
hook.startLoop("session-123", "Build something")
// #when - session goes idle (API will throw)
// when - session goes idle (API will throw)
const startTime = Date.now()
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
const elapsed = Date.now() - startTime
// #then - should complete quickly (not hang for 10s)
// then - should complete quickly (not hang for 10s)
expect(elapsed).toBeLessThan(2000)
// #then - loop should continue (API error = no completion detected)
// then - loop should continue (API error = no completion detected)
expect(promptCalls.length).toBe(1)
expect(apiCallCount).toBeGreaterThan(0)
})