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:
@@ -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", () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user