import { expect, test } from "@playwright/test"; import { getUserFacingImageGenerationErrorMessage, getUserFacingRequestErrorMessage, isProviderBalanceError, isRateLimitError, } from "@/lib/request-error"; import { isImageModelBalanceBlocked, resolveConfiguredImageBalanceFallback } from "@/lib/ai/runtime/image-balance"; import { serverLoader } from "../helpers/canvas-download-server-loader"; test("a bare HTTP 429 is rate limiting, not insufficient balance", () => { const error = Object.assign(new Error("Too many requests (HTTP 429)"), { status: 429 }); expect(isProviderBalanceError(error)).toBe(false); expect(isRateLimitError(error)).toBe(true); expect(getUserFacingImageGenerationErrorMessage(error)).toContain("请求过于频繁"); expect(getUserFacingRequestErrorMessage(error)).toContain("请求过于频繁"); }); test("an explicit provider balance error takes precedence over HTTP 429", () => { const error = Object.assign(new Error("余额不足,请点击充值页面续费(HTTP 429)"), { status: 429 }); expect(isProviderBalanceError(error)).toBe(true); expect(isRateLimitError(error)).toBe(false); expect(getUserFacingImageGenerationErrorMessage(error)).toContain("服务商线路余额"); expect(getUserFacingImageGenerationErrorMessage(error)).toContain("与您的积分余额无关"); expect(getUserFacingImageGenerationErrorMessage(error)).not.toMatch(/^积分不足/); }); test("nested billing details and generic quota limits are kept distinct", () => { const nested = { status: 429, message: "request failed", data: { error: { message: "insufficient_balance" } } }; expect(isProviderBalanceError(nested)).toBe(true); expect(isRateLimitError(nested)).toBe(false); const usageLimit = new Error("Rate quota exceeded (HTTP 429)"); expect(isProviderBalanceError(usageLimit)).toBe(false); expect(isRateLimitError(usageLimit)).toBe(true); expect(isProviderBalanceError(new Error("该模型额度不足(HTTP 429)"))).toBe(true); }); test("confirmed balance failures remain isolated until explicitly restored", () => { const model = { healthLastEventType: "quota", healthLastError: "余额不足(HTTP 429)", }; expect(isImageModelBalanceBlocked(model)).toBe(true); expect(isImageModelBalanceBlocked({ healthLastEventType: "error", healthAutoDisabledReason: "provider_balance_or_billing_quota: 余额不足" })).toBe(true); expect(isImageModelBalanceBlocked({ ...model, healthLastEventType: "success", healthLastError: null })).toBe(false); }); test("old selections use only configured, enabled replacements that preserve the requested inputs", () => { const requested = { healthLastEventType: "quota", healthLastError: "余额不足", fallbackModelId1: "verified" }; const verified = { model: { id: "verified", modelId: "same-family", isEnabled: true, supportsImageGen: true, imageGenConfig: { supportedImageSizes: ["2K"], supportedAspectRatios: ["1:1"], maxReferenceImages: 8 } }, provider: { isEnabled: true } }; const input = { imageSize: "2K", aspectRatio: "1:1", referenceImageCount: 2 }; expect(resolveConfiguredImageBalanceFallback(requested, [verified], input)).toBe(verified); expect(resolveConfiguredImageBalanceFallback(requested, [verified], { ...input, referenceImageCount: 9 })).toBeUndefined(); expect(resolveConfiguredImageBalanceFallback(requested, [verified], { ...input, imageSize: "4K" })).toBeUndefined(); expect(resolveConfiguredImageBalanceFallback(requested, [verified], { ...input, aspectRatio: "9:16" })).toBeUndefined(); expect(resolveConfiguredImageBalanceFallback(requested, [{ ...verified, provider: { isEnabled: false } }], input)).toBeUndefined(); expect(resolveConfiguredImageBalanceFallback({ ...requested, fallbackModelId1: null }, [verified], input)).toBeUndefined(); }); function healthHarness() { let model: any = { id: "test", modelId: "test", isEnabled: true, excludeFromFailover: true, excludeFromHealthBan: true, healthStatus: "healthy", healthConsecutiveFailures: 0 }; const events: any[] = []; const query = (rows: () => any[]) => ({ from() { return this; }, where() { return this; }, limit: async () => rows(), then: (resolve: any) => Promise.resolve(rows()).then(resolve), returning: async () => rows() }); const db = { select: () => query(() => [model]), insert: () => ({ values: async (value: any) => { events.push(value); } }), update: () => ({ set: (value: any) => { model = { ...model, ...value }; return query(() => [model]); } }), }; const health = serverLoader({ "@/lib/db": { db } })("lib/ai/runtime/model-health.ts"); return { health, events, get model() { return model; }, age() { model.healthAutoDisabledAt = new Date(Date.now() - 4 * 60 * 60_000); } }; } test("quota pauses an exempt model, survives cooldown and late completion, and can be restored", async () => { const h = healthHarness(); await h.health.recordAiModelHealthFailure({ model: h.model, error: new Error("余额不足(HTTP 429)") }); expect(h.model.isEnabled).toBe(false); expect(h.model.healthStatus).toBe("auto_disabled"); expect(h.events[0].eventType).toBe("quota"); await h.health.recordAiModelHealthFailure({ model: h.model, error: new Error("HTTP 503 unavailable") }); await h.health.recordAiModelHealthFailure({ model: h.model, error: new Error("Too many requests (HTTP 429)") }); expect(h.model.healthStatus).toBe("auto_disabled"); expect(isImageModelBalanceBlocked(h.model)).toBe(true); h.age(); expect(await h.health.reviveAiModelsFromCooldown()).toEqual([]); await h.health.recordAiModelHealthSuccess({ model: h.model }); expect(h.model.isEnabled).toBe(false); await h.health.restoreAiModelHealth(h.model.id); expect(h.model.isEnabled).toBe(true); expect(isImageModelBalanceBlocked(h.model)).toBe(false); }); test("ordinary rate limiting does not trigger billing isolation", async () => { const h = healthHarness(); await h.health.recordAiModelHealthFailure({ model: h.model, error: new Error("Too many requests (HTTP 429)") }); expect(h.model.isEnabled).toBe(true); expect(isImageModelBalanceBlocked(h.model)).toBe(false); });