feat(v5): Etappe W-1 — Laufwerk: Wache, Typ, Auswerfen mit Nachsehen
Ampel / ampel (push) Successful in 1m21s
Ampel / ampel (push) Successful in 1m21s
kern/laufwerk in drei Schichten: codes.ts (Steuercodes HERGELEITET wie win_ioctl.py, gegen die Doku-Zahlen getestet), win32.ts (der einzige koffi-Ort im Kern, Muster aus beweise/laufwerk.js), disc.ts (Logik gegen die LaufwerkApi-Schnittstelle — classify mit den cdrom.py-Schwellen, Geraete-Info mit ready/empty/unknown + Klartext-Grund, Auswurf als entriegeln→auswerfen→NACHSEHEN mit injizierbarem Warten). Dazu wache.ts im 3-Sekunden-Takt (§ 5 Plan A): Uebergangslogik pur, gescheiterte Runde behaelt den Stand (R2) und meldet laut (R4). Fenster zeigt Laufwerke live (Modell, Typ, Groesse, Grund) mit Auswerfen-Knopf; Ereigniszeilen fuer Disc rein/raus und Fehler. Bündel-Falle gefunden und behoben: Ein woertliches require überlebt Rollup nicht — win32 wird per dynamischem import() als eigener Chunk gebaut; der Fehler war dank R4 im Fenster sichtbar statt still. Gemessen am echten BU40N (30.08.2026, RIPPY_MESSUNG=1): Laufwerk G: status=ready typ=bluray groesse=33759690752 modell=HL-DT-ST BD-RE BU40N 1.03 serial=0025114C0149 Der Auswurf-Beweis (messung.auswurf) folgt am Ende der Sitzung — der BU40N kann die Schublade nicht selbst einziehen. 52 Tests gruen (Steuercodes, classify, Auswurf-Ablauf, Geraete-Info, Wache, Smoke weiterhin gruen mit Laufwerks-Kachel im Bild). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6ed8035dcc
commit
a45069af5e
@@ -0,0 +1,282 @@
|
||||
// Tests der Laufwerks-Schicht — komplett ohne Windows und ohne koffi:
|
||||
// disc.ts arbeitet gegen die LaufwerkApi-Schnittstelle, hier nachgebildet.
|
||||
// Die Testfälle wandern sinngemäß aus dem bewiesenen Docker-Zweig mit
|
||||
// (test_win_ioctl.py, test_windows.py, test_cdrom.py) — ein Treiber, der
|
||||
// dieselben Fälle besteht, kennt dieselben Fallen (KONZEPT § 10).
|
||||
import { describe, expect, it, beforeEach } from 'vitest'
|
||||
import {
|
||||
CDROM_DISK_AUDIO_TRACK,
|
||||
CDROM_DISK_DATA_TRACK,
|
||||
ERROR_ACCESS_DENIED,
|
||||
ERROR_INVALID_FUNCTION,
|
||||
ERROR_NOT_READY,
|
||||
IOCTL_CDROM_DISK_TYPE,
|
||||
IOCTL_DISK_GET_LENGTH_INFO,
|
||||
IOCTL_STORAGE_CHECK_VERIFY2,
|
||||
IOCTL_STORAGE_EJECT_MEDIA,
|
||||
IOCTL_STORAGE_MEDIA_REMOVAL,
|
||||
IOCTL_STORAGE_QUERY_PROPERTY,
|
||||
DRIVE_CDROM,
|
||||
Win32Fehler,
|
||||
type LaufwerkApi,
|
||||
} from '../src/kern/laufwerk/codes'
|
||||
import {
|
||||
angabenSpeicherLeeren,
|
||||
auswerfenMitGrund,
|
||||
classify,
|
||||
geraeteInfo,
|
||||
geraetepfad,
|
||||
kennung,
|
||||
listOptischeLaufwerke,
|
||||
textBei,
|
||||
BLURAY_MIN_BYTES,
|
||||
UHD_MIN_BYTES,
|
||||
} from '../src/kern/laufwerk/disc'
|
||||
|
||||
describe('Steuercodes — gegen die Microsoft-Dokumentation gerechnet', () => {
|
||||
// Sollwerte aus der Doku (dieselbe Gegenprobe wie test_win_ioctl.py):
|
||||
// ein Zahlendreher sähe sonst aus wie „das Laufwerk kann das nicht".
|
||||
it.each([
|
||||
['IOCTL_STORAGE_CHECK_VERIFY2', IOCTL_STORAGE_CHECK_VERIFY2, 0x002d0800],
|
||||
['IOCTL_STORAGE_MEDIA_REMOVAL', IOCTL_STORAGE_MEDIA_REMOVAL, 0x002d4804],
|
||||
['IOCTL_STORAGE_EJECT_MEDIA', IOCTL_STORAGE_EJECT_MEDIA, 0x002d4808],
|
||||
['IOCTL_STORAGE_QUERY_PROPERTY', IOCTL_STORAGE_QUERY_PROPERTY, 0x002d1400],
|
||||
['IOCTL_CDROM_DISK_TYPE', IOCTL_CDROM_DISK_TYPE, 0x00020040],
|
||||
['IOCTL_DISK_GET_LENGTH_INFO', IOCTL_DISK_GET_LENGTH_INFO, 0x0007405c],
|
||||
])('%s stimmt mit der Doku überein', (_name, wert, erwartet) => {
|
||||
expect(wert).toBe(erwartet)
|
||||
})
|
||||
})
|
||||
|
||||
describe('classify — Typ aus Spur-Flags und Größe', () => {
|
||||
it('Audio-Spur ist eine CD, egal wie groß', () => {
|
||||
expect(classify(CDROM_DISK_AUDIO_TRACK, 700 * 1e6)).toBe('cd')
|
||||
expect(classify(CDROM_DISK_AUDIO_TRACK | CDROM_DISK_DATA_TRACK, 0)).toBe('cd')
|
||||
})
|
||||
it('Datenspur wird nach Größe eingeordnet', () => {
|
||||
expect(classify(CDROM_DISK_DATA_TRACK, 4.7e9)).toBe('dvd')
|
||||
// Der gemessene Fall vom 28.08.2026: BD-50 mit 48.149.364.736 Bytes
|
||||
// (44,84 GiB) liegt unter der 55-GiB-Schwelle und ist „bluray".
|
||||
expect(classify(CDROM_DISK_DATA_TRACK, 48_149_364_736)).toBe('bluray')
|
||||
expect(classify(CDROM_DISK_DATA_TRACK, 66 * 1024 ** 3)).toBe('uhd')
|
||||
})
|
||||
it('die Schwellen selbst', () => {
|
||||
expect(classify(CDROM_DISK_DATA_TRACK, BLURAY_MIN_BYTES)).toBe('bluray')
|
||||
expect(classify(CDROM_DISK_DATA_TRACK, BLURAY_MIN_BYTES - 1)).toBe('dvd')
|
||||
expect(classify(CDROM_DISK_DATA_TRACK, UHD_MIN_BYTES)).toBe('uhd')
|
||||
})
|
||||
it('keine Spur-Info heißt unknown, nicht dvd', () => {
|
||||
expect(classify(0, 33e9)).toBe('unknown')
|
||||
})
|
||||
})
|
||||
|
||||
describe('geraetepfad und kennung — hin und zurück', () => {
|
||||
it('baut den Gerätepfad OHNE abschließenden Backslash', () => {
|
||||
// Mit Backslash öffnet CreateFileW das Wurzelverzeichnis statt des
|
||||
// Geräts — die Steuercodes scheitern dann mit ERROR_INVALID_FUNCTION.
|
||||
expect(geraetepfad('G')).toBe('\\\\.\\G:')
|
||||
expect(geraetepfad('g:')).toBe('\\\\.\\G:')
|
||||
expect(geraetepfad('G:\\')).toBe('\\\\.\\G:')
|
||||
})
|
||||
it('wirft bei leerer Angabe', () => {
|
||||
expect(() => geraetepfad('')).toThrow()
|
||||
})
|
||||
it('kennung löst den Pfad wieder auf (der /dev/G-404-Fund)', () => {
|
||||
expect(kennung('\\\\.\\G:')).toBe('G')
|
||||
expect(kennung(geraetepfad('e'))).toBe('E')
|
||||
})
|
||||
})
|
||||
|
||||
describe('textBei — STORAGE_DEVICE_DESCRIPTOR auswerten', () => {
|
||||
it('liest die Zeichenkette am Offset', () => {
|
||||
const puffer = Buffer.alloc(40)
|
||||
puffer.writeUInt32LE(32, 12) // Offset an Position 12 zeigt auf Byte 32
|
||||
puffer.write('HL-DT-ST\x00', 32, 'latin1')
|
||||
expect(textBei(puffer, 12)).toBe('HL-DT-ST')
|
||||
})
|
||||
it('Offset 0 heißt „gibt es nicht"', () => {
|
||||
expect(textBei(Buffer.alloc(40), 12)).toBe('')
|
||||
})
|
||||
it('Offset außerhalb des Puffers ist leer, kein Absturz', () => {
|
||||
const puffer = Buffer.alloc(16)
|
||||
puffer.writeUInt32LE(999, 12)
|
||||
expect(textBei(puffer, 12)).toBe('')
|
||||
expect(textBei(Buffer.alloc(2), 12)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
// ── Die Win32-Nachbildung (Muster aus test_windows.py) ──────────────────
|
||||
interface FakeVerhalten {
|
||||
buchstaben?: string[]
|
||||
typen?: Record<string, number>
|
||||
oeffnenFehler?: number
|
||||
antworten?: Map<number, Buffer | number> // Buffer = Antwort, number = Win32-Fehlercode
|
||||
}
|
||||
|
||||
function fakeApi(verhalten: FakeVerhalten): LaufwerkApi & { protokoll: string[] } {
|
||||
const protokoll: string[] = []
|
||||
return {
|
||||
protokoll,
|
||||
laufwerksbuchstaben: () => verhalten.buchstaben ?? ['C', 'G'],
|
||||
laufwerkstyp: (b) => verhalten.typen?.[b] ?? (b === 'G' ? DRIVE_CDROM : 3),
|
||||
oeffnen(pfad, zugriff) {
|
||||
protokoll.push(`oeffnen ${pfad} zugriff=${zugriff}`)
|
||||
if (verhalten.oeffnenFehler !== undefined) throw new Win32Fehler('CreateFileW', verhalten.oeffnenFehler)
|
||||
return { pfad }
|
||||
},
|
||||
schliessen() {
|
||||
protokoll.push('schliessen')
|
||||
},
|
||||
steuern(_h, code, eingabe) {
|
||||
protokoll.push(`steuern 0x${code.toString(16)}${eingabe ? ` ein=${eingabe.toString('hex')}` : ''}`)
|
||||
const antwort = verhalten.antworten?.get(code)
|
||||
if (antwort === undefined) return Buffer.alloc(0)
|
||||
if (typeof antwort === 'number') throw new Win32Fehler(`DeviceIoControl(0x${code.toString(16)})`, antwort)
|
||||
return antwort
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function u32(wert: number): Buffer {
|
||||
const b = Buffer.alloc(4)
|
||||
b.writeUInt32LE(wert)
|
||||
return b
|
||||
}
|
||||
|
||||
function u64(wert: bigint): Buffer {
|
||||
const b = Buffer.alloc(8)
|
||||
b.writeBigUInt64LE(wert)
|
||||
return b
|
||||
}
|
||||
|
||||
const sofort = async (): Promise<void> => {}
|
||||
|
||||
beforeEach(() => angabenSpeicherLeeren())
|
||||
|
||||
describe('listOptischeLaufwerke', () => {
|
||||
it('nimmt nur DRIVE_CDROM und baut Gerätepfade', () => {
|
||||
const api = fakeApi({ buchstaben: ['C', 'E', 'G'], typen: { C: 3, E: DRIVE_CDROM, G: DRIVE_CDROM } })
|
||||
expect(listOptischeLaufwerke(api)).toEqual(['\\\\.\\E:', '\\\\.\\G:'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('auswerfenMitGrund — entriegeln, auswerfen, NACHSEHEN (R3)', () => {
|
||||
it('meldet ok erst, wenn das Medium wirklich weg ist', async () => {
|
||||
let verifyRunde = 0
|
||||
const api = fakeApi({})
|
||||
api.steuern = (_h, code, eingabe) => {
|
||||
api.protokoll.push(`steuern 0x${code.toString(16)}${eingabe ? ` ein=${eingabe.toString('hex')}` : ''}`)
|
||||
if (code === IOCTL_STORAGE_CHECK_VERIFY2) {
|
||||
verifyRunde += 1
|
||||
// Erst beim zweiten Nachsehen ist die Disc draußen.
|
||||
if (verifyRunde >= 2) throw new Win32Fehler('DeviceIoControl', ERROR_NOT_READY)
|
||||
return Buffer.alloc(0)
|
||||
}
|
||||
return Buffer.alloc(0)
|
||||
}
|
||||
const { grund, fehler } = await auswerfenMitGrund(api, '\\\\.\\G:', sofort)
|
||||
expect(grund).toBe('ok')
|
||||
expect(fehler).toBeNull()
|
||||
// Reihenfolge: erst entriegeln (MEDIA_REMOVAL mit 0), dann auswerfen.
|
||||
const reihenfolge = api.protokoll.filter((z) => z.startsWith('steuern'))
|
||||
expect(reihenfolge[0]).toContain(IOCTL_STORAGE_MEDIA_REMOVAL.toString(16))
|
||||
expect(reihenfolge[0]).toContain('ein=00')
|
||||
expect(reihenfolge[1]).toContain(IOCTL_STORAGE_EJECT_MEDIA.toString(16))
|
||||
})
|
||||
|
||||
it('Erfolg des Auswurf-Befehls allein ist KEIN ok — bleibt-drin', async () => {
|
||||
// Der v1-Fehler: CDROMEJECT quittiert Erfolg auf verriegeltem Laufwerk
|
||||
// und tut nichts. CHECK_VERIFY2 sagt hier weiter „Medium da".
|
||||
const api = fakeApi({ antworten: new Map() })
|
||||
const { grund } = await auswerfenMitGrund(api, '\\\\.\\G:', sofort)
|
||||
expect(grund).toBe('bleibt-drin')
|
||||
})
|
||||
|
||||
it('kein Zugriff aufs Gerät heißt kein-zugriff, wirft nicht', async () => {
|
||||
const api = fakeApi({ oeffnenFehler: ERROR_ACCESS_DENIED })
|
||||
const { grund, fehler } = await auswerfenMitGrund(api, '\\\\.\\G:', sofort)
|
||||
expect(grund).toBe('kein-zugriff')
|
||||
expect(fehler).toContain('Win32-Fehler 5')
|
||||
})
|
||||
|
||||
it('abgelehnter Auswurf-Befehl heißt abgelehnt', async () => {
|
||||
const api = fakeApi({ antworten: new Map([[IOCTL_STORAGE_EJECT_MEDIA, ERROR_INVALID_FUNCTION]]) })
|
||||
const { grund } = await auswerfenMitGrund(api, '\\\\.\\G:', sofort)
|
||||
expect(grund).toBe('abgelehnt')
|
||||
})
|
||||
|
||||
it('gescheitertes Entriegeln verhindert den Auswurf-Versuch nicht', async () => {
|
||||
const api = fakeApi({
|
||||
antworten: new Map<number, Buffer | number>([
|
||||
[IOCTL_STORAGE_MEDIA_REMOVAL, ERROR_INVALID_FUNCTION],
|
||||
[IOCTL_STORAGE_CHECK_VERIFY2, ERROR_NOT_READY],
|
||||
]),
|
||||
})
|
||||
const { grund } = await auswerfenMitGrund(api, '\\\\.\\G:', sofort)
|
||||
expect(grund).toBe('ok')
|
||||
})
|
||||
})
|
||||
|
||||
describe('geraeteInfo — ready/empty/unknown, nie eine falsche Aussage', () => {
|
||||
const beschreibung = (() => {
|
||||
// STORAGE_DEVICE_DESCRIPTOR: Offsets bei 12/16/20/24 → Strings ab 40.
|
||||
const b = Buffer.alloc(96)
|
||||
b.writeUInt32LE(40, 12)
|
||||
b.write('HL-DT-ST\x00', 40, 'latin1')
|
||||
b.writeUInt32LE(56, 16)
|
||||
b.write('BD-RE BU40N\x00', 56, 'latin1')
|
||||
b.writeUInt32LE(72, 20)
|
||||
b.write('1.03\x00', 72, 'latin1')
|
||||
b.writeUInt32LE(80, 24)
|
||||
b.write('0025114C0149\x00', 80, 'latin1')
|
||||
return b
|
||||
})()
|
||||
|
||||
it('Disc drin: ready mit Typ, Größe, Modell, Seriennummer', () => {
|
||||
const api = fakeApi({
|
||||
antworten: new Map<number, Buffer | number>([
|
||||
[IOCTL_STORAGE_CHECK_VERIFY2, Buffer.alloc(0)],
|
||||
[IOCTL_CDROM_DISK_TYPE, u32(CDROM_DISK_DATA_TRACK)],
|
||||
[IOCTL_DISK_GET_LENGTH_INFO, u64(33_759_690_752n)],
|
||||
[IOCTL_STORAGE_QUERY_PROPERTY, beschreibung],
|
||||
]),
|
||||
})
|
||||
const info = geraeteInfo(api, '\\\\.\\G:')
|
||||
expect(info).toMatchObject({
|
||||
id: 'G',
|
||||
status: 'ready',
|
||||
typ: 'bluray',
|
||||
groesseBytes: 33_759_690_752,
|
||||
modell: 'HL-DT-ST BD-RE BU40N 1.03',
|
||||
seriennummer: '0025114C0149',
|
||||
grund: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('leeres Laufwerk: empty, kein Fehler', () => {
|
||||
const api = fakeApi({
|
||||
antworten: new Map<number, Buffer | number>([[IOCTL_STORAGE_CHECK_VERIFY2, ERROR_NOT_READY]]),
|
||||
})
|
||||
expect(geraeteInfo(api, '\\\\.\\G:').status).toBe('empty')
|
||||
})
|
||||
|
||||
it('Gerät nicht ansprechbar: unknown MIT Grund — nicht empty (R2)', () => {
|
||||
const api = fakeApi({ oeffnenFehler: ERROR_ACCESS_DENIED })
|
||||
const info = geraeteInfo(api, '\\\\.\\G:')
|
||||
expect(info.status).toBe('unknown')
|
||||
expect(info.grund).toContain('hält das Laufwerk fest')
|
||||
})
|
||||
|
||||
it('der 30.08.-Fall: Medium da, aber Typ-Abfrage bricht — ready mit Grund', () => {
|
||||
const api = fakeApi({
|
||||
antworten: new Map<number, Buffer | number>([
|
||||
[IOCTL_STORAGE_CHECK_VERIFY2, Buffer.alloc(0)],
|
||||
[IOCTL_DISK_GET_LENGTH_INFO, ERROR_INVALID_FUNCTION],
|
||||
]),
|
||||
})
|
||||
const info = geraeteInfo(api, '\\\\.\\G:')
|
||||
expect(info.status).toBe('ready')
|
||||
expect(info.typ).toBe('unknown')
|
||||
expect(info.grund).toContain('beantwortet keine Medien-Abfragen mehr')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
// AUSWURF-MESSUNG an echter Hardware — der W-1-Beweis „Auswerfen wirkt
|
||||
// (nachgemessen, nicht geglaubt)". Läuft NUR auf ausdrückliches Verlangen:
|
||||
//
|
||||
// RIPPY_MESSUNG_AUSWURF=1 npx vitest run test/messung.auswurf.test.ts
|
||||
//
|
||||
// ⚠ Wirft die Disc im ersten optischen Laufwerk WIRKLICH aus. Der BU40N
|
||||
// kann die Schublade nicht selbst einziehen (LOAD_MEDIA →
|
||||
// ERROR_INVALID_FUNCTION, windows.py 28.08.2026) — nach dem Lauf bleibt
|
||||
// der Tray offen. Deshalb getrennt von messung.laufwerk (nur lesend).
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const messen = process.env.RIPPY_MESSUNG_AUSWURF === '1' && process.platform === 'win32'
|
||||
|
||||
describe.skipIf(!messen)('Auswurf am echten Laufwerk — Zustand davor und danach', () => {
|
||||
it('entriegelt, wirft aus und SIEHT NACH', async () => {
|
||||
const { win32Api } = await import('../src/kern/laufwerk/win32')
|
||||
const { auswerfenMitGrund, geraeteInfo, listOptischeLaufwerke } = await import('../src/kern/laufwerk/disc')
|
||||
|
||||
const api = win32Api()
|
||||
const geraete = listOptischeLaufwerke(api)
|
||||
expect(geraete.length).toBeGreaterThan(0)
|
||||
const geraet = geraete[0]
|
||||
|
||||
const davor = geraeteInfo(api, geraet)
|
||||
console.log(`MESSUNG davor: status=${davor.status} typ=${davor.typ}`)
|
||||
expect(davor.status).toBe('ready') // ohne Disc misst dieser Lauf nichts
|
||||
|
||||
const start = Date.now()
|
||||
const { grund, fehler } = await auswerfenMitGrund(api, geraet)
|
||||
const dauerMs = Date.now() - start
|
||||
console.log(`MESSUNG auswurf: grund=${grund} fehler=${fehler ?? '-'} dauer=${dauerMs}ms`)
|
||||
|
||||
const danach = geraeteInfo(api, geraet)
|
||||
console.log(`MESSUNG danach: status=${danach.status}`)
|
||||
|
||||
// Der eigentliche Beweis ist der ZUSTAND davor/danach — nicht der
|
||||
// Rückgabewert des Steuercodes (windows.py: 4 → 1 am echten Gerät).
|
||||
expect(grund).toBe('ok')
|
||||
expect(danach.status).toBe('empty')
|
||||
})
|
||||
})
|
||||
|
||||
describe.skipIf(messen)('Auswurf-Messung übersprungen', () => {
|
||||
it('läuft nur mit RIPPY_MESSUNG_AUSWURF=1 unter Windows', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
// MESSUNG an echter Hardware — läuft nur auf Verlangen:
|
||||
//
|
||||
// RIPPY_MESSUNG=1 npx vitest run test/messung.laufwerk.test.ts
|
||||
//
|
||||
// In der Ampel (Linux, kein Laufwerk) wird sie übersprungen; die Ergebnisse
|
||||
// werden im SAVEPOINT.md mit Ausgabe dokumentiert (beweisen statt
|
||||
// behaupten). NUR LESEND — kein Auswerfen, kein Verriegeln; der
|
||||
// Auswurf-Beweis ist ein eigener, ausdrücklicher Lauf (messung.auswurf).
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const messen = process.env.RIPPY_MESSUNG === '1' && process.platform === 'win32'
|
||||
|
||||
describe.skipIf(!messen)('Messung am echten Laufwerk', () => {
|
||||
it('listet Laufwerke und liest ihren Zustand', async () => {
|
||||
// koffi erst hier laden — beim Einsammeln der Tests bleibt alles rein.
|
||||
const { win32Api } = await import('../src/kern/laufwerk/win32')
|
||||
const { geraeteInfo, listOptischeLaufwerke } = await import('../src/kern/laufwerk/disc')
|
||||
|
||||
const api = win32Api()
|
||||
const geraete = listOptischeLaufwerke(api)
|
||||
console.log(`MESSUNG optische Laufwerke: ${JSON.stringify(geraete)}`)
|
||||
expect(Array.isArray(geraete)).toBe(true)
|
||||
|
||||
for (const geraet of geraete) {
|
||||
const info = geraeteInfo(api, geraet)
|
||||
console.log(
|
||||
`MESSUNG ${info.name} status=${info.status} typ=${info.typ} ` +
|
||||
`groesse=${info.groesseBytes} modell="${info.modell}" ` +
|
||||
`serial=${info.seriennummer}${info.grund.length > 0 ? ` grund="${info.grund}"` : ''}`,
|
||||
)
|
||||
expect(info.id.length).toBeGreaterThan(0)
|
||||
expect(['ready', 'empty', 'unknown']).toContain(info.status)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Ohne Messung ist die Datei trotzdem ein gültiges Test-Modul.
|
||||
describe.skipIf(messen)('Messung übersprungen', () => {
|
||||
it('läuft nur mit RIPPY_MESSUNG=1 unter Windows', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user