Files
rippy/rippy-windows/test/db.test.ts
T
HitonabiandClaude Fable 5.1 3f7f962dff feat(v5): Kern 5.4.0 — Vorgaenge, Kompressions-Warteschlange, Roh-Aufraeumen, Sprachen je Titel, Erkennung v2
WAS: Der Rip endet, sobald die Roh-Dateien liegen; die Kompression laeuft
danach in einer Warteschlange (eine zur Zeit), das Laufwerk ist frei fuer
die naechste Disc. Jeder Rip ist jetzt ein Datensatz (Tabelle vorgaenge,
Schema 4): Roh-Ordner, Zielordner, Phase, Auftrag als JSON. Darauf bauen:
Roh-Aufraeumen nach Regel (Vorgabe 7 Tage, nur bei gelungener Kompression
und nur wenn jede fertige Datei da und nicht leer ist), Wiederholen ab
Kompression, Aufraeumen halber Dateien nach Abbruch/Fehler, Wiederaufnahme
nach Neustart. Unkomprimierte Rips (4K, kein HandBrake) werden in den
Zielordner VERSCHOBEN statt unter roh\ liegen zu bleiben.
Sprachen je Titel reisen vom Info-Lauf (SINFO) bis in HandBrake; fremder
Ton schaltet den deutschen Untertitel vor (--subtitle-default=1). Vor der
Kompression prueft ein HandBrake-Scan die Roh-Datei. MakeMKVs
Auswahlregel wird gesetzt, wenn keine steht, damit alle Sprachen in die
Roh-Datei kommen. Staffel und erste Folge von Hand schlagen die
Laufzeit-Raterei. Durchgehender Fortschritt, nach Groesse gewichtet.
Erkennung v2: leere TMDb-Eintraege (Stubs) zaehlen nicht, Laufzeit wird
nach dem Info-Lauf gegen TMDb gehalten, Titel-Struktur als Fingerabdruck,
bdmt_deu als zweite Titel-Variante. TMDb liefert Logo, Tagline,
Bewertung, FSK und Besetzung in derselben Anfrage. Laufwerks-Gesundheit
aus dem Windows-Ereignisprotokoll (wevtutil): Disc oder Hardware.

WARUM: Uebergabe-Punkte 1-5, 8-12 des SAVEPOINT vom 01.09.2026 — vom
Commander gutgeheissen (7 Tage / Untertitel vorgewaehlt / entkoppelte
Kompression waren seine Wahl).

GEMESSEN (Regel D): HandBrake 1.11 Sprach-Schalter an einer ffmpeg-
Testdatei (jpn/deu/deu/eng + deu/eng), HandBrake --scan --json Format,
TMDb movie/149 + tv/46296 mit append_to_response, wevtutil /f:xml an
diesem PC (338x cdrom 7, 8x cdrom 11 in 30 Tagen), makemkvcon --minlength,
MakeMKV-Auswahlregel laut forum.makemkv.com t=4386.

348 Tests gruen (vorher 261), Typpruefung und Bau gruen. Oberflaeche
folgt im naechsten Schritt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-01 22:02:03 +02:00

130 lines
5.1 KiB
TypeScript

// 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)
})
})