evOo-local/packages/core/src/composer-drafts.test.ts

177 lines
4.9 KiB
TypeScript

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<string, string> } {
const map = new Map<string, string>();
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);
});
});