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>
120 lines
4.2 KiB
TypeScript
120 lines
4.2 KiB
TypeScript
// Tests der Disc-Wache — die Übergangs-Logik pur, und der Fehlerpfad:
|
|
// Eine gescheiterte Runde darf den Zustand NICHT löschen (R2) und muss
|
|
// gemeldet werden (R4).
|
|
import { describe, expect, it } from 'vitest'
|
|
import type { GeraeteInfo } from '../src/gemeinsam/nachrichten'
|
|
import { DiscWache, laufwerksUnterschiede } from '../src/kern/laufwerk/wache'
|
|
|
|
function geraet(id: string, status: GeraeteInfo['status'], typ: GeraeteInfo['typ'] = 'unknown'): GeraeteInfo {
|
|
return {
|
|
id,
|
|
name: `Laufwerk ${id}:`,
|
|
typ,
|
|
pfad: `\\\\.\\${id}:`,
|
|
status,
|
|
modell: '',
|
|
seriennummer: '',
|
|
groesseBytes: 0,
|
|
grund: '',
|
|
}
|
|
}
|
|
|
|
function alsMap(...geraete: GeraeteInfo[]): Map<string, GeraeteInfo> {
|
|
return new Map(geraete.map((g) => [g.id, g]))
|
|
}
|
|
|
|
describe('laufwerksUnterschiede', () => {
|
|
it('empty → ready ist eine eingelegte Disc', () => {
|
|
const ereignisse = laufwerksUnterschiede(alsMap(geraet('G', 'empty')), alsMap(geraet('G', 'ready', 'bluray')))
|
|
expect(ereignisse).toEqual([{ art: 'disc-eingelegt', geraet: expect.objectContaining({ id: 'G' }) }])
|
|
})
|
|
|
|
it('ready → empty ist eine entnommene Disc', () => {
|
|
const ereignisse = laufwerksUnterschiede(alsMap(geraet('G', 'ready')), alsMap(geraet('G', 'empty')))
|
|
expect(ereignisse).toEqual([{ art: 'disc-entnommen', geraet: expect.objectContaining({ id: 'G' }) }])
|
|
})
|
|
|
|
it('ready → unknown ist KEINE Entnahme — „konnte nicht nachsehen" (R2)', () => {
|
|
const ereignisse = laufwerksUnterschiede(alsMap(geraet('G', 'ready')), alsMap(geraet('G', 'unknown')))
|
|
expect(ereignisse).toEqual([])
|
|
})
|
|
|
|
it('neues Laufwerk mit Disc meldet beides', () => {
|
|
const ereignisse = laufwerksUnterschiede(alsMap(), alsMap(geraet('G', 'ready', 'dvd')))
|
|
expect(ereignisse.map((e) => e.art)).toEqual(['laufwerk-neu', 'disc-eingelegt'])
|
|
})
|
|
|
|
it('verschwundenes Laufwerk wird gemeldet', () => {
|
|
const ereignisse = laufwerksUnterschiede(alsMap(geraet('G', 'empty')), alsMap())
|
|
expect(ereignisse).toEqual([{ art: 'laufwerk-weg', id: 'G' }])
|
|
})
|
|
})
|
|
|
|
describe('DiscWache', () => {
|
|
it('meldet Änderungen und behält bei einem Rundenfehler den Stand (R2/R4)', () => {
|
|
let stand: GeraeteInfo[] = [geraet('G', 'empty')]
|
|
let lesenBricht = false
|
|
const meldungen: string[] = []
|
|
const fehler: string[] = []
|
|
|
|
const wache = new DiscWache(
|
|
() => {
|
|
if (lesenBricht) throw new Error('Win32 kaputt')
|
|
return stand
|
|
},
|
|
(meldung) => meldungen.push(meldung.ereignisse.map((e) => e.art).join(',') || 'stand'),
|
|
(f) => fehler.push(String(f)),
|
|
999_999,
|
|
)
|
|
|
|
wache.starten() // Runde 1: erster Stand
|
|
expect(wache.aktuellerStand()).toHaveLength(1)
|
|
expect(meldungen).toEqual(['laufwerk-neu'])
|
|
|
|
stand = [geraet('G', 'ready', 'bluray')]
|
|
wache.runde() // Runde 2: Disc rein
|
|
expect(meldungen).toEqual(['laufwerk-neu', 'disc-eingelegt'])
|
|
|
|
lesenBricht = true
|
|
wache.runde() // Runde 3: Lesen bricht — Stand bleibt, Fehler gemeldet
|
|
expect(fehler).toHaveLength(1)
|
|
expect(wache.aktuellerStand()).toHaveLength(1)
|
|
expect(wache.aktuellerStand()[0].status).toBe('ready')
|
|
|
|
lesenBricht = false
|
|
wache.runde() // Runde 4: nichts geändert — keine neue Meldung
|
|
expect(meldungen).toHaveLength(2)
|
|
|
|
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)
|
|
})
|
|
})
|