Files
dogfather-universe/server/pruef-loeschen.mjs
T
DogFatherGitandClaude Opus 5 d26cdc78ff Der Loeschknopf war auf dem Handy unsichtbar -- und mein erster Ersatz unlesbar
GEFUNDEN, WEIL ICH MIR DEN CHAT ANGESEHEN HABE

Nach dem Umbau von gestern Abend habe ich den Chat im Browser
aufgemacht statt nur die Pruefungen zu lesen. Im Bild klaffte zwischen
"reagieren" und "anheften" eine Luecke von 87 px, in der nichts stand.
Nachgemessen: Der Loeschknopf ist dort -- sichtbar 0.

URSACHE: `.chat-nachricht__weg-knopf { opacity: 0 }`, sichtbar erst
beim Ueberfahren mit der Maus. Das war vertretbar, solange er nur die
EIGENE Nachricht zuruecknahm: ein seltener, absichtlicher Griff.

Seit gestern traegt derselbe Knopf das Notfall-Loeschen fuer DogFather
und die rechte Hand. Und damit wird es zum Fehler:

  * AUF EINEM HANDY GIBT ES KEIN UEBERFAHREN. Der Knopf war dort
    dauerhaft unsichtbar -- eine Funktion, die man auf dem Geraet nicht
    erreicht, gibt es auf dem Geraet nicht. Filipe besteht bei jedem
    Auftrag darauf, dass es auf dem Handy genauso gut sein muss.
  * Er belegte trotzdem Platz. Eine Luecke, die nichts erklaert, sieht
    nach einem Fehler aus.

Was davor schuetzt, versehentlich zu loeschen, ist nicht die
Unsichtbarkeit, sondern die Rueckfrage -- die steht seit dem 19.09. als
eigener Dialog da. Ein Knopf, den man nicht findet, schuetzt niemanden;
er verhindert nur, dass jemand ihn absichtlich benutzt.

UND MEIN ERSTER ERSATZ WAR MESSBAR FALSCH

Ich gab dem Notfall-Knopf den Warnton, gedaempft gemischt mit der
leisen Schrift. Nachgerechnet, bevor ich ihn ausgeliefert habe:

    auf Babyblau            1,91:1   (noetig 4,5)
    auf den dunklen Toenen  4,06 bis 4,13

Unlesbar auf der hellen Kachel, zu wenig auf allen anderen. Die leise
Schriftfarbe ist nicht irgendein Grau -- sie ist genau der Wert, der
auf JEDER Kachel 4,5:1 schafft. Wer daran mischt, gibt diese Zusage
auf.

DER PUNKT TRAEGT DIE FARBE, NICHT DIE SCHRIFT. Denselben Weg geht das
Haus schon beim Namen in der Blase, mit derselben Begruendung: Fuer
eine Schriftfarbe laesst sich bei dreizehn waehlbaren Flaechen kein
Kontrast garantieren, fuer einen 6-px-Punkt daneben ist das egal.
Beim Ueberfahren wird die Schrift HELLER statt bunter -- wer den Knopf
anvisiert, soll ihn am besten lesen koennen, nicht am schlechtesten.

EIN DRITTER FEHLER, DEN DIE GEGENPROBE GEFANGEN HAT

Die neue Pruefung (pruef-loeschen, Abschnitt 5b) sollte zweierlei
sichern: kein `opacity: 0`, und die Notfall-Regel setzt keine eigene
Schriftfarbe. Ich schrieb dafuer `/\bcolor:/` -- gedacht als
Wortgrenze. Geschrieben wurde es durch eine Python-Zeichenkette, und
dort ist \b das BACKSPACE-ZEICHEN. In der Datei stand
`/<0x08>color:/`, ein Ausdruck, der nie etwas findet.

Die Zeile "setzt keine eigene Schriftfarbe" war dadurch GRUEN, ohne je
gesucht zu haben. Aufgefallen ist es nur, weil die Gegenprobe daneben
dieselbe Suche benutzt und ROT wurde -- sie sollte ja etwas finden.

Ohne diese Gegenprobe haette ich eine Pruefung ausgeliefert, die genau
den Fehler nicht sieht, fuer den sie gebaut wurde. Jetzt zwei
schlichte Textsuchen ohne regulaeren Ausdruck.

pruef-loeschen: 25 -> 31, 0 Fehler. Mitgelaufen: pruef-chatkachel (33),
pruef-chat-optik.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-23 00:44:07 +02:00

