feat(v5): Etappe W-0 — das Geruest steht
Electron 44 + TypeScript, drei Prozesse (haupt / kern als utilityProcess / fenster mit React), Nachrichten-Schema mit Pruef-Funktionen, node:sqlite als einzige Datenbank-Stelle (kern/speicher/db.ts, § 6.7), Prozess-Leine im Haupt (Job Object aus beweise/leine.js — Begruendung fuer den Ort im Dateikopf), MessagePort direkt Fenster<->Kern, Einzelinstanz-Sperre, Kern-Neustart-Wache, strikte CSP im gebauten Fenster. Wächter-Tests nach § 4.3: koffi nur an zwei benannten Orten (R1), kein leerer catch und kein catch-mit-Leerwert (R2/R4), kein HTTP-Server (§ 4.1), node:sqlite nur in db.ts (§ 6.7). Dazu Datenbank- und Schema-Tests: 18/18 gruen, Typpruefung in drei Kontexten. Der W-0-Beweis (npm run smoke, echtes Programm): SMOKE: OK — fenster=geladen kern=pid:13676,node:24.18.1 datenbank=ok leine=gesetzt pong=ok version=5.0.0-w0 Nachgemessen ausserdem: taskkill /F auf den Haupt-Prozess (ohne /T, der Absturz-Fall aus rc10) — der Kern stirbt mit. Bau-Fallen dokumentiert (BAUEN.md): npm 11 blockt Electrons Install-Skript (§ 3.5), plugin-react 6 verlangt Vite 8 (deshalb 5.2), TypeScript bewusst auf 5.9.3 gepinnt statt tsgo 7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
564bd17f84
commit
c539e65cd2
@@ -0,0 +1,73 @@
|
||||
// Tests für kern/speicher/db.ts — laufen mit dem System-Node (node:sqlite
|
||||
// gibt es dort wie in Electrons Node, KONZEPT-WINDOWS.md § 3.4).
|
||||
import { mkdtempSync, rmSync, existsSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { Datenbank } from '../src/kern/speicher/db'
|
||||
|
||||
let aufraeumen: Array<() => void> = []
|
||||
|
||||
afterEach(() => {
|
||||
// LIFO — zuletzt Registriertes zuerst: die Datenbank muss ZU sein, bevor
|
||||
// ihr Ordner gelöscht wird (Windows verweigert das Löschen offener Dateien).
|
||||
for (const weg of aufraeumen.splice(0).reverse()) weg()
|
||||
})
|
||||
|
||||
function frischerPfad(): string {
|
||||
const ordner = mkdtempSync(join(tmpdir(), 'rippy-db-test-'))
|
||||
aufraeumen.push(() => rmSync(ordner, { recursive: true, force: true }))
|
||||
return join(ordner, 'unterordner', 'rippy.db')
|
||||
}
|
||||
|
||||
describe('Datenbank', () => {
|
||||
it('legt Datei samt fehlender Ordner an und meldet Schema-Version', () => {
|
||||
const pfad = frischerPfad()
|
||||
const db = new Datenbank(pfad)
|
||||
aufraeumen.push(() => db.schliessen())
|
||||
expect(existsSync(pfad)).toBe(true)
|
||||
expect(db.schemaVersion()).toBe(1)
|
||||
})
|
||||
|
||||
it('schreibt und liest eine Einstellung', () => {
|
||||
const db = new Datenbank(':memory:')
|
||||
aufraeumen.push(() => db.schliessen())
|
||||
db.einstellungSchreiben('ablage', 'E:\\Rippy')
|
||||
expect(db.einstellungLesen('ablage')).toBe('E:\\Rippy')
|
||||
})
|
||||
|
||||
it('überschreibt eine vorhandene Einstellung', () => {
|
||||
const db = new Datenbank(':memory:')
|
||||
aufraeumen.push(() => db.schliessen())
|
||||
db.einstellungSchreiben('ablage', 'C:\\alt')
|
||||
db.einstellungSchreiben('ablage', 'E:\\neu')
|
||||
expect(db.einstellungLesen('ablage')).toBe('E:\\neu')
|
||||
})
|
||||
|
||||
it('unbekannter Schlüssel ist null, kein Fehler und kein Leerwert', () => {
|
||||
// R2: „nicht gefunden" ist eine Aussage (null) — kein erfundener
|
||||
// Leerstring, der als Wert gelesen würde.
|
||||
const db = new Datenbank(':memory:')
|
||||
aufraeumen.push(() => db.schliessen())
|
||||
expect(db.einstellungLesen('gibt-es-nicht')).toBeNull()
|
||||
})
|
||||
|
||||
it('bleibt über Schließen und Neuöffnen erhalten', () => {
|
||||
const pfad = frischerPfad()
|
||||
const erste = new Datenbank(pfad)
|
||||
erste.einstellungSchreiben('letzter_start', '2026-08-30T12:00:00Z')
|
||||
erste.schliessen()
|
||||
const zweite = new Datenbank(pfad)
|
||||
aufraeumen.push(() => zweite.schliessen())
|
||||
expect(zweite.einstellungLesen('letzter_start')).toBe('2026-08-30T12:00:00Z')
|
||||
})
|
||||
|
||||
it('wirft bei nicht anlegbarem Pfad, statt still weiterzumachen', () => {
|
||||
// R4: Der Aufrufer (kern/index.ts) fängt das und MELDET es — aber die
|
||||
// Datenbank selbst darf so einen Zustand nie verschlucken.
|
||||
// NUL ist unter Windows als Ordnername unzulässig; unter Linux (CI)
|
||||
// scheitert das Anlegen unterhalb einer Datei genauso zuverlässig.
|
||||
const kaputt = join(process.platform === 'win32' ? 'C:\\NUL<>:' : '/dev/null/x', 'rippy.db')
|
||||
expect(() => new Datenbank(kaputt)).toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
// Tests für das Nachrichten-Schema (gemeinsam/nachrichten.ts). Die
|
||||
// Prüf-Funktionen sind die Eingangstür beider Prozesse — was hier
|
||||
// durchkommt, wird drüben ungeprüft verwendet.
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
istDbStatus,
|
||||
istFensterNachricht,
|
||||
istHauptNachricht,
|
||||
istKernNachricht,
|
||||
} from '../src/gemeinsam/nachrichten'
|
||||
|
||||
describe('istKernNachricht', () => {
|
||||
it('akzeptiert kern-bereit mit vollständigen Feldern', () => {
|
||||
expect(
|
||||
istKernNachricht({
|
||||
art: 'kern-bereit',
|
||||
pid: 1234,
|
||||
nodeVersion: '24.18.1',
|
||||
datenbank: { ok: true, pfad: 'C:\\x\\rippy.db' },
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('akzeptiert kern-bereit auch mit Datenbank-Fehler', () => {
|
||||
expect(
|
||||
istKernNachricht({
|
||||
art: 'kern-bereit',
|
||||
pid: 1,
|
||||
nodeVersion: '24.18.1',
|
||||
datenbank: { ok: false, fehler: 'Platte voll' },
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('akzeptiert pong und kern-fehler', () => {
|
||||
expect(istKernNachricht({ art: 'pong', zeit: 17 })).toBe(true)
|
||||
expect(istKernNachricht({ art: 'kern-fehler', text: 'kaputt' })).toBe(true)
|
||||
})
|
||||
|
||||
it('lehnt Unvollständiges und Fremdes ab', () => {
|
||||
expect(istKernNachricht(null)).toBe(false)
|
||||
expect(istKernNachricht('kern-bereit')).toBe(false)
|
||||
expect(istKernNachricht({ art: 'kern-bereit', pid: 1 })).toBe(false)
|
||||
expect(istKernNachricht({ art: 'pong', zeit: 'gleich' })).toBe(false)
|
||||
expect(istKernNachricht({ art: 'unbekannt' })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('istDbStatus', () => {
|
||||
it('verlangt zum ok-Wert den Pfad, zum Fehler den Text', () => {
|
||||
expect(istDbStatus({ ok: true, pfad: 'x' })).toBe(true)
|
||||
expect(istDbStatus({ ok: false, fehler: 'y' })).toBe(true)
|
||||
expect(istDbStatus({ ok: true })).toBe(false)
|
||||
expect(istDbStatus({ ok: false })).toBe(false)
|
||||
expect(istDbStatus({ ok: 'ja' })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('istFensterNachricht', () => {
|
||||
it('akzeptiert nur ping mit Zeit', () => {
|
||||
expect(istFensterNachricht({ art: 'ping', zeit: 1 })).toBe(true)
|
||||
expect(istFensterNachricht({ art: 'ping' })).toBe(false)
|
||||
expect(istFensterNachricht({ art: 'pong', zeit: 1 })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('istHauptNachricht', () => {
|
||||
it('akzeptiert nur fenster-port', () => {
|
||||
expect(istHauptNachricht({ art: 'fenster-port' })).toBe(true)
|
||||
expect(istHauptNachricht({ art: 'port' })).toBe(false)
|
||||
expect(istHauptNachricht(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
// Die Wächter — mechanische Prüfung der Regeln aus KONZEPT-WINDOWS.md § 4.3.
|
||||
// Sie LESEN Quelltext, sie führen nichts aus. Jede Regel ist aus einem
|
||||
// bezahlten Fehler abgeleitet; die Fundstelle steht jeweils dabei.
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
||||
import { join, relative, sep } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const WURZEL = join(__dirname, '..')
|
||||
const SRC = join(WURZEL, 'src')
|
||||
|
||||
function quelldateien(ordner: string): string[] {
|
||||
const ergebnis: string[] = []
|
||||
for (const name of readdirSync(ordner)) {
|
||||
const pfad = join(ordner, name)
|
||||
if (statSync(pfad).isDirectory()) {
|
||||
ergebnis.push(...quelldateien(pfad))
|
||||
} else if (/\.(ts|tsx)$/.test(name)) {
|
||||
ergebnis.push(pfad)
|
||||
}
|
||||
}
|
||||
return ergebnis
|
||||
}
|
||||
|
||||
function relativPosix(pfad: string): string {
|
||||
return relative(WURZEL, pfad).split(sep).join('/')
|
||||
}
|
||||
|
||||
const dateien = quelldateien(SRC).map((pfad) => ({
|
||||
pfad: relativPosix(pfad),
|
||||
inhalt: readFileSync(pfad, 'utf8'),
|
||||
}))
|
||||
|
||||
describe('R1 — Win32 lebt nur an den zwei benannten Orten', () => {
|
||||
// § 4.3 R1: kein koffi-Import irgendwo sonst. Der zweite Ort ist die
|
||||
// Prozess-Leine im Haupt — Begründung im Kopf von src/haupt/leine.ts:
|
||||
// Das Job-Handle muss bei dem Prozess liegen, dessen Tod alles mitreißt.
|
||||
const erlaubt = new Set(['src/haupt/leine.ts', 'src/kern/laufwerk/win32.ts'])
|
||||
|
||||
it('koffi wird nirgendwo sonst angefasst', () => {
|
||||
const verstoesse = dateien
|
||||
.filter((d) => !erlaubt.has(d.pfad))
|
||||
.filter((d) => /from\s+['"]koffi['"]|require\(\s*['"]koffi['"]\s*\)/.test(d.inhalt))
|
||||
.map((d) => d.pfad)
|
||||
expect(verstoesse).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('R2/R4 — kein Fehlschlag verschwindet still', () => {
|
||||
it('kein leerer catch-Block', () => {
|
||||
// R4: „Kein stilles catch {}" — ein except-pass hat einmal eine Stunde
|
||||
// gekostet (AGENTS.md). Wer einen Fehler bewusst trägt, schreibt hin warum.
|
||||
const verstoesse = dateien
|
||||
.filter((d) => /catch\s*(\([^)]*\))?\s*\{\s*\}/.test(d.inhalt))
|
||||
.map((d) => d.pfad)
|
||||
expect(verstoesse).toEqual([])
|
||||
})
|
||||
|
||||
it('kein catch, das leere Listen/Objekte als Antwort erfindet', () => {
|
||||
// R2: Fünfmal catch(() => []) im alten UI — jeder verpasste Abruf hieß
|
||||
// „es gibt keine Jobs", die Liste leerte sich im Sekundentakt.
|
||||
const muster = /catch\s*(\([^)]*\))?\s*\{\s*return\s+(\[\s*\]|\{\s*\})/
|
||||
const pfeilMuster = /catch\s*\(\s*\(\s*\)\s*=>\s*(\[\s*\]|\{\s*\})\s*\)/
|
||||
const verstoesse = dateien
|
||||
.filter((d) => muster.test(d.inhalt) || pfeilMuster.test(d.inhalt))
|
||||
.map((d) => d.pfad)
|
||||
expect(verstoesse).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('§ 4.1 — kein HTTP-Server, kein Port, kein localhost', () => {
|
||||
it('niemand öffnet einen Server oder lauscht auf einem Port', () => {
|
||||
// Ein lokaler Webserver in einer Einzelplatz-Anwendung ist eine offene
|
||||
// Tür ohne Gegenwert (§ 4.1/§ 14). ELECTRON_RENDERER_URL (dev-Server von
|
||||
// electron-vite) ist Werkzeug, kein Programmteil — der Treffer auf den
|
||||
// reinen Variablennamen ist erlaubt.
|
||||
const muster = /createServer\s*\(|\.listen\s*\(\s*\d|from\s+['"]express['"]|localhost:\d/
|
||||
const verstoesse = dateien.filter((d) => muster.test(d.inhalt)).map((d) => d.pfad)
|
||||
expect(verstoesse).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('§ 6.7 — EINE Quelle für Einstellungen', () => {
|
||||
it('node:sqlite wird nur in kern/speicher/db.ts angefasst', () => {
|
||||
// Der rc11-Fund: Oberfläche schrieb in die Datenbank, der Betrieb las
|
||||
// aus einer Datei, die niemand schrieb. Hier ist die Datenbank EIN Modul.
|
||||
const verstoesse = dateien
|
||||
.filter((d) => d.pfad !== 'src/kern/speicher/db.ts')
|
||||
.filter((d) => /['"]node:sqlite['"]/.test(d.inhalt))
|
||||
.map((d) => d.pfad)
|
||||
expect(verstoesse).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user