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]>
627 lines
30 KiB
JavaScript
627 lines
30 KiB
JavaScript
/* =====================================================================
|
|
ENTWICKLUNG — die Auswertung je Person (19.09.2026)
|
|
|
|
Filipe: „dan perfektionierst du eine neue wo diagramme texte und so
|
|
zu jedem modi gemacht wird und diese kategorie kannst du entwicklung
|
|
nenen."
|
|
|
|
---------------------------------------------------------------------
|
|
WAS HIER GEPRÜFT WIRD — UND WARUM GENAU DAS
|
|
|
|
Eine Seite, die Diagramme über MENSCHEN zeigt, kann auf drei Arten
|
|
falsch sein, und nur eine davon ist ein Programmfehler:
|
|
|
|
1. SIE ZEIGT ZU VIEL. Der Anlass eines „da hakt es“ ist für ein
|
|
Gespräch aufgeschrieben. Landete er in der Spur, wäre jede
|
|
längst geklärte Beobachtung auf Dauer lesbar. Block 5 („Wie
|
|
geht es dir damit?“) darf überhaupt nicht hierherkommen.
|
|
2. SIE BEWERTET. Punktzahl, Prozentwert, Rangliste, Vergleich
|
|
zwischen zwei Menschen — nichts davon darf entstehen, auch
|
|
nicht versehentlich über einen Umweg.
|
|
3. SIE BEHAUPTET EINEN VERLAUF, DEN ES NICHT GIBT. Ein Diagramm
|
|
über einen einzigen Zeitpunkt ist kein Diagramm. Dann muss die
|
|
Seite das SAGEN — der dritte Ausgang.
|
|
|
|
ZU JEDER PRÜFUNG, DIE „ALLES IN ORDNUNG“ SAGEN KANN, GEHÖRT EINE
|
|
GEGENPROBE, die beweist, dass sie auch „nicht in Ordnung“ sagen
|
|
kann. Eine Prüfung, die immer bestätigt, bestätigt nichts.
|
|
|
|
Aufrufen mit: node server/pruef-werdegang.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { request as httpAnfrage } from "node:http";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
|
|
const PORT = await eigenerPort(import.meta, "pruef-werdegang");
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-weg-"));
|
|
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(180_000, "pruef-werdegang");
|
|
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 { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const W = await import("./workspace-werdegang.js");
|
|
const kat = await import("./workspace-entwicklungs-punkte.js");
|
|
const rechte = await import("./rechte.js");
|
|
const ent = await import("./workspace-entwicklung.js");
|
|
|
|
const oeffne = () => new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
|
|
/* ABGELEITET, NICHT ABGESCHRIEBEN. Jede feste Zahl in einer Prüfung ist
|
|
eine Rechnung von gestern -- am 17.09. wurden aus sechs Fragen zwölf
|
|
und vier Prüfungen waren rot, ohne dass etwas kaputt war. */
|
|
const PUNKTE = kat.ENTWICKLUNG_FREMD;
|
|
const SELBST = kat.ENTWICKLUNG_SELBST;
|
|
|
|
let idDogi, idHand, idModi, idCreator;
|
|
{
|
|
const d = oeffne();
|
|
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);
|
|
const anlegen = (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");
|
|
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"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
};
|
|
idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
|
|
idModi = anlegen("Diene", "modi", "CODE-MODI-0001");
|
|
idCreator = anlegen("Tili", "creator", "CODE-CREA-0001");
|
|
d.close();
|
|
}
|
|
|
|
function ruf(pfad, host, methode, kopf = {}, koerper = null) {
|
|
return new Promise((fertig, schief) => {
|
|
const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode,
|
|
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) {
|
|
const r = await ruf("/workspace/api/anmelden", host, "POST", {}, { rolle, code });
|
|
return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
|
|
}
|
|
|
|
const kDogi = await anmelden("admin", "CODE-DOGI-0001", CREW);
|
|
const kHand = await anmelden("hand", "CODE-HAND-0001", CREW);
|
|
const kModi = await anmelden("modi", "CODE-MODI-0001", CREW);
|
|
ok(!!kDogi && !!kHand && !!kModi, "DogFather, rechte Hand und ein Modi sind angemeldet");
|
|
|
|
/* =======================================================================
|
|
1. DIE KACHEL HEISST „ENTWICKLUNG“ UND FÜHRT AUCH DORTHIN
|
|
|
|
Der ganze Auftrag hängt an einer Umbenennung: Die alte Kachel heißt
|
|
seit heute früh „Eure Aufgaben“, damit der Name frei wird. Wenn die
|
|
neue Kachel auf die alte Seite zeigt, ist genau nichts gewonnen.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 1. Die Kachel ===");
|
|
{
|
|
const ich = json(await ruf("/workspace/api/ich", CREW, "GET", { cookie: kDogi }));
|
|
const alle = [...(ich.bereiche || []), ...(ich.bereiche_zusatz || [])];
|
|
|
|
const neu = alle.find((k) => k.name === "Entwicklung");
|
|
ok(!!neu, "eine Kachel „Entwicklung“ steht bei DogFather");
|
|
ok(neu?.ziel === "werdegang.html",
|
|
`und führt auf die Auswertung (${neu?.ziel})`);
|
|
|
|
/* GEGENPROBE. Ohne sie bewiese die Zeile darüber nichts: Der Katalog
|
|
muss weiterhin erreichbar sein, unter seinem neuen Namen. */
|
|
const alt = alle.find((k) => k.ziel === "entwicklung.html");
|
|
ok(alt?.name === "Eure Aufgaben",
|
|
`der Katalog heißt weiter „${alt?.name}“ und liegt woanders`);
|
|
ok(neu?.ziel !== alt?.ziel, "beide Kacheln zeigen auf VERSCHIEDENE Seiten");
|
|
|
|
/* ZWEI KACHELN, ZWEI FARBEN. Gleiche Farbe hieße, dass man sie auf
|
|
der Startseite verwechselt -- und sie stehen nebeneinander. */
|
|
ok(neu?.ton && alt?.ton && neu.ton !== alt.ton,
|
|
`verschiedene Farbtöne (${alt?.ton} / ${neu?.ton})`);
|
|
|
|
const css = readFileSync(
|
|
new URL("../workspace/assets/css/start.css", import.meta.url), "utf8");
|
|
ok(new RegExp(`\\[data-ton="${neu?.ton}"\\]`).test(css),
|
|
`Ton ${neu?.ton} ist in start.css wirklich definiert`);
|
|
|
|
/* UND BEI DER RECHTEN HAND AUCH. Sie arbeitet an denselben Dingen --
|
|
eine Kachel nur bei DogFather wäre die Über-/Unterordnung, die es
|
|
in diesem Haus nicht gibt. */
|
|
const beiHand = json(await ruf("/workspace/api/ich", CREW, "GET", { cookie: kHand }));
|
|
ok([...(beiHand.bereiche || []), ...(beiHand.bereiche_zusatz || [])]
|
|
.some((k) => k.ziel === "werdegang.html"),
|
|
"die rechte Hand hat dieselbe Kachel");
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. WER HINEINDARF
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 2. Wer hineindarf ===");
|
|
{
|
|
const tafel = rechte.tafelJetzt();
|
|
const erlaubt = tafel["/workspace/werdegang.html"] || [];
|
|
ok(erlaubt.length > 0, "die Seite steht in der Rechtetafel");
|
|
ok(erlaubt.includes("admin") && erlaubt.includes("hand"),
|
|
"DogFather und die rechte Hand dürfen sie öffnen");
|
|
ok(erlaubt.includes("modi"),
|
|
"ein Modi auch — er sieht dort seine eigene Bewegung");
|
|
/* GEGENPROBE: Die Agentur hat gar keine Entwicklungskarte. Eine Seite,
|
|
die man öffnen kann und die dann leer bleibt, sieht aus wie ein
|
|
Fehler -- und ist einer. */
|
|
ok(!erlaubt.includes("creator") && !erlaubt.includes("gast"),
|
|
"die Agentur und die Community nicht");
|
|
|
|
ok(json(await ruf("/workspace/api/werdegang/liste", CREW, "GET", { cookie: kDogi })).liste !== undefined,
|
|
"DogFather bekommt die Liste");
|
|
const beiModi = await ruf("/workspace/api/werdegang/liste", CREW, "GET", { cookie: kModi });
|
|
ok(beiModi.code === 404,
|
|
`ein Modi bekommt sie nicht (${beiModi.code} — nicht 403: wer „kein Zugriff“ liest, fragt als Nächstes, wer denn dann)`);
|
|
const fremdeKarte = await ruf(`/workspace/api/werdegang/person/${idHand}`, CREW, "GET", { cookie: kModi });
|
|
ok(fremdeKarte.code === 404, `und auch keine fremde Karte (${fremdeKarte.code})`);
|
|
const ohne = await ruf("/workspace/api/werdegang/liste", CREW, "GET", {});
|
|
ok(ohne.code === 401, `ohne Anmeldung gar nichts (${ohne.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. DIE SPUR — SIE MERKT SICH, WAS DIE TABELLE VERGISST
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Die Spur ===");
|
|
{
|
|
const p1 = PUNKTE[0].schluessel, p2 = PUNKTE[1].schluessel;
|
|
|
|
const a = await ruf(`/workspace/api/entwicklung/person/${idModi}/${p1}`, CREW, "PUT",
|
|
{ cookie: kDogi }, { stufe: "waechst" });
|
|
ok(a.code === 200, "eine Einschätzung lässt sich setzen");
|
|
|
|
const b = await ruf(`/workspace/api/entwicklung/person/${idModi}/${p1}`, CREW, "PUT",
|
|
{ cookie: kDogi }, { stufe: "laeuft" });
|
|
ok(b.code === 200, "und ändern");
|
|
|
|
const d = oeffne();
|
|
const spur = d.prepare(
|
|
"SELECT * FROM entwicklung_lauf WHERE person_id = ? ORDER BY id").all(idModi);
|
|
ok(spur.length === 2, `beide Schritte stehen in der Spur (${spur.length})`);
|
|
ok(spur[0].vorher === null && spur[0].stufe === "waechst",
|
|
"der erste hat kein „vorher“ — er war der erste");
|
|
ok(spur[1].vorher === "waechst" && spur[1].stufe === "laeuft",
|
|
"der zweite weiß, was vorher dastand (waechst → laeuft)");
|
|
ok(spur.every((z) => z.nachgetragen === 0),
|
|
"beide sind beobachtet, nicht nachgetragen");
|
|
|
|
/* ==== DIE WICHTIGSTE PRÜFUNG DER GANZEN DATEI =====================
|
|
|
|
KEIN ANLASS IN DER SPUR. Neben einem „da hakt es“ steht ein Satz,
|
|
woran man es gemerkt hat. Eine Chronik dieser Sätze wäre das
|
|
Belastendste, was dieses Haus speichern kann.
|
|
|
|
GEPRÜFT WIRD DIE TABELLE SELBST, nicht die Ausgabe: Eine Spalte,
|
|
die es gibt, wird eines Tages gefüllt.
|
|
============================================================== */
|
|
const spalten = d.prepare("PRAGMA table_info(entwicklung_lauf)").all().map((z) => z.name);
|
|
ok(!spalten.includes("anlass"),
|
|
`die Spur hat gar keine Spalte für den Anlass (${spalten.join(", ")})`);
|
|
|
|
/* Und der Anlass steht trotzdem dort, wo er hingehört -- sonst wäre
|
|
die Zeile oben nur deshalb grün, weil die Funktion gar nichts tut. */
|
|
await ruf(`/workspace/api/entwicklung/person/${idModi}/${p2}`, CREW, "PUT",
|
|
{ cookie: kDogi }, { stufe: "hakt", anlass: "Am Dienstag im Stream, zweimal nacheinander." });
|
|
const stand = d.prepare(
|
|
"SELECT anlass FROM entwicklung_stand WHERE person_id = ? AND schluessel = ?")
|
|
.get(idModi, p2);
|
|
ok(/Dienstag/.test(stand?.anlass || ""),
|
|
"im Katalog steht er weiterhin — dort gehört er hin");
|
|
const inSpur = d.prepare(
|
|
"SELECT COUNT(*) AS n FROM entwicklung_lauf WHERE person_id = ? AND schluessel = ?")
|
|
.get(idModi, p2).n;
|
|
ok(inSpur === 1, "die Spur kennt den Punkt, nicht den Satz dazu");
|
|
|
|
/* BLOCK 5 DARF NIE HIERHERKOMMEN. „Wie geht es dir damit?“ beantwortet
|
|
nur die Person selbst; diese Antworten haben eine eigene Tabelle,
|
|
die niemand sonst liest. */
|
|
const selbst = SELBST[0].schluessel;
|
|
await ruf(`/workspace/api/entwicklung/mich/${selbst}`, CREW, "PUT",
|
|
{ cookie: kModi }, { stufe: "hakt" });
|
|
const inSpur2 = d.prepare(
|
|
"SELECT COUNT(*) AS n FROM entwicklung_lauf WHERE schluessel = ?").get(selbst).n;
|
|
ok(inSpur2 === 0, "eine Selbstauskunft aus Block 5 landet NICHT in der Spur");
|
|
/* Gegenprobe: sie ist trotzdem gespeichert worden. */
|
|
const selbstStand = d.prepare(
|
|
"SELECT stufe FROM entwicklung_stand WHERE person_id = ? AND schluessel = ?")
|
|
.get(idModi, selbst);
|
|
ok(selbstStand?.stufe === "hakt",
|
|
"sie steht aber sehr wohl im Katalog — die Zeile darüber ist keine Ausrede");
|
|
d.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. DIE ÜBERGÄNGE HEISSEN RICHTIG
|
|
|
|
Reine Rechnung, ohne Server -- und mit Gegenprobe: Wer nur prüft,
|
|
dass „vor“ herauskommt, wenn „vor“ gemeint ist, hätte auch mit einer
|
|
Funktion recht, die IMMER „vor“ sagt.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Die Übergänge ===");
|
|
{
|
|
ok(W.artVon(null, "laeuft", 0) === "neu", "zum ersten Mal gesetzt → neu");
|
|
ok(W.artVon("hakt", "waechst", 0) === "vor", "hakt → wächst ist ein Schritt nach vorn");
|
|
ok(W.artVon("waechst", "laeuft", 0) === "vor", "wächst → läuft auch");
|
|
ok(W.artVon("laeuft", "hakt", 0) === "zurueck", "läuft → hakt ist einer zurück");
|
|
ok(W.artVon("unklar", "laeuft", 0) === "geklaert",
|
|
"aus „kann ich nicht sagen“ wird etwas → geklärt, nicht „vor“");
|
|
ok(W.artVon("laeuft", "unklar", 0) === "offen",
|
|
"und umgekehrt → offen, nicht „zurück“");
|
|
ok(W.artVon("laeuft", "laeuft", 0) === "gleich", "derselbe Wert ist keine Bewegung");
|
|
ok(W.artVon(null, "laeuft", 1) === "anfang",
|
|
"die Startaufnahme ist der Nullpunkt, nicht ein Ereignis");
|
|
|
|
/* GEGENPROBE zur Rangfolge: „kann ich nicht sagen“ hat KEINEN Rang.
|
|
Bekäme sie einen, würde „ich habe es nicht gesehen“ irgendwann wie
|
|
ein halbes „da hakt es“ gelesen. */
|
|
ok(W.artVon("unklar", "hakt", 0) === "geklaert",
|
|
"auch unklar → hakt ist geklärt und kein Rückschritt");
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. DIE MONATSSPALTEN — UND DIE FALLE AM MONATSENDE
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5. Die Monatsspalten ===");
|
|
{
|
|
const s = W.monatsspalten();
|
|
ok(s.length === W.MONATE, `${s.length} Spalten`);
|
|
ok(s.join(",") === [...s].sort().join(","), "älteste zuerst");
|
|
|
|
/* DIE FALLE: Wer am 31. März einen Monat abzieht, landet in
|
|
JavaScript wieder im März -- den 31. Februar gibt es nicht, und
|
|
das Datum rollt über. Eine Spalte fiele aus, und zwar still. */
|
|
const ende = W.monatsspalten(6, new Date("2026-03-31T12:00:00Z"));
|
|
ok(ende.join(" ") === "2025-10 2025-11 2025-12 2026-01 2026-02 2026-03",
|
|
`am 31. März stimmen alle sechs Monate (${ende.join(" ")})`);
|
|
ok(new Set(ende).size === 6, "kein Monat doppelt, keiner übersprungen");
|
|
|
|
/* UND DIE ZWEITE FALLE: toISOString rechnet in UTC. Zwischen
|
|
Mitternacht und zwei Uhr am Monatsersten liefert das den VORMONAT.
|
|
Gerechnet wird deshalb in Europe/Berlin. */
|
|
ok(W.monatVon("2026-04-01T00:30:00+02:00") === "2026-04",
|
|
"am 1. April um 00:30 Ortszeit ist es April, nicht März");
|
|
ok(W.monatVon("2026-03-31T23:30:00+02:00") === "2026-03",
|
|
"und am 31. März um 23:30 noch März");
|
|
ok(W.monatVon("kaputt") === null, "ein unlesbares Datum ergibt null, nicht „jetzt“");
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. DIE AUSWERTUNG — UND DER DRITTE AUSGANG
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 6. Die Auswertung ===");
|
|
{
|
|
const d = json(await ruf(`/workspace/api/werdegang/person/${idModi}`, CREW, "GET", { cookie: kDogi }));
|
|
ok(d.person?.name === "Diene", "die Karte gehört der richtigen Person");
|
|
ok(d.gesetzt === 2, `zwei Punkte sind beantwortet (${d.gesetzt})`);
|
|
ok(d.gesamt === PUNKTE.length,
|
|
`von ${PUNKTE.length} im Katalog (${d.gesamt}) — gezählt, nicht abgeschrieben`);
|
|
|
|
/* DER DRITTE AUSGANG. Alles ist heute passiert, also gibt es genau
|
|
EINEN Monat mit Inhalt. Die Seite muss das SAGEN statt eine Linie
|
|
zwischen einem Wert und dem Rand zu zeichnen. */
|
|
ok(d.verlauf_moeglich === false,
|
|
"mit einem einzigen Monat wird kein Verlauf behauptet");
|
|
ok(d.monate_mit_inhalt === 1 && d.verlauf_ab === W.VERLAUF_AB,
|
|
`und die Antwort sagt, warum (${d.monate_mit_inhalt} von ${d.verlauf_ab} nötigen Monaten)`);
|
|
|
|
/* KEINE NOTE. Nirgends im ganzen Datenpaket darf eine Punktzahl, ein
|
|
Prozentwert oder ein Durchschnitt stehen. Geprüft am ROHTEXT --
|
|
ein Feld, das ich hier namentlich abfrage, müsste ich kennen; ein
|
|
neues, das jemand später hinzufügt, fiele durch. */
|
|
const roh = JSON.stringify(d).toLowerCase();
|
|
ok(!/"(punktzahl|score|prozent|durchschnitt|note|rang|platz)"/.test(roh),
|
|
"kein Feld heißt Punktzahl, Prozent, Durchschnitt, Note oder Rang");
|
|
ok(!/anlass/.test(roh), "und der Anlass kommt nicht mit");
|
|
ok(!roh.includes("dienstag"),
|
|
"auch nicht sein Inhalt — die Zeile darüber ist keine Wortklauberei");
|
|
|
|
/* KEIN FREMDER NAME. Auf einer Karte steht genau ein Mensch. */
|
|
ok(!roh.includes("rieke") && !roh.includes("tili"),
|
|
"auf der Karte steht kein anderer Name");
|
|
|
|
ok(typeof d.satz === "string" && d.satz.length > 20,
|
|
"ein Satz fasst die Karte zusammen");
|
|
ok(Array.isArray(d.texte), "und es gibt Texte dazu");
|
|
const arten = d.texte.map((t) => t.art);
|
|
ok(arten.includes("offen"),
|
|
"darunter der Hinweis, worüber die Auswertung schweigt");
|
|
/* DIE REIHENFOLGE IST DIE AUSSAGE: Was sich bewegt hat und was trägt,
|
|
steht VOR dem, was liegt. Rückmeldung, die auf Defizite zeigt,
|
|
senkt die Leistung messbar. */
|
|
const iVor = arten.indexOf("vor"), iLiegt = arten.indexOf("liegt");
|
|
ok(iLiegt === -1 || iVor === -1 || iVor < iLiegt,
|
|
"und was sich bewegt hat steht vor dem, was liegt");
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. ZWEI MONATE — JETZT ENTSTEHT EIN VERLAUF
|
|
|
|
GEGENPROBE zu Abschnitt 6: Ohne sie wäre „kein Verlauf möglich“ nur
|
|
deshalb grün, weil die Funktion vielleicht NIE einen Verlauf sieht.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 7. Zwei Monate ===");
|
|
{
|
|
/* Eine Zeile mit einem Datum aus dem Vormonat -- von Hand in die
|
|
Spur, weil man die Uhr nicht vorstellen kann. Der Weg dorthin ist
|
|
derselbe, den die Anwendung benutzt. */
|
|
const d = oeffne();
|
|
const vormonat = new Date();
|
|
vormonat.setDate(15);
|
|
vormonat.setMonth(vormonat.getMonth() - 1);
|
|
const p3 = PUNKTE[2].schluessel;
|
|
d.prepare(`INSERT INTO entwicklung_lauf
|
|
(person_id, schluessel, von_id, stufe, vorher, nachgetragen, am)
|
|
VALUES (?,?,?,?,NULL,0,?)`)
|
|
.run(idModi, p3, idDogi, "hakt", vormonat.toISOString());
|
|
d.prepare(`INSERT INTO entwicklung_stand
|
|
(person_id, schluessel, von_id, stufe, anlass, am)
|
|
VALUES (?,?,?,?,NULL,?)`)
|
|
.run(idModi, p3, idDogi, "hakt", vormonat.toISOString());
|
|
d.close();
|
|
|
|
const k = json(await ruf(`/workspace/api/werdegang/person/${idModi}`, CREW, "GET", { cookie: kDogi }));
|
|
ok(k.verlauf_moeglich === true, "mit zwei Monaten gibt es einen Verlauf");
|
|
ok(k.spalten.length === W.MONATE, `${k.spalten.length} Monatsspalten`);
|
|
ok(k.streifen.length === 3, `drei Punkte im Streifen (${k.streifen.length})`);
|
|
|
|
/* FORTGESCHRIEBEN, NICHT LÜCKENHAFT: Was im Vormonat galt, gilt auch
|
|
diesen Monat -- niemand hat es geändert. Eine leere Zelle hieße
|
|
„keine Einschätzung“, und das wäre falsch. */
|
|
const zeile = k.streifen.find((z) => z.schluessel === p3);
|
|
const letzte = zeile.zellen.at(-1);
|
|
ok(letzte.stufe === "hakt",
|
|
"eine Einschätzung aus dem Vormonat gilt auch diesen Monat weiter");
|
|
ok(zeile.zellen.filter((c) => c.stufe).length === 2,
|
|
"davor steht nichts — vor der ersten Einschätzung gab es keine");
|
|
|
|
/* DIE BEWEGUNG ZÄHLT DAS RICHTIGE. */
|
|
const summe = (f) => k.bewegung.reduce((s, m) => s + m[f], 0);
|
|
ok(summe("neu") === 3, `drei Punkte wurden zum ersten Mal gesetzt (${summe("neu")})`);
|
|
ok(summe("vor") === 1, `einer ging nach vorn (${summe("vor")})`);
|
|
ok(summe("zurueck") === 0, `keiner zurück (${summe("zurueck")})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
8. DIE STARTAUFNAHME ZÄHLT NICHT ALS BEWEGUNG
|
|
|
|
Der Tag, an dem die Spur entstand, war kein Tag, an dem 43 Dinge
|
|
gleichzeitig passiert sind.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 8. Die Startaufnahme ===");
|
|
{
|
|
const d = oeffne();
|
|
const p4 = PUNKTE[3].schluessel;
|
|
const alt = new Date(); alt.setDate(15); alt.setMonth(alt.getMonth() - 2);
|
|
d.prepare(`INSERT INTO entwicklung_lauf
|
|
(person_id, schluessel, von_id, stufe, vorher, nachgetragen, am)
|
|
VALUES (?,?,?,?,NULL,1,?)`)
|
|
.run(idHand, p4, idDogi, "laeuft", alt.toISOString());
|
|
d.prepare(`INSERT INTO entwicklung_stand
|
|
(person_id, schluessel, von_id, stufe, anlass, am)
|
|
VALUES (?,?,?,?,NULL,?)`)
|
|
.run(idHand, p4, idDogi, "laeuft", alt.toISOString());
|
|
d.close();
|
|
|
|
const k = json(await ruf(`/workspace/api/werdegang/person/${idHand}`, CREW, "GET", { cookie: kDogi }));
|
|
const summe = (f) => k.bewegung.reduce((s, m) => s + m[f], 0);
|
|
ok(summe("neu") + summe("vor") + summe("zurueck") + summe("geklaert") === 0,
|
|
"eine nachgetragene Zeile ist keine Bewegung");
|
|
/* Gegenprobe: sie ist trotzdem da -- im Streifen. Sonst wäre die
|
|
Zeile oben nur deshalb grün, weil gar nichts gespeichert wurde. */
|
|
ok(k.streifen.length === 1 && k.streifen[0].zellen.some((c) => c.stufe === "laeuft"),
|
|
"im Streifen steht sie sehr wohl");
|
|
ok(/nichts bewegt/.test(k.satz),
|
|
`und der Satz sagt es ehrlich: ${(k.satz || "").slice(0, 60)}…`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
9. DIE EIGENE SICHT — OHNE NAMEN, OHNE ANLASS, OHNE STREIFEN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 9. Die eigene Sicht ===");
|
|
{
|
|
const m = json(await ruf("/workspace/api/werdegang/mich", CREW, "GET", { cookie: kModi }));
|
|
ok(m.hat_karte === true, "ein Modi hat eine eigene Karte");
|
|
ok(Array.isArray(m.bewegung) && m.bewegung.length === W.MONATE,
|
|
"und sieht seine Bewegung über dieselben Monate");
|
|
ok(m.streifen === undefined,
|
|
"aber KEINEN Streifen — welcher Punkt wackelte, sagt ihm nur ein Mensch");
|
|
const roh = JSON.stringify(m).toLowerCase();
|
|
ok(!roh.includes("filipe") && !roh.includes("rieke"),
|
|
"kein Name eines Beurteilers");
|
|
ok(!roh.includes("dienstag"), "kein Anlasstext");
|
|
ok(/frag/i.test(m.hinweis || ""),
|
|
"stattdessen der Hinweis, danach zu fragen");
|
|
|
|
/* Seine eigene Antwort aus Block 5 darf hier nicht auftauchen -- sie
|
|
hat er über SICH gegeben, nicht jemand über ihn. */
|
|
ok(m.bewegung.reduce((s, x) => s + x.neu, 0) === 3,
|
|
"gezählt werden nur die Einschätzungen anderer");
|
|
|
|
/* GEGENPROBE: DogFather hat keine Karte -- und bekommt einen Satz
|
|
statt einer leeren Seite. */
|
|
const beiDogi = json(await ruf("/workspace/api/werdegang/mich", CREW, "GET", { cookie: kDogi }));
|
|
ok(beiDogi.hat_karte === false, "für DogFather wird keine Karte geführt");
|
|
}
|
|
|
|
/* =======================================================================
|
|
10. DIE LISTE VERGLEICHT NIEMANDEN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 10. Die Liste ===");
|
|
{
|
|
const l = json(await ruf("/workspace/api/werdegang/liste", CREW, "GET", { cookie: kDogi }));
|
|
ok(l.liste.length === 2, `zwei Karten — die rechte Hand und der Modi (${l.liste.length})`);
|
|
/* GEGENPROBE: Ein Creator steht NICHT drin. Für die Agentur wird
|
|
keine Entwicklungskarte geführt. */
|
|
ok(!l.liste.some((p) => p.name === "Tili"), "ein Creator steht nicht darin");
|
|
ok(!l.liste.some((p) => p.name === "Filipe"), "DogFather auch nicht");
|
|
|
|
/* NACH NAMEN SORTIERT, NICHT NACH AKTIVITÄT. Eine Liste, die die mit
|
|
der meisten Bewegung nach oben stellt, ist eine Rangliste mit einem
|
|
anderen Namen. */
|
|
const namen = l.liste.map((p) => p.name);
|
|
ok(namen.join(",") === [...namen].sort((a, b) => a.localeCompare(b, "de")).join(","),
|
|
`alphabetisch (${namen.join(", ")})`);
|
|
ok(/kein vergleich/i.test(l.hinweis || ""),
|
|
"und die Seite sagt selbst, dass sie niemanden vergleicht");
|
|
}
|
|
|
|
/* =======================================================================
|
|
11. IM BROWSER
|
|
|
|
Der Teil, den keine Schnittstellenprüfung ersetzt: Zeichnet die Seite
|
|
wirklich etwas, und bleibt die Konsole still?
|
|
|
|
UND DER NEBENBEFUND VOM 19.09.: Bis heute bekamen elf Seiten von
|
|
Team Dogi KEINE Kachelfarbe, weil `zuSeite()` nur die Agenturliste
|
|
kennt. Hier wird nachgemessen, dass diese Seite ihre bekommt.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 11. Im Browser ===");
|
|
{
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text()); });
|
|
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
|
|
|
await kontext.addCookies(kDogi.split("; ").filter(Boolean).map((teil) => {
|
|
const i = teil.indexOf("=");
|
|
return { name: teil.slice(0, i), value: teil.slice(i + 1), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/werdegang.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(900);
|
|
|
|
const liste = await seite.evaluate(() => ({
|
|
karten: document.querySelectorAll('.w-karte').length,
|
|
hinweis: document.getElementById('liste-hinweis')?.textContent || '',
|
|
ton: document.body.dataset.ton || null,
|
|
buehne: document.body.dataset.buehne || null,
|
|
zeichen: !!document.querySelector('.kopf-zeichen'),
|
|
}));
|
|
ok(liste.karten === 2, `zwei Karten stehen auf der Seite (${liste.karten})`);
|
|
ok(/kein vergleich/i.test(liste.hinweis), "der Hinweis steht sichtbar darüber");
|
|
ok(liste.ton === "40",
|
|
`die Seite trägt die Farbe ihrer Kachel (data-ton=${liste.ton}) — das ging bis heute nur auf den Agenturseiten`);
|
|
ok(liste.buehne === "wald", `und ihre Bühne (${liste.buehne})`);
|
|
ok(liste.zeichen, "das Zeichen der Kachel steht im Kopf");
|
|
|
|
await seite.locator('.w-karte').first().click();
|
|
await seite.waitForTimeout(700);
|
|
|
|
const karte = await seite.evaluate(() => ({
|
|
name: document.getElementById('karte-name')?.textContent || '',
|
|
balken: document.querySelectorAll('.w-bewegung__balken').length,
|
|
zellen: document.querySelectorAll('.w-streifen__zelle').length,
|
|
zeichenInZellen: [...document.querySelectorAll('.w-streifen__zelle')]
|
|
.every((z) => (z.textContent || '').trim().length > 0),
|
|
legende: document.querySelectorAll('.w-legende__punkt').length,
|
|
texte: document.querySelectorAll('.w-text').length,
|
|
breiter: document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
|
}));
|
|
ok(/Diene|Rieke/.test(karte.name), `eine Karte öffnet sich (${karte.name})`);
|
|
ok(karte.zellen > 0, `der Streifen ist gezeichnet (${karte.zellen} Zellen)`);
|
|
/* DIE FARBE IST NIE DIE AUSKUNFT. Jede Zelle trägt ihr Zeichen als
|
|
Text -- wer Farben schlecht unterscheidet, liest dasselbe. */
|
|
ok(karte.zeichenInZellen, "jede Zelle trägt ihr Zeichen, nicht nur ihre Farbe");
|
|
ok(karte.legende === 2, `unter dem Diagramm steht eine Legende (${karte.legende} Einträge)`);
|
|
ok(karte.texte > 0, `und die Texte dazu (${karte.texte})`);
|
|
ok(!karte.breiter, "nichts steht über den rechten Rand hinaus");
|
|
|
|
/* HANDY. Zwölf Seiten hatten am 06.09. genau hier ihren Fehler: ein
|
|
Knopf lag über einem anderen, weil eine feste Schwelle nicht mehr
|
|
passte. */
|
|
await seite.setViewportSize({ width: 412, height: 915 });
|
|
await seite.waitForTimeout(500);
|
|
const handy = await seite.evaluate(() => ({
|
|
breiter: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
|
|
balkenSichtbar: [...document.querySelectorAll('.w-bewegung__balken')]
|
|
.every((b) => b.getBoundingClientRect().width >= 8),
|
|
}));
|
|
ok(!handy.breiter, "auf 412 px steht nichts über den Rand hinaus");
|
|
ok(handy.balkenSichtbar, "und die Balken sind noch breit genug, um sie zu sehen");
|
|
|
|
if (process.env.SCHIRM) {
|
|
await seite.setViewportSize({ width: 1280, height: 1100 });
|
|
await seite.waitForTimeout(400);
|
|
await seite.screenshot({
|
|
path: "C:/Users/qciga/Documents/Obelix/DogiHompage/pruef-werdegang.png",
|
|
fullPage: true,
|
|
});
|
|
}
|
|
ok(konsole.length === 0,
|
|
konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 120) : "keine Fehler im Browser");
|
|
await browser.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
12. DIE BEIDEN ROLLENLISTEN DÜRFEN NICHT AUSEINANDERLAUFEN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 12. Was zusammengehört ===");
|
|
{
|
|
const hier = W.KARTEN_ROLLEN.join(",");
|
|
const quelle = readFileSync(
|
|
new URL("./workspace-entwicklung.js", import.meta.url), "utf8");
|
|
const dort = (quelle.match(/const KARTEN_ROLLEN = \[([^\]]+)\]/) || [])[1] || "";
|
|
const sauber = dort.replace(/["\s]/g, "");
|
|
ok(sauber === hier,
|
|
`dieselben Rollen in beiden Dateien (${hier} / ${sauber})`);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------- */
|
|
melde("");
|
|
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
|
|
process.exit(fehler ? 1 : 0);
|