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<string, string>();
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([]);
});
});