Chat: "@rudel" ruft das ganze Team -- und nur die vier duerfen es
Filipe: "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."
DAS WAR EINE OFFENE FRAGE, UND ER HAT SIE BEANTWORTET
-----------------------------------------------------
In chat-erwaehnung.js stand seit dem 20.09. woertlich: "KEIN @alle. Es
waere in fuenf Minuten gebaut und ist der zuverlaessigste Weg, dass alle
die Benachrichtigungen abschalten -- und dann kommt auch die an, die
wirklich fuer einen bestimmten Menschen gedacht war. Wenn Filipe es
ausdruecklich will, gehoert dazu eine Entscheidung, WER es benutzen
darf; das ist eine Frage an ihn, keine, die ich hier beantworte."
Seine Antwort ist genau diese Entscheidung, und sie ist die Sicherung:
Team Dogi und DogFather duerfen rufen, sonst niemand.
WEN DER RUF ERREICHT: DAS TEAM IM RAUM, NICHT DEN RAUM
------------------------------------------------------
"Rudel" heisst das Team -- dieselben vier Gruppen, die auch rufen
duerfen. In einem Team-Kanal ist das jeder Anwesende, also "alle auf
einmal". Sichtbar wird der Unterschied im TREFF, und dort haette die
andere Lesart wehgetan: Dort sitzt die Community. Ein Ruf, der jeden
Zuschauer weckt, waere etwas anderes als der bestellte -- und beim
zweiten Mal haetten sie die Benachrichtigungen abgeschaltet.
WO WAS ENTSCHIEDEN WIRD
-----------------------
chat-erwaehnung.js bleibt ohne Abhaengigkeiten (die Pruefung soll sie
lesen koennen, ohne einen Server hochzufahren). Sie sagt nur, DASS
gerufen wurde; der Aufrufer sagt ihr, ob der Schreibende darf. Wer zum
Rudel gehoert, entscheidet workspace-chat.js ueber istTeamDogi().
Das Schluesselwort gewinnt gegen einen Menschen, der "Rudel" heisst.
Heute heisst niemand so -- aber das ist eine Tatsache von heute, kein
Gesetz. So herum verliert niemand eine Meldung (wer so heisst, ist im
Rudel dabei); andersherum haette ein einziger Zugang den Ruf ans Team
stillschweigend abgeschaltet.
istTeamDogi() NEU IN workspace.js
---------------------------------
Derselbe Ausdruck ("admin oder TEAM_DOGI_ROLLEN") stand dort dreimal
wortgleich: siehtModis, kanaeleFuer, kategorienFuer. Drei gleiche
Ausdruecke sind drei Gelegenheiten, dass einer beim naechsten
Rollenzuschnitt nicht mitgeht. Jetzt eine Stelle, die drei benutzen.
DIE NACHRICHT MERKT SICH DEN RUF (Spalte chat_nachrichten.rudel)
----------------------------------------------------------------
Beim Lesen muesste der Browser sonst wissen, ob der Absender es DAMALS
durfte. Er kennt nur dessen heutige Rolle -- wechselt jemand die Rolle,
verschwaende die Hervorhebung rueckwirkend aus einem Satz von vorletzter
Woche. Und es waere die zweite Rechnung ueber dieselbe Frage.
Vorgabe 0; alle alten Nachrichten haben kein Rudel gerufen, und das ist
keine Annahme, sondern eine Tatsache: Das Wort gab es noch nicht.
DIE MELDUNG SAGT, WAS LOS IST
-----------------------------
"X hat das Rudel gerufen", nicht "X hat dich erwaehnt" -- letzteres
stimmt beim Rudel nicht, und wer dreimal liest, dass er gemeint sei,
und jedes Mal merkt, dass es alle betraf, glaubt beim vierten Mal auch
dem echten nicht mehr. Auf DEMSELBEN Schalter wie die Erwaehnung: Ein
vierter Schalter waere der, den jemand abschaltet und der dann genau im
wichtigen Moment fehlt. Jeder Ruf steht im Protokoll (chat_rudel) --
"@rudel wird zu oft benutzt" soll eine Zahl sein koennen, kein Gefuehl.
DIE FARBE WIRD ABGELEITET, NICHT GESETZT
----------------------------------------
Eine Nachricht liegt in der Blase, deren Farbe ihr Absender AUSGESUCHT
hat -- siebzehn Moeglichkeiten. Eine feste Farbe darauf ist eine Wette,
und genau die habe ich am 22.09. beim Loeschknopf verloren (1,91:1 auf
Babyblau, gemessen, nachdem es live war). Die Marke nimmt deshalb
`--blase-text` -- die Schrift, die schriftFuer() fuer DIESE Blase mit
mindestens 7:1 ausgerechnet hat. Unterschieden wird ueber Form statt
Farbton: Toenung, Kante, Gewicht 700, ein Zeichen davor. In der
Auswahlliste darf es einen eigenen Ton haben -- sie liegt auf der
Flaeche des Hauses, deren Farbe feststeht.
DER VORSCHLAG ERSCHEINT NUR, WO ER ETWAS BEWIRKT
------------------------------------------------
In einem Zweier-Gespraech mit einem Creator ist ausser mir niemand aus
dem Team. `darf_rudel` fragt deshalb beides: darf ich, und sitzt hier
noch jemand aus dem Team. Dieselbe Ueberlegung wie beim eigenen Namen,
den die Liste auch nicht anbietet. Die Schranke beim Schreiben haengt
nicht daran -- wer es von Hand tippt, ruft eben niemanden.
GEPRUEFT
--------
pruef-erwaehnung: 119 Pruefungen, 0 Fehler (vorher 66).
Neu darin, und die zweite ist die wichtigere:
* der Modi ruft im Treff genau das Team -- die Liste wird aus der
Besetzung ABGELEITET, nicht abgeschrieben
* der Gast im selben Raum ruft NICHTS: kein Eintrag, kein Merkmal,
kein Vorschlag. Ohne diese Pruefung stuende Filipes "nur die
modis, rechte hand, linke hand und dogfather" bloss im Kommentar
* beide Fassungen der Regel (Server und Browser) an 14 zusaetzlichen
Proben nebeneinander, mit beiden Rechten -- samt Gegenprobe, dass
der Vergleich einen Unterschied ueberhaupt sehen kann
* am Bildschirm: der Ruf steht oben in der Liste, sagt daneben, was
er bedeutet, Enter setzt ihn ein, und im Satz ist er an Gewicht
und Rahmen erkennbar -- nicht nur an der Farbe
Beim Bauen gemessen statt vermutet: Ein Scout und ein Modi kommen gar
nicht in denselben Raum (403, Haeusertrennung) -- deshalb prueft der
Verhaltenstest im Treff. Und ein Gast meldet sich nur mit
Altersbestaetigung an (400 ohne).
Die Nachtruhe des Treffs wird in dieser Pruefung abgeschaltet (gleiche
Stunden = keine Nachtruhe). Sonst waere sie zwischen Mitternacht und
sechs rot und danach gruen -- ein Test, der die Wanduhr misst.
pruef-chat, pruef-treffchat (110), pruef-chat-kanaele (81),
pruef-chat-ausbau (64): alle unveraendert gruen.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+425
-1
@@ -40,7 +40,7 @@ import { fileURLToPath } from "node:url";
|
||||
import { request as httpAnfrage } from "node:http";
|
||||
import { eigenerPort } from "./helfer-port.mjs";
|
||||
import { notbremse } from "./helfer-notbremse.mjs";
|
||||
import { erwaehnungenFinden, erwaehnteIds } from "./chat-erwaehnung.js";
|
||||
import { erwaehnungenFinden, erwaehnteIds, rudelGerufen, RUDEL_WORT } from "./chat-erwaehnung.js";
|
||||
|
||||
const HIER = dirname(fileURLToPath(import.meta.url));
|
||||
const PORT = await eigenerPort(import.meta, "pruef-erwaehnung");
|
||||
@@ -50,6 +50,19 @@ 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";
|
||||
/* KEINE NACHTRUHE WAEHREND DER PRUEFUNG (23.09.2026).
|
||||
|
||||
Abschnitt 7 schreibt im Treff -- und dort gilt von 0 bis 6 Uhr
|
||||
Schreibverbot. Eine Pruefung, die zwischen Mitternacht und sechs
|
||||
scheitert und danach wieder gruen ist, misst die Uhr, nicht das
|
||||
Programm. Genau diese Falle steht in den Hausregeln ("Ein Test, der
|
||||
die Wanduhr als Annahme benutzt, misst irgendwann das Gegenteil").
|
||||
|
||||
Gleiche Werte heissen "keine Nachtruhe" -- so steht es in
|
||||
istNachtruhe(), erste Zeile. Die Nachtruhe SELBST wird in
|
||||
pruef-treffchat geprueft, dort gehoert sie hin. */
|
||||
process.env.TREFF_NACHT_AB = "0";
|
||||
process.env.TREFF_NACHT_BIS = "0";
|
||||
|
||||
const express = (await import("express")).default;
|
||||
const ec = express.response.cookie;
|
||||
@@ -141,6 +154,73 @@ melde("\n=== 1. Wer ist gemeint? ===");
|
||||
immer jemanden findet, findet nichts. */
|
||||
ok(erwaehnteIds("@VanVan", []).length === 0,
|
||||
"Gegenprobe: ohne Teilnehmerliste ist niemand gemeint");
|
||||
|
||||
/* ---- "@RUDEL" — ALLE AUF EINMAL (23.09.2026) ------------------
|
||||
|
||||
Filipe: „dan will ich auch dass nur die modis, rechte hand, linke
|
||||
hand und dogfather, alle auch auf einmal markieren können im chat
|
||||
mit einem, @rudel ,dann sollen alle eine benarichtigung bekommen."
|
||||
|
||||
DIE WICHTIGSTE PRÜFUNG HIER IST DIE ZWEITE: dass es NICHTS tut,
|
||||
wenn der Schreibende nicht darf. Sonst hätte die Einschränkung,
|
||||
die Filipe ausdrücklich genannt hat, nur im Kommentar gestanden.
|
||||
|
||||
Die Grenzregeln (2) und (3) gelten für das Schlüsselwort genauso
|
||||
wie für einen Namen — sonst ruft eine Mailadresse das ganze Team. */
|
||||
const rudelFaelle = [
|
||||
["@rudel alle mal herhören", true, true, "der einfache Fall"],
|
||||
["@RUDEL", true, true, "gross geschrieben trifft auch"],
|
||||
["@rudel alle mal herhören", false, false,
|
||||
"wer nicht darf, ruft nichts — und merkt nichts davon"],
|
||||
["schreib an [email protected]", true, false,
|
||||
"eine Mailadresse ruft das Rudel nicht"],
|
||||
["@rudeltier", true, false, "ein längeres Wort ist kein Ruf"],
|
||||
["das rudel ohne at", true, false, "ohne @ ist es nur ein Wort"],
|
||||
["(@rudel)", true, true, "in Klammern"],
|
||||
["@Tili und @rudel", true, true, "neben einem Namen"],
|
||||
];
|
||||
let rudelStimmt = 0;
|
||||
for (const [text, darf, soll, was] of rudelFaelle) {
|
||||
const ist = rudelGerufen(text, leute, { rudel: darf });
|
||||
if (ist === soll) rudelStimmt++;
|
||||
ok(ist === soll, `@rudel: ${was}${ist === soll ? "" : ` (${JSON.stringify(text)}, darf=${darf} -> ${ist})`}`);
|
||||
}
|
||||
ok(rudelStimmt === rudelFaelle.length && rudelFaelle.length >= 8,
|
||||
`alle ${rudelFaelle.length} Rudel-Fälle stimmen`);
|
||||
|
||||
/* OHNE ANGABE DARF MAN NICHT. Eine vergessene Angabe soll zu wenig
|
||||
können, nicht zu viel — sonst ruft der erste Aufruf, der die
|
||||
Option nicht kennt, aus Versehen das ganze Team. */
|
||||
ok(rudelGerufen("@rudel", leute) === false,
|
||||
"ohne Angabe gilt: darf nicht (eine vergessene Angabe kann zu wenig, nie zu viel)");
|
||||
|
||||
/* NEBEN EINEM NAMEN BLEIBT DER NAME. "@Tili und @rudel" ist beides. */
|
||||
ok(JSON.stringify(erwaehnteIds("@Tili und @rudel", leute, { rudel: true })) === "[3]",
|
||||
"neben dem Ruf bleibt der einzeln Genannte einzeln genannt");
|
||||
|
||||
/* DAS SCHLÜSSELWORT GEWINNT GEGEN EINEN MENSCHEN NAMENS "RUDEL".
|
||||
Heute heisst niemand so — aber das ist eine Tatsache von heute.
|
||||
So herum verliert niemand eine Meldung: Wer "Rudel" heisst, ist
|
||||
im Rudel ohnehin dabei. Andersherum wäre der Ruf ans Team durch
|
||||
einen einzigen Zugang stillschweigend abgeschaltet. */
|
||||
const mitRudel = [{ id: 20, name: "Rudel" }, { id: 21, name: "Anna" }];
|
||||
ok(rudelGerufen("@Rudel hallo", mitRudel, { rudel: true }) === true,
|
||||
"heisst jemand „Rudel“, gewinnt trotzdem das Schlüsselwort");
|
||||
ok(JSON.stringify(erwaehnteIds("@Rudel hallo", mitRudel, { rudel: false })) === "[20]",
|
||||
" und wer nicht rufen darf, spricht damit genau diesen Menschen an");
|
||||
|
||||
/* DAS WORT STEHT AN EINER STELLE. Stünde es zweimal, hiesse es nach
|
||||
einer Umbenennung an einer davon anders. */
|
||||
ok(RUDEL_WORT === "rudel", `das Wort kommt aus der Datei („${RUDEL_WORT}“)`);
|
||||
|
||||
/* GEGENPROBE: Die Auflösung liefert beim Rudel KEINE Personennummer
|
||||
— es ist keine Person. Wer daraus eine ID läse, bekäme `null` in
|
||||
die Empfängerliste und benachrichtigte niemanden. */
|
||||
const rt = erwaehnungenFinden("@rudel", leute, { rudel: true });
|
||||
ok(rt.length === 1 && rt[0].rudel === true && rt[0].id === null,
|
||||
"der Ruf ist keine Person (id null, rudel true)");
|
||||
ok(erwaehnteIds("@rudel", leute, { rudel: true }).length === 0,
|
||||
" und taucht deshalb in der Namensliste gar nicht auf");
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
@@ -391,6 +471,22 @@ melde("\n=== 4. Die Meldung sagt, dass DU gemeint bist ===");
|
||||
ok(art?.vorgabe === true, " und ist von sich aus an");
|
||||
ok(ARTEN.find((a) => a.schluessel === "chat_nachricht")?.schluessel !== art?.schluessel,
|
||||
" getrennt von der gewöhnlichen Chatnachricht — zwei Schalter, zwei Entscheidungen");
|
||||
|
||||
/* ---- UND BEIM RUDEL SAGT SIE, DASS ALLE GEMEINT SIND ----------
|
||||
|
||||
Filipe: „dann sollen alle eine benarichtigung bekommen."
|
||||
|
||||
WARUM NICHT DERSELBE SATZ WIE BEI DER ERWÄHNUNG: Weil er nicht
|
||||
stimmt. Beim Rudel ist man mitgemeint, nicht gemeint — und wer
|
||||
dreimal „hat dich erwähnt" liest und jedes Mal merkt, dass es
|
||||
alle betraf, glaubt beim vierten Mal auch dem echten nicht mehr. */
|
||||
const gerufen = pushTextFuer("rudel", n, raumId);
|
||||
ok(/Rudel gerufen/.test(gerufen.titel), `-> ${gerufen.titel}`);
|
||||
ok(gerufen.titel !== erw.titel && gerufen.titel !== normal.titel,
|
||||
" und unterscheidet sich von BEIDEN anderen Meldungen");
|
||||
ok(gerufen.text.includes("schaust du mal"),
|
||||
" der Anfang der Nachricht steht auch hier darunter");
|
||||
ok(gerufen.ziel === normal.ziel, " und führt in dasselbe Gespräch");
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
@@ -451,6 +547,43 @@ melde("\n=== 5. Server und Browser rechnen gleich ===");
|
||||
}
|
||||
ok(gleich === proben.length && proben.length >= 10,
|
||||
`beide kommen bei allen ${proben.length} Proben auf dasselbe Ergebnis`);
|
||||
|
||||
/* ---- UND DASSELBE MIT "@RUDEL" (23.09.2026) ----------------
|
||||
|
||||
DER DRITTE PARAMETER MUSS MITGEPRUEFT WERDEN, sonst waere er die
|
||||
Stelle, an der die beiden Fassungen auseinanderlaufen -- und
|
||||
zwar unbemerkt, weil die elf Proben darueber ihn gar nicht
|
||||
benutzen. Beide Wahrheitswerte, damit auch das Nichtstun
|
||||
verglichen wird. */
|
||||
const rudelProben = [
|
||||
"@rudel alle mal herhoeren", "@RUDEL", "[email protected]",
|
||||
"@rudeltier", "(@rudel)", "@Tili und @rudel", "kein at rudel",
|
||||
];
|
||||
let rGleich = 0, rVerglichen = 0;
|
||||
for (const t of rudelProben) {
|
||||
for (const darf of [true, false]) {
|
||||
rVerglichen++;
|
||||
const a = JSON.stringify(erwaehnungenFinden(t, leute, { rudel: darf }));
|
||||
const b = JSON.stringify(browserFassung(t, leute, { rudel: darf }));
|
||||
if (a === b) rGleich++;
|
||||
else {
|
||||
console.log(` ungleich bei ${JSON.stringify(t)} (darf=${darf})`);
|
||||
console.log(` Server: ${a}`);
|
||||
console.log(` Browser: ${b}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
ok(rGleich === rVerglichen && rVerglichen >= 14,
|
||||
`auch beim Rudel dasselbe Ergebnis (${rVerglichen} Proben, beide Rechte)`);
|
||||
|
||||
/* GEGENPROBE DES VERGLEICHS: Er muss einen Unterschied auch SEHEN.
|
||||
Ohne das waere er eine Schleife, die immer „gleich“ sagt -- und
|
||||
genau so eine stand am 01.09. gruen da, ohne je etwas anzusehen. */
|
||||
const ungleichErkannt =
|
||||
JSON.stringify(erwaehnungenFinden("@rudel", leute, { rudel: true }))
|
||||
!== JSON.stringify(browserFassung("@rudel", leute, { rudel: false }));
|
||||
ok(ungleichErkannt,
|
||||
"Gegenprobe: bei verschiedenen Rechten kommt Verschiedenes heraus — der Vergleich kann Unterschiede sehen");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,6 +600,39 @@ melde("\n=== 5. Server und Browser rechnen gleich ===");
|
||||
liegt, wenn Enter statt zu wählen die halbe Nachricht abschickt,
|
||||
oder wenn der Name am Ende grau bleibt wie jedes andere Wort.
|
||||
===================================================================== */
|
||||
/* EIN RAUM, IN DEM DER RUF ETWAS BEWIRKT (23.09.2026).
|
||||
|
||||
Fuer den Bildschirmteil wird ein Gespraech gebraucht, in dem ausser
|
||||
mir noch jemand aus dem Team sitzt -- sonst bietet die Liste den Ruf
|
||||
gar nicht erst an (und das ist richtig so: ein Vorschlag, der
|
||||
niemanden erreicht, ist ein Vorschlag, der nichts tut).
|
||||
|
||||
ZWEI ZUGAENGE MIT DER DOGFATHER-ROLLE, und zwar bewusst statt eines
|
||||
Modis: Der Browser dieser Pruefung laeuft auf 127.0.0.1, und dort
|
||||
steht die Agentur-Wand -- die Rolle "modi" gibt es auf ihr nicht
|
||||
(siehe der Kommentar ganz oben). Wer hier das Team braucht, nimmt
|
||||
die einzige Team-Rolle, die auf beiden Waenden vorkommt. */
|
||||
let raumTeam = null;
|
||||
{
|
||||
const d3 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const sch = d3.prepare("SELECT wert FROM einstellungen WHERE schluessel = ?")
|
||||
.get("code_kennung_schluessel").wert;
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync("CODE-DOGI-0002", salt, 64,
|
||||
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
d3.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
||||
+ " VALUES (?,?,?,?,?,?,1,?)")
|
||||
.run("Zweiter", "admin", hash, salt, 32768,
|
||||
createHmac("sha256", sch).update("CODE-DOGI-0002").digest("hex"), jetzt);
|
||||
const idZwei = d3.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
d3.close();
|
||||
const g = await ruf("/workspace/api/chat/gruppe", aDogi.keks, "POST",
|
||||
{ name: "Team intern", mit: [idZwei] });
|
||||
raumTeam = json(g).raum_id ?? null;
|
||||
ok(Number.isInteger(raumTeam),
|
||||
`ein Raum, in dem noch jemand aus dem Team sitzt (Raum ${raumTeam})`);
|
||||
}
|
||||
|
||||
melde("\n=== 6. Am echten Bildschirm ===");
|
||||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||||
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
@@ -631,12 +797,270 @@ try {
|
||||
const jetztRaus = await s.locator(".chat-nachricht").count();
|
||||
ok(jetztRaus > vorher + 1, ` und Enter schickt wieder ab (${jetztRaus}, vorher ${vorher})`);
|
||||
|
||||
/* ---- "@RUDEL" AM BILDSCHIRM (23.09.2026) -----------------------
|
||||
|
||||
Alles bisher hat bewiesen, dass der Server den Richtigen ruft.
|
||||
Hier geht es um die andere Haelfte: Findet man es, und sieht man
|
||||
es? Ein Ruf, den niemand vorgeschlagen bekommt und der im Satz
|
||||
grau bleibt wie jedes andere Wort, ist eine Attrappe. */
|
||||
if (Number.isInteger(raumTeam)) {
|
||||
await s.goto(`${BASIS}/workspace/chat.html?raum=${raumTeam}`, { waitUntil: "networkidle" });
|
||||
await s.waitForTimeout(1200);
|
||||
await s.click("#text");
|
||||
await s.type("#text", "@", { delay: 40 });
|
||||
await s.waitForTimeout(400);
|
||||
const eintraege = await liste.locator(".erwaehn-liste__eintrag").count();
|
||||
const ersterName = await liste.locator(".erwaehn-liste__name").first().innerText();
|
||||
const ersterZusatz = await liste.locator(".erwaehn-liste__rolle").first().innerText();
|
||||
ok(eintraege >= 2 && ersterName === "rudel",
|
||||
`der Ruf steht ganz oben in der Liste (${eintraege} Eintraege, erster: ${ersterName})`);
|
||||
/* WAS ES BEDEUTET, MUSS DANEBENSTEHEN. "rudel" allein sagt keinem
|
||||
Neuen, dass damit das ganze Team gemeint ist -- und wer das erst
|
||||
nach dem Absenden erfaehrt, hat schon alle geweckt. */
|
||||
ok(/alle im Team/i.test(ersterZusatz),
|
||||
` und daneben steht, was es bedeutet ("${ersterZusatz}")`);
|
||||
const alsPerson = await liste.locator('.erwaehn-liste__eintrag[data-rudel="ja"]').count();
|
||||
ok(alsPerson === 1, ` genau einmal, als eigene Sorte Eintrag (${alsPerson})`);
|
||||
|
||||
await s.keyboard.press("Enter");
|
||||
await s.waitForTimeout(300);
|
||||
ok((await s.inputValue("#text")) === "@rudel ",
|
||||
`Enter setzt den Ruf ein: ${JSON.stringify(await s.inputValue("#text"))}`);
|
||||
await s.type("#text", "bitte alle kurz schauen", { delay: 10 });
|
||||
await s.keyboard.press("Enter");
|
||||
await s.waitForTimeout(1500);
|
||||
|
||||
const marke = await s.evaluate(() => {
|
||||
const m = document.querySelector('.chat-erwaehnung[data-rudel="ja"]');
|
||||
if (!m) return null;
|
||||
const st = getComputedStyle(m);
|
||||
const normal = document.querySelector(".chat-nachricht__text");
|
||||
return {
|
||||
wort: m.textContent.trim(),
|
||||
gewicht: st.fontWeight,
|
||||
schatten: st.boxShadow !== "none",
|
||||
eigen: m.dataset.ich === "ja",
|
||||
farbeGleichText: st.color === getComputedStyle(normal).color,
|
||||
};
|
||||
});
|
||||
ok(!!marke, "der Ruf ist im Satz hervorgehoben");
|
||||
/* Das Zeichen steht per Stilvorlage davor (::before) und zaehlt
|
||||
nicht zum Textinhalt -- geprueft wird deshalb das Wort. */
|
||||
ok(marke?.wort === "@rudel", ` und zwar genau: ${marke?.wort}`);
|
||||
/* NICHT NUR DIE FARBE. Wer Farben schlecht unterscheidet, muss es
|
||||
trotzdem sehen -- dieselbe Regel wie beim eigenen Namen. */
|
||||
ok(Number(marke?.gewicht) >= 600 && marke?.schatten === true,
|
||||
` erkennbar an Gewicht (${marke?.gewicht}) und Rahmen (${marke?.schatten}), nicht nur an der Farbe`);
|
||||
/* GEGENPROBE: Es ist NICHT die Marke fuer "du bist gemeint". Saehen
|
||||
beide gleich aus, verloere die persoenliche ihre Wirkung. */
|
||||
ok(marke?.eigen === false,
|
||||
" und es ist nicht die Marke fuer den eigenen Namen — alle gemeint ist etwas anderes als du gemeint");
|
||||
} else {
|
||||
ok(false, "ohne Team-Raum ist der Ruf am Bildschirm nicht messbar");
|
||||
}
|
||||
|
||||
await dogi.ctx.close();
|
||||
await van.ctx.close();
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
7. "@RUDEL" DURCH DIE SCHNITTSTELLE (23.09.2026)
|
||||
|
||||
Filipe: „dan will ich auch dass nur die modis, rechte hand, linke
|
||||
hand und dogfather, alle auch auf einmal markieren können im chat
|
||||
mit einem, @rudel ,dann sollen alle eine benarichtigung bekommen."
|
||||
|
||||
ALLES BISHER WAR RECHNEN OHNE SERVER. Hier geht es durch die
|
||||
richtige Tür.
|
||||
|
||||
GEPRÜFT WIRD IM TREFF, UND DAS IST KEINE BEQUEMLICHKEIT. Der Treff
|
||||
ist der einzige Raum des Hauses, in dem Team Dogi und Menschen
|
||||
sitzen, die NICHT dazugehören (Rolle „gast" -- die Community). In
|
||||
einem gewöhnlichen Gespräch geht das gar nicht: Die Häusertrennung
|
||||
lässt einen Scout und einen Modi nicht in denselben Raum, das ist
|
||||
beim Bauen dieser Prüfung gemessen worden (403, „Mit mindestens
|
||||
einer Person kannst du nicht schreiben").
|
||||
|
||||
Damit ist der Treff die einzige Stelle, an der der Unterschied
|
||||
zwischen „alle im Raum" und „alle im Team" überhaupt sichtbar wird
|
||||
-- und zugleich die, an der die falsche Lesart wehtäte: Sie hätte
|
||||
jeden Zuschauer geweckt.
|
||||
|
||||
ZWEI FRAGEN, UND DIE ZWEITE IST DIE WICHTIGERE:
|
||||
1. Ruft der Modi wirklich sein Team?
|
||||
2. Ruft ein Gast wirklich NICHTS? -- Ohne diese Prüfung stünde
|
||||
Filipes „nur die modis, rechte hand, linke hand und dogfather"
|
||||
bloss im Kommentar.
|
||||
===================================================================== */
|
||||
melde("\n" + "=== 7. Der Ruf ans Rudel geht durch die Tür ===");
|
||||
{
|
||||
/* Die Anmeldewand entscheidet am HOST, in welchem Haus sie steht.
|
||||
Team Dogi gibt es nur auf crew.… -- ohne diesen Kopf käme jede
|
||||
Anfrage auf der Agenturseite an, und die Rolle "modi" gibt es
|
||||
dort gar nicht. `fetch` verwirft den Host-Kopf, deshalb node:http. */
|
||||
const CREW = "crew.dogfather-universe.com";
|
||||
function crewRuf(pfad, keks, methode = "GET", koerper = null) {
|
||||
return new Promise((fertig, schief) => {
|
||||
const daten = koerper === null ? null : Buffer.from(JSON.stringify(koerper));
|
||||
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 j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ }
|
||||
fertig({
|
||||
code: antwort.statusCode, json: j, text,
|
||||
keks: [].concat(antwort.headers["set-cookie"] || [])
|
||||
.map((c) => c.split(";")[0]).join("; "),
|
||||
});
|
||||
});
|
||||
});
|
||||
a.on("error", schief);
|
||||
if (daten) a.write(daten);
|
||||
a.end();
|
||||
});
|
||||
}
|
||||
|
||||
/* Drei weitere Zugänge: ein Modi, die linke Hand und ein Gast. */
|
||||
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const schluessel2 = d2.prepare(
|
||||
"SELECT wert FROM einstellungen WHERE schluessel = ?").get("code_kennung_schluessel").wert;
|
||||
const anlegen2 = (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");
|
||||
d2.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", schluessel2).update(code).digest("hex"), jetzt);
|
||||
return d2.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
};
|
||||
const idModi = anlegen2("Frida", "modi", "CODE-MODI-0001");
|
||||
const idLinke = anlegen2("Lino", "linke", "CODE-LINK-0001");
|
||||
const idGast = anlegen2("Zuschauer Zoe", "gast", "CODE-GAST-0001");
|
||||
d2.close();
|
||||
|
||||
const aModi = await crewRuf("/workspace/api/anmelden", null, "POST",
|
||||
{ rolle: "modi", code: "CODE-MODI-0001" });
|
||||
const aLinke = await crewRuf("/workspace/api/anmelden", null, "POST",
|
||||
{ rolle: "linke", code: "CODE-LINK-0001" });
|
||||
/* EIN GAST BESTAETIGT SEIN ALTER. Ohne `alter_ok` kommt 400 --
|
||||
gemessen, nicht vermutet; der erste Anlauf dieser Pruefung ist
|
||||
genau daran haengengeblieben. Das Feld gilt NUR fuer die
|
||||
Community-Kachel, fuer alle anderen Rollen verlangt der Server es
|
||||
gar nicht erst. Dieselbe Stelle steht in pruef-treff. */
|
||||
const aGast = await crewRuf("/workspace/api/anmelden", null, "POST",
|
||||
{ rolle: "gast", code: "CODE-GAST-0001", alter_ok: true });
|
||||
ok(!!aModi.keks && !!aLinke.keks && !!aGast.keks,
|
||||
`Modi, linke Hand und ein Gast sind angemeldet (${aModi.code}/${aLinke.code}/${aGast.code})`);
|
||||
|
||||
/* DER TREFF FUELLT SICH, WENN MAN IHN AUFMACHT. treffAngleichen()
|
||||
laeuft beim Abrufen der Gespraechsliste -- also einmal je Zugang. */
|
||||
for (const k of [aModi.keks, aLinke.keks, aGast.keks]) {
|
||||
await crewRuf("/workspace/api/chat/raeume", k);
|
||||
}
|
||||
|
||||
const drin = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const treff = drin.prepare(
|
||||
"SELECT id FROM chat_raeume WHERE art = 'kanal' AND kategorie = ?").get("treff")?.id;
|
||||
/* DRITTER AUSGANG: Gibt es den Treff nicht, steht hier warum --
|
||||
statt dass die folgenden Pruefungen an `undefined` arbeiten und
|
||||
Fehler melden, die mit dem Rudel nichts zu tun haben. */
|
||||
ok(Number.isInteger(treff), `der Treff steht (Raum ${treff})`);
|
||||
|
||||
const besetzung = Number.isInteger(treff) ? drin.prepare(
|
||||
`SELECT t.person_id AS id, p.rolle FROM chat_teilnehmer t
|
||||
JOIN personen p ON p.id = t.person_id
|
||||
WHERE t.raum_id = ? AND t.raus_am IS NULL`).all(treff) : [];
|
||||
const gaeste = besetzung.filter((x) => x.rolle === "gast").map((x) => x.id);
|
||||
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Saesse kein Gast im Treff,
|
||||
waere die Gegenprobe weiter unten leer und trotzdem gruen --
|
||||
genau der Fehler, der am 01.09. dreimal gruen dastand. */
|
||||
ok(gaeste.includes(idGast) && besetzung.length >= 3,
|
||||
`im Treff sitzen ${besetzung.length} Leute, darunter ein Gast`);
|
||||
|
||||
/* WER ERWARTET WIRD, WIRD ABGELEITET -- nicht abgeschrieben. Eine
|
||||
feste Liste hier waere beim naechsten Rollenzuschnitt still
|
||||
falsch, und die Pruefung haette den Fehler mitgemacht. */
|
||||
const TEAM = new Set(["admin", "hand", "linke", "modi"]);
|
||||
const erwartet = besetzung
|
||||
.filter((x) => TEAM.has(x.rolle) && x.id !== idModi)
|
||||
.map((x) => x.id).sort((x, y) => x - y);
|
||||
|
||||
/* ---- 1. DER MODI RUFT ------------------------------------------ */
|
||||
const ruf1 = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`,
|
||||
aModi.keks, "POST", { text: "@rudel bitte alle kurz herschauen" });
|
||||
ok(ruf1.code === 201, `der Modi darf im Treff schreiben (${ruf1.code} ${ruf1.code === 201 ? "" : ruf1.text.slice(0, 90)})`);
|
||||
ok(ruf1.json?.nachricht?.rudel === true,
|
||||
" und die Nachricht weiss, dass sie das Rudel gerufen hat");
|
||||
|
||||
const nrId = ruf1.json?.nachricht?.id;
|
||||
const gemeint = drin.prepare(
|
||||
"SELECT person_id FROM chat_erwaehnungen WHERE nachricht_id = ? ORDER BY person_id")
|
||||
.all(nrId).map((x) => x.person_id).sort((x, y) => x - y);
|
||||
ok(gemeint.length > 0 && JSON.stringify(gemeint) === JSON.stringify(erwartet),
|
||||
`gerufen wird genau das Team im Raum (${JSON.stringify(gemeint)}, erwartet ${JSON.stringify(erwartet)})`);
|
||||
/* DIE EIGENTLICHE AUSSAGE. "Rudel" heisst das Team -- nicht jeder im
|
||||
Raum. Im Treff sitzt die Community; ein Ruf, der dort jeden
|
||||
Zuschauer weckt, waere etwas anderes als der bestellte. */
|
||||
ok(!gemeint.includes(idGast),
|
||||
" der Gast im selben Raum aber NICHT — das Rudel ist das Team, nicht der Raum");
|
||||
ok(!gemeint.includes(idModi),
|
||||
" und der Rufer selbst auch nicht — man ruft nicht sich");
|
||||
ok(gemeint.includes(idLinke),
|
||||
" die linke Hand ist dabei — sie gehoert zum Team");
|
||||
|
||||
/* Die Spalte bleibt stehen -- sie ist es, aus der beim Lesen die
|
||||
Hervorhebung entsteht. */
|
||||
const gespeichert = drin.prepare(
|
||||
"SELECT rudel FROM chat_nachrichten WHERE id = ?").get(nrId)?.rudel;
|
||||
ok(gespeichert === 1, `und es steht an der Nachricht (rudel = ${gespeichert})`);
|
||||
|
||||
const verlauf = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`, aModi.keks);
|
||||
const geladen = (verlauf.json?.nachrichten || []).find((x) => x.id === nrId);
|
||||
ok(geladen?.rudel === true, "der Verlauf bringt es mit — auch nach dem Neuladen");
|
||||
ok(verlauf.json?.darf_rudel === true, "und der Modi bekommt gesagt, dass er rufen darf");
|
||||
ok(verlauf.json?.rudel_wort === RUDEL_WORT,
|
||||
`das Wort kommt vom Server, nicht aus dem Browser (${verlauf.json?.rudel_wort})`);
|
||||
|
||||
/* ---- 2. GEGENPROBE: DER GAST RUFT INS LEERE --------------------- */
|
||||
const ruf2 = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`,
|
||||
aGast.keks, "POST", { text: "@rudel und ihr alle mal" });
|
||||
ok(ruf2.code === 201, `der Gast darf im Treff schreiben wie alle (${ruf2.code})`);
|
||||
const nr2 = ruf2.json?.nachricht?.id;
|
||||
ok(ruf2.json?.nachricht?.rudel !== true, " aber sein @rudel ruft niemanden");
|
||||
const gemeint2 = drin.prepare(
|
||||
"SELECT person_id FROM chat_erwaehnungen WHERE nachricht_id = ?").all(nr2);
|
||||
ok(gemeint2.length === 0,
|
||||
` kein einziger Eintrag (${gemeint2.length}) — nur Team Dogi und DogFather duerfen rufen`);
|
||||
const gespeichert2 = drin.prepare(
|
||||
"SELECT rudel FROM chat_nachrichten WHERE id = ?").get(nr2)?.rudel;
|
||||
ok(!gespeichert2, ` und an der Nachricht steht nichts (rudel = ${gespeichert2})`);
|
||||
|
||||
const verlauf2 = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`, aGast.keks);
|
||||
ok(verlauf2.json?.darf_rudel === false,
|
||||
"dem Gast sagt der Server, dass er nicht rufen darf — der Vorschlag erscheint gar nicht erst");
|
||||
|
||||
/* ---- 3. EIN NAME WIRKT WEITERHIN FUER ALLE ---------------------- */
|
||||
const ruf3 = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`,
|
||||
aGast.keks, "POST", { text: "@Frida schaust du mal?" });
|
||||
const gemeint3 = drin.prepare(
|
||||
"SELECT person_id FROM chat_erwaehnungen WHERE nachricht_id = ?")
|
||||
.all(ruf3.json?.nachricht?.id).map((x) => x.person_id);
|
||||
ok(gemeint3.length === 1 && gemeint3[0] === idModi,
|
||||
"wer nicht rufen darf, kann trotzdem einzeln ansprechen — das bleibt unberuehrt");
|
||||
|
||||
drin.close();
|
||||
}
|
||||
|
||||
console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`);
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
process.exit(fehler ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user