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:
co-authored by
Claude Opus 5
parent
12b9720386
commit
8c4456d005
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -88,3 +88,32 @@ describe('DiscWache', () => {
|
||||
wache.stoppen()
|
||||
})
|
||||
})
|
||||
|
||||
// 5.7.0 (Durchsicht F6): runde() von außen — nach dem Auswurf — ersetzt die
|
||||
// geplante Runde, statt eine zweite Takt-Kette anzulegen. Vorher: nach
|
||||
// zwanzig Auswürfen zwanzig Ketten (Sonde 3, neun statt fünf Runden).
|
||||
describe('DiscWache — eine Takt-Kette, egal wie oft runde() von außen kommt', () => {
|
||||
it('drei Aufrufe von außen, danach nur EINE Kette', async () => {
|
||||
let gelesen = 0
|
||||
const wache = new DiscWache(
|
||||
() => {
|
||||
gelesen += 1
|
||||
return []
|
||||
},
|
||||
() => {},
|
||||
() => {},
|
||||
20,
|
||||
)
|
||||
wache.starten()
|
||||
wache.runde()
|
||||
wache.runde()
|
||||
const vorher = gelesen
|
||||
await new Promise((f) => setTimeout(f, 130))
|
||||
wache.stoppen()
|
||||
const runden = gelesen - vorher
|
||||
// Eine Kette im 20-ms-Takt schafft in 130 ms höchstens ~7 Runden; drei
|
||||
// Ketten schafften vorher ~19.
|
||||
expect(runden).toBeLessThanOrEqual(8)
|
||||
expect(runden).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user