feat(v5): 5.5.0 — Kern und Oberfläche aufgeteilt, Übersicht und Einstellungen neu, Erst-Einrichtung mit Poster-Kulisse
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>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
6678c81592
commit
3dc6798c79
@@ -0,0 +1,198 @@
|
||||
// Tests der Automatik (kern/dienste/automatik.ts, 5.5.0): Rippys Vorschlag
|
||||
// als Auftrag, Staffel/Folge aus Disc und Serien-Merker, und der Ablauf
|
||||
// mit Fake-Uhr — sicher → Titel lesen → Countdown → Rip; unsicher → nur
|
||||
// sagen, warum; Stopp und Jetzt.
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { AutomatikStand, GeraeteInfo, KernNachricht, RipAuftragOptionen, RipAuftragTitel, TitelStand } from '../src/gemeinsam/nachrichten'
|
||||
import type { Protokoll } from '../src/gemeinsam/protokoll'
|
||||
import { AUTOMATIK_SICHER, AutomatikDienst, auftragAusVorschlag, optionenAus, type AutomatikAbhaengigkeiten } from '../src/kern/dienste/automatik'
|
||||
import type { KernKontext } from '../src/kern/kontext'
|
||||
import type { Zuordnung } from '../src/kern/metadaten/zuordnung'
|
||||
|
||||
function titel(nr: number, gewaehlt: boolean, rolle: 'hauptfilm' | 'extra' | 'folge' = 'hauptfilm'): TitelStand['titel'][number] {
|
||||
return { nr, dauerS: 3000 + nr, groesseBytes: 1e9 * (nr + 1), kapitel: 12, gewaehlt, rolle, grund: gewaehlt ? 'Hauptfilm' : 'zu kurz', sprachen: { audio: [], untertitel: [] } }
|
||||
}
|
||||
|
||||
function stand(teile: TitelStand['titel'], unsicher = false, fehler = ''): TitelStand {
|
||||
return { id: 'G', laeuft: false, titel: teile, einordnung: unsicher ? 'zwei fast gleich lange Titel — bitte nachsehen' : 'Film: ein Hauptfilm', unsicher, fehler }
|
||||
}
|
||||
|
||||
function geraet(typ: GeraeteInfo['typ'] = 'bluray'): GeraeteInfo {
|
||||
return { id: 'G', name: 'G:', typ, pfad: 'G:\\', status: 'ready', modell: 'Fake', seriennummer: '', groesseBytes: 4e10, grund: '' }
|
||||
}
|
||||
|
||||
function zuordnung(confidence: number): Zuordnung {
|
||||
return { titel: 'Akira', jahr: 1988, confidence, meta: {} as never }
|
||||
}
|
||||
|
||||
describe('auftragAusVorschlag', () => {
|
||||
it('nimmt nur die vorausgewählten Titel, mit Rolle, Dauer und Größe — Sprachen leer (Einstellung gilt)', () => {
|
||||
const auftrag = auftragAusVorschlag(stand([titel(0, true), titel(1, false), titel(2, true, 'extra')]))
|
||||
expect(auftrag).toEqual<RipAuftragTitel[]>([
|
||||
{ nr: 0, rolle: 'hauptfilm', dauerS: 3000, groesseBytes: 1e9 },
|
||||
{ nr: 2, rolle: 'extra', dauerS: 3002, groesseBytes: 3e9 },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('optionenAus', () => {
|
||||
it('ohne alles: nichts vorgeben', () => {
|
||||
expect(optionenAus(null, null)).toEqual<RipAuftragOptionen>({ staffel: null, ersteFolge: null })
|
||||
})
|
||||
it('die Staffel der Disc schlägt den Merker', () => {
|
||||
expect(optionenAus({ staffel: 2, discNr: 2 }, { staffel: 1, naechsteFolge: 5 })).toEqual({ staffel: 2, ersteFolge: 5 })
|
||||
})
|
||||
it('Disc 1 fängt nie beim Merker an — sonst begänne die neue Ausgabe bei Folge 5', () => {
|
||||
expect(optionenAus({ staffel: null, discNr: 1 }, { staffel: 1, naechsteFolge: 5 })).toEqual({ staffel: 1, ersteFolge: null })
|
||||
})
|
||||
it('Disc-Nummer unbekannt oder > 1: ab der gemerkten Folge weiter', () => {
|
||||
expect(optionenAus({ staffel: null, discNr: null }, { staffel: 1, naechsteFolge: 3 })).toEqual({ staffel: 1, ersteFolge: 3 })
|
||||
expect(optionenAus({ staffel: null, discNr: 3 }, { staffel: 1, naechsteFolge: 9 })).toEqual({ staffel: 1, ersteFolge: 9 })
|
||||
})
|
||||
})
|
||||
|
||||
function aufbau(sekunden = 3) {
|
||||
const nachrichten: AutomatikStand[] = []
|
||||
const hinweise: string[] = []
|
||||
const kontext = {
|
||||
db: null,
|
||||
dbStatus: { ok: false, fehler: 'Test' },
|
||||
anFenster: (n: KernNachricht) => {
|
||||
if (n.art === 'automatik') nachrichten.push(n.stand)
|
||||
},
|
||||
anHauptUndFenster: () => {},
|
||||
hinweis: (t: string) => hinweise.push(t),
|
||||
fehler: () => {},
|
||||
protokoll: { schreiben: () => {} } as unknown as Protokoll,
|
||||
smoke: true,
|
||||
} as unknown as KernKontext
|
||||
const gestartet: Array<{ id: string; auftrag: RipAuftragTitel[]; optionen: RipAuftragOptionen }> = []
|
||||
const benachrichtigt: string[] = []
|
||||
let tick: (() => void) | null = null
|
||||
let gestoppt = 0
|
||||
let an = true
|
||||
let ripLaeuft = false
|
||||
let titelStand: TitelStand = stand([titel(0, true), titel(1, false)])
|
||||
let angaben: { staffel: number | null; discNr: number | null } | null = null
|
||||
let merker: { staffel: number; naechsteFolge: number } | null = null
|
||||
const abh: AutomatikAbhaengigkeiten = {
|
||||
an: () => an,
|
||||
sekunden: () => sekunden,
|
||||
ripLaeuft: () => ripLaeuft,
|
||||
titelLesen: async () => titelStand,
|
||||
ripStarten: (id, auftrag, optionen) => gestartet.push({ id, auftrag, optionen }),
|
||||
discAngaben: () => angaben,
|
||||
serienMerker: () => merker,
|
||||
benachrichtigen: (t, x) => benachrichtigt.push(`${t}: ${x}`),
|
||||
takt: (fn) => {
|
||||
tick = fn
|
||||
return 1 as unknown as NodeJS.Timeout
|
||||
},
|
||||
taktStoppen: () => {
|
||||
gestoppt += 1
|
||||
tick = null
|
||||
},
|
||||
}
|
||||
const dienst = new AutomatikDienst(kontext, abh)
|
||||
return {
|
||||
dienst,
|
||||
nachrichten,
|
||||
hinweise,
|
||||
gestartet,
|
||||
benachrichtigt,
|
||||
tick: () => {
|
||||
if (tick === null) throw new Error('kein Takt')
|
||||
tick()
|
||||
},
|
||||
gestopptZahl: () => gestoppt,
|
||||
setAn: (w: boolean) => (an = w),
|
||||
setRipLaeuft: (w: boolean) => (ripLaeuft = w),
|
||||
setTitel: (s: TitelStand) => (titelStand = s),
|
||||
setAngaben: (a: typeof angaben) => (angaben = a),
|
||||
setMerker: (m: typeof merker) => (merker = m),
|
||||
}
|
||||
}
|
||||
|
||||
describe('AutomatikDienst', () => {
|
||||
it('sicher erkannt, nicht gerippt: liest Titel, zählt herunter, startet mit Rippys Vorschlag', async () => {
|
||||
const a = aufbau(3)
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.95), null)
|
||||
expect(a.nachrichten[0].text).toContain('liest die Titel')
|
||||
expect(a.nachrichten[1]).toMatchObject({ aktiv: true, sekunden: 3 })
|
||||
a.tick()
|
||||
a.tick()
|
||||
expect(a.nachrichten[a.nachrichten.length - 1]).toMatchObject({ aktiv: true, sekunden: 1 })
|
||||
expect(a.gestartet).toHaveLength(0)
|
||||
a.tick()
|
||||
expect(a.gestartet).toHaveLength(1)
|
||||
expect(a.gestartet[0].auftrag).toEqual([{ nr: 0, rolle: 'hauptfilm', dauerS: 3000, groesseBytes: 1e9 }])
|
||||
expect(a.nachrichten[a.nachrichten.length - 1]).toMatchObject({ aktiv: false, text: 'Automatik: Rip gestartet (1 Titel).' })
|
||||
expect(a.benachrichtigt[0]).toContain('Automatik hat auf Laufwerk G gestartet')
|
||||
expect(a.gestopptZahl()).toBe(1)
|
||||
})
|
||||
it('unsichere Erkennung: nichts passiert von selbst — nur der Grund', async () => {
|
||||
const a = aufbau()
|
||||
await a.dienst.pruefen(geraet(), zuordnung(AUTOMATIK_SICHER - 0.01), null)
|
||||
expect(a.nachrichten).toHaveLength(1)
|
||||
expect(a.nachrichten[0]).toMatchObject({ aktiv: false })
|
||||
expect(a.nachrichten[0].text).toContain('nicht sicher genug')
|
||||
expect(a.gestartet).toHaveLength(0)
|
||||
})
|
||||
it('schon gerippt: wartet und nennt das Datum', async () => {
|
||||
const a = aufbau()
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), '2026-08-30T10:00:00Z')
|
||||
expect(a.nachrichten[0].text).toContain('schon gerippt (2026-08-30)')
|
||||
expect(a.gestartet).toHaveLength(0)
|
||||
})
|
||||
it('aus, CD, unbekannte Disc oder laufender Rip: gar nichts', async () => {
|
||||
const a = aufbau()
|
||||
a.setAn(false)
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
a.setAn(true)
|
||||
await a.dienst.pruefen(geraet('cd'), zuordnung(0.99), null)
|
||||
await a.dienst.pruefen(geraet('unknown'), zuordnung(0.99), null)
|
||||
a.setRipLaeuft(true)
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
expect(a.nachrichten).toHaveLength(0)
|
||||
})
|
||||
it('unsicherer Titel-Lauf (zwei fast gleich lange Titel): kein Countdown, die Einordnung wird gesagt', async () => {
|
||||
const a = aufbau()
|
||||
a.setTitel(stand([titel(0, true), titel(1, false)], true))
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
expect(a.nachrichten[a.nachrichten.length - 1].text).toContain('bitte nachsehen')
|
||||
expect(a.gestartet).toHaveLength(0)
|
||||
})
|
||||
it('Stopp beendet den Countdown mit Grund; Jetzt überspringt ihn', async () => {
|
||||
const a = aufbau(30)
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
a.dienst.stopp('G', 'Automatik gestoppt — du entscheidest.')
|
||||
expect(a.gestopptZahl()).toBe(1)
|
||||
expect(a.nachrichten[a.nachrichten.length - 1]).toMatchObject({ aktiv: false, text: 'Automatik gestoppt — du entscheidest.' })
|
||||
expect(() => a.tick()).toThrow('kein Takt')
|
||||
// Jetzt braucht einen aktiven Countdown und den gemerkten Titel-Stand.
|
||||
a.dienst.jetzt('G')
|
||||
expect(a.gestartet).toHaveLength(0)
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
a.dienst.titelMerken('G', stand([titel(0, true), titel(1, false)]))
|
||||
a.dienst.jetzt('G')
|
||||
expect(a.gestartet).toHaveLength(1)
|
||||
})
|
||||
it('Serie: Staffel/Folge aus Disc und Merker, Merker nur mit gemerkter Zuordnung', async () => {
|
||||
const a = aufbau(1)
|
||||
a.setTitel(stand([titel(0, true, 'folge'), titel(1, true, 'folge')]))
|
||||
a.setAngaben({ staffel: null, discNr: 2 })
|
||||
a.setMerker({ staffel: 1, naechsteFolge: 3 })
|
||||
a.dienst.zuordnungMerken('G', zuordnung(0.99))
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
a.tick()
|
||||
expect(a.gestartet[0].optionen).toEqual({ staffel: 1, ersteFolge: 3 })
|
||||
})
|
||||
it('Disc raus: Countdown weg, Stand weg', async () => {
|
||||
const a = aufbau(5)
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
expect(a.dienst.stand('G')).not.toBeNull()
|
||||
a.dienst.discEntnommen('G')
|
||||
expect(a.dienst.stand('G')).toBeNull()
|
||||
expect(a.gestopptZahl()).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,112 @@
|
||||
// Tests der Benachrichtigung (kern/dienste/benachrichtigung.ts, 5.5.0):
|
||||
// Was an Discord und Telegram geht (Form laut deren Doku), welche Kanäle
|
||||
// eingerichtet sind, und dass ein Fehlschlag gesagt wird, aber nichts
|
||||
// kippt.
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { KernNachricht } from '../src/gemeinsam/nachrichten'
|
||||
import type { Protokoll } from '../src/gemeinsam/protokoll'
|
||||
import { BenachrichtigungsDienst, discordAnfrage, kanaeleAus, kanaeleEingerichtet, telegramAnfrage } from '../src/kern/dienste/benachrichtigung'
|
||||
import type { EinstellungsDienst } from '../src/kern/dienste/einstellungen'
|
||||
import type { KernKontext } from '../src/kern/kontext'
|
||||
|
||||
function fakeKontext() {
|
||||
const fehler: string[] = []
|
||||
const hinweise: string[] = []
|
||||
const zeilen: string[] = []
|
||||
const nachrichten: KernNachricht[] = []
|
||||
const kontext = {
|
||||
db: null,
|
||||
dbStatus: { ok: false, fehler: 'Test' },
|
||||
anFenster: (n: KernNachricht) => nachrichten.push(n),
|
||||
anHauptUndFenster: (n: KernNachricht) => nachrichten.push(n),
|
||||
hinweis: (t: string) => hinweise.push(t),
|
||||
fehler: (t: string) => fehler.push(t),
|
||||
protokoll: { schreiben: (t: string) => zeilen.push(t) } as unknown as Protokoll,
|
||||
smoke: true,
|
||||
} as unknown as KernKontext
|
||||
return { kontext, fehler, hinweise, zeilen, nachrichten }
|
||||
}
|
||||
|
||||
function fakeEinstellungen(werte: Record<string, string>): EinstellungsDienst {
|
||||
return { lesen: (s: string, vorgabe = '') => werte[s] ?? vorgabe } as unknown as EinstellungsDienst
|
||||
}
|
||||
|
||||
describe('discordAnfrage / telegramAnfrage', () => {
|
||||
it('Discord: POST an den Webhook mit { content, username } — Titel fett, dann Text', () => {
|
||||
const a = discordAnfrage('https://discord.com/api/webhooks/1/abc', 'Rip fertig', 'Akira (1988)')
|
||||
expect(a.url).toBe('https://discord.com/api/webhooks/1/abc')
|
||||
const body = JSON.parse(a.body) as { content: string; username: string }
|
||||
expect(body.content).toBe('**Rip fertig**\nAkira (1988)')
|
||||
expect(body.username).toBe('Rippy')
|
||||
})
|
||||
it('Discord: content bleibt unter 2000 Zeichen', () => {
|
||||
const a = discordAnfrage('https://x', 'T', 'x'.repeat(5000))
|
||||
expect((JSON.parse(a.body) as { content: string }).content.length).toBeLessThanOrEqual(2000)
|
||||
})
|
||||
it('Telegram: bot<TOKEN>/sendMessage mit { chat_id, text }', () => {
|
||||
const a = telegramAnfrage('123:ABC', '42', 'Rip fertig', 'Akira')
|
||||
expect(a.url).toBe('https://api.telegram.org/bot123:ABC/sendMessage')
|
||||
expect(JSON.parse(a.body)).toEqual({ chat_id: '42', text: 'Rip fertig\nAkira' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('kanaeleAus / kanaeleEingerichtet', () => {
|
||||
it('trimmt und erkennt: Discord allein reicht, Telegram nur mit Token UND Chat-ID', () => {
|
||||
expect(kanaeleEingerichtet(kanaeleAus((s) => (s === 'discordWebhook' ? ' https://d ' : '')))).toBe(true)
|
||||
expect(kanaeleEingerichtet(kanaeleAus((s) => (s === 'telegramToken' ? 't' : '')))).toBe(false)
|
||||
expect(kanaeleEingerichtet(kanaeleAus((s) => (s === 'telegramToken' ? 't' : s === 'telegramChatId' ? '1' : '')))).toBe(true)
|
||||
expect(kanaeleEingerichtet(kanaeleAus(() => ''))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('BenachrichtigungsDienst', () => {
|
||||
it('schickt über jeden eingerichteten Kanal und schreibt ins Protokoll', async () => {
|
||||
const { kontext, fehler, zeilen } = fakeKontext()
|
||||
const aufrufe: Array<{ url: string; body: string }> = []
|
||||
const holen = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
aufrufe.push({ url: String(url), body: String(init?.body) })
|
||||
return { status: 204 }
|
||||
}) as unknown as typeof fetch
|
||||
const d = new BenachrichtigungsDienst(kontext, fakeEinstellungen({ discordWebhook: 'https://d', telegramToken: 't', telegramChatId: '9' }), holen)
|
||||
expect(await d.senden('Titel', 'Text')).toEqual([])
|
||||
expect(aufrufe.map((a) => a.url)).toEqual(['https://d', 'https://api.telegram.org/bott/sendMessage'])
|
||||
expect(fehler).toEqual([])
|
||||
expect(zeilen[0]).toContain('Benachrichtigt (Discord, Telegram): Titel')
|
||||
})
|
||||
it('ein HTTP-Fehler wird gesagt (kern-fehler), aber nicht geworfen', async () => {
|
||||
const { kontext, fehler } = fakeKontext()
|
||||
const holen = (async () => ({ status: 404 })) as unknown as typeof fetch
|
||||
const d = new BenachrichtigungsDienst(kontext, fakeEinstellungen({ discordWebhook: 'https://d' }), holen)
|
||||
expect(await d.senden('T', 'x')).toEqual(['Discord: HTTP 404'])
|
||||
expect(fehler).toEqual(['Benachrichtigung Discord: HTTP 404'])
|
||||
})
|
||||
it('ein Netzfehler ebenso', async () => {
|
||||
const { kontext } = fakeKontext()
|
||||
const holen = (async () => {
|
||||
throw new Error('fetch failed')
|
||||
}) as unknown as typeof fetch
|
||||
const d = new BenachrichtigungsDienst(kontext, fakeEinstellungen({ discordWebhook: 'https://d' }), holen)
|
||||
expect(await d.senden('T', 'x')).toEqual(['Discord: fetch failed'])
|
||||
})
|
||||
it('ausgeschaltet: nichts geht raus — außer erzwungen (Testnachricht)', async () => {
|
||||
const { kontext } = fakeKontext()
|
||||
let aufrufe = 0
|
||||
const holen = (async () => {
|
||||
aufrufe += 1
|
||||
return { status: 200 }
|
||||
}) as unknown as typeof fetch
|
||||
const d = new BenachrichtigungsDienst(kontext, fakeEinstellungen({ benachrichtigungen: 'aus', discordWebhook: 'https://d' }), holen)
|
||||
expect(await d.senden('T', 'x')).toEqual([])
|
||||
expect(aufrufe).toBe(0)
|
||||
expect(await d.senden('T', 'x', true)).toEqual([])
|
||||
expect(aufrufe).toBe(1)
|
||||
})
|
||||
it('testen ohne Kanal: sagt, was fehlt; mit Kanal: „Testnachricht verschickt."', async () => {
|
||||
const ohne = fakeKontext()
|
||||
await new BenachrichtigungsDienst(ohne.kontext, fakeEinstellungen({}), (async () => ({ status: 200 })) as unknown as typeof fetch).testen()
|
||||
expect(ohne.fehler[0]).toContain('kein Kanal eingerichtet')
|
||||
const mit = fakeKontext()
|
||||
await new BenachrichtigungsDienst(mit.kontext, fakeEinstellungen({ discordWebhook: 'https://d' }), (async () => ({ status: 200 })) as unknown as typeof fetch).testen()
|
||||
expect(mit.hinweise).toEqual(['Testnachricht verschickt.'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
// Tests der Fensterlage (haupt/fensterlage.ts, 5.5.0): 80 % des
|
||||
// Arbeitsbereichs, mittig, Mindestmaß; gemerkte Lage nur, wenn sie noch auf
|
||||
// einem Bildschirm liegt; Lesen/Schreiben der Datei.
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import { anfangsLage, lageLesen, lageSchreiben, liegtSichtbar, startLage } from '../src/haupt/fensterlage'
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), 'rippy-lage-'))
|
||||
afterAll(() => rmSync(ordner, { recursive: true, force: true }))
|
||||
|
||||
describe('startLage', () => {
|
||||
it('Full HD ohne Taskleiste: 80 % und mittig', () => {
|
||||
expect(startLage({ x: 0, y: 0, width: 1920, height: 1040 })).toEqual({ x: 192, y: 104, width: 1536, height: 832, maximiert: false })
|
||||
})
|
||||
it('kleiner Bildschirm: Mindestmaß 1100 × 720, aber nie größer als der Bildschirm', () => {
|
||||
expect(startLage({ x: 0, y: 0, width: 1280, height: 720 })).toEqual({ x: 90, y: 0, width: 1100, height: 720, maximiert: false })
|
||||
expect(startLage({ x: 0, y: 0, width: 1024, height: 600 })).toEqual({ x: 0, y: 0, width: 1024, height: 600, maximiert: false })
|
||||
})
|
||||
it('Zweitmonitor rechts: der Versatz des Arbeitsbereichs zählt mit', () => {
|
||||
const l = startLage({ x: 1920, y: 0, width: 2560, height: 1400 })
|
||||
expect(l.x).toBe(1920 + 256)
|
||||
expect(l.width).toBe(2048)
|
||||
})
|
||||
})
|
||||
|
||||
describe('liegtSichtbar / anfangsLage', () => {
|
||||
const bildschirme = [
|
||||
{ x: 0, y: 0, width: 1920, height: 1080 },
|
||||
{ x: 1920, y: 0, width: 2560, height: 1440 },
|
||||
]
|
||||
it('auf einem Bildschirm: ja; im Nirgendwo eines abgestöpselten Monitors: nein', () => {
|
||||
expect(liegtSichtbar({ x: 100, y: 100, width: 1200, height: 800, maximiert: false }, bildschirme)).toBe(true)
|
||||
expect(liegtSichtbar({ x: 2500, y: 200, width: 1200, height: 800, maximiert: false }, bildschirme)).toBe(true)
|
||||
expect(liegtSichtbar({ x: -3000, y: 200, width: 1200, height: 800, maximiert: false }, bildschirme)).toBe(false)
|
||||
expect(liegtSichtbar({ x: 100, y: 100, width: 300, height: 200, maximiert: false }, bildschirme)).toBe(false)
|
||||
})
|
||||
it('anfangsLage: gemerkt, wenn sichtbar — sonst Start', () => {
|
||||
const arbeit = { x: 0, y: 0, width: 1920, height: 1040 }
|
||||
const gemerkt = { x: 2000, y: 50, width: 1400, height: 900, maximiert: true }
|
||||
expect(anfangsLage(gemerkt, arbeit, bildschirme)).toBe(gemerkt)
|
||||
expect(anfangsLage({ ...gemerkt, x: 9000 }, arbeit, bildschirme)).toEqual(startLage(arbeit))
|
||||
expect(anfangsLage(null, arbeit, bildschirme)).toEqual(startLage(arbeit))
|
||||
})
|
||||
})
|
||||
|
||||
describe('lageLesen / lageSchreiben', () => {
|
||||
it('Runde durch die Datei', () => {
|
||||
const pfad = join(ordner, 'fenster-lage.json')
|
||||
lageSchreiben(pfad, { x: 10, y: 20, width: 1300, height: 850, maximiert: true })
|
||||
expect(lageLesen(pfad)).toEqual({ x: 10, y: 20, width: 1300, height: 850, maximiert: true })
|
||||
})
|
||||
it('fehlende oder kaputte Datei heißt Startlage (null)', () => {
|
||||
expect(lageLesen(join(ordner, 'gibt-es-nicht.json'))).toBeNull()
|
||||
const kaputt = join(ordner, 'kaputt.json')
|
||||
writeFileSync(kaputt, '{"x": "zehn"}')
|
||||
expect(lageLesen(kaputt)).toBeNull()
|
||||
writeFileSync(kaputt, 'kein json')
|
||||
expect(lageLesen(kaputt)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
// Kleine pure Helfer aus 5.5.0: Sprachlisten fürs Dropdown, die Bilanz in
|
||||
// der Übersicht, der Serien-Merker aus der Einstellung, die Platz-Prüfung
|
||||
// im Dialog (dieselbe Rechnung wie im Kern).
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { platzUrteil as platzUrteilKern } from '../src/kern/dienste/speicher'
|
||||
import { platzUrteil } from '../src/gemeinsam/platz'
|
||||
import { serienMerkerAusJson } from '../src/kern/dienste/erkennung'
|
||||
import { statistikAus } from '../src/kern/dienste/vorgaenge'
|
||||
import { sprachName, sprachlisteAus, sprachlisteText } from '../src/gemeinsam/sprachen'
|
||||
|
||||
describe('sprachen', () => {
|
||||
it('sprachlisteAus: trimmt, kleinschreibt, entdoppelt, hält die Reihenfolge', () => {
|
||||
expect(sprachlisteAus('deu, ENG ,, jpn,deu')).toEqual(['deu', 'eng', 'jpn'])
|
||||
expect(sprachlisteAus('')).toEqual([])
|
||||
})
|
||||
it('sprachName kennt die üblichen, fällt sonst auf den Code zurück', () => {
|
||||
expect(sprachName('deu')).toBe('Deutsch')
|
||||
expect(sprachName('jpn')).toBe('Japanisch')
|
||||
expect(sprachName('xyz')).toBe('xyz')
|
||||
})
|
||||
it('sprachlisteText ist die Einstellung, wie der Kern sie liest', () => {
|
||||
expect(sprachlisteText(['deu', 'eng'])).toBe('deu,eng')
|
||||
})
|
||||
})
|
||||
|
||||
describe('statistikAus', () => {
|
||||
it('zählt Discs, Fertige, Bytes und die Dauer nur der Fertigen', () => {
|
||||
const s = statistikAus([
|
||||
{ phase: 'fertig', rohBytes: 30e9, fertigBytes: 8e9, begonnen: '2026-09-01T10:00:00Z', beendet: '2026-09-01T11:30:00Z' },
|
||||
{ phase: 'fertig', rohBytes: 20e9, fertigBytes: 5e9, begonnen: '2026-09-01T12:00:00Z', beendet: '2026-09-01T12:30:00Z' },
|
||||
{ phase: 'fehler', rohBytes: 10e9, fertigBytes: 0, begonnen: '2026-09-01T13:00:00Z', beendet: '2026-09-01T13:10:00Z' },
|
||||
{ phase: 'komprimiert', rohBytes: 25e9, fertigBytes: 0, begonnen: '2026-09-01T14:00:00Z', beendet: null },
|
||||
])
|
||||
expect(s).toEqual({ discs: 4, fertige: 2, rohBytes: 85e9, fertigBytes: 13e9, dauerS: 7200 })
|
||||
})
|
||||
it('leer ist leer', () => {
|
||||
expect(statistikAus([])).toEqual({ discs: 0, fertige: 0, rohBytes: 0, fertigBytes: 0, dauerS: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('serienMerkerAusJson', () => {
|
||||
it('liest gültige Einträge, überspringt kaputte, verträgt Müll', () => {
|
||||
const m = serienMerkerAusJson('{"tv:46296":{"titel":"Spartacus","staffel":1,"naechsteFolge":5,"wann":"2026-09-01T10:00:00Z"},"tv:1":{"staffel":"eins"},"x":5}')
|
||||
expect(m).toEqual({ 'tv:46296': { titel: 'Spartacus', staffel: 1, naechsteFolge: 5, wann: '2026-09-01T10:00:00Z' } })
|
||||
expect(serienMerkerAusJson(null)).toEqual({})
|
||||
expect(serienMerkerAusJson('')).toEqual({})
|
||||
expect(serienMerkerAusJson('nicht json')).toEqual({})
|
||||
expect(serienMerkerAusJson('[1,2]')).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Platz-Prüfung: eine Rechnung für Kern und Dialog', () => {
|
||||
it('der Kern reicht dieselbe Funktion durch', () => {
|
||||
expect(platzUrteilKern).toBe(platzUrteil)
|
||||
})
|
||||
it('reicht / knapp / zu wenig', () => {
|
||||
expect(platzUrteil(60e9, 40e9, true)).toEqual({ text: '', hart: false })
|
||||
expect(platzUrteil(45e9, 40e9, true).hart).toBe(false)
|
||||
expect(platzUrteil(45e9, 40e9, true).text.startsWith('Knapp')).toBe(true)
|
||||
expect(platzUrteil(30e9, 40e9, false).hart).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -105,7 +105,7 @@ describe('istFensterNachricht — rip-start mit Sprachen und Optionen (5.4.0)',
|
||||
|
||||
describe('istKernNachricht — Vorgänge, Kompression, Gesundheit (5.4.0)', () => {
|
||||
it('vorgaenge braucht Liste, Belegung und Warteschlange', () => {
|
||||
expect(istKernNachricht({ art: 'vorgaenge', stand: { vorgaenge: [], rohGesamtBytes: 0, fremde: [], regel: '', kompression: { laeuft: null, wartend: [] } } })).toBe(true)
|
||||
expect(istKernNachricht({ art: 'vorgaenge', stand: { vorgaenge: [], rohGesamtBytes: 0, fremde: [], regel: '', kompression: { laeuft: null, wartend: [], pausiert: false }, statistik: { discs: 0, fertige: 0, rohBytes: 0, fertigBytes: 0, dauerS: 0 } } })).toBe(true)
|
||||
expect(istKernNachricht({ art: 'vorgaenge', stand: { vorgaenge: [] } })).toBe(false)
|
||||
})
|
||||
it('kompression-ergebnis und laufwerk-gesundheit', () => {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Tests der Protokoll-Datei (gemeinsam/protokoll.ts, 5.5.0): eine Datei je
|
||||
// Tag, Zeile mit Zeitstempel und Quelle, Aufräumen nach 14 Tagen — und
|
||||
// fremde Dateien im Ordner bleiben unangetastet.
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import { Protokoll, protokollDateiname, veraltet, zeitstempel } from '../src/gemeinsam/protokoll'
|
||||
|
||||
const wurzel = mkdtempSync(join(tmpdir(), 'rippy-protokoll-'))
|
||||
afterAll(() => rmSync(wurzel, { recursive: true, force: true }))
|
||||
|
||||
describe('protokollDateiname / zeitstempel / veraltet', () => {
|
||||
it('Name und Stempel sind lokal und zweistellig', () => {
|
||||
const d = new Date(2026, 8, 2, 7, 5, 9)
|
||||
expect(protokollDateiname(d)).toBe('rippy-2026-09-02.log')
|
||||
expect(zeitstempel(d)).toBe('2026-09-02 07:05:09')
|
||||
})
|
||||
it('veraltet: älter als 14 Tage ja, 13 Tage nein, fremde Namen nie', () => {
|
||||
const jetzt = new Date(2026, 8, 20)
|
||||
const namen = ['rippy-2026-09-01.log', 'rippy-2026-09-07.log', 'rippy-2026-09-19.log', 'notizen.txt', 'rippy-alt.log']
|
||||
expect(veraltet(namen, jetzt)).toEqual(['rippy-2026-09-01.log'])
|
||||
expect(veraltet(namen, jetzt, 5)).toEqual(['rippy-2026-09-01.log', 'rippy-2026-09-07.log'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Protokoll', () => {
|
||||
it('legt den Ordner an, schreibt Zeilen mit Stempel und Quelle, räumt Altes weg', () => {
|
||||
const ordner = join(wurzel, 'logs')
|
||||
let uhr = new Date(2026, 8, 2, 21, 13, 5)
|
||||
const p = new Protokoll(ordner, 'kern', () => uhr)
|
||||
p.schreiben('Datenbank geöffnet: C:\\x\\rippy.db')
|
||||
p.schreiben('zweite Zeile')
|
||||
const datei = join(ordner, 'rippy-2026-09-02.log')
|
||||
expect(existsSync(datei)).toBe(true)
|
||||
const zeilen = readFileSync(datei, 'utf8').trimEnd().split('\n')
|
||||
expect(zeilen).toEqual(['2026-09-02 21:13:05 [kern] Datenbank geöffnet: C:\\x\\rippy.db', '2026-09-02 21:13:05 [kern] zweite Zeile'])
|
||||
|
||||
writeFileSync(join(ordner, 'rippy-2026-08-01.log'), 'alt\n')
|
||||
writeFileSync(join(ordner, 'fremd.txt'), 'bleibt\n')
|
||||
uhr = new Date(2026, 8, 3)
|
||||
expect(p.aufraeumen()).toBe(1)
|
||||
expect(existsSync(join(ordner, 'rippy-2026-08-01.log'))).toBe(false)
|
||||
expect(existsSync(join(ordner, 'fremd.txt'))).toBe(true)
|
||||
expect(existsSync(datei)).toBe(true)
|
||||
})
|
||||
it('ein nicht schreibbarer Ordner kippt nichts — die Fehlschläge werden beim nächsten Erfolg vermerkt', () => {
|
||||
const sperre = join(wurzel, 'datei-statt-ordner')
|
||||
writeFileSync(sperre, 'x')
|
||||
const p = new Protokoll(join(sperre, 'logs'), 'haupt', () => new Date(2026, 8, 2))
|
||||
expect(() => p.schreiben('geht nicht')).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
// Tests des freien Platzes (kern/dienste/speicher.ts, 5.5.0): der nächste
|
||||
// vorhandene Elternpfad, die Messung mit nachgebautem statfs, das Urteil
|
||||
// vor dem Rip. Der echte statfs-Aufruf ist am 01.09.2026 gemessen (siehe
|
||||
// Kopf der Datei) — hier zählt die Rechnung. Die Pfade stehen mit
|
||||
// Schrägstrichen: dirname versteht sie auf Windows UND auf dem Linux-Runner
|
||||
// der CI gleich.
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { platzMessen, platzUrteil, vorhandenerPfad } from '../src/kern/dienste/speicher'
|
||||
|
||||
describe('vorhandenerPfad', () => {
|
||||
it('nimmt den Ordner selbst, wenn es ihn gibt', () => {
|
||||
expect(vorhandenerPfad('X:/rippy/ablage', (p) => p === 'X:/rippy/ablage')).toBe('X:/rippy/ablage')
|
||||
})
|
||||
it('geht hoch bis zum ersten vorhandenen Elternpfad — die Ablage darf noch fehlen', () => {
|
||||
const da = new Set(['X:/', 'X:/rippy'])
|
||||
expect(vorhandenerPfad('X:/rippy/ablage/Filme', (p) => da.has(p))).toBe('X:/rippy')
|
||||
})
|
||||
it('bleibt an der Wurzel stehen, wenn nichts existiert (Windows: Q:/ — auf dem Linux-Runner endet dirname bei „.")', () => {
|
||||
expect(vorhandenerPfad('Q:/gibt/es/nicht', () => false)).toBe(process.platform === 'win32' ? 'Q:/' : '.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('platzMessen', () => {
|
||||
it('rechnet bavail × bsize und blocks × bsize', () => {
|
||||
const platz = platzMessen('X:/rippy', () => ({ bavail: 1000, bsize: 4096, blocks: 5000 }), () => true)
|
||||
expect(platz).toEqual({ pfad: 'X:/rippy', ok: true, freiBytes: 4_096_000, gesamtBytes: 20_480_000, fehler: '' })
|
||||
})
|
||||
it('ein leerer Pfad ist nicht messbar — und sagt es (R2)', () => {
|
||||
const platz = platzMessen(' ', () => ({ bavail: 1, bsize: 1, blocks: 1 }))
|
||||
expect(platz.ok).toBe(false)
|
||||
expect(platz.fehler).toBe('kein Pfad')
|
||||
})
|
||||
it('ein Fehler der Messung wird zum Grund, nie zu einer erfundenen Null', () => {
|
||||
const platz = platzMessen(
|
||||
'Z:/weg',
|
||||
() => {
|
||||
throw new Error('ENOENT: no such file or directory')
|
||||
},
|
||||
() => true,
|
||||
)
|
||||
expect(platz.ok).toBe(false)
|
||||
expect(platz.freiBytes).toBe(0)
|
||||
expect(platz.fehler).toContain('ENOENT')
|
||||
})
|
||||
})
|
||||
|
||||
describe('platzUrteil', () => {
|
||||
const GB = 1e9
|
||||
it('reicht: Roh plus 25 % plus 2 GB Luft passen', () => {
|
||||
expect(platzUrteil(60 * GB, 40 * GB, true)).toEqual({ text: '', hart: false })
|
||||
})
|
||||
it('knapp: der Rip passt, die Kompression nicht mehr sicher', () => {
|
||||
const urteil = platzUrteil(45 * GB, 40 * GB, true)
|
||||
expect(urteil.hart).toBe(false)
|
||||
expect(urteil.text.startsWith('Knapp')).toBe(true)
|
||||
expect(urteil.text).toContain('45,0 GB frei')
|
||||
})
|
||||
it('zu wenig: nicht einmal der verlustfreie Rip passt', () => {
|
||||
const urteil = platzUrteil(30 * GB, 40 * GB, false)
|
||||
expect(urteil.hart).toBe(true)
|
||||
expect(urteil.text.startsWith('Zu wenig Platz')).toBe(true)
|
||||
})
|
||||
it('ohne Kompression zählt nur Roh plus Luft', () => {
|
||||
expect(platzUrteil(43 * GB, 40 * GB, false).text).toBe('')
|
||||
expect(platzUrteil(43 * GB, 40 * GB, true).text.startsWith('Knapp')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -27,7 +27,7 @@ function auftrag(id: string): KompressionsAuftrag {
|
||||
}
|
||||
|
||||
function fertig(text = 'ok'): AblageErgebnis {
|
||||
return { status: 'fertig', zielOrdner: 'Z', dateien: [], text, komprimiert: true, preset: 'p', fertigBytes: 1, hinweise: [] }
|
||||
return { status: 'fertig', zielOrdner: 'Z', dateien: [], text, komprimiert: true, preset: 'p', fertigBytes: 1, hinweise: [], folgenVergeben: [] }
|
||||
}
|
||||
|
||||
/** Ein Encode, den der Test von außen beendet. */
|
||||
|
||||
Reference in New Issue
Block a user