fix(v5): kein stilles Scheitern mehr — Rip-Start meldet unerreichbare Ablage, Kern-Neustart verdrahtet das Fenster neu, eine Wache-Kette je Laufwerk, Rettungs-Abbild überlebt, Titel-Auftrag schlägt „unbekannt"

WAS:
- pipeline.starten fängt jede Ausnahme und meldet sie als Fehler des
  Laufwerks; vor dem Start eine Schreibprobe am Zielordner mit Klartext
  („nicht erreichbar … NAS verbunden?"). Vorher warf mkdirSync bei toter
  Ablage aus starten() heraus, der Aufrufer schluckte es: kein Fehler im
  Fenster, keine Protokollzeile, der Dialog ging zu (Sonde 10).
- kern/index.ts: unbehandelte Promise-Ablehnungen landen im Protokoll und
  im Fenster (gemessen: ein Electron-utilityProcess stirbt daran nicht,
  er schweigt); echte Ausnahmen werden protokolliert, der Kern endet
  kontrolliert, der Haupt startet ihn neu. Die Einlege-Kette hat ein
  Fangnetz.
- kernstart.ts spannt nach einem Kern-Neustart den Fenster-Port neu auf;
  das Fenster schließt den toten Port und meldet den Neustart als Zeile.
  Vorher blieb es taub, bis Rippy komplett neu gestartet wurde.
- wache.runde() löscht den geplanten Timer, bevor es einen neuen setzt —
  jeder Auswurf legte sonst eine weitere 3-s-Kette an (Sonde 3).
- beenden() räumt den leeren Roh-Ordner, verschont aber rettung.iso und
  den Bericht (RETTUNG_DATEIEN).
- Typ „unbekannt" heißt nur ohne Titel-Auftrag ISO; mit Auftrag rippt
  makemkvcon, der Typ folgt der Größe (Blu-ray/DVD) für Preset und Ordner.
WARUM: Durchsicht 12.09.2026, Funde F2, F4, F6, F8, F10 — jeder mit Test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-12 21:27:47 +02:00
co-authored by Claude Opus 5
parent 12b9720386
commit 8c4456d005
7 changed files with 247 additions and 16 deletions
+70
View File
@@ -259,3 +259,73 @@ describe('Pipeline — die Rip-Phase (5.4.0)', () => {
expect(readdirSync(ordner)).toEqual(['alt.mkv'])
})
})
// ── 5.7.0: Funde der Durchsicht vom 12.09.2026 ─────────────────────────
describe('Pipeline — Durchsicht 5.7.0', () => {
// F2: Vorher warf mkdirSync aus starten() heraus, der Aufrufer schluckte
// die Ablehnung — kein Fehler im Fenster, keine Zeile im Protokoll
// (Sonde 10). Jetzt: eine Fehler-Meldung, keine Ausnahme.
it('F2: Ablage nicht erreichbar → Fehler-Meldung statt stiller Ausnahme', async () => {
// Ein Laufwerksbuchstabe, den es nicht gibt — wie eine NAS, die gerade
// weg ist. (Unter Linux ein Ort, an dem niemand anlegen darf.)
const weg = process.platform === 'win32' ? 'Q:\\rippy-nicht-da' : '/proc/rippy-nicht-da'
const { pipeline, meldungen, endet } = umgebung({ ablage: () => weg })
await pipeline.starten('G', AUFTRAG)
expect(meldungen).toHaveLength(1)
expect(meldungen[0].phase).toBe('fehler')
expect(meldungen[0].text).toContain('nicht erreichbar')
expect(meldungen[0].text).toContain(weg)
expect(endet).toHaveLength(0) // kein Vorgang angelegt, also keiner beendet
expect(pipeline.laeuft('G')).toBe(false)
})
it('F2: eine Ausnahme mitten im Lauf wird zur Fehler-Meldung des Laufwerks', async () => {
plan.schritte = [{ status: 'success', datei: 'title_t00.mkv' }]
const { pipeline, meldungen } = umgebung({
vorgangBeginnt: () => {
throw new Error('Datenbank gesperrt')
},
})
await expect(pipeline.starten('G', AUFTRAG)).resolves.toBeUndefined()
expect(meldungen.at(-1)!.phase).toBe('fehler')
expect(meldungen.at(-1)!.text).toContain('Datenbank gesperrt')
expect(pipeline.laeuft('G')).toBe(false)
})
// F10: Ein Titel-Auftrag auf einer unbekannt gemeldeten Disc ist ein
// Video-Rip, kein ISO — makemkvcon hat die Titel ja gerade geliefert.
it('F10: Typ unbekannt MIT Titel-Auftrag rippt mit MakeMKV (Typ nach Größe)', async () => {
plan.schritte = [{ status: 'success', datei: 'title_t00.mkv' }]
const { pipeline, meldungen, eingereiht } = umgebung({ geraet: () => ({ ...GERAET, typ: 'unknown' }) })
await pipeline.starten('G', [AUFTRAG[0]])
expect(meldungen.some((m) => m.text.includes('1:1-Abbild'))).toBe(false)
expect(eingereiht).toHaveLength(1)
expect(eingereiht[0].discTyp).toBe('bluray')
})
// F8: Der Roh-Ordner ohne brauchbare Datei wird geräumt — das Rettungs-
// Abbild aber nie: dafür wurde die Disc stundenlang gelesen.
it('F8: rettung.iso überlebt einen leer ausgegangenen Rip aus dem Abbild', async () => {
plan.schritte = [{ status: 'error', fehler: 'MakeMKV endete mit Code 1' }, { status: 'error', fehler: 'MakeMKV endete mit Code 1' }]
const { pipeline, ablage, meldungen } = umgebung({
rettungsAbbild: async (_geraet, ziel) => {
writeFileSync(ziel, 'ABBILD')
writeFileSync(`${ziel}.rettung.txt`, 'Bericht')
return { status: 'success', bytes: 6, unlesbareSektoren: 1, stellen: [{ offset: 0, bytes: 2048 }] }
},
})
// Erst ein normaler Rip, der Lesefehler sieht — nur dann gibt es die Rettung.
plan.schritte = [{ status: 'error', fehler: 'Lesefehler', lesefehler: true }]
await pipeline.starten('G', [AUFTRAG[0]])
expect(pipeline.rettungGrund('G')).not.toBe('')
plan.schritte = [{ status: 'error', fehler: 'MakeMKV endete mit Code 1' }]
await pipeline.rettungStarten('G')
const roh = join(ablage, 'roh')
const ordner = readdirSync(roh)
expect(ordner.length).toBeGreaterThan(0)
const inhalt = ordner.flatMap((o) => readdirSync(join(roh, o)))
expect(inhalt).toContain('rettung.iso')
expect(inhalt).toContain('rettung.iso.rettung.txt')
expect(meldungen.at(-1)!.text).toContain('Rettungs-Abbild bleibt')
})
})