carbon-tokenization / frontend /tests /chat-persistence.test.ts
tfrere's picture
tfrere HF Staff
feat(frontend): editor refresh (embed studio, comment popover, shiki, top bar, hooks, styles)
76fc93a
Raw
History Blame Contribute Delete
5.98 kB
/**
* Chat Persistence Tests
*
* Unit tests for saveMessages, loadMessages, chatStorageKey, and getStableAnonId.
* Uses a minimal localStorage mock (no jsdom needed).
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import {
chatStorageKey,
saveMessages,
loadMessages,
getStableAnonId,
} from "../src/utils/chat-persistence";
// Minimal localStorage mock
const store: Record<string, string> = {};
const localStorageMock = {
getItem: vi.fn((key: string) => store[key] ?? null),
setItem: vi.fn((key: string, value: string) => { store[key] = value; }),
removeItem: vi.fn((key: string) => { delete store[key]; }),
clear: vi.fn(() => { for (const k in store) delete store[k]; }),
get length() { return Object.keys(store).length; },
key: vi.fn((_i: number) => null),
};
vi.stubGlobal("localStorage", localStorageMock);
beforeEach(() => {
localStorageMock.clear();
vi.clearAllMocks();
});
// Factory for UIMessage-like objects
function msg(overrides: Record<string, unknown> = {}) {
return {
id: "m1",
role: "user" as const,
parts: [{ type: "text", text: "hello" }],
content: "hello",
createdAt: new Date("2025-06-01T12:00:00Z"),
...overrides,
};
}
describe("chatStorageKey", () => {
it("builds key from user name and scope", () => {
expect(chatStorageKey("alice", "article")).toBe("chat:alice:article");
});
it("handles embed scopes", () => {
expect(chatStorageKey("bob", "embed:banner.html")).toBe("chat:bob:embed:banner.html");
});
});
describe("saveMessages / loadMessages round-trip", () => {
it("saves and loads a single message", () => {
const m = msg();
saveMessages("alice", "article", [m as any]);
const loaded = loadMessages("alice", "article");
expect(loaded).toBeDefined();
expect(loaded).toHaveLength(1);
expect(loaded![0].id).toBe("m1");
expect(loaded![0].role).toBe("user");
});
it("restores createdAt as a Date object", () => {
const m = msg();
saveMessages("alice", "article", [m as any]);
const loaded = loadMessages("alice", "article")! as any;
expect(loaded[0].createdAt).toBeInstanceOf(Date);
expect(loaded[0].createdAt!.toISOString()).toBe("2025-06-01T12:00:00.000Z");
});
it("handles messages without createdAt", () => {
const m = msg({ createdAt: undefined });
saveMessages("alice", "article", [m as any]);
const loaded = loadMessages("alice", "article")! as any;
expect(loaded[0].createdAt).toBeUndefined();
});
it("preserves parts array", () => {
const m = msg({ parts: [{ type: "text", text: "hi" }, { type: "text", text: "there" }] });
saveMessages("alice", "article", [m as any]);
const loaded = loadMessages("alice", "article")!;
expect(loaded[0].parts).toEqual([{ type: "text", text: "hi" }, { type: "text", text: "there" }]);
});
it("saves multiple messages and preserves order", () => {
const msgs = [
msg({ id: "m1", role: "user" }),
msg({ id: "m2", role: "assistant", content: "hey" }),
msg({ id: "m3", role: "user", content: "thanks" }),
];
saveMessages("alice", "article", msgs as any);
const loaded = loadMessages("alice", "article")!;
expect(loaded).toHaveLength(3);
expect(loaded.map((m) => m.id)).toEqual(["m1", "m2", "m3"]);
expect(loaded.map((m) => m.role)).toEqual(["user", "assistant", "user"]);
});
});
describe("saveMessages - edge cases", () => {
it("removes key when saving empty array", () => {
saveMessages("alice", "article", [msg() as any]);
expect(localStorageMock.setItem).toHaveBeenCalled();
saveMessages("alice", "article", []);
expect(localStorageMock.removeItem).toHaveBeenCalledWith("chat:alice:article");
const loaded = loadMessages("alice", "article");
expect(loaded).toBeUndefined();
});
it("does not throw when localStorage.setItem fails", () => {
localStorageMock.setItem.mockImplementationOnce(() => { throw new Error("QuotaExceeded"); });
expect(() => saveMessages("alice", "article", [msg() as any])).not.toThrow();
});
});
describe("loadMessages - edge cases", () => {
it("returns undefined for missing key", () => {
expect(loadMessages("unknown", "article")).toBeUndefined();
});
it("returns undefined for corrupt JSON", () => {
store["chat:alice:article"] = "not json!!!";
expect(loadMessages("alice", "article")).toBeUndefined();
});
it("returns undefined for non-array JSON", () => {
store["chat:alice:article"] = JSON.stringify({ not: "an array" });
expect(loadMessages("alice", "article")).toBeUndefined();
});
it("returns undefined for empty array JSON", () => {
store["chat:alice:article"] = JSON.stringify([]);
expect(loadMessages("alice", "article")).toBeUndefined();
});
});
describe("user scoping", () => {
it("isolates messages between users", () => {
saveMessages("alice", "article", [msg({ id: "a1" }) as any]);
saveMessages("bob", "article", [msg({ id: "b1" }) as any]);
expect(loadMessages("alice", "article")![0].id).toBe("a1");
expect(loadMessages("bob", "article")![0].id).toBe("b1");
});
it("isolates article and embed scopes for same user", () => {
saveMessages("alice", "article", [msg({ id: "art" }) as any]);
saveMessages("alice", "embed:chart.html", [msg({ id: "emb" }) as any]);
expect(loadMessages("alice", "article")![0].id).toBe("art");
expect(loadMessages("alice", "embed:chart.html")![0].id).toBe("emb");
});
});
describe("getStableAnonId", () => {
it("returns a string starting with 'anon-'", () => {
const id = getStableAnonId();
expect(id).toMatch(/^anon-/);
});
it("returns the same value on subsequent calls", () => {
const first = getStableAnonId();
const second = getStableAnonId();
expect(first).toBe(second);
});
it("persists the id in localStorage", () => {
const id = getStableAnonId();
expect(store["collab-editor:anon-id"]).toBe(id);
});
});