import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@/lib/prisma", () => ({ prisma: { quoteRecord: { findFirst: vi.fn(), findMany: vi.fn(), update: vi.fn(), }, alertLog: { create: vi.fn() }, }, })); vi.mock("@/modules/cache/redis-cache", () => ({ getL3: vi.fn(), })); vi.mock("@/modules/pricing/engine", () => ({ getMarkupPercent: vi.fn().mockResolvedValue(0), applyMarkupToQuotes: vi.fn((q: unknown[]) => q), })); import { prisma } from "@/lib/prisma"; import { getL3 } from "@/modules/cache/redis-cache"; import { sweepTimedOutQuote, sweepTimedOutQuotes, } from "@/workers/scheduler/timeout-sweeper"; 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" }, ]; const mockedFindFirst = vi.mocked(prisma.quoteRecord.findFirst); const mockedUpdate = vi.mocked(prisma.quoteRecord.update); const mockedGetL3 = vi.mocked(getL3); const mockedAlertCreate = vi.mocked(prisma.alertLog.create); beforeEach(() => { vi.clearAllMocks(); }); describe("sweepTimedOutQuote", () => { it("无 L3 → failed + QUOTE_TIMEOUT", async () => { mockedFindFirst.mockResolvedValue({ quoteId: "QTE_001", cargoHash: "hash1", customerId: "CUST_001", createdAt: new Date(Date.now() - 421_000), } as never); mockedGetL3.mockResolvedValue(null); await sweepTimedOutQuote("QTE_001"); expect(mockedUpdate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: "failed", errorCode: "QUOTE_TIMEOUT", }), }), ); expect(mockedAlertCreate).toHaveBeenCalled(); }); it("有 L3 → done stale", async () => { mockedFindFirst.mockResolvedValue({ quoteId: "QTE_002", cargoHash: "hash2", customerId: "CUST_001", createdAt: new Date(Date.now() - 421_000), } as never); mockedGetL3.mockResolvedValue({ quotes: FOUR_TIERS }); await sweepTimedOutQuote("QTE_002"); expect(mockedUpdate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: "done", sourceType: "stale", isRealtime: false, }), }), ); }); it("未超时 → 不处理", async () => { mockedFindFirst.mockResolvedValue({ quoteId: "QTE_003", cargoHash: "hash3", customerId: "CUST_001", createdAt: new Date(Date.now() - 5_000), } as never); await sweepTimedOutQuote("QTE_003"); expect(mockedUpdate).not.toHaveBeenCalled(); }); }); describe("sweepTimedOutQuotes", () => { it("批量扫描返回处理数量", async () => { vi.mocked(prisma.quoteRecord.findMany).mockResolvedValue([ { quoteId: "QTE_A" }, { quoteId: "QTE_B" }, ] as never); mockedFindFirst.mockResolvedValue(null); const count = await sweepTimedOutQuotes(); expect(count).toBe(2); }); });