From a39811cacafd0aab37236764b6be893de907f56a Mon Sep 17 00:00:00 2001 From: spiceoogway Date: Fri, 30 Jan 2026 02:38:26 -0500 Subject: [PATCH] test: add comprehensive tests for categorizeError() function - Tests timeout errors (timeout keyword, 'timed out', ETIMEDOUT) - Tests authentication errors (401, unauthorized, 403, forbidden) - Tests rate limit errors (rate limit keyword, HTTP 429) - Tests unknown/unrecognized errors - Tests API/model errors (400, 500, 503, quota, billing) - Tests network errors (ECONNREFUSED, ENOTFOUND, DNS, ENETUNREACH) - Tests file system errors (ENOENT, EACCES, EPERM, EISDIR) - Tests configuration errors (missing key/token) - Tests context/memory errors - Export categorizeError() function for testing - 37 passing tests covering all error categorization paths --- .../reply/agent-runner-execution.ts | 2 +- src/auto-reply/reply/categorize-error.test.ts | 318 ++++++++++++++++++ 2 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 src/auto-reply/reply/categorize-error.test.ts diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 36305bc68..266a483e8 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -55,7 +55,7 @@ export type AgentRunLoopResult = * Categorize errors to provide better error messages to users. * Returns error message, type, and optional hint for remediation. */ -function categorizeError(err: unknown): { +export function categorizeError(err: unknown): { message: string; type: "model" | "tool" | "network" | "config" | "timeout" | "unknown"; hint?: string; diff --git a/src/auto-reply/reply/categorize-error.test.ts b/src/auto-reply/reply/categorize-error.test.ts new file mode 100644 index 000000000..17c85c58a --- /dev/null +++ b/src/auto-reply/reply/categorize-error.test.ts @@ -0,0 +1,318 @@ +import { describe, expect, it } from "vitest"; + +import { categorizeError } from "./agent-runner-execution.js"; + +describe("categorizeError", () => { + describe("timeout errors", () => { + it("categorizes lowercase 'timeout' as timeout type", () => { + const error = new Error("Request timeout after 30s"); + const result = categorizeError(error); + + expect(result.type).toBe("timeout"); + expect(result.message).toBe("Request timeout after 30s"); + expect(result.hint).toBe("Operation took too long - try increasing timeout"); + }); + + it("categorizes 'timed out' as timeout type", () => { + const error = new Error("Connection timed out"); + const result = categorizeError(error); + + expect(result.type).toBe("timeout"); + expect(result.hint).toBe("Operation took too long - try increasing timeout"); + }); + + it("categorizes ETIMEDOUT as network type (network error code takes precedence)", () => { + const error = new Error("ETIMEDOUT: socket hang up"); + const result = categorizeError(error); + + // ETIMEDOUT is caught by network errors before timeout section + expect(result.type).toBe("network"); + expect(result.hint).toBe("Connection failed - check network connectivity"); + }); + + it("handles uppercase TIMEOUT", () => { + const error = new Error("TIMEOUT ERROR"); + const result = categorizeError(error); + + expect(result.type).toBe("timeout"); + }); + }); + + describe("authentication errors", () => { + it("categorizes 401 as config type", () => { + const error = new Error("HTTP 401: Unauthorized"); + const result = categorizeError(error); + + expect(result.type).toBe("config"); + expect(result.message).toBe("HTTP 401: Unauthorized"); + expect(result.hint).toBe("Check API credentials and permissions"); + }); + + it("categorizes 'unauthorized' as config type", () => { + const error = new Error("Request failed: unauthorized access"); + const result = categorizeError(error); + + expect(result.type).toBe("config"); + expect(result.hint).toBe("Check API credentials and permissions"); + }); + + it("categorizes 'authentication' errors as config type (case-sensitive)", () => { + const error = new Error("authentication failed for API key"); + const result = categorizeError(error); + + expect(result.type).toBe("config"); + expect(result.hint).toBe("Check API credentials and permissions"); + }); + + it("categorizes 403 forbidden as config type", () => { + const error = new Error("HTTP 403 forbidden"); + const result = categorizeError(error); + + expect(result.type).toBe("config"); + expect(result.hint).toBe("Access denied - check permissions"); + }); + + it("categorizes 'forbidden' keyword as config type", () => { + const error = new Error("Access forbidden to resource"); + const result = categorizeError(error); + + expect(result.type).toBe("config"); + }); + }); + + describe("rate limit errors", () => { + it("categorizes 'rate limit' as model type", () => { + const error = new Error("rate limit exceeded"); + const result = categorizeError(error); + + expect(result.type).toBe("model"); + expect(result.message).toBe("rate limit exceeded"); + expect(result.hint).toBe("Rate limit exceeded - retry in a few moments"); + }); + + it("categorizes HTTP 429 as model type", () => { + const error = new Error("HTTP 429: Too Many Requests"); + const result = categorizeError(error); + + expect(result.type).toBe("model"); + expect(result.hint).toBe("Rate limit exceeded - retry in a few moments"); + }); + + it("handles rate limit with mixed case", () => { + const error = new Error("rate limit exceeded"); + const result = categorizeError(error); + + expect(result.type).toBe("model"); + }); + }); + + describe("unknown errors", () => { + it("categorizes unrecognized error as unknown type", () => { + const error = new Error("Something weird happened"); + const result = categorizeError(error); + + expect(result.type).toBe("unknown"); + expect(result.message).toBe("Something weird happened"); + expect(result.hint).toBeUndefined(); + }); + + it("categorizes generic error message as unknown", () => { + const error = new Error("An unexpected error occurred"); + const result = categorizeError(error); + + expect(result.type).toBe("unknown"); + expect(result.hint).toBeUndefined(); + }); + + it("handles non-Error objects", () => { + const result = categorizeError("plain string error"); + + expect(result.type).toBe("unknown"); + expect(result.message).toBe("plain string error"); + }); + + it("handles null/undefined errors", () => { + const result = categorizeError(null); + + expect(result.type).toBe("unknown"); + expect(result.message).toBe("null"); + }); + }); + + describe("API/model errors", () => { + it("categorizes HTTP 400 as model type", () => { + const error = new Error("HTTP 400: Bad Request"); + const result = categorizeError(error); + + expect(result.type).toBe("model"); + expect(result.hint).toBe("Invalid request parameters"); + }); + + it("categorizes 'invalid request' as model type", () => { + const error = new Error("invalid request format"); + const result = categorizeError(error); + + expect(result.type).toBe("model"); + }); + + it("categorizes HTTP 500 as model type", () => { + const error = new Error("HTTP 500: Internal Server Error"); + const result = categorizeError(error); + + expect(result.type).toBe("model"); + expect(result.hint).toBe("API service error - try again later"); + }); + + it("categorizes HTTP 503 as model type", () => { + const error = new Error("HTTP 503: Service Unavailable"); + const result = categorizeError(error); + + expect(result.type).toBe("model"); + expect(result.hint).toBe("API service error - try again later"); + }); + + it("categorizes quota errors as config type", () => { + const error = new Error("quota exceeded for this account"); + const result = categorizeError(error); + + expect(result.type).toBe("config"); + expect(result.hint).toBe("Check billing and API quota limits"); + }); + + it("categorizes billing errors as config type", () => { + const error = new Error("billing issue detected"); + const result = categorizeError(error); + + expect(result.type).toBe("config"); + expect(result.hint).toBe("Check billing and API quota limits"); + }); + }); + + describe("network errors", () => { + it("categorizes ECONNREFUSED as network type", () => { + const error = new Error("ECONNREFUSED: Connection refused"); + const result = categorizeError(error); + + expect(result.type).toBe("network"); + expect(result.hint).toBe("Connection failed - check network connectivity"); + }); + + it("categorizes ENOTFOUND as network type", () => { + const error = new Error("ENOTFOUND: DNS lookup failed"); + const result = categorizeError(error); + + expect(result.type).toBe("network"); + expect(result.hint).toBe("DNS resolution failed - check hostname"); + }); + + it("categorizes DNS errors as network type", () => { + const error = new Error("DNS resolution error"); + const result = categorizeError(error); + + expect(result.type).toBe("network"); + }); + + it("categorizes EAI_AGAIN as network type", () => { + const error = new Error("EAI_AGAIN: temporary failure"); + const result = categorizeError(error); + + expect(result.type).toBe("network"); + expect(result.hint).toBe("DNS resolution failed - check hostname"); + }); + + it("categorizes ENETUNREACH as network type", () => { + const error = new Error("ENETUNREACH: Network is unreachable"); + const result = categorizeError(error); + + expect(result.type).toBe("network"); + expect(result.hint).toBe("Network unreachable - check connection"); + }); + + it("categorizes EHOSTUNREACH as network type", () => { + const error = new Error("EHOSTUNREACH: No route to host"); + const result = categorizeError(error); + + expect(result.type).toBe("network"); + expect(result.hint).toBe("Network unreachable - check connection"); + }); + }); + + describe("file system errors (tool type)", () => { + it("categorizes ENOENT as tool type", () => { + const error = new Error("ENOENT: no such file or directory"); + const result = categorizeError(error); + + expect(result.type).toBe("tool"); + expect(result.hint).toBe("File or directory not found"); + }); + + it("categorizes ENOTDIR as tool type", () => { + const error = new Error("ENOTDIR: not a directory"); + const result = categorizeError(error); + + expect(result.type).toBe("tool"); + expect(result.hint).toBe("File or directory not found"); + }); + + it("categorizes EACCES as tool type", () => { + const error = new Error("EACCES: permission denied"); + const result = categorizeError(error); + + expect(result.type).toBe("tool"); + expect(result.hint).toBe("Permission denied"); + }); + + it("categorizes EPERM as tool type", () => { + const error = new Error("EPERM: operation not permitted"); + const result = categorizeError(error); + + expect(result.type).toBe("tool"); + expect(result.hint).toBe("Permission denied"); + }); + + it("categorizes EISDIR as tool type", () => { + const error = new Error("EISDIR: illegal operation on a directory"); + const result = categorizeError(error); + + expect(result.type).toBe("tool"); + expect(result.hint).toBe("Expected file but found directory"); + }); + }); + + describe("configuration errors", () => { + it("categorizes missing API key as config type", () => { + const error = new Error("missing API key"); + const result = categorizeError(error); + + expect(result.type).toBe("config"); + expect(result.hint).toBe("Missing required configuration or credentials"); + }); + + it("categorizes missing token as config type", () => { + const error = new Error("missing authentication token"); + const result = categorizeError(error); + + expect(result.type).toBe("config"); + // "authentication" keyword triggers auth error hint first + expect(result.hint).toBe("Check API credentials and permissions"); + }); + + it("categorizes missing API token without authentication keyword", () => { + const error = new Error("missing API token for request"); + const result = categorizeError(error); + + expect(result.type).toBe("config"); + expect(result.hint).toBe("Missing required configuration or credentials"); + }); + }); + + describe("context/memory errors", () => { + it("categorizes context too large as model type", () => { + const error = new Error("context window too large"); + const result = categorizeError(error); + + expect(result.type).toBe("model"); + expect(result.hint).toBe("Conversation too long - try clearing history"); + }); + }); +});