Chat: die GIF-Kiste des Rudels -- und eine Luecke in der Sicherung
Filipe: "dan will ich auch dass man im chat auch sprachnachrichten und
gifts reinschicken kann. also nur die modis rechte linke hand und
dogfather."
ICH LESE "gifts" ALS GIFs -- die bewegten Bildchen -- und sage das
ausdruecklich, statt es stillschweigend anzunehmen. Im Zusammenhang
("im chat reinschicken", direkt neben Sprachnachrichten) passt nichts
anderes. Sollte er Geschenke gemeint haben, sagt er es, und dann baue
ich das.
EINE EIGENE KISTE STATT GIPHY ODER TENOR
----------------------------------------
Beide brauchen ein Konto und einen Schluessel und bekommen bei jedem
Tastendruck mit, wonach hier gesucht wird -- an eine fremde Firma, aus
einem Arbeitsplatz heraus, in dem sonst nichts nach aussen geht. Dazu
kosten sie ab einer Menge Geld. Beides steht quer zu dem, wie dieses
Haus gebaut ist.
Die Kiste laeuft auf demselben Server wie alles andere, kostet nichts
und wird mit der Zeit besser statt schlechter: Was darin liegt, hat das
Team ausgesucht. Zehn gute GIFs, die alle kennen, sind im Alltag mehr
wert als zehn Millionen fremde -- ein Katalog mit allem ist nicht mehr
Auswahl, sondern weniger.
HABEN WIR DAS SCHON? WIRD AM INHALT BEANTWORTET
-----------------------------------------------
Der Schluessel ist der SHA-256 der Datei. Ueber den NAMEN zu
vergleichen waere die naheliegende Abkuerzung -- und sie versagt genau
dort, wo Dateien "giphy.gif", "giphy(1).gif" und "download.gif"
heissen, also fast immer. Dasselbe Bild waere dreimal dieselbe Kachel.
Dasselbe GIF zweimal hineinzulegen ist deshalb KEIN Fehler: Es ist
drin, und genau das wollte man. Eine Absage waere formal richtig und im
Erleben falsch.
DAS WICHTIGSTE STUECK: ES WIRD KOPIERT, NICHT VERWIESEN
-------------------------------------------------------
Eine Nachricht zurueckzunehmen entfernt ihre Datei von der Platte. Laege
ein Kachel-GIF in demselben Ordner und mehrere Nachrichten zeigten
darauf, naehme das Zuruecknehmen EINER Nachricht das GIF fuer alle
anderen mit -- und in drei Gespraechen stuende ab da ein kaputtes Bild.
Das ist die Sorte Fehler, die erst Wochen spaeter auffaellt und dann
nicht mehr zu reparieren ist.
Deshalb liegt die Kiste in einem eigenen Ordner (chat-gifs/), und beim
Verschicken wird kopiert. Danach ist es ein ganz normaler Anhang: im
Verlauf, in der Suche, zuruecknehmbar, unter derselben Nachtruhe. Keine
zweite Sorte Nachricht mit eigenen Rechten und eigenem Loeschen.
Geprueft wird genau das: GIF hineinlegen, verschicken, aus der Kiste
nehmen -- und danach steht das verschickte immer noch im Gespraech.
"ALSO NUR" GILT AUCH HIER
-------------------------
Ansehen, hineinlegen, verschicken: alle drei nur fuer Team Dogi und
DogFather (istTeamDogi). Der Satz stand im selben Atemzug wie die
Sprachnachrichten; es gibt keinen Grund, warum er fuer das eine gelten
sollte und fuer das andere nicht. Gemessen auch am Knopf vorbei:
403/403/403.
AUGENSCHONEND -- HIER EINE ECHTE FRAGE, KEINE FORMSACHE
--------------------------------------------------------
Zwoelf gleichzeitig laufende Bildchen sind das Gegenteil von ruhig. Wer
"Bewegung reduzieren" gesetzt hat, bekommt deshalb STEHENDE Bilder: das
erste Einzelbild, im Browser auf eine Zeichenflaeche gemalt (kostet
keine zweite Datei), und es laeuft erst, wenn man es anfasst oder mit
der Tastatur darauf steht.
Fuer alle anderen laufen sie. Eine GIF-Auswahl, in der sich nichts
bewegt, ist eine, in der man nicht sieht, was man verschickt -- deshalb
steht dazu eine Gegenprobe in der Pruefung.
NEBENBEFUND, UND ER WIEGT SCHWERER ALS DIE GIFS
-----------------------------------------------
Weil die Kiste einen neuen Datenordner braucht, lief
tools/wiederherstellung-proben.mjs -- und meldete, dass der Ordner
"material" seit dem 22.09. NICHT gesichert wird. 2,4 MB
Materialbibliothek auf dem Server, nach einem Plattenausfall weg, ohne
dass die taegliche Meldung je etwas gesagt haette. Genau die Luecke,
wegen der diese Probe ueberhaupt gebaut wurde -- und sie hat sie
gefunden, weil sie die Ordner aus dem QUELLTEXT liest statt aus einer
gepflegten Liste.
Eingetragen. Danach frisch geholt und zurueckgespielt: 18 von 18 gruen,
"DIE SICHERUNG LAESST SICH ZURUECKSPIELEN" (vorher 3 Fehler).
Dabei war die Gegenprobe daneben selbst kaputt: Sie prueft, ob ein
erfundener Ordner als fehlend erkannt wird, und verlangte dafuer
`=== 1`. Das setzt voraus, dass sonst nichts fehlt. An dem Tag, an dem
wirklich etwas fehlte, wurde sie rot und zeigte damit auf sich selbst
statt auf den Fund -- zwei Meldungen, eine Ursache, und die zweite
lenkt von der ersten ab. Jetzt zaehlt sie die Differenz.
GEPRUEFT
--------
pruef-chat-anhaenge: 109 Pruefungen, 0 Fehler (vorher 86, davor 60).
pruef-chat, pruef-chat-aufloesen (126), pruef-chat-optik: gruen.
tools/wiederherstellung-proben: 18 von 18.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -49,6 +49,11 @@ notbremse(600_000, "pruef-chat-anhaenge");
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||||
const ANHANG_ORDNER = join(ordner, "chat-anhaenge");
|
||||
/* Der Datenordner dieses Laufs. Gebraucht wird er, um nachzusehen, WO
|
||||
eine Datei liegt: in der GIF-Kiste oder bei den Anhaengen. Genau
|
||||
dieser Unterschied ist der Kern des GIF-Abschnitts -- ein
|
||||
verschicktes GIF ist eine KOPIE, kein Verweis. */
|
||||
const DATEN_ORDNER_P = ordner;
|
||||
|
||||
const PROTOKOLL = join(process.cwd(), "pruef-chat-anhaenge-lauf.txt");
|
||||
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
|
||||
@@ -834,7 +839,192 @@ async function tonAufnehmen(seite, ms) {
|
||||
`ein Video im Ton-Behälter wird abgelehnt (HTTP ${alsVideo}) — sonst liefe es ohne Bild und niemand wüsste es`);
|
||||
}
|
||||
|
||||
melde("\n=== 10. Stille Fehler ===");
|
||||
/* =======================================================================
|
||||
DIE GIF-KISTE (23.09.2026)
|
||||
|
||||
Filipe: „dan will ich auch dass man im chat auch sprachnachrichten
|
||||
und gifts reinschicken kann. also nur die modis rechte linke hand
|
||||
und dogfather."
|
||||
|
||||
„gifts" ist hier als GIFs gelesen -- die bewegten Bildchen.
|
||||
|
||||
DIE WICHTIGSTE PRUEFUNG IN DIESEM ABSCHNITT IST DIE VORLETZTE: Ein
|
||||
GIF aus der Kiste zu verschicken KOPIERT die Datei. Verwiese die
|
||||
Nachricht auf dieselbe Datei, dann naehme das Zuruecknehmen EINER
|
||||
Nachricht das GIF fuer alle anderen mit -- und in drei Gespraechen
|
||||
stuende ab da ein kaputtes Bild. Das ist die Sorte Fehler, die erst
|
||||
Wochen spaeter auffaellt und dann nicht mehr zu reparieren ist.
|
||||
======================================================================= */
|
||||
melde("\n=== 10. Die GIF-Kiste ===");
|
||||
|
||||
/* EIN ECHTES GIF, kein gebasteltes Muster: Diese 43 Bytes sind das
|
||||
bekannte durchsichtige 1x1-GIF89a -- jedes Programm der Welt oeffnet
|
||||
es. Es geht hier nicht um das Bild, sondern um den Weg. */
|
||||
const GIF_B64 = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
|
||||
{
|
||||
await oeffne(dogi.seite, raumDogi);
|
||||
|
||||
/* ---- Der Knopf ist da -- und bei Patrick nicht ------------------ */
|
||||
ok(await dogi.seite.locator("#gif-auf").isVisible(), "DogFather sieht den GIF-Knopf");
|
||||
await oeffne(pat.seite, raumId);
|
||||
ok(!(await pat.seite.locator("#gif-auf").isVisible()),
|
||||
"der Scout sieht ihn nicht — dieselbe Regel wie bei den Sprachnachrichten");
|
||||
|
||||
/* ---- Die Kiste ist leer, und sie sagt das auch ------------------- */
|
||||
await dogi.seite.click("#gif-auf");
|
||||
await dogi.seite.waitForSelector("#gif-tafel:not([hidden])", { timeout: 8000 });
|
||||
const leerSatz = (await dogi.seite.locator("#gif-leer").innerText()).trim();
|
||||
ok(/noch leer/.test(leerSatz), `die leere Kiste sagt, was zu tun ist ("${leerSatz.slice(0, 48)}…")`);
|
||||
|
||||
/* ---- Ein GIF hineinlegen, ueber den echten Knopf ----------------- */
|
||||
await dogi.seite.setInputFiles("#gif-feld", {
|
||||
name: "daumen-hoch.gif", mimeType: "image/gif", buffer: Buffer.from(GIF_B64, "base64"),
|
||||
});
|
||||
await dogi.seite.waitForTimeout(2200);
|
||||
const kacheln = await dogi.seite.locator(".gif-kachel").count();
|
||||
ok(kacheln === 1, `es liegt ein GIF in der Kiste (${kacheln})`);
|
||||
|
||||
const db4 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const inKiste = db4.prepare(
|
||||
"SELECT id, datei, name, typ, breite, hoehe, von_id FROM chat_gifs ORDER BY id DESC LIMIT 1").get();
|
||||
db4.close();
|
||||
ok(inKiste?.typ === "image/gif", `erkannt als ${inKiste?.typ}`);
|
||||
ok(inKiste?.name === "daumen-hoch", `der Name bleibt ohne Endung ("${inKiste?.name}")`);
|
||||
ok(inKiste?.breite === 1 && inKiste?.hoehe === 1,
|
||||
`die Maße kommen aus der Datei (${inKiste?.breite}×${inKiste?.hoehe})`);
|
||||
ok(existsSync(join(DATEN_ORDNER_P, "chat-gifs", inKiste?.datei || "-")),
|
||||
"die Datei liegt in der eigenen Kiste, nicht bei den Anhängen");
|
||||
|
||||
/* ---- DASSELBE NOCH EINMAL IST KEIN FEHLER ------------------------ */
|
||||
await dogi.seite.setInputFiles("#gif-feld", {
|
||||
/* ANDERER NAME, GLEICHER INHALT -- genau der Fall, der über den
|
||||
Namen nicht zu erkennen wäre: "giphy.gif", "giphy(1).gif",
|
||||
"download.gif" sind fast immer dasselbe Bild. */
|
||||
name: "giphy(1).gif", mimeType: "image/gif", buffer: Buffer.from(GIF_B64, "base64"),
|
||||
});
|
||||
await dogi.seite.waitForTimeout(1800);
|
||||
const nachZweitem = await dogi.seite.locator(".gif-kachel").count();
|
||||
ok(nachZweitem === 1,
|
||||
`dasselbe GIF unter anderem Namen ergibt KEINE zweite Kachel (${nachZweitem}) — verglichen wird der Inhalt`);
|
||||
const hinweis = (await dogi.seite.locator("#fehler").innerText()).trim();
|
||||
ok(/schon in der Kiste/.test(hinweis), ` und es steht als Hinweis da, nicht als Fehler ("${hinweis}")`);
|
||||
|
||||
/* ---- Verschicken ------------------------------------------------- */
|
||||
const vorher = await dogi.seite.locator(".chat-nachricht").count();
|
||||
await dogi.seite.click(".gif-kachel__knopf");
|
||||
await dogi.seite.waitForTimeout(2200);
|
||||
const nachher = await dogi.seite.locator(".chat-nachricht").count();
|
||||
ok(nachher === vorher + 1, `das GIF steht im Verlauf (${vorher} -> ${nachher})`);
|
||||
ok(!(await dogi.seite.locator("#gif-tafel").isVisible()),
|
||||
" und die Kiste geht zu — sonst steht sie vor dem, was man gerade geschickt hat");
|
||||
|
||||
const db5 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const alsNachricht = db5.prepare(`SELECT anhang_art, anhang_typ, anhang_datei, anhang_name
|
||||
FROM chat_nachrichten WHERE anhang_typ = 'image/gif' ORDER BY id DESC LIMIT 1`).get();
|
||||
db5.close();
|
||||
ok(alsNachricht?.anhang_art === "bild",
|
||||
"es ist ein ganz normaler Anhang geworden — keine zweite Sorte Nachricht");
|
||||
ok(alsNachricht?.anhang_name === "daumen-hoch.gif",
|
||||
`mit seinem Namen (${alsNachricht?.anhang_name})`);
|
||||
|
||||
/* ---- DIE EIGENTLICHE AUSSAGE: ES WURDE KOPIERT ------------------- */
|
||||
ok(alsNachricht?.anhang_datei !== inKiste?.datei,
|
||||
"die verschickte Datei ist eine KOPIE, kein Verweis auf die Kiste");
|
||||
ok(existsSync(join(DATEN_ORDNER_P, "chat-anhaenge", alsNachricht?.anhang_datei || "-")),
|
||||
" sie liegt bei den Anhängen");
|
||||
|
||||
/* ---- Aus der Kiste nehmen: das Verschickte bleibt ---------------- */
|
||||
await dogi.seite.evaluate(() => { window.confirm = () => true; });
|
||||
await dogi.seite.click("#gif-auf");
|
||||
await dogi.seite.waitForTimeout(600);
|
||||
await dogi.seite.click(".gif-kachel__weg");
|
||||
await dogi.seite.waitForTimeout(1500);
|
||||
const restKacheln = await dogi.seite.locator(".gif-kachel").count();
|
||||
ok(restKacheln === 0, `aus der Kiste genommen (${restKacheln} übrig)`);
|
||||
ok(!existsSync(join(DATEN_ORDNER_P, "chat-gifs", inKiste?.datei || "-")),
|
||||
" die Datei der Kiste ist weg");
|
||||
/* UND DAS IST DER PUNKT: Das verschickte GIF steht weiter im
|
||||
Gespräch. Wäre es ein Verweis gewesen, stünde dort jetzt ein
|
||||
kaputtes Bild -- in jedem Gespräch, in das es je geschickt wurde. */
|
||||
ok(existsSync(join(DATEN_ORDNER_P, "chat-anhaenge", alsNachricht?.anhang_datei || "-")),
|
||||
" das bereits verschickte GIF bleibt unberührt");
|
||||
|
||||
/* ---- Die Gegenprobe: der Scout kommt an die Kiste nicht heran ---- */
|
||||
const absagen = await absichtlich([pat], () => pat.seite.evaluate(async ([r, b64]) => {
|
||||
const roh = atob(b64);
|
||||
const puffer = new Uint8Array(roh.length);
|
||||
for (let i = 0; i < roh.length; i++) puffer[i] = roh.charCodeAt(i);
|
||||
const liste = await fetch("/workspace/api/chat/gifs");
|
||||
const rein = await fetch("/workspace/api/chat/gifs", {
|
||||
method: "POST", headers: { "Content-Type": "image/gif", "X-Dateiname": "x.gif" },
|
||||
body: puffer });
|
||||
const raus = await fetch(`/workspace/api/chat/raeume/${r}/gif`, {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ gif_id: 1 }) });
|
||||
return { liste: liste.status, rein: rein.status, raus: raus.status };
|
||||
}, [raumId, GIF_B64]));
|
||||
ok(absagen.liste === 403 && absagen.rein === 403 && absagen.raus === 403,
|
||||
`am Knopf vorbei: ansehen ${absagen.liste}, hineinlegen ${absagen.rein}, schicken ${absagen.raus}`);
|
||||
|
||||
/* ---- Nur GIFs kommen hinein -------------------------------------- */
|
||||
const keinGif = await absichtlich([dogi], () => dogi.seite.evaluate(async () => {
|
||||
const a = await fetch("/workspace/api/chat/gifs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "image/gif", "X-Dateiname": "eigentlich.png" },
|
||||
body: new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1]),
|
||||
});
|
||||
return { status: a.status, text: (await a.json())?.fehler || "" };
|
||||
}));
|
||||
ok(keinGif.status === 415,
|
||||
`ein PNG mit dem Etikett "image/gif" kommt nicht hinein (HTTP ${keinGif.status}) — entschieden wird am Inhalt`);
|
||||
|
||||
/* ---- Augenschonend: bei Bewegungsverzicht stehen sie still ------- */
|
||||
const ruhig = await browser.newContext({
|
||||
viewport: { width: 1280, height: 900 }, reducedMotion: "reduce",
|
||||
});
|
||||
const rs = await ruhig.newPage();
|
||||
await rs.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
||||
await rs.click('.rolle[data-rolle="admin"]');
|
||||
await rs.fill("#code", "CODE-DOGI-0001");
|
||||
await rs.click("button[type=submit]");
|
||||
await rs.waitForURL("**/start.html", { timeout: 25000 });
|
||||
/* Noch ein GIF in die Kiste, sonst ist nichts zu sehen. */
|
||||
await rs.goto(`${BASIS}/workspace/chat.html?raum=${raumDogi}`, { waitUntil: "networkidle" });
|
||||
await rs.waitForSelector("#schreiben:not([hidden])", { timeout: 8000 });
|
||||
await rs.click("#gif-auf");
|
||||
await rs.waitForTimeout(500);
|
||||
await rs.setInputFiles("#gif-feld", {
|
||||
name: "ruhig.gif", mimeType: "image/gif", buffer: Buffer.from(GIF_B64, "base64"),
|
||||
});
|
||||
await rs.waitForTimeout(2200);
|
||||
const ruhigeKachel = await rs.evaluate(() => {
|
||||
const k = document.querySelector(".gif-kachel__knopf");
|
||||
return { tag: k?.firstElementChild?.tagName || "", da: !!k };
|
||||
});
|
||||
ok(ruhigeKachel.da && ruhigeKachel.tag === "CANVAS",
|
||||
`bei „Bewegung reduzieren" steht das GIF still (${ruhigeKachel.tag}) — zwölf laufende Bildchen sind das Gegenteil von ruhig`);
|
||||
/* GEGENPROBE: Im normalen Fall LAEUFT es. Sonst bewiese die Zeile
|
||||
darüber nur, dass die Kachel immer stillsteht -- und dann sähe
|
||||
niemand, was er verschickt. */
|
||||
/* NEU LADEN, WEIL DIE KISTE SICH GEAENDERT HAT. Dieses Fenster hat
|
||||
die Liste zuletzt geholt, als sie leer war -- ohne das Neuladen
|
||||
zeigte die Gegenprobe auf ein leeres Gitter und wuerde rot, ohne
|
||||
dass irgendetwas kaputt waere. (Beim Bauen genau so passiert.) */
|
||||
await oeffne(dogi.seite, raumDogi);
|
||||
await dogi.seite.click("#gif-auf");
|
||||
await dogi.seite.waitForTimeout(1200);
|
||||
const normaleKachel = await dogi.seite.evaluate(() => ({
|
||||
tag: document.querySelector(".gif-kachel__knopf")?.firstElementChild?.tagName || "",
|
||||
kacheln: document.querySelectorAll(".gif-kachel").length,
|
||||
}));
|
||||
ok(normaleKachel.kacheln === 1 && normaleKachel.tag === "IMG",
|
||||
`Gegenprobe: ohne diese Einstellung läuft es (${normaleKachel.kacheln} Kachel, ${normaleKachel.tag})`);
|
||||
await ruhig.close();
|
||||
}
|
||||
|
||||
melde("\n=== 11. Stille Fehler ===");
|
||||
{
|
||||
/* ABSICHTLICH PROVOZIERTE ABSAGEN ZÄHLEN NICHT.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user