Files
dogfather-universe/server/pruef-befinden.mjs
T
DogFatherGit 3a49e30aa4 Die Probezeit hat einen Inhalt -- und man sieht, wer was gesehen hat
Filipe: "wie talente bewertet werden, aufgaben bekommen, analysiert
werden von vanvan und mir, alles moegliche, wie gesagt ich will hoch
profissionel arbeiten."

WAS GEMESSEN FEHLTE

  Die Stufe "Probe" hatte eine Frist von 30 Tagen, einen Buddy, ein
  Datum -- und einen Satz: "Nach 30 Tagen: uebernehmen oder sauber
  beenden." Worauf diese Entscheidung sich gruenden soll, stand
  nirgends. Eine Probezeit ohne Inhalt ist keine Probe, sondern eine
  Wartezeit; am dreissigsten Tag entscheidet dann das Bauchgefuehl
  dessen, der zuletzt etwas gesehen hat.

  Und: `von_id` stand seit dem Bau in talent_merkmal und wurde nie
  ausgeliefert. Man sah, DASS ein Merkmal gesetzt ist -- nicht, von
  wem. Bei zwei Leuten, die unabhaengig beobachten, ist genau das die
  Auskunft.

DER PROBEPLAN

  Zwoelf Schritte in drei Phasen: mitlaufen -> selbst machen -> allein.
  Die Reihenfolge ist der Punkt (Discord empfiehlt sie so, in der
  Ausbildung heisst sie "see one, do one"). Wer die mittlere Stufe
  ueberspringt, hat am Tag 30 zwei Sorten Kandidaten, die man nicht
  unterscheiden kann.

  JEDER SCHRITT HAT EINEN ZUSTAENDIGEN -- sechs das Talent, drei der
  Buddy, drei die Leitung. Ohne diese Angabe wandern in jeder Probezeit
  dieselben drei an niemanden: Der Buddy denkt, die Leitung macht es.

  KEINE PUNKTZAHL. Am Ende stehen drei Fragen, die ein Mensch
  beantwortet; die Haken sagen nur, ob er genug gesehen hat. Und
  "entscheidbar" haengt an DREI Schritten, nicht an einer Anzahl: Wer
  nie allein gearbeitet hat und wen das Team nie beurteilt hat, ueber
  den weiss man nichts, egal wie viele andere Haken stehen. Neun von
  zwoelf reichen nicht, drei genuegen -- wenn es die richtigen sind.

WAS DAS HINSEHEN GEFUNDEN HAT

  `.t-schritt` gibt es auf dieser Seite schon -- es ist die Zeile mit
  dem naechsten Schritt der Trichterstufe. Meine Regeln standen
  SPAETER in derselben Datei und haetten sie ueberschrieben. Gefunden
  hat das keine Ueberlegung, sondern das Nachzaehlen im Browser: 13
  Elemente mit `.t-schritt`, obwohl der Plan zwoelf hat.

ZWEI PRUEFUNGEN WAREN SCHON VORHER ROT (gemessen mit git stash)

  pruef-schritt (6 rot) und pruef-befinden (4 rot) -- seit den
  Erweiterungen von heute frueh: aus sechs Befinden-Fragen wurden
  zwoelf, aus vier Kategorien sechs, und die Personenkachel zaehlt seit
  heute in Kaesten statt in einem Satz. Die Zahlen standen als feste
  Werte in den Pruefdateien.

  Nicht die Zahlen angepasst, sondern ABGELEITET: aus
  ENTWICKLUNG_BLOECKE und ENTWICKLUNG_PUNKTE.befinden. Eine feste Zahl
  ist eine Rechnung von gestern; wer sie nachtraegt, traegt sie beim
  naechsten Mal wieder nach.

GEPRUEFT

  pruef-nachwuchs 123 -> 152. Vier Gegenproben, jede trifft ihr Ziel:
  Liste auch vor der Probe -> 1 rot, Abhaken ohne Stufenpruefung -> 7
  rot, "entscheidbar" nach Anzahl statt nach den drei -> 3 rot, Name
  nicht mitgeliefert -> 1 rot.

  Dazu gruen: schritt 65, befinden 75, entwicklung 43, uebergang,
  neue-seiten, rechtetafel, css-klassen, struktur, deutsche-texte,
  modi-wortleck.
2026-09-17 16:15:21 +02:00

533 lines
26 KiB
JavaScript

/* =====================================================================
Wie geht's dir? (15.09.2026)
Filipe, mit dem Bildschirmfoto der Kachel: „ich will dass du diese
seite perfektionnierst, denn gerade wenn ich drauf drücke geht die
entwicklungsseite auf."
---------------------------------------------------------------------
DER FEHLER WAR SCHLIMMER ALS EIN FALSCHER VERWEIS
Auf der Kachel steht „die Antworten sieht nur du" — und sie öffnete
eine Seite voller Namen und Beobachtungen über ANDERE Menschen. Die
erste Prüfung hier ist deshalb: Führt die Kachel dorthin, wo sie
hinführt zu führen verspricht?
---------------------------------------------------------------------
UND DIE EIGENTLICHE FRAGE: BLEIBT ES PRIVAT?
Ein Verlauf über das Befinden eines Menschen ist das Empfindlichste,
was dieses Haus speichert. Er existiert nur, weil er IHM hilft. Also:
Sieht DogFather ihn? (darf nicht)
Steht etwas davon im Protokoll? (nur DASS, nie WAS)
Ändert er die Ampel? (darf nicht)
Aufrufen mit: node server/pruef-befinden.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 { portMussFreiSein } from "./helfer-port.mjs";
const PORT = await portMussFreiSein(4419, "pruef-befinden");
const ordner = mkdtempSync(join(tmpdir(), "ws-bef-"));
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-befinden");
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);
/* WIE VIELE FRAGEN ES GIBT -- aus dem Katalog, nicht abgeschrieben.
Vier Zeilen dieser Datei nannten frueher feste Zahlen ("2 Runden x 6",
"zwoelf Antworten", "alle sechs Fragen"). Am 17.09.2026 wurden aus
sechs Fragen zwoelf, und sie waren ab da rot, ohne dass irgendetwas
kaputt war. Eine feste Zahl ist eine Rechnung von gestern. */
const anzahlFragen = (await import("./workspace-entwicklungs-punkte.js"))
.ENTWICKLUNG_PUNKTE.befinden.length;
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 modul = await import("./workspace-befinden.js");
const kat = await import("./workspace-entwicklungs-punkte.js");
const rechte = await import("./rechte.js");
const oeffne = () => new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
let idDogi, idFrida;
{
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");
idFrida = anlegen("Frida", "modi", "CODE-MODI-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("; ");
}
/* DOGFATHER AUF DER TEAM-ADRESSE.
Nicht aus Bequemlichkeit: Auf workspace. liefert der Server ihm
`bereiche: null` -- der Browser nimmt dort seine eigene Liste, und
die kennt diese Kachel gar nicht. Die Kachel, die Filipe im
Bildschirmfoto angetippt hat, steht auf crew. Wer sie auf der
falschen Adresse sucht, prueft an ihr vorbei. */
const kDogi = await anmelden("admin", "CODE-DOGI-0001", CREW);
const kFrida = await anmelden("modi", "CODE-MODI-0001", CREW);
ok(!!kDogi && !!kFrida, "DogFather und ein Modi sind angemeldet");
/* =======================================================================
1. DIE KACHEL HÄLT, WAS SIE VERSPRICHT
Der gemeldete Fehler. Er war schlimmer als ein falscher Verweis: Auf
der Kachel steht „die Antworten sieht nur du" -- und sie oeffnete
eine Seite voller Namen anderer Menschen.
======================================================================= */
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 kachel = alle.find((k) => /Wie geht/i.test(k.name || ""));
ok(!!kachel, `die Kachel „${kachel?.name}“ steht bei DogFather`);
ok(kachel?.ziel === "befinden.html",
`und zeigt auf die eigene Seite (${kachel?.ziel})`);
/* GEGENPROBE -- ohne sie beweist die Zeile nichts: Die Kachel
„Entwicklung" zeigt weiterhin auf die Entwicklungsseite. */
const andere = alle.find((k) => k.name === "Entwicklung");
ok(andere?.ziel === "entwicklung.html",
`die Kachel „Entwicklung“ zeigt weiterhin dorthin (${andere?.ziel})`);
ok((rechte.tafelJetzt()["/workspace/befinden.html"] || []).includes("modi"),
"die Seite steht in der Rechtetafel und ein Modi darf sie öffnen");
ok(!(rechte.tafelJetzt()["/workspace/befinden.html"] || []).includes("creator"),
"die Agentur nicht — sie hat auch keine Entwicklungskarte");
const beiFrida = json(await ruf("/workspace/api/ich", CREW, "GET", { cookie: kFrida }));
const ihre = [...(beiFrida.bereiche || []), ...(beiFrida.bereiche_zusatz || [])]
.find((k) => /Wie geht/i.test(k.name || ""));
ok(ihre?.ziel === "befinden.html", "und bei einem Modi führt sie ebenfalls dorthin");
}
/* =======================================================================
2. SECHS FRAGEN, EIN RHYTHMUS
======================================================================= */
melde("");
melde("=== 2. Sechs Fragen, ein Rhythmus ===");
{
const d = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida }));
ok(d.punkte?.length === kat.ENTWICKLUNG_SELBST.length,
`${d.punkte?.length} Fragen — so viele wie im Katalog`);
/* Die Recherche: fuenf bis fuenfzehn, unter zwei Minuten. Sechs sind
richtig; zwanzig waeren es nicht. */
ok(d.punkte.length >= 5 && d.punkte.length <= 15,
"zwischen fünf und fünfzehn — kurz genug, dass man sie wirklich beantwortet");
ok(d.rhythmus_tage === modul.RHYTHMUS_TAGE,
`alle ${d.rhythmus_tage} Tage wird wieder gefragt`);
ok(d.rhythmus?.faellig === true, "am Anfang ist sofort etwas fällig");
ok(d.rhythmus?.letzte === null, "und es gab noch keine Runde");
ok(Array.isArray(d.verlauf) && d.verlauf.length === 0, "der Verlauf ist leer");
/* EINE HALBE RUNDE IST KEINE RUNDE. */
const halb = await ruf("/workspace/api/befinden/runde", CREW, "POST", { cookie: kFrida }, {});
ok(halb.code === 400, `ohne Antworten lässt sich nichts festhalten (${halb.code})`);
ok(json(halb).fehler === "unvollstaendig"
&& json(halb).offen === d.punkte.length,
`und die Absage sagt, wie viele fehlen (${json(halb).offen} von ${json(halb).gesamt})`);
}
/* =======================================================================
3. EINE RUNDE, EIN VERLAUF
======================================================================= */
melde("");
melde("=== 3. Der Verlauf entsteht ===");
{
const d = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida }));
for (const p of d.punkte) {
await ruf(`/workspace/api/entwicklung/mich/${p.schluessel}`, CREW, "PUT",
{ cookie: kFrida }, { stufe: "laeuft" });
}
const erste = await ruf("/workspace/api/befinden/runde", CREW, "POST", { cookie: kFrida }, {});
ok(erste.code === 201, `mit allen Antworten geht es (${erste.code})`);
ok(json(erste).anzahl === d.punkte.length,
`${json(erste).anzahl} Antworten sind in den Verlauf gewandert`);
const nach = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida }));
ok(nach.verlauf.length === 1, `der Verlauf hat ${nach.verlauf.length} Runde`);
ok(nach.rhythmus.faellig === false,
"und es ist erst einmal nichts mehr fällig — die Seite fragt nicht jeden Tag");
ok(nach.rhythmus.vollstaendig === true, "die Runde gilt als vollständig");
/* Eine zweite Runde mit anderen Antworten -- erst dann gibt es ein
Muster, und erst dann ist der Verlauf ueberhaupt etwas wert. */
await ruf(`/workspace/api/entwicklung/mich/${d.punkte[0].schluessel}`, CREW, "PUT",
{ cookie: kFrida }, { stufe: "hakt", anlass: "Diese Woche war viel los." });
const zweite = await ruf("/workspace/api/befinden/runde", CREW, "POST", { cookie: kFrida }, {});
ok(zweite.code === 201, `eine zweite Runde geht auch sofort (${zweite.code})`);
const nach2 = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida }));
ok(nach2.verlauf.length === 2, `jetzt ${nach2.verlauf.length} Runden`);
const neu = nach2.verlauf[0].antworten[d.punkte[0].schluessel];
const alt = nach2.verlauf[1].antworten[d.punkte[0].schluessel];
ok(neu === "hakt" && alt === "laeuft",
`und man sieht die Veränderung (${alt} → ${neu})`);
/* DIE ANTWORTEN DER ALTEN RUNDE BLEIBEN, WIE SIE WAREN. Ein Verlauf,
der sich rueckwirkend aendert, ist kein Verlauf. */
ok(Object.keys(nach2.verlauf[1].antworten).length === d.punkte.length,
"die alte Runde steht unverändert da");
}
/* =======================================================================
4. ES BLEIBT PRIVAT
Die eigentliche Frage. Ein Verlauf ueber das Befinden eines Menschen
ist das Empfindlichste, was dieses Haus speichert.
======================================================================= */
melde("");
melde("=== 4. Privat ===");
{
/* DogFather bekommt SEINEN Verlauf, nicht ihren. */
const seiner = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kDogi }));
ok(seiner.verlauf.length === 0,
`DogFather hat einen eigenen, leeren Verlauf (${seiner.verlauf.length})`);
/* Es gibt gar keinen Weg zu einem fremden. */
const fremd = await ruf(`/workspace/api/befinden?person=${idFrida}`, CREW, "GET", { cookie: kDogi });
ok(json(fremd).verlauf?.length === 0,
"auch mit einer fremden Nummer in der Adresse bekommt er nur seinen eigenen");
/* Im Protokoll steht nur, DASS eine Runde war -- nie was darin stand. */
const d = oeffne();
const spur = d.prepare(
"SELECT detail FROM protokoll WHERE aktion = 'befinden_runde' ORDER BY id DESC").all();
const verlaufZeilen = d.prepare("SELECT COUNT(*) AS n FROM befinden_verlauf").get().n;
d.close();
ok(spur.length === 2, `${spur.length} Runden stehen im Protokoll`);
ok(spur.every((z) => !/laeuft|hakt|waechst|unklar/.test(z.detail || "")),
`aber keine einzige Antwort darin (${spur[0]?.detail})`);
/* DIE ZAHL WIRD GERECHNET, NICHT ABGESCHRIEBEN.
Hier stand `=== 12` mit dem Kommentar "2 Runden x 6". Am
17.09.2026 wurden aus sechs Fragen zwoelf, und drei Zeilen in
dieser Datei waren ab da rot, ohne dass irgendetwas kaputt war.
Eine feste Zahl ist eine Rechnung von gestern. */
ok(verlaufZeilen === 2 * anzahlFragen,
`im Verlauf liegen ${verlaufZeilen} Antworten (2 Runden x ${anzahlFragen})`);
/* UND DIE AMPEL ZAEHLT WEITER NUR DEN AKTUELLEN STAND, ohne Namen. */
const a = await ruf("/workspace/api/entwicklung/ampel", CREW, "GET", { cookie: kDogi });
ok(a.code === 200, `die Ampel antwortet (${a.code})`);
ok(!/Frida/.test(a.text), "und nennt keinen Namen");
}
/* =======================================================================
5. DIE ENTWICKLUNGSSEITE TUT NUR NOCH EINE SACHE
======================================================================= */
melde("");
melde("=== 5. Eine Seite, ein Zweck ===");
{
const e = readFileSync(new URL("../workspace/entwicklung.html", import.meta.url), "utf8");
const b = readFileSync(new URL("../workspace/befinden.html", import.meta.url), "utf8");
ok(/href="befinden\.html"/.test(e),
"die Entwicklungsseite verweist auf die sechs Fragen");
ok(!/id="meins"/.test(e),
"und zeichnet sie nicht mehr selbst — zwei Stände wären zwei Wahrheiten");
ok(/id="fragen"/.test(b) && /id="verlauf"/.test(b),
"die neue Seite hat die Fragen und den Verlauf");
/* AUF DER EIGENEN SEITE STEHT KEIN FREMDER NAME -- nicht einmal ein
Platz dafuer. */
ok(!/personen-block|meine-karte|abweichung/.test(b),
"und keinen einzigen Abschnitt über andere Menschen");
ok(/befinden\.js/.test(b) && !/entwicklung\.js/.test(b),
"sie lädt ihr eigenes Skript, nicht das der Entwicklungsseite");
}
/* =======================================================================
6. IM BROWSER
======================================================================= */
melde("");
melde("=== 6. 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));
/* ALS MODI, NICHT ALS DOGFATHER: Die Seite gehoert dem Menschen, der
sie oeffnet -- und der haeufigste Fall ist ein Modi. Der Keks kommt
von der Anmeldung oben, weil Playwright die Host-Kopfzeile beim
Seitenaufruf nicht setzen kann. */
await kontext.addCookies(kFrida.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/start.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(700);
/* DER GEMELDETE FEHLER, IM BROWSER NACHGESTELLT: auf die Kachel
druecken und nachsehen, wo man landet. */
const ziel = await seite.evaluate(() => {
const k = [...document.querySelectorAll('a')]
.find((a) => /Wie geht/i.test(a.textContent || ''));
return k ? k.getAttribute('href') : null;
});
ok(/befinden\.html/.test(ziel || ""),
`die Kachel auf der Startseite führt zu ${ziel}`);
await seite.goto(`http://127.0.0.1:${PORT}/workspace/befinden.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
const stand = await seite.evaluate(() => ({
fragen: document.querySelectorAll('.e-punkt--meins').length,
knoepfe: document.querySelectorAll('.e-wahl__knopf').length,
takt: document.getElementById('takt')?.hidden === false,
taktText: document.getElementById('takt-satz')?.textContent.trim() || '',
verlauf: document.getElementById('verlauf-block')?.hidden === false,
spalten: document.querySelectorAll('.b-verlauf__zeile--kopf .b-verlauf__datum').length,
punkte: document.querySelectorAll('.b-verlauf__punkt').length,
text: document.querySelector('main')?.innerText || '',
}));
ok(stand.fragen >= 5, `${stand.fragen} Fragen stehen auf der Seite`);
ok(stand.knoepfe === stand.fragen * 4, `mit je vier Knöpfen (${stand.knoepfe})`);
ok(stand.takt === true, `der Rhythmus steht oben: „${stand.taktText.slice(0, 70)}“`);
/* FRIDA HAT ZWEI RUNDEN -- bei ihr MUSS er dastehen. */
ok(stand.verlauf === true, "ihr Verlauf steht auf der Seite");
ok(stand.spalten === 2, `mit zwei Ständen nebeneinander (${stand.spalten})`);
ok(stand.punkte === 2 * anzahlFragen,
`und ${2 * anzahlFragen} Antworten darin (${stand.punkte})`);
/* KEIN FREMDER NAME AUF DEM BILDSCHIRM. Ihr eigener darf oben in der
Kopfleiste stehen -- gemessen wird deshalb nur der Inhalt, und
gesucht wird nach DogFather, also nach dem, der hier nichts zu
suchen hat. */
ok(!/Filipe/.test(stand.text), "und kein anderer Mensch steht darauf");
/* GEGENPROBE mit einem zweiten Menschen: DogFather hat keine einzige
Runde. Bei ihm muss derselbe Block WEG sein -- sonst hiesse „er
erscheint ab der zweiten Runde" nur, dass er immer erscheint. */
const zweiter = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
await zweiter.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: "/" };
}));
const seite2 = await zweiter.newPage();
await seite2.goto(`http://127.0.0.1:${PORT}/workspace/befinden.html`, { waitUntil: "networkidle" });
await seite2.waitForTimeout(800);
const ohne = await seite2.evaluate(() => ({
fragen: document.querySelectorAll('.e-punkt--meins').length,
verlauf: document.getElementById('verlauf-block')?.hidden === false,
knopf: document.getElementById('takt-knopf')?.hidden === false,
}));
ok(ohne.fragen >= 5, `auch bei ihm stehen die Fragen da (${ohne.fragen})`);
ok(ohne.verlauf === false, "aber kein Verlauf — er hat noch keine Runde");
ok(ohne.knopf === false,
"und kein Knopf zum Abschließen, solange nichts beantwortet ist");
ok(konsole.length === 0,
konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 90) : "keine Fehler im Browser");
await browser.close();
}
/* =======================================================================
7. WAS DEIN VERLAUF SAGT
Filipe: „mach mir auch eine analyse, mach eine perfektion draus. eine
professionelle auch mit infos und so aus aller welt was tiktok
angeht."
Eine Auswertung über das eigene Befinden ist das Empfindlichste, was
dieses Haus rechnet. Also dieselben drei Fragen wie beim Verlauf:
Sagt sie das Richtige? Sagt sie es nur IHR? Und sagt sie gar nichts,
wenn sie noch nichts sagen kann?
======================================================================= */
melde("");
melde("=== 7. Die Analyse ===");
{
const holen = async (keks, host) =>
json(await ruf("/workspace/api/befinden", host, "GET", { cookie: keks }));
/* Stand nach Abschnitt 3: zwei Runden. In der zweiten hakt der erste
Punkt, vorher lief er -- das ist „neu", noch kein Muster. */
const d = await holen(kFrida, CREW);
const erste = d.punkte[0].schluessel;
ok(d.analyse?.moeglich === true, `mit ${d.analyse?.runden} Runden gibt es eine Analyse`);
ok(d.analyse.zeilen.length === d.punkte.length,
`sie geht alle ${d.analyse.zeilen.length} Fragen durch, nicht nur die schlechten`);
const z1 = d.analyse.zeilen.find((z) => z.schluessel === erste);
ok(z1.art === "neu", `der frisch hakende Punkt gilt als neu, nicht als Muster (${z1.art})`);
ok(/noch kein Muster/i.test(d.analyse.satz),
`und der Satz oben sagt genau das: „${d.analyse.satz.slice(0, 60)}…“`);
/* DIE EINORDNUNG IST DER TEIL, DER „AUS ALLER WELT" HEISST. Sie steht
bei JEDER Zeile -- auch bei denen, die laufen. */
ok(d.analyse.zeilen.every((z) => z.welt && z.welt.length > 40),
"zu jeder der sechs Fragen steht ein belegter Befund");
ok(new Set(d.analyse.zeilen.map((z) => z.welt)).size === d.punkte.length,
"und jeder ist ein anderer — kein Satz, der zu allen passen würde");
/* „Was hilft" NUR dort, wo es etwas zu tun gibt. */
ok(d.analyse.zeilen.filter((z) => z.hilft).length === 1,
`ein Rat steht nur an der Stelle, an der es hakt (${d.analyse.zeilen.filter((z) => z.hilft).length})`);
/* ---- Und jetzt dasselbe ein zweites Mal: daraus wird ein Muster ---- */
for (const p of d.punkte) {
await ruf(`/workspace/api/entwicklung/mich/${p.schluessel}`, CREW, "PUT",
{ cookie: kFrida }, p.schluessel === erste
? { stufe: "hakt", anlass: "Immer noch dasselbe." } : { stufe: "laeuft" });
}
const dritte = await ruf("/workspace/api/befinden/runde", CREW, "POST", { cookie: kFrida }, {});
ok(dritte.code === 201, `eine dritte Runde (${dritte.code})`);
const n = await holen(kFrida, CREW);
const z2 = n.analyse.zeilen.find((z) => z.schluessel === erste);
ok(z2.art === "dauerhaft", `jetzt ist es ein Muster (${z2.art})`);
ok(z2.runden === modul.MUSTER_AB, `über ${z2.runden} Runden`);
ok(/kein schlechter Tag/i.test(n.analyse.satz),
`und der Satz sagt, worauf es ankommt: „${n.analyse.satz.slice(0, 64)}…“`);
/* DIE REIHENFOLGE MACHT DIE AUSSAGE -- das Muster gehört nach oben.
Sortiert wird im Browser; hier wird geprüft, dass die Angabe dafür
überhaupt mitkommt. */
ok(n.analyse.zahlen.dauerhaft === 1 && n.analyse.zahlen.neu === 0,
`die Zahlen daneben stimmen (${JSON.stringify(n.analyse.zahlen)})`);
/* ---- Sie gehört ihr allein ---- */
const seine = await holen(kDogi, CREW);
ok(seine.analyse?.moeglich === false,
"DogFather bekommt seine eigene — und die ist leer");
ok(!JSON.stringify(seine).includes("Immer noch dasselbe"),
"nichts von ihr steht darin");
/* ---- Und ohne zweite Runde gibt es sie gar nicht ---- */
ok(seine.analyse.ab === modul.MUSTER_AB,
`sie sagt, ab wann es sie gibt (ab ${seine.analyse.ab} Runden)`);
ok(Array.isArray(seine.analyse.zeilen) && seine.analyse.zeilen.length === 0,
"und liefert bis dahin keine einzige Zeile");
/* ---- Im Protokoll steht weiterhin nichts davon ---- */
const db = oeffne();
const spuren = db.prepare("SELECT aktion, detail FROM protokoll").all();
db.close();
ok(!spuren.some((x) => /analyse/i.test(x.aktion || "")),
"eine Analyse ist kein Vorgang — sie steht in keinem Protokoll");
ok(!spuren.some((x) => /Immer noch dasselbe/.test(x.detail || "")),
"und ihr Anlass erst recht nicht");
}
/* =======================================================================
8. DIE ANALYSE AUF DEM BILDSCHIRM
======================================================================= */
melde("");
melde("=== 8. Die Analyse 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 } });
await kontext.addCookies(kFrida.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: "/" };
}));
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 seite.goto(`http://127.0.0.1:${PORT}/workspace/befinden.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(1000);
const a = await seite.evaluate(() => ({
da: document.getElementById("analyse-block")?.hidden === false,
satz: document.getElementById("analyse-satz")?.textContent.trim() || "",
zeilen: document.querySelectorAll(".b-analyse__zeile").length,
ersteMarke: document.querySelector(".b-analyse__zeile")?.dataset.ton || "",
welt: document.querySelectorAll(".b-analyse__welt").length,
hilft: document.querySelectorAll(".b-analyse__hilft").length,
quellen: [...document.querySelectorAll(".b-analyse__quelle-hin")].map((x) => x.href),
blank: [...document.querySelectorAll(".b-analyse__quelle-hin")]
.every((x) => x.rel.includes("noopener")),
}));
ok(a.da, "die Analyse steht auf der Seite");
ok(a.zeilen === anzahlFragen,
`mit allen ${anzahlFragen} Fragen (${a.zeilen})`);
/* DAS MUSTER STEHT OBEN. Alphabetisch oder in Katalogreihenfolge
müsste man es suchen -- und wer sucht, liest es beim zweiten Mal
nicht mehr. */
ok(a.ersteMarke === "hakt",
`und das, was seit mehreren Runden hakt, steht zuoberst (${a.ersteMarke})`);
ok(a.welt === anzahlFragen,
`zu jeder der ${anzahlFragen} Zeilen die Einordnung (${a.welt})`);
ok(a.hilft === 1, `ein Rat, genau dort, wo es hakt (${a.hilft})`);
ok(a.quellen.length === 3, `${a.quellen.length} Quellen stehen darunter`);
ok(a.quellen.some((x) => /tiktok/i.test(x)),
"darunter eine zu TikTok — danach hatte Filipe ausdrücklich gefragt");
ok(a.blank, "die Verweise öffnen ohne Zugriff aufs eigene Fenster (noopener)");
if (process.env.SCHIRM) {
await seite.locator("#analyse-block").screenshot(
{ path: "C:/Users/qciga/Documents/Obelix/DogiHompage/blick.png" });
}
ok(konsole.length === 0,
konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 90) : "keine Fehler im Browser");
await browser.close();
}
/* ------------------------------------------------------------------- */
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);