Files
dogfather-universe/server/pruef-chat.mjs
T
DogFatherGitandClaude Opus 5 60802170a1 B4 + B5: Loeschen heisst Loeschen, die Schrift wechselt mit, und Blau wird Babyblau
Die beiden Auftraege gehoeren zusammen, und zwar in dieser Reihenfolge:
Eine babyblaue Kachel ist HELL. Ohne mitwechselnde Schrift waere sie
unlesbar (1,63:1 gemessen). Erst B4 macht B5 moeglich.

B4a -- LOESCHEN HEISST LOESCHEN
  "Geloeschte Nachrichten verschwinden vollstaendig - keine Spur, kein
   'wurde geloescht'-Hinweis, bei niemandem, auch nicht bei DogFather."

Bis heute blieb die Zeile stehen: Text geleert, weg_am gesetzt, und im
Chat stand "Nachricht zurueckgenommen". Das war ausdruecklich so
begruendet ("ein Loch im Verlauf wirft mehr Fragen auf"). Das Argument
beantwortet aber eine andere Frage: Ein Hinweis "hier stand etwas"
MARKIERT die Stelle. Wer etwas aus Versehen schreibt, will es weg
haben und nicht unterstrichen.

Jetzt ein echtes DELETE. Vier Raender, an denen eine Spur bleiben
koennte, alle gemessen:
  1. die Zeile selbst
  2. chat_reaktionen    (ON DELETE CASCADE - nachgesehen, nicht angenommen)
  3. chat_erwaehnungen  (ebenso)
  4. letzte_am am Raum  - wird neu gerechnet, sonst stuende er oben in
                          der Liste mit einem Zeitpunkt, zu dem es
                          nichts mehr gibt
Dazu: ein Zitat auf eine geloeschte Nachricht wird weggelassen; in der
Gespraechsliste steht kein Hinweis mehr; der Knopf heisst "loeschen".

Die 5 alten zurueckgenommenen Zeilen (Text bereits leer) raeumt eine
einmalige, wiederholbare Umstellung ab - mit Sicherung davor, und nur
wenn es wirklich etwas zu tun gibt.

B4b -- WER DARF WAS
  "jeder nur seine eigenen - ausser DogFather und rechte Hand"
`darfJedeNachrichtLoeschen` = admin oder hand. NICHT fuehrtTeamDogi
(das schloesse die linke Hand ein) und nicht istLeitung (das schloesse
Spicy Media ein, die private Chats nicht einmal sehen darf). Das Recht
kommt vom Server ins Skript, nicht aus einer Rolle im Browser: chat.js
bekommt jeder, der die Seite oeffnet.

B4c -- DIE SCHRIFT WECHSELT MIT
  "am besten schwarz auf hellen Kacheln - und weiss, wenn jemand eine
   schwarze Kachel waehlt"
`schriftFuer(farbe)` waehlt zwischen zwei Paaren, gerechnet aus der
Leuchtdichte, mit denselben Schwellen wie das Rechenwerkzeug (7:1 fuer
den Text, 4,5:1 fuer die Fusszeile). Keine dritte Spalte in
CHAT_KACHELN, die jemand pflegen muesste. Flaeche und Schrift werden
im Browser in EINEM Griff gesetzt (blaseFaerben) - zwei Stellen waeren
irgendwann eine helle Kachel mit heller Schrift.

B4d -- DIE NAMEN
Der Name ist jetzt ein eigener Streifen mit Kante darunter, .84rem,
und der Rollenpunkt wird ein 3x14-Balken. Vorher stand er als erste
ZEILE in der Blase und las sich wie der Anfang des Satzes.

B5 -- BABYBLAU
  "jedes normales blaues herz durch babyblaues herz ersetzen ... jeder
   normale blaue farbe, sei es die kachel im chat oder emojis, nur
   babyblau bitte."
* Herz: U+1F499 -> U+1FA75. Nicht geglaubt, sondern gemessen: 43,9 px
  breit wie die anderen Herzen, ein Ersatzkaestchen waere 20,7. Die
  vier vorhandenen blauen Herzen in der Datenbank wandern mit - sonst
  waeren es vier Reaktionen, die ERLAUBT nicht mehr kennt: still weg.
