Files
rippy/rippy-windows/test/laufwerk.test.ts
T
HitonabiandClaude Fable 5 a45069af5e
Ampel / ampel (push) Successful in 1m21s
feat(v5): Etappe W-1 — Laufwerk: Wache, Typ, Auswerfen mit Nachsehen
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>
2026-08-30 17:28:46 +02:00

283 lines
11 KiB
TypeScript

// 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')
})
})