import { beforeEach, describe, expect, it, vi } from "vitest"; import { buildGlobalPageUrl } from "@/router/global-route-context"; import { getCharacterRoutes, ROUTES } from "@/router/routes"; import { consumePendingChatImageReturn } from "../chat_image_return_session"; import { clearPendingChatUnlock, peekPendingChatUnlock, } from "../chat_unlock_session"; import { consumeSubscriptionExitUrl, consumeSubscriptionExplicitExitUrl, getSubscriptionFallbackExitUrl, peekSubscriptionExplicitExitUrl, resolveSubscriptionSuccessExitUrl, } from "../subscription_exit"; vi.mock("../chat_image_return_session", () => ({ consumePendingChatImageReturn: vi.fn(), })); vi.mock("../chat_unlock_session", () => ({ clearPendingChatUnlock: vi.fn(), peekPendingChatUnlock: vi.fn(), })); const consumePendingChatImageReturnMock = vi.mocked( consumePendingChatImageReturn, ); const clearPendingChatUnlockMock = vi.mocked(clearPendingChatUnlock); const peekPendingChatUnlockMock = vi.mocked(peekPendingChatUnlock); describe("subscription exit helpers", () => { beforeEach(() => { consumePendingChatImageReturnMock.mockReset(); clearPendingChatUnlockMock.mockReset(); peekPendingChatUnlockMock.mockReset(); }); it("uses explicit chat image return urls first", async () => { consumePendingChatImageReturnMock.mockResolvedValue({ reason: "image_paywall", characterId: "elio", messageId: "msg_1", returnUrl: "/chat?image=msg_1", createdAt: 1, }); await expect(consumeSubscriptionExplicitExitUrl()).resolves.toBe( "/chat?image=msg_1", ); }); it("falls back to chat when requested", async () => { consumePendingChatImageReturnMock.mockResolvedValue(null); peekPendingChatUnlockMock.mockResolvedValue(null); await expect(consumeSubscriptionExitUrl("chat")).resolves.toBe( getCharacterRoutes("elio").chat, ); }); it("falls back to sidebar by default", async () => { consumePendingChatImageReturnMock.mockResolvedValue(null); peekPendingChatUnlockMock.mockResolvedValue(null); expect(getSubscriptionFallbackExitUrl(null)).toBe(ROUTES.sidebar); await expect(consumeSubscriptionExitUrl(null)).resolves.toBe( ROUTES.sidebar, ); }); it("returns to global sidebar with the source character", async () => { const expectedUrl = buildGlobalPageUrl( ROUTES.sidebar, getCharacterRoutes("maya").chat, ); expect(getSubscriptionFallbackExitUrl("sidebar", "maya")).toBe( expectedUrl, ); await expect( consumeSubscriptionExitUrl("sidebar", "maya"), ).resolves.toBe(expectedUrl); await expect( resolveSubscriptionSuccessExitUrl("sidebar", "maya"), ).resolves.toBe(expectedUrl); expect(consumePendingChatImageReturnMock).not.toHaveBeenCalled(); expect(peekPendingChatUnlockMock).not.toHaveBeenCalled(); }); it("returns directly to private room without consuming chat state", async () => { consumePendingChatImageReturnMock.mockResolvedValue({ reason: "image_paywall", characterId: "elio", messageId: "msg_1", returnUrl: "/chat?image=msg_1", createdAt: 1, }); peekPendingChatUnlockMock.mockResolvedValue({ reason: "single_message_unlock", characterId: "elio", displayMessageId: "msg_1", messageId: "msg_1", kind: "image", returnUrl: "/chat?image=msg_1", stage: "payment", createdAt: 1, }); expect(getSubscriptionFallbackExitUrl("private-room")).toBe( getCharacterRoutes("elio").privateRoom, ); await expect(consumeSubscriptionExitUrl("private-room")).resolves.toBe( getCharacterRoutes("elio").privateRoom, ); expect(consumePendingChatImageReturnMock).not.toHaveBeenCalled(); expect(peekPendingChatUnlockMock).not.toHaveBeenCalled(); expect(clearPendingChatUnlockMock).not.toHaveBeenCalled(); }); it("keeps private room ahead of stale chat state after payment", async () => { peekPendingChatUnlockMock.mockResolvedValue({ reason: "single_message_unlock", characterId: "elio", displayMessageId: "msg_1", messageId: "msg_1", kind: "image", returnUrl: "/chat?image=msg_1", stage: "payment", createdAt: 1, }); await expect( resolveSubscriptionSuccessExitUrl("private-room", "maya"), ).resolves.toBe(getCharacterRoutes("maya").privateRoom); expect(peekPendingChatUnlockMock).not.toHaveBeenCalled(); expect(clearPendingChatUnlockMock).not.toHaveBeenCalled(); }); it("keeps pending chat unlocks ahead of chat fallback after payment", async () => { peekPendingChatUnlockMock.mockResolvedValue({ reason: "single_message_unlock", characterId: "elio", displayMessageId: "msg_1", messageId: "msg_1", kind: "image", returnUrl: "/chat?image=msg_1", stage: "payment", createdAt: 1, }); await expect(resolveSubscriptionSuccessExitUrl("chat")).resolves.toBe( "/chat?image=msg_1", ); expect(clearPendingChatUnlockMock).not.toHaveBeenCalled(); }); it("peeks chat unlock return urls without clearing them", async () => { peekPendingChatUnlockMock.mockResolvedValue({ reason: "single_message_unlock", characterId: "elio", displayMessageId: "msg_1", messageId: "msg_1", kind: "image", returnUrl: "/chat?image=msg_1", stage: "payment", createdAt: 1, }); await expect(peekSubscriptionExplicitExitUrl()).resolves.toBe( "/chat?image=msg_1", ); expect(clearPendingChatUnlockMock).not.toHaveBeenCalled(); }); it("clears chat unlock return urls when consumed", async () => { consumePendingChatImageReturnMock.mockResolvedValue(null); peekPendingChatUnlockMock.mockResolvedValue({ reason: "single_message_unlock", characterId: "elio", displayMessageId: "msg_1", messageId: "msg_1", kind: "image", returnUrl: "/chat?image=msg_1", stage: "payment", createdAt: 1, }); await expect(consumeSubscriptionExplicitExitUrl()).resolves.toBe( "/chat?image=msg_1", ); expect(clearPendingChatUnlockMock).toHaveBeenCalledTimes(1); }); });