Files
dogfather-universe/server/pruef-gifs.mjs
T
DogFatherGitandClaude Opus 5 24523cdd4b Die GIF-Kiste ist fertig – eine Prüfung sagt es jetzt auch
Filipe: „mach das mit den gifs auch fertig."

ERGEBNIS: SIE WAR ES SCHON. Hineinlegen, Kiste anzeigen, verschicken,
herausnehmen, Duplikat-Erkennung am Inhalt, Fortschrittsanzeige mit
Abbruch, Standbild bei „Bewegung reduzieren" -- alles vorhanden, alles
live, und pruef-chat-anhaenge deckt die Wege ab.

SECHS „BEFUNDE" MEINES ERSTEN LAUFS WAREN ALLE MESSFEHLER:
  * zu frueh gemessen (`darf_gif` kommt mit den Raumdaten; der Knopf
    wird erst danach freigeschaltet)
  * kein Content-Type gesetzt -> „Keine Datei empfangen"
  * Selektor `.chat-raum` erfunden; die Klasse heisst
    `chat-raum__knopf`
  * nach `title*='ausnehm'` gesucht; der Knopf heisst „Aus der Kiste
    nehmen" und hat die Klasse `gif-kachel__weg`
  * `x-name` geschickt; der Server liest `x-dateiname`
  * nach einer GIF-Adresse im Verlauf gesucht -- das GIF wird beim
    Verschicken KOPIERT und haengt danach als Anhang
  * und einmal `curl` auf chat.html ohne Anmeldung: eine Umleitung,
    null Treffer, und ich hielt die Tafel fuer nicht ausgeliefert

Ich haette beinahe gebaut, was es laengst gibt -- wie heute frueh beim
Farbwerkzeug. Der Unterschied: Diesmal habe ich vor dem Bauen
nachgesehen.

WAS BLEIBT, IST DIE PRUEFUNG. pruef-chat-anhaenge prueft die WEGE;
ungeprueft war die OBERFLAECHE -- dass der Knopf fuer Team Dogi
erscheint und fuer einen Creator nicht, dass die Tafel aufgeht, dass
an jeder Kachel ein Weg zum Herausnehmen steht, dass ein verschicktes
GIF wirklich im Verlauf landet. Genau dort haette ich gebaut, was es
schon gibt. 17 Pruefungen, 0 Fehler.

=== Und der Durchgang durch die Pruefungen (erster Teil) ===

Gesucht nach dem Muster, das heute fuenfmal zugeschlagen hat:
abgeschriebene Listen. Gefunden: pruef-buehne kennt 19 von 38 Seiten,
pruef-workspace-seiten 18 -- je VIERZEHN mit Kopfzeile und damit
ungeprueft. support.html fehlt in beiden; sie ist heute entstanden.

In pruef-workspace-seiten steht die Lehre woertlich im Kopf: „Eine
Pruefung, die eine Seite nicht kennt, kann auf ihr nichts finden."

Ein Probelauf mit abgeleiteter Liste: 30 statt 18 Seiten, 28 rot --
24 davon, weil die Seite gar kein `data-buehne` traegt. Das ist eine
Gestaltungsfrage (welche Szene wohin), keine Reparatur. DIE
ERWEITERUNG IST DESHALB WIEDER DRAUSSEN: Eine Pruefung, die ab sofort
dauerhaft rot ist, wird ab dem zweiten Mal ueberlesen -- und dann auch
die echte Meldung.

Nebenbefund mit Gegenprobe belegt: `aufgaben.html` ist 1560 px breit
statt 1240 (verursacht von BUTTON.schnitt) -- und war das schon VOR
meiner Aenderung. Die fuenfte bestehende rote Pruefung an diesem Tag.

Beides steht in der Vault-Notiz zum Entscheiden.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-24 12:42:37 +02:00

266 lines
12 KiB
JavaScript

