import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { MODELS } from "@/lib/chat/models"; import { loadConversations, saveConversations, storageKey } from "@/lib/chat/storage"; import type { Conversation } from "@/lib/chat/types"; const conversation: Conversation = { id: "conversation-1", title: "Рабочий вопрос", modelId: "chatgpt", updatedAt: "2026-07-26T10:00:00.000Z", messages: [ { id: "message-1", role: "user", content: "Привет", createdAt: "2026-07-26T09:59:00.000Z", status: "complete", }, ], }; let storage: Storage; beforeEach(() => { const values = new Map(); storage = { get length() { return values.size; }, clear: () => values.clear(), getItem: (key) => values.get(key) ?? null, key: (index) => [...values.keys()][index] ?? null, removeItem: (key) => values.delete(key), setItem: (key, value) => values.set(key, value), }; Object.defineProperty(window, "localStorage", { configurable: true, value: storage }); }); afterEach(() => { storage.clear(); vi.restoreAllMocks(); }); describe("chat storage", () => { it("namespaces conversations by authenticated user", () => { saveConversations("user-a", [conversation]); expect(loadConversations("user-a")).toEqual([conversation]); expect(loadConversations("user-b")).toEqual([]); }); it("discards malformed persisted data", () => { storage.setItem(storageKey("demo-user"), "{not-json"); expect(loadConversations("demo-user")).toEqual([]); }); it("uses only the safe Aegida auto fallback before catalog discovery", () => { expect(MODELS.map((model) => model.id)).toEqual(["auto"]); }); it("retains conversations with dynamic Gate model slugs", () => { saveConversations("demo-user", [ { ...conversation, modelId: "claude-sonnet.4" }, ]); expect(loadConversations("demo-user")[0]?.modelId).toBe("claude-sonnet.4"); }); it("discards persisted conversations with invalid nested data", () => { storage.setItem( storageKey("demo-user"), JSON.stringify([{ ...conversation, messages: [{ ...conversation.messages[0], status: "pending" }] }]), ); expect(loadConversations("demo-user")).toEqual([]); }); it("discards persisted conversations with impossible calendar dates", () => { storage.setItem( storageKey("demo-user"), JSON.stringify([{ ...conversation, updatedAt: "2026-02-30T10:00:00.000Z" }]), ); expect(loadConversations("demo-user")).toEqual([]); }); it("discards persisted conversations with non-ISO timestamps", () => { storage.setItem( storageKey("demo-user"), JSON.stringify([{ ...conversation, messages: [{ ...conversation.messages[0], createdAt: "26 July 2026" }] }]), ); expect(loadConversations("demo-user")).toEqual([]); }); it("sorts loaded conversations newest first", () => { const older = { ...conversation, id: "older", updatedAt: "2026-07-25T10:00:00.000Z" }; storage.setItem(storageKey("demo-user"), JSON.stringify([older, conversation])); expect(loadConversations("demo-user").map(({ id }) => id)).toEqual(["conversation-1", "older"]); }); it("persists at most 50 conversations", () => { const conversations = Array.from({ length: 51 }, (_, index) => ({ ...conversation, id: `conversation-${index}`, })); saveConversations("demo-user", conversations); expect(loadConversations("demo-user")).toHaveLength(50); }); it("returns an empty list when browser storage throws", () => { vi.spyOn(storage, "getItem").mockImplementation(() => { throw new Error("Storage unavailable"); }); expect(loadConversations("demo-user")).toEqual([]); }); });