Files
rippy/rippy-windows/test/automatik.test.ts
T
HitonabiandClaude Fable 5.1 3dc6798c79 feat(v5): 5.5.0 — Kern und Oberfläche aufgeteilt, Übersicht und Einstellungen neu, Erst-Einrichtung mit Poster-Kulisse
Commander-Wünsche vom 02.09.2026 vor der Freigabe von 5.4.0 (die deshalb nie
veröffentlicht wurde):

Kern: kern/index.ts ist nur noch Bootstrap und Router; Dienste in
kern/dienste/ (Einstellungen, Werkzeuge, Laufwerke, Erkennung, Vorgänge,
Speicher, Benachrichtigung, Automatik), Kontext in kern/kontext.ts,
Protokoll-Datei in gemeinsam/protokoll.ts, Platz-Rechnung in gemeinsam/platz.ts.

Oberfläche: fenster/App.tsx ist nur noch der Rahmen; bausteine.tsx, dialoge/,
uebersicht/ (LaufwerkKachel, KompressionsKarte, SpeicherKarte, VerlaufKarte),
einstellungen/ (Karten je Funktion, SprachAuswahl), einrichtung/
(ErstEinrichtung, PosterHintergrund).

Neu: Bibliothek weg, dafür Verlauf; Speicher-und-Bilanz-Karte (statfs);
LED-Fußleiste; MakeMKV-Kopf in die Einstellungen; Sprach-Chips mit Dropdown
statt Allgemeinem Preset; Karten Automatik/Benachrichtigung/Allgemein/Update;
Durchsuchen bei Werkzeugen; Arbeitsordner lokal (Encode dort, dann in die
Ablage); Platz-Prüfung vor dem Rip (Dialog und Kern); Automatik mit Countdown;
Serien-Merker über Discs; Rettungs-Abbild mit Nullen nur nach gesehenen
Lesefehlern (kern/rip/rettung.ts, Phase rettet, iso:-Quelle); Warteschlange
pausieren; Discord/Telegram-Benachrichtigung; Protokoll-Datei je Tag;
Fensterlage 80 % mittig, gemerkt; Erst-Einrichtung in acht Schritten vor dem
Poster-Grid.

Beweise: 413 Tests (neu: speicher, benachrichtigung, automatik, rettung,
fensterlage, protokoll, kleinkram-550), drei Typprüfungen, Klick-Beweis mit
29 Schritten inklusive Erst-Einrichtung (zweiter Start mit
RIPPY_START_BEREICH=einrichtung), Bilder beweise/klick/01…09.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 17:51:15 +02:00

199 lines
9.0 KiB
TypeScript

