From a2f075cc4d2fe6ae12c843e93bb007054f8b3003 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 10 Dec 2025 07:23:18 +0900 Subject: [PATCH] Add: E2E tests for controlled error handling paths --- e2e/experts/error-handling.toml | 41 ++++++++++++++++++++++++++++++++ e2e/graceful-recovery.test.ts | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 e2e/experts/error-handling.toml create mode 100644 e2e/graceful-recovery.test.ts diff --git a/e2e/experts/error-handling.toml b/e2e/experts/error-handling.toml new file mode 100644 index 00000000..d6cc3de2 --- /dev/null +++ b/e2e/experts/error-handling.toml @@ -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"] diff --git a/e2e/graceful-recovery.test.ts b/e2e/graceful-recovery.test.ts new file mode 100644 index 00000000..282a6f30 --- /dev/null +++ b/e2e/graceful-recovery.test.ts @@ -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) + }) +})