import { beforeEach, describe, expect, it } from "vitest"; import type { ComposerDraftStorage } from "./composer-drafts.js"; import { COMPOSER_DRAFT_LIMIT, COMPOSER_DRAFT_STORAGE_KEY, clearComposerDraft, composerDraftThreadKey, EMPTY_COMPOSER_DRAFT, isComposerDraftEmpty, readComposerDraft, resetMemoryComposerDrafts, writeComposerDraft, } from "./composer-drafts.js"; function createFakeStorage(): ComposerDraftStorage & { map: Map } { const map = new Map(); return { map, getItem(key: string) { return map.get(key) ?? null; }, setItem(key: string, value: string) { map.set(key, value); }, removeItem(key: string) { map.delete(key); }, }; } describe("composer-drafts", () => { beforeEach(() => { resetMemoryComposerDrafts(); }); it("derives thread keys for bot and group conversations", () => { expect(composerDraftThreadKey({ botId: "bot-1" })).toBe("bot:bot-1"); expect(composerDraftThreadKey({ groupId: "grp-1", botId: "bot-1" })).toBe("group:grp-1"); expect(composerDraftThreadKey({ botId: " ", groupId: null })).toBeUndefined(); }); it("detects empty vs non-empty drafts", () => { expect(isComposerDraftEmpty(EMPTY_COMPOSER_DRAFT)).toBe(true); expect( isComposerDraftEmpty({ text: "Weiter an der Aufgabe schreiben", selectedSkill: null, selectedMentions: [], }), ).toBe(false); expect( isComposerDraftEmpty({ text: "", selectedSkill: { id: "skill-1", name: "research", description: "Research skill", source: "user", readOnly: false, }, selectedMentions: [], }), ).toBe(false); }); it("persists and restores drafts across thread and page switches independently", () => { const storage = createFakeStorage(); const botKey = composerDraftThreadKey({ botId: "alpha" }); const groupKey = composerDraftThreadKey({ groupId: "team-room" }); writeComposerDraft( botKey, { text: "Entwurf im ersten Chat", selectedSkill: { id: "sk-1", name: "summarize", description: "Summarize notes", source: "builtin", readOnly: true, }, selectedMentions: [ { kind: "bot", id: "helper-bot", name: "Helper", color: "#112233", }, ], }, storage, ); writeComposerDraft( groupKey, { text: "Andere Nachricht in der Gruppe", selectedSkill: null, selectedMentions: [], }, storage, ); expect(readComposerDraft(botKey, storage)).toEqual({ text: "Entwurf im ersten Chat", selectedSkill: { id: "sk-1", name: "summarize", description: "Summarize notes", source: "builtin", readOnly: true, }, selectedMentions: [ { kind: "bot", id: "helper-bot", name: "Helper", color: "#112233", }, ], }); expect(readComposerDraft(groupKey, storage)).toEqual({ text: "Andere Nachricht in der Gruppe", selectedSkill: null, selectedMentions: [], }); clearComposerDraft(botKey, storage); expect(readComposerDraft(botKey, storage)).toEqual(EMPTY_COMPOSER_DRAFT); expect(readComposerDraft(groupKey, storage).text).toBe("Andere Nachricht in der Gruppe"); clearComposerDraft(groupKey, storage); expect(storage.map.has(COMPOSER_DRAFT_STORAGE_KEY)).toBe(false); }); it("evicts oldest drafts beyond COMPOSER_DRAFT_LIMIT", () => { const storage = createFakeStorage(); for (let index = 0; index < COMPOSER_DRAFT_LIMIT + 5; index += 1) { writeComposerDraft( `bot:b-${index}`, { text: `Draft ${index}`, selectedSkill: null, selectedMentions: [], }, storage, ); } expect(readComposerDraft("bot:b-0", storage)).toEqual(EMPTY_COMPOSER_DRAFT); expect(readComposerDraft("bot:b-4", storage)).toEqual(EMPTY_COMPOSER_DRAFT); expect(readComposerDraft("bot:b-5", storage).text).toBe("Draft 5"); expect(readComposerDraft(`bot:b-${COMPOSER_DRAFT_LIMIT + 4}`, storage).text).toBe( `Draft ${COMPOSER_DRAFT_LIMIT + 4}`, ); }); it("falls back to in-memory storage when persistent storage is unavailable", () => { const key = composerDraftThreadKey({ botId: "mobile-bot" }); writeComposerDraft( key, { text: "Mobile Entwurf bleibt beim Tab-Wechsel erhalten", selectedSkill: null, selectedMentions: [], }, null, ); expect(readComposerDraft(key, null).text).toBe( "Mobile Entwurf bleibt beim Tab-Wechsel erhalten", ); clearComposerDraft(key, null); expect(readComposerDraft(key, null)).toEqual(EMPTY_COMPOSER_DRAFT); }); });