pruef-auskunft und pruef-treff-werkzeuge -- beide bestraften eine Verbesserung. Das ist die unangenehmste Sorte roter Pruefung: Wer sie zweimal so erlebt, faengt an, rote Laeufe zu erklaeren statt sie zu lesen. 1. "DIE COMMUNITY HAT KEINEN STECKBRIEF" So stand es woertlich in der Bedingung. Am 19.09. hat Filipe genau das Gegenteil entschieden -- "jeder soll ein steckbrief haben, jeder der ein account hat ... jeder soll genau wie ich foto und so hinzufuegen koennen" -- und die Community hat seitdem einen. Was die Zeile schuetzen wollte, steht in ihrem eigenen Kommentar: "Ohne die zweite Stelle haette die Gruppe mit den wenigsten Rechten auch dieses nicht -- und sie ist die groesste." Die Sorge ist richtig und hat mit der Anzahl der Seiten nichts zu tun. Die Frage lautet: KOMMT JEDE ROLLE AN IHRE AUSKUNFT? Genau das wird jetzt gemessen -- welche Seiten den Knopf tragen, aus den Dateien gelesen statt aufgezaehlt, und ob jede der acht Rollen mindestens eine davon erreichen darf, aus der Rechtetafel gelesen statt angenommen. Mit Gegenprobe (eine erfundene Rolle erreicht keine) und mit der Zahl in der Bedingung. 2. "DIE COMMUNITY STEHT BEI 8 VON 33" Hier stand `community.seiten < 5`. Die Zahl stimmte an dem Tag, an dem sie geschrieben wurde. Seither hat der Treff eigene Seiten bekommen -- Regeln, Steckbrief, Hilfe, Draussen -- und die Community steht bei acht. Die Pruefung war rot, WEIL der Treff gewachsen ist. Dieselbe Fehlerklasse wie die Umbruchschwelle vom 06.09.: Wer eine Zahl notiert, notiert einen Zustand, keine Regel. Gemeint war eine ORDNUNG: Ein Mitglied sieht weniger als die Moderation, die weniger als die rechte Hand, die weniger als DogFather. Nachgemessen 8 / 19 / 25 / 32 von 33. Geprueft wird jetzt das Verhaeltnis -- und zusaetzlich, dass die Community das Minimum ueber ALLE acht Rollen ist, nicht nur in dieser Kette. Das bleibt richtig, wenn der Treff weiterwaechst, und faellt sofort auf, wenn jemand die Reihenfolge versehentlich umdreht. pruef-auskunft 46/0 (war 46, davon 1 rot) pruef-treff-werkzeuge 73/0 (war 70, davon 1 rot) Damit sind alle vier Pruefungen gruen, die heute frueh rot waren. Co-Authored-By: Claude Opus 5 <[email protected]>
631 lines
31 KiB
JavaScript
631 lines
31 KiB
JavaScript
/* =====================================================================
|
|
Die Werkzeuge des Treffs (11.09.2026)
|
|
|
|
pruef-treff.mjs fragt: WER SIEHT WAS. Diese Datei fragt: WAS
|
|
PASSIERT, wenn jemand etwas tut — „Will ich auch", anheften,
|
|
freigeben, melden, entfernen, sperren, übernehmen.
|
|
|
|
---------------------------------------------------------------------
|
|
ZU JEDER REGEL GEHÖRT HIER DIE GEGENPROBE, und zwar aus demselben
|
|
Grund wie überall in diesem Haus: Eine Prüfung, die immer bestätigt,
|
|
bestätigt nichts. Wenn „ein Modi darf nicht dauerhaft ausschließen"
|
|
grün ist, muss daneben stehen, dass DogFather es kann — sonst wäre
|
|
die Zeile auch dann grün, wenn NIEMAND es kann.
|
|
|
|
Konkret geprüft:
|
|
|
|
* „Will ich auch" ist EINE Stimme. Zweimal drücken ändert nichts,
|
|
und das Zurücknehmen geht.
|
|
* Am Anschlagbrett hängen höchstens drei. Die vierte wird
|
|
abgelehnt — und nach dem Abnehmen einer geht sie durch.
|
|
* Freigeben kann nur das Team. Ohne Freigabe ist der Eintrag für
|
|
die Community nicht da (steht in pruef-treff), mit Freigabe
|
|
schon.
|
|
* Melden ohne Grund geht nicht. Mit Grund landet es in der Liste
|
|
des Teams (DSA Art. 16).
|
|
* Entfernen ohne Grund geht nicht. Mit Grund ist der Eintrag weg
|
|
UND der Grund noch da (DSA Art. 17) — das ist der eigentliche
|
|
Punkt: Eine Begründung, die mit dem Beitrag verschwindet, ist
|
|
keine.
|
|
* Die Bannleiter: Ein Modi darf bis zu sieben Tage, DogFather auch
|
|
dauerhaft. Und der dauerhafte Ausschluss wirkt — die Sitzung ist
|
|
weg, nicht nur das Schreiben.
|
|
* Aus „Mitmachen" wird ein Talent, und zwar genau einmal.
|
|
|
|
Aufrufen mit: node server/pruef-treff-werkzeuge.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { request as httpAnfrage } from "node:http";
|
|
import { heuteLokal } from "./helfer-tag.mjs";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
|
|
const PORT = await eigenerPort(import.meta, "pruef-treff-werkzeuge");
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-treffw-"));
|
|
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";
|
|
|
|
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 }); };
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
await import("./index.js");
|
|
notbremse(240_000, "pruef-treff-werkzeuge");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
|
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const melde = (t) => console.log(t);
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const CREW = "crew.dogfather-universe.com";
|
|
const HAUS = "workspace.dogfather-universe.com";
|
|
const HAUPT = "dogfather-universe.com";
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const treffModul = await import("./workspace-treff.js");
|
|
const { BEREICHE } = await import("./workspace-bereiche.js");
|
|
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
const heute = heuteLokal();
|
|
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);
|
|
|
|
/* Die Mitglieder werden ALT angelegt (40 Tage), damit sie schreiben
|
|
duerfen. Die Stufen selbst misst pruef-treff; hier waere ein frisches
|
|
Konto nur eine Huerde vor jeder einzelnen Zeile. */
|
|
const ALT = new Date(Date.now() - 40 * 86400000).toISOString();
|
|
function anlegen(name, rolle, code, erstellt = jetzt) {
|
|
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"), erstellt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
anlegen("Frida", "modi", "CODE-MODI-0001");
|
|
const idKessi = anlegen("Kessi", "gast", "CODE-GAST-0001", ALT);
|
|
const idBo = anlegen("Bo", "gast", "CODE-GAST-0002", ALT);
|
|
/* Ein dritter Zugang, nur fuer den Ausschluss. Ihn Bo zu geben waere
|
|
bequem gewesen und haette den Rest der Datei zerlegt: Ein
|
|
Ausgeschlossener hat keine Sitzung mehr, also waere ab da JEDE
|
|
weitere Zeile mit Bo gruen geworden, weil alles 401 antwortet --
|
|
und "die Talente sieht er nicht" haette nichts mehr bewiesen. */
|
|
const idPim = anlegen("Pim", "gast", "CODE-GAST-0003", ALT);
|
|
|
|
const rein = d.prepare(
|
|
"INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)"
|
|
+ " VALUES (?,?,?,?,?,?,?)");
|
|
const ersteArt = (b) => Object.keys(BEREICHE[b].arten)[0];
|
|
function eintrag(bereich, titel, von = idDogi) {
|
|
rein.run(bereich, ersteArt(bereich), titel, "", heute, jetzt, von);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
const wunsch1 = eintrag("wunsch", "WUNSCH-EINS");
|
|
const anschlag = [1, 2, 3, 4].map((n) => eintrag("anschlag", `ANSAGE-${n}`));
|
|
const highlight1 = eintrag("highlight", "CLIP-EINS");
|
|
const bosBeitrag = eintrag("treff", "BO-SAGT-HALLO", idBo);
|
|
const mitmachen1 = eintrag("mitmachen", "ICH-HAETTE-LUST", idBo);
|
|
d.close();
|
|
|
|
/* EIN DELETE TRAEGT KEINEN KOERPER (gemessen 11.09.2026).
|
|
|
|
Nodes HTTP-Parser weist ein DELETE mit Koerper mit einem leeren 400
|
|
ab, bevor Express es sieht -- nachgemessen: PUT mit Koerper 200,
|
|
DELETE ohne Koerper 200, DELETE mit Koerper 400 mit genau einem
|
|
Kopffeld. Die Pruefung schickt deshalb gar keinen erst; wuerde sie
|
|
es tun, meldete sie einen Fehler in einer Route, in der keiner ist.
|
|
Genau so ist dieser Befund entstanden. */
|
|
function roh(pfad, host, kopf = {}, koerper = null, methode = null) {
|
|
if (methode === "DELETE") koerper = null;
|
|
return new Promise((fertig, schief) => {
|
|
const a = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: pfad,
|
|
method: methode || (koerper ? "POST" : "GET"),
|
|
headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf },
|
|
}, (antwort) => {
|
|
let text = "";
|
|
antwort.on("data", (s) => { text += s; });
|
|
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
|
|
});
|
|
a.on("error", schief);
|
|
if (koerper) a.write(JSON.stringify(koerper));
|
|
a.end();
|
|
});
|
|
}
|
|
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
|
|
async function anmelden(rolle, code, host) {
|
|
/* Die Community bestaetigt beim ersten Mal ihr Alter (seit 15.09.2026).
|
|
Nur fuer 'gast' -- fuer alle anderen muss der Server das Feld gar
|
|
nicht erst sehen. */
|
|
const r = await roh("/workspace/api/anmelden", host, {},
|
|
rolle === "gast" ? { rolle, code, alter_ok: true } : { rolle, code });
|
|
const kekse = [].concat(r.kopf?.["set-cookie"] || []);
|
|
return kekse.map((c) => c.split(";")[0]).join("; ");
|
|
}
|
|
|
|
const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS);
|
|
const kModi = await anmelden("modi", "CODE-MODI-0001", CREW);
|
|
const kKessi = await anmelden("gast", "CODE-GAST-0001", CREW);
|
|
const kBo = await anmelden("gast", "CODE-GAST-0002", CREW);
|
|
ok(!!kDogi && !!kModi && !!kKessi && !!kBo, "alle vier sind angemeldet");
|
|
|
|
/* =======================================================================
|
|
1. WILL ICH AUCH — eine Stimme, nicht zwei
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 1. Will ich auch ===");
|
|
{
|
|
const weg = `/workspace/api/treff/eintrag/${wunsch1}/willich`;
|
|
const a = await roh(weg, CREW, { cookie: kKessi }, {}, "PUT");
|
|
ok(a.code === 200 && json(a).stimmen === 1, `Kessi stimmt ab (${json(a).stimmen})`);
|
|
|
|
/* ZWEIMAL DRUECKEN IST EINMAL. Das ist die eigentliche Regel: Der
|
|
zusammengesetzte Schluessel laesst gar keine zweite Zeile zu. */
|
|
const b = await roh(weg, CREW, { cookie: kKessi }, {}, "PUT");
|
|
ok(b.code === 200 && json(b).stimmen === 1, `zweimal druecken aendert nichts (${json(b).stimmen})`);
|
|
|
|
const c = await roh(weg, CREW, { cookie: kBo }, {}, "PUT");
|
|
ok(c.code === 200 && json(c).stimmen === 2, `Bo auch (${json(c).stimmen})`);
|
|
|
|
const e = await roh(weg, CREW, { cookie: kKessi }, {}, "DELETE");
|
|
ok(e.code === 200 && json(e).stimmen === 1 && json(e).meine === false,
|
|
`Kessi nimmt sie zurueck (${json(e).stimmen})`);
|
|
|
|
/* Und die Zahl kommt am Brett an -- sonst waere sie eine Zahl, die
|
|
nur die Route kennt. */
|
|
const brett = await roh("/workspace/api/bereich/wunsch", CREW, { cookie: kBo });
|
|
const zeile = (json(brett).eintraege || []).find((x) => x.id === wunsch1);
|
|
ok(zeile?.stimmen === 1 && zeile?.meine_stimme === 1,
|
|
`am Brett steht sie auch (${zeile?.stimmen}, meine ${zeile?.meine_stimme})`);
|
|
|
|
/* GEGENPROBE: Auf einem anderen Brett gibt es keine Stimme. Ohne
|
|
diese Zeile waere "es geht" auch dann gruen, wenn es ueberall
|
|
ginge -- und dann waere die Wunschliste keine Abstimmung mehr,
|
|
sondern ein Nebeneffekt. */
|
|
const f = await roh(`/workspace/api/treff/eintrag/${bosBeitrag}/willich`, CREW,
|
|
{ cookie: kKessi }, {}, "PUT");
|
|
ok(f.code === 400, `im Gespraech gibt es kein "Will ich auch" (${f.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. ANHEFTEN — höchstens drei
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 2. Anheften ===");
|
|
{
|
|
const heften = (id, keks, m = "PUT") =>
|
|
roh(`/workspace/api/treff/eintrag/${id}/anheften`, m === "PUT" ? CREW : CREW,
|
|
{ cookie: keks }, {}, m);
|
|
|
|
let durch = 0;
|
|
for (const id of anschlag.slice(0, 3)) {
|
|
const a = await heften(id, kModi);
|
|
if (a.code === 200) durch++;
|
|
}
|
|
ok(durch === 3, `drei Ansagen haengen (${durch})`);
|
|
|
|
const vierte = await heften(anschlag[3], kModi);
|
|
ok(vierte.code === 409, `die vierte wird abgelehnt (${vierte.code})`);
|
|
ok(/3/.test(json(vierte).fehler || ""),
|
|
"und die Absage nennt die Zahl, statt nur nein zu sagen");
|
|
|
|
/* NACH DEM ABNEHMEN GEHT SIE DURCH. Ohne diese Zeile waere die
|
|
Grenze auch dann gruen, wenn ueberhaupt nichts mehr angeheftet
|
|
werden koennte. */
|
|
await heften(anschlag[0], kModi, "DELETE");
|
|
const nochmal = await heften(anschlag[3], kModi);
|
|
ok(nochmal.code === 200, `nach dem Abnehmen geht die vierte (${nochmal.code})`);
|
|
|
|
/* Und die Community heftet nichts an. */
|
|
const gast = await roh(`/workspace/api/treff/eintrag/${anschlag[1]}/anheften`, CREW,
|
|
{ cookie: kKessi }, {}, "PUT");
|
|
ok(gast.code === 404, `ein Mitglied kann nichts anheften (${gast.code})`);
|
|
|
|
const brett = await roh("/workspace/api/bereich/anschlag", CREW, { cookie: kModi });
|
|
const oben = (json(brett).eintraege || [])[0];
|
|
ok(oben?.angeheftet === 1, `oben steht eine angeheftete (${oben?.titel})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. MELDEN — der Grund ist Pflicht (DSA Art. 16)
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Melden ===");
|
|
{
|
|
const weg = `/workspace/api/treff/eintrag/${bosBeitrag}/melden`;
|
|
const ohne = await roh(weg, CREW, { cookie: kKessi }, { grund: "" });
|
|
ok(ohne.code === 400, `ohne Grund geht es nicht (${ohne.code})`);
|
|
|
|
const kurz = await roh(weg, CREW, { cookie: kKessi }, { grund: "doof" });
|
|
ok(kurz.code === 400, `"doof" ist kein Grund (${kurz.code})`);
|
|
|
|
const mit = await roh(weg, CREW, { cookie: kKessi },
|
|
{ grund: "Da steht die Handynummer von jemandem drin." });
|
|
ok(mit.code === 200, `mit Grund geht es (${mit.code})`);
|
|
|
|
const liste = await roh("/workspace/api/treff/meldungen", CREW, { cookie: kModi });
|
|
const m = (json(liste).meldungen || [])[0];
|
|
ok(liste.code === 200 && m?.eintrag_id === bosBeitrag,
|
|
`der Modi sieht die Meldung (${(json(liste).meldungen || []).length})`);
|
|
ok(m?.autor_rolle_name === "Community",
|
|
`und wer sie geschrieben hat, mit vollem Rollennamen (${m?.autor_rolle_name})`);
|
|
|
|
/* Die Community sieht die Meldungen NICHT -- sonst waere ablesbar,
|
|
wer wen gemeldet hat. */
|
|
const gast = await roh("/workspace/api/treff/meldungen", CREW, { cookie: kBo });
|
|
ok(gast.code === 404, `ein Mitglied sieht die Liste nicht (${gast.code})`);
|
|
|
|
/* Entscheiden braucht ebenfalls einen Satz. */
|
|
const leer = await roh(`/workspace/api/treff/meldung/${m?.id}`, CREW,
|
|
{ cookie: kModi }, { status: "erledigt", entscheid: "" }, "PATCH");
|
|
ok(leer.code === 400, `entscheiden ohne Begruendung geht nicht (${leer.code})`);
|
|
|
|
const fertig = await roh(`/workspace/api/treff/meldung/${m?.id}`, CREW,
|
|
{ cookie: kModi }, { status: "erledigt", entscheid: "Nummer entfernt, Bo Bescheid gesagt." },
|
|
"PATCH");
|
|
ok(fertig.code === 200, `mit Begruendung schon (${fertig.code})`);
|
|
|
|
const zweimal = await roh(`/workspace/api/treff/meldung/${m?.id}`, CREW,
|
|
{ cookie: kModi }, { status: "abgelehnt", entscheid: "Doch nicht so schlimm gewesen." },
|
|
"PATCH");
|
|
ok(zweimal.code === 409, `zweimal entscheiden geht nicht (${zweimal.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. ENTFERNEN — mit Grund, und der Grund überlebt (DSA Art. 17)
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Entfernen ===");
|
|
{
|
|
const weg = `/workspace/api/bereich/treff/${bosBeitrag}`;
|
|
const ohne = await roh(weg, CREW, { cookie: kModi }, null, "DELETE");
|
|
ok(ohne.code === 400, `ohne Grund wird nichts entfernt (${ohne.code})`);
|
|
|
|
/* Und der Eintrag ist auch wirklich noch da. Ein 400 sagt nur, was
|
|
der Server GEANTWORTET hat -- nicht, was er getan hat. */
|
|
const noch = await roh("/workspace/api/bereich/treff", CREW, { cookie: kModi });
|
|
ok((json(noch).eintraege || []).some((x) => x.id === bosBeitrag),
|
|
"und der Beitrag steht noch da");
|
|
|
|
const mit = await roh(`${weg}?grund=`
|
|
+ encodeURIComponent("Enthielt die Telefonnummer einer anderen Person."),
|
|
CREW, { cookie: kModi }, null, "DELETE");
|
|
ok(mit.code === 200, `mit Grund geht es (${mit.code})`);
|
|
|
|
const danach = await roh("/workspace/api/bereich/treff", CREW, { cookie: kModi });
|
|
ok(!(json(danach).eintraege || []).some((x) => x.id === bosBeitrag),
|
|
"der Beitrag ist weg");
|
|
|
|
/* DAS IST DER EIGENTLICHE PUNKT: Die Begruendung ist NICHT weg.
|
|
Haenge sie mit ON DELETE CASCADE am Eintrag, waere sie im selben
|
|
Moment verschwunden wie das, was sie begruendet. */
|
|
const akte = await roh("/workspace/api/treff/entfernt", HAUS, { cookie: kDogi });
|
|
const e = (json(akte).entfernt || [])[0];
|
|
ok(e?.eintrag_id === bosBeitrag && /Telefonnummer/.test(e?.grund || ""),
|
|
`und der Grund steht noch da (${e?.grund?.slice(0, 30)}…)`);
|
|
ok(e?.titel === "BO-SAGT-HALLO" && e?.autor_name === "Bo",
|
|
`samt Titel und Verfasser (${e?.titel}, ${e?.autor_name})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. DIE BANNLEITER — Modis befristet, dauerhaft nur DogFather
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5. Massnahmen ===");
|
|
{
|
|
const weg = "/workspace/api/treff/massnahme";
|
|
|
|
const ohne = await roh(weg, CREW, { cookie: kModi },
|
|
{ person_id: idBo, art: "pause", tage: 3, grund: "" });
|
|
ok(ohne.code === 400, `ohne Grund geht keine Massnahme (${ohne.code})`);
|
|
|
|
const kurz = await roh(weg, CREW, { cookie: kModi },
|
|
{ person_id: idBo, art: "pause", tage: 3, grund: "Hat mehrfach gegen Regel 2 verstossen." });
|
|
ok(kurz.code === 200, `ein Modi darf drei Tage Pause geben (${kurz.code})`);
|
|
|
|
const lang = await roh(weg, CREW, { cookie: kModi },
|
|
{ person_id: idPim, art: "ausschluss", tage: 30, grund: "Immer wieder dasselbe, trotz Hinweis." });
|
|
ok(lang.code === 403, `dreissig Tage darf er nicht (${lang.code})`);
|
|
ok(/7/.test(json(lang).fehler || ""), "und die Absage nennt die Grenze");
|
|
|
|
const dauer = await roh(weg, CREW, { cookie: kModi },
|
|
{ person_id: idPim, art: "ausschluss", grund: "Endgueltig, nach mehreren Hinweisen." });
|
|
ok(dauer.code === 403, `und dauerhaft schon gar nicht (${dauer.code})`);
|
|
|
|
/* GEGENPROBE. Ohne sie waeren die beiden Zeilen darueber auch dann
|
|
gruen, wenn NIEMAND ausschliessen koennte. Sieben Tage, damit Pim
|
|
fuer die naechste Zeile noch hereinkommt. */
|
|
const dogi7 = await roh(weg, CREW, { cookie: kModi },
|
|
{ person_id: idPim, art: "ausschluss", tage: 7, grund: "Sieben Tage, das darf er." });
|
|
ok(dogi7.code === 200, `sieben Tage darf ein Modi (${dogi7.code})`);
|
|
|
|
/* Und gegen jemanden aus dem TEAM geht gar nichts -- das gehoert
|
|
nach "Personen & Zugaenge", nicht hierher. */
|
|
const gegenTeam = await roh(weg, HAUS, { cookie: kDogi },
|
|
{ person_id: idDogi, art: "pause", tage: 1, grund: "Nur ein Versuch, das darf nicht gehen." });
|
|
ok(gegenTeam.code === 404, `gegen das Team gibt es keine Massnahme (${gegenTeam.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. DER AUSSCHLUSS WIRKT — nicht nur beim Schreiben
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 6. Ausschluss ===");
|
|
{
|
|
/* Kessi kommt herein -- vor dem Ausschluss. Ohne diese Zeile waere
|
|
"sie kommt nicht mehr herein" auch dann gruen, wenn sie es nie
|
|
konnte. */
|
|
/* "dabei" und nicht "stamm": Kessi ist 40 Tage dabei, hat aber
|
|
keinen eigenen Beitrag geschrieben. Beide Bedingungen muessen
|
|
erfuellt sein -- genau das misst diese Zeile nebenbei mit. */
|
|
const vorher = await roh("/workspace/api/treff/lage", CREW, { cookie: kKessi });
|
|
ok(vorher.code === 200 && json(vorher).stufe === "dabei",
|
|
`Kessi ist drin, Stufe ${json(vorher).stufe}`);
|
|
|
|
const aus = await roh("/workspace/api/treff/massnahme", HAUS, { cookie: kDogi },
|
|
{ person_id: idKessi, art: "ausschluss", grund: "Dauerhaft, nach mehreren Vorfaellen." });
|
|
ok(aus.code === 200 && json(aus).bis === null,
|
|
`DogFather schliesst dauerhaft aus (bis: ${json(aus).bis})`);
|
|
|
|
/* DIE SITZUNG IST WEG, nicht nur das Schreiben. Ein
|
|
Ausgeschlossener, der weiter mitlesen kann, ist bei Belaestigung
|
|
genau der Teil, der weh tut. */
|
|
const lage = await roh("/workspace/api/treff/lage", CREW, { cookie: kKessi });
|
|
ok(lage.code === 401, `ihre Sitzung gilt nicht mehr (${lage.code})`);
|
|
|
|
const brett = await roh("/workspace/api/bereich/treff", CREW, { cookie: kKessi });
|
|
ok(brett.code === 401, `und sie liest auch nichts mehr (${brett.code})`);
|
|
|
|
/* Und sie kommt auch nicht wieder herein -- gar nicht erst. Ohne
|
|
diese Zeile waere die Anmeldung gelungen, das Plaetzchen gesetzt,
|
|
und dahinter eine Seite, auf der nichts geht. Das sieht aus wie
|
|
ein kaputtes Programm, nicht wie eine Entscheidung. */
|
|
const neu = await anmelden("gast", "CODE-GAST-0001", CREW);
|
|
ok(!neu, "und sie kann sich nicht neu anmelden");
|
|
|
|
/* GEGENPROBE: Bo hat nur eine PAUSE -- kein Ausschluss. Er darf
|
|
weiter lesen, aber nicht schreiben. Ohne diese Zeile waeren die
|
|
vier darueber auch dann gruen, wenn jede Massnahme jeden
|
|
aussperrt. */
|
|
const boLage = await roh("/workspace/api/treff/lage", CREW, { cookie: kBo });
|
|
ok(boLage.code === 200, `Bo liest weiter (${boLage.code})`);
|
|
ok(json(boLage).sperre?.art === "pause" && !!json(boLage).sperre?.bis,
|
|
`und sieht seine Pause mit Enddatum (${json(boLage).sperre?.bis?.slice(0, 10)})`);
|
|
ok(json(boLage).schreiben?.treff === false, "schreiben darf er nicht");
|
|
|
|
/* Und Pim, sieben Tage ausgeschlossen, kommt gar nicht mehr herein
|
|
-- ein befristeter Ausschluss ist ein Ausschluss, keine Pause. */
|
|
const pim = await anmelden("gast", "CODE-GAST-0003", CREW);
|
|
ok(!pim, "ein befristeter Ausschluss sperrt ebenfalls aus");
|
|
|
|
const versuch = await roh("/workspace/api/bereich/treff", CREW, { cookie: kBo },
|
|
{ art: ersteArt("treff"), titel: "BO-TROTZDEM", text: "Hallo?", datum: heute });
|
|
ok(versuch.code === 403, `und es geht auch wirklich nicht (${versuch.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. AUS „MITMACHEN" WIRD EIN TALENT — genau einmal
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 7. Die Bruecke zu den Talenten ===");
|
|
{
|
|
const weg = `/workspace/api/treff/mitmachen/${mitmachen1}/uebernehmen`;
|
|
const a = await roh(weg, HAUS, { cookie: kDogi }, {});
|
|
ok(a.code === 200 && json(a).talent_id > 0, `Bo landet bei den Talenten (${a.code})`);
|
|
|
|
const b = await roh(weg, HAUS, { cookie: kDogi }, {});
|
|
ok(b.code === 409, `ein zweites Mal legt keinen zweiten an (${b.code})`);
|
|
|
|
const talente = await roh("/workspace/api/bereich/talente", HAUS, { cookie: kDogi });
|
|
const t = (json(talente).eintraege || []).filter((x) => x.creator_id === idBo);
|
|
ok(t.length === 1, `und es steht genau einer dort (${t.length})`);
|
|
ok(/Bo/.test(t[0]?.titel || ""), `mit seinem Namen (${t[0]?.titel})`);
|
|
|
|
/* Die Community kommt an die Talente nicht heran -- sonst waere die
|
|
Bruecke ein Fenster. */
|
|
const gast = await roh("/workspace/api/bereich/talente", CREW, { cookie: kBo });
|
|
ok(gast.code === 404, `ein Mitglied sieht die Talente nicht (${gast.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
7b. DIE COMMUNITY STEHT IN KEINER ARBEITSLISTE — aber in der Verwaltung
|
|
|
|
Seit der Treff in Team Dogi eingezogen ist, sind Mitglieder und Team
|
|
auf derselben Adresse. Was vorher die Adresse getrennt hat, muss
|
|
jetzt jede einzelne Liste halten.
|
|
|
|
DREI LISTEN, ZWEI ANTWORTEN. Einladen und Chatten: nein, und zwar
|
|
auch für DogFather, der sonst überall "alle" bekommt. Verwaltung: ja
|
|
— sonst legt er einen Zugang an und der verschwindet im selben
|
|
Moment aus der Liste.
|
|
|
|
MIT GEGENPROBE IN BEIDE RICHTUNGEN: Jede "steht nicht drin"-Zeile
|
|
steht neben einer, die beweist, dass die Liste überhaupt jemanden
|
|
enthält. Eine leere Liste erfüllt sonst jede Nein-Bedingung.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 7b. Keine Arbeitsliste, aber die Verwaltung ===");
|
|
{
|
|
const namen = (a) => {
|
|
try {
|
|
const j = JSON.parse(a.text);
|
|
const feld = Object.values(j).find(Array.isArray) || [];
|
|
return feld.map((x) => x?.name).filter(Boolean);
|
|
} catch { return []; }
|
|
};
|
|
|
|
for (const [was, weg] of [
|
|
["die Einladungsliste", "/workspace/api/einladbar"],
|
|
["die Chat-Partner", "/workspace/api/chat/partner"],
|
|
]) {
|
|
const a = await roh(weg, HAUS, { cookie: kDogi });
|
|
const liste = namen(a);
|
|
/* DIE ZAHL GEHÖRT IN DIE BEDINGUNG: Wäre die Liste leer, wäre
|
|
"Kessi steht nicht drin" trivial wahr. */
|
|
ok(a.code === 200 && liste.length > 0,
|
|
`${was} hat überhaupt Einträge (${liste.length})`);
|
|
ok(!liste.includes("Kessi") && !liste.includes("Bo"),
|
|
`${was} enthält niemanden aus der Community${
|
|
liste.filter((n) => n === "Kessi" || n === "Bo").join(", ")}`);
|
|
ok(liste.includes("Frida"),
|
|
`aber den Modi — ${was} filtert nicht einfach alles weg`);
|
|
}
|
|
|
|
/* UND DIE VERWALTUNG ZEIGT SIE. Auf der Team-Adresse, denn dort
|
|
wohnt der Treff jetzt. */
|
|
const verwaltung = await roh("/workspace/api/personen", CREW, { cookie: kDogi });
|
|
const inVerwaltung = namen(verwaltung);
|
|
ok(verwaltung.code === 200 && inVerwaltung.includes("Kessi"),
|
|
`in "Personen & Zugänge" auf crew. steht die Community (${inVerwaltung.length} Personen)`);
|
|
ok(inVerwaltung.includes("Frida"),
|
|
"und das Team ebenfalls — eine Liste, nicht zwei");
|
|
|
|
/* Die Rolle lässt sich dort auch VERGEBEN. Ein Knopf, den es nicht
|
|
gibt, macht die Liste nutzlos: Ohne ihn kann DogFather nirgends
|
|
einen Zugang zum Treff anlegen. */
|
|
const ich = await roh("/workspace/api/ich", CREW, { cookie: kDogi });
|
|
let darf = [];
|
|
try { darf = JSON.parse(ich.text).darf_anlegen || []; } catch { /* bleibt leer */ }
|
|
ok(darf.includes("gast"),
|
|
`und DogFather darf dort Community-Zugänge anlegen (${darf.join(", ")})`);
|
|
|
|
/* GEGENPROBE: Ein Modi darf das nicht — Zugänge vergibt DogFather.
|
|
Ohne diese Zeile wäre die darüber auch dann grün, wenn es jeder
|
|
dürfte. */
|
|
const ichModi = await roh("/workspace/api/ich", CREW, { cookie: kModi });
|
|
let darfModi = [];
|
|
try { darfModi = JSON.parse(ichModi.text).darf_anlegen || []; } catch { /* leer */ }
|
|
ok(!darfModi.includes("gast"),
|
|
`ein Modi nicht (${darfModi.length ? darfModi.join(", ") : "gar nichts"})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
8. DIE REGELN NENNEN DIE ZAHLEN, DIE WIRKLICH GELTEN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 8. Der Regeltext ===");
|
|
{
|
|
const a = await roh("/workspace/api/treff/regeln", CREW, { cookie: kBo });
|
|
const r = json(a);
|
|
ok(a.code === 200 && r.mindestalter === treffModul.MINDESTALTER,
|
|
`Mindestalter ${r.mindestalter} -- dieselbe Zahl wie im Code`);
|
|
ok(r.mindestalter === 18, "und sie ist 18 (Entscheidung Filipe, 11.09.2026)");
|
|
ok(Array.isArray(r.stufen) && r.stufen.length === treffModul.STUFEN.length,
|
|
`alle ${r.stufen?.length} Stufen kommen mit`);
|
|
ok(r.anschlag_max === treffModul.ANSCHLAG_MAX,
|
|
`und die Zahl am Anschlagbrett (${r.anschlag_max})`);
|
|
|
|
/* Die Seite selbst gehoert der Community UND denen, die moderieren
|
|
-- ein Modi, der die Regeln nicht sehen kann, moderiert nach
|
|
Gefuehl. */
|
|
const rechte = await import("./rechte.js");
|
|
const wer = rechte.SEITEN["/workspace/treff-regeln.html"] || [];
|
|
ok(wer.includes("gast") && wer.includes("modi") && wer.includes("admin"),
|
|
`die Regelseite steht fuer ${wer.join(", ")} offen`);
|
|
ok(!wer.includes("manager") && !wer.includes("creator") && !wer.includes("spicy"),
|
|
"und nicht fuer die Agentur");
|
|
|
|
/* "Wer sieht was" gehoert denen, die das Team fuehren.
|
|
|
|
STAND HIER BIS ZUM ABEND AUF `length === 1 && [0] === "admin"`.
|
|
Filipe danach: "ich will die kategorie auch auf der team dogi seite
|
|
fuer die dogfather und rechte hand rollen" -- die Zeile war rot,
|
|
obwohl genau das Richtige passierte.
|
|
|
|
Geprueft wird jetzt, was die Zeile eigentlich behaupten will: NICHT
|
|
die Agentur, NICHT die Modis, NICHT die Community. Wer dazukommt,
|
|
ist eine Entscheidung; wer NIE dazugehoert, ist der Riegel. */
|
|
const tafel = rechte.SEITEN["/workspace/rechte.html"] || [];
|
|
const drin = ["admin", "hand"].filter((r) => tafel.includes(r));
|
|
const drausen = ["spicy", "manager", "scout", "creator", "modi", "gast"]
|
|
.filter((r) => tafel.includes(r));
|
|
ok(drin.length === 2 && drausen.length === 0,
|
|
`"Wer sieht was" fuer ${tafel.join(", ")} -- und fuer sonst niemanden`);
|
|
|
|
const fremd = await roh("/workspace/api/rechte/tafel", CREW, { cookie: kModi });
|
|
ok(fremd.code === 404, `ein Modi bekommt die Tafel nicht (${fremd.code})`);
|
|
|
|
const meine = await roh("/workspace/api/rechte/tafel", HAUS, { cookie: kDogi });
|
|
const t = json(meine);
|
|
ok(meine.code === 200 && t.seiten?.length === rechte.alleSeiten().length,
|
|
`DogFather bekommt alle ${t.seiten?.length} Seiten`);
|
|
/* EINE FESTE ZAHL IST EINE RECHNUNG VON GESTERN (berichtigt
|
|
21.09.2026).
|
|
|
|
Hier stand `community.seiten < 5`. Die Zahl stimmte an dem Tag,
|
|
an dem sie geschrieben wurde. Seither hat der Treff eigene Seiten
|
|
bekommen -- Regeln, Steckbrief, Hilfe, Draussen -- und die
|
|
Community steht bei acht. Die Pruefung war damit rot, WEIL der
|
|
Treff gewachsen ist. Dieselbe Fehlerklasse wie die Umbruchschwelle
|
|
vom 06.09.: Wer eine Zahl notiert, notiert einen Zustand, keine
|
|
Regel.
|
|
|
|
DIE AUSSAGE, DIE GEMEINT WAR, IST EINE ORDNUNG, KEINE GRENZE:
|
|
Ein Mitglied sieht weniger als die Moderation, die weniger als die
|
|
rechte Hand, die weniger als DogFather. Wer Rechte vergibt, darf
|
|
diese Reihenfolge nicht versehentlich umdrehen -- und genau das
|
|
faellt an einer Zahl nicht auf, an der Ordnung sofort.
|
|
|
|
Nachgemessen am 21.09.: 8 / 19 / 25 / 32 von 33 Seiten. Die Zahlen
|
|
stehen hier nur als Beleg; gemessen wird ihr Verhaeltnis, und das
|
|
bleibt richtig, wenn der Treff weiterwaechst. */
|
|
const zahl = (schluessel) =>
|
|
(t.rollen || []).find((x) => x.schluessel === schluessel)?.seiten;
|
|
const kette = ["gast", "modi", "hand", "admin"].map((r) => [r, zahl(r)]);
|
|
|
|
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Fehlte eine Rolle in der
|
|
Auskunft, waere `undefined < undefined` schlicht false -- aber
|
|
`[...].every(...)` ueber eine leere Liste waere wahr. Deshalb
|
|
zuerst: Sind ueberhaupt alle vier da? */
|
|
ok(kette.every(([, n]) => Number.isFinite(n)),
|
|
`die Auskunft nennt alle vier: ${kette.map(([r, n]) => `${r} ${n}`).join(", ")}`);
|
|
|
|
const steigt = kette.every(([, n], i) => i === 0 || n > kette[i - 1][1]);
|
|
ok(steigt,
|
|
steigt
|
|
? `und sie stehen in der richtigen Ordnung `
|
|
+ `(${kette.map(([r, n]) => `${r} ${n}`).join(" < ")} von ${t.seiten?.length})`
|
|
: `DIE ORDNUNG STIMMT NICHT: `
|
|
+ `${kette.map(([r, n]) => `${r} ${n}`).join(", ")} -- jemand `
|
|
+ `weiter unten sieht mehr als jemand weiter oben`);
|
|
|
|
/* UND DIE COMMUNITY IST DAS MINIMUM UEBER ALLE ROLLEN, nicht nur in
|
|
dieser Kette: Die Agentur steht daneben, nicht darueber, und auch
|
|
ihr gegenueber darf ein Mitglied nicht mehr Seiten haben. */
|
|
const alleZahlen = (t.rollen || []).filter((x) => Number.isFinite(x.seiten));
|
|
ok(alleZahlen.length >= 4 && alleZahlen.every((x) => x.schluessel === "gast"
|
|
|| x.seiten > zahl("gast")),
|
|
`die Community hat die wenigsten von allen ${alleZahlen.length} Rollen `
|
|
+ `(${zahl("gast")} von ${t.seiten?.length})`);
|
|
|
|
/* GEGENPROBE: Kann diese Ordnung ueberhaupt "nein" sagen? Umgekehrt
|
|
gelesen muss dieselbe Rechnung falsch herauskommen -- sonst waere
|
|
sie eine Bestaetigungsmaschine. */
|
|
const rueckwaerts = [...kette].reverse();
|
|
ok(!rueckwaerts.every(([, n], i) => i === 0 || n > rueckwaerts[i - 1][1]),
|
|
"Gegenprobe: umgekehrt gelesen faellt dieselbe Rechnung durch");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------- */
|
|
melde("");
|
|
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
/* Der Ordner bleibt manchmal liegen, weil der Dienst die Datenbank
|
|
unter Windows noch offen haelt. Das ist ein Aufraeumrest, kein
|
|
Befund -- deshalb faellt der Lauf daran nicht durch. */
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
|
|
process.exit(fehler ? 1 : 0);
|