Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions e2e/experts/error-handling.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
model = "claude-sonnet-4-5"
temperature = 0.3

[provider]
providerName = "anthropic"

envPath = [".env", ".env.local"]

[experts."e2e-tool-error-recovery"]
version = "1.0.0"
description = "E2E test expert for tool error recovery"
instruction = """
You are an E2E test expert that tests graceful error recovery from tool failures.

When asked to read a file:
1. First, attempt to read the specified file using readTextFile (use the exact filename given)
2. If the tool returns an error (file not found), acknowledge the error
3. Then call attemptCompletion to report what happened

IMPORTANT: Always use readTextFile first with the exact path given by the user.
"""

[experts."e2e-tool-error-recovery".skills."@perstack/base"]
type = "mcpStdioSkill"
command = "npx"
packageName = "@perstack/base"
pick = ["attemptCompletion", "readTextFile"]

[experts."e2e-invalid-delegate"]
version = "1.0.0"
description = "E2E test expert with nonexistent delegate"
instruction = """
You are a test expert. Delegate to nonexistent-expert to complete the task.
"""
delegates = ["nonexistent-expert"]

[experts."e2e-invalid-delegate".skills."@perstack/base"]
type = "mcpStdioSkill"
command = "npx"
packageName = "@perstack/base"
pick = ["attemptCompletion"]
42 changes: 42 additions & 0 deletions e2e/graceful-recovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest"
import { assertEventSequenceContains } from "./lib/assertions.js"
import { filterEventsByType } from "./lib/event-parser.js"
import { runCli, runExpert } from "./lib/runner.js"

describe("Graceful Error Recovery", () => {
describe("Tool Error Recovery", () => {
it("should recover from file not found error and complete successfully", async () => {
const result = await runExpert(
"e2e-tool-error-recovery",
"Read the file at nonexistent_file_12345.txt and report what happened",
{
configPath: "./e2e/experts/error-handling.toml",
timeout: 180000,
},
)
expect(result.exitCode).toBe(0)
expect(
assertEventSequenceContains(result.events, ["startRun", "callTools", "resolveToolResults"])
.passed,
).toBe(true)
const resolveEvents = filterEventsByType(result.events, "resolveToolResults")
const hasFileNotFoundError = resolveEvents.some((e) => {
const toolResults = (e as { toolResults?: { result?: { text?: string }[] }[] }).toolResults
return toolResults?.some((tr) => tr.result?.some((r) => r.text?.includes("does not exist")))
})
expect(hasFileNotFoundError).toBe(true)
expect(assertEventSequenceContains(result.events, ["completeRun"]).passed).toBe(true)
}, 200000)
})

describe("Invalid Configuration", () => {
it("should fail with clear message for nonexistent delegate", async () => {
const result = await runCli(
["run", "--config", "./e2e/experts/error-handling.toml", "e2e-invalid-delegate", "test"],
{ timeout: 60000 },
)
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toMatch(/not found|nonexistent/i)
}, 120000)
})
})