* Kachel: neue, HELLE Kachel "Babyblau" mit dem Wert von --akzent.
  Gemessen: Text 10,78:1, Fusszeile 4,87:1. Sie steht bewusst nicht in
  der Rechnung des Werkzeugs (das rechnet elf Toene auf EINE
  Leuchtdichte) - sie hat eine andere Leuchtdichte und dafuer ihre
  eigene Schrift. Dasselbe Versprechen, anderer Weg.

PRUEFUNGEN
  pruef-loeschen.mjs (NEU, 20/0): alle vier Raender, die vier Faelle
    der Rechtegrenze (auch: die linke Hand darf NICHT), das babyblaue
    Herz am laufenden Server, und dass der Satz "Nachricht
    zurueckgenommen" nur noch in Kommentaren steht - mit Gegenprobe,
    dass die Suche diesen Unterschied wirklich macht.
  pruef-chatkachel.mjs (33/0): misst jede Kachel mit IHRER Schrift und
    fragt dafuer dieselbe Funktion wie der Server. Neu: dass die
    Schrift ueberhaupt wechselt. Die Leuchtdichte-Regel gilt jetzt fuer
    die gerechneten Toene - das Versprechen dahinter loest die
    Kontrastzeile darueber direkt ein.
  Vier alte Pruefungen umgedreht, die den alten Zustand festgeschrieben
    hatten: pruef-chat, pruef-chat-ausbau, pruef-chat-aufloesen,
    pruef-chat-optik. Alle mit scharferer Messung als vorher (Zahl
    davor/danach statt "ein Hinweis ist da").
  Mitgelaufen und gruen: pruef-alle-sehen-es, pruef-treffchat (110/0).

Gesichert: workspace-vor-loeschumbau-20260922-233313.db
(946 KB, integrity_check ok, 111 Nachrichten, 49 Reaktionen).

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