// Tests der Automatik (kern/dienste/automatik.ts, 5.5.0): Rippys Vorschlag
// als Auftrag, Staffel/Folge aus Disc und Serien-Merker, und der Ablauf
// mit Fake-Uhr — sicher → Titel lesen → Countdown → Rip; unsicher → nur
// sagen, warum; Stopp und Jetzt.
import { describe, expect, it } from 'vitest'
import type { AutomatikStand, GeraeteInfo, KernNachricht, RipAuftragOptionen, RipAuftragTitel, TitelStand } from '../src/gemeinsam/nachrichten'
import type { Protokoll } from '../src/gemeinsam/protokoll'
import { AUTOMATIK_SICHER, AutomatikDienst, auftragAusVorschlag, optionenAus, type AutomatikAbhaengigkeiten } from '../src/kern/dienste/automatik'
import type { KernKontext } from '../src/kern/kontext'
import type { Zuordnung } from '../src/kern/metadaten/zuordnung'
function titel(nr: number, gewaehlt: boolean, rolle: 'hauptfilm' | 'extra' | 'folge' = 'hauptfilm'): TitelStand['titel'][number] {
return { nr, dauerS: 3000 + nr, groesseBytes: 1e9 * (nr + 1), kapitel: 12, gewaehlt, rolle, grund: gewaehlt ? 'Hauptfilm' : 'zu kurz', sprachen: { audio: [], untertitel: [] } }
}
function stand(teile: TitelStand['titel'], unsicher = false, fehler = ''): TitelStand {
return { id: 'G', laeuft: false, titel: teile, einordnung: unsicher ? 'zwei fast gleich lange Titel — bitte nachsehen' : 'Film: ein Hauptfilm', unsicher, fehler }
}
function geraet(typ: GeraeteInfo['typ'] = 'bluray'): GeraeteInfo {
return { id: 'G', name: 'G:', typ, pfad: 'G:\\', status: 'ready', modell: 'Fake', seriennummer: '', groesseBytes: 4e10, grund: '' }
}
function zuordnung(confidence: number): Zuordnung {
return { titel: 'Akira', jahr: 1988, confidence, meta: {} as never }
}
describe('auftragAusVorschlag', () => {
it('nimmt nur die vorausgewählten Titel, mit Rolle, Dauer und Größe — Sprachen leer (Einstellung gilt)', () => {
const auftrag = auftragAusVorschlag(stand([titel(0, true), titel(1, false), titel(2, true, 'extra')]))
expect(auftrag).toEqual<RipAuftragTitel[]>([
{ nr: 0, rolle: 'hauptfilm', dauerS: 3000, groesseBytes: 1e9 },
{ nr: 2, rolle: 'extra', dauerS: 3002, groesseBytes: 3e9 },
])
})
})
describe('optionenAus', () => {
it('ohne alles: nichts vorgeben', () => {
expect(optionenAus(null, null)).toEqual<RipAuftragOptionen>({ staffel: null, ersteFolge: null })
})
it('die Staffel der Disc schlägt den Merker', () => {
expect(optionenAus({ staffel: 2, discNr: 2 }, { staffel: 1, naechsteFolge: 5 })).toEqual({ staffel: 2, ersteFolge: 5 })
})
it('Disc 1 fängt nie beim Merker an — sonst begänne die neue Ausgabe bei Folge 5', () => {
expect(optionenAus({ staffel: null, discNr: 1 }, { staffel: 1, naechsteFolge: 5 })).toEqual({ staffel: 1, ersteFolge: null })
})
it('Disc-Nummer unbekannt oder > 1: ab der gemerkten Folge weiter', () => {
expect(optionenAus({ staffel: null, discNr: null }, { staffel: 1, naechsteFolge: 3 })).toEqual({ staffel: 1, ersteFolge: 3 })
expect(optionenAus({ staffel: null, discNr: 3 }, { staffel: 1, naechsteFolge: 9 })).toEqual({ staffel: 1, ersteFolge: 9 })
})
})
function aufbau(sekunden = 3) {
const nachrichten: AutomatikStand[] = []
const hinweise: string[] = []
const kontext = {
db: null,
dbStatus: { ok: false, fehler: 'Test' },
anFenster: (n: KernNachricht) => {
if (n.art === 'automatik') nachrichten.push(n.stand)
},
anHauptUndFenster: () => {},
hinweis: (t: string) => hinweise.push(t),
fehler: () => {},
protokoll: { schreiben: () => {} } as unknown as Protokoll,
smoke: true,
} as unknown as KernKontext
const gestartet: Array<{ id: string; auftrag: RipAuftragTitel[]; optionen: RipAuftragOptionen }> = []
const benachrichtigt: string[] = []
let tick: (() => void) | null = null
let gestoppt = 0
let an = true
let ripLaeuft = false
let titelStand: TitelStand = stand([titel(0, true), titel(1, false)])
let angaben: { staffel: number | null; discNr: number | null } | null = null
let merker: { staffel: number; naechsteFolge: number } | null = null
const abh: AutomatikAbhaengigkeiten = {
an: () => an,
sekunden: () => sekunden,
ripLaeuft: () => ripLaeuft,
titelLesen: async () => titelStand,
ripStarten: (id, auftrag, optionen) => gestartet.push({ id, auftrag, optionen }),
discAngaben: () => angaben,
serienMerker: () => merker,
benachrichtigen: (t, x) => benachrichtigt.push(`${t}: ${x}`),
takt: (fn) => {
tick = fn
return 1 as unknown as NodeJS.Timeout
},
taktStoppen: () => {
gestoppt += 1
tick = null
},
}
const dienst = new AutomatikDienst(kontext, abh)
return {
dienst,
nachrichten,
hinweise,
gestartet,
benachrichtigt,
tick: () => {
if (tick === null) throw new Error('kein Takt')
tick()
},
gestopptZahl: () => gestoppt,
setAn: (w: boolean) => (an = w),
setRipLaeuft: (w: boolean) => (ripLaeuft = w),
setTitel: (s: TitelStand) => (titelStand = s),
setAngaben: (a: typeof angaben) => (angaben = a),
setMerker: (m: typeof merker) => (merker = m),
}
}
describe('AutomatikDienst', () => {
it('sicher erkannt, nicht gerippt: liest Titel, zählt herunter, startet mit Rippys Vorschlag', async () => {
const a = aufbau(3)
await a.dienst.pruefen(geraet(), zuordnung(0.95), null)
expect(a.nachrichten[0].text).toContain('liest die Titel')
expect(a.nachrichten[1]).toMatchObject({ aktiv: true, sekunden: 3 })
a.tick()
a.tick()
expect(a.nachrichten[a.nachrichten.length - 1]).toMatchObject({ aktiv: true, sekunden: 1 })
expect(a.gestartet).toHaveLength(0)
a.tick()
expect(a.gestartet).toHaveLength(1)
expect(a.gestartet[0].auftrag).toEqual([{ nr: 0, rolle: 'hauptfilm', dauerS: 3000, groesseBytes: 1e9 }])
expect(a.nachrichten[a.nachrichten.length - 1]).toMatchObject({ aktiv: false, text: 'Automatik: Rip gestartet (1 Titel).' })
expect(a.benachrichtigt[0]).toContain('Automatik hat auf Laufwerk G gestartet')
expect(a.gestopptZahl()).toBe(1)
})
it('unsichere Erkennung: nichts passiert von selbst — nur der Grund', async () => {
const a = aufbau()
await a.dienst.pruefen(geraet(), zuordnung(AUTOMATIK_SICHER - 0.01), null)
expect(a.nachrichten).toHaveLength(1)
expect(a.nachrichten[0]).toMatchObject({ aktiv: false })
expect(a.nachrichten[0].text).toContain('nicht sicher genug')
expect(a.gestartet).toHaveLength(0)
})
it('schon gerippt: wartet und nennt das Datum', async () => {
const a = aufbau()
await a.dienst.pruefen(geraet(), zuordnung(0.99), '2026-08-30T10:00:00Z')
expect(a.nachrichten[0].text).toContain('schon gerippt (2026-08-30)')
expect(a.gestartet).toHaveLength(0)
})
it('aus, CD, unbekannte Disc oder laufender Rip: gar nichts', async () => {
const a = aufbau()
a.setAn(false)
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
a.setAn(true)
await a.dienst.pruefen(geraet('cd'), zuordnung(0.99), null)
await a.dienst.pruefen(geraet('unknown'), zuordnung(0.99), null)
a.setRipLaeuft(true)
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
expect(a.nachrichten).toHaveLength(0)
})
it('unsicherer Titel-Lauf (zwei fast gleich lange Titel): kein Countdown, die Einordnung wird gesagt', async () => {
const a = aufbau()
a.setTitel(stand([titel(0, true), titel(1, false)], true))
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
expect(a.nachrichten[a.nachrichten.length - 1].text).toContain('bitte nachsehen')
expect(a.gestartet).toHaveLength(0)
})
it('Stopp beendet den Countdown mit Grund; Jetzt überspringt ihn', async () => {
const a = aufbau(30)
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
a.dienst.stopp('G', 'Automatik gestoppt — du entscheidest.')
expect(a.gestopptZahl()).toBe(1)
expect(a.nachrichten[a.nachrichten.length - 1]).toMatchObject({ aktiv: false, text: 'Automatik gestoppt — du entscheidest.' })
expect(() => a.tick()).toThrow('kein Takt')
// Jetzt braucht einen aktiven Countdown und den gemerkten Titel-Stand.
a.dienst.jetzt('G')
expect(a.gestartet).toHaveLength(0)
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
a.dienst.titelMerken('G', stand([titel(0, true), titel(1, false)]))
a.dienst.jetzt('G')
expect(a.gestartet).toHaveLength(1)
})
it('Serie: Staffel/Folge aus Disc und Merker, Merker nur mit gemerkter Zuordnung', async () => {
const a = aufbau(1)
a.setTitel(stand([titel(0, true, 'folge'), titel(1, true, 'folge')]))
a.setAngaben({ staffel: null, discNr: 2 })
a.setMerker({ staffel: 1, naechsteFolge: 3 })
a.dienst.zuordnungMerken('G', zuordnung(0.99))
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
a.tick()
expect(a.gestartet[0].optionen).toEqual({ staffel: 1, ersteFolge: 3 })
})
it('Disc raus: Countdown weg, Stand weg', async () => {
const a = aufbau(5)
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
expect(a.dienst.stand('G')).not.toBeNull()
a.dienst.discEntnommen('G')
expect(a.dienst.stand('G')).toBeNull()
expect(a.gestopptZahl()).toBe(1)
})
})