From c0d2896419c996234f383b41676e9377178f54ec Mon Sep 17 00:00:00 2001 From: Dogfather Date: Wed, 23 Sep 2026 12:23:27 +0200 Subject: [PATCH] pruef-chat-neu: die drei neuen Sachen am Stueck, als Modi, auf dem Handy Jedes Stueck war geprueft -- und jedes in einer anderen Lage als der echten: pruef-chat-anhaenge Sprachnachricht und GIF, aber als DogFather auf der AGENTURadresse, mit der Maus pruef-erwaehnung der Rudel-Ruf, aber gegen den Server Der Fall, den Filipe wirklich hat -- ein MODI auf crew., mit dem FINGER, und ein zweiter Mensch, der es bekommen soll -- war nie am Stueck gemessen. Genau in solchen Luecken sitzen die Fehler, die niemand sieht: Jeder Einzeltest ist gruen, und zusammen geht es trotzdem nicht. ZWEI BROWSER, ZWEI MENSCHEN. Eine Sprachnachricht, die nur beim Absender im Verlauf steht, ist keine. Jede der drei Proben wartet deshalb darauf, dass die ZWEITE Person sie von selbst bekommt -- ohne Neuladen. 19 Pruefungen, 0 Fehler: * Sprachnachricht: Knopf da, Zeit laeuft, Blase mit Laenge, passt auf 390 px, kommt bei der zweiten Modi an * GIF-Kiste: passt auf den Bildschirm, sagt was zu tun ist, GIF hineinlegen, verschicken, ankommen -- und die zweite Modi sieht dasselbe GIF in IHRER Kiste (sie gehoert dem Rudel) * Rudel-Ruf: das @ oeffnet die Liste, "rudel" steht oben mit "alle im Team" daneben, 44 px hoch, im Bild; die Nachricht kommt an, der Ruf leuchtet beim Empfaenger (Gewicht 700), und in der Datenbank stehen genau die Richtigen -- die andere Modi und DogFather, nicht der Rufer selbst ZWEI EIGENE FEHLER BEIM BAUEN, beide in der Datei aufgeschrieben, weil sie sich wiederholen werden: (1) OHNE NOTBREMSE. Der erste Lauf blieb stehen und sah von aussen aus wie "laeuft noch" -- ich musste ihn von Hand abbrechen. Ein Werkzeug, das haengt, ist schlimmer als eines, das scheitert; das steht seit dem 06.09. in den Hausregeln, und ich habe es beim Bauen eines Wegwerf-Werkzeugs trotzdem weggelassen. (2) MIT ENTER ABGESCHICKT. Auf einem Beruehrgeraet schickt Enter ABSICHTLICH nicht ab -- sonst kaeme man nie zu einer zweiten Zeile. Die Messung meldete daraufhin "kommt nicht an"; in Wahrheit war nie etwas abgeschickt worden. Dasselbe Muster wie beim `pointer: coarse` heute frueh: Wer im falschen Geraeteprofil misst, misst ein anderes Programm. Die Datei ist aus einem Wegwerf-Werkzeug entstanden. Sie bleibt, weil der Weg, den sie geht, sonst von keiner Pruefung gegangen wird. pruef-ports: 8 von 8 -- die neue Datei verschiebt die Portnummern aller spaeteren, und das haelt. Co-Authored-By: Claude Opus 5 --- server/pruef-chat-neu.mjs | 282 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 server/pruef-chat-neu.mjs diff --git a/server/pruef-chat-neu.mjs b/server/pruef-chat-neu.mjs new file mode 100644 index 00000000..ec792729 --- /dev/null +++ b/server/pruef-chat-neu.mjs @@ -0,0 +1,282 @@ +/* ===================================================================== + pruef-chat-neu.mjs — Sprachnachricht, GIF-Kiste und der Ruf ans Rudel + + Alle drei sind in der Nacht zum 23.09.2026 entstanden: + + „dan will ich auch dass man im chat auch sprachnachrichten und + gifts reinschicken kann. also nur die modis rechte linke hand + und dogfather." + „dan will ich auch dass nur die modis, rechte hand, linke hand + und dogfather, alle auch auf einmal markieren koennen im chat + mit einem, @rudel ,dann sollen alle eine benarichtigung + bekommen." + + --------------------------------------------------------------------- + WARUM ES DIESE DATEI GIBT, obwohl jedes Stueck schon geprueft ist + + Geprueft war jedes einzeln -- und jedes in einer anderen Lage als + der echten: + + pruef-chat-anhaenge Sprachnachricht und GIF, aber als DogFather + auf der AGENTURadresse, mit der Maus + pruef-erwaehnung der Rudel-Ruf, aber gegen den Server + + Der Fall, den Filipe wirklich hat -- ein MODI auf crew., mit dem + FINGER, und ein zweiter Mensch, der es bekommen soll -- war nie am + Stueck gemessen. Genau in solchen Luecken sitzen die Fehler, die + niemand sieht: Jeder Einzeltest ist gruen, und zusammen geht es + trotzdem nicht. + + ZWEI BROWSER, ZWEI MENSCHEN. Eine Sprachnachricht, die nur beim + Absender im Verlauf steht, ist keine. Deshalb wartet jede der drei + Proben darauf, dass die ZWEITE Person sie von selbst bekommt -- + ohne Neuladen. + + --------------------------------------------------------------------- + ZWEI EIGENE FEHLER BEIM BAUEN, beide aufgeschrieben, weil sie sich + wiederholen werden: + + (1) OHNE NOTBREMSE. Der erste Lauf blieb stehen und sah von aussen + aus wie „laeuft noch". Ein Werkzeug, das haengt, ist schlimmer + als eines, das scheitert. + + (2) MIT ENTER ABGESCHICKT. Auf einem Beruehrgeraet schickt Enter + ABSICHTLICH nicht ab -- sonst kaeme man nie zu einer zweiten + Zeile. Die Messung meldete daraufhin „kommt nicht an"; in + Wahrheit war nie etwas abgeschickt worden. Dasselbe Muster wie + beim `pointer: coarse` am selben Tag: Wer im falschen + Geraeteprofil misst, misst ein anderes Programm. + + AUFRUF: node server/pruef-chat-neu.mjs + ===================================================================== */ +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request as httpAnfrage } from "node:http"; +import { execFileSync } from "node:child_process"; +import { createServer as httpsServer } from "node:https"; + +import { eigenerPort } from "./helfer-port.mjs"; +const PORT = await eigenerPort(import.meta, "pruef-chat-neu"); +const CREW = "crew.dogfather-universe.com"; +const ordner = mkdtempSync(join(tmpdir(), "ws-drei-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; +process.env.TREFF_NACHT_AB = "0"; +process.env.TREFF_NACHT_BIS = "0"; + +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. Ein Messwerkzeug, das HAENGT, ist schlimmer als eines, + das scheitert: Von aussen sieht Stillstand aus wie "laeuft noch". + Genau das ist mir hier gerade passiert. */ +const { notbremse } = await import("./helfer-notbremse.mjs"); +notbremse(240_000, "pruef-chat-neu"); +await new Promise((r) => setTimeout(r, 1300)); + + +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 k = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel,wert,geaendert,von) VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", k, jetzt); +const mach = (n, r, c) => { + const s = randomBytes(16).toString("hex"); + const h = scryptSync(c, s, 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(n, r, h, s, 32768, createHmac("sha256", k).update(c).digest("hex"), jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +}; +const idDogi = mach("Filipe", "admin", "CODE-DOGI-0001"); +mach("Rieke", "hand", "CODE-HAND-0001"); +const idA = mach("Frida", "modi", "CODE-MODI-0001"); +const idB = mach("Nele", "modi", "CODE-MODI-0002"); +d.close(); + +const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt"); +execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", schl, + "-out", zert, "-days", "2", "-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`], + { stdio: "ignore" }); +const HP = PORT + 1; +const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) }, (von, nach) => { + const w = httpAnfrage({ host: "127.0.0.1", port: PORT, path: von.url, method: von.method, + headers: { ...von.headers, host: `${CREW}:${HP}` } }, + (a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); }); + von.pipe(w); +}); +await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r)); +const SEITE = `https://${CREW}:${HP}`; + +const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await pw.chromium.launch({ + args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors", + "--use-fake-device-for-media-stream", "--use-fake-ui-for-media-stream"] }); + +async function alsPerson(rolle, code) { + const ctx = await browser.newContext({ + viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, + hasTouch: true, isMobile: true, reducedMotion: "reduce", + permissions: ["microphone"] }); + const seite = await ctx.newPage(); + const meckern = []; + seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_CERT|net::ERR/i.test(m.text())) meckern.push(m.text().slice(0, 110)); }); + seite.on("pageerror", (e) => meckern.push("Absturz: " + String(e).slice(0, 110))); + await seite.goto(`${SEITE}/workspace/`, { waitUntil: "networkidle" }); + await seite.click(`.rolle[data-rolle="${rolle}"]`); + await seite.fill("#code", code); + await seite.click("button[type=submit]"); + await seite.waitForTimeout(1300); + return { ctx, seite, meckern }; +} + +let fehler = 0, geprueft = 0; +const ok = (b, t) => { + geprueft++; + console.log((b ? " ok " : " FEHL ") + t); + if (!b) fehler++; +}; + +const frida = await alsPerson("modi", "CODE-MODI-0001"); +const nele = await alsPerson("modi", "CODE-MODI-0002"); + +/* Beide in den Treff -- dort sitzen alle. */ +for (const p of [frida, nele]) { + await p.seite.goto(`${SEITE}/workspace/chat.html?raum=treff`, { waitUntil: "networkidle" }); + await p.seite.waitForTimeout(1800); +} + +console.log("\n=== 1. Sprachnachricht ==="); +{ + const vorher = await nele.seite.locator(".chat-nachricht").count(); + const knopfDa = await frida.seite.locator("#ton-auf").isVisible(); + ok(knopfDa, "der Modi sieht den Mikrofonknopf auf crew."); + await frida.seite.click("#ton-auf"); + await frida.seite.waitForSelector("#tonleiste:not([hidden])", { timeout: 8000 }); + await frida.seite.waitForTimeout(2400); + const zeit = (await frida.seite.locator("#ton-zeit").innerText()).trim(); + ok(/^0:0[2-9]$/.test(zeit), `die Zeit laeuft (${zeit})`); + await frida.seite.click("#ton-senden"); + await frida.seite.waitForTimeout(3200); + const blase = await frida.seite.evaluate(() => { + const t = document.querySelector(".chat-ton"); + return t ? { marke: t.querySelector(".chat-ton__marke")?.textContent.trim(), + dauer: t.querySelector(".chat-ton__dauer")?.textContent.trim(), + breit: Math.round(t.getBoundingClientRect().width) } : null; + }); + ok(!!blase, `sie steht in seinem Verlauf (${blase?.marke} ${blase?.dauer})`); + ok((blase?.breit || 0) >= 200 && (blase?.breit || 0) <= 390, + `und passt auf den Bildschirm (${blase?.breit} px)`); + /* UND KOMMT BEIM ANDEREN AN, ohne dass der etwas tut. */ + const beiNele = await nele.seite.waitForFunction( + (v) => document.querySelectorAll(".chat-nachricht").length > v && document.querySelector(".chat-ton"), + vorher, { timeout: 12000 }).then(() => true).catch(() => false); + ok(beiNele, "die zweite Modi bekommt sie von selbst -- ohne Neuladen"); +} + +console.log("\n=== 2. GIF aus der Kiste ==="); +{ + const GIF = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + ok(await frida.seite.locator("#gif-auf").isVisible(), "der Modi sieht den GIF-Knopf"); + await frida.seite.click("#gif-auf"); + await frida.seite.waitForSelector("#gif-tafel:not([hidden])", { timeout: 8000 }); + const tafel = await frida.seite.evaluate(() => { + const t = document.getElementById("gif-tafel"); + const r = t.getBoundingClientRect(); + return { b: Math.round(r.width), h: Math.round(r.height), + imBild: r.left >= -1 && r.right <= innerWidth + 1 && r.top >= -1, + leer: (document.getElementById("gif-leer")?.textContent || "").trim().slice(0, 40) }; + }); + ok(tafel.imBild, `die Kiste passt auf den Bildschirm (${tafel.b}x${tafel.h})`); + ok(/noch leer/.test(tafel.leer), `und sagt, was zu tun ist ("${tafel.leer}…")`); + await frida.seite.setInputFiles("#gif-feld", { + name: "daumen.gif", mimeType: "image/gif", buffer: Buffer.from(GIF, "base64") }); + await frida.seite.waitForTimeout(2400); + const kacheln = await frida.seite.locator(".gif-kachel").count(); + ok(kacheln === 1, `ein GIF liegt in der Kiste (${kacheln})`); + const vorher2 = await nele.seite.locator(".chat-nachricht").count(); + await frida.seite.click(".gif-kachel__knopf"); + await frida.seite.waitForTimeout(2600); + const beiNele = await nele.seite.waitForFunction( + (v) => document.querySelectorAll(".chat-nachricht").length > v, + vorher2, { timeout: 12000 }).then(() => true).catch(() => false); + ok(beiNele, "das GIF kommt bei der zweiten Modi an"); + /* Und die Kiste ist fuer BEIDE da -- sie gehoert dem Rudel. */ + await nele.seite.click("#gif-auf"); + await nele.seite.waitForTimeout(1600); + const beiIhr = await nele.seite.locator(".gif-kachel").count(); + ok(beiIhr === 1, `die zweite Modi sieht dasselbe GIF in ihrer Kiste (${beiIhr})`); + await nele.seite.keyboard.press("Escape").catch(() => {}); +} + +console.log("\n=== 3. Der Ruf ans Rudel ==="); +{ + const vorher = await nele.seite.locator(".chat-nachricht").count(); + await frida.seite.click("#text"); + await frida.seite.type("#text", "@", { delay: 60 }); + await frida.seite.waitForTimeout(700); + const liste = await frida.seite.evaluate(() => { + const l = document.getElementById("erwaehn-liste"); + if (!l || l.hidden) return null; + const erste = l.querySelector(".erwaehn-liste__eintrag"); + const r = erste?.getBoundingClientRect(); + return { erster: l.querySelector(".erwaehn-liste__name")?.textContent.trim() || "", + zusatz: l.querySelector(".erwaehn-liste__rolle")?.textContent.trim() || "", + hoch: Math.round(r?.height || 0), + imBild: r ? (r.left >= -1 && r.right <= innerWidth + 1) : false }; + }); + ok(!!liste, "ein @ oeffnet die Liste"); + ok(liste?.erster === "rudel", `der Ruf steht oben (${liste?.erster})`); + ok(/alle im Team/i.test(liste?.zusatz || ""), `mit der Erklaerung daneben (${liste?.zusatz})`); + ok((liste?.hoch || 0) >= 44 && liste?.imBild, + `am Daumen treffbar und im Bild (${liste?.hoch} px)`); + await frida.seite.keyboard.press("Enter"); + await frida.seite.waitForTimeout(400); + await frida.seite.type("#text", "bitte alle kurz schauen", { delay: 10 }); + /* NICHT MIT ENTER. Auf einem Beruehrgeraet schickt Enter + ABSICHTLICH nicht ab -- sonst kaeme man nie zu einer zweiten + Zeile (steht so in chat.html). Der erste Anlauf dieser Messung + hat genau das uebersehen und einen Fehler gemeldet, den es nicht + gibt: Die Nachricht war nie abgeschickt worden. */ + await frida.seite.click("#senden"); + await frida.seite.waitForTimeout(2800); + const beiNele = await nele.seite.waitForFunction( + (v) => document.querySelectorAll(".chat-nachricht").length > v, + vorher, { timeout: 12000 }).then(() => true).catch(() => false); + ok(beiNele, "die Nachricht kommt bei der zweiten Modi an"); + const marke = await nele.seite.evaluate(() => { + const m = document.querySelector('.chat-erwaehnung[data-rudel="ja"]'); + return m ? { wort: m.textContent.trim(), + gewicht: getComputedStyle(m).fontWeight } : null; + }); + ok(!!marke, `und der Ruf leuchtet bei ihr (${marke?.wort}, Gewicht ${marke?.gewicht})`); + /* Und in der Datenbank steht, dass sie gemeint war. */ + const db2 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true }); + const gemeint = db2.prepare(`SELECT e.person_id AS wer FROM chat_erwaehnungen e + JOIN chat_nachrichten n ON n.id = e.nachricht_id WHERE n.rudel = 1`).all().map((x) => x.wer); + db2.close(); + ok(gemeint.includes(idB) && gemeint.includes(idDogi) && !gemeint.includes(idA), + `gerufen wurden ${gemeint.length} -- die andere Modi und DogFather, nicht sie selbst`); +} + +const alleMeldungen = [...frida.meckern, ...nele.meckern]; +console.log(""); +ok(alleMeldungen.length === 0, + alleMeldungen.length + ? `ein Browser meldet etwas: ${alleMeldungen[0]}` + : "kein Browser meldet etwas"); + +await frida.ctx.close(); await nele.ctx.close(); +await browser.close(); +await new Promise((r) => vorbau.close(r)); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ } + +console.log(""); +console.log(`${geprueft} Pruefungen, ${fehler} Fehler`); +console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +process.exit(fehler ? 1 : 0);