|
|
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();
|
|
|
});
|
|
|
});
|