Hustler-Pixelwelt T1+T2: Import der Zeichenfläche (Bilder, CSS mit hk-Vorsatz, Geometrie), helles Haus mit grauen Raum-Ebenen, Press Start 2P selbst gehostet

- scripts/hustler-import.mjs liest pixelwelt-v2 (+ pixelwelt-woms/haus) und schreibt src/assets/hustler, src/styles/hustler/{hochhaus,keller,pult}.css und src/lib/hustlerWelt.js; idempotent
- haus.png mit allen Räumen hell, acht graue Ebenen für die Such-Räume (GRAUE_RAEUME, .hk-raum-grau-<raum>)
- Press Start 2P lateinisch aus @fontsource/press-start-2p@5.2.7 (OFL), noch nirgends importiert
- 40 neue Tests (Assets, Geometrie, CSS-Umformung)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-22 07:55:25 +00:00
1 parent 6e24868995
commit 8bd365b1fc
195 files changed
+2697

No files matched your search

+294
View File
@@ -0,0 +1,294 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
// Umformer und Planer aus scripts/hustler-import.mjs: CSS-Zerlegung (Kommentare, Zeichenketten, verschachtelte
// @media, @keyframes), Selektoren, Animationsnamen, Blob-Adressen, Fehlermeldungen und Wiederholbarkeit.
const { wandleCss, wandleSelektor, baue, pruefsumme, schreibe, ImportFehler } = await import('../../scripts/hustler-import.mjs')
const V2 = process.env.HUSTLER_V2 || '/root/reports/workers-2026-10-16/pixelwelt-v2'
const hatV2 = existsSync(path.join(V2, 'entwurf/Main.dc.html'))
// Löscht nur Ordner, die dieser Test selbst unter os.tmpdir() mit dem Vorsatz hustler- angelegt hat
function loesche(ordner) {
assert.equal(path.dirname(ordner), os.tmpdir(), 'nur Ordner direkt unter dem Temp-Verzeichnis')
assert.ok(path.basename(ordner).startsWith('hustler-'), 'nur selbst angelegte Ordner')
rmSync(ordner, { recursive: true, force: true })
}
const BLOB = (id) => `../../assets/hustler/${id}.png`
const umformen = (css, extra = {}) => wandleCss(css, { wurzel: 'hk-t', kfVorsatz: 'hk-x-', blobPfad: BLOB, ...extra })
const nur = (css, extra) => umformen(css, extra).css
test('Wurzel: body, html und :root werden zur Wurzelklasse, doppelte fallen zusammen', () => {
assert.equal(nur('body{margin:0}'), '.hk-t{margin:0}')
assert.equal(nur('html,body{margin:0}'), '.hk-t{margin:0}')
assert.equal(nur(':root{--a:1}'), '.hk-t{--a:1}')
assert.equal(nur('html body{margin:0}'), '.hk-t{margin:0}')
assert.equal(nur('html > body{margin:0}'), '.hk-t{margin:0}')
assert.equal(nur('body.dunkel{color:#fff}'), '.hk-t.hk-dunkel{color:#fff}')
assert.equal(nur('body > .a, html .b{x:1}'), '.hk-t > .hk-a,.hk-t .hk-b{x:1}')
assert.equal(nur('body::selection{x:1}'), '.hk-t::selection{x:1}')
})
test('andere Selektoren stehen unter der Wurzel, auch Element-Selektoren', () => {
assert.equal(nur('a{color:red}'), '.hk-t a{color:red}')
assert.equal(nur('a:hover{color:red}'), '.hk-t a:hover{color:red}')
assert.equal(nur('a, button{x:1}'), '.hk-t a,.hk-t button{x:1}')
assert.equal(nur('*{box-sizing:border-box}'), '.hk-t *{box-sizing:border-box}')
assert.equal(nur('button:focus-visible{outline:0}'), '.hk-t button:focus-visible{outline:0}')
assert.equal(nur('.suche::-webkit-search-cancel-button{display:none}'), '.hk-t .hk-suche::-webkit-search-cancel-button{display:none}')
assert.equal(nur('li:nth-child(2n+1){x:1}'), '.hk-t li:nth-child(2n+1){x:1}')
})
test('jede Klasse im Selektor bekommt hk- (auch in :not und nach Kombinatoren), Deklarationen bleiben', () => {
assert.equal(nur('.panel{width:.5em}'), '.hk-t .hk-panel{width:.5em}')
assert.equal(nur('a:not(.knopf):hover{color:#7a2e3a}'), '.hk-t a:not(.hk-knopf):hover{color:#7a2e3a}')
assert.equal(nur('.a.b > .c + .d ~ .e .f{x:1}'), '.hk-t .hk-a.hk-b > .hk-c + .hk-d ~ .hk-e .hk-f{x:1}')
assert.equal(nur('.stempel:not(.stempel-frage):hover{background:#fff}'), '.hk-t .hk-stempel:not(.hk-stempel-frage):hover{background:#fff}')
assert.equal(nur('.a{transition:opacity .12s steps(2); margin:0 .5em; font-size:.9rem}'), '.hk-t .hk-a{transition:opacity .12s steps(2); margin:0 .5em; font-size:.9rem}')
assert.equal(nur('.a{content:".b .c"}'), '.hk-t .hk-a{content:".b .c"}')
})
test('Attributselektoren und Zeichenketten im Selektor bleiben unberührt, Klassen danach werden trotzdem ergänzt', () => {
assert.equal(nur('a[href^=".x"] .c{x:1}'), '.hk-t a[href^=".x"] .hk-c{x:1}')
assert.equal(nur('[data-a="b.c"].d{x:1}'), '.hk-t [data-a="b.c"].hk-d{x:1}')
assert.equal(nur('input[type=text]:focus .z{x:1}'), '.hk-t input[type=text]:focus .hk-z{x:1}')
})
test('html/body/:root mitten im Selektor werden nicht geraten, sondern gemeldet', () => {
assert.throws(() => nur('.dunkel body{x:1}'), (e) => e instanceof ImportFehler && /mitten im Selektor/.test(e.message))
assert.throws(() => wandleSelektor('a:not(body)', 'hk-t'), ImportFehler)
assert.throws(() => wandleSelektor(' ', 'hk-t'), /leerer Selektor/)
})
test('@keyframes: Namen bekommen den Vorsatz, Animationen und animation-name ziehen mit (nur ganze, bekannte Namen)', () => {
const { css, keyframes } = umformen(`
@keyframes funkeln{0%,100%{opacity:.25}50%{opacity:1}}
@keyframes k24x4{to{background-position:-96px 0}}
.a{animation:funkeln 3.2s steps(4) infinite}
.b{animation:k24x4 600ms steps(4) infinite, funkeln 1s linear -0.5s infinite alternate}
.c{animation-name:funkeln,k24x4; animation-duration:.5s}
.d{animation:none!important}
.e{animation:funkeln2 1s linear infinite}
.f{-webkit-animation:funkeln 1s}
`)
assert.deepEqual(keyframes, ['hk-x-funkeln', 'hk-x-k24x4'])
const zeilen = css.split('\n')
assert.equal(zeilen[0], '@keyframes hk-x-funkeln{0%,100%{opacity:.25}50%{opacity:1}}')
assert.equal(zeilen[1], '@keyframes hk-x-k24x4{to{background-position:-96px 0}}')
assert.equal(zeilen[2], '.hk-t .hk-a{animation:hk-x-funkeln 3.2s steps(4) infinite}')
assert.equal(zeilen[3], '.hk-t .hk-b{animation:hk-x-k24x4 600ms steps(4) infinite, hk-x-funkeln 1s linear -0.5s infinite alternate}')
assert.equal(zeilen[4], '.hk-t .hk-c{animation-name:hk-x-funkeln,hk-x-k24x4; animation-duration:.5s}')
assert.equal(zeilen[5], '.hk-t .hk-d{animation:none!important}')
assert.equal(zeilen[6], '.hk-t .hk-e{animation:funkeln2 1s linear infinite}', 'unbekannte Namen (nur Teilstring eines Keyframes) bleiben')
assert.equal(zeilen[7], '.hk-t .hk-f{-webkit-animation:hk-x-funkeln 1s}')
})
test('Animationen: Schlüsselwörter, steps(1,end) und Zahlen wie .5s oder -3s bleiben, Namen nach einer Funktion werden trotzdem umbenannt', () => {
const { css } = umformen('@keyframes dampfzeit{0%{opacity:1}} .a{animation:k10 1000ms steps(4) infinite, dampfzeit 3200ms steps(1,end) infinite -.5s}')
assert.match(css, /\.hk-t \.hk-a\{animation:k10 1000ms steps\(4\) infinite, hk-x-dampfzeit 3200ms steps\(1,end\) infinite -\.5s\}/)
const { css: c2 } = umformen('@keyframes a{to{x:1}} .b{animation:cubic-bezier(.2,.7,.2,1) a 2s ease-in-out infinite}')
assert.match(c2, /animation:cubic-bezier\(\.2,\.7,\.2,1\) hk-x-a 2s ease-in-out infinite/)
})
test('verschachtelte @media/@supports: Regeln darin werden wie oben umgeformt, die Bedingung bleibt', () => {
const { css } = umformen(`
@media (prefers-reduced-motion: reduce){.a,.b{animation:none} body{margin:1px} @supports (display:grid){.c{display:grid}}}
@media (min-width:720px){a:hover{color:red}}
`)
assert.equal(css, [
'@media (prefers-reduced-motion: reduce){',
'.hk-t .hk-a,.hk-t .hk-b{animation:none}',
'.hk-t{margin:1px}',
'@supports (display:grid){',
'.hk-t .hk-c{display:grid}',
'}',
'}',
'@media (min-width:720px){',
'.hk-t a:hover{color:red}',
'}',
].join('\n'))
})
test('Kommentare und Zeichenketten: Klammern und Semikola in Texten stören nicht', () => {
const { css } = umformen(`
/* Kopf { mit Klammer } */
.a{content:"}{;"; /* mitten { drin */ color:red}
/* .b{x:1} */
.c::after{content:'a\\'b}'}
.d{font-family:'Pixelify Sans',monospace}
`)
assert.equal(css, [
'.hk-t .hk-a{content:"}{;"; color:red}',
".hk-t .hk-c::after{content:'a\\'b}'}",
".hk-t .hk-d{font-family:'Pixelify Sans',monospace}",
].join('\n'))
assert.throws(() => umformen('.a{content:"offen}'), /Zeichenkette ohne Ende/)
assert.throws(() => umformen('.a{color:red} /* offen'), /Kommentar ohne Ende/)
})
test('@font-face und @import entfallen samt Inhalt (deren Blob-Adressen werden gar nicht erst aufgelöst)', () => {
const aufgeloest = []
const { css } = umformen('@import url("x.css");\n@font-face{font-family:"X";src:url(/_blob/zzz)}\n.a{color:red}\n@import "y.css";', { blobPfad: (id) => { aufgeloest.push(id); return BLOB(id) } })
assert.equal(css, '.hk-t .hk-a{color:red}')
assert.deepEqual(aufgeloest, [])
})
test('Blob-Adressen: url(…) in allen Schreibweisen und in border-image/mask werden zum Asset-Pfad', () => {
const { css } = umformen(`
.a{background:url(/_blob/abc1) 0 0 / 10px repeat, linear-gradient(red, blue)}
.b{background-image:url("/_blob/abc2")}
.c{background-image:url( '/_blob/abc3' )}
.d{border-image:url(/_blob/abc4) 8 fill stretch}
.e{-webkit-mask-image:url(/_blob/abc5); mask-image:url(/_blob/abc5)}
.f{background:URL(/_blob/abc6)}
.g{background:url(data:image/png;base64,AAAA)}
`)
assert.match(css, /\.hk-t \.hk-a\{background:url\(\.\.\/\.\.\/assets\/hustler\/abc1\.png\) 0 0 \/ 10px repeat, linear-gradient\(red, blue\)\}/)
assert.match(css, /\.hk-b\{background-image:url\("\.\.\/\.\.\/assets\/hustler\/abc2\.png"\)\}/)
assert.match(css, /\.hk-c\{background-image:url\( '\.\.\/\.\.\/assets\/hustler\/abc3\.png' \)\}|\.hk-c\{background-image:url\('\.\.\/\.\.\/assets\/hustler\/abc3\.png'\)\}/)
assert.match(css, /\.hk-d\{border-image:url\(\.\.\/\.\.\/assets\/hustler\/abc4\.png\) 8 fill stretch\}/)
assert.match(css, /\.hk-e\{-webkit-mask-image:url\(\.\.\/\.\.\/assets\/hustler\/abc5\.png\); mask-image:url\(\.\.\/\.\.\/assets\/hustler\/abc5\.png\)\}/)
assert.match(css, /\.hk-f\{background:url\(\.\.\/\.\.\/assets\/hustler\/abc6\.png\)\}/)
assert.match(css, /\.hk-g\{background:url\(data:image\/png;base64,AAAA\)\}/, 'data:-Adressen mit ; bleiben ganz')
assert.ok(!css.includes('/_blob/'))
})
test('Blob-Adressen: unbekannte Id und unerwartete Schreibweise brechen mit klarer Meldung ab', () => {
const streng = (id) => { throw new ImportFehler(`Im CSS steht /_blob/${id}, aber blobs.json kennt diese Adresse nicht.`) }
assert.throws(() => umformen('.a{background:url(/_blob/unbekannt)}', { blobPfad: streng }), /kennt diese Adresse nicht/)
assert.throws(() => umformen('.a{background:url(/_blob/ab/cd)}'), /unerwartete Blob-Adresse/)
})
test('nicht unterstützte Konstrukte werden gemeldet statt falsch umgeformt', () => {
assert.throws(() => umformen('.a{.b{x:1}}'), /verschachtelte Regel/)
assert.throws(() => umformen('@page{margin:0}'), /nicht unterstützte At-Regel/)
assert.throws(() => umformen('.a{color:red'), ImportFehler)
assert.throws(() => umformen('.a{color}'), /Deklaration ohne/)
assert.throws(() => umformen('}'), /unerwartete schließende Klammer/)
assert.throws(() => umformen('@namespace svg url(x);'), /nicht unterstützte Anweisung/)
})
test('Keyframe-Namen aus @keyframes in @media und mit Vendor-Präfix werden ebenfalls erfasst', () => {
const { css, keyframes } = umformen('@media (min-width:1px){@keyframes w{to{x:1}}} @-webkit-keyframes w{to{x:1}} .a{animation:w 1s}')
assert.deepEqual(keyframes, ['hk-x-w'])
assert.match(css, /\.hk-t \.hk-a\{animation:hk-x-w 1s\}/)
assert.match(css, /@-webkit-keyframes hk-x-w\{to\{x:1\}\}/)
})
// ───────────────────────── Planer gegen die echte Zeichenfläche (übersprungen, wenn sie fehlt) ─────────────────────────
const opt = { skip: hatV2 ? false : `pixelwelt-v2 nicht gefunden (${V2}); HUSTLER_V2 setzen` }
test('baue: zwei Läufe liefern byte-gleiche Ausgaben (gleiche Prüfsumme); je Schlüssel der Tabelle eine PNG, dazu 3 CSS + 1 JS', opt, async () => {
const quellen = JSON.parse(readFileSync(new URL('../../scripts/hustler-import/quellen.json', import.meta.url), 'utf8'))
const a = await baue({ v2: V2 })
const b = await baue({ v2: V2 })
assert.equal(pruefsumme(a.dateien), pruefsumme(b.dateien))
const namen = [...a.dateien.keys()]
const verschieden = new Set(Object.values(quellen).flatMap((t) => Object.values(t)))
assert.equal(namen.filter((n) => n.endsWith('.png')).length, verschieden.size)
assert.deepEqual(namen.filter((n) => !n.endsWith('.png')).sort(), ['src/lib/hustlerWelt.js', 'src/styles/hustler/hochhaus.css', 'src/styles/hustler/keller.css', 'src/styles/hustler/pult.css'])
for (const name of ['hochhaus', 'keller', 'pult']) {
assert.equal(a.bericht.bildschirme[name].dateien, Object.keys(quellen[name]).length, `${name}: jedes Bild der Tabelle wird im CSS benutzt`)
}
// gemeinsam benutzte Bilder liegen einmal, die Zielpfade spiegeln die Ordner ohne „entwurf/“
assert.ok(a.dateien.has('src/assets/hustler/ui/knopf.png'))
assert.ok(a.dateien.has('src/assets/hustler/keller/ui/robo.png'))
assert.ok(a.dateien.has('src/assets/hustler/ui/robo.png'))
assert.ok(a.dateien.has('src/assets/hustler/pult/eigen/lichterkette-0.png'))
assert.ok(![...a.dateien.keys()].some((n) => n.includes('/entwurf/')))
})
test('baue: die Dateien im Repo entsprechen dem, was das Skript aus pixelwelt-v2 erzeugt (außer wenn mit pixelwelt-woms gebaut wurde)', opt, async () => {
const plan = await baue({ v2: V2 })
const repo = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../..')
const cssDatei = readFileSync(path.join(repo, 'src/styles/hustler/keller.css'), 'utf8')
assert.equal(cssDatei, plan.dateien.get('src/styles/hustler/keller.css'))
const pultDatei = readFileSync(path.join(repo, 'src/styles/hustler/pult.css'), 'utf8')
assert.equal(pultDatei, plan.dateien.get('src/styles/hustler/pult.css'))
})
test('baue: fehlende Schlüssel, Dateien und falsche Schlüssel brechen mit deutscher Meldung ab', opt, async () => {
const quellen = JSON.parse(readFileSync(new URL('../../scripts/hustler-import/quellen.json', import.meta.url), 'utf8'))
const ohneKey = structuredClone(quellen)
delete ohneKey.pult.klammer
await assert.rejects(baue({ v2: V2, quellen: ohneKey }), (e) => e instanceof ImportFehler && /Schlüssel „klammer“.*fehlt in scripts\/hustler-import\/quellen\.json/.test(e.message))
const ohneDatei = structuredClone(quellen)
ohneDatei.keller.keller = 'keller/szene/gibt-es-nicht.png'
await assert.rejects(baue({ v2: V2, quellen: ohneDatei }), (e) => e instanceof ImportFehler && /Quelldatei fehlt: keller\/szene\/gibt-es-nicht\.png/.test(e.message))
const falscherKey = structuredClone(quellen)
falscherKey.hochhaus['gibt-es-nicht'] = 'haus/haus.png'
await assert.rejects(baue({ v2: V2, quellen: falscherKey }), /Schlüssel „gibt-es-nicht“ steht nicht in/)
const unbekannterBildschirm = { ...structuredClone(quellen), handy: {} }
await assert.rejects(baue({ v2: V2, quellen: unbekannterBildschirm }), /unbekannter Bildschirm „handy“/)
const gefaehrlich = structuredClone(quellen)
gefaehrlich.pult.pult = '../../etc/passwd.png'
await assert.rejects(baue({ v2: V2, quellen: gefaehrlich }), /ungültiger Quellpfad/)
await assert.rejects(baue({ v2: path.join(V2, 'gibt-es-nicht') }), /pixelwelt-v2 existiert nicht/)
})
test('baue: eine veränderte Datei fällt über hochgeladen.sha256 auf; ein veralteter Blob im CSS ebenso', opt, async () => {
const tmp = mkdtempSync(path.join(os.tmpdir(), 'hustler-v2-'))
try {
cpSync(V2, tmp, { recursive: true })
const datei = path.join(tmp, 'detail/dampf.png')
const bytes = readFileSync(datei)
bytes[bytes.length - 1] ^= 0xff
writeFileSync(datei, bytes)
await assert.rejects(baue({ v2: tmp }), (e) => e instanceof ImportFehler && /Prüfsumme stimmt nicht: detail\/dampf\.png/.test(e.message))
// Blob-Adresse im CSS, die blobs.json nicht kennt
writeFileSync(datei, readFileSync(path.join(V2, 'detail/dampf.png')))
const html = path.join(tmp, 'keller/entwurf/BotLeads.dc.html')
writeFileSync(html, readFileSync(html, 'utf8').replace('</style>', '.neu{background:url(/_blob/ffffffffffffffffffffffffffffffff)}</style>'))
await assert.rejects(baue({ v2: tmp }), (e) => e instanceof ImportFehler && /BotLeads\.dc\.html steht \/_blob\/ffffffffffffffffffffffffffffffff, aber .*blobs\.json kennt diese Adresse nicht/.test(e.message))
} finally {
loesche(tmp)
}
})
test('baue mit pixelwelt-woms: gleichnamige Haus-Dateien ersetzen die aus v2, graue Räume werden Klassen und Daten, alte Ebenen verschwinden wieder', opt, async () => {
const tmp = mkdtempSync(path.join(os.tmpdir(), 'hustler-woms-'))
const wurzel = mkdtempSync(path.join(os.tmpdir(), 'hustler-wurzel-'))
try {
mkdirSync(path.join(tmp, 'haus'))
cpSync(path.join(V2, 'haus/schild-a.png'), path.join(tmp, 'haus/haus.png'))
cpSync(path.join(V2, 'ui/panel.png'), path.join(tmp, 'haus/raum-grau-netz.png'))
cpSync(path.join(V2, 'ui/knopf.png'), path.join(tmp, 'haus/raum-grau-kleinanzeigen.png'))
writeFileSync(path.join(tmp, 'haus/manifest.json'), JSON.stringify({ graueRaeume: { netz: { x: 1040, y: 558, w: 24, h: 24 }, kleinanzeigen: { file: 'raum-grau-kleinanzeigen.png', x: 1140, y: 558 } } }))
const ohne = await baue({ v2: V2 })
const mit = await baue({ v2: V2, woms: tmp })
assert.notEqual(pruefsumme(ohne.dateien), pruefsumme(mit.dateien))
assert.ok(mit.dateien.get('src/assets/hustler/haus/haus.png').equals(readFileSync(path.join(V2, 'haus/schild-a.png'))), 'haus.png stammt aus pixelwelt-woms')
assert.ok(mit.dateien.get('src/assets/hustler/haus/haus-fenster-a.png').equals(ohne.dateien.get('src/assets/hustler/haus/haus-fenster-a.png')), 'andere Dateien bleiben aus v2')
assert.deepEqual(mit.bericht.graueRaeume, ['netz', 'kleinanzeigen'])
const css = mit.dateien.get('src/styles/hustler/hochhaus.css')
assert.match(css, /\.hk-hochhaus \.hk-raum-grau-netz\{position:absolute; left:1040px; top:558px; width:24px; height:24px; background-image:url\(\.\.\/\.\.\/assets\/hustler\/haus\/raum-grau-netz\.png\); image-rendering:pixelated; pointer-events:none\}/)
assert.match(css, /\.hk-hochhaus \.hk-raum-grau-kleinanzeigen\{position:absolute; left:1140px; top:558px; width:24px; height:24px;/)
const welt = mit.dateien.get('src/lib/hustlerWelt.js')
assert.match(welt, /export const GRAUE_RAEUME = Object\.freeze\(\{\s*netz: Object\.freeze\(\{ left: 1040, top: 558, width: 24, height: 24 \}\),\s*kleinanzeigen:/)
// schreiben, nochmal schreiben (nichts mehr zu tun), dann ohne woms: die grauen Ebenen werden entfernt
const z1 = schreibe(mit, wurzel)
assert.equal(z1.geschrieben, mit.dateien.size)
assert.equal(schreibe(mit, wurzel).geschrieben, 0)
assert.ok(existsSync(path.join(wurzel, 'src/assets/hustler/haus/raum-grau-netz.png')))
const z2 = schreibe(ohne, wurzel)
assert.deepEqual(z2.entfernt.sort(), ['raum-grau-kleinanzeigen.png', 'raum-grau-netz.png'])
assert.ok(!existsSync(path.join(wurzel, 'src/assets/hustler/haus/raum-grau-netz.png')))
// Fehler: kein Such-Raum, falsche Größe
writeFileSync(path.join(tmp, 'haus/manifest.json'), JSON.stringify({ graueRaeume: { bruecke: { x: 1, y: 1 } } }))
await assert.rejects(baue({ v2: V2, woms: tmp }), /„bruecke“ ist kein Such-Raum/)
writeFileSync(path.join(tmp, 'haus/manifest.json'), JSON.stringify({ graueRaeume: { netz: { x: 1040, y: 558, w: 99, h: 24 } } }))
await assert.rejects(baue({ v2: V2, woms: tmp }), /99×24 im Manifest, aber raum-grau-netz\.png ist 24×24/)
// ein Ordner ohne graueRaeume ändert an den Räumen nichts
writeFileSync(path.join(tmp, 'haus/manifest.json'), JSON.stringify({}))
assert.deepEqual((await baue({ v2: V2, woms: tmp })).bericht.graueRaeume, [])
} finally {
loesche(tmp)
loesche(wurzel)
}
})