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>
283 lines
11 KiB
TypeScript
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')
|
|
})
|
|
})
|