// Tests der Verkettung Rip → Kompression → Ablage — mit nachgebildeten // Werkzeug-Läufen. Die Regeln: Kompression nur wenn gewollt UND HandBrake // da; ein Kompressions-Fehler lässt den verlustfreien Rip STEHEN und sagt // wo er liegt; Statusmeldungen tragen die Phase. import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import type { RipStatus } from '../src/gemeinsam/nachrichten' const ripErgebnis = vi.hoisted(() => ({ wert: { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: false } as unknown, })) const encodeErgebnis = vi.hoisted(() => ({ wert: { status: 'success', ausgabe: 'R:\\fertig\\a.mkv' } as unknown, })) vi.mock('../src/kern/rip/makemkv', () => ({ rippen: vi.fn(async () => ripErgebnis.wert), })) vi.mock('../src/kern/komprimieren/handbrake', () => ({ komprimieren: vi.fn(async () => encodeErgebnis.wert), })) import { Pipeline, type PipelineUmgebung } from '../src/kern/ablauf/pipeline' let wegwerf: string[] = [] afterEach(() => { for (const ordner of wegwerf.splice(0)) rmSync(ordner, { recursive: true, force: true }) }) function umgebung(überschreiben: Partial = {}): { pipeline: Pipeline meldungen: RipStatus[] } { const ablage = mkdtempSync(join(tmpdir(), 'rippy-pipe-')) wegwerf.push(ablage) const meldungen: RipStatus[] = [] const pipeline = new Pipeline({ makemkvcon: () => 'C:\\mk\\makemkvcon64.exe', handbrake: () => 'C:\\hb\\HandBrakeCLI.exe', flac: () => '', geraet: () => ({ id: 'G', name: 'Laufwerk G:', typ: 'bluray', pfad: '\\\\.\\G:', status: 'ready', modell: '', seriennummer: '', groesseBytes: 33e9, grund: '', }), ablage: () => ablage, einstellungen: () => ({}), melden: (s) => meldungen.push(s), jetzt: () => new Date('2026-08-30T12:00:00Z'), ...überschreiben, }) return { pipeline, meldungen } } describe('Pipeline', () => { it('kettet Rip → Kompression → Ablage und meldet die Phasen', async () => { ripErgebnis.wert = { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: false } encodeErgebnis.wert = { status: 'success', ausgabe: 'R:\\fertig\\a.mkv' } const { pipeline, meldungen } = umgebung() await pipeline.starten('G') const phasen = meldungen.map((m) => m.phase) expect(phasen[0]).toBe('rippt') expect(phasen).toContain('komprimiert') expect(phasen).toContain('legt-ab') expect(phasen.at(-1)).toBe('fertig') expect(meldungen.at(-1)?.dateien).toEqual(['R:\\fertig\\a.mkv']) }) it('Kompression abgewählt: Roh-Rip ist das Ergebnis, mit Begründung', async () => { ripErgebnis.wert = { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: false } const { pipeline, meldungen } = umgebung({ einstellungen: () => ({ transcodePresetBluray: 'keine' }), }) await pipeline.starten('G') const letzte = meldungen.at(-1) expect(letzte?.phase).toBe('fertig') expect(letzte?.text).toContain('abgewählt') expect(letzte?.dateien).toEqual(['R:\\roh\\a.mkv']) }) it('ohne HandBrake bleibt es beim Roh-Rip — mit klarem Satz', async () => { ripErgebnis.wert = { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: false } const { pipeline, meldungen } = umgebung({ handbrake: () => '' }) await pipeline.starten('G') expect(meldungen.at(-1)?.text).toContain('HandBrakeCLI nicht gefunden') }) it('Kompressions-Fehler: Roh bleibt stehen und wird GENANNT', async () => { ripErgebnis.wert = { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: false } encodeErgebnis.wert = { status: 'error', fehler: 'HandBrake endete mit Code 3' } const { pipeline, meldungen } = umgebung() await pipeline.starten('G') const letzte = meldungen.at(-1) expect(letzte?.phase).toBe('fehler') expect(letzte?.text).toContain('bleibt stehen') expect(letzte?.dateien).toEqual(['R:\\roh\\a.mkv']) }) it('Lesefehler überleben bis in die Fertig-Meldung (Rückgabewert-0-Falle)', async () => { ripErgebnis.wert = { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: true } encodeErgebnis.wert = { status: 'success', ausgabe: 'R:\\fertig\\a.mkv' } const { pipeline, meldungen } = umgebung() await pipeline.starten('G') const letzte = meldungen.at(-1) expect(letzte?.lesefehler).toBe(true) expect(letzte?.text).toContain('nicht lesen') }) it('leeres Laufwerk: ein Satz statt makemkvcon (aber unknown rippt!)', async () => { const { pipeline, meldungen } = umgebung({ geraet: () => ({ id: 'G', name: 'Laufwerk G:', typ: 'unknown', pfad: '\\\\.\\G:', status: 'empty', modell: '', seriennummer: '', groesseBytes: 0, grund: '', }), }) await pipeline.starten('G') expect(meldungen[0].phase).toBe('fehler') expect(meldungen[0].text).toContain('Kein Datenträger') }) it('NFO landet im fertigen Ordner', async () => { ripErgebnis.wert = { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: false } encodeErgebnis.wert = { status: 'success', ausgabe: 'R:\\fertig\\a.mkv' } const ablage = mkdtempSync(join(tmpdir(), 'rippy-pipe-')) wegwerf.push(ablage) const meldungen: RipStatus[] = [] const pipeline = new Pipeline({ makemkvcon: () => 'x', handbrake: () => 'y', flac: () => '', geraet: () => ({ id: 'G', name: 'Laufwerk G:', typ: 'dvd', pfad: '\\\\.\\G:', status: 'ready', modell: '', seriennummer: '', groesseBytes: 4e9, grund: '', }), ablage: () => ablage, einstellungen: () => ({}), melden: (s) => meldungen.push(s), jetzt: () => new Date('2026-08-30T12:00:00Z'), }) await pipeline.starten('G') expect(meldungen.at(-1)?.phase).toBe('fertig') const { existsSync, readdirSync } = await import('node:fs') const filme = join(ablage, 'Filme') const ordner = readdirSync(filme) expect(ordner).toHaveLength(1) expect(existsSync(join(filme, ordner[0], 'movie.nfo'))).toBe(true) }) })