Der sechste Bereich, und zwei Pruefungen, die den Lauf lahmgelegt haben
DER AGENTUR-BEREICH (Stufe 4 des Plans)
Kernprinzip 04 des Konzepts lautet woertlich "Die Agentur bleibt
angebunden" -- und dafuer gab es bis heute nichts. Fuenf Bereiche
standen, der sechste fehlte vollstaendig. Damit stand nirgends, welche
Kampagne laeuft, welche Schulung ansteht und wie ein Anliegen an die
Agentur ausgegangen ist. Das lief ueber private Nachrichten: nicht
auffindbar, nicht nachvollziehbar, beim naechsten Mal von vorn.
Vier Arten, und die vierte ist der Punkt: kampagne, schulung, anliegen
und ZUSTAENDIGKEIT. Die letzte ist keine Verlegenheit, sondern das, was
das Konzept ausdruecklich fordert -- Betreuung und Umsetzung sind unsere
Seite, offizielle Wege sind Agenturseite. Solange das nur im Kopf steht,
wird es bei jedem Streitfall neu verhandelt.
Der Umbau war das eigentliche Risiko, nicht der Bereich: Ein CHECK
laesst sich in SQLite nicht aendern, die Tabelle muss neu gebaut werden
-- und dabei werden ALLE vorhandenen Eintraege umgeschrieben. Deshalb
geht pruef-agentur.mjs (31 Pruefungen) den Weg wirklich: Sie baut eine
Datenbank im ALTEN Stand nach, fuellt sie mit allen fuenf Bereichen,
laesst die echte Anwendung darueberlaufen und zaehlt danach jede Zeile
UND jedes Feld nach -- auch die seltenen Spalten (hook, creator_extern),
die man beim Abschreiben des Bauplans vergisst. Wichtigste Gegenprobe:
Der CHECK muss danach noch BEISSEN. Eine Umstellung, die nebenbei die
Schranke entfernt, sieht aus wie ein Erfolg und ist der schlimmere
Ausgang.
Nebenbei drei abgeschriebene Bereichslisten beseitigt (Wochenbericht,
Suche, Report-Ziele). Im Wochenbericht waere der neue Bereich sonst
schlicht nicht vorgekommen -- ohne Fehler, ohne Luecke, einfach nicht da.
ZWEI PRUEFUNGEN, DIE DEN GESAMTLAUF ZUM STILLSTAND BRACHTEN
pruef-call-kategorien.mjs hing DREI STUNDEN. Ursache war ein Zeitzuender
in ihr selbst: Sie legte Calls auf "heute 18:00" und erwartete zwei
davon unter "Heute". Ab 18 Uhr sind die vorbei, der Server schiebt sie
nach "Protokoll fehlt", und die Oberflaeche unterteilt erst ab FUENF
anstehenden. Uebrig blieben vier, die Bloecke verschwanden, und die
Pruefung wartete auf einen Klick auf einen Block, den es nicht gab.
Genau die Sorte Fehler, vor der die Projektnotiz vom 06.09. warnt: Sie
war um 17:59 gruen und um 18:01 rot, ohne dass sich am Code etwas
geaendert hatte. Jetzt liegen alle Zeiten RELATIV zu jetzt, und die
Erwartung wird mit derselben Vorschrift gerechnet, die calls.js benutzt
-- statt Zahlen, die nur zu einer bestimmten Tageszeit stimmen.
Und der Grund, warum daraus ein Stillstand statt eines Fehlers wurde:
index.js setzt bewusst zwei Auffangnetze (uncaughtException,
unhandledRejection). Fuer den Betrieb richtig -- ein Fehler darf die
Website nicht offline nehmen. Fuer eine Pruefung fatal: Sie importiert
index.js in denselben Prozess und erbt die Netze. Ihr eigener Absturz
wird dann nur protokolliert, und der Express-Server haelt den Prozess
danach ewig am Leben. Kein Fehler, kein Ergebnis, kein Ende.
Deshalb zwei Abhilfen, die zusammengehoeren:
* server/helfer-notbremse.mjs -- haengt eigene Zuhoerer DANEBEN
(process.on ergaenzt, es ersetzt nicht) und beendet den Lauf mit
Fehler; dazu ein Wecker. In alle 42 Browserpruefungen eingebaut.
Der Betrieb bleibt unveraendert.
* tools/alles-pruefen.mjs gibt jeder Datei eine Frist von 600 s. Wer
sie reisst, wird abgebrochen und ZAEHLT ALS FEHLGESCHLAGEN, mit der
letzten Ausgabe davor als Beleg. Einzelne Dateien nachzuruesten hilft
nur bis zur naechsten ohne Notbremse -- deshalb sitzt sie dort, wo
sie fuer alle gilt, auch fuer die, die es noch nicht gibt.
AUSSERDEM: pruef-breiten.mjs pruefte 16 Seiten aus einer Liste von Hand,
chat.html und leistung.html fehlten darin. Der Chat war bei keiner
einzigen der neun Breiten je gemessen worden. Wie bei pruef-handy kommt
die Liste jetzt aus dem Verzeichnis.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
/* DER SECHSTE BEREICH — "Die Agentur bleibt angebunden"
|
||||
|
||||
Kernprinzip 04 des Konzepts, und bis zum 06.09.2026 gab es dafuer
|
||||
nichts: Fuenf Bereiche standen (LIVE, Content, Technik, Community,
|
||||
Schutz), der sechste fehlte vollstaendig.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DAS EIGENTLICHE RISIKO IST NICHT DER BEREICH, SONDERN DER UMBAU
|
||||
|
||||
Der erlaubte Wertebereich einer Spalte steckt in SQLite in einem
|
||||
CHECK, und ein CHECK laesst sich nicht aendern -- die Tabelle muss
|
||||
neu gebaut werden. Dabei werden ALLE vorhandenen Eintraege
|
||||
umgeschrieben: LIVE-Auswertungen, Content-Ideen, Technikprobleme,
|
||||
Community-Konflikte, Vorfaelle. Wer dort eine Spalte vergisst,
|
||||
verliert ihren Inhalt still.
|
||||
|
||||
Und das ist ein Weg, den man nicht zurueckdrehen kann. Deshalb wird
|
||||
er hier nicht "im Prinzip" geprueft, sondern WIRKLICH GEGANGEN: Die
|
||||
Pruefung baut eine Datenbank im ALTEN Stand nach, fuellt sie, laesst
|
||||
die echte Anwendung darueberlaufen und zaehlt danach jede Zeile und
|
||||
jedes Feld nach.
|
||||
|
||||
GEGENPROBEN durchgehend -- vor allem die eine, auf die es ankommt:
|
||||
Der CHECK muss danach immer noch BEISSEN. Eine Umstellung, die
|
||||
nebenbei die Schranke entfernt, sieht aus wie ein Erfolg und ist der
|
||||
schlimmere Ausgang.
|
||||
===================================================================== */
|
||||
|
||||
import { mkdtempSync, rmSync, appendFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawn } from "node:child_process";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { scryptSync, randomBytes } from "node:crypto";
|
||||
import { heuteLokal } from "./helfer-zeit.mjs";
|
||||
|
||||
const HIER = dirname(fileURLToPath(import.meta.url));
|
||||
const { portMussFreiSein } = await import("./helfer-port.mjs");
|
||||
const PORT = 4341;
|
||||
await portMussFreiSein(PORT, "die Agentur-Pruefung");
|
||||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-agentur-"));
|
||||
const DB = join(ordner, "workspace.db");
|
||||
|
||||
const PROTOKOLL = join(process.cwd(), "pruef-agentur-lauf.txt");
|
||||
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
|
||||
const melde = (z) => {
|
||||
console.log(z);
|
||||
try { appendFileSync(PROTOKOLL, z + "\n"); } catch { /* Zugabe */ }
|
||||
};
|
||||
let fehler = 0;
|
||||
let gemacht = 0;
|
||||
const ok = (b, t) => { gemacht++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Die Anwendung als eigener Prozess -- weil sie ZWEIMAL starten muss:
|
||||
einmal, um die Datenbank anzulegen, und einmal nach dem Zurueckbau,
|
||||
damit die Wanderung wirklich laeuft. In einem Prozess ginge das
|
||||
nicht: Ein Modul wird nur einmal geladen.
|
||||
--------------------------------------------------------------------- */
|
||||
let laufend = null;
|
||||
async function serverStarten() {
|
||||
const kind = spawn(process.execPath, [join(HIER, "index.js")], {
|
||||
env: {
|
||||
...process.env,
|
||||
WORKSPACE_DB: DB,
|
||||
PORT: String(PORT),
|
||||
SITE_ACCESS_SECRET: "lokaler-test",
|
||||
SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
let aus = "";
|
||||
kind.stdout.on("data", (d) => { aus += d; });
|
||||
kind.stderr.on("data", (d) => { aus += d; });
|
||||
for (let i = 0; i < 80; i++) {
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
if (kind.exitCode !== null) break;
|
||||
try {
|
||||
const a = await fetch(BASIS + "/workspace/", { redirect: "manual" });
|
||||
if (a.status < 500) { laufend = kind; return { kind, aus: () => aus }; }
|
||||
} catch { /* noch nicht oben */ }
|
||||
}
|
||||
throw new Error("Server startete nicht:\n" + aus.slice(-1200));
|
||||
}
|
||||
async function serverStoppen() {
|
||||
if (!laufend) return;
|
||||
laufend.kill();
|
||||
laufend = null;
|
||||
/* Warten, bis der Port wirklich frei ist -- sonst startet der zweite
|
||||
Lauf gegen einen halb geschlossenen Anschluss und misst Unsinn. */
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
try { await fetch(BASIS + "/workspace/", { redirect: "manual" }); } catch { return; }
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
1. Erster Start: die Datenbank entsteht
|
||||
======================================================================= */
|
||||
melde("=== Die Datenbank anlegen ===");
|
||||
{
|
||||
const { aus } = await serverStarten();
|
||||
ok(true, "die Anwendung ist oben und hat das Schema angelegt");
|
||||
void aus;
|
||||
}
|
||||
|
||||
const jetzt = new Date().toISOString();
|
||||
/* ORTSZEIT fuer das Tagesdatum, nicht der Schnitt aus dem UTC-Stempel.
|
||||
`jetzt.slice(0, 10)` liegt zwischen Mitternacht und zwei Uhr einen Tag
|
||||
daneben -- die Pruefung waere dann nachts rot, ohne dass sich etwas
|
||||
geaendert haette. Gefunden von pruef-struktur, das genau danach sucht. */
|
||||
const heute = heuteLokal();
|
||||
|
||||
function anlegen(d, name, rolle, code) {
|
||||
const salz = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(code, salz, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
||||
.run(name, rolle, hash, salz, 32768, jetzt);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
|
||||
let idDogi, idLuna;
|
||||
{
|
||||
const d = new DatabaseSync(DB);
|
||||
idDogi = anlegen(d, "Filipe", "admin", "CODE-DOGI-0001");
|
||||
idLuna = anlegen(d, "Luna", "creator", "CODE-CREA-0001");
|
||||
d.close();
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
2. Die Datenbank auf den ALTEN Stand zurueckbauen
|
||||
=======================================================================
|
||||
|
||||
Genau so sieht sie auf dem Server aus, bevor die Wanderung laeuft.
|
||||
Ohne diesen Schritt pruefte die ganze Datei nur eine frisch angelegte
|
||||
Tabelle -- und damit ausgerechnet den Fall, den es in der Wirklichkeit
|
||||
nicht gibt. */
|
||||
melde("\n=== Zurueck auf den alten Stand (fuenf Bereiche) ===");
|
||||
await serverStoppen();
|
||||
|
||||
const ALTE_EINTRAEGE = [
|
||||
["live", "auswertung", "Auswertung Montag", "Der Hook saß, die Mitte hing.", 4, "hoch"],
|
||||
["content", "idee", "Serie: Basteln mit Kindern", "Fünf Teile.", null, "mittel"],
|
||||
["technik", "problem", "Ton bricht ab", "Nur bei OBS 30.", null, "hoch"],
|
||||
["community", "konflikt", "Streit in den Kommentaren", "Zwei Zuschauer.", null, "niedrig"],
|
||||
["schutz", "vorfall", "Meldung wegen Sprache", "Geklärt.", null, "hoch"],
|
||||
];
|
||||
|
||||
{
|
||||
const d = new DatabaseSync(DB);
|
||||
d.exec("PRAGMA foreign_keys = OFF");
|
||||
d.exec("DROP TABLE IF EXISTS eintraege");
|
||||
/* WOERTLICH der alte Bauplan, mit allen Spalten, die zu diesem
|
||||
Zeitpunkt schon da waren. */
|
||||
d.exec(`
|
||||
CREATE TABLE eintraege (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
bereich TEXT NOT NULL
|
||||
CHECK (bereich IN ('live','content','technik','community','schutz')),
|
||||
art TEXT NOT NULL,
|
||||
titel TEXT NOT NULL,
|
||||
text TEXT,
|
||||
datum TEXT NOT NULL,
|
||||
bewertung INTEGER CHECK (bewertung IS NULL OR (bewertung BETWEEN 1 AND 5)),
|
||||
dringlichkeit TEXT NOT NULL DEFAULT 'mittel'
|
||||
CHECK (dringlichkeit IN ('hoch','mittel','niedrig')),
|
||||
status TEXT NOT NULL DEFAULT 'offen'
|
||||
CHECK (status IN ('offen','erledigt')),
|
||||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
erstellt TEXT NOT NULL,
|
||||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
geaendert TEXT,
|
||||
hook TEXT,
|
||||
format TEXT,
|
||||
saeule_id INTEGER,
|
||||
geplant TEXT,
|
||||
creator_extern TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_eintraege_bereich ON eintraege (bereich, creator_id);`);
|
||||
|
||||
const setzen = d.prepare(`INSERT INTO eintraege
|
||||
(bereich, art, titel, text, datum, bewertung, dringlichkeit, status,
|
||||
creator_id, erstellt, erstellt_von, hook, creator_extern)
|
||||
VALUES (?,?,?,?,?,?,?, 'offen', ?,?,?,?,?)`);
|
||||
for (const [b, art, titel, text, bew, dring] of ALTE_EINTRAEGE) {
|
||||
setzen.run(b, art, titel, text, heute, bew, dring, idLuna, jetzt, idDogi,
|
||||
b === "content" ? "Der Hook: 'Das habt ihr noch nie gesehen'" : null,
|
||||
b === "live" ? "Gastauftritt Mia" : null);
|
||||
}
|
||||
d.exec("PRAGMA foreign_keys = ON");
|
||||
|
||||
const n = d.prepare("SELECT COUNT(*) AS n FROM eintraege").get().n;
|
||||
ok(n === ALTE_EINTRAEGE.length, `${n} Eintraege im alten Stand angelegt`);
|
||||
|
||||
/* GEGENPROBE: Der alte CHECK muss "agentur" WIRKLICH ablehnen --
|
||||
sonst gaebe es nichts umzustellen, und der Rest dieser Datei
|
||||
bewiese nichts. */
|
||||
let abgelehnt = false;
|
||||
try {
|
||||
d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, erstellt)
|
||||
VALUES ('agentur','kampagne','Darf noch nicht',?,?)`).run(heute, jetzt);
|
||||
} catch { abgelehnt = true; }
|
||||
ok(abgelehnt, "im alten Stand wird 'agentur' von der Datenbank abgelehnt");
|
||||
d.close();
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
3. Die Wanderung -- der Weg, den man nicht zurueckdrehen kann
|
||||
======================================================================= */
|
||||
melde("\n=== Die Umstellung ===");
|
||||
let ausgabe = "";
|
||||
{
|
||||
const { aus } = await serverStarten();
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
ausgabe = aus();
|
||||
}
|
||||
|
||||
ok(/Bereich 'agentur' freigeschaltet/.test(ausgabe),
|
||||
"die Anwendung meldet die Umstellung"
|
||||
+ (/agentur/.test(ausgabe) ? "" : ` — Ausgabe: ${ausgabe.slice(-300)}`));
|
||||
|
||||
{
|
||||
const d = new DatabaseSync(DB, { readOnly: true });
|
||||
const plan = d.prepare(
|
||||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='eintraege'").get()?.sql || "";
|
||||
ok(/'agentur'/.test(plan), "der neue Bauplan kennt 'agentur'");
|
||||
|
||||
/* KEINE ZEILE DARF FEHLEN -- und keine darf sich VERAENDERT haben.
|
||||
Die Zeilenzahl allein reichte nicht: Wer beim Umschreiben eine
|
||||
Spalte vergisst, behaelt alle Zeilen und verliert ihren Inhalt.
|
||||
Genau das faellt sonst erst Wochen spaeter auf. */
|
||||
const alle = d.prepare("SELECT * FROM eintraege ORDER BY id").all();
|
||||
ok(alle.length === ALTE_EINTRAEGE.length,
|
||||
`alle ${alle.length} Eintraege sind noch da (vorher ${ALTE_EINTRAEGE.length})`);
|
||||
|
||||
let abweichungen = [];
|
||||
ALTE_EINTRAEGE.forEach(([b, art, titel, text, bew, dring], i) => {
|
||||
const z = alle[i];
|
||||
if (!z) { abweichungen.push(`${b}: Zeile fehlt`); return; }
|
||||
if (z.bereich !== b) abweichungen.push(`${b}: bereich = ${z.bereich}`);
|
||||
if (z.art !== art) abweichungen.push(`${b}: art = ${z.art}`);
|
||||
if (z.titel !== titel) abweichungen.push(`${b}: titel = ${z.titel}`);
|
||||
if (z.text !== text) abweichungen.push(`${b}: text = ${z.text}`);
|
||||
if (z.bewertung !== bew) abweichungen.push(`${b}: bewertung = ${z.bewertung}`);
|
||||
if (z.dringlichkeit !== dring) abweichungen.push(`${b}: dringlichkeit = ${z.dringlichkeit}`);
|
||||
if (z.creator_id !== idLuna) abweichungen.push(`${b}: creator_id = ${z.creator_id}`);
|
||||
if (z.erstellt_von !== idDogi) abweichungen.push(`${b}: erstellt_von = ${z.erstellt_von}`);
|
||||
});
|
||||
ok(abweichungen.length === 0,
|
||||
abweichungen.length ? `Inhalte haben sich veraendert: ${abweichungen.slice(0, 5).join(" | ")}`
|
||||
: `jedes Feld jeder Zeile ist unveraendert (${alle.length} Zeilen x 8 Felder geprueft)`);
|
||||
|
||||
/* Auch die SELTENEN Spalten -- gerade die vergisst man beim
|
||||
Abschreiben des Bauplans. */
|
||||
const mitHook = alle.find((z) => z.bereich === "content");
|
||||
const mitExtern = alle.find((z) => z.bereich === "live");
|
||||
ok(/Das habt ihr noch nie gesehen/.test(mitHook?.hook || ""),
|
||||
`die Spalte "hook" hat ihren Inhalt behalten ("${String(mitHook?.hook).slice(0, 30)}…")`);
|
||||
ok(mitExtern?.creator_extern === "Gastauftritt Mia",
|
||||
`die Spalte "creator_extern" ebenfalls ("${mitExtern?.creator_extern}")`);
|
||||
|
||||
/* Der Index muss nach dem Tausch wieder da sein -- er wird beim DROP
|
||||
mit entfernt. Ohne ihn laeuft alles weiter, nur langsam, und
|
||||
niemand merkt es je. */
|
||||
const indizes = d.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='eintraege'").all()
|
||||
.map((r) => r.name);
|
||||
ok(indizes.includes("idx_eintraege_bereich"),
|
||||
`der Index steht wieder (${indizes.join(", ")})`);
|
||||
d.close();
|
||||
}
|
||||
|
||||
/* GEGENPROBE, DIE WICHTIGSTE: Die Schranke muss noch BEISSEN. Eine
|
||||
Umstellung, die nebenbei den CHECK entfernt, sieht aus wie ein Erfolg
|
||||
und ist der schlimmere Ausgang -- ab da kann jeder Tippfehler einen
|
||||
Bereich erfinden, den es nicht gibt. */
|
||||
{
|
||||
const d = new DatabaseSync(DB);
|
||||
let abgelehnt = false;
|
||||
try {
|
||||
d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, erstellt)
|
||||
VALUES ('erfundenerbereich','x','Darf nicht',?,?)`).run(heute, jetzt);
|
||||
} catch { abgelehnt = true; }
|
||||
ok(abgelehnt, "ein erfundener Bereich wird weiterhin abgelehnt — der CHECK beisst noch");
|
||||
|
||||
let angenommen = false;
|
||||
try {
|
||||
d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, erstellt)
|
||||
VALUES ('agentur','kampagne','Direkt in die Datenbank',?,?)`).run(heute, jetzt);
|
||||
angenommen = true;
|
||||
d.exec("DELETE FROM eintraege WHERE titel = 'Direkt in die Datenbank'");
|
||||
} catch { /* bleibt false */ }
|
||||
ok(angenommen, "'agentur' wird jetzt angenommen");
|
||||
d.close();
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
4. Der Bereich im Betrieb
|
||||
======================================================================= */
|
||||
melde("\n=== Der Bereich im Betrieb ===");
|
||||
|
||||
async function anmelden(rolle, code) {
|
||||
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle, code }) });
|
||||
const keks = (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
||||
if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`);
|
||||
return keks;
|
||||
}
|
||||
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
|
||||
const keksLuna = await anmelden("creator", "CODE-CREA-0001");
|
||||
|
||||
async function ruf(art, weg, keks, koerper) {
|
||||
const a = await fetch(BASIS + weg, {
|
||||
method: art,
|
||||
headers: { "Content-Type": "application/json", cookie: keks, origin: BASIS },
|
||||
body: koerper ? JSON.stringify(koerper) : undefined,
|
||||
});
|
||||
let daten = null;
|
||||
try { daten = await a.json(); } catch { /* kein JSON */ }
|
||||
return { status: a.status, daten };
|
||||
}
|
||||
|
||||
/* Die vier Arten -- jede einzeln, denn eine, die der Server nicht kennt,
|
||||
waere im Formular waehlbar und beim Speichern ein Fehler. */
|
||||
const ARTEN = ["kampagne", "schulung", "anliegen", "zustaendig"];
|
||||
{
|
||||
const { daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksDogi);
|
||||
ok(daten?.einstellung?.name === "Agentur",
|
||||
`der Bereich meldet sich als "${daten?.einstellung?.name}"`);
|
||||
const arten = Object.keys(daten?.einstellung?.arten || {});
|
||||
ok(arten.length === 4 && ARTEN.every((a) => arten.includes(a)),
|
||||
`vier Arten: ${arten.join(", ")}`);
|
||||
ok(daten?.einstellung?.dringlichkeit === true,
|
||||
"Dringlichkeit ist an — ein Anliegen von gestern ist etwas anderes als eines von vor drei Wochen");
|
||||
ok(daten?.einstellung?.bewertung === false,
|
||||
"Bewertung ist aus — hier wird zusammengearbeitet, nicht benotet");
|
||||
}
|
||||
|
||||
let angelegt = 0;
|
||||
for (const art of ARTEN) {
|
||||
const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
|
||||
art, titel: `Test ${art}`, text: "Inhalt", datum: heute,
|
||||
dringlichkeit: "mittel", creator_id: idLuna,
|
||||
});
|
||||
if (status === 200 || status === 201) angelegt++;
|
||||
else ok(false, `Art "${art}" liess sich nicht anlegen (HTTP ${status})`);
|
||||
}
|
||||
ok(angelegt === ARTEN.length, `alle ${angelegt} Arten lassen sich anlegen`);
|
||||
|
||||
/* GEGENPROBE: eine erfundene Art muss abgelehnt werden. Sonst pruefte
|
||||
die Zeile darueber nur, dass der Server irgendetwas annimmt. */
|
||||
{
|
||||
const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
|
||||
art: "erfundeneart", titel: "Darf nicht", datum: heute,
|
||||
});
|
||||
ok(status >= 400, `eine erfundene Art wird abgelehnt (HTTP ${status})`);
|
||||
}
|
||||
|
||||
/* Der Creator SIEHT seinen Agentur-Bereich -- er ist der Grund, warum
|
||||
es ihn gibt. */
|
||||
{
|
||||
const { status, daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksLuna);
|
||||
ok(status === 200, `Luna darf den Bereich oeffnen (HTTP ${status})`);
|
||||
ok((daten?.eintraege || []).length === ARTEN.length,
|
||||
`sie sieht ihre ${(daten?.eintraege || []).length} Eintraege`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
5. Kommt er ueberall an, wo Bereiche vorkommen?
|
||||
======================================================================= */
|
||||
melde("\n=== Ueberall, wo Bereiche vorkommen ===");
|
||||
{
|
||||
/* Der Wochenbericht. Hier stand die Liste der fuenf Bereiche noch
|
||||
einmal von Hand abgeschrieben -- der neue waere schlicht nicht
|
||||
vorgekommen, ohne Fehler und ohne Luecke. */
|
||||
const { daten } = await ruf("GET", "/workspace/api/report", keksDogi);
|
||||
const b = daten?.bereiche || {};
|
||||
ok(Object.keys(b).length === 6,
|
||||
`der Wochenbericht kennt ${Object.keys(b).length} Bereiche: ${Object.keys(b).join(", ")}`);
|
||||
ok(b.agentur && b.agentur.neu >= ARTEN.length,
|
||||
`und zaehlt ${b.agentur?.neu} neue Agentur-Eintraege`);
|
||||
}
|
||||
{
|
||||
/* Die Suche. Ein Treffer ohne Ueberschrift sieht aus wie ein Fehler. */
|
||||
const { daten } = await ruf("GET", "/workspace/api/suche?q=" + encodeURIComponent("Test kampagne"), keksDogi);
|
||||
const text = JSON.stringify(daten);
|
||||
ok(/Agentur/.test(text),
|
||||
"die Suche beschriftet Agentur-Treffer mit dem richtigen Namen");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
6. Die Kachel — mit einem echten Browser
|
||||
======================================================================= */
|
||||
melde("\n=== Die Kachel ===");
|
||||
{
|
||||
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
const browser = await pw.chromium.launch();
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const seite = await ctx.newPage();
|
||||
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
||||
await seite.click('.rolle[data-rolle="admin"]');
|
||||
await seite.fill("#code", "CODE-DOGI-0001");
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
||||
await seite.waitForTimeout(1100);
|
||||
|
||||
const kacheln = await seite.$$eval(".kachel", (k) => k.map((x) => ({
|
||||
name: x.querySelector(".kachel__name")?.textContent.trim(),
|
||||
ziel: x.querySelector(".kachel__link")?.getAttribute("href"),
|
||||
ton: x.dataset.ton,
|
||||
pfade: x.querySelectorAll(".kachel__zeichen path").length,
|
||||
})));
|
||||
const a = kacheln.find((x) => x.name === "Agentur");
|
||||
ok(!!a, `die Kachel "Agentur" steht da (${kacheln.length} Kacheln insgesamt)`);
|
||||
ok(a?.ziel === "bereich.html?b=agentur", `sie fuehrt nach "${a?.ziel}"`);
|
||||
ok(Number(a?.pfade) >= 4, `ihr Zeichen hat ${a?.pfade} Pfade — es ist wirklich gezeichnet`);
|
||||
|
||||
/* KEINE ZWEI KACHELN MIT DEMSELBEN FARBTON NEBENEINANDER. Das Auge
|
||||
vergleicht nur, was benachbart ist -- zwei gleiche Toene direkt
|
||||
nebeneinander lesen sich als Fehler. */
|
||||
const toene = kacheln.map((x) => x.ton);
|
||||
const nachbarnGleich = toene.filter((t, i) => i > 0 && t === toene[i - 1]);
|
||||
ok(nachbarnGleich.length === 0,
|
||||
nachbarnGleich.length ? `zwei Nachbarkacheln haben denselben Ton (${nachbarnGleich.join(", ")})`
|
||||
: `keine zwei benachbarten Kacheln teilen einen Farbton (${toene.length} geprueft)`);
|
||||
|
||||
/* Die Seite selbst muss aufgehen und ihren Namen tragen. */
|
||||
await seite.goto(BASIS + "/workspace/bereich.html?b=agentur", { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(900);
|
||||
const titel = await seite.$eval(".titel", (e) => e.textContent.trim()).catch(() => "");
|
||||
ok(/Agentur/i.test(titel), `die Bereichsseite heisst "${titel}"`);
|
||||
|
||||
/* .eintrag-karte -- der Name aus bereich.js. Beim ersten Anlauf stand
|
||||
hier geraten ".eintrag, .karte"; das traf nichts und meldete "0
|
||||
Eintraege", obwohl vier dastanden. Ein falscher Selektor meldet
|
||||
einen Fehler, den es nicht gibt -- genauso schaedlich wie einer,
|
||||
der etwas uebersieht. */
|
||||
const eintraege = await seite.$$eval(".eintrag-karte", (e) => e.length).catch(() => 0);
|
||||
ok(eintraege >= ARTEN.length,
|
||||
`sie zeigt ${eintraege} Eintraege (mindestens die ${ARTEN.length} angelegten)`);
|
||||
|
||||
/* Und die Titel muessen die sein, die oben angelegt wurden -- sonst
|
||||
bewiese die Zahl nur, dass IRGENDWAS gezeichnet wurde. */
|
||||
const kartenTitel = await seite.$$eval(".eintrag-titel", (e) => e.map((x) => x.textContent.trim()));
|
||||
const fehlend = ARTEN.map((a) => `Test ${a}`).filter((t) => !kartenTitel.includes(t));
|
||||
ok(fehlend.length === 0,
|
||||
fehlend.length ? `diese Eintraege fehlen auf der Seite: ${fehlend.join(", ")}`
|
||||
: `alle vier Titel stehen da: ${kartenTitel.join(" · ")}`);
|
||||
|
||||
/* GEGENPROBE zum Selektor: Ein erfundener Bereich darf NICHT wie ein
|
||||
echter aussehen -- weder in der Ueberschrift noch mit Eintraegen.
|
||||
Die Anwendung schickt einen dorthin zurueck, wo er hingehoert. */
|
||||
await seite.goto(BASIS + "/workspace/bereich.html?b=gibtesnicht", { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(900);
|
||||
const falsch = await seite.$eval(".titel", (e) => e.textContent.trim()).catch(() => "");
|
||||
const falschKarten = await seite.$$eval(".eintrag-karte", (e) => e.length).catch(() => 0);
|
||||
ok(!/Agentur/i.test(falsch) && falschKarten === 0,
|
||||
`ein erfundener Bereich zeigt weder die Agentur noch Eintraege `
|
||||
+ `("${falsch}", ${falschKarten} Karten)`);
|
||||
|
||||
await ctx.close();
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
await serverStoppen();
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
|
||||
melde(`\n${gemacht} Pruefungen`);
|
||||
melde(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
||||
process.exit(fehler ? 1 : 0);
|
||||
Reference in New Issue
Block a user