import { beforeEach, describe, expect, it, vi } from "vitest"; import { handleRpaFailure } from "@/modules/quote/fallback-orchestrator"; vi.mock("@/modules/cache/redis-cache", () => ({ getL3: vi.fn(), })); vi.mock("@/lib/prisma", () => ({ prisma: { quoteRecord: { update: vi.fn() }, alertLog: { create: vi.fn() }, }, })); vi.mock("@/modules/pricing/engine", () => ({ getMarkupPercent: vi.fn().mockResolvedValue(10), getMarkupRule: vi.fn().mockResolvedValue({ type: "percent", percent: 10 }), applyMarkupToQuotes: vi.fn((quotes: unknown[]) => quotes.map((q) => ({ ...(q as object), markup_percent: 10 })), ), })); import { getL3 } from "@/modules/cache/redis-cache"; import { prisma } from "@/lib/prisma"; const mockedGetL3 = vi.mocked(getL3); const mockedUpdate = vi.mocked(prisma.quoteRecord.update); const mockedAlertCreate = vi.mocked(prisma.alertLog.create); const FOUR_TIERS = [ { service_level: "standard", rate_option: "lowest", raw_freight: 320, surcharges: 0, raw_total: 320, carrier: "M", transit_days: "5", transit_description: "x" }, { service_level: "standard", rate_option: "fastest", raw_freight: 365, surcharges: 0, raw_total: 365, carrier: "M", transit_days: "3", transit_description: "x" }, { service_level: "guaranteed", rate_option: "lowest", raw_freight: 380, surcharges: 0, raw_total: 380, carrier: "M", transit_days: "4", transit_description: "x" }, { service_level: "guaranteed", rate_option: "fastest", raw_freight: 410, surcharges: 0, raw_total: 410, carrier: "M", transit_days: "3", transit_description: "x" }, ]; describe("handleRpaFailure", () => { beforeEach(() => { vi.clearAllMocks(); mockedUpdate.mockResolvedValue({} as never); mockedAlertCreate.mockResolvedValue({} as never); }); it("TC-205:L3 命中 → done stale + STALE_FALLBACK", async () => { mockedGetL3.mockResolvedValue({ quotes: FOUR_TIERS }); const result = await handleRpaFailure( "QTE_20260616_0001", "abc123", "CUST_001", new Error("captcha"), ); expect(result.status).toBe("done"); if (result.status === "done") { expect(result.sourceType).toBe("stale"); expect(result.isRealtime).toBe(false); } expect(mockedAlertCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ alertType: "STALE_FALLBACK" }), }), ); }); it("TC-206:无 L3 → failed QUOTE_UNAVAILABLE + RPA_FAILED", async () => { mockedGetL3.mockResolvedValue(null); const result = await handleRpaFailure( "QTE_20260616_0001", "abc123", "CUST_001", new Error("timeout"), ); expect(result).toEqual({ status: "failed", errorCode: "QUOTE_UNAVAILABLE", errorMessage: "timeout", }); expect(mockedAlertCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ alertType: "RPA_FAILED" }), }), ); }); it("skipStaleFallback 时不使用 L3 缓存", async () => { mockedGetL3.mockResolvedValue({ quotes: FOUR_TIERS }); const result = await handleRpaFailure( "QTE_20260616_0001", "abc123", "CUST_001", new Error("priority1 timeout"), { skipStaleFallback: true }, ); expect(result.status).toBe("failed"); expect(mockedGetL3).not.toHaveBeenCalled(); }); });