import { describe, expect, it } from "vitest"; import { ChatSendResponse } from "@/data/dto/chat/chat_send_response"; import type { ChatState } from "@/stores/chat/chat-state"; import { applyHttpSendOutput, countLockedHistoryMessages, localMessagesToUi, sendResponseToUiMessage, } from "@/stores/chat/chat-machine.helpers"; function makeResponse( overrides: Partial[0]> = {}, ): ChatSendResponse { return ChatSendResponse.from({ reply: "AI reply", audioUrl: "", messageId: "msg-1", isGuest: false, timestamp: 1782356425363, image: { type: null, url: null }, lockDetail: { locked: false, showContent: true, showUpgrade: false, reason: null, hint: null, detail: null, }, ...overrides, }); } function makeChatState(overrides: Partial = {}): ChatState { return { messages: [], isReplyingAI: true, pendingReplyCount: 1, upgradePromptVisible: false, upgradeReason: null, upgradeHint: null, upgradeDetail: null, isLoadingMore: false, hasMore: true, historyOffset: 0, historyLoaded: true, paymentUnlockPending: false, unlockHistoryPromptVisible: false, lockedHistoryCount: 0, isUnlockingHistory: false, unlockHistoryError: null, ...overrides, }; } describe("sendResponseToUiMessage", () => { it("maps locked voice messages without treating them as private text", () => { const message = sendResponseToUiMessage( makeResponse({ audioUrl: "https://proapi.banlv-ai.com/audio/5198b93c-2915-406a.mp3", lockDetail: { locked: true, showContent: false, showUpgrade: true, reason: "voice_message", hint: "这语气好撩呀,让人有点心跳加速呢…", detail: { messageId: "msg-1" }, }, }), ); expect(message.content).toBe(""); expect(message.audioUrl).toBe( "https://proapi.banlv-ai.com/audio/5198b93c-2915-406a.mp3", ); expect(message.locked).toBe(true); expect(message.lockReason).toBe("voice_message"); expect(message.lockedPrivate).toBeUndefined(); expect(message.privateMessageHint).toBe( "这语气好撩呀,让人有点心跳加速呢…", ); }); it("maps locked private text messages as private messages", () => { const message = sendResponseToUiMessage( makeResponse({ lockDetail: { locked: true, showContent: false, showUpgrade: true, reason: "private_message", hint: "Elio has a private message for you.", detail: { messageId: "msg-1" }, }, }), ); expect(message.content).toBe(""); expect(message.locked).toBe(true); expect(message.lockReason).toBe("private_message"); expect(message.isPrivate).toBe(true); expect(message.lockedPrivate).toBe(true); expect(message.privateMessageHint).toBe( "Elio has a private message for you.", ); }); it("keeps paywalled image messages visible but locked", () => { const message = sendResponseToUiMessage( makeResponse({ image: { type: "jpg", url: "https://example.com/private-photo.jpg", }, lockDetail: { locked: true, showContent: false, showUpgrade: true, reason: "image", hint: "Activate VIP to view the full photo.", detail: { imageId: "image-1" }, }, }), ); expect(message.content).toBe(""); expect(message.imageUrl).toBe("https://example.com/private-photo.jpg"); expect(message.imagePaywalled).toBe(true); expect(message.locked).toBe(true); expect(message.lockReason).toBe("image"); expect(message.lockedPrivate).toBeUndefined(); }); }); describe("applyHttpSendOutput", () => { it("removes the optimistic user message when the daily limit is reached", () => { const context = makeChatState({ messages: [ { content: "hello", isFromAI: false, date: "2026-06-25", }, ], }); const nextState = applyHttpSendOutput(context, { response: makeResponse({ reply: "", messageId: "", lockDetail: { locked: true, showContent: false, showUpgrade: true, reason: "daily_limit", hint: "Free message limit reached.", detail: { type: "daily_msg_limit", usedToday: 3, limit: 3, }, }, }), reply: null, }); expect(nextState.messages).toEqual([]); expect(nextState.isReplyingAI).toBe(false); expect(nextState.upgradePromptVisible).toBe(true); expect(nextState.upgradeReason).toBe("daily_limit"); expect(nextState.upgradeHint).toBeNull(); expect(nextState.upgradeDetail).toEqual({ type: "daily_msg_limit", usedToday: 3, limit: 3, }); }); }); describe("localMessagesToUi", () => { it("maps locked voice messages from history", () => { const [message] = localMessagesToUi([ { id: "voice-1", role: "assistant", type: "voice", content: "hidden voice transcript", createdAt: "2026-06-25T12:00:00.000Z", audioUrl: "https://example.com/voice.mp3", image: { type: null, url: null }, lockDetail: { locked: true, showContent: false, showUpgrade: true, reason: "voice_message", hint: "A locked voice message is waiting.", detail: { messageId: "voice-1" }, }, }, ]); expect(message.content).toBe(""); expect(message.audioUrl).toBe("https://example.com/voice.mp3"); expect(message.locked).toBe(true); expect(message.lockReason).toBe("voice_message"); expect(message.lockedPrivate).toBeUndefined(); expect(message.privateMessageHint).toBe("A locked voice message is waiting."); }); it("hides text content for image history messages", () => { const [message] = localMessagesToUi([ { id: "image-1", role: "assistant", type: "image", content: "This text should not render with an image.", createdAt: "2026-06-25T12:00:00.000Z", audioUrl: null, image: { type: "jpg", url: "https://example.com/image.jpg", }, lockDetail: { locked: false, showContent: true, showUpgrade: false, reason: null, hint: null, detail: null, }, }, ]); expect(message.content).toBe(""); expect(message.imageUrl).toBe("https://example.com/image.jpg"); expect(message.imagePaywalled).toBeUndefined(); }); }); describe("countLockedHistoryMessages", () => { it("counts only unlockable locked AI messages", () => { expect( countLockedHistoryMessages([ { content: "", isFromAI: true, date: "2026-06-29", locked: true, lockReason: "private_message", }, { content: "", isFromAI: true, date: "2026-06-29", locked: true, lockReason: "voice_message", }, { content: "", isFromAI: true, date: "2026-06-29", locked: true, imageUrl: "https://example.com/locked.jpg", imagePaywalled: true, lockReason: "image", }, { content: "", isFromAI: true, date: "2026-06-29", locked: true, lockReason: "daily_limit", }, { content: "user text", isFromAI: false, date: "2026-06-29", locked: true, lockReason: "private_message", }, ]), ).toBe(3); }); });