/** * Flock hold-service:继续 / 放弃 / 超时 / 越权 */ import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@/modules/address/parked-session-queue", () => ({ requestReleaseParkedSession: vi.fn().mockResolvedValue(undefined), })); vi.mock("@/lib/flock/flock-quote-hold-store", () => ({ assertFlockQuoteHoldUsable: vi.fn(), clearFlockQuoteHold: vi.fn().mockResolvedValue(undefined), readFlockQuoteHold: vi.fn(), updateFlockQuoteHoldStatus: vi.fn(), })); import { requestReleaseParkedSession } from "@/modules/address/parked-session-queue"; import { assertFlockQuoteHoldUsable, clearFlockQuoteHold, readFlockQuoteHold, updateFlockQuoteHoldStatus, } from "@/lib/flock/flock-quote-hold-store"; import { FLOCK_HOLD_DECISION_MS, FLOCK_HOLD_MSG, FLOCK_HOLD_TOTAL_MS, buildFlockQuoteHoldState, } from "@/lib/constants/flock-quote-hold"; import { continueFlockQuoteHold, declineFlockQuoteHold, } from "@/modules/flock/hold-service"; const SESSION = "flock_q-hold-1"; const BASE = { customerId: "CUST_001", quoteId: "q-hold-1", sessionId: SESSION, }; function makeState( patch?: Partial>, ) { const now = Date.now(); return { ...buildFlockQuoteHoldState({ quoteId: BASE.quoteId, sessionId: SESSION, customerId: BASE.customerId, nowMs: now, }), ...patch, }; } describe("continueFlockQuoteHold / declineFlockQuoteHold", () => { beforeEach(() => { vi.clearAllMocks(); }); it("decline:释放驻留并清理 Redis", async () => { vi.mocked(readFlockQuoteHold).mockResolvedValue(makeState()); vi.mocked(updateFlockQuoteHoldStatus).mockResolvedValue( makeState({ status: "declined" }), ); const r = await declineFlockQuoteHold(BASE); expect(r.released).toBe(true); expect(requestReleaseParkedSession).toHaveBeenCalledWith(SESSION); expect(clearFlockQuoteHold).toHaveBeenCalledWith(SESSION); }); it("decline:客户不匹配 → 无权", async () => { vi.mocked(readFlockQuoteHold).mockResolvedValue( makeState({ customer_id: "OTHER" }), ); await expect(declineFlockQuoteHold(BASE)).rejects.toThrow(/无权/); }); it("decline:quote 不匹配 → 报错", async () => { vi.mocked(readFlockQuoteHold).mockResolvedValue( makeState({ quote_id: "other-q" }), ); await expect(declineFlockQuoteHold(BASE)).rejects.toThrow(/不匹配/); }); it("continue:awaiting → filling", async () => { vi.mocked(assertFlockQuoteHoldUsable).mockResolvedValue(makeState()); vi.mocked(updateFlockQuoteHoldStatus).mockResolvedValue( makeState({ status: "filling" }), ); const r = await continueFlockQuoteHold(BASE); expect(r.status).toBe("filling"); expect(updateFlockQuoteHoldStatus).toHaveBeenCalledWith( SESSION, "filling", ); }); it("continue:已 filling / checking_out → 幂等返回", async () => { vi.mocked(assertFlockQuoteHoldUsable).mockResolvedValue( makeState({ status: "filling" }), ); const r = await continueFlockQuoteHold(BASE); expect(r.status).toBe("filling"); expect(updateFlockQuoteHoldStatus).not.toHaveBeenCalled(); vi.mocked(assertFlockQuoteHoldUsable).mockResolvedValue( makeState({ status: "checking_out" }), ); await expect(continueFlockQuoteHold(BASE)).resolves.toEqual({ status: "filling", }); }); it("continue:60s 决策窗已过 → 自动 decline + 中文超时", async () => { const parked = 1_000_000; const state = makeState({ parked_at_ms: parked, decision_deadline_ms: parked + FLOCK_HOLD_DECISION_MS, total_deadline_ms: parked + FLOCK_HOLD_TOTAL_MS, status: "awaiting_decision", }); // 模拟「现在」已过决策窗:直接构造已到期的 deadline const expiredDecision = { ...state, decision_deadline_ms: Date.now() - 1, total_deadline_ms: Date.now() + 60_000, }; vi.mocked(assertFlockQuoteHoldUsable).mockResolvedValue(expiredDecision); vi.mocked(readFlockQuoteHold).mockResolvedValue(expiredDecision); vi.mocked(updateFlockQuoteHoldStatus).mockResolvedValue({ ...expiredDecision, status: "declined", }); await expect(continueFlockQuoteHold(BASE)).rejects.toThrow( FLOCK_HOLD_MSG.decisionTimeoutApi, ); expect(requestReleaseParkedSession).toHaveBeenCalled(); }); it("continue:5min 总硬限已过 → totalTimeoutApi", async () => { const expired = makeState({ status: "filling", total_deadline_ms: Date.now() - 1, decision_deadline_ms: Date.now() - 120_000, }); vi.mocked(assertFlockQuoteHoldUsable).mockResolvedValue(expired); vi.mocked(readFlockQuoteHold).mockResolvedValue(expired); vi.mocked(updateFlockQuoteHoldStatus).mockResolvedValue({ ...expired, status: "declined", }); await expect(continueFlockQuoteHold(BASE)).rejects.toThrow( FLOCK_HOLD_MSG.totalTimeoutApi, ); }); it("continue:会话不存在 → assert 抛出", async () => { vi.mocked(assertFlockQuoteHoldUsable).mockRejectedValue( new Error(FLOCK_HOLD_MSG.sessionGone), ); await expect(continueFlockQuoteHold(BASE)).rejects.toThrow( /询价会话已失效/, ); }); });