feat(v5): MakeMKV-Selbst-Update (§ 9) und Kino-Look fürs Fenster
Ampel / ampel (push) Successful in 1m34s
Ampel / ampel (push) Successful in 1m34s
Update-Strecke: Quellenkette (Hersteller 525 → Forum → Archiv, höchste gewinnt, 31.08. nachgemessen), MZ- + Versions-Ressourcen-Prüfung (GuinpinSoft inc, gemessen), Download in .neu, Start übers Haupt (shell/UAC — Installer hängt bewusst NICHT an der Leine), Warte-Poll bis die EXE-Version wechselt, dann Schlüssel neu prüfen. Ehrlicher Zweig: installierte == neueste Fassung → kein sinnloser Download. Fenster: Laufwerks-Kachel als Bühne (TMDb-Backdrop, Poster, Beschreibung, Phasen-Label, Restzeit, aufklappbares Protokoll), Bibliothek als Poster-Regal (DB-Schema 3: poster_pfad, Migration), Schlüssel-Karte mit Update-Knopf und Beschaffungs-Balken, MakeMKV-Version als Badge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5622d8e47d
commit
d5ea48cd1e
@@ -0,0 +1,185 @@
|
||||
// Die MakeMKV-Beschaffungskette (§ 9) — Fixtures aus den Live-Messungen
|
||||
// vom 31.08.2026: Hersteller-Seite HTTP 525, Forum 1.18.4 in 0,2 s,
|
||||
// Archiv 1.18.2 in 7,8 s.
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
dateiPruefen,
|
||||
hoechsteVersion,
|
||||
istNeuer,
|
||||
makemkvInstallerHolen,
|
||||
makemkvNeuesteVersion,
|
||||
makemkvQuellen,
|
||||
versionsquellen,
|
||||
} from '../src/kern/werkzeuge/beschaffen'
|
||||
import { istFensterNachricht, istHauptNachricht, istKernNachricht } from '../src/gemeinsam/nachrichten'
|
||||
|
||||
const aufraeumen: string[] = []
|
||||
afterEach(() => {
|
||||
for (const ordner of aufraeumen.splice(0).reverse()) {
|
||||
rmSync(ordner, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function tempOrdner(): string {
|
||||
const ordner = mkdtempSync(join(tmpdir(), 'rippy-beschaffung-'))
|
||||
aufraeumen.push(ordner)
|
||||
return ordner
|
||||
}
|
||||
|
||||
function antwort(body: string | Buffer, status = 200): Response {
|
||||
return new Response(typeof body === 'string' ? body : new Uint8Array(body), { status })
|
||||
}
|
||||
|
||||
describe('hoechsteVersion', () => {
|
||||
it('vergleicht numerisch — als Zeichenkette wäre 1.9.2 größer als 1.18.4', () => {
|
||||
expect(hoechsteVersion('MakeMKV v1.9.2 … MakeMKV 1.18.4 … MakeMKV v1.18.2')).toBe('1.18.4')
|
||||
})
|
||||
it('leerer Text → leere Antwort', () => {
|
||||
expect(hoechsteVersion('')).toBe('')
|
||||
expect(hoechsteVersion('nichts hier')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('istNeuer', () => {
|
||||
it('numerisch je Stelle', () => {
|
||||
expect(istNeuer('1.18.4', '1.18.2')).toBe(true)
|
||||
expect(istNeuer('1.9.2', '1.18.4')).toBe(false)
|
||||
expect(istNeuer('1.18.4', '1.18.4')).toBe(false)
|
||||
expect(istNeuer('1.18.4.1', '1.18.4')).toBe(true)
|
||||
})
|
||||
it('Unbekanntes ist nie neuer — außer gegenüber gar nichts', () => {
|
||||
expect(istNeuer('', '1.0.0')).toBe(false)
|
||||
expect(istNeuer('1.0.0', '')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Quellenketten', () => {
|
||||
it('Versionsquellen: der Hersteller zuerst, und nur er ist maßgeblich', () => {
|
||||
const quellen = versionsquellen()
|
||||
expect(quellen).toHaveLength(3)
|
||||
expect(quellen[0].name).toBe('Hersteller-Seite')
|
||||
expect(quellen[0].massgeblich).toBe(true)
|
||||
expect(quellen.slice(1).every((q) => !q.massgeblich)).toBe(true)
|
||||
// Zeitstempel in der Vergangenheit — sonst holt das Archiv live nach.
|
||||
expect(quellen[2].url).toContain('web.archive.org/web/2025/')
|
||||
})
|
||||
|
||||
it('Datei-Quellen: Hersteller → Archiv (mit id_ für die ROHE Datei)', () => {
|
||||
const quellen = makemkvQuellen('1.18.4')
|
||||
expect(quellen.map(([name]) => name)).toEqual(['Hersteller', 'Internet Archive'])
|
||||
expect(quellen[0][1]).toBe('https://www.makemkv.com/download/Setup_MakeMKV_v1.18.4.exe')
|
||||
expect(quellen[1][1]).toBe(
|
||||
'https://web.archive.org/web/2025id_/https://www.makemkv.com/download/Setup_MakeMKV_v1.18.4.exe',
|
||||
)
|
||||
})
|
||||
|
||||
it('eine eigene Basis steht davor — wer sie einträgt, will sie', () => {
|
||||
const quellen = makemkvQuellen('1.18.4', 'https://mein-server.example/mkv/')
|
||||
expect(quellen[0]).toEqual(['eigene Quelle', 'https://mein-server.example/mkv/Setup_MakeMKV_v1.18.4.exe'])
|
||||
expect(quellen).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('makemkvNeuesteVersion', () => {
|
||||
it('der Hersteller ist maßgeblich — antwortet er, wird nicht weitergefragt', async () => {
|
||||
const gefragt: string[] = []
|
||||
const holen = (async (url: string | URL | Request) => {
|
||||
gefragt.push(String(url))
|
||||
return antwort('<html>MakeMKV 1.19.0 is out</html>')
|
||||
}) as typeof fetch
|
||||
expect(await makemkvNeuesteVersion(holen)).toBe('1.19.0')
|
||||
expect(gefragt).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('Hersteller tot (525, wie gemessen) → höchste aus Forum und Archiv', async () => {
|
||||
const holen = (async (url: string | URL | Request) => {
|
||||
const wo = String(url)
|
||||
if (wo.includes('www.makemkv.com')) return antwort('cloudflare', 525)
|
||||
if (wo.includes('forum')) return antwort('MakeMKV v1.18.4 released')
|
||||
return antwort('MakeMKV v1.18.2 … MakeMKV v1.18.1')
|
||||
}) as typeof fetch
|
||||
expect(await makemkvNeuesteVersion(holen)).toBe('1.18.4')
|
||||
}, 15_000)
|
||||
|
||||
it('keine Quelle → leere Antwort statt Wurf', async () => {
|
||||
expect(await makemkvNeuesteVersion(undefined, [])).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dateiPruefen', () => {
|
||||
it('ohne MZ-Kopf ist es kein Windows-Programm', () => {
|
||||
const ordner = tempOrdner()
|
||||
const pfad = join(ordner, 'seite.exe')
|
||||
writeFileSync(pfad, '<html>Fehlerseite</html>')
|
||||
const urteil = dateiPruefen(pfad, '1.18.4')
|
||||
expect(urteil.ok).toBe(false)
|
||||
expect(urteil.grund).toContain('MZ')
|
||||
})
|
||||
|
||||
it('unter Windows zählt die Versions-Ressource (gemessene Felder)', () => {
|
||||
const ordner = tempOrdner()
|
||||
const pfad = join(ordner, 'setup.exe')
|
||||
writeFileSync(pfad, 'MZ\x90rest')
|
||||
// Die am 31.08.2026 an der echten Datei gemessenen Werte:
|
||||
const echt = { firma: 'GuinpinSoft inc', produkt: 'MakeMKV', version: 'v1.18.4' }
|
||||
expect(dateiPruefen(pfad, '1.18.4', () => echt, 'win32').ok).toBe(true)
|
||||
expect(dateiPruefen(pfad, '1.18.4', () => ({ ...echt, firma: 'Boese GmbH' }), 'win32').ok).toBe(false)
|
||||
expect(dateiPruefen(pfad, '1.19.0', () => echt, 'win32').ok).toBe(false)
|
||||
// Auf Linux (CI) gibt es keine Versions-Ressourcen — MZ-Test muss reichen.
|
||||
expect(dateiPruefen(pfad, '1.18.4', () => echt, 'linux').ok).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('makemkvInstallerHolen', () => {
|
||||
const GROSS = Buffer.concat([Buffer.from('MZ'), Buffer.alloc(1024 * 1024 + 100, 7)])
|
||||
|
||||
it('Fehlerseite mit HTTP 200 fällt durch die Mindestgröße — die nächste Quelle gewinnt', async () => {
|
||||
const ordner = tempOrdner()
|
||||
const holen = (async (url: string | URL | Request) =>
|
||||
String(url).includes('web.archive.org') ? antwort(GROSS) : antwort('<html>kaputt</html>')) as typeof fetch
|
||||
const ergebnis = await makemkvInstallerHolen(
|
||||
'1.18.4',
|
||||
() => {},
|
||||
'',
|
||||
holen,
|
||||
ordner,
|
||||
(p, v) => dateiPruefen(p, v, () => ({ firma: 'GuinpinSoft inc', produkt: 'MakeMKV', version: 'v1.18.4' }), 'win32'),
|
||||
)
|
||||
expect(ergebnis.quelle).toBe('Internet Archive')
|
||||
expect(readFileSync(ergebnis.pfad).length).toBe(GROSS.length)
|
||||
expect(existsSync(`${ergebnis.pfad}.neu`)).toBe(false)
|
||||
})
|
||||
|
||||
it('liefert keine Quelle, steht im Wurf der Bericht ALLER Versuche', async () => {
|
||||
const ordner = tempOrdner()
|
||||
const holen = (async () => antwort('weg', 404)) as typeof fetch
|
||||
await expect(makemkvInstallerHolen('1.18.4', () => {}, '', holen, ordner)).rejects.toThrow(
|
||||
/Hersteller: HTTP 404[\s\S]*Internet Archive: HTTP 404[\s\S]*makemkv\.com/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Nachrichten-Schema der Update-Strecke', () => {
|
||||
it('Fenster → Kern: makemkv-update und schluessel-pruefen', () => {
|
||||
expect(istFensterNachricht({ art: 'makemkv-update' })).toBe(true)
|
||||
expect(istFensterNachricht({ art: 'schluessel-pruefen' })).toBe(true)
|
||||
})
|
||||
it('Haupt → Kern: installer-ergebnis', () => {
|
||||
expect(istHauptNachricht({ art: 'installer-ergebnis', fehler: null })).toBe(true)
|
||||
expect(istHauptNachricht({ art: 'installer-ergebnis', fehler: 'UAC abgelehnt' })).toBe(true)
|
||||
expect(istHauptNachricht({ art: 'installer-ergebnis', fehler: 5 })).toBe(false)
|
||||
})
|
||||
it('Kern → Haupt/Fenster: beschaffung-status und installer-starten', () => {
|
||||
expect(
|
||||
istKernNachricht({
|
||||
art: 'beschaffung-status',
|
||||
stand: { werkzeug: 'makemkv', laeuft: true, text: 'lädt', anteil: 0.5, fehler: false },
|
||||
}),
|
||||
).toBe(true)
|
||||
expect(istKernNachricht({ art: 'installer-starten', pfad: 'C:\\x\\setup.exe' })).toBe(true)
|
||||
expect(istKernNachricht({ art: 'installer-starten', pfad: '' })).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user