fix(stop-continuation): clear chat.message fallback stop state before work resumes

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-11 21:24:03 +09:00
parent e4bb60ba07
commit 9287abe157
2 changed files with 182 additions and 1 deletions
+164
View File
@@ -17,6 +17,27 @@ import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/s
type ChatMessagePart = { type: string; text?: string; [key: string]: unknown }
type ChatMessageHandlerOutput = { message: Record<string, unknown>; parts: ChatMessagePart[] }
function createStopContinuationGuardMock(isStopped: boolean) {
const clearCalls: string[] = []
const isStoppedCalls: string[] = []
return {
guard: {
"chat.message": async () => {},
stop: () => {},
isStopped: (sessionID: string) => {
isStoppedCalls.push(sessionID)
return isStopped
},
clear: (sessionID: string) => {
clearCalls.push(sessionID)
},
},
clearCalls,
isStoppedCalls,
}
}
function createMockHandlerArgs(overrides?: {
pluginConfig?: Record<string, unknown>
shouldOverride?: boolean
@@ -214,6 +235,149 @@ describe("createChatMessageHandler - /start-work integration", () => {
})
})
describe("createChatMessageHandler - stop continuation clearing for raw slash fallback", () => {
test("clears stop state before raw /start-work resumes work through chat.message", async () => {
// given
const stopContinuationGuard = createStopContinuationGuardMock(true)
const startWorkCalls: string[] = []
const args = createMockHandlerArgs()
args.hooks.stopContinuationGuard = stopContinuationGuard.guard
args.hooks.startWork = {
"chat.message": async (input: { sessionID: string }) => {
startWorkCalls.push(input.sessionID)
},
}
const handler = createChatMessageHandler(args)
const output: ChatMessageHandlerOutput = {
message: {},
parts: [{ type: "text", text: "/start-work" }],
}
// when
await handler(createMockInput("sisyphus"), output)
// then
expect(startWorkCalls).toEqual(["test-session"])
expect(stopContinuationGuard.isStoppedCalls).toEqual(["test-session"])
expect(stopContinuationGuard.clearCalls).toEqual(["test-session"])
})
test("clears stop state before raw /ulw-loop resumes work through chat.message", async () => {
// given
const stopContinuationGuard = createStopContinuationGuardMock(true)
const startLoopCalls: Array<{ sessionID: string; prompt: string; ultrawork: boolean }> = []
const args = createMockHandlerArgs()
args.hooks.stopContinuationGuard = stopContinuationGuard.guard
args.hooks.ralphLoop = {
startLoop: (sessionID: string, prompt: string, options?: { ultrawork?: boolean }) => {
startLoopCalls.push({ sessionID, prompt, ultrawork: options?.ultrawork === true })
return true
},
cancelLoop: () => true,
}
const handler = createChatMessageHandler(args)
const output: ChatMessageHandlerOutput = {
message: {},
parts: [{ type: "text", text: "/ulw-loop ship it" }],
}
// when
await handler(createMockInput("sisyphus"), output)
// then
expect(startLoopCalls).toEqual([
{ sessionID: "test-session", prompt: "ship it", ultrawork: true },
])
expect(stopContinuationGuard.isStoppedCalls).toEqual(["test-session"])
expect(stopContinuationGuard.clearCalls).toEqual(["test-session"])
})
test("clears stop state before raw /ralph-loop resumes work through chat.message", async () => {
// given
const stopContinuationGuard = createStopContinuationGuardMock(true)
const startLoopCalls: Array<{ sessionID: string; prompt: string; ultrawork: boolean }> = []
const args = createMockHandlerArgs()
args.hooks.stopContinuationGuard = stopContinuationGuard.guard
args.hooks.ralphLoop = {
startLoop: (sessionID: string, prompt: string, options?: { ultrawork?: boolean }) => {
startLoopCalls.push({ sessionID, prompt, ultrawork: options?.ultrawork === true })
return true
},
cancelLoop: () => true,
}
const handler = createChatMessageHandler(args)
const output: ChatMessageHandlerOutput = {
message: {},
parts: [{ type: "text", text: "/ralph-loop keep going" }],
}
// when
await handler(createMockInput("sisyphus"), output)
// then
expect(startLoopCalls).toEqual([
{ sessionID: "test-session", prompt: "keep going", ultrawork: false },
])
expect(stopContinuationGuard.isStoppedCalls).toEqual(["test-session"])
expect(stopContinuationGuard.clearCalls).toEqual(["test-session"])
})
test("does not clear stop state when the session was not stopped", async () => {
// given
const stopContinuationGuard = createStopContinuationGuardMock(false)
const startWorkCalls: string[] = []
const startLoopCalls: Array<{ sessionID: string; prompt: string; ultrawork: boolean }> = []
const args = createMockHandlerArgs()
args.hooks.stopContinuationGuard = stopContinuationGuard.guard
args.hooks.startWork = {
"chat.message": async (input: { sessionID: string }) => {
startWorkCalls.push(input.sessionID)
},
}
args.hooks.ralphLoop = {
startLoop: (sessionID: string, prompt: string, options?: { ultrawork?: boolean }) => {
startLoopCalls.push({ sessionID, prompt, ultrawork: options?.ultrawork === true })
return true
},
cancelLoop: () => true,
}
const handler = createChatMessageHandler(args)
// when
await handler(createMockInput("sisyphus"), {
message: {},
parts: [{ type: "text", text: "/start-work" }],
})
await handler(createMockInput("sisyphus"), {
message: {},
parts: [{ type: "text", text: "/ulw-loop continue" }],
})
await handler(createMockInput("sisyphus"), {
message: {},
parts: [{ type: "text", text: "/ralph-loop continue" }],
})
// then
expect(startWorkCalls).toEqual([
"test-session",
"test-session",
"test-session",
])
expect(startLoopCalls).toEqual([
{ sessionID: "test-session", prompt: "continue", ultrawork: true },
{ sessionID: "test-session", prompt: "continue", ultrawork: false },
])
expect(stopContinuationGuard.isStoppedCalls).toEqual([
"test-session",
"test-session",
"test-session",
"test-session",
"test-session",
])
expect(stopContinuationGuard.clearCalls).toHaveLength(0)
})
})
describe("createChatMessageHandler - /ulw-loop raw slash fallback", () => {
test("starts ultrawork loop when /ulw-loop arrives through chat.message without native command expansion", async () => {
// given