// Tests für kern/speicher/db.ts — laufen mit dem System-Node (node:sqlite // gibt es dort wie in Electrons Node, KONZEPT-WINDOWS.md § 3.4). import { mkdtempSync, rmSync, existsSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { Datenbank, SCHEMA_VERSION } from '../src/kern/speicher/db' let aufraeumen: Array<() => void> = [] afterEach(() => { // LIFO — zuletzt Registriertes zuerst: die Datenbank muss ZU sein, bevor // ihr Ordner gelöscht wird (Windows verweigert das Löschen offener Dateien). for (const weg of aufraeumen.splice(0).reverse()) weg() }) function frischerPfad(): string { const ordner = mkdtempSync(join(tmpdir(), 'rippy-db-test-')) aufraeumen.push(() => rmSync(ordner, { recursive: true, force: true })) return join(ordner, 'unterordner', 'rippy.db') } describe('Datenbank', () => { it('legt Datei samt fehlender Ordner an und meldet Schema-Version', () => { const pfad = frischerPfad() const db = new Datenbank(pfad) aufraeumen.push(() => db.schliessen()) expect(existsSync(pfad)).toBe(true) expect(db.schemaVersion()).toBe(SCHEMA_VERSION) }) it('schreibt und liest eine Einstellung', () => { const db = new Datenbank(':memory:') aufraeumen.push(() => db.schliessen()) db.einstellungSchreiben('ablage', 'E:\\Rippy') expect(db.einstellungLesen('ablage')).toBe('E:\\Rippy') }) it('überschreibt eine vorhandene Einstellung', () => { const db = new Datenbank(':memory:') aufraeumen.push(() => db.schliessen()) db.einstellungSchreiben('ablage', 'C:\\alt') db.einstellungSchreiben('ablage', 'E:\\neu') expect(db.einstellungLesen('ablage')).toBe('E:\\neu') }) it('unbekannter Schlüssel ist null, kein Fehler und kein Leerwert', () => { // R2: „nicht gefunden" ist eine Aussage (null) — kein erfundener // Leerstring, der als Wert gelesen würde. const db = new Datenbank(':memory:') aufraeumen.push(() => db.schliessen()) expect(db.einstellungLesen('gibt-es-nicht')).toBeNull() }) it('bleibt über Schließen und Neuöffnen erhalten', () => { const pfad = frischerPfad() const erste = new Datenbank(pfad) erste.einstellungSchreiben('letzter_start', '2026-08-30T12:00:00Z') erste.schliessen() const zweite = new Datenbank(pfad) aufraeumen.push(() => zweite.schliessen()) expect(zweite.einstellungLesen('letzter_start')).toBe('2026-08-30T12:00:00Z') }) it('wirft bei nicht anlegbarem Pfad, statt still weiterzumachen', () => { // R4: Der Aufrufer (kern/index.ts) fängt das und MELDET es — aber die // Datenbank selbst darf so einen Zustand nie verschlucken. // NUL ist unter Windows als Ordnername unzulässig; unter Linux (CI) // scheitert das Anlegen unterhalb einer Datei genauso zuverlässig. const kaputt = join(process.platform === 'win32' ? 'C:\\NUL<>:' : '/dev/null/x', 'rippy.db') expect(() => new Datenbank(kaputt)).toThrow() }) }) // ── Vorgänge (5.4.0): welcher Roh-Ordner gehört zu welchem Rip ────────── describe('Datenbank — Vorgänge', () => { const vorgang = { id: 'bluray-2026-09-01T20-00-00', laufwerk: 'G', typ: 'bluray', titel: 'Spartacus', jahr: 2010, phase: 'rippt' as const, begonnen: '2026-09-01T20:00:00.000Z', beendet: null, rohOrdner: 'E:\Rippy\roh\bluray-2026-09-01T20-00-00', rohBytes: 0, rohGeloeschtAm: null, zielOrdner: '', fertigBytes: 0, komprimiert: false, preset: '', fehler: '', auftrag: '{}', lesefehler: false, fertigeDateien: '[]', } it('anlegen, lesen, aktualisieren (nur die genannten Felder), listen, löschen', () => { const db = new Datenbank(':memory:') aufraeumen.push(() => db.schliessen()) db.vorgangAnlegen(vorgang) expect(db.vorgangLesen(vorgang.id)).toEqual(vorgang) db.vorgangAktualisieren(vorgang.id, { phase: 'fertig', komprimiert: true, fertigBytes: 4800, beendet: '2026-09-01T21:00:00.000Z' }) const neu = db.vorgangLesen(vorgang.id)! expect(neu.phase).toBe('fertig') expect(neu.komprimiert).toBe(true) expect(neu.fertigBytes).toBe(4800) expect(neu.titel).toBe('Spartacus') expect(neu.rohGeloeschtAm).toBeNull() db.vorgangAnlegen({ ...vorgang, id: 'dvd-2026-09-02T10-00-00', begonnen: '2026-09-02T10:00:00.000Z' }) expect(db.vorgangListe().map((v) => v.id)).toEqual(['dvd-2026-09-02T10-00-00', vorgang.id]) db.vorgangLoeschen(vorgang.id) expect(db.vorgangLesen(vorgang.id)).toBeNull() expect(db.vorgangAktualisieren('gibt-es-nicht', { phase: 'fertig' })).toBeUndefined() }) it('Schema 3 → 4: eine Bestands-Datenbank bekommt die Tabelle ohne Datenverlust', () => { const pfad = frischerPfad() const alt = new Datenbank(pfad) alt.einstellungSchreiben('ablage', 'E:\Rippy') alt.schliessen() const neu = new Datenbank(pfad) aufraeumen.push(() => neu.schliessen()) expect(neu.einstellungLesen('ablage')).toBe('E:\Rippy') expect(neu.vorgangListe()).toEqual([]) expect(neu.schemaVersion()).toBe(SCHEMA_VERSION) }) })