/* DIE GIF-KISTE, VON VORN BIS HINTEN.
Filipe: „mach das mit den gifs auch fertig."
ERGEBNIS: SIE IST FERTIG. Alle sechs „Befunde" meines ersten
Laufs waren Messfehler -- zu frueh gemessen, falscher Kopf,
falscher Selektor, falsche Adresse, falscher Header, und einmal
eine Umleitung fuer einen nicht angemeldeten curl-Aufruf.
WARUM SIE TROTZDEM BLEIBT: pruef-chat-anhaenge prueft die WEGE
(hineinlegen, verschicken, Duplikate, Typ am Inhalt). Diese hier
prueft die OBERFLAECHE -- dass der Knopf fuer Team Dogi erscheint
und fuer einen Creator nicht, dass die Tafel aufgeht, dass an
jeder Kachel ein Weg zum Herausnehmen steht. Genau das war
ungeprueft, und genau dort haette ich beinahe gebaut, was es
laengst gibt. */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-gf-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
const { eigenerPort } = await import("./helfer-port.mjs");
const PORT = await eigenerPort(import.meta, "die GIF-Kisten-Pruefung");
process.env.PORT = `${PORT}`;
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs";
await import("./index.js");
notbremse(200_000, "pruef-gifs");
await new Promise((r) => setTimeout(r, 900));
const BASIS = `http://127.0.0.1:${PORT}`;
let fehler = 0, geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
const ks = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", ks, jetzt);
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n,"
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768,
createHmac("sha256", ks).update(code).digest("hex"), jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("Miss", "modi", "CODE-MODI-0001");
anlegen("Luna", "creator", "CODE-CREA-0001");
d.close();
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
/* Ein winziges, gueltiges GIF (1x1). Echte Kopfbytes -- die Erkennung
liest sie, nicht den Namen. */
const GIF_B64 = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
try {
const kontext = await browser.newContext({ viewport: { width: 1200, height: 900 } });
const seite = await kontext.newPage();
const konsole = [];
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
await seite.click('.rolle[data-rolle="admin"]');
await seite.fill("#code", "CODE-DOGI-0001");
await seite.click("button[type=submit]");
await seite.waitForURL("**/start.html", { timeout: 15000 });
await seite.goto(BASIS + "/workspace/chat.html", { waitUntil: "networkidle" });
await seite.waitForTimeout(2000);
/* EINEN RAUM WIRKLICH OEFFNEN UND WARTEN.
Beim ersten Lauf stand "knopfSichtbar: false" -- und ich hielt
das fuer einen Befund. Es war zu frueh gemessen: `darf_gif`
kommt mit den Raumdaten, und die Zeile, die den Knopf
freischaltet, laeuft erst danach. `istTeamDogi(admin)` ist
true, nachgemessen.
Gewartet wird auf den ZUSTAND, nicht auf eine Zahl: bis der
Knopf sichtbar ist, laengstens acht Sekunden. */
/* DIE KLASSE HEISST `chat-raum__knopf` -- nachgesehen in chat.js
(Zeile 810), nicht geraten. Mein erster Versuch probierte drei
erfundene Namen und traf keinen; der Raum blieb zu, und der
GIF-Knopf deshalb verborgen. */
await seite.evaluate(() => {
document.querySelector(".chat-raum__knopf")?.click();
});
try {
await seite.waitForFunction(
() => { const k = document.getElementById("gif-auf"); return k && !k.hidden; },
{ timeout: 8000 });
} catch { /* dann ist er wirklich nicht da -- die Pruefung sagt es */ }
const lage = await seite.evaluate(() => {
const k = document.getElementById("gif-auf");
return {
knopfDa: !!k,
knopfSichtbar: k ? !k.hidden : false,
tafelDa: !!document.getElementById("gif-tafel"),
raumOffen: !!document.querySelector("#eingabe, .chat__eingabe, #nachricht"),
};
});
console.log(" " + JSON.stringify(lage));
ok(lage.knopfDa && lage.knopfSichtbar, "der GIF-Knopf steht da");
ok(lage.tafelDa, "und die Kiste gibt es");
/* ---- 1. Ein GIF hineinlegen (ueber die Route, wie der Knopf) ---- */
const rein = await seite.evaluate(async (b64) => {
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
/* MIT Content-Type. Ohne ihn nimmt express.raw den Rumpf nicht
an ("Keine Datei empfangen") -- beim ersten Lauf sah das aus
wie eine kaputte Route. Der echte Knopf schickt eine File,
und die bringt ihren Typ mit. */
const a = await fetch("/workspace/api/chat/gifs", {
method: "POST",
credentials: "same-origin",
/* DER SERVER LIEST `x-dateiname`, nicht `x-name` -- nachgesehen
in workspace-chat.js. Mit dem falschen Kopf hiess das GIF
"anhang", und ich hielt das fuer einen verlorenen Namen. */
headers: { "x-dateiname": encodeURIComponent("Winken.gif"),
"Content-Type": "application/octet-stream" },
body: bytes,
});
return { code: a.status, text: await a.text() };
}, GIF_B64);
console.log(" hineinlegen: HTTP " + rein.code + " " + rein.text.slice(0, 80));
ok(rein.code === 200 || rein.code === 201, `ein GIF geht in die Kiste (${rein.code})`);
/* ---- 2. Steht es in der Kiste? ---- */
const liste = await seite.evaluate(async () => {
const a = await fetch("/workspace/api/chat/gifs", { credentials: "same-origin" });
return a.ok ? await a.json() : { fehler: a.status };
});
const erstes = (liste.gifs || [])[0];
ok((liste.gifs || []).length >= 1, `die Kiste hat ${(liste.gifs || []).length} GIF(s)`);
ok(!!erstes?.weg, `mit einer Adresse zum Anzeigen (${erstes?.weg})`);
/* ---- 3. Die Tafel zeigt es ---- */
await seite.evaluate(() => document.getElementById("gif-auf")?.click());
await seite.waitForTimeout(900);
const tafel = await seite.evaluate(() => {
const t = document.getElementById("gif-tafel");
return {
offen: t ? !t.hidden : false,
kacheln: document.querySelectorAll("#gif-tafel img").length,
/* DER KNOPF HEISST `gif-kachel__weg` -- nachgesehen in chat.js,
nicht geraten. Mein erster Versuch suchte nach dem Wort
"ausnehm" im Titel; dort steht "Aus der Kiste nehmen", und
der Knopf fiel durch. Ich haette gebaut, was es laengst
gibt -- so wie heute frueh beim Farbwerkzeug. */
wegKnoepfe: document.querySelectorAll("#gif-tafel .gif-kachel__weg").length,
knoepfeGesamt: document.querySelectorAll("#gif-tafel button").length,
};
});
console.log(" Tafel: " + JSON.stringify(tafel));
ok(tafel.offen, "die Kiste geht auf");
ok(tafel.kacheln >= 1, `und zeigt das GIF (${tafel.kacheln})`);
ok(tafel.wegKnoepfe >= 1,
`es gibt einen Weg zum Herausnehmen (${tafel.wegKnoepfe} von ${tafel.knoepfeGesamt} Knoepfen)`);
ok((liste.gifs || [])[0]?.name === "Winken",
`der Name bleibt erhalten ("${(liste.gifs || [])[0]?.name}")`);
/* ==== DER EIGENTLICHE ZWECK: EIN GIF VERSCHICKEN ==============
Das hatte ich beim ersten Anlauf gar nicht gemessen -- ich habe
die Kiste geprueft und den Grund vergessen, aus dem es sie gibt.
Ein GIF, das man hineinlegen und herausnehmen kann, aber nicht
verschicken, waere eine perfekte Kiste ohne Zweck. */
const geschickt = await seite.evaluate(async () => {
const knopf = document.querySelector("#gif-tafel .gif-kachel__knopf");
if (!knopf) return { keinKnopf: true };
const vorher = document.querySelectorAll(".nachricht, .chat-nachricht, [data-nachricht]").length;
knopf.click();
await new Promise((r) => setTimeout(r, 1500));
/* NACH EINEM BILD AUSSERHALB DER TAFEL FRAGEN -- nicht nach
einer bestimmten Adresse.
Mein erster Versuch suchte nach "/chat/gifs/<n>/bild". Das
GIF wird beim Verschicken aber KOPIERT und haengt danach als
Anhang an der Nachricht (anhang_datei, anhang_art "bild") --
mit der Adresse der Anhaenge, nicht der Kiste. Wieder eine
Messung, die eine Annahme prueft statt der Sache.
Die Sache ist: Steht im Verlauf ein Bild, das vorher nicht da
war? Das gilt unabhaengig davon, welchen Weg es nimmt. */
const bilder = [...document.querySelectorAll("img")]
.filter((i) => !i.closest("#gif-tafel"))
.filter((i) => (i.src || "").includes("/workspace/api/"));
return {
vorher,
nachher: document.querySelectorAll(".nachricht, .chat-nachricht, [data-nachricht]").length,
imVerlauf: bilder.length > 0,
adressen: bilder.slice(0, 2).map((i) => (i.src || "").replace(/^https?:\/\/[^/]+/, "")),
tafelZu: document.getElementById("gif-tafel")?.hidden === true,
};
});
console.log(" verschicken: " + JSON.stringify(geschickt));
ok(!geschickt.keinKnopf, "es gibt eine Kachel zum Anklicken");
ok(geschickt.imVerlauf === true,
"das GIF steht danach wirklich im Verlauf");
ok(geschickt.tafelZu === true,
"und die Tafel geht zu -- man will sehen, wie es ankommt");
/* ---- Und wirklich herausnehmen ---- */
const raus = await seite.evaluate(async (id) => {
const a = await fetch("/workspace/api/chat/gifs/" + id,
{ method: "DELETE", credentials: "same-origin" });
const b = await fetch("/workspace/api/chat/gifs", { credentials: "same-origin" });
const j = b.ok ? await b.json() : {};
return { code: a.status, uebrig: (j.gifs || []).length };
}, erstes?.id);
ok(raus.code === 200, `es laesst sich herausnehmen (${raus.code})`);
ok(raus.uebrig === 0, `und ist wirklich weg (${raus.uebrig} uebrig)`);
/* ---- 4. Wer es NICHT darf ---- */
await kontext.close();
const k2 = await browser.newContext({ viewport: { width: 1200, height: 900 } });
const s2 = await k2.newPage();
await s2.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
const kachel = await s2.$('.rolle[data-rolle="creator"]') || await s2.$(".rolle");
if (kachel) await kachel.click();
await s2.fill("#code", "CODE-CREA-0001");
await s2.click("button[type=submit]");
await s2.waitForURL("**/start.html", { timeout: 15000 });
await s2.goto(BASIS + "/workspace/chat.html", { waitUntil: "networkidle" });
await s2.waitForTimeout(1600);
const alsCreator = await s2.evaluate(async (b64) => {
const k = document.getElementById("gif-auf");
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const a = await fetch("/workspace/api/chat/gifs", {
method: "POST", credentials: "same-origin",
headers: { "Content-Type": "application/octet-stream" },
body: bytes,
});
return { knopf: k ? !k.hidden : false, code: a.status };
}, GIF_B64);
console.log(" als Creator: " + JSON.stringify(alsCreator));
ok(alsCreator.knopf === false, "ein Creator sieht den Knopf nicht");
ok(alsCreator.code >= 400, `und kommt auch an der Route nicht durch (${alsCreator.code})`);
await k2.close();
ok(konsole.length === 0, konsole.length ? konsole[0] : "keine Abstuerze");
} finally {
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
}
console.log("");
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
process.exit(fehler ? 1 : 0);