Filipe: "ich will dass diese zwei kategorien perfektionniert werden, ich
will dass fertige aufgaben da stehen und so ... so dass dogfather und
rechte hand es so einfach wie möglich haben aber so professionell wie
auch nur möglich."
Bis heute waren beide Kacheln ein leeres Formular: Titel tippen, Art
wählen, Text schreiben. Ein leeres Feld beantwortet keine Frage — es
stellt sie noch einmal.
ENTWICKLUNG: 34 Beobachtungen in fünf Blöcken, vier Antworten je Zeile
(Läuft · Wächst gerade · Da hakt es · Kann ich nicht sagen). Es sind
BEOBACHTUNGEN, keine Eigenschaften — "Absagen kommen rechtzeitig" statt
"ist zuverlässig". Eine Eigenschaft kann man nur bestätigen oder
bestreiten; ein Verhalten kann man gesehen haben oder nicht.
TALENTE: 21 Merkmale in fünf Gruppen, 6 Warnzeichen getrennt geführt,
fünf Trichterstufen mit je GENAU EINEM nächsten Schritt. Eine Liste
wächst und wird nie wieder angesehen; ein Trichter fragt bei jeder Karte
"und jetzt?". Die Standzeit steht dabei — "seit 41 Tagen beobachtet"
ist eine Auskunft über uns, nicht über den Kandidaten.
VIER RIEGEL, JEDER MIT GEGENPROBE:
· Block 5 ("Wie geht es dir damit?") gehört der Person — lesend UND
schreibend, auch vor DogFather. Er kommt gar nicht erst aus dem
Server; Schreiben gibt dasselbe 404 wie ein erfundener Schlüssel.
Nach außen dringt eine Ampel OHNE Namen, und die schweigt, solange
das Team so klein ist, dass "jemandem geht es zu viel" dasselbe wäre
wie ein Name.
· Der Stand des anderen kommt erst nach dem eigenen Klick — sonst
klickt man dasselbe. Sichtbar ist nur, DASS es einen gibt: sonst
sähen "noch niemand" und "du darfst es nicht sehen" gleich aus.
· "Da hakt es" ohne Anlass wird abgelehnt. "Läuft" nicht — 28
Pflichtfelder je Person wären das Gegenteil von "so einfach wie
möglich".
· Talente gehört DogFather und der rechten Hand. Ein Modi bekommt 404
auf Seite UND Schnittstelle.
WAS NICHT GEBAUT IST, UND WARUM: keine Punktzahl, kein Durchschnitt,
keine Rangliste, keine Gesamtnote. Eine Zahl über einem Menschen wird
gelesen, verglichen und weitererzählt. Und kein Rot — "da hakt es" ist
eine Beobachtung, kein Alarm.
DER PLAN LAG AN EINER STELLE FALSCH, gemessen statt vermutet: Er sagte
"keine neue Tabelle, der Stand liegt in punkt_stand". Dort ist der
Schlüssel (bereich, schluessel, creator_id) — genau EIN Stand je Punkt
und Person. Zwei unabhängige Augenpaare passen da nicht hinein, ohne
eine lebende Tabelle neu zu bauen. Also eine eigene.
EIN RÜCKSCHLAG, DEN NUR DIE PRÜFUNG GEFUNDEN HAT: Die Liste der
Bretter, die ein Modi oder die rechte Hand öffnen darf, wird aus den
KACHELZIELEN abgeleitet. Als die Kacheln auf die neuen Seiten zeigten,
fielen beide Bretter heraus — und die Seite verlinkte auf ein 404. Kein
Fehler im Riegel, sondern in seiner Quelle.
Dazu zwei kleinere Funde vom Hinsehen statt vom Messen: Die Ampel sagte
"Solange ihr zu 1 seid" (jetzt drei Sätze für drei Lagen), und die
Fußzeile der Talente-Seite lag auf der Bühne — die Kontrastprüfung hatte
sie übersprungen, weil ein Link darin steht. Beide behoben, und die
Prüfung hat den stillen Aussetzer gleich mit verloren.
Geprüft: rollen 314 · start-ansicht 147 · crew-adresse 132 · neue-seiten
70 · nachwuchs 69 · entwicklung 40 · css-klassen 30 · zwischenspeicher
21 · alle-wege 19 · rechtetafel 19 — alles grün.
Was noch fehlt, steht in der Notiz, Abschnitt 9: die eigene Karte für
die Person, der Übergang "Probe" → Zugang, der Vorlagentext zum
Ansprechen.
Co-Authored-By: Claude Opus 5 <[email protected]>
463 lines
24 KiB
JavaScript
463 lines
24 KiB
JavaScript
/* =====================================================================
|
||
Entwicklung & Nachwuchs — die Riegel (11.09.2026)
|
||
|
||
Der Bereich hat vier Regeln, und jede einzelne wäre ohne Prüfung
|
||
eine Zusage statt einer Eigenschaft:
|
||
|
||
1. Block 5 gehört der Person. DogFather sieht ihn nicht — lesend
|
||
UND schreibend. Mit Gegenprobe: die Person selbst sieht ihn.
|
||
2. Der Stand des anderen kommt erst nach dem eigenen Klick. Mit
|
||
Gegenprobe: danach kommt er auch wirklich.
|
||
3. „Da hakt es" ohne Anlass wird abgelehnt. Mit Gegenprobe: mit
|
||
Anlass geht es durch.
|
||
4. Talente gehört DogFather und der rechten Hand. Ein Modi bekommt
|
||
404 — und zwar auf Seite UND Schnittstelle.
|
||
|
||
Dazu der Katalog selbst (Schlüssel, Stufen, keine Dopplungen) und
|
||
das Verblassen nach 90 Tagen — von beiden Seiten der Grenze, sonst
|
||
beweist es nichts.
|
||
|
||
Aufrufen mit: node server/pruef-nachwuchs.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 { portMussFreiSein } from "./helfer-port.mjs";
|
||
|
||
const PORT = await portMussFreiSein(4404, "pruef-nachwuchs");
|
||
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-nachwuchs-"));
|
||
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-nachwuchs");
|
||
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 { DatabaseSync } = await import("node:sqlite");
|
||
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
||
const kat = await import("./workspace-entwicklungs-punkte.js");
|
||
const tal = await import("./workspace-talent-punkte.js");
|
||
const rechte = await import("./rechte.js");
|
||
|
||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
const jetzt = new Date().toISOString();
|
||
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);
|
||
function 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;
|
||
}
|
||
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||
const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
|
||
const idModi = anlegen("Frida", "modi", "CODE-MODI-0001");
|
||
anlegen("Nina", "modi", "CODE-MODI-0002");
|
||
anlegen("Mara", "manager", "CODE-MANA-0001");
|
||
|
||
/* Ein Kandidat im Talente-Brett -- ohne ihn waere jede Zeile ueber den
|
||
Trichter ein gruener Haken ueber der Null. */
|
||
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)"
|
||
+ " VALUES ('talente','ruhig','Kessi aus dem Chat','Faellt seit Wochen auf.',?,?,?,?)")
|
||
.run(jetzt.slice(0, 10), jetzt, idDogi, "Im Stream am Dienstag");
|
||
const idTalent = d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
d.close();
|
||
|
||
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) {
|
||
const r = await roh("/workspace/api/anmelden", host, {}, { rolle, code });
|
||
return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
|
||
}
|
||
const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS);
|
||
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. DER KATALOG SELBST
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 1. Die zwei Kataloge ===");
|
||
{
|
||
const alle = kat.ENTWICKLUNG_ALLE;
|
||
ok(alle.length > 20, `${alle.length} Beobachtungen im Katalog`);
|
||
ok(new Set(alle.map((p) => p.schluessel)).size === alle.length,
|
||
"kein Schlüssel doppelt");
|
||
ok(alle.every((p) => p.schluessel.startsWith("e-")),
|
||
"jeder Schlüssel beginnt mit e- (keine Kollision mit der Checkliste)");
|
||
ok(alle.every((p) => p.titel && p.text && p.stufe),
|
||
"jede Beobachtung hat Titel, Erklärung und Erwartungsstufe");
|
||
const stufen = new Set(alle.map((p) => p.stufe));
|
||
ok(kat.ENTWICKLUNG_ERWARTUNG.every((e) => stufen.has(e.schluessel)),
|
||
`alle drei Erwartungsstufen kommen vor (${[...stufen].join(", ")})`);
|
||
|
||
/* DER RIEGEL IST EINE EIGENSCHAFT DES KATALOGS, nicht nur des
|
||
Servers: Genau ein Block ist `nurSelbst`, und die abgeleiteten
|
||
Listen passen dazu. Verrutscht das hier, greift der Server
|
||
weiterhin -- aber die Oberflaeche zeigte etwas anderes an. */
|
||
const selbstBloecke = kat.ENTWICKLUNG_BLOECKE.filter((b) => b.nurSelbst);
|
||
ok(selbstBloecke.length === 1, `genau ein Block gehört der Person selbst (${selbstBloecke.length})`);
|
||
ok(kat.ENTWICKLUNG_FREMD.length + kat.ENTWICKLUNG_SELBST.length === alle.length,
|
||
`${kat.ENTWICKLUNG_FREMD.length} fremd + ${kat.ENTWICKLUNG_SELBST.length} selbst = ${alle.length}`);
|
||
ok(kat.ENTWICKLUNG_SELBST.length > 0 && kat.ENTWICKLUNG_FREMD.length > 0,
|
||
"beide Listen haben Inhalt — sonst prüft die Zeile darüber nichts");
|
||
|
||
const merk = tal.TALENT_ALLE;
|
||
ok(merk.length > 15, `${merk.length} Merkmale im Talente-Katalog`);
|
||
ok(tal.TALENT_WARNZEICHEN.length > 0, `${tal.TALENT_WARNZEICHEN.length} Warnzeichen, getrennt geführt`);
|
||
ok(tal.TALENT_NACH_SCHLUESSEL.size === merk.length + tal.TALENT_WARNZEICHEN.length,
|
||
"kein Schlüssel doppelt zwischen Merkmalen und Warnzeichen");
|
||
ok([...merk, ...tal.TALENT_WARNZEICHEN].every((m) => m.schluessel.startsWith("t-")),
|
||
"jeder Schlüssel beginnt mit t-");
|
||
|
||
const raenge = tal.TALENT_STUFEN.map((s) => s.rang);
|
||
ok(JSON.stringify(raenge) === JSON.stringify([...raenge].sort((a, b) => a - b)),
|
||
`die fünf Stufen stehen in der richtigen Reihenfolge (${tal.TALENT_STUFEN.map((s) => s.name).join(" → ")})`);
|
||
ok(tal.TALENT_STUFEN.slice(0, -1).every((s) => s.knopf && s.schritt),
|
||
"jede Stufe außer der letzten hat GENAU EINEN nächsten Schritt mit Knopf");
|
||
ok(tal.talentNaechste("imteam") === null, "und hinter der letzten kommt nichts mehr");
|
||
ok(tal.talentStufe("gibt-es-nicht") === null,
|
||
"eine erfundene Stufe ergibt null statt der ersten — sie soll auffallen");
|
||
}
|
||
|
||
/* =======================================================================
|
||
2. BLOCK 5 GEHÖRT DER PERSON
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 2. Block 5 ===");
|
||
{
|
||
const selbst = kat.ENTWICKLUNG_SELBST[0].schluessel;
|
||
|
||
const karte = await roh(`/workspace/api/entwicklung/person/${idModi}`, HAUS, { cookie: kDogi });
|
||
const bloecke = json(karte).bloecke || [];
|
||
const alleSchluessel = bloecke.flatMap((b) => (b.punkte || []).map((p) => p.schluessel));
|
||
ok(karte.code === 200 && alleSchluessel.length > 0,
|
||
`DogFather bekommt die Karte des Modis (${alleSchluessel.length} Punkte)`);
|
||
const durch = kat.ENTWICKLUNG_SELBST.filter((p) => alleSchluessel.includes(p.schluessel));
|
||
ok(durch.length === 0,
|
||
`und KEINEN Punkt aus Block 5${durch.length ? " -- DURCH: " + durch.map((p) => p.schluessel).join(", ") : ""}`);
|
||
ok(!/Wie geht es dir/.test(karte.text), "auch der Blockname steht nicht in der Antwort");
|
||
|
||
/* Und schreiben geht auch nicht -- mit derselben Antwort wie ein
|
||
erfundener Schluessel, damit man an der Antwort nichts ablesen kann. */
|
||
const schreib = await roh(`/workspace/api/entwicklung/person/${idModi}/${selbst}`, HAUS,
|
||
{ cookie: kDogi }, { stufe: "laeuft" }, "PUT");
|
||
const erfunden = await roh(`/workspace/api/entwicklung/person/${idModi}/e-gibt-es-nicht`, HAUS,
|
||
{ cookie: kDogi }, { stufe: "laeuft" }, "PUT");
|
||
ok(schreib.code === 404, `DogFather kann dort auch nichts setzen (${schreib.code})`);
|
||
ok(schreib.code === erfunden.code,
|
||
`dieselbe Antwort wie bei einem erfundenen Punkt (${schreib.code}/${erfunden.code})`);
|
||
|
||
/* GEGENPROBE: Die Person selbst sieht ihn sehr wohl. Ohne diese
|
||
Zeile waeren die drei darueber auch dann gruen, wenn Block 5 fuer
|
||
NIEMANDEN existierte. */
|
||
const meins = await roh("/workspace/api/entwicklung/mich", CREW, { cookie: kModi });
|
||
const meine = (json(meins).punkte || []).map((p) => p.schluessel);
|
||
ok(meins.code === 200 && meine.length === kat.ENTWICKLUNG_SELBST.length,
|
||
`der Modi selbst sieht alle ${meine.length} Fragen`);
|
||
|
||
const setzen = await roh(`/workspace/api/entwicklung/mich/${selbst}`, CREW,
|
||
{ cookie: kModi }, { stufe: "hakt" }, "PUT");
|
||
ok(setzen.code === 200, `und kann sie beantworten (${setzen.code})`);
|
||
|
||
/* Und danach steht sie immer noch nicht in DogFathers Karte. */
|
||
const nachher = await roh(`/workspace/api/entwicklung/person/${idModi}`, HAUS, { cookie: kDogi });
|
||
ok(!nachher.text.includes(selbst),
|
||
"auch nach der Antwort steht sie nicht in DogFathers Karte");
|
||
|
||
/* Und in die andere Richtung: ueber jemand ANDEREN kann man dort
|
||
nichts eintragen -- es gibt gar keine Nummer in der Adresse. */
|
||
const fremdPunkt = await roh(`/workspace/api/entwicklung/mich/${kat.ENTWICKLUNG_FREMD[0].schluessel}`,
|
||
CREW, { cookie: kModi }, { stufe: "laeuft" }, "PUT");
|
||
ok(fremdPunkt.code === 404,
|
||
`über „mich" lässt sich kein fremder Punkt setzen (${fremdPunkt.code})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
3. DIE AMPEL VERRÄT KEINEN NAMEN
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 3. Die Ampel ===");
|
||
{
|
||
/* BEIDE SEITEN DER GRENZE, sonst beweist es nichts.
|
||
|
||
Die Ampel schweigt, solange das Team so klein ist, dass „jemandem
|
||
geht es gerade zu viel" dasselbe waere wie ein Name. Eine Pruefung,
|
||
die nur eine Seite ansieht, waere auch dann gruen, wenn die Ampel
|
||
IMMER schwiege -- oder immer redete. Beides waere ein Fehler, und
|
||
keiner davon faellt von allein auf.
|
||
|
||
Die Schwelle wird nicht abgeschrieben, sondern aus dem Verhalten
|
||
abgeleitet: Erst mit vollem Team messen, dann eine Person
|
||
abschalten und noch einmal. Was dazwischen kippt, IST die Grenze. */
|
||
const voll = json(await roh("/workspace/api/entwicklung/ampel", HAUS, { cookie: kDogi }));
|
||
ok(voll.team >= 3 && voll.sagbar === true,
|
||
`bei ${voll.team} Personen sagt die Ampel etwas (${voll.lage})`);
|
||
ok(typeof voll.geantwortet === "number" && voll.geantwortet >= 1,
|
||
`und zaehlt, wie viele geantwortet haben (${voll.geantwortet} von ${voll.team})`);
|
||
/* Ein Modi hat in Abschnitt 2 „hakt" gesetzt -- die Ampel muss das
|
||
sehen, ohne zu sagen, wer es war. */
|
||
ok(voll.lage === "jemand",
|
||
`sie meldet die Lage aus Abschnitt 2 (${voll.lage})`);
|
||
ok(!/Frida|Nina|Rieke|Filipe/.test(JSON.stringify(voll)),
|
||
"und in der ganzen Antwort steht kein einziger Name");
|
||
|
||
/* Jetzt eine Person abschalten -- das Team faellt unter die Schwelle. */
|
||
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
dd.prepare("UPDATE personen SET aktiv = 0 WHERE name = 'Nina'").run();
|
||
dd.close();
|
||
|
||
const klein = json(await roh("/workspace/api/entwicklung/ampel", HAUS, { cookie: kDogi }));
|
||
ok(klein.team === voll.team - 1 && klein.sagbar === false,
|
||
`bei ${klein.team} Personen schweigt sie`);
|
||
ok(/Name/.test(klein.grund || ""),
|
||
"und sagt, warum – ein stilles Nichts sieht aus wie ein Fehler");
|
||
ok(!/Frida|Nina|Rieke|Filipe/.test(JSON.stringify(klein)),
|
||
"auch im schweigenden Fall steht kein Name da");
|
||
|
||
const dd2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
dd2.prepare("UPDATE personen SET aktiv = 1 WHERE name = 'Nina'").run();
|
||
dd2.close();
|
||
|
||
const modi = await roh("/workspace/api/entwicklung/ampel", CREW, { cookie: kModi });
|
||
ok(modi.code === 404, `ein Modi bekommt sie gar nicht (${modi.code})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
4. DER STAND DES ANDEREN KOMMT ERST NACH DEM EIGENEN
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 4. Zwei Augenpaare ===");
|
||
{
|
||
const punkt = kat.ENTWICKLUNG_FREMD[0].schluessel;
|
||
|
||
const gesetzt = await roh(`/workspace/api/entwicklung/person/${idModi}/${punkt}`, CREW,
|
||
{ cookie: kHand }, { stufe: "laeuft", anlass: "Gestern im Chat gesehen." }, "PUT");
|
||
ok(gesetzt.code === 200, `die rechte Hand setzt einen Punkt (${gesetzt.code})`);
|
||
|
||
const vorher = await roh(`/workspace/api/entwicklung/person/${idModi}`, HAUS, { cookie: kDogi });
|
||
const p1 = (json(vorher).bloecke || []).flatMap((b) => b.punkte || [])
|
||
.find((p) => p.schluessel === punkt);
|
||
ok(p1 && p1.andere === null,
|
||
"DogFather sieht ihren Stand NICHT, bevor er selbst gesetzt hat");
|
||
ok(p1 && p1.andere_offen === 1,
|
||
`aber er sieht, DASS es einen gibt (${p1?.andere_offen}) – sonst wäre „noch niemand" dasselbe Bild`);
|
||
ok(!vorher.text.includes("Gestern im Chat gesehen"),
|
||
"und ihr Anlasstext wird gar nicht erst ausgeliefert");
|
||
|
||
const eigen = await roh(`/workspace/api/entwicklung/person/${idModi}/${punkt}`, HAUS,
|
||
{ cookie: kDogi }, { stufe: "waechst" }, "PUT");
|
||
ok(eigen.code === 200, `DogFather setzt selbst (${eigen.code})`);
|
||
|
||
/* GEGENPROBE: jetzt kommt er. */
|
||
const nachher = await roh(`/workspace/api/entwicklung/person/${idModi}`, HAUS, { cookie: kDogi });
|
||
const p2 = (json(nachher).bloecke || []).flatMap((b) => b.punkte || [])
|
||
.find((p) => p.schluessel === punkt);
|
||
ok(p2 && p2.andere?.length === 1 && p2.andere[0].stufe === "laeuft",
|
||
`danach sieht er ihn (${p2?.andere?.[0]?.stufe})`);
|
||
|
||
/* UND DIE ABWEICHUNG IST DAS ERGEBNIS. */
|
||
const ab = json(nachher).abweichungen || [];
|
||
ok(ab.length === 1 && ab[0].schluessel === punkt,
|
||
`die Abweichung steht oben in der Karte (${ab.length})`);
|
||
ok(ab[0].meins === "waechst" && ab[0].andere === "laeuft",
|
||
`mit beiden Ständen nebeneinander (${ab[0]?.meins} / ${ab[0]?.andere})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
5. „DA HAKT ES" BRAUCHT EINEN ANLASS
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 5. Kein Klick ohne Anlass ===");
|
||
{
|
||
const punkt = kat.ENTWICKLUNG_FREMD[1].schluessel;
|
||
const ohne = await roh(`/workspace/api/entwicklung/person/${idModi}/${punkt}`, HAUS,
|
||
{ cookie: kDogi }, { stufe: "hakt", anlass: "" }, "PUT");
|
||
ok(ohne.code === 400, `„Da hakt es" ohne Anlass wird abgelehnt (${ohne.code})`);
|
||
ok(/gemerkt|Satz/.test(json(ohne).fehler || ""),
|
||
"und die Absage sagt, was fehlt, statt nur nein");
|
||
|
||
const mit = await roh(`/workspace/api/entwicklung/person/${idModi}/${punkt}`, HAUS,
|
||
{ cookie: kDogi }, { stufe: "hakt", anlass: "Am Samstag zweimal nicht reagiert." }, "PUT");
|
||
ok(mit.code === 200, `mit Anlass geht es durch (${mit.code})`);
|
||
|
||
/* GEGENPROBE: „Läuft" braucht keinen — sonst wären es 28 Pflichtfelder
|
||
je Person, und „so einfach wie möglich" war die halbe Aufgabe. */
|
||
const laeuft = await roh(`/workspace/api/entwicklung/person/${idModi}/${kat.ENTWICKLUNG_FREMD[2].schluessel}`,
|
||
HAUS, { cookie: kDogi }, { stufe: "laeuft" }, "PUT");
|
||
ok(laeuft.code === 200, `„Läuft" geht ohne Anlass (${laeuft.code})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
6. NACH 90 TAGEN VERBLASST EINE EINSCHÄTZUNG
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 6. Verblassen ===");
|
||
{
|
||
const modul = await import("./workspace-entwicklung.js");
|
||
const alt = kat.ENTWICKLUNG_FREMD[3].schluessel;
|
||
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
dd.prepare(`INSERT INTO entwicklung_stand (person_id, schluessel, von_id, stufe, anlass, am)
|
||
VALUES (?,?,?,'laeuft',NULL,?)`)
|
||
.run(idModi, alt, idDogi,
|
||
new Date(Date.now() - (modul.VERBLASST_NACH_TAGEN + 10) * 86400000).toISOString());
|
||
dd.close();
|
||
|
||
const r = await roh(`/workspace/api/entwicklung/person/${idModi}`, HAUS, { cookie: kDogi });
|
||
const punkte = (json(r).bloecke || []).flatMap((b) => b.punkte || []);
|
||
const alterPunkt = punkte.find((p) => p.schluessel === alt);
|
||
ok(alterPunkt?.meins?.verblasst === true,
|
||
`ein Stand von vor ${modul.VERBLASST_NACH_TAGEN + 10} Tagen gilt als verblasst`);
|
||
|
||
/* GEGENPROBE, und ohne sie beweist die Zeile darüber nichts: Einer
|
||
von heute gilt NICHT als verblasst. */
|
||
const frisch = punkte.find((p) => p.schluessel === kat.ENTWICKLUNG_FREMD[0].schluessel);
|
||
ok(frisch?.meins?.verblasst === false,
|
||
"einer von heute nicht");
|
||
|
||
const lage = json(await roh("/workspace/api/entwicklung/lage", HAUS, { cookie: kDogi }));
|
||
const zeile = (lage.personen || []).find((p) => p.id === idModi);
|
||
ok(zeile?.veraltet === 1, `und die Übersicht zählt ihn (${zeile?.veraltet})`);
|
||
ok(zeile?.abweichungen === 1, `samt der Abweichung (${zeile?.abweichungen})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
7. TALENTE GEHÖRT DOGFATHER UND DER RECHTEN HAND
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 7. Talente ===");
|
||
{
|
||
const lage = await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi });
|
||
const r = json(lage);
|
||
ok(lage.code === 200 && (r.kandidaten || []).length === 1,
|
||
`DogFather sieht den Trichter (${(r.kandidaten || []).length} Kandidat)`);
|
||
ok((r.trichter || []).length === tal.TALENT_STUFEN.length,
|
||
`alle ${r.trichter?.length} Stufen kommen mit`);
|
||
const k = r.kandidaten[0];
|
||
ok(k.stufe === "aufgefallen",
|
||
`ein neuer Kandidat startet auf „${k.stufe_name}" (${k.stufe})`);
|
||
ok(!!k.schritt && !!k.knopf, `und hat genau einen nächsten Schritt: „${k.knopf}"`);
|
||
|
||
/* DER RIEGEL. Auf Seite UND Schnittstelle -- eine Kachel, die es
|
||
nicht gibt, ist keine Sicherung. */
|
||
const modiApi = await roh("/workspace/api/talente/lage", CREW, { cookie: kModi });
|
||
ok(modiApi.code === 404, `ein Modi bekommt die Schnittstelle nicht (${modiApi.code})`);
|
||
const modiSeite = await roh("/workspace/talente.html", CREW, { cookie: kModi });
|
||
ok(modiSeite.code !== 200, `und die Seite auch nicht (${modiSeite.code})`);
|
||
const wer = rechte.SEITEN["/workspace/talente.html"] || [];
|
||
ok(wer.length === 2 && wer.includes("admin") && wer.includes("hand"),
|
||
`die Tafel gibt sie genau an ${wer.join(" und ")}`);
|
||
|
||
/* GEGENPROBE: die rechte Hand kommt hinein. */
|
||
const handApi = await roh("/workspace/api/talente/lage", CREW, { cookie: kHand });
|
||
ok(handApi.code === 200, `die rechte Hand schon (${handApi.code})`);
|
||
|
||
/* Merkmale: ein Klick setzt, der zweite nimmt zurück. */
|
||
const m = tal.TALENT_ALLE[0].schluessel;
|
||
const w = tal.TALENT_WARNZEICHEN[0].schluessel;
|
||
const eins = await roh(`/workspace/api/talente/${idTalent}/merkmal/${m}`, HAUS,
|
||
{ cookie: kDogi }, {}, "PUT");
|
||
ok(eins.code === 200 && json(eins).gesetzt === true, "ein Merkmal wird gesetzt");
|
||
const zwei = await roh(`/workspace/api/talente/${idTalent}/merkmal/${m}`, HAUS,
|
||
{ cookie: kDogi }, {}, "PUT");
|
||
ok(zwei.code === 200 && json(zwei).gesetzt === false, "und der zweite Klick nimmt es zurück");
|
||
await roh(`/workspace/api/talente/${idTalent}/merkmal/${m}`, HAUS, { cookie: kDogi }, {}, "PUT");
|
||
await roh(`/workspace/api/talente/${idTalent}/merkmal/${w}`, HAUS, { cookie: kDogi }, {}, "PUT");
|
||
|
||
const nach = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })).kandidaten[0];
|
||
ok(nach.merkmale.length === 1 && nach.warnzeichen.length === 1,
|
||
`Merkmale und Warnzeichen stehen getrennt (${nach.merkmale.length} / ${nach.warnzeichen.length})`);
|
||
ok(!nach.merkmale.includes(w),
|
||
"ein Warnzeichen landet nicht zwischen den Merkmalen");
|
||
|
||
/* Die Stufe wandert -- und die Standzeit beginnt von vorn. */
|
||
const weiter = await roh(`/workspace/api/talente/${idTalent}/stufe`, HAUS,
|
||
{ cookie: kDogi }, { stufe: "beobachtet" }, "PUT");
|
||
ok(weiter.code === 200, `ein Schritt weiter im Trichter (${weiter.code})`);
|
||
const nach2 = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })).kandidaten[0];
|
||
ok(nach2.stufe === "beobachtet" && nach2.tage === 0,
|
||
`Stufe „${nach2.stufe_name}", Standzeit beginnt bei ${nach2.tage}`);
|
||
ok(nach2.liegt === false, "und liegt damit auch nicht");
|
||
|
||
const erfunden = await roh(`/workspace/api/talente/${idTalent}/stufe`, HAUS,
|
||
{ cookie: kDogi }, { stufe: "chef" }, "PUT");
|
||
ok(erfunden.code === 400, `eine erfundene Stufe wird abgelehnt (${erfunden.code})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
8. LIEGENGEBLIEBEN IST EINE AUSKUNFT ÜBER UNS
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 8. Standzeit ===");
|
||
{
|
||
const stufe = tal.talentStufe("beobachtet");
|
||
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
dd.prepare("UPDATE talent_stufe SET seit = ? WHERE eintrag_id = ?")
|
||
.run(new Date(Date.now() - (stufe.frist + 11) * 86400000).toISOString(), idTalent);
|
||
dd.close();
|
||
|
||
const r = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
|
||
const k = r.kandidaten[0];
|
||
ok(k.liegt === true && k.tage === stufe.frist + 11,
|
||
`nach ${k.tage} Tagen auf einer Stufe mit Frist ${stufe.frist} gilt die Karte als liegengeblieben`);
|
||
const t = (r.trichter || []).find((x) => x.schluessel === "beobachtet");
|
||
ok(t?.liegt === 1, `und der Trichter zählt sie (${t?.liegt})`);
|
||
|
||
/* GEGENPROBE von der anderen Seite der Grenze -- sonst wäre „liegt"
|
||
auch dann grün, wenn es immer liegt. */
|
||
const dd2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
dd2.prepare("UPDATE talent_stufe SET seit = ? WHERE eintrag_id = ?")
|
||
.run(new Date(Date.now() - (stufe.frist - 1) * 86400000).toISOString(), idTalent);
|
||
dd2.close();
|
||
const r2 = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
|
||
ok(r2.kandidaten[0].liegt === false,
|
||
`einen Tag vor der Frist noch nicht (${r2.kandidaten[0].tage} Tage)`);
|
||
}
|
||
|
||
/* ------------------------------------------------------------------- */
|
||
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);
|