419 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.
/* CHAT — wer darf mit wem, und wer bekommt nichts zu sehen.
Wunsch Filipe, 06.09.2026. Auf Nachfrage entschieden: Zweier-
Gespräche UND Gruppen; Manager und Scouts dürfen sich auch ohne
gemeinsamen Creator schreiben.
WARUM DIESE PRÜFUNG ZUERST KAM, vor der Oberfläche: Ein Chat ist der
Ort, an dem Leute Dinge schreiben, die nicht jeder lesen soll. Ein
Fehler in der Sichtbarkeit ist hier teurer als überall sonst im
Haus -- eine falsche Zahl auf einer Kachel ärgert, ein mitgelesenes
Gespräch ist ein Vertrauensbruch.
GEMESSEN WIRD:
1. Die Regel selbst: Wer erscheint in wessen Partnerliste?
2. Kann man sie umgehen, indem man die Anfrage von Hand baut?
3. Kommt man an einen fremden Verlauf, wenn man die Raumnummer rät?
4. Ist eine Gruppe ein Schlupfloch zu jemandem, den man einzeln
nicht anschreiben dürfte?
5. Und die Alltagsfragen: Gibt es je Paar genau EIN Gespräch?
Zählt "ungelesen" richtig? Bleibt ein Zurücknehmen sichtbar?
GEGENPROBEN durchgehend: Jede Sperre wird auch von der erlaubten
Seite versucht. Eine Prüfung, die immer "verboten" sagt, beweist
nichts. */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-chat-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
const { eigenerPort } = await import("./helfer-port.mjs");
const PORT = await eigenerPort(import.meta, "die Chatpruefung");
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");
await new Promise((r) => setTimeout(r, 1000));
const BASIS = `http://127.0.0.1:${PORT}`;
let fehler = 0;
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
/* ---- Ein Bestand mit ECHTER Struktur ------------------------------------
Zwei getrennte Ketten, damit "fremd" wirklich fremd ist:
Cigdem (Manager) → Patrick (Scout) → Luna (Creator)
Meral (Manager) → Sven (Scout) → Nora (Creator)
Dazu DogFather über allem.
Ohne die zweite Kette liesse sich nicht prüfen, ob ein Creator an
einen fremden Creator kommt -- es gäbe schlicht keinen. */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
for (let i = 0; i < 60; i++) {
try {
const p = new DatabaseSync(process.env.WORKSPACE_DB);
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
} catch { await new Promise((r) => setTimeout(r, 250)); }
}
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768, jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idCigdem = anlegen("Cigdem", "manager", "CODE-MANA-0001");
const idMeral = anlegen("Meral", "manager", "CODE-MANA-0002");
const idPat = anlegen("Patrick", "scout", "CODE-SCOU-0001");
const idSven = anlegen("Sven", "scout", "CODE-SCOU-0002");
const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
const idNora = anlegen("Nora", "creator", "CODE-CREA-0002");
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, jetzt);
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idNora, idSven, jetzt);
d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)").run(idPat, idCigdem, jetzt);
d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)").run(idSven, idMeral, jetzt);
async function anmelden(rolle, code) {
const a = await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle, code }) });
const keks = (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; ");
return { ok: a.ok, keks };
}
const rufe = (keks, weg, koerper, methode = "GET") =>
fetch(BASIS + weg, {
method: methode,
headers: { "Content-Type": "application/json", Cookie: keks },
body: koerper === undefined ? undefined : JSON.stringify(koerper),
});
const holen = async (keks, weg) => (await rufe(keks, weg)).json();
const dogi = await anmelden("admin", "CODE-DOGI-0001");
const cigdem = await anmelden("manager", "CODE-MANA-0001");
const meral = await anmelden("manager", "CODE-MANA-0002");
const pat = await anmelden("scout", "CODE-SCOU-0001");
const luna = await anmelden("creator", "CODE-CREA-0001");
const nora = await anmelden("creator", "CODE-CREA-0002");
ok([dogi, cigdem, meral, pat, luna, nora].every((x) => x.ok && x.keks),
"alle sechs Rollen sind angemeldet");
/* =======================================================================
1. Die Regel: wer erscheint in wessen Liste?
======================================================================= */
console.log("\n=== Mit wem kann man schreiben? ===");
{
const namen = async (keks) => {
const { personen } = await holen(keks, "/workspace/api/chat/partner");
return (personen || []).map((p) => p.name).sort();
};
const beiLuna = await namen(luna.keks);
/* Ein Creator: sein Scout, dessen Manager, DogFather. Kein fremder
Creator, kein fremder Scout. */
ok(JSON.stringify(beiLuna) === JSON.stringify(["Cigdem", "Filipe", "Patrick"]),
`Creator Luna: ${beiLuna.join(", ")}`);
ok(!beiLuna.includes("Nora"),
"Luna sieht die andere Creatorin NICHT — Creator bleiben getrennt");
ok(!beiLuna.includes("Sven") && !beiLuna.includes("Meral"),
"und auch nicht den fremden Scout oder dessen Manager");
const beiPat = await namen(pat.keks);
/* Ein Scout: sein Creator, DogFather -- UND das ganze Team
(Entscheidung vom 06.09.2026: "für Absprachen im Team"). */
ok(beiPat.includes("Luna") && beiPat.includes("Filipe"),
`Scout Patrick erreicht seinen Creator und DogFather`);
ok(beiPat.includes("Meral") && beiPat.includes("Sven"),
`und das Team: ${beiPat.join(", ")}`);
ok(!beiPat.includes("Nora"),
"aber NICHT die fremde Creatorin — die Öffnung gilt nur fürs Team");
const beiDogi = await namen(dogi.keks);
ok(beiDogi.length === 6, `DogFather erreicht alle ${beiDogi.length} anderen`);
ok(!beiDogi.includes("Filipe"), "aber nicht sich selbst");
}
/* =======================================================================
2. Lässt sich die Regel von Hand umgehen?
======================================================================= */
console.log("\n=== Die Regel sitzt im Server, nicht in der Oberfläche ===");
{
/* Luna baut die Anfrage selbst und will Nora anschreiben. */
const a = await rufe(luna.keks, "/workspace/api/chat/direkt", { mit: idNora }, "POST");
ok(a.status === 403, `Luna → Nora von Hand: abgelehnt (${a.status})`);
const b = await rufe(luna.keks, "/workspace/api/chat/direkt", { mit: idSven }, "POST");
ok(b.status === 403, `Luna → fremder Scout: abgelehnt (${b.status})`);
/* GEGENPROBE: Zu ihrem eigenen Scout geht es. Ohne diesen Nachweis
bewiese "403" nur, dass die Route überhaupt ablehnt. */
const c = await rufe(luna.keks, "/workspace/api/chat/direkt", { mit: idPat }, "POST");
ok(c.ok, `Luna → ihr eigener Scout: geht (${c.status})`);
/* Und sich selbst anschreiben ergibt keinen Sinn. */
const e = await rufe(luna.keks, "/workspace/api/chat/direkt", { mit: idLuna }, "POST");
ok(e.status === 403, `sich selbst anschreiben: abgelehnt (${e.status})`);
}
/* =======================================================================
3. Ein fremder Verlauf, mit geratener Nummer
======================================================================= */
console.log("\n=== Kommt man an fremde Gespräche? ===");
let raumLunaPat = 0;
{
const { raum_id } = await (await rufe(luna.keks, "/workspace/api/chat/direkt",
{ mit: idPat }, "POST")).json();
raumLunaPat = raum_id;
ok(Number.isInteger(raumLunaPat) && raumLunaPat > 0, `Gespräch Luna↔Patrick ist #${raumLunaPat}`);
await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
{ text: "Vertraulich: mein Umsatz war 3200." }, "POST");
/* Nora rät die Nummer. */
const a = await rufe(nora.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`);
ok(a.status === 404, `Fremde liest Verlauf: ${a.status} (404, nicht 403 — die Nummer soll nichts verraten)`);
/* Und sie versucht hineinzuschreiben. */
const b = await rufe(nora.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
{ text: "Hallo?" }, "POST");
ok(b.status === 404, `Fremde schreibt hinein: ${b.status}`);
/* Auch als gelesen markieren geht nicht -- das wäre sonst ein Weg,
die Existenz eines Raums zu erfahren. */
const c = await rufe(nora.keks, `/workspace/api/chat/raeume/${raumLunaPat}/gelesen`,
{ bis: 999 }, "PUT");
ok(c.status === 404, `Fremde markiert als gelesen: ${c.status}`);
/* GEGENPROBE: Patrick, der drin ist, kommt heran. */
const e = await holen(pat.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`);
ok(e.nachrichten?.length === 1 && /3200/.test(e.nachrichten[0].text),
`Patrick liest den Verlauf (${e.nachrichten?.length} Nachricht)`);
/* Selbst DogFather kommt nicht ungefragt in ein fremdes Gespräch.
Das ist Absicht: Ein Chat, in dem der Chef stillschweigend mitliest,
ist kein Chat. Er darf jederzeit selbst schreiben -- aber sichtbar. */
const f = await rufe(dogi.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`);
ok(f.status === 404,
`auch DogFather liest fremde Gespräche NICHT mit (${f.status}) — er ist kein Teilnehmer`);
}
/* =======================================================================
4. Genau EIN Gespräch je Paar
======================================================================= */
console.log("\n=== Ein Paar, ein Gespräch ===");
{
/* Von der anderen Seite geöffnet muss derselbe Raum kommen -- sonst
schreibt einer ins Leere, während der andere wartet. */
const { raum_id: vonPat } = await (await rufe(pat.keks, "/workspace/api/chat/direkt",
{ mit: idLuna }, "POST")).json();
ok(vonPat === raumLunaPat,
`Patrick öffnet dasselbe Gespräch (#${vonPat} = #${raumLunaPat}), keine zweite Kopie`);
/* Und mehrfaches Öffnen legt nichts Neues an. */
for (let i = 0; i < 3; i++) {
await rufe(luna.keks, "/workspace/api/chat/direkt", { mit: idPat }, "POST");
}
const p = new DatabaseSync(process.env.WORKSPACE_DB);
const n = p.prepare("SELECT COUNT(*) AS n FROM chat_raeume WHERE art = 'direkt'").get().n;
p.close();
ok(n === 1, `nach fünfmaligem Öffnen: ${n} Gespräch in der Datenbank`);
}
/* =======================================================================
5. Gruppen — und ob sie ein Schlupfloch sind
======================================================================= */
console.log("\n=== Gruppen ===");
{
/* DER PUNKT: Luna darf Nora nicht einzeln anschreiben. Kann sie es
über eine Gruppe? */
const a = await rufe(luna.keks, "/workspace/api/chat/gruppe",
{ name: "Umweg", mit: [idPat, idNora] }, "POST");
ok(a.status === 403,
`eine Gruppe ist KEIN Schlupfloch: ${a.status} (Nora war dabei)`);
/* GEGENPROBE: Ohne Nora geht dieselbe Gruppe. */
const b = await rufe(luna.keks, "/workspace/api/chat/gruppe",
{ name: "Luna, Patrick & Cigdem", mit: [idPat, idCigdem] }, "POST");
ok(b.status === 201, `mit erlaubten Leuten: angelegt (${b.status})`);
const { raum_id: gruppe } = await b.json();
const sicht = await holen(luna.keks, `/workspace/api/chat/raeume/${gruppe}/nachrichten`);
ok(sicht.raum?.teilnehmer?.length === 3,
`die Gruppe hat ${sicht.raum?.teilnehmer?.length} Mitglieder`);
ok(sicht.raum?.name === "Luna, Patrick & Cigdem",
`und ihren Namen: "${sicht.raum?.name}"`);
/* Wer sie angelegt hat, führt sie. */
const chefin = sicht.raum.teilnehmer.find((t) => t.leitung);
ok(chefin?.id === idLuna, `die Leitung liegt bei ${chefin?.name}`);
/* Ohne Namen keine Gruppe. */
const c = await rufe(luna.keks, "/workspace/api/chat/gruppe",
{ name: "", mit: [idPat] }, "POST");
ok(c.status === 400, `ohne Namen: abgelehnt (${c.status})`);
/* Und ohne Leute auch nicht. */
const e = await rufe(luna.keks, "/workspace/api/chat/gruppe",
{ name: "Allein", mit: [] }, "POST");
ok(e.status === 400, `ohne Mitglieder: abgelehnt (${e.status})`);
/* Nora sieht die Gruppe nicht in ihrer Liste. */
const { raeume } = await holen(nora.keks, "/workspace/api/chat/raeume");
ok(!(raeume || []).some((r) => r.id === gruppe),
`Nora sieht die fremde Gruppe nicht (${raeume?.length || 0} eigene Gespräche)`);
}
/* =======================================================================
6. Ungelesen — die Zahl, auf die man sich verlässt
======================================================================= */
console.log("\n=== Ungelesene Nachrichten ===");
{
const vorher = (await holen(pat.keks, "/workspace/api/chat/ungelesen")).ungelesen;
await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
{ text: "Erste" }, "POST");
await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
{ text: "Zweite" }, "POST");
const nachher = (await holen(pat.keks, "/workspace/api/chat/ungelesen")).ungelesen;
ok(nachher === vorher + 2, `Patrick: ${vorher} → ${nachher} ungelesen`);
/* Die EIGENEN zählen nicht mit -- sonst stünde nach jeder eigenen
Nachricht eine rote Zahl an der eigenen Kopfleiste. */
const beiLuna = (await holen(luna.keks, "/workspace/api/chat/ungelesen")).ungelesen;
ok(beiLuna === 0, `Luna hat ${beiLuna} ungelesene (ihre eigenen zählen nicht)`);
/* Lesen setzt sie zurück. */
const verlauf = await holen(pat.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`);
const letzteId = verlauf.nachrichten.at(-1).id;
await rufe(pat.keks, `/workspace/api/chat/raeume/${raumLunaPat}/gelesen`,
{ bis: letzteId }, "PUT");
const danach = (await holen(pat.keks, "/workspace/api/chat/ungelesen")).ungelesen;
ok(danach === 0, `nach dem Lesen: ${danach}`);
/* NUR VORWÄRTS: Ein verspäteter Aufruf mit einer alten Nummer darf
den Stand nicht zurückdrehen -- sonst stünde Gelesenes wieder als
ungelesen da. */
await rufe(pat.keks, `/workspace/api/chat/raeume/${raumLunaPat}/gelesen`, { bis: 1 }, "PUT");
const zurueck = (await holen(pat.keks, "/workspace/api/chat/ungelesen")).ungelesen;
ok(zurueck === 0, `ein alter "gelesen bis 1" dreht nichts zurück (${zurueck})`);
}
/* =======================================================================
7. Zurücknehmen
======================================================================= */
console.log("\n=== Eine Nachricht zurücknehmen ===");
{
const a = await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
{ text: "Das war ein Versehen" }, "POST");
const { nachricht } = await a.json();
/* Patrick darf sie NICHT zurücknehmen -- fremde Worte aus einem
Gespräch zu nehmen, in dem Absprachen stehen, wäre ein Eingriff in
die Geschichte. */
const b = await rufe(pat.keks, `/workspace/api/chat/nachrichten/${nachricht.id}`, undefined, "DELETE");
ok(b.status === 403, `fremde Nachricht zurücknehmen: abgelehnt (${b.status})`);
/* Auch DogFather nicht. */
const c = await rufe(dogi.keks, `/workspace/api/chat/nachrichten/${nachricht.id}`, undefined, "DELETE");
ok(c.status === 404 || c.status === 403,
`auch DogFather nicht (${c.status})`);
/* Die eigene schon. */
const e = await rufe(luna.keks, `/workspace/api/chat/nachrichten/${nachricht.id}`, undefined, "DELETE");
ok(e.ok, `die eigene: geht (${e.status})`);
/* UND SIE IST WIRKLICH WEG (22.09.2026 abends).
------------------------------------------------------------------
Hier stand das Gegenteil: „sie bleibt sichtbar -- als
zurueckgenommen, nicht als Loch." Das war die bewusste
Entscheidung bis heute Nachmittag.
Filipe: „Geloeschte Nachrichten verschwinden vollstaendig -- keine
Spur, kein ‚wurde geloescht'-Hinweis, bei niemandem, auch nicht
bei DogFather."
Die Zeile misst jetzt beides: dass sie fehlt UND dass der Rest des
Verlaufs noch da ist. Ohne den zweiten Teil waere ein Verlauf, der
ueberhaupt nichts mehr liefert, ebenfalls gruen. */
const verlauf = await holen(pat.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`);
const weg = verlauf.nachrichten.find((n) => n.id === nachricht.id);
ok(!weg && verlauf.nachrichten.length > 0,
`sie steht in keinem Verlauf mehr, und die uebrigen`
+ ` ${verlauf.nachrichten.length} stehen noch da`);
ok(!verlauf.nachrichten.some((n) => n.zurueckgenommen),
"und es steht nirgends ein Hinweis, dass hier etwas stand");
ok(!JSON.stringify(verlauf).includes("Das war ein Versehen"),
"der Text ist wirklich weg, nicht nur ausgeblendet");
}
/* =======================================================================
8. Grenzen der Eingabe
======================================================================= */
console.log("\n=== Was nicht hineingeht ===");
{
for (const [was, koerper, status] of [
["leerer Text", { text: "" }, 400],
["nur Leerzeichen", { text: " " }, 400],
["ohne Feld", {}, 400],
["zu lang (4001)", { text: "z".repeat(4001) }, 400],
]) {
const a = await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
koerper, "POST");
ok(a.status === status, `${was}: ${a.status}`);
}
/* GEGENPROBE: 4000 Zeichen gehen. */
const a = await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
{ text: "z".repeat(4000) }, "POST");
ok(a.status === 201, `genau 4000 Zeichen: ${a.status} — die Grenze ist nicht zu streng`);
}
/* =======================================================================
9. Ohne Anmeldung gar nichts
======================================================================= */
console.log("\n=== Ohne Anmeldung ===");
{
for (const [was, weg, methode] of [
["Partnerliste", "/workspace/api/chat/partner", "GET"],
["Gesprächsliste", "/workspace/api/chat/raeume", "GET"],
["Verlauf", `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`, "GET"],
["Ungelesen", "/workspace/api/chat/ungelesen", "GET"],
["Strom", "/workspace/api/chat/strom", "GET"],
]) {
const a = await fetch(BASIS + weg, { method: methode });
ok(a.status === 401, `${was}: ${a.status}`);
}
}
d.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
/* HIER IST process.exit() RICHTIG -- anders als in pruef-push-weg.
Diese Pruefung startet einen Express-Server, und der laeuft von
selbst weiter: offene Verbindungen, ein Zeitgeber fuer die
Sicherung, bei dieser Pruefung zusaetzlich die offenen
Ereignisstroeme des Chats. Mit `exitCode` allein wartet Node ewig
darauf, dass all das zu Ende geht -- der Lauf lief in die
Zeitueberschreitung, obwohl alle Pruefungen schon durch waren.
Der Absturz, der in pruef-push-weg gegen exit() sprach, kam von
fetch-Verbindungen mitten im Schliessen. Die gibt es hier nicht
mehr: Die Browser sind zu, bevor diese Zeile laeuft. */
process.exit(fehler ? 1 : 0);