refactor(todo-continuation-enforcer): update message mock structure and remove unreliable abort error handling tests
- Add MockMessage interface to match new message structure - Update client mock to include messages() method - Remove abort error detection tests that were unreliable - Simplify error handling logic for better testability 🤖 Generated with assistance of OhMyOpenCode (https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -8,6 +8,16 @@ describe("todo-continuation-enforcer", () => {
|
||||
let promptCalls: Array<{ sessionID: string; agent?: string; model?: { providerID?: string; modelID?: string }; text: string }>
|
||||
let toastCalls: Array<{ title: string; message: string }>
|
||||
|
||||
interface MockMessage {
|
||||
info: {
|
||||
id: string
|
||||
role: "user" | "assistant"
|
||||
error?: { name: string; data?: { message: string } }
|
||||
}
|
||||
}
|
||||
|
||||
let mockMessages: MockMessage[] = []
|
||||
|
||||
function createMockPluginInput() {
|
||||
return {
|
||||
client: {
|
||||
@@ -16,6 +26,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ id: "1", content: "Task 1", status: "pending", priority: "high" },
|
||||
{ id: "2", content: "Task 2", status: "completed", priority: "medium" },
|
||||
]}),
|
||||
messages: async () => ({ data: mockMessages }),
|
||||
prompt: async (opts: any) => {
|
||||
promptCalls.push({
|
||||
sessionID: opts.path.id,
|
||||
@@ -51,6 +62,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
beforeEach(() => {
|
||||
promptCalls = []
|
||||
toastCalls = []
|
||||
mockMessages = []
|
||||
setMainSession(undefined)
|
||||
subagentSessions.clear()
|
||||
})
|
||||
@@ -165,56 +177,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
expect(promptCalls[0].sessionID).toBe(bgTaskSession)
|
||||
})
|
||||
|
||||
test("should skip injection when abort error occurs immediately before idle", async () => {
|
||||
// #given - session that just had an abort error
|
||||
const sessionID = "main-error"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
|
||||
// #when - abort error occurs
|
||||
await hook.handler({
|
||||
event: { type: "session.error", properties: { sessionID, error: { name: "AbortError", message: "aborted" } } },
|
||||
})
|
||||
|
||||
// #when - session goes idle immediately after abort
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
|
||||
// #then - no continuation injected (abort was immediately before idle)
|
||||
expect(promptCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should clear abort state on user message and allow injection", async () => {
|
||||
// #given - session with abort error, then user clears it
|
||||
const sessionID = "main-error-clear"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
|
||||
// #when - abort error occurs
|
||||
await hook.handler({
|
||||
event: { type: "session.error", properties: { sessionID, error: { message: "aborted" } } },
|
||||
})
|
||||
|
||||
// #when - user sends message (clears abort state)
|
||||
await hook.handler({
|
||||
event: { type: "message.updated", properties: { info: { sessionID, role: "user" } } },
|
||||
})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 2500))
|
||||
|
||||
// #then - continuation injected (abort state was cleared by user message)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
})
|
||||
|
||||
test("should cancel countdown on user message after grace period", async () => {
|
||||
// #given - session starting countdown
|
||||
@@ -430,112 +393,11 @@ describe("todo-continuation-enforcer", () => {
|
||||
expect(promptCalls.length).toBe(2)
|
||||
}, { timeout: 15000 })
|
||||
|
||||
// ============================================================
|
||||
// ABORT "IMMEDIATELY BEFORE" DETECTION TESTS
|
||||
// These tests verify that abort errors only block continuation
|
||||
// when they occur IMMEDIATELY before session.idle, not based
|
||||
// on a time-based cooldown.
|
||||
// ============================================================
|
||||
|
||||
test("should skip injection ONLY when abort error occurs immediately before idle", async () => {
|
||||
// #given - session with incomplete todos
|
||||
const sessionID = "main-abort-immediate"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
|
||||
// #when - abort error occurs (with abort-specific error)
|
||||
await hook.handler({
|
||||
event: {
|
||||
type: "session.error",
|
||||
properties: {
|
||||
sessionID,
|
||||
error: { name: "MessageAbortedError", message: "The operation was aborted" }
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// #when - session goes idle IMMEDIATELY after abort (no other events in between)
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
|
||||
// #then - no continuation injected (abort was immediately before idle)
|
||||
expect(promptCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should inject normally when abort error is followed by assistant activity before idle", async () => {
|
||||
// #given - session with incomplete todos
|
||||
const sessionID = "main-abort-then-assistant"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
|
||||
// #when - abort error occurs
|
||||
await hook.handler({
|
||||
event: {
|
||||
type: "session.error",
|
||||
properties: {
|
||||
sessionID,
|
||||
error: { name: "MessageAbortedError", message: "The operation was aborted" }
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// #when - assistant sends a message (intervening event clears abort state)
|
||||
await hook.handler({
|
||||
event: {
|
||||
type: "message.updated",
|
||||
properties: { info: { sessionID, role: "assistant" } }
|
||||
},
|
||||
})
|
||||
|
||||
// #when - session goes idle (abort is no longer "immediately before")
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 2500))
|
||||
|
||||
// #then - continuation injected (abort was NOT immediately before idle)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
})
|
||||
|
||||
test("should inject normally when abort error is followed by tool execution before idle", async () => {
|
||||
// #given - session with incomplete todos
|
||||
const sessionID = "main-abort-then-tool"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
|
||||
// #when - abort error occurs
|
||||
await hook.handler({
|
||||
event: {
|
||||
type: "session.error",
|
||||
properties: {
|
||||
sessionID,
|
||||
error: { message: "aborted" }
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// #when - tool execution happens (intervening event)
|
||||
await hook.handler({
|
||||
event: { type: "tool.execute.after", properties: { sessionID } },
|
||||
})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 2500))
|
||||
|
||||
// #then - continuation injected (abort was NOT immediately before idle)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
})
|
||||
|
||||
test("should NOT skip for non-abort errors even if immediately before idle", async () => {
|
||||
// #given - session with incomplete todos
|
||||
@@ -566,74 +428,105 @@ describe("todo-continuation-enforcer", () => {
|
||||
expect(promptCalls.length).toBe(1)
|
||||
})
|
||||
|
||||
test("should inject after abort if time passes and new idle event occurs", async () => {
|
||||
// #given - session with incomplete todos, abort happened previously
|
||||
const sessionID = "main-abort-time-passed"
|
||||
|
||||
|
||||
|
||||
|
||||
// ============================================================
|
||||
// API-BASED ABORT DETECTION TESTS
|
||||
// These tests verify that abort is detected by checking
|
||||
// the last assistant message's error field via session.messages API
|
||||
// ============================================================
|
||||
|
||||
test("should skip injection when last assistant message has MessageAbortedError", async () => {
|
||||
// #given - session where last assistant message was aborted
|
||||
const sessionID = "main-api-abort"
|
||||
setMainSession(sessionID)
|
||||
|
||||
mockMessages = [
|
||||
{ info: { id: "msg-1", role: "user" } },
|
||||
{ info: { id: "msg-2", role: "assistant", error: { name: "MessageAbortedError", data: { message: "The operation was aborted" } } } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
|
||||
// #when - abort error occurs
|
||||
await hook.handler({
|
||||
event: {
|
||||
type: "session.error",
|
||||
properties: {
|
||||
sessionID,
|
||||
error: { name: "AbortError", message: "cancelled" }
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// #when - first idle (immediately after abort) - should be skipped
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 3500))
|
||||
expect(promptCalls).toHaveLength(0)
|
||||
|
||||
// #when - second idle event occurs (abort is no longer "immediately before")
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 3500))
|
||||
|
||||
// #then - continuation injected on second idle (abort state was consumed)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
}, { timeout: 15000 })
|
||||
|
||||
test("should handle multiple abort errors correctly - only last one matters", async () => {
|
||||
// #given - session with incomplete todos
|
||||
const sessionID = "main-multi-abort"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
|
||||
// #when - first abort error
|
||||
await hook.handler({
|
||||
event: {
|
||||
type: "session.error",
|
||||
properties: { sessionID, error: { message: "aborted" } }
|
||||
},
|
||||
})
|
||||
|
||||
// #when - second abort error (immediately before idle)
|
||||
await hook.handler({
|
||||
event: {
|
||||
type: "session.error",
|
||||
properties: { sessionID, error: { message: "interrupted" } }
|
||||
},
|
||||
})
|
||||
|
||||
// #when - idle immediately after second abort
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
|
||||
// #then - no continuation (abort was immediately before)
|
||||
// #then - no continuation (last message was aborted)
|
||||
expect(promptCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should inject when last assistant message has no error", async () => {
|
||||
// #given - session where last assistant message completed normally
|
||||
const sessionID = "main-api-no-error"
|
||||
setMainSession(sessionID)
|
||||
|
||||
mockMessages = [
|
||||
{ info: { id: "msg-1", role: "user" } },
|
||||
{ info: { id: "msg-2", role: "assistant" } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
|
||||
// #then - continuation injected (no abort)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
})
|
||||
|
||||
test("should inject when last message is from user (not assistant)", async () => {
|
||||
// #given - session where last message is from user
|
||||
const sessionID = "main-api-user-last"
|
||||
setMainSession(sessionID)
|
||||
|
||||
mockMessages = [
|
||||
{ info: { id: "msg-1", role: "assistant" } },
|
||||
{ info: { id: "msg-2", role: "user" } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
|
||||
// #then - continuation injected (last message is user, not aborted assistant)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
})
|
||||
|
||||
test("should skip when last assistant message has any abort-like error", async () => {
|
||||
// #given - session where last assistant message has AbortError (DOMException style)
|
||||
const sessionID = "main-api-abort-dom"
|
||||
setMainSession(sessionID)
|
||||
|
||||
mockMessages = [
|
||||
{ info: { id: "msg-1", role: "user" } },
|
||||
{ info: { id: "msg-2", role: "assistant", error: { name: "AbortError" } } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
|
||||
// #then - no continuation (abort error detected)
|
||||
expect(promptCalls).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user