import { beforeEach, describe, expect, it } from "vitest"; import { __resetLocalStorage } from "../__mocks__/@vicinae/api"; import { MAX_STORED_CHARS_PER_TURN, MAX_TURNS_PER_SESSION } from "../lib/constants"; import { capTurn, capTurns, createSession, createTurn, clearSessions, deleteSession, listSessions, loadSession, saveSession, withTitleFromFirstTurn, } from "../lib/sessions"; import type { ChatTurn } from "../lib/types"; function turn(index: number, response = `answer ${index}`): ChatTurn { return { id: `turn-${index}`, prompt: `question ${index}`, response, thinking: "", model: "test-model", createdAt: 1_000 + index, toolCalls: [], attachments: [], }; } beforeEach(() => { __resetLocalStorage(); }); describe("session store", () => { it("round-trips a session through LocalStorage", async () => { const session = { ...createSession("test-model", 1_000), title: "First chat", turns: [turn(1)] }; await saveSession(session); const summaries = await listSessions(); expect(summaries).toHaveLength(1); expect(summaries[0]).toMatchObject({ id: session.id, title: "First chat", turnCount: 1 }); const loaded = await loadSession(session.id); expect(loaded?.turns[0].response).toBe("answer 1"); expect(loaded?.model).toBe("test-model"); }); it("returns undefined for an unknown session id", async () => { expect(await loadSession("does-not-exist")).toBeUndefined(); }); it("orders sessions by most recently updated", async () => { const older = { ...createSession("m", 1_000), title: "older", updatedAt: 1_000 }; const newer = { ...createSession("m", 2_000), title: "newer", updatedAt: 5_000 }; await saveSession(older); await saveSession(newer); const summaries = await listSessions(); expect(summaries.map((summary) => summary.title)).toEqual(["newer", "older"]); }); it("updates an existing index entry instead of duplicating it", async () => { const session = createSession("m", 1_000); await saveSession(session); await saveSession({ ...session, title: "renamed", updatedAt: 2_000 }); const summaries = await listSessions(); expect(summaries).toHaveLength(1); expect(summaries[0].title).toBe("renamed"); }); it("deletes a session and its index entry", async () => { const session = createSession("m"); await saveSession(session); await deleteSession(session.id); expect(await listSessions()).toHaveLength(0); expect(await loadSession(session.id)).toBeUndefined(); }); it("clears everything", async () => { await saveSession(createSession("m", 1)); await saveSession(createSession("m", 2)); await clearSessions(); expect(await listSessions()).toHaveLength(0); }); it("survives a corrupt index rather than throwing", async () => { const { LocalStorage } = await import("../__mocks__/@vicinae/api"); await LocalStorage.setItem("lam.sessions.index", "{not json"); await expect(listSessions()).resolves.toEqual([]); }); it("returns undefined for a corrupt session body", async () => { const { LocalStorage } = await import("../__mocks__/@vicinae/api"); await LocalStorage.setItem("lam.session.broken", '{"id":"broken"}'); await expect(loadSession("broken")).resolves.toBeUndefined(); }); }); describe("caps", () => { it("keeps only the most recent turns", () => { const many = Array.from({ length: MAX_TURNS_PER_SESSION + 5 }, (_, index) => turn(index)); const capped = capTurns(many); expect(capped).toHaveLength(MAX_TURNS_PER_SESSION); // The oldest five should be the ones dropped, not the newest. expect(capped[0].id).toBe("turn-5"); expect(capped[capped.length - 1].id).toBe(`turn-${MAX_TURNS_PER_SESSION + 4}`); }); it("truncates an oversized turn and marks it", () => { const huge = turn(1, "x".repeat(MAX_STORED_CHARS_PER_TURN + 500)); const capped = capTurn(huge); expect(capped.response.length).toBeLessThan(MAX_STORED_CHARS_PER_TURN + 100); expect(capped.response).toContain("[truncated]"); }); it("applies caps on save so storage cannot grow without bound", async () => { const session = { ...createSession("m", 1_000), turns: Array.from({ length: MAX_TURNS_PER_SESSION + 10 }, (_, index) => turn(index)), }; await saveSession(session); const loaded = await loadSession(session.id); expect(loaded?.turns).toHaveLength(MAX_TURNS_PER_SESSION); }); }); describe("withTitleFromFirstTurn", () => { it("names the session from its first prompt", () => { const session = { ...createSession("m", 1_000), turns: [turn(1)] }; expect(withTitleFromFirstTurn(session).title).toBe("question 1"); }); it("leaves an already-named session alone", () => { const session = { ...createSession("m", 1_000), title: "Kept", turns: [turn(1)] }; expect(withTitleFromFirstTurn(session).title).toBe("Kept"); }); it("leaves an empty session unnamed", () => { expect(withTitleFromFirstTurn(createSession("m", 1_000)).title).toBe("New chat"); }); }); describe("createTurn", () => { it("starts with an empty response so the UI can stream into it", () => { const created = createTurn("hello", "test-model", 1_000); expect(created.response).toBe(""); expect(created.prompt).toBe("hello"); expect(created.toolCalls).toEqual([]); }); });