fix: address cubic review — broaden error detection, add test coverage

1. isAgentNotFoundError now handles:
   - Plain objects with .message field (not just Error instances)
   - "agent.name"/"undefined" error variants from SDK validation
   - The original "Agent not found" format

2. New tests:
   - agent.name/undefined error variant triggers fallback
   - Plain object errors with .message field trigger fallback
   - "fallback also fails" test now verifies retry was attempted (callCount=2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jim Park
2026-04-04 22:17:22 -07:00
parent 9470cbe090
commit 51508c4949
2 changed files with 142 additions and 2 deletions
@@ -144,11 +144,13 @@ describe("background-agent spawner agent-not-found fallback", () => {
test("calls onTaskError if fallback agent also fails", async () => {
//#given
let callCount = 0
const client = {
session: {
get: async () => ({ data: { directory: "/tmp/test" } }),
create: async () => ({ data: { id: "session-fallback" } }),
promptAsync: async () => {
callCount++
throw new Error('Agent not found: "Sisyphus-Junior". Available agents: build, explore, general, plan')
},
},
@@ -188,8 +190,135 @@ describe("background-agent spawner agent-not-found fallback", () => {
await new Promise(resolve => setTimeout(resolve, 50))
//#then
// Verify retry was attempted (2 calls: original + fallback)
expect(callCount).toBe(2)
expect(onTaskError).toHaveBeenCalled()
})
test("retries on agent.name/undefined error variant", async () => {
//#given
const promptCalls: any[] = []
let callCount = 0
const client = {
session: {
get: async () => ({ data: { directory: "/tmp/test" } }),
create: async () => ({ data: { id: "session-fallback" } }),
promptAsync: async (args: any) => {
callCount++
promptCalls.push({ body: { ...args.body } })
if (callCount === 1) {
throw new Error("Cannot read properties of undefined (reading 'agent.name')")
}
return { data: {} }
},
},
} as any
const onTaskError = mock(() => {})
const task = createTask({
description: "Test task",
prompt: "Do work",
agent: "Sisyphus-Junior",
parentSessionID: "ses_parent",
parentMessageID: "msg_parent",
})
const item = {
task,
input: {
description: task.description,
prompt: task.prompt,
agent: task.agent,
parentSessionID: task.parentSessionID,
parentMessageID: task.parentMessageID,
parentModel: task.parentModel,
parentAgent: task.parentAgent,
model: task.model,
},
}
const ctx = {
client,
directory: "/tmp/test",
concurrencyManager: { release: () => {} },
tmuxEnabled: false,
onTaskError,
}
//#when
await startTask(item as any, ctx as any)
await new Promise(resolve => setTimeout(resolve, 50))
//#then
expect(promptCalls).toHaveLength(2)
expect(promptCalls[0].body.agent).toBe("Sisyphus-Junior")
expect(promptCalls[1].body.agent).toBe("general")
expect(onTaskError).not.toHaveBeenCalled()
})
test("detects agent error from plain object with message field", async () => {
//#given
const promptCalls: any[] = []
let callCount = 0
const client = {
session: {
get: async () => ({ data: { directory: "/tmp/test" } }),
create: async () => ({ data: { id: "session-fallback" } }),
promptAsync: async (args: any) => {
callCount++
promptCalls.push({ body: { ...args.body } })
if (callCount === 1) {
throw { message: 'Agent not found: "Custom-Agent"', name: "UnknownError" }
}
return { data: {} }
},
},
} as any
const onTaskError = mock(() => {})
const task = createTask({
description: "Test task",
prompt: "Do work",
agent: "Custom-Agent",
parentSessionID: "ses_parent",
parentMessageID: "msg_parent",
})
const item = {
task,
input: {
description: task.description,
prompt: task.prompt,
agent: task.agent,
parentSessionID: task.parentSessionID,
parentMessageID: task.parentMessageID,
parentModel: task.parentModel,
parentAgent: task.parentAgent,
model: task.model,
},
}
const ctx = {
client,
directory: "/tmp/test",
concurrencyManager: { release: () => {} },
tmuxEnabled: false,
onTaskError,
}
//#when
await startTask(item as any, ctx as any)
await new Promise(resolve => setTimeout(resolve, 50))
//#then
expect(promptCalls).toHaveLength(2)
expect(promptCalls[1].body.agent).toBe("general")
expect(onTaskError).not.toHaveBeenCalled()
})
})
describe("background-agent spawner fallback model promotion", () => {
+13 -2
View File
@@ -11,8 +11,19 @@ import type { ConcurrencyManager } from "./concurrency"
export const FALLBACK_AGENT = "general"
export function isAgentNotFoundError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error)
return message.includes("Agent not found")
const message =
typeof error === "string"
? error
: error instanceof Error
? error.message
: typeof error === "object" && error !== null && typeof (error as { message?: unknown }).message === "string"
? (error as { message: string }).message
: String(error)
return (
message.includes("Agent not found") ||
message.includes("agent.name") ||
(message.includes("agent") && message.includes("undefined"))
)
}
export function buildFallbackBody(