316 lines
8.7 KiB
TypeScript
316 lines
8.7 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
|
|
import { classifyErrorType, isRetryableError } from "./error-classifier"
|
|
|
|
describe("runtime-fallback provider matrix quota tests", () => {
|
|
describe("OpenAI provider", () => {
|
|
test("classifies OpenAI insufficient_quota error as quota_exceeded", () => {
|
|
//#given
|
|
const error = {
|
|
name: "InsufficientQuotaError",
|
|
message: "You exceeded your current quota. Please check your plan and billing details.",
|
|
provider: "openai",
|
|
}
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
// quota exhaustion triggers fallback to next configured model
|
|
expect(retryable).toBe(true)
|
|
})
|
|
|
|
test("classifies OpenAI billing_hard_limit error as quota_exceeded", () => {
|
|
//#given
|
|
const error = {
|
|
name: "BillingError",
|
|
message: "Billing hard limit reached. You have exceeded your hard limit.",
|
|
provider: "openai",
|
|
}
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
})
|
|
|
|
test("classifies OpenAI rate limit as retryable", () => {
|
|
//#given
|
|
const error = {
|
|
name: "RateLimitError",
|
|
statusCode: 429,
|
|
message: "Rate limit reached for requests",
|
|
provider: "openai",
|
|
}
|
|
|
|
//#when
|
|
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
|
|
|
|
//#then
|
|
expect(retryable).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("Anthropic provider", () => {
|
|
test("classifies Anthropic quota exceeded as retryable to trigger fallback", () => {
|
|
//#given
|
|
const error = {
|
|
name: "QuotaExceededError",
|
|
message: "Your account has exceeded its quota. Please upgrade your plan.",
|
|
provider: "anthropic",
|
|
}
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
// quota exhaustion triggers fallback to next configured model
|
|
expect(retryable).toBe(true)
|
|
})
|
|
|
|
test("classifies Anthropic subscription quota as retryable to trigger fallback", () => {
|
|
//#given
|
|
const error = {
|
|
name: "AI_APICallError",
|
|
message: "Subscription quota exceeded. You can continue using free models.",
|
|
provider: "anthropic",
|
|
}
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
// quota exhaustion triggers fallback to next configured model
|
|
expect(retryable).toBe(true)
|
|
})
|
|
|
|
test("classifies Anthropic cooling down with retry signal as retryable (auto-retry pattern)", () => {
|
|
//#given
|
|
const error = {
|
|
name: "AI_APICallError",
|
|
message: "All credentials for model claude-opus-4-7 are cooling down [retrying in ~2 weeks]",
|
|
provider: "anthropic",
|
|
}
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
|
|
|
|
//#then
|
|
expect(errorType).toBeUndefined()
|
|
expect(retryable).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("Google/Gemini provider", () => {
|
|
test("classifies Google API key missing as missing_api_key", () => {
|
|
//#given
|
|
const error = {
|
|
name: "AI_LoadAPIKeyError",
|
|
message:
|
|
"Google Generative AI API key is missing. Pass it using the 'apiKey' parameter or the GOOGLE_GENERATIVE_AI_API_KEY environment variable.",
|
|
provider: "google",
|
|
}
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
|
|
|
|
//#then
|
|
expect(errorType).toBe("missing_api_key")
|
|
expect(retryable).toBe(true)
|
|
})
|
|
|
|
test("classifies Google quota exceeded as quota_exceeded", () => {
|
|
//#given
|
|
const error = {
|
|
name: "QuotaExceededError",
|
|
message: "Quota exceeded for quota metric 'Generate Content API requests'",
|
|
provider: "google",
|
|
}
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
// quota exhaustion triggers fallback to next configured model
|
|
expect(retryable).toBe(true)
|
|
})
|
|
|
|
test("classifies Google rate limit exceeded as retryable", () => {
|
|
//#given
|
|
const error = {
|
|
name: "ResourceExhausted",
|
|
statusCode: 429,
|
|
message: "Rate limit exceeded. Please try again later.",
|
|
provider: "google",
|
|
}
|
|
|
|
//#when
|
|
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
|
|
|
|
//#then
|
|
expect(retryable).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("Generic provider patterns", () => {
|
|
test("classifies exhausted capacity as quota_exceeded", () => {
|
|
//#given
|
|
const error = {
|
|
message: "Sorry, you've exhausted your capacity",
|
|
}
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
})
|
|
|
|
test("classifies out of credits as quota_exceeded", () => {
|
|
//#given
|
|
const error = {
|
|
message: "You are out of credits. Please purchase more.",
|
|
}
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
})
|
|
|
|
test("classifies payment required (402) as quota_exceeded", () => {
|
|
//#given
|
|
const error = {
|
|
statusCode: 402,
|
|
message: "Payment Required",
|
|
}
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
})
|
|
|
|
test("classifies out of credits as quota_exceeded", () => {
|
|
//#given
|
|
const error = {
|
|
message: "You are out of credits. Please purchase more.",
|
|
}
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
})
|
|
|
|
test("classifies exhausted capacity as quota_exceeded", () => {
|
|
//#given
|
|
const error = {
|
|
message: "Sorry, you've exhausted your capacity",
|
|
}
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
})
|
|
})
|
|
|
|
describe("Provider-specific error name patterns", () => {
|
|
test("classifies BillingError as quota_exceeded", () => {
|
|
//#given
|
|
const error = { name: "BillingError", message: "Billing issue" }
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
})
|
|
|
|
test("classifies InsufficientQuota as quota_exceeded", () => {
|
|
//#given
|
|
const error = { name: "InsufficientQuota", message: "Not enough quota" }
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
})
|
|
|
|
test("classifies QuotaExceeded as quota_exceeded", () => {
|
|
//#given
|
|
const error = { name: "QuotaExceeded", message: "Quota limit reached" }
|
|
|
|
//#when
|
|
const errorType = classifyErrorType(error)
|
|
|
|
//#then
|
|
expect(errorType).toBe("quota_exceeded")
|
|
})
|
|
})
|
|
|
|
describe("HTTP status code matrix", () => {
|
|
test("429 rate limit is retryable", () => {
|
|
//#given
|
|
const error = { statusCode: 429, message: "Too many requests" }
|
|
|
|
//#when
|
|
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
|
|
|
|
//#then
|
|
expect(retryable).toBe(true)
|
|
})
|
|
|
|
test("402 payment required triggers fallback via quota_exceeded", () => {
|
|
//#given
|
|
const error = { statusCode: 402, message: "Payment Required" }
|
|
|
|
//#when
|
|
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
|
|
|
|
//#then
|
|
// payment required is classified as quota_exceeded, which triggers fallback
|
|
expect(retryable).toBe(true)
|
|
})
|
|
|
|
test("500 server error is retryable", () => {
|
|
//#given
|
|
const error = { statusCode: 500, message: "Internal Server Error" }
|
|
|
|
//#when
|
|
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
|
|
|
|
//#then
|
|
expect(retryable).toBe(true)
|
|
})
|
|
|
|
test("503 service unavailable is retryable", () => {
|
|
//#given
|
|
const error = { statusCode: 503, message: "Service Unavailable" }
|
|
|
|
//#when
|
|
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
|
|
|
|
//#then
|
|
expect(retryable).toBe(true)
|
|
})
|
|
})
|
|
})
|