425 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
LÖSCHEN HEISST LÖSCHEN — und wer es darf
=====================================================================
Filipe am 22.09.2026:
„Gelöschte Nachrichten verschwinden vollständig – keine Spur, kein
‚wurde gelöscht'-Hinweis, bei niemandem, auch nicht bei
DogFather."
„Löschen darf weiterhin jeder nur seine eigenen – außer DogFather
und rechte Hand: die dürfen jede Nachricht löschen, für den
Notfall."
---------------------------------------------------------------------
WARUM DAS NICHT „EIN STATUS WENIGER" IST
Vorher blieb die Zeile stehen: Text geleert, `weg_am` gesetzt, und
der Chat zeigte „Nachricht zurückgenommen". Es war so begründet --
„ein Loch im Verlauf wirft mehr Fragen auf als der Hinweis". Das
Argument beantwortet nur eine andere Frage: Ein Hinweis „hier stand
etwas" MARKIERT die Stelle. Wer etwas aus Versehen schreibt, will es
weg haben und nicht unterstrichen.
Ein echtes DELETE hat aber vier Ränder, und an jedem kann eine Spur
übrig bleiben. Genau die werden hier gemessen:
1. die Zeile selbst
2. Reaktionen darauf (chat_reaktionen, ON DELETE CASCADE)
3. Erwähnungen darin (chat_erwaehnungen, ON DELETE CASCADE)
4. `letzte_am` am Raum -- sonst stünde er in der Liste ganz
oben, mit einem Zeitpunkt, zu dem
es nichts mehr gibt
Die Punkte 2 und 3 hängen an `PRAGMA foreign_keys = ON`. Dass die
Regel dort steht, ist eine Behauptung im Quelltext; dass sie WIRKT,
sagt nur eine Messung.
---------------------------------------------------------------------
DIE RECHTEGRENZE HAT VIER FÄLLE, NICHT ZWEI
Eine Prüfung, die nur „der Eigene darf" und „ein Fremder darf nicht"
misst, übersieht die Hälfte: Die linke Hand führt Team Dogi mit
(`fuehrtTeamDogi`) und darf es trotzdem NICHT -- Filipe hat zwei
Rollen genannt, nicht drei. Und Spicy Media, das sonst fast alles
darf, darf private Chats nicht einmal sehen.
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { notbremse } from "./helfer-notbremse.mjs";
import { eigenerPort } from "./helfer-port.mjs";
const ordner = mkdtempSync(join(tmpdir(), "ws-loesch-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
const PORT = await eigenerPort(import.meta, "pruef-loeschen");
process.env.PORT = `${PORT}`;
process.env.SITE_ACCESS_SECRET = "lokaler-test";
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 }); };
await import("./index.js");
notbremse(120_000, "pruef-loeschen");
await new Promise((r) => setTimeout(r, 800));
let fehler = 0, geprueft = 0, offen = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const unklar = (t) => { offen++; console.log(" -- " + t); };
/* ---------- Leute ------------------------------------------------------ */
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 schluessel = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", schluessel, 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", schluessel).update(code).digest("hex"), jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idChef = anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
const idLinke = anlegen("Lino", "linke", "CODE-LINK-0001");
const idModi = anlegen("Frida", "modi", "CODE-MODI-0001");
const idModi2 = anlegen("Nele", "modi", "CODE-MODI-0002");
/* ---------- Anfragen (node:http wegen des Host-Kopfes) ----------------- */
const { request: httpAnfrage } = await import("node:http");
const CREW = "crew.dogfather-universe.com";
function anfrage(pfad, { methode = "GET", keks = "", rumpf = null } = {}) {
return new Promise((fertig, schief) => {
const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf));
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: methode,
headers: {
Host: CREW, Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/`,
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
...(keks ? { Cookie: keks } : {}),
},
}, (antwort) => {
const st = [];
antwort.on("data", (x) => st.push(x));
antwort.on("end", () => {
const text = Buffer.concat(st).toString("utf8");
let json = null; try { json = JSON.parse(text); } catch { /* kein JSON */ }
fertig({ code: antwort.statusCode, json,
keks: [].concat(antwort.headers["set-cookie"] || [])
.map((c) => c.split(";")[0]).join("; ") });
});
});
a.on("error", schief);
if (daten) a.write(daten);
a.end();
});
}
const anmelden = async (rolle, code) =>
(await anfrage("/workspace/api/anmelden", { methode: "POST", rumpf: { rolle, code } })).keks;
const ruf = (keks, pfad, o = {}) => anfrage(pfad, { ...o, keks });
const keksChef = await anmelden("admin", "CODE-DOGI-0001");
const keksHand = await anmelden("hand", "CODE-HAND-0001");
const keksLinke = await anmelden("linke", "CODE-LINK-0001");
const keksModi = await anmelden("modi", "CODE-MODI-0001");
/* Nele schreibt die fremden Nachrichten -- sie ist in keinem der
vier Faelle die Loeschende, damit "fremd" fuer alle vier
wirklich fremd ist. */
const keksModi2 = await anmelden("modi", "CODE-MODI-0002");
console.log("=== Löschen heißt Löschen ===");
console.log("");
/* Ein Kanal, in dem alle drin sind -- die Kanalregel vom selben Tag
holt sie beim ersten Blick in die Liste hinein. */
const kanal = await ruf(keksChef, "/workspace/api/chat/kanal",
{ methode: "POST", rumpf: { kategorie: "chat", mit: [] } });
const raumId = kanal.json?.raum_id;
await ruf(keksChef, "/workspace/api/chat/raeume");
const drin = d.prepare(
"SELECT COUNT(*) AS n FROM chat_teilnehmer WHERE raum_id = ? AND raus_am IS NULL").get(raumId).n;
ok(kanal.code === 201 && drin === 5, `ein Kanal mit allen fünf steht (${drin})`);
const schreiben = async (keks, text) => {
const a = await ruf(keks, `/workspace/api/chat/raeume/${raumId}/nachrichten`,
{ methode: "POST", rumpf: { text } });
return a.json?.id || a.json?.nachricht?.id || null;
};
/* =====================================================================
1. EINE EIGENE NACHRICHT -- MIT ALLEM DRAN
===================================================================== */
console.log("--- Eine eigene löschen, mit Reaktion und Erwähnung ---");
const idNachricht = await schreiben(keksModi, "Hallo @Rieke, das hier ist ein Versehen");
ok(Number.isInteger(idNachricht), `sie ist geschrieben (#${idNachricht})`);
await ruf(keksChef, `/workspace/api/chat/nachrichten/${idNachricht}/reaktion`,
{ methode: "PUT", rumpf: { zeichen: "\u{1F44D}" } });
const vorher = {
zeile: d.prepare("SELECT COUNT(*) AS n FROM chat_nachrichten WHERE id = ?").get(idNachricht).n,
reaktion: d.prepare("SELECT COUNT(*) AS n FROM chat_reaktionen WHERE nachricht_id = ?")
.get(idNachricht).n,
erwaehnung: d.prepare("SELECT COUNT(*) AS n FROM chat_erwaehnungen WHERE nachricht_id = ?")
.get(idNachricht).n,
};
ok(vorher.zeile === 1 && vorher.reaktion === 1 && vorher.erwaehnung === 1,
`vorher: Zeile ${vorher.zeile}, Reaktion ${vorher.reaktion}, Erwähnung ${vorher.erwaehnung}`
+ " — es gibt also wirklich etwas zu löschen");
const weg = await ruf(keksModi, `/workspace/api/chat/nachrichten/${idNachricht}`,
{ methode: "DELETE" });
ok(weg.code === 200, `die eigene löschen geht (${weg.code})`);
const nachher = {
zeile: d.prepare("SELECT COUNT(*) AS n FROM chat_nachrichten WHERE id = ?").get(idNachricht).n,
reaktion: d.prepare("SELECT COUNT(*) AS n FROM chat_reaktionen WHERE nachricht_id = ?")
.get(idNachricht).n,
erwaehnung: d.prepare("SELECT COUNT(*) AS n FROM chat_erwaehnungen WHERE nachricht_id = ?")
.get(idNachricht).n,
};
ok(nachher.zeile === 0, "die Zeile ist weg — nicht geleert, weg");
ok(nachher.reaktion === 0 && nachher.erwaehnung === 0,
`und Reaktion (${nachher.reaktion}) und Erwähnung (${nachher.erwaehnung}) mit ihr`
+ " — ON DELETE CASCADE wirkt wirklich");
/* KEINE SPUR IM VERLAUF -- auch nicht bei DogFather. */
const verlaufChef = await ruf(keksChef, `/workspace/api/chat/raeume/${raumId}/nachrichten`);
const alle = verlaufChef.json?.nachrichten || [];
ok(!alle.some((n) => n.id === idNachricht),
"sie steht in keinem Verlauf mehr — auch nicht in dem von DogFather");
ok(!alle.some((n) => n.zurueckgenommen),
"und es steht nirgends eine zurückgenommene Nachricht");
/* =====================================================================
2. DIE VIER FÄLLE DER RECHTEGRENZE
===================================================================== */
console.log("");
console.log("--- Wer darf eine FREMDE löschen ---");
const faelle = [
["DogFather", keksChef, 200, "er ist einer der zwei, die es dürfen"],
["die rechte Hand", keksHand, 200, "sie ist die zweite"],
["die linke Hand", keksLinke, 403,
"sie führt Team Dogi mit und darf es trotzdem nicht — genannt waren zwei Rollen"],
["ein Modi", keksModi, 403, "er nicht"],
];
let geprueftFaelle = 0;
for (const [wer, keks, erwartet, warum] of faelle) {
const id = await schreiben(keksModi2, `Fremde Nachricht für ${wer}`);
if (!Number.isInteger(id)) { unklar(`für ${wer} ließ sich keine Nachricht anlegen`); continue; }
const a = await ruf(keks, `/workspace/api/chat/nachrichten/${id}`, { methode: "DELETE" });
const nochDa = d.prepare("SELECT COUNT(*) AS n FROM chat_nachrichten WHERE id = ?").get(id).n;
geprueftFaelle++;
ok(a.code === erwartet && (erwartet === 200 ? nochDa === 0 : nochDa === 1),
`${wer}: ${a.code} (erwartet ${erwartet}), Zeile danach ${nochDa} — ${warum}`);
}
ok(geprueftFaelle === faelle.length,
`alle ${faelle.length} Fälle wirklich durchgespielt (${geprueftFaelle})`);
/* =====================================================================
3. DER RAUM VERGISST DEN ZEITPUNKT
===================================================================== */
console.log("");
console.log("--- Was der Raum danach über sich sagt ---");
const letzteId = await schreiben(keksModi, "Die allerletzte");
const vorLoeschen = d.prepare("SELECT letzte_am FROM chat_raeume WHERE id = ?").get(raumId).letzte_am;
await ruf(keksModi, `/workspace/api/chat/nachrichten/${letzteId}`, { methode: "DELETE" });
const nachLoeschen = d.prepare("SELECT letzte_am FROM chat_raeume WHERE id = ?").get(raumId).letzte_am;
ok(!!vorLoeschen && vorLoeschen !== nachLoeschen,
`der Raum rechnet seinen letzten Zeitpunkt neu (${vorLoeschen} → ${nachLoeschen})`);
/* =====================================================================
4. DAS BABYBLAUE HERZ
===================================================================== */
console.log("");
console.log("--- Babyblau statt Blau ---");
const { ERLAUBT } = await import("./workspace-reaktionen.js");
ok(ERLAUBT.has("\u{1FA75}"), "das babyblaue Herz steht im Katalog");
ok(!ERLAUBT.has("\u{1F499}"), "das normale blaue nicht mehr");
/* GEGENPROBE AM LAUFENDEN SERVER: Was der Katalog sagt, muss auch der
Weg zum Setzen sagen. Zwei Listen, die auseinanderlaufen, wären hier
besonders unangenehm -- man setzt ein Zeichen und es verschwindet. */
const idFuerHerz = await schreiben(keksModi, "Ein Satz zum Liebhaben");
const babyOk = await ruf(keksChef, `/workspace/api/chat/nachrichten/${idFuerHerz}/reaktion`,
{ methode: "PUT", rumpf: { zeichen: "\u{1FA75}" } });
const blauAb = await ruf(keksHand, `/workspace/api/chat/nachrichten/${idFuerHerz}/reaktion`,
{ methode: "PUT", rumpf: { zeichen: "\u{1F499}" } });
ok(babyOk.code === 200, `ein babyblaues Herz geht durch (${babyOk.code})`);
ok(blauAb.code === 400, `ein blaues wird abgewiesen (${blauAb.code})`);
/* =====================================================================
5. IM CHAT-SKRIPT STEHT DER SATZ NICHT MEHR
===================================================================== */
console.log("");
console.log("--- Und im Browser ---");
const { readFileSync } = await import("node:fs");
const { dirname } = await import("node:path");
const { fileURLToPath } = await import("node:url");
const HIER = dirname(fileURLToPath(import.meta.url));
const chatJs = readFileSync(join(HIER, "..", "workspace", "assets", "js", "chat.js"), "utf8");
/* NUR AUSSERHALB VON KOMMENTAREN. Die Begründung, warum der Satz weg
ist, nennt ihn -- und muss das auch dürfen. Gesucht wird deshalb
nach ihm als Zeichenkette in Anführungszeichen, also so, wie er auf
den Bildschirm käme. */
const alsText = /(['"`])Nachricht zurückgenommen\1/.test(chatJs);
ok(!alsText, "der Satz steht in chat.js an keiner Stelle mehr,"
+ " an der ihn jemand lesen koennte");
ok(/Nachricht zurückgenommen/.test(chatJs),
"Gegenprobe: der Satz kommt in den Kommentaren durchaus vor —"
+ " die Suche oben unterscheidet also wirklich");
/* =====================================================================
5b. DER LOESCHKNOPF IST ERREICHBAR -- AUCH OHNE MAUS
=====================================================================
ZWEI FEHLER AN DERSELBEN STELLE, beide erst im Bildschirmfoto
aufgefallen (23.09.2026):
(1) `opacity: 0`, sichtbar erst beim Ueberfahren mit der Maus. Das
war vertretbar, solange der Knopf nur die EIGENE Nachricht
zuruecknahm. Seit er auch das Notfall-Loeschen traegt, ist er
auf einem Handy dauerhaft unsichtbar -- und er belegte trotzdem
87 px, also eine Luecke in der Fusszeile, die nichts erklaert.
(2) Der Notfall-Knopf bekam den Warnton, gedaempft gemischt mit der
leisen Schrift. Nachgerechnet: 1,91:1 auf Babyblau und 4,06 bis
4,13 auf den dunklen Toenen -- unlesbar auf der einen, zu wenig
auf allen anderen. Die leise Schriftfarbe ist genau der Wert,
der ueberall 4,5:1 schafft; wer daran mischt, gibt diese Zusage
auf.
Beides steht in der Stilvorlage und laesst sich dort pruefen. Eine
Messung im Browser waere genauer, braucht aber eine fremde
Nachricht, eine Rolle mit dem Recht und einen Zeiger -- fuer eine
Frage, deren Antwort in zwei Zeilen CSS steht.
===================================================================== */
console.log("");
console.log("--- Der Loeschknopf in der Stilvorlage ---");
{
const chatCss = readFileSync(
join(HIER, "..", "workspace", "assets", "css", "chat.css"), "utf8");
const regel = (wahl) => {
const i = chatCss.indexOf(wahl + " {");
return i < 0 ? null : chatCss.slice(i, chatCss.indexOf("}", i));
};
const grund = regel(".chat-nachricht__weg-knopf");
ok(!!grund, "die Regel zum Loeschknopf steht in chat.css");
ok(!!grund && !/opacity:\s*0/.test(grund),
"er ist nicht auf durchsichtig gesetzt -- auf einem Handy gibt es kein"
+ " Ueberfahren, und ein Knopf, den man dort nicht sieht, gibt es dort nicht");
const notfall = regel('.chat-nachricht__weg-knopf[data-notfall="ja"]');
ok(!!notfall, "und die Regel fuer das Notfall-Loeschen");
/* EIN EINFACHER TEXTVERGLEICH, KEIN REGULAERER AUSDRUCK.
-------------------------------------------------------------------
Hier stand `/\bcolor:/` -- gedacht als Wortgrenze, damit
`color-mix` nicht mitzaehlt. Geschrieben wurde es aber durch eine
Python-Zeichenkette, und dort ist `\b` das BACKSPACE-ZEICHEN. In
der Datei landete `/<0x08>color:/`, also ein Ausdruck, der nie
etwas findet.
Die Zeile darunter (die Gegenprobe) wurde dadurch rot -- und nur
deshalb ist es aufgefallen. DIESE Zeile hier war gruen, ohne je
etwas gesucht zu haben: genau der Fehler, vor dem die Hausregel
warnt, und ohne die Gegenprobe haette ihn niemand gesehen.
Jetzt zwei schlichte Textsuchen: „color:" muss vorkommen, und
Treffer, die zu `color-mix:` gehoeren, gibt es nicht -- die
Eigenschaft heisst `color-mix` nur als WERT, nie als Name vor
einem Doppelpunkt. */
ok(!!notfall && !notfall.includes("color:"),
"sie setzt KEINE eigene Schriftfarbe -- die Markierung sitzt im Punkt"
+ " davor, so wie beim Namen in der Blase");
ok(!!notfall && /::before/.test(
chatCss.slice(chatCss.indexOf('.chat-nachricht__weg-knopf[data-notfall="ja"]'),
chatCss.indexOf('.chat-nachricht__weg-knopf[data-notfall="ja"]') + 600)),
"der Punkt ist wirklich da (::before)");
/* GEGENPROBE: Die Suche muss eine Schriftfarbe auch FINDEN koennen.
Sonst waere „setzt keine" nur die Abwesenheit einer Messung. */
ok((regel(".chat-nachricht__von") || "").includes("color:"),
"Gegenprobe: in der Regel zum Namen findet dieselbe Suche sehr wohl eine Farbe");
}
/* =====================================================================
6. DIE UMSTELLUNG RAEUMT WIRKLICH AUF
ENTSTANDEN AUS EINEM FEHLER, DER SCHON LIVE WAR (22.09.2026, 23:33).
In der ersten Fassung stand `d.prepare("DELETE ...").changes` statt
`.run().changes` -- die Eigenschaft der vorbereiteten ANWEISUNG,
nicht die des Laufs. Die Anweisung wurde vorbereitet und nie
ausgefuehrt.
NICHTS STUERZTE AB, NICHTS WAR ROT. Der einzige Hinweis war ein Wort
im Protokoll: "undefined zurueckgenommene Nachrichten endgueltig
entfernt". Haette dort niemand hingesehen, waeren die fuenf Zeilen
liegen geblieben -- und weil die Sicherung VOR dem Loeschen
geschrieben wird, waere bei jedem Neustart eine weitere
Sicherungsdatei entstanden.
DESHALB WIRD HIER DIE ZAHL GEPRUEFT, nicht nur das Ergebnis. `0`
und `undefined` sehen in einer Meldung fast gleich aus; in einer
Bedingung nicht.
===================================================================== */
console.log("");
console.log("--- Die Umstellung fuer alte Zeilen ---");
{
const { loeschspurenAufraeumen } = await import("./workspace.js");
const idAlt = await schreiben(keksModi, "Aus einer alten Fassung");
/* Den alten Zustand von Hand herstellen: Text leer, weg_am gesetzt.
Genau so sahen die fuenf Zeilen in der echten Datenbank aus. */
d.prepare("UPDATE chat_nachrichten SET text = '', weg_am = ? WHERE id = ?")
.run(new Date().toISOString(), idAlt);
const davor = d.prepare(
"SELECT COUNT(*) AS n FROM chat_nachrichten WHERE weg_am IS NOT NULL AND text = ''").get().n;
ok(davor === 1, `eine alte zurueckgenommene Zeile steht da (${davor})`);
/* OHNE PFAD: Dann schreibt die Funktion keine Sicherung -- hier wird
das Aufraeumen gemessen, nicht das Sichern. */
const erg = loeschspurenAufraeumen(d, null);
ok(typeof erg.entfernt === "number",
`sie meldet eine ZAHL (${typeof erg.entfernt}: ${erg.entfernt})`
+ " — genau hier stand vorher undefined");
ok(erg.entfernt === 1, `und zwar die richtige (${erg.entfernt})`);
const danach = d.prepare(
"SELECT COUNT(*) AS n FROM chat_nachrichten WHERE weg_am IS NOT NULL AND text = ''").get().n;
ok(danach === 0, `die Zeile ist wirklich weg (${danach} uebrig)`);
/* WIEDERHOLBAR: Beim zweiten Mal gibt es nichts zu tun, und es wird
auch nichts gesichert. Ohne diese Zeile waere nicht bewiesen, dass
bei jedem Neustart nicht doch wieder etwas passiert. */
const nochmal = loeschspurenAufraeumen(d, null);
ok(nochmal.entfernt === 0 && nochmal.gesichert === null,
`beim zweiten Mal passiert nichts (${nochmal.entfernt}, Sicherung ${nochmal.gesichert})`);
}
/* ---------- Ende ------------------------------------------------------ */
d.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Windows haelt manchmal */ }
console.log("");
console.log(`${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"}`
+ ` -- ${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`);
process.exit(fehler === 0 ? 0 : 1);