Merge pull request #3065 from code-yeongyu/fix/issue-2984-v2

fix: reset consecutiveFailures on abort so session recovers after user cancel
This commit is contained in:
YeonGyu-Kim
2026-04-04 20:34:10 +09:00
committed by GitHub
11 changed files with 344 additions and 18 deletions
@@ -1270,7 +1270,7 @@ describe("todo-continuation-enforcer", () => {
expect(promptCalls).toHaveLength(0)
})
test("should inject when abort flag is stale (>3s old)", async () => {
test("should keep skipping after cancel even when the abort window is stale", async () => {
fakeTimers.restore()
// given - session with incomplete todos and old abort timestamp
const sessionID = "main-stale-abort"
@@ -1299,8 +1299,7 @@ describe("todo-continuation-enforcer", () => {
await wait(3000)
// then - continuation injected (abort flag is stale)
expect(promptCalls.length).toBeGreaterThan(0)
expect(promptCalls).toHaveLength(0)
}, { timeout: 15000 })
test("should clear abort flag on user message activity", async () => {
@@ -1343,6 +1342,44 @@ describe("todo-continuation-enforcer", () => {
expect(promptCalls.length).toBeGreaterThan(0)
}, { timeout: 15000 })
test("should reset failure state and keep skipping after a cancelled run", async () => {
fakeTimers.restore()
const sessionID = "main-reset-after-cancel"
setMainSession(sessionID)
mockMessages = [
{ info: { id: "msg-1", role: "user" } },
{ info: { id: "msg-2", role: "assistant" } },
]
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await wait(2500)
expect(promptCalls.length).toBeGreaterThan(0)
promptCalls.length = 0
await hook.handler({
event: {
type: "session.error",
properties: { sessionID, error: { name: "MessageAbortedError" } },
},
})
await wait(3100)
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await wait(2500)
expect(promptCalls).toHaveLength(0)
}, { timeout: 15000 })
test("should clear abort flag on assistant message activity", async () => {
fakeTimers.restore()
// given - session with abort detected
@@ -1866,4 +1903,64 @@ describe("todo-continuation-enforcer", () => {
expect(promptCalls).toHaveLength(0)
})
test("should reset consecutiveFailures after user-initiated abort and resume after fresh activity [regression #2984]", async () => {
fakeTimers.restore()
const sessionID = "main-abort-recovery"
setMainSession(sessionID)
const mockInput = createMockPluginInput()
mockInput.client.session.todo = async () => ({
data: [
{ id: "1", content: "Write tests", status: "pending", priority: "high" },
],
})
let shouldFail = true
let promptCallCount = 0
mockInput.client.session.promptAsync = async (_opts: PromptRequestOptions) => {
promptCallCount++
if (shouldFail) {
throw new Error("promptAsync failed (3ms) unknown error")
}
promptCalls.push({
sessionID: _opts.path.id,
agent: _opts.body.agent,
model: _opts.body.model,
text: _opts.body.parts[0].text,
})
}
const hook = createTodoContinuationEnforcer(mockInput, {})
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
await wait(2500)
expect(promptCallCount).toBe(1)
await hook.handler({
event: {
type: "session.error",
properties: { sessionID, error: { name: "MessageAbortedError" } },
},
})
shouldFail = false
await wait(9000)
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
await wait(2500)
expect(promptCallCount).toBe(1)
await hook.handler({
event: {
type: "message.updated",
properties: { info: { sessionID, role: "user" } },
},
})
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
await wait(2500)
expect(promptCallCount).toBe(2)
expect(promptCalls).toHaveLength(1)
}, { timeout: 20000 })
})