Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion apps/tischler/app/[locale]/werkstoff/panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,19 @@ describe("Werkstoff-Panel: der Auftrag bestimmt, welche Bretter es gibt", () =>
});

describe("Werkstoff-Panel: die Szene zeigt nur, was zum Auftrag passt", () => {
it("Regelbetrieb: das ausgelieferte Bundle, unverändert — die Szene erscheint, mit der Lücke, ohne Fehlersatz", async () => {
// Der Weg des Menschen: Seite auf, Bundle so, wie es im Repo liegt. Kein verbogener Auftrag,
// kein eingesetztes Fixture — das ausgelieferte Paar (auftrag.json ↔ modell.glb) muss durch die
// Prüfung der Seite kommen. Gemessen war das (craft#48), gehalten hat es kein Test (R48b-5).
vi.stubGlobal("fetch", stub());
render(<Werkstoffseite />);
const szene = await waitFor(() => screen.getByTestId("szene"));
expect(szene.getAttribute("data-url")).toBe("/werkstoff-bundle/modell.glb");
expect(szene.getAttribute("data-luecken")).toBe("teil:Rw");
expect(document.body.textContent).not.toMatch(/⚠|anderes Erzeugnis|Kein 3D-Modell/);
expect(screen.getAllByRole("button", { name: /^(Bo|Se):/ })).toHaveLength(4);
});

it("nennt der Auftrag kein Modell: Schaltflächen bleiben, und der Satz sagt es", async () => {
vi.stubGlobal(
"fetch",
Expand All @@ -156,7 +169,8 @@ describe("Werkstoff-Panel: die Szene zeigt nur, was zum Auftrag passt", () => {
"fetch",
stub((url, roh) => (url.endsWith("modell.glb") ? roh : roh)),
);
// Das echte Bundle trägt lokal ein Modell (nicht in git); der Test nimmt das Fixture.
// Das ausgelieferte Modell liegt im Repo (Regelbetrieb-Test oben). Hier nimmt der Test bewusst
// das Mini-Fixture und setzt dessen Hash in den Auftrag: der Zweig »Tap in der Szene → Karte«.
const buf = new Uint8Array(readFileSync(join(__dirname, "../../../lib/werkstoff/fixtures/demo-mini.glb"))).buffer;
const hash = createHash("sha256").update(new Uint8Array(buf)).digest("hex");
const f = vi.mocked(globalThis.fetch);
Expand Down
37 changes: 37 additions & 0 deletions apps/tischler/lib/werkstoff/auftrag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,43 @@ describe("glbKnoten — was das 3D-Modell an Namen trägt", () => {
expect((fehler as Error).message).toMatch(muster);
expect((fehler as Error).name).not.toMatch(/RangeError|SyntaxError/);
});

// Die Trennlinie liegt bei 20 Bytes (der ganze Header). Darunter ist die Datei »zu kurz« — auch
// mit gültigem Magic; und die Länge wird VOR dem Magic geprüft, damit drei Bytes »xyz« nicht als
// »Magic fehlt« gemeldet werden. Parität zu cody-cad#71 (Länge vor Magic, Fälle 3/12/19/20).
const kopf = (n: number) => {
const b = new Uint8Array(n);
b.set([0x67, 0x6c, 0x54, 0x46].slice(0, Math.min(4, n))); // »glTF«
return b;
};
it.each([
["0 Bytes", new Uint8Array(0), /zu kurz.*0 Bytes, mindestens 20/],
["3 Bytes ohne Magic (Länge vor Magic)", Uint8Array.from([0x78, 0x79, 0x7a]), /zu kurz.*3 Bytes, mindestens 20/],
["12 Bytes mit gültigem Magic", kopf(12), /zu kurz.*12 Bytes, mindestens 20/],
["19 Bytes mit gültigem Magic (eins unter der Linie)", kopf(19), /zu kurz.*19 Bytes, mindestens 20/],
])("»%s« ist »zu kurz«, nicht »Magic fehlt«", (_name, bytes, muster) => {
let fehler: unknown;
try {
glbKnoten(bytes.buffer);
} catch (e) {
fehler = e;
}
expect(fehler).toBeInstanceOf(Error);
expect((fehler as Error).message).toMatch(muster);
expect((fehler as Error).message).not.toMatch(/Magic/);
expect((fehler as Error).name).not.toMatch(/RangeError/);
});

it("20 Bytes — genau der Header mit leerem JSON-Chunk — sind nicht »zu kurz«: der Fehler ist der leere Chunk", () => {
const b = kopf(20);
const dv = new DataView(b.buffer);
dv.setUint32(4, 2, true);
dv.setUint32(8, 20, true);
dv.setUint32(12, 0, true);
dv.setUint32(16, 0x4e4f534a, true);
expect(() => glbKnoten(b.buffer)).toThrow(/unlesbar/);
expect(() => glbKnoten(b.buffer)).not.toThrow(/zu kurz/);
});
});

describe("pruefeSzene — Modell und Auftrag müssen sich decken, in beide Richtungen", () => {
Expand Down
7 changes: 6 additions & 1 deletion apps/tischler/lib/werkstoff/auftrag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,13 @@ export function glbKnoten(buf: ArrayBuffer): SzenenKnoten {
// Der ganze Header, nicht nur das Magic (Review craft#47 Runde 2): Version, Gesamtlänge,
// Chunk-Typ, Chunk-Länge in beide Richtungen — jede Lüge ist ein Klartext-Fehler, nie ein
// RangeError oder SyntaxError aus der Tiefe.
// Länge VOR Magic: eine Datei unter 20 Bytes ist »zu kurz«, auch mit gültigem Magic — sonst
// sieht der Handwerker bei einem abgebrochenen Download die falsche Ursache (cody-cad#71, R68-3).
if (buf.byteLength < 20) {
throw new Error(`glTF-Binary zu kurz für einen Header (${buf.byteLength} Bytes, mindestens 20)`);
}
const dv = new DataView(buf);
if (buf.byteLength < 20 || dv.getUint32(0, true) !== 0x46546c67) {
if (dv.getUint32(0, true) !== 0x46546c67) {
throw new Error("Kein glTF-Binary (Magic »glTF« fehlt)");
}
const version = dv.getUint32(4, true);
Expand Down
9 changes: 5 additions & 4 deletions scripts/build-demo-bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ echo "▸ deps + engine build"
pnpm install --prefer-offline
pnpm --filter @craft-codex/core build

echo "▸ tests (Abbruch bei rot — kein kaputtes Bundle auf den Stick)"
pnpm test

echo "▸ Werkstoff-Bundle: das Modell im Auftrag muss die Datei im Bundle sein (sonst zeigt die Seite 404 statt Möbel)"
echo "▸ Werkstoff-Bundle: das Modell im Auftrag muss die Datei im Bundle sein (sonst zeigt die Seite 404 statt Möbel) — VOR den Tests: eigenes Schloss, nicht das zweite an derselben Tür"
WB="$ROOT/apps/tischler/public/werkstoff-bundle"
if [ -f "$WB/auftrag.json" ] && grep -q '"modell"' "$WB/auftrag.json"; then
SOLL=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['modell']['glb_sha256'])" "$WB/auftrag.json")
Expand All @@ -31,6 +28,10 @@ if [ -f "$WB/auftrag.json" ] && grep -q '"modell"' "$WB/auftrag.json"; then
[ "$IST" = "$SOLL" ] || { echo "✗ $DATEI hat Hash $IST, der Auftrag nennt $SOLL — anderes Erzeugnis"; exit 1; }
echo " ✓ $DATEI = $SOLL"
fi

echo "▸ tests (Abbruch bei rot — kein kaputtes Bundle auf den Stick)"
pnpm test

echo "▸ production build"
(cd apps/tischler && pnpm build)

Expand Down
Loading