SPB Git

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%
4.7 KB · 133 lines typescript
Raw Blame History
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