diff --git a/src/plugin/command-execute-before.test.ts b/src/plugin/command-execute-before.test.ts new file mode 100644 index 000000000..5bcf19888 --- /dev/null +++ b/src/plugin/command-execute-before.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, mock, test } from "bun:test" + +import { createCommandExecuteBeforeHandler } from "./command-execute-before" + +describe("createCommandExecuteBeforeHandler", () => { + test("#given stopped session and /ulw-loop #when command.execute.before runs #then clear is called", async () => { + // given + const clear = mock(() => {}) + const isStopped = mock(() => true) + const startLoop = mock(() => true) + const handler = createCommandExecuteBeforeHandler({ + hooks: { + ralphLoop: { + startLoop, + cancelLoop: mock(() => true), + }, + stopContinuationGuard: { + isStopped, + clear, + }, + }, + }) + + // when + await handler( + { + command: "ulw-loop", + sessionID: "ses-stopped", + arguments: "Ship feature", + }, + { + parts: [], + }, + ) + + // then + expect(startLoop).toHaveBeenCalledTimes(1) + expect(isStopped).toHaveBeenCalledWith("ses-stopped") + expect(clear).toHaveBeenCalledTimes(1) + expect(clear).toHaveBeenCalledWith("ses-stopped") + }) + + test("#given stopped session and /start-work #when command.execute.before runs #then clear is called", async () => { + // given + const clear = mock(() => {}) + const isStopped = mock(() => true) + const startWorkHook = mock(async () => {}) + const handler = createCommandExecuteBeforeHandler({ + hooks: { + startWork: { + "command.execute.before": startWorkHook, + }, + stopContinuationGuard: { + isStopped, + clear, + }, + }, + }) + + // when + await handler( + { + command: "start-work", + sessionID: "ses-stopped", + arguments: "", + }, + { + parts: [], + }, + ) + + // then + expect(startWorkHook).toHaveBeenCalledTimes(1) + expect(isStopped).toHaveBeenCalledWith("ses-stopped") + expect(clear).toHaveBeenCalledTimes(1) + expect(clear).toHaveBeenCalledWith("ses-stopped") + }) + + test("#given non-stopped session and /ulw-loop #when command.execute.before runs #then clear is not called", async () => { + // given + const clear = mock(() => {}) + const isStopped = mock(() => false) + const startLoop = mock(() => true) + const handler = createCommandExecuteBeforeHandler({ + hooks: { + ralphLoop: { + startLoop, + cancelLoop: mock(() => true), + }, + stopContinuationGuard: { + isStopped, + clear, + }, + }, + }) + + // when + await handler( + { + command: "ulw-loop", + sessionID: "ses-running", + arguments: "Ship feature", + }, + { + parts: [], + }, + ) + + // then + expect(startLoop).toHaveBeenCalledTimes(1) + expect(isStopped).toHaveBeenCalledWith("ses-running") + expect(clear).not.toHaveBeenCalled() + }) +}) diff --git a/src/plugin/command-execute-before.ts b/src/plugin/command-execute-before.ts index f480bbc8b..f1663b832 100644 --- a/src/plugin/command-execute-before.ts +++ b/src/plugin/command-execute-before.ts @@ -1,5 +1,6 @@ import type { CreatedHooks } from "../create-hooks" import { parseRalphLoopArguments } from "../hooks/ralph-loop/command-arguments" +import { log } from "../shared/logger" type CommandExecuteBeforeInput = { command: string @@ -45,6 +46,13 @@ export function createCommandExecuteBeforeHandler(args: { }) output.message ??= {} output.message[NATIVE_LOOP_TRIGGERED_FLAG] = true + if (hooks.stopContinuationGuard?.isStopped(sessionID)) { + hooks.stopContinuationGuard.clear(sessionID) + log("[stop-continuation] Stop state cleared by native command", { + sessionID, + command: normalizedCommand, + }) + } } else if (normalizedCommand === "cancel-ralph") { hooks.ralphLoop.cancelLoop(sessionID) output.message ??= {} @@ -58,6 +66,13 @@ export function createCommandExecuteBeforeHandler(args: { && hasPartsOutput(output) ) { await hooks.startWork["command.execute.before"]?.(input, output) + if (hooks.stopContinuationGuard?.isStopped(sessionID)) { + hooks.stopContinuationGuard.clear(sessionID) + log("[stop-continuation] Stop state cleared by native command", { + sessionID, + command: normalizedCommand, + }) + } } } }