Gemessen: 165 Pruefdateien, 129 verschiedene Nummern -- NEUNZEHN
doppelt, vier davon dreifach. Niemand hatte das gewollt; jede neue
Pruefung wurde von einer vorhandenen abgeschrieben, und die Nummer kam
mit. Meine eigene Notiz sagte "sieben" -- auch eine Bestandsliste
altert.
Der Waechter faengt den Schaden ab, aber er kann nur melden, was schon
passiert ist: Zwei Pruefungen mit derselben Nummer koennen nie
gleichzeitig laufen, und ein liegengebliebener Prozess der einen laesst
die andere abbrechen mit einer Meldung, die wie ein Befund aussieht.
Genau das ist mir am 20.09. zweimal passiert.
Jetzt leitet jede Datei ihre Nummer aus ihrer STELLE IM ALPHABET ab
(eigenerPort in helfer-port.mjs), zwei je Datei. Nicht ueber eine
Pruefsumme: Bei 165 Namen in 4900 Nummern waeren nach dem
Geburtstagsproblem rund DREI Zusammenstoesse zu erwarten -- ein Hash
tauscht eine sichtbare Doppelung gegen eine unsichtbare. Die Stelle im
Alphabet ist eindeutig von der Bauart her.
--- ZWEI FEHLER AUF DEM WEG, BEIDE LEHRREICH -------------------------
1. DER ERSTE VERSUCH WAR GRUEN UND KAPUTT. Ersetzt wurde mit einem
Muster: "([^"]*4231[^"]*)". Das hielt
{ host: "127.0.0.1", port: 4231, path: "/404.html" }
fuer eine Zeichenkette -- ein Muster kann eine oeffnende nicht von
einer schliessenden Anfuehrung unterscheiden. Heraus kam
{ host: "127.0.0.1`, port: ${PORT}, path: `/404.html" }
also GUELTIGER Code ohne port-Feld. `node --check` sagte gruen fuer
alle 164 Dateien. Aufgefallen ist es erst, weil ich vier Vertreter
gegen eine vorher gemessene Grundlinie laufen liess: pruef-schranke
39/0 vorher, 38/1 nachher.
Alles zurueckgenommen und mit einem Zerleger neu gemacht, der weiss,
ob eine Stelle Code, Zeichenkette, Vorlage, Kommentar oder
regulaerer Ausdruck ist. In pruef-ics stand die Nummer in einem
regulaeren Ausdruck -- der wird jetzt gebaut statt hingeschrieben.
2. EIN MODUL, DAS BEIM IMPORTIEREN ARBEITET, IST EINE FALLE. Der
zweite Durchgang importierte den ersten, um seine Mechanik zu
benutzen -- und fuehrte dessen Hauptlauf gleich mit aus. Die
zweiten Nummern wurden dadurch als erste behandelt, zwei Aufrufe
bekamen dieselbe Nummer, und in pruef-content stand `const PORT`
zweimal.
--- WAS DAS DAUERHAFT HAELT -----------------------------------------
pruef-portnummern.mjs (neu, 8 Pruefungen) fragt nicht "welche Nummern
sind doppelt", sondern "wer traegt ueberhaupt noch eine von Hand ein"
und "wer startet einen Server, ohne seine Nummer abzuleiten". Die
zweite Frage hat sofort etwas gefunden, das in KEINER Doppelungsliste
stand: pruef-push-weg belegte 4341 und 4342, rief den Waechter aber
gar nicht auf -- dieselbe Nummer wie pruef-agentur. Eine Liste zeigt
nur, was auf ihr steht.
Mit Gegenprobe: Eine unbekannte Datei bekommt keine geratene Nummer,
sondern einen Abbruch, und eine dritte Nummer je Datei gibt es nicht.
--- NACHGEMESSEN ----------------------------------------------------
Sechzehn Pruefungen gegen ihre vorher gemessene Grundlinie, je eine
Vertreterin jeder umgestellten Bauweise (eine Nummer, zwei Nummern,
Nummer in einer Zeichenkette, in einer Vorlage, in einem regulaeren
Ausdruck, dynamische Einfuhr, Nachtlauf mit zwei Laeufen):
crew-adresse 132/0 · schranke 39/0 · content 45/0 · entwicklung 46/0
anruf 127/0 · ics 37/0 · push-weg 20/0 · arten 28/0 · video 67/0
kanalzeile 14/0 · agentur 62/0 · spicy 83/0 · creator-anlegen 50/0
push-ziel 10/0 · portnummern 8/0
Alle exakt wie vorher. Zwei waren schon vorher rot und sind es
unveraendert geblieben (crew-wand-bild 41/4, teilen 14/3) -- per
`git stash` belegt, nicht angenommen.
Co-Authored-By: Claude Opus 5 <[email protected]>
427 lines
20 KiB
JavaScript
427 lines
20 KiB
JavaScript
/* DIE TERMINARTEN — und der Umbau, der sie freischaltet.
|
||
|
||
Wunsch Filipe (08.09.2026): *"da sollen auch noch kategorien wie
|
||
bigmatch, turniere, special-live ... informier dich, was man da alles
|
||
noch gebrauchen koennte."*
|
||
|
||
---------------------------------------------------------------------
|
||
WARUM DAS EINE EIGENE PRUEFUNG BRAUCHT
|
||
|
||
Eine Terminart ist ein erlaubter Wert in einer Spalte, und der steckt
|
||
in einem CHECK. Ein CHECK laesst sich in SQLite nicht aendern -- die
|
||
Tabelle muss neu gebaut werden, und zwar ZWEI davon (termine und
|
||
termin_serien). An `termine` haengen Teilnehmer, Wecker, Protokolle
|
||
und Serien.
|
||
|
||
Der gefaehrliche Ausgang ist nicht der Absturz, sondern der STILLE
|
||
Fehlschlag: Ein `replace`, das nichts findet, gibt den Text
|
||
unveraendert zurueck. Die Umstellung liefe durch, meldete Erfolg --
|
||
und die Datenbank lehnte beim ersten BigMatch trotzdem ab. Genau
|
||
dieser Fehler ist bei der Rolle 'spicy' passiert, gleich zweimal.
|
||
|
||
Deshalb wird hier nicht "im Prinzip" geprueft, sondern der Weg
|
||
WIRKLICH GEGANGEN: Die Pruefung baut eine Datenbank im alten Stand
|
||
nach, fuellt sie mit Terminen samt abhaengigen Zeilen, laesst die
|
||
echte Anwendung darueberlaufen und zaehlt danach nach.
|
||
|
||
UND DIE ZWEI LISTEN. Die erlaubten Arten stehen an zwei Stellen: im
|
||
CHECK (workspace.js) und in der Pruefliste des Servers
|
||
(workspace-kalender.js). Zwei Listen fuer dieselbe Frage laufen
|
||
auseinander -- hier werden sie gegeneinander gehalten.
|
||
|
||
---------------------------------------------------------------------
|
||
ES WAREN VIER LISTEN, NICHT ZWEI (08.09.2026, nachgetragen)
|
||
|
||
Die erste Fassung dieser Pruefung endete beim "HTTP 201": Der Server
|
||
nimmt die neue Art an, also ist sie da. Sie war grün. Und sie hat den
|
||
schwersten Fehler des Tages nicht gesehen.
|
||
|
||
In kalender.js standen ZWEI WEITERE Aufzaehlungen derselben Arten:
|
||
`zeigen = { call, termin, review, frist }` und die Schalterleiste
|
||
`['call','termin','review','frist']`. Beide blieben stehen. Gefiltert
|
||
wurde mit `zeigen[e.art]` -- fuer 'bigmatch' ist das `undefined`.
|
||
|
||
Also: anlegen ging, speichern ging, der Server meldete Erfolg, die
|
||
Zeile stand in der Datenbank -- und im Kalender war der Eintrag in
|
||
KEINER Ansicht zu sehen. Kein Fehler, keine Meldung, nichts. Filipe
|
||
haette sein erstes BigMatch eingetragen und eine leere Seite gesehen.
|
||
|
||
Gefunden hat es kein Test, sondern ein BILDSCHIRMFOTO: In der
|
||
Schalterleiste standen vier Arten statt zehn.
|
||
|
||
Deshalb hoert diese Pruefung jetzt nicht mehr beim Serverstatus auf.
|
||
Sie legt je Art einen Termin an, oeffnet den Kalender in einem echten
|
||
Browser und ZAEHLT NACH, ob er dort steht. Ein Server-OK ist keine
|
||
Sichtbarkeit.
|
||
|
||
Aufruf: node server/pruef-arten.mjs */
|
||
|
||
import { mkdtempSync, rmSync, readFileSync } 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 { notbremse } from "./helfer-notbremse.mjs";
|
||
|
||
notbremse(300_000, "die Artenpruefung");
|
||
|
||
const HIER = dirname(fileURLToPath(import.meta.url));
|
||
const { eigenerPort } = await import("./helfer-port.mjs");
|
||
const PORT = await eigenerPort(import.meta, "die Artenpruefung");
|
||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-arten-"));
|
||
const DB = join(ordner, "workspace.db");
|
||
|
||
let fehler = 0, gemacht = 0;
|
||
const ok = (b, t) => { gemacht++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||
|
||
let kind = null;
|
||
async function starten() {
|
||
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 < 100; 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) return () => aus; } catch { /* noch nicht oben */ }
|
||
}
|
||
throw new Error("Server startete nicht:\n" + aus.slice(-1500));
|
||
}
|
||
async function stoppen() {
|
||
if (!kind) return;
|
||
kind.kill(); kind = null;
|
||
await new Promise((r) => setTimeout(r, 700));
|
||
}
|
||
|
||
/* =======================================================================
|
||
1. DIE BEIDEN LISTEN MUESSEN DIESELBEN SEIN
|
||
======================================================================= */
|
||
console.log("=== Zwei Listen, eine Wahrheit ===");
|
||
const NEUE = ["bigmatch", "turnier", "special", "collab", "raid", "charity"];
|
||
{
|
||
const schema = readFileSync(join(HIER, "workspace.js"), "utf8");
|
||
const kalender = readFileSync(join(HIER, "workspace-kalender.js"), "utf8");
|
||
|
||
/* NICHT DER ERSTE TREFFER DER DATEI (08.09.2026).
|
||
|
||
Hier stand schlicht `schema.match(/CHECK \(art IN \(([^)]*)\)\)/)`.
|
||
Der erste Treffer ist aber der CHAT -- `art IN ('direkt','gruppe')`,
|
||
rund 80 Zeilen vor den Terminen. Die Pruefung verglich also die
|
||
Chatraumarten mit den Terminarten und meldete rot, obwohl am Code
|
||
nichts falsch war. Ein Fehlalarm kostet dasselbe Vertrauen wie ein
|
||
uebersehener Fehler.
|
||
|
||
Deshalb wird ab dem Bauplan der jeweiligen Tabelle gesucht -- und
|
||
BEIDE Termintabellen werden geprueft, nicht eine stellvertretend. */
|
||
const ausTabelle = (name) => {
|
||
const ab = schema.indexOf(`CREATE TABLE IF NOT EXISTS ${name} (`);
|
||
if (ab < 0) return [];
|
||
const treffer = schema.slice(ab, ab + 4000).match(/CHECK \(art IN \(([^)]*)\)\)/);
|
||
return (treffer?.[1] || "").split(",")
|
||
.map((x) => x.trim().replace(/'/g, "")).filter(Boolean);
|
||
};
|
||
const ausCheck = ausTabelle("termine");
|
||
const ausSerien = ausTabelle("termin_serien");
|
||
ok(JSON.stringify(ausCheck) === JSON.stringify(ausSerien) && ausCheck.length > 0,
|
||
`termine und termin_serien tragen dieselbe Liste (${ausCheck.length}/${ausSerien.length})`);
|
||
|
||
const liste = kalender.match(/const ARTEN = \[([^\]]*)\]/);
|
||
const ausServer = (liste?.[1] || "").split(",")
|
||
.map((x) => x.trim().replace(/["']/g, "")).filter(Boolean);
|
||
|
||
ok(ausCheck.length >= 9, `der CHECK kennt ${ausCheck.length} Arten`);
|
||
ok(ausServer.length >= 9, `die Pruefliste kennt ${ausServer.length} Arten`);
|
||
ok(JSON.stringify([...ausCheck].sort()) === JSON.stringify([...ausServer].sort()),
|
||
`beide Listen sind gleich (CHECK: ${ausCheck.join(",")} | Server: ${ausServer.join(",")})`);
|
||
for (const a of NEUE) {
|
||
ok(ausCheck.includes(a) && ausServer.includes(a), ` "${a}" steht in beiden`);
|
||
}
|
||
}
|
||
|
||
/* =======================================================================
|
||
2. DER ALTE STAND -- und der Umbau darueber hinweg
|
||
======================================================================= */
|
||
console.log("\n=== Der Umbau einer bestehenden Datenbank ===");
|
||
await starten();
|
||
await stoppen();
|
||
|
||
const jetzt = new Date().toISOString();
|
||
|
||
/* ORTSZEIT FUER DAS TAGESDATUM, NICHT UTC (08.09.2026).
|
||
|
||
Hier wurde der Tag mit `jetzt.slice(0, 10)` gebildet -- also aus UTC.
|
||
Zwischen Mitternacht und zwei Uhr Ortszeit ist das noch der VORTAG,
|
||
die Anwendung rechnet aber mit heuteLokal(). Ein Termin waere fuer
|
||
gestern angelegt und heute nicht gefunden worden; die Pruefung waere
|
||
nachts rot geworden, ohne dass am Code etwas falsch ist.
|
||
|
||
Das ist mir an genau diesem Abend zweimal passiert: erst im
|
||
Messskript fuer die Startseite ("Heute=0", obwohl drei Termine
|
||
angelegt waren -- ich habe den Code verdaechtigt, der richtig lag),
|
||
und dann hier. pruef-struktur.mjs hat die zweite Stelle gefunden,
|
||
bevor sie schaden konnte. Genau dafuer gibt es sie. */
|
||
const tagOrt = (versatzTage = 0) => {
|
||
const d = new Date(Date.now() + versatzTage * 86400000);
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`
|
||
+ `-${String(d.getDate()).padStart(2, "0")}`;
|
||
};
|
||
let idDogi, idLuna, terminVorher, serienVorher;
|
||
{
|
||
const d = new DatabaseSync(DB);
|
||
d.exec("PRAGMA foreign_keys = OFF");
|
||
|
||
/* Beide Tabellen auf den alten CHECK zuruecksetzen -- woertlich so,
|
||
wie sie vor heute aussahen, aber mit allen Spalten, die inzwischen
|
||
dazugekommen sind. Genau das ist der Fall, der gefaehrlich ist. */
|
||
for (const t of ["termine", "termin_serien"]) {
|
||
const alt = d.prepare(
|
||
"SELECT sql FROM sqlite_master WHERE type='table' AND name=?").get(t).sql;
|
||
const zurueck = alt
|
||
.replace(new RegExp(
|
||
"CREATE TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?[\"'`]?" + t + "[\"'`]?", "i"),
|
||
`CREATE TABLE ${t}_alt`)
|
||
.replace(/art\s+IN\s*\([^)]*\)/i, "art IN ('termin','call','review')");
|
||
/* Greift der Rueckbau nicht, ist alles danach wertlos -- dann
|
||
prueft diese Datei den heutigen Stand gegen sich selbst. */
|
||
if (!zurueck.includes(`${t}_alt`) || zurueck.includes("'bigmatch'")) {
|
||
console.log(` FEHL der alte Stand von ${t} liess sich nicht nachbauen.`);
|
||
process.exit(1);
|
||
}
|
||
const sp = d.prepare(`PRAGMA table_info(${t})`).all().map((z) => `"${z.name}"`).join(", ");
|
||
d.exec("BEGIN");
|
||
d.exec(zurueck);
|
||
d.exec(`INSERT INTO ${t}_alt (${sp}) SELECT ${sp} FROM ${t};`);
|
||
d.exec(`DROP TABLE ${t};`);
|
||
d.exec(`ALTER TABLE ${t}_alt RENAME TO ${t};`);
|
||
d.exec("COMMIT");
|
||
}
|
||
d.exec("PRAGMA foreign_keys = ON");
|
||
|
||
const anlegen = (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;
|
||
};
|
||
idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||
idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
|
||
|
||
const heute = tagOrt();
|
||
const rein = d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von)
|
||
VALUES (?,?,?,?,?,?,?)`);
|
||
rein.run("Alter Call", "call", heute + "T10:00", 30, idLuna, jetzt, idDogi);
|
||
rein.run("Alter Termin", "termin", heute + "T12:00", 30, idLuna, jetzt, idDogi);
|
||
rein.run("Altes Review", "review", heute + "T14:00", 45, idLuna, jetzt, idDogi);
|
||
/* Abhaengige Zeilen -- ohne sie pruefte der Umbau eine Tabelle statt
|
||
eines Netzes. */
|
||
const tid = d.prepare("SELECT id FROM termine WHERE titel = 'Alter Call'").get().id;
|
||
d.prepare("INSERT INTO termin_teilnehmer (termin_id, person_id) VALUES (?,?)").run(tid, idLuna);
|
||
d.prepare("INSERT INTO termin_wecker (termin_id, person_id, minuten_vorher, gesetzt) VALUES (?,?,?,?)")
|
||
.run(tid, idDogi, 1440, jetzt);
|
||
d.prepare(`INSERT INTO termin_serien (titel, art, takt, start_tag, uhrzeit, dauer_min, aktiv, erstellt, erstellt_von)
|
||
VALUES ('Wochen-Call','call','woechentlich',?,'18:00',30,1,?,?)`)
|
||
.run(heute, jetzt, idDogi);
|
||
|
||
terminVorher = d.prepare("SELECT COUNT(*) AS n FROM termine").get().n;
|
||
serienVorher = d.prepare("SELECT COUNT(*) AS n FROM termin_serien").get().n;
|
||
|
||
/* GEGENPROBE: Der alte Stand muss 'bigmatch' WIRKLICH ablehnen --
|
||
sonst gaebe es nichts umzustellen, und alles danach bewiese nichts. */
|
||
let abgelehnt = false;
|
||
try {
|
||
d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, erstellt, erstellt_von)
|
||
VALUES ('Darf noch nicht','bigmatch',?,30,?,?)`).run(heute + "T20:00", jetzt, idDogi);
|
||
} catch { abgelehnt = true; }
|
||
d.close();
|
||
ok(abgelehnt, "der alte Stand lehnt die Art 'bigmatch' wirklich ab");
|
||
ok(terminVorher === 3 && serienVorher === 1,
|
||
`${terminVorher} Termine und ${serienVorher} Serie angelegt`);
|
||
}
|
||
|
||
/* Jetzt die echte Anwendung darueberlaufen lassen. */
|
||
await starten();
|
||
|
||
{
|
||
const d = new DatabaseSync(DB);
|
||
/* NICHT DIE ANZAHL VERGLEICHEN (08.09.2026).
|
||
|
||
Erst stand hier `nachher === terminVorher`. Das schlug fehl -- 28
|
||
statt 3 --, und zwar zu Recht: Der Serverstart breitet die Serie
|
||
aus und legt dabei echte Termine an. Die Pruefung mass also den
|
||
Umbau UND die Serienausbreitung in einer Zahl und konnte nicht
|
||
sagen, welches von beiden gemeint war.
|
||
|
||
Der Umbau wird deshalb namentlich geprueft: Genau diese drei
|
||
Zeilen muessen den Tabellentausch ueberlebt haben. */
|
||
const alte = d.prepare(
|
||
"SELECT titel FROM termine WHERE titel LIKE 'Alte%' ORDER BY titel").all().map((z) => z.titel);
|
||
ok(alte.length === 3 && alte.join("|") === "Alter Call|Alter Termin|Altes Review",
|
||
`die drei alten Termine haben den Tabellentausch ueberlebt (${alte.join(", ") || "keiner"})`);
|
||
const serienNachher = d.prepare("SELECT COUNT(*) AS n FROM termin_serien").get().n;
|
||
ok(serienNachher === serienVorher, `die Serie ebenfalls (${serienNachher})`);
|
||
|
||
/* UND WEIL DIE ZAHL AUFGEFALLEN IST, wird sie gemessen -- aber gegen
|
||
die Regel, die WIRKLICH gilt.
|
||
|
||
Erst stand hier "kein Serientermin ausserhalb des laufenden
|
||
Monats", nach Filipes Satz vom 07.09.2026: *"nicht monate im
|
||
vorraus immer nur fuer den monat selbst"*. Die Pruefung wurde rot
|
||
(25 Termine bis in den Maerz) -- und lag falsch. Der Satz gehoerte
|
||
zum CALLS-BRETT (er zeigte dessen Bildschirmfoto); dort ist er
|
||
umgesetzt und wird von pruef-call-kategorien gemessen. Der KALENDER
|
||
legt bewusst 180 Tage im Voraus an, begruendet in workspace-serien.js:
|
||
der 90-Tage-Plan aus dem Konzept mit Reserve.
|
||
|
||
Beinahe haette ich den Code an eine Regel angepasst, die es an
|
||
dieser Stelle nie gab. Eine Pruefung, die eine erfundene Regel
|
||
misst, ist schlimmer als keine: Sie wird rot, jemand "repariert"
|
||
etwas Heiles, und beim naechsten Mal glaubt ihr niemand mehr. */
|
||
const HORIZONT = 180;
|
||
const grenze = tagOrt(HORIZONT + 1);
|
||
const ausSerie = d.prepare(
|
||
"SELECT beginn FROM termine WHERE serie_id IS NOT NULL ORDER BY beginn").all();
|
||
const zuWeit = ausSerie.filter((z) => z.beginn.slice(0, 10) > grenze);
|
||
ok(ausSerie.length > 0, `die Serie hat Termine erzeugt (${ausSerie.length})`);
|
||
ok(zuWeit.length === 0,
|
||
`keiner reicht ueber den ${HORIZONT}-Tage-Horizont hinaus (${zuWeit.length} zu weit: `
|
||
+ `${zuWeit.slice(0, 3).map((z) => z.beginn).join(", ") || "–"})`);
|
||
|
||
/* Die abhaengigen Zeilen -- der eigentliche Prueffall des Umbaus. */
|
||
const teiln = d.prepare("SELECT COUNT(*) AS n FROM termin_teilnehmer").get().n;
|
||
const wecker = d.prepare("SELECT COUNT(*) AS n FROM termin_wecker").get().n;
|
||
ok(teiln === 1, `der Teilnehmer haengt noch am Termin (${teiln})`);
|
||
ok(wecker === 1, `der Wecker ebenfalls (${wecker})`);
|
||
const kaputt = d.prepare("PRAGMA foreign_key_check").all();
|
||
ok(kaputt.length === 0, `keine verwaisten Verweise (${kaputt.length})`);
|
||
|
||
/* Die CHECK-Regel selbst: in BEIDEN Tabellen. */
|
||
for (const t of ["termine", "termin_serien"]) {
|
||
const plan = d.prepare(
|
||
"SELECT sql FROM sqlite_master WHERE type='table' AND name=?").get(t).sql;
|
||
const regel = plan.match(/art\s+IN\s*\(([^)]*)\)/i)?.[1] || "";
|
||
ok(regel.includes("'bigmatch'") && regel.includes("'charity'"),
|
||
`${t} kennt die neuen Arten (${regel.slice(0, 60)}…)`);
|
||
}
|
||
|
||
/* UND DIE SCHRANKE BEISST NOCH. Eine Umstellung, die den CHECK
|
||
nebenbei entfernt, sieht aus wie ein Erfolg und ist der schlimmere
|
||
Ausgang. */
|
||
let erfunden = false;
|
||
try {
|
||
d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, erstellt, erstellt_von)
|
||
VALUES ('Quatsch','quatsch',?,30,?,?)`).run(tagOrt() + "T21:00", jetzt, idDogi);
|
||
} catch { erfunden = true; }
|
||
ok(erfunden, "eine erfundene Art wird weiterhin abgelehnt");
|
||
d.close();
|
||
}
|
||
|
||
/* =======================================================================
|
||
3. UEBER DIE SCHNITTSTELLE
|
||
======================================================================= */
|
||
console.log("\n=== Anlegen mit den neuen Arten ===");
|
||
{
|
||
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }) });
|
||
const keks = (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
||
ok(!!keks, "als DogFather angemeldet");
|
||
|
||
const morgen = tagOrt(1);
|
||
let angelegt = 0;
|
||
for (const art of NEUE) {
|
||
const r = await fetch(BASIS + "/workspace/api/termine", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", cookie: keks, origin: BASIS },
|
||
body: JSON.stringify({ titel: `Probe ${art}`, art, beginn: morgen + "T20:00", dauer_min: 60 }),
|
||
});
|
||
if (r.status === 201) angelegt++;
|
||
}
|
||
ok(angelegt === NEUE.length,
|
||
`alle ${NEUE.length} neuen Arten lassen sich anlegen (${angelegt})`);
|
||
|
||
/* GEGENPROBE ueber die Schnittstelle: eine erfundene Art muss
|
||
abgelehnt werden, und zwar mit 400 -- nicht mit einem Serverfehler. */
|
||
const quatsch = await fetch(BASIS + "/workspace/api/termine", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", cookie: keks, origin: BASIS },
|
||
body: JSON.stringify({ titel: "Quatsch", art: "quatsch", beginn: morgen + "T21:00" }),
|
||
});
|
||
ok(quatsch.status === 400, `eine erfundene Art wird abgelehnt (HTTP ${quatsch.status})`);
|
||
|
||
/* =====================================================================
|
||
4. UND JETZT DER TEIL, DER GEFEHLT HAT: IST ES AUCH ZU SEHEN?
|
||
===================================================================== */
|
||
console.log("\n=== Im echten Kalender sichtbar ===");
|
||
const { chromium } = await import(
|
||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||
const browser = await chromium.launch();
|
||
try {
|
||
const sitzung = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||
const kekse = keks.split("; ").map((z) => {
|
||
const [name, ...wert] = z.split("=");
|
||
return { name, value: wert.join("="), domain: "127.0.0.1", path: "/" };
|
||
});
|
||
await sitzung.addCookies(kekse);
|
||
const seite = await sitzung.newPage();
|
||
await seite.goto(BASIS + "/workspace/kalender.html", { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(1500);
|
||
|
||
/* In die Listenansicht -- dort steht jeder Eintrag mit Titel da,
|
||
unabhaengig davon, welcher Monat gerade angezeigt wird. */
|
||
const liste = await seite.$("button:has-text('Liste')");
|
||
if (liste) { await liste.click(); await seite.waitForTimeout(900); }
|
||
|
||
const text = await seite.evaluate(() => document.body.innerText);
|
||
let sichtbar = 0;
|
||
for (const art of NEUE) {
|
||
const da = text.includes(`Probe ${art}`);
|
||
if (da) sichtbar++;
|
||
else console.log(` FEHL "Probe ${art}" steht in der Datenbank, aber NICHT im Kalender`);
|
||
}
|
||
ok(sichtbar === NEUE.length,
|
||
`alle ${NEUE.length} neuen Arten sind im Kalender zu sehen (${sichtbar})`);
|
||
|
||
/* Und die Schalterleiste muss sie kennen -- sonst laesst sich eine
|
||
Art zwar sehen, aber nicht mehr ausblenden. */
|
||
const schalter = await seite.$$eval(".k-chip", (ns) => ns.map((n) => n.textContent));
|
||
const fehlend = NEUE.filter((a) => !schalter.some((s) => s.toLowerCase().includes(a.slice(0, 5))));
|
||
ok(fehlend.length === 0,
|
||
`die Schalterleiste kennt die neuen Arten (${schalter.length} Schalter, `
|
||
+ `fehlt: ${fehlend.join(",") || "keine"})`);
|
||
|
||
/* GEGENPROBE: Ein Schalter muss auch WIRKEN. Sonst waere die
|
||
Sichtbarkeit oben nur deshalb gruen, weil gar nicht gefiltert
|
||
wird -- eine Pruefung, die immer bestaetigt. */
|
||
const vorher = (await seite.evaluate(() => document.body.innerText)).includes("Probe bigmatch");
|
||
const knopf = (await seite.$$(".k-chip")).find(async () => true);
|
||
const bmSchalter = await seite.$(".k-chip:has-text('BigMatch')");
|
||
let wirkt = false;
|
||
if (bmSchalter) {
|
||
await bmSchalter.click();
|
||
await seite.waitForTimeout(600);
|
||
const nachher = (await seite.evaluate(() => document.body.innerText)).includes("Probe bigmatch");
|
||
wirkt = vorher && !nachher;
|
||
}
|
||
ok(wirkt, `der Schalter blendet die Art auch wirklich aus (vorher da: ${vorher})`);
|
||
void knopf;
|
||
} finally {
|
||
await browser.close();
|
||
}
|
||
}
|
||
|
||
await stoppen();
|
||
console.log(`\n${gemacht} Pruefungen`);
|
||
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
process.exit(fehler ? 1 : 0);
|