Files
DogFatherGitandClaude Opus 5 87a3a14978 pruef-gifs war nicht kaputt, sondern nachts rot -- und ein Weg war ungeprüft
DIE ZWEI FEHLER AUS DEM LETZTEN LAUF WAREN KEINE FEHLER AM PRODUKT.

    FEHL das GIF steht danach wirklich im Verlauf
    FEHL und die Tafel geht zu -- man will sehen, wie es ankommt

Gemessen statt geraten: eine Sonde, die jede Anfrage mitschreibt. Die
Antwort stand sofort da, um 04:30 Uhr:

    POST /workspace/api/chat/raeume/1/gif
    403 {"fehler":"Das Rudel schläft. Ab 06:00 Uhr geht es weiter …",
         "nachtruhe":{"zu":true,"ab":0,"bis":6,"minuten":90}}

Die Pruefung klickte „das erste Gespraech in der Liste". Das erste ist
der TREFF, den der Server beim Start selbst anlegt -- und im Treff gilt
die Nachtruhe von 0 bis 6 Uhr. Also: tagsueber gruen, nachts rot, seit
es diese Pruefung gibt. Es ist derselbe Fehler wie am 06.09. im Shop
(„ein Test, der die Wanduhr als Annahme benutzt, misst irgendwann das
Gegenteil") -- dort ein Oeffnungstermin, hier ein Raum mit Nachtruhe.

Warum es nie jemandem auffiel: Fuer Dogfather Universe gibt es keinen
naechtlichen Pruefdienst (nachgesehen: `systemctl list-timers` kennt nur
`vandiy-pruefung` und `runone-pruefung`). Die Pruefung lief immer nur
dann, wenn jemand sie von Hand startete -- und das war bisher nie
nachts.

WAS GEAENDERT IST

1. pruef-gifs legt ein eigenes Gespraech mit Miss an und oeffnet GENAU
   DAS. Fuer ein normales Gespraech gibt es keine Nachtruhe; die Messung
   gilt jetzt rund um die Uhr. Findet es den Knopf nicht, bricht es laut
   ab statt still auf nichts zu klicken.

   Gemessen, 04:40 Uhr:
     verschicken: {"vorher":0,"nachher":1,"imVerlauf":true,
                   "adressen":["/workspace/api/chat/anhang/1"],
                   "tafelZu":true}
   17 Pruefungen, 0 Fehler. Und nebenbei belegt die Adresse, was der
   Quelltext behauptet: Das GIF wird KOPIERT und haengt danach als
   ganz normaler Anhang an der Nachricht.

2. DIE NACHTRUHE IST NICHT UNTER DEN TISCH GEFALLEN -- sie steht jetzt
   dort, wo sie hingehoert: in pruef-treffchat (110 -> 114). Dort wird
   das Fenster ueber die EINSTELLUNG verschoben, nicht ueber die
   Systemuhr, und beide Zustaende kommen in einem Lauf vor.

   DENN DER GIF-WEG WAR DORT ALS EINZIGER DER DREI UEBERHAUPT NICHT
   GEPRUEFT. Im Quelltext steht neben der Regel woertlich: „Sie nur
   beim Text und beim Anhang zu pruefen hiesse, dass man nachts zwar
   nicht schreiben, aber ein GIF schicken kann -- der Weg, den jeder
   findet, der es einmal versucht." Genau dieser Weg hatte keine
   Pruefung. Der Kommentar hat den Fehler beschrieben und nicht
   verhindert -- dieselbe Luecke wie beim Spaltenverlust vom 11.09.

   Gemessen, beide Zweige in einem Lauf:
     Fenster 7–9 Uhr, jetzt 4 -> 404 „Dieses GIF gibt es nicht mehr."
     Fenster 4–6 Uhr, jetzt 4 -> 403 „Das Rudel schläft. Ab 06:00 …"

   ZWEI ENTSCHEIDUNGEN DABEI, beide mit Grund:
   · Gefragt wird als DogFather, nicht als Gast. Die GIF-Kiste gehoert
     dem Rudel; ein Gast bekaeme seine 403 von der falschen Schranke
     (`nurRudel`) -- gruen, ohne die Nachtruhe je beruehrt zu haben.
   · Mit einer Nummer, die es NICHT gibt. Kommt trotzdem die
     Nachtruhe-Absage, steht die Schranke VOR dem Nachschlagen.
     Stuende sie dahinter, verriete der Server nachts an der Nummer,
     welche GIFs es gibt.

AM PRODUKT IST NICHTS GEAENDERT. Nur zwei Pruefdateien -- kein Stempel,
kein Neustart noetig.

GEPRUEFT: pruef-gifs 17 ok (vorher 15 ok / 2 FEHL) ·
pruef-treffchat 114 ok (vorher 110).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-02 04:36:14 +02:00

318 lines
15 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");
const idMiss = 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. */
/* ==== EIN GANZ NORMALES GESPRAECH, NICHT DAS ERSTE DER LISTE ======
(02.10.2026 -- und das ist eine Zeitbombe, die hochgegangen ist)
Hier stand `document.querySelector(".chat-raum__knopf")?.click()`
-- also „nimm das erste Gespraech". Das erste ist der TREFF, den
der Server beim Start selbst anlegt. Und im Treff gilt die
Nachtruhe: zwischen 0 und 6 Uhr weist der Server jedes Schreiben
mit 403 ab, Text, Anhang und GIF.
FOLGE: Diese Pruefung war tagsueber gruen und nachts rot --
seit es sie gibt. Gemessen um 04:30 Uhr, und die Antwort stand
im Protokoll:
403 {"fehler":"Das Rudel schläft. Ab 06:00 Uhr geht es
weiter …","nachtruhe":{"zu":true,"ab":0,"bis":6}}
Nicht das GIF war kaputt, sondern die Uhr war eine Annahme. Das
ist derselbe Fehler wie am 06.09. im Shop („ein Test, der die
Wanduhr als Annahme benutzt, misst irgendwann das Gegenteil") --
dort war es ein Oeffnungstermin, hier ein Raum mit Nachtruhe.
Deshalb wird jetzt ein eigenes Gespraech mit Miss angelegt und
GENAU DAS geoeffnet: Fuer ein normales Gespraech gibt es keine
Nachtruhe, die Messung gilt rund um die Uhr.
UND DIE NACHTRUHE IST NICHT UNGEPRUEFT GEBLIEBEN: Sie steht
jetzt in pruef-treffchat, wo das Zeitfenster richtig gesetzt
wird (ueber die Einstellung, nicht ueber die Systemuhr) und
beide Zustaende in einem Lauf vorkommen. Dort war der GIF-Weg
als einziger der drei noch gar nicht geprueft. */
await seite.evaluate(async (id) => {
await fetch("/workspace/api/chat/direkt", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({ mit: id }),
});
}, idMiss);
await seite.reload({ waitUntil: "networkidle" });
await seite.waitForSelector(".chat-raum__knopf", { timeout: 8000 });
const zuMiss = seite.locator(".chat-raum__knopf", { hasText: "Miss" }).first();
/* LAUT ABBRECHEN, WENN ES IHN NICHT GIBT. Ein stilles
`?.click()` auf nichts waere wieder eine Messung, die „der Raum
ist zu" meldet und den Grund verschweigt. */
await zuMiss.waitFor({ timeout: 8000 });
await zuMiss.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" });
/* KEIN NOTNAGEL AUF IRGENDEINE KACHEL (01.10.2026). Hier stand
`|| await s2.$(".rolle")`. Er greift heute nie -- die
Creator-Kachel steht auf der Agenturseite. Dieselbe Zeile hat in
pruef-glocke drei Rollen unpruefbar gemacht: Seit die Kachel
bindend ist, wird eine falsch geklickte Anmeldung zu Recht
abgewiesen, und heraus kam eine Zeitsperre, die wie ein Fehler
am Produkt aussah. */
const kachel = await s2.$('.rolle[data-rolle="creator"]');
if (!kachel) throw new Error('Kachel "creator" steht nicht auf der Anmeldeseite');
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);