spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: tests/anthropic/errors.test.ts4 * Description: Error taxonomy tests — SDK error mapping, context-overflow detection, retry-after parsing, secret redaction.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import { APIConnectionError, APIError, APIUserAbortError } from "@anthropic-ai/sdk";12import {13 ModelError,14 classifyModelError,15 parseRetryAfterMs,16 redactSecrets,17} from "../../src/anthropic/errors.js";1819function apiError(status: number, message: string, headers?: Record<string, string>): APIError {20 return APIError.generate(21 status,22 { error: { type: "error", message } },23 message,24 new Headers(headers ?? {}),25 );26}2728describe("classifyModelError", () => {29 it("maps 429 to retryable and extracts retry-after (delta-seconds)", () => {30 const err = classifyModelError(apiError(429, "rate limited", { "retry-after": "7" }));31 expect(err.kind).toBe("retryable");32 expect(err.retryable).toBe(true);33 expect(err.status).toBe(429);34 expect(err.retryAfterMs).toBe(7_000);35 });3637 it("maps 500 and 529 (overloaded) to retryable", () => {38 expect(classifyModelError(apiError(500, "internal server error")).kind).toBe("retryable");39 expect(classifyModelError(apiError(529, "overloaded_error: Overloaded")).kind).toBe("retryable");40 });4142 it("maps network failures to retryable", () => {43 expect(44 classifyModelError(new APIConnectionError({ message: "Connection error." })).kind,45 ).toBe("retryable");46 expect(classifyModelError(new TypeError("fetch failed")).kind).toBe("retryable");47 });4849 it("maps 401/403 to fatal auth", () => {50 const err = classifyModelError(apiError(401, "invalid x-api-key"));51 expect(err.kind).toBe("auth");52 expect(err.retryable).toBe(false);53 expect(classifyModelError(apiError(403, "forbidden")).kind).toBe("auth");54 });5556 it("maps a generic 400 to fatal invalid-request", () => {57 const err = classifyModelError(apiError(400, "tools.0.input_schema: field required"));58 expect(err.kind).toBe("invalid-request");59 expect(err.retryable).toBe(false);60 });6162 it("detects the API's context-length error shape as context-overflow", () => {63 const err = classifyModelError(64 apiError(400, "prompt is too long: 210734 tokens > 200000 maximum"),65 );66 expect(err.kind).toBe("context-overflow");67 expect(err.retryable).toBe(false);68 expect(69 classifyModelError(70 apiError(400, "input length and `max_tokens` exceed context limit: 199999 + 8192 > 200000"),71 ).kind,72 ).toBe("context-overflow");73 expect(classifyModelError(apiError(413, "request too large")).kind).toBe("context-overflow");74 });7576 it("maps abort errors to cancelled", () => {77 expect(classifyModelError(new APIUserAbortError()).kind).toBe("cancelled");78 const abortError = new Error("The operation was aborted");79 abortError.name = "AbortError";80 expect(classifyModelError(abortError).kind).toBe("cancelled");81 });8283 it("passes through an existing ModelError unchanged", () => {84 const original = new ModelError("context-overflow", "too long");85 expect(classifyModelError(original)).toBe(original);86 });8788 it("maps unknown errors to fatal invalid-request (a KHAELOR bug, surfaced loudly)", () => {89 const err = classifyModelError(new Error("undefined is not a function"));90 expect(err.kind).toBe("invalid-request");91 expect(err.retryable).toBe(false);92 });93});9495describe("secret hygiene", () => {96 it("redacts API keys and auth header values from messages", () => {97 const err = new ModelError(98 "auth",99 "bad key sk-ant-api03-abc_DEF-123 with authorization: Bearer sk-ant-zzz and x-api-key: whatever123",100 );101 expect(err.message).not.toContain("sk-ant-api03");102 expect(err.message).not.toContain("whatever123");103 expect(err.message).toContain("[redacted]");104 });105106 it("redactSecrets leaves normal text intact", () => {107 expect(redactSecrets("prompt is too long: 210734 tokens")).toBe(108 "prompt is too long: 210734 tokens",109 );110 });111});112113describe("parseRetryAfterMs", () => {114 it("parses delta-seconds", () => {115 expect(parseRetryAfterMs("2")).toBe(2_000);116 expect(parseRetryAfterMs("0")).toBe(0);117 });118119 it("parses an HTTP-date in the future", () => {120 const value = new Date(Date.now() + 5_000).toUTCString();121 const ms = parseRetryAfterMs(value);122 expect(ms).toBeGreaterThan(0);123 expect(ms).toBeLessThanOrEqual(5_000);124 });125126 it("returns undefined for absent or garbage values", () => {127 expect(parseRetryAfterMs(null)).toBeUndefined();128 expect(parseRetryAfterMs(undefined)).toBeUndefined();129 expect(parseRetryAfterMs("")).toBeUndefined();130 expect(parseRetryAfterMs("soon")).toBeUndefined();131 });132});133