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);