aegida-console / lib / chat / storage.test.ts
storage.test.ts
Raw
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([]);
  });
});