2026-03-11 17:05:13 +09:00
|
|
|
declare const require: (name: string) => any
|
2026-04-04 20:06:56 +09:00
|
|
|
const { describe, expect, test, beforeEach, afterEach, mock, spyOn } = require("bun:test")
|
|
|
|
|
import * as connectedProvidersCache from "./connected-providers-cache"
|
2026-02-19 04:41:00 +02:00
|
|
|
|
2026-04-04 20:06:56 +09:00
|
|
|
let readConnectedProvidersCacheSpy: ReturnType<typeof spyOn> | undefined
|
|
|
|
|
const { shouldRetryError, selectFallbackProvider } = await import("./model-error-classifier")
|
2026-02-19 04:41:00 +02:00
|
|
|
|
2026-03-11 17:05:13 +09:00
|
|
|
describe("model-error-classifier", () => {
|
2026-02-19 04:41:00 +02:00
|
|
|
beforeEach(() => {
|
2026-04-04 20:06:56 +09:00
|
|
|
readConnectedProvidersCacheSpy?.mockRestore()
|
|
|
|
|
readConnectedProvidersCacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
readConnectedProvidersCacheSpy?.mockRestore()
|
|
|
|
|
readConnectedProvidersCacheSpy = undefined
|
2026-02-19 04:41:00 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("treats overloaded retry messages as retryable", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { message: "Provider is overloaded" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-04 18:35:09 +01:00
|
|
|
test("treats cooling-down auto-retry messages as retryable", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = {
|
|
|
|
|
message:
|
2026-04-17 14:51:52 +09:00
|
|
|
"All credentials for model claude-opus-4-7-thinking are cooling down [retrying in ~5 days attempt #1]",
|
2026-03-04 18:35:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-19 04:41:00 +02:00
|
|
|
test("selectFallbackProvider prefers first connected provider in preference order", () => {
|
|
|
|
|
//#given
|
2026-04-04 20:06:56 +09:00
|
|
|
readConnectedProvidersCacheSpy?.mockReturnValue(["anthropic", "nvidia"])
|
2026-02-19 04:41:00 +02:00
|
|
|
|
|
|
|
|
//#when
|
2026-02-21 03:03:57 +09:00
|
|
|
const provider = selectFallbackProvider(["anthropic", "nvidia"], "nvidia")
|
2026-02-19 04:41:00 +02:00
|
|
|
|
|
|
|
|
//#then
|
2026-02-21 03:03:57 +09:00
|
|
|
expect(provider).toBe("anthropic")
|
2026-02-19 04:41:00 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("selectFallbackProvider falls back to next connected provider when first is disconnected", () => {
|
|
|
|
|
//#given
|
2026-04-04 20:06:56 +09:00
|
|
|
readConnectedProvidersCacheSpy?.mockReturnValue(["nvidia"])
|
2026-02-19 04:41:00 +02:00
|
|
|
|
|
|
|
|
//#when
|
2026-02-21 03:03:57 +09:00
|
|
|
const provider = selectFallbackProvider(["anthropic", "nvidia"])
|
2026-02-19 04:41:00 +02:00
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(provider).toBe("nvidia")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("selectFallbackProvider uses provider preference order when cache is missing", () => {
|
|
|
|
|
//#given - no cache file
|
|
|
|
|
|
|
|
|
|
//#when
|
2026-02-21 03:03:57 +09:00
|
|
|
const provider = selectFallbackProvider(["anthropic", "nvidia"], "nvidia")
|
2026-02-19 04:41:00 +02:00
|
|
|
|
|
|
|
|
//#then
|
2026-02-21 03:03:57 +09:00
|
|
|
expect(provider).toBe("anthropic")
|
2026-02-19 04:41:00 +02:00
|
|
|
})
|
2026-03-04 18:35:09 +01:00
|
|
|
|
2026-03-04 18:55:43 +01:00
|
|
|
test("selectFallbackProvider uses connected preferred provider when fallback providers are unavailable", () => {
|
2026-03-04 18:35:09 +01:00
|
|
|
//#given
|
2026-04-04 20:06:56 +09:00
|
|
|
readConnectedProvidersCacheSpy?.mockReturnValue(["provider-x"])
|
2026-03-04 18:35:09 +01:00
|
|
|
|
|
|
|
|
//#when
|
2026-03-04 18:55:43 +01:00
|
|
|
const provider = selectFallbackProvider(["provider-y"], "provider-x")
|
2026-03-04 18:35:09 +01:00
|
|
|
|
|
|
|
|
//#then
|
2026-03-04 18:55:43 +01:00
|
|
|
expect(provider).toBe("provider-x")
|
2026-03-04 18:35:09 +01:00
|
|
|
})
|
2026-03-12 01:40:24 +09:00
|
|
|
|
2026-04-05 09:13:14 +09:00
|
|
|
test("treats QuotaExceededError (PascalCase name) as non-retryable STOP error", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { name: "QuotaExceededError" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("treats quotaexceedederror (lowercase name) as non-retryable STOP error", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { name: "quotaexceedederror" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("treats InsufficientCreditsError (PascalCase name) as non-retryable STOP error", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { name: "InsufficientCreditsError" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("treats insufficientcreditserror (lowercase name) as non-retryable STOP error", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { name: "insufficientcreditserror" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("treats FreeUsageLimitError (PascalCase name) as non-retryable STOP error", () => {
|
2026-03-12 01:40:24 +09:00
|
|
|
//#given
|
|
|
|
|
const error = { name: "FreeUsageLimitError" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
2026-04-05 09:13:14 +09:00
|
|
|
expect(result).toBe(false)
|
2026-03-12 01:40:24 +09:00
|
|
|
})
|
|
|
|
|
|
2026-04-05 09:13:14 +09:00
|
|
|
test("treats freeusagelimiterror (lowercase name) as non-retryable STOP error", () => {
|
2026-03-12 01:40:24 +09:00
|
|
|
//#given
|
|
|
|
|
const error = { name: "freeusagelimiterror" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
2026-04-05 09:13:14 +09:00
|
|
|
expect(result).toBe(false)
|
2026-03-12 01:40:24 +09:00
|
|
|
})
|
2026-04-05 10:17:50 +09:00
|
|
|
|
2026-04-06 16:56:05 +09:00
|
|
|
test("treats quota reset message as non-retryable STOP error (no error name)", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { message: "quota will reset after 1 hour" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("treats quota exceeded message as non-retryable STOP error (no error name)", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { message: "quota exceeded for this billing period" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("treats usage limit reached message as non-retryable STOP error (no error name)", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { message: "usage limit has been reached for your account" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("treats insufficient credits message as non-retryable STOP error (no error name)", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { message: "insufficient credits to complete this request" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-05 10:17:50 +09:00
|
|
|
test("treats 'bad request' message as retryable (GitHub Copilot rolling update)", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { message: "400 Bad Request" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("treats 'bad request' lowercase as retryable", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { message: "bad request: model temporarily unavailable" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(true)
|
|
|
|
|
})
|
2026-04-06 17:38:37 +09:00
|
|
|
|
|
|
|
|
test("treats subscription quota message as non-retryable", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { message: "Subscription quota exceeded. You can continue using free models." }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("treats HTTP 429 rate limit message as retryable", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { message: "429 Too Many Requests: rate limit reached" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(true)
|
|
|
|
|
})
|
2026-04-28 15:29:33 +09:00
|
|
|
|
|
|
|
|
test("treats forbidden provider message as retryable", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { message: "Forbidden: Selected provider is forbidden" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(true)
|
|
|
|
|
})
|
2026-05-01 01:25:01 +09:00
|
|
|
|
|
|
|
|
test("does not treat unrelated forbidden messages as retryable", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { message: "EACCES: forbidden write to /etc/hosts" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("does not treat unrelated 403 messages as retryable", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const error = { message: "Tool returned HTTP 403 for the requested URL" }
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = shouldRetryError(error)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toBe(false)
|
|
|
|
|
})
|
2026-02-19 04:41:00 +02:00
|
|
|
})
|
2026-04-04 02:35:23 +09:00
|
|
|
|
|
|
|
|
export {}
|