import { describe, it, expect } from 'vitest'; import { readDocs, storeDocs, storeDocText, docTextKey, isDocKey, LS_DOCS, LS_SRC, DOC_TEXT_PREFIX } from '../src/docstore.js'; /* Storage-Attrappe mit Zähler — die Vergleich-vor-Schreiben-Regel ist nur über die Zahl der setItem-Aufrufe zu beweisen. */ function mem(init){ const m = new Map(Object.entries(init || {})); let writes = 0; return { getItem: k => (m.has(k) ? m.get(k) : null), setItem: (k, v) => { writes++; m.set(k, String(v)); }, removeItem: k => { m.delete(k); }, keys: () => [...m.keys()], writes: () => writes, }; } const A = {id: 'example', name: 'Example', text: '- a'}; const B = {id: 'k1', name: 'Sprint 15', text: '- b', source: 'https://x.example/p'}; describe('docstore — Index + Text je Dokument (D83)', () => { it('Rundreise: storeDocs schreibt, readDocs liest dasselbe zurück', () => { const s = mem(); storeDocs(s, [A, B], s.keys()); expect(readDocs(s)).toEqual({docs: [A, B], legacy: false}); }); it('der Index trägt KEINEN Text — der liegt je Dokument unter eigenem Schlüssel', () => { const s = mem(); storeDocs(s, [A], s.keys()); expect(s.getItem(LS_DOCS)).not.toContain('- a'); expect(s.getItem(docTextKey('example'))).toBe('- a'); }); it('Altformat (Texte im Array) wird gelesen und als legacy gemeldet', () => { const s = mem({[LS_DOCS]: JSON.stringify([A, B])}); const r = readDocs(s); expect(r.legacy).toBe(true); expect(r.docs).toEqual([A, B]); }); it('ein fehlender Text-Schlüssel kostet nur diesen einen Text, nicht die Liste', () => { const s = mem(); storeDocs(s, [A, B], s.keys()); s.removeItem(docTextKey('k1')); const r = readDocs(s); expect(r.docs.map(d => d.id)).toEqual(['example', 'k1']); expect(r.docs[1].text).toBe(''); expect(r.docs[0].text).toBe('- a'); }); it('kaputter oder unbrauchbarer Index -> null (Beispiel-Pfad des Aufrufers)', () => { expect(readDocs(mem({[LS_DOCS]: '{kaputt'}))).toBe(null); expect(readDocs(mem({[LS_DOCS]: '"kein Array"'}))).toBe(null); expect(readDocs(mem({[LS_DOCS]: '[]'}))).toBe(null); expect(readDocs(mem({[LS_DOCS]: JSON.stringify([{name: 'ohne id'}])}))).toBe(null); expect(readDocs(mem())).toBe(null); }); it('unveränderte Schlüssel werden nicht neu geschrieben', () => { const s = mem(); storeDocs(s, [A, B], s.keys()); const vorher = s.writes(); storeDocs(s, [A, B], s.keys()); /* nichts geändert */ expect(s.writes()).toBe(vorher); storeDocs(s, [A, {...B, text: '- b2'}], s.keys()); /* ein Text geändert */ expect(s.writes()).toBe(vorher + 1); }); it('Texte gelöschter Dokumente werden abgeräumt, fremde Schlüssel bleiben', () => { const s = mem({'werkbaum-ui': '{}'}); storeDocs(s, [A, B], s.keys()); storeDocs(s, [A], s.keys()); /* B gelöscht */ expect(s.getItem(docTextKey('k1'))).toBe(null); expect(s.getItem('werkbaum-ui')).toBe('{}'); }); it('storeDocText schreibt Text-Schlüssel UND Spiegel — und nur bei Änderung', () => { const s = mem(); storeDocText(s, 'example', '- neu'); expect(s.getItem(docTextKey('example'))).toBe('- neu'); expect(s.getItem(LS_SRC)).toBe('- neu'); const vorher = s.writes(); storeDocText(s, 'example', '- neu'); expect(s.writes()).toBe(vorher); }); it('ids mit Doppelpunkten und URLs (live:/url:) tragen als Schlüssel', () => { const live = {id: 'live:https://w.example/api/v1/documents/abc', name: 'Plan', text: '- x'}; const s = mem(); storeDocs(s, [live], s.keys()); expect(readDocs(s).docs).toEqual([live]); expect(s.getItem(DOC_TEXT_PREFIX + live.id)).toBe('- x'); }); it('fehlender Name fällt auf die id zurück, Quota-Fehler laufen zum Aufrufer', () => { const s = mem({[LS_DOCS]: JSON.stringify([{id: 'x'}])}); expect(readDocs(s).docs[0].name).toBe('x'); const voll = {getItem: () => null, setItem: () => { throw new Error('QuotaExceeded'); }, removeItem: () => {}}; expect(() => storeDocs(voll, [A], [])).toThrow(); }); }); describe('isDocKey — Fremd-Tab-Erkennung (D84)', () => { it('erkennt Index, aktives Dokument, Spiegel und Text-Schlüssel', () => { expect(isDocKey(LS_DOCS)).toBe(true); expect(isDocKey('werkbaum-active')).toBe(true); expect(isDocKey(LS_SRC)).toBe(true); expect(isDocKey(DOC_TEXT_PREFIX + 'live:https://x/api/v1/documents/a')).toBe(true); }); it('fremde Schlüssel und Ansichts-Zustand sind keine Dokument-Schlüssel', () => { expect(isDocKey('werkbaum-ui')).toBe(false); expect(isDocKey('werkbaum-lang')).toBe(false); expect(isDocKey('werkbaum-snaps')).toBe(false); expect(isDocKey(null)).toBe(false); }); });