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>
121 lines
5.7 KiB
TypeScript
121 lines
5.7 KiB
TypeScript
// Tests des Rettungs-Abbilds (kern/rip/rettung.ts, 5.5.0) mit einem
|
|
// nachgebauten Gerät: heile Bereiche kommen 1:1, ein Sektor, der einmal
|
|
// zickt, wird beim zweiten Versuch gelesen, was dauerhaft unlesbar ist,
|
|
// wird mit Nullen gefüllt — und NUR das. Dazu Abbruch, unbekannte Größe,
|
|
// der Bericht.
|
|
import { describe, expect, it } from 'vitest'
|
|
import { BLOCK_BYTES, SEKTOR_BYTES, rettungsAbbild, rettungsBericht, stelleHinzu, type UnlesbareStelle } from '../src/kern/rip/rettung'
|
|
|
|
/** Ein Gerät mit `groesse` Bytes (Muster: Byte = Position mod 251), dauerhaft
|
|
* kaputten Sektoren und solchen, die genau `zickt`-mal scheitern. */
|
|
function geraet(groesse: number, kaputt: number[], zickt: Record<number, number> = {}) {
|
|
const daten = Buffer.alloc(groesse)
|
|
for (let i = 0; i < groesse; i++) daten[i] = i % 251
|
|
const versuche: Array<[number, number]> = []
|
|
const restZicken = { ...zickt }
|
|
const leser = async (puffer: Buffer, laenge: number, position: number): Promise<number> => {
|
|
versuche.push([position, laenge])
|
|
const ersterSektor = Math.floor(position / SEKTOR_BYTES)
|
|
const letzterSektor = Math.floor((position + laenge - 1) / SEKTOR_BYTES)
|
|
for (let s = ersterSektor; s <= letzterSektor; s++) {
|
|
if (kaputt.includes(s)) throw new Error(`I/O-Fehler Sektor ${s}`)
|
|
if ((restZicken[s] ?? 0) > 0) {
|
|
restZicken[s] -= 1
|
|
throw new Error(`zickt Sektor ${s}`)
|
|
}
|
|
}
|
|
daten.copy(puffer, 0, position, position + laenge)
|
|
return laenge
|
|
}
|
|
const teile: Buffer[] = []
|
|
const schreiber = async (puffer: Buffer, laenge: number): Promise<void> => {
|
|
teile.push(Buffer.from(puffer.subarray(0, laenge)))
|
|
}
|
|
return { daten, leser, schreiber, ausgabe: () => Buffer.concat(teile), versuche }
|
|
}
|
|
|
|
describe('rettungsAbbild', () => {
|
|
it('eine heile Disc kommt 1:1 — ohne feines Lesen, ohne Nullen', async () => {
|
|
const g = geraet(2 * BLOCK_BYTES + 5000, [])
|
|
const e = await rettungsAbbild({ leser: g.leser, schreiber: g.schreiber, erwarteteBytes: g.daten.length })
|
|
expect(e.status).toBe('success')
|
|
expect(e.bytes).toBe(g.daten.length)
|
|
expect(e.unlesbareSektoren).toBe(0)
|
|
expect(g.ausgabe().equals(g.daten)).toBe(true)
|
|
// Drei Lesevorgänge: zwei volle Blöcke und der Rest.
|
|
expect(g.versuche).toHaveLength(3)
|
|
})
|
|
it('zwei dauerhaft kaputte Sektoren werden genullt — der Rest bleibt', async () => {
|
|
const kaputt = [600, 601] // im zweiten Block (Block = 512 Sektoren)
|
|
const g = geraet(3 * BLOCK_BYTES, kaputt)
|
|
const fortschritte: number[] = []
|
|
const e = await rettungsAbbild({ leser: g.leser, schreiber: g.schreiber, erwarteteBytes: g.daten.length, aufFortschritt: (p) => fortschritte.push(p) })
|
|
expect(e.status).toBe('success')
|
|
expect(e.unlesbareSektoren).toBe(2)
|
|
expect(e.stellen).toEqual<UnlesbareStelle[]>([{ offset: 600 * SEKTOR_BYTES, bytes: 2 * SEKTOR_BYTES }])
|
|
const erwartet = Buffer.from(g.daten)
|
|
erwartet.fill(0, 600 * SEKTOR_BYTES, 602 * SEKTOR_BYTES)
|
|
expect(g.ausgabe().equals(erwartet)).toBe(true)
|
|
expect(fortschritte[fortschritte.length - 1]).toBe(100)
|
|
})
|
|
it('ein Sektor, der einmal zickt, wird beim zweiten Versuch gelesen — keine Null', async () => {
|
|
const g = geraet(BLOCK_BYTES, [], { 100: 1 })
|
|
const e = await rettungsAbbild({ leser: g.leser, schreiber: g.schreiber, erwarteteBytes: g.daten.length })
|
|
expect(e.status).toBe('success')
|
|
expect(e.unlesbareSektoren).toBe(0)
|
|
expect(g.ausgabe().equals(g.daten)).toBe(true)
|
|
})
|
|
it('zickt er öfter als es Versuche gibt, wird er genullt', async () => {
|
|
const g = geraet(BLOCK_BYTES, [], { 100: 10 })
|
|
const e = await rettungsAbbild({ leser: g.leser, schreiber: g.schreiber, erwarteteBytes: g.daten.length, versuche: 2 })
|
|
expect(e.unlesbareSektoren).toBe(1)
|
|
expect(e.stellen).toEqual([{ offset: 100 * SEKTOR_BYTES, bytes: SEKTOR_BYTES }])
|
|
})
|
|
it('Abbruch: cancelled mit dem Stand bis dahin', async () => {
|
|
const g = geraet(4 * BLOCK_BYTES, [])
|
|
const steuerung = new AbortController()
|
|
let geschrieben = 0
|
|
const schreiber = async (p: Buffer, n: number): Promise<void> => {
|
|
await g.schreiber(p, n)
|
|
geschrieben += 1
|
|
if (geschrieben === 2) steuerung.abort()
|
|
}
|
|
const e = await rettungsAbbild({ leser: g.leser, schreiber, erwarteteBytes: g.daten.length, abbruch: steuerung.signal })
|
|
expect(e.status).toBe('cancelled')
|
|
expect(e.bytes).toBe(2 * BLOCK_BYTES)
|
|
})
|
|
it('unbekannte Größe: Fehler, kein Abbild', async () => {
|
|
const g = geraet(10, [])
|
|
const e = await rettungsAbbild({ leser: g.leser, schreiber: g.schreiber, erwarteteBytes: 0 })
|
|
expect(e.status).toBe('error')
|
|
})
|
|
it('ein Schreibfehler bricht mit Stand ab', async () => {
|
|
const g = geraet(2 * BLOCK_BYTES, [])
|
|
const schreiber = async (): Promise<void> => {
|
|
throw new Error('ENOSPC')
|
|
}
|
|
const e = await rettungsAbbild({ leser: g.leser, schreiber, erwarteteBytes: g.daten.length })
|
|
expect(e.status).toBe('error')
|
|
if (e.status === 'error') expect(e.fehler).toContain('ENOSPC')
|
|
})
|
|
})
|
|
|
|
describe('stelleHinzu / rettungsBericht', () => {
|
|
it('fasst angrenzende Bereiche zusammen', () => {
|
|
const stellen: UnlesbareStelle[] = []
|
|
stelleHinzu(stellen, 0, 2048)
|
|
stelleHinzu(stellen, 2048, 2048)
|
|
stelleHinzu(stellen, 8192, 2048)
|
|
expect(stellen).toEqual([
|
|
{ offset: 0, bytes: 4096 },
|
|
{ offset: 8192, bytes: 2048 },
|
|
])
|
|
})
|
|
it('der Bericht nennt Stand, Sektoren, Anteil und die Stellen', () => {
|
|
const text = rettungsBericht('G:', { status: 'success', bytes: 1_000_000, unlesbareSektoren: 2, stellen: [{ offset: 4096, bytes: 4096 }] }, 1_000_000)
|
|
expect(text).toContain('Stand: vollständig')
|
|
expect(text).toContain('Unlesbare Sektoren: 2 (0.4096 %) an 1 Stellen')
|
|
expect(text).toContain('ab Byte 4096: 4096 Bytes genullt')
|
|
})
|
|
})
|