DER BEFUND
Im naechtlichen Lauf standen dreizehn Pruefungen mit derselben Zeile
rot:
[uncaughtException] Error: spawnSync openssl ENOENT
Kein Programmfehler. Die Pruefungen brauchen einen https-Vorbau (der
Sitzungskeks des Hauses ist `secure` und kommt ueber http nicht an),
und dafuer erzeugen sie ein Wegwerf-Zertifikat mit openssl.
GEMESSEN, NICHT VERMUTET
openssl liegt hier: C:\Program Files\Git\mingw64\bin\openssl.exe
C:\Program Files\Git\usr\bin\openssl.exe
im System-PATH steht: C:\Program Files\Git\cmd (nur git.exe)
im Benutzer-PATH: nichts mit Git
Wer aus Git Bash startet, erbt die mingw-Pfade und merkt nie etwas.
Die Aufgabenplanung startet `node.exe` DIREKT, ohne Shell -- und
bekommt sie nicht. Bei mir gruen, nachts rot, und der Grund steht
nicht im Code.
Das Schlimmste daran ist nicht der Ausfall: Dreizehn dauerhaft rote
Zeilen in einer Notiz, die Filipe morgens liest, gewoehnen einem das
Hinsehen ab -- und decken dabei die echten Befunde zu.
DIE REPARATUR
`server/helfer-openssl.mjs` sucht openssl erst im PATH (ohne `where`
oder `which` -- beides sind selbst Programme und koennen genauso
fehlen), danach an den Orten, an denen es auf einem Windows-Rechner
mit Git wirklich liegt.
Dazu `zertifikatBauen(schluessel, zertifikat, host)`. Die acht
Aufrufformen im Haus waren identisch bis auf die Variablennamen
(schl/zert, schluesselDatei/zertDatei, zKey/zCrt, sk/zt, key/crt) --
eine Stelle traegt alle neunundzwanzig. Zwei Tage zuvor war eine
davon um ein `-addext` aermer als die anderen; gemerkt hat es
niemand, weil der Browser den fehlenden Alternativnamen erst bei
einer Weiterleitung anmahnt. Eine Stelle kann nicht von sich selbst
abweichen.
KEIN EINTRAG IN DEN SYSTEM-PATH. `mingw64\bin` enthaelt rund hundert
Programme mit Unix-Namen (`find`, `sort`, `link`), die gleichnamige
Windows-Befehle verdecken. Das fuer eine Pruefung zu aendern haette
an ganz anderen Stellen Fehler verursacht, die niemand hierher
zurueckverfolgt.
GEGENPROBE IN BEIDE RICHTUNGEN, mit dem PATH des Nachtlaufs:
vorher (direkter Aufruf): ABSTURZ: spawnSync openssl ENOENT
nachher (ueber den Helfer): Zertifikat gebaut, 1236 Bytes
Und eine echte Pruefung unter denselben Bedingungen:
PATH ohne mingw64 -> pruef-chat-neu-stelle
63 Pruefungen, 0 Fehler, 0 Abstuerze
Genau diese Datei stand im Nachtlauf mit ENOENT rot.
EINE WACHE DAGEGEN
pruef-struktur prueft ab jetzt, dass niemand openssl wieder direkt
ruft -- der naechste merkt es dort und nicht erst in einem
Nachtlauf, den niemand liest. Drei Gegenproben.
Beim ersten Anlauf schlug sie auf ihre EIGENEN Probetexte an: Sie
liest alle Serverdateien, und dazu gehoert sie selbst. Die Texte
werden jetzt zusammengesetzt. Derselbe Selbsttreffer ist mir heute
schon zweimal passiert.
DER DRITTE AUSGANG BLEIBT, WO ER HINGEHOERT
`helfer-kachel-echtfarbe.mjs` faengt den Fehler weiterhin ab und
meldet `moeglich: false` mit Grund, statt abzubrechen. Eine
Sicherung abzuschaffen, weil ihr Anlass gerade behoben ist, ist der
Anfang des naechsten stillen Fehlschlags.
NEBENBEI: pruef-community-sicht
Sie war rot mit "ZU VIEL: reaktion.html" -- mein eigener Rueckstand
vom 28.09. Die Reaction steht als Kachel im Community-Bereich; die
Liste war nicht nachgezogen. Jetzt 10 / 0.
Die Liste bleibt bewusst von Hand gepflegt: Sie ist eine ABSICHT,
keine Ableitung. Aus rechte.js gelesen verglichen sich zwei Kopien
derselben Quelle -- immer gruen, nie ein Beweis.
GEPRUEFT
pruef-struktur ALLES IN ORDNUNG (mit der neuen Wache)
pruef-community-sicht 10 / 0
pruef-chat-neu-stelle 63 / 0 (mit dem PATH des Nachtlaufs)
pruef-notizen, -teamlage-karten, -werdegang: EXIT 0, keine Abstuerze
mess-quer EXIT 0
29 Dateien: node --check auf allen, kein direkter Aufruf mehr
728 lines
33 KiB
JavaScript
728 lines
33 KiB
JavaScript
/* =====================================================================
|
|
DER NOTIZBLOCK (26.09.2026)
|
|
|
|
Filipe: „für die modis, rechte und linke hand und dogfather eine
|
|
kachel hinzufügst, sie soll: Notizen, heißen. ich will dass du das
|
|
auch wie ein notizblock erstellst."
|
|
|
|
---------------------------------------------------------------------
|
|
DREI DINGE WERDEN GEPRÜFT, UND ZWEI DAVON HAT NIEMAND BESTELLT
|
|
|
|
1. WER DARF ÜBERHAUPT. Genau die vier Rollen, und niemand sonst —
|
|
auch nicht über die Schnittstelle, auch nicht von der anderen
|
|
Adresse aus. Die Rollenmenge wird dabei nicht abgeschrieben,
|
|
sondern aus dem Programm gelesen und gegen die Rechtetafel
|
|
gehalten: Zwei Listen für dieselbe Frage laufen auseinander.
|
|
|
|
2. NIEMAND SIEHT DIE NOTIZEN EINES ANDEREN. Auch DogFather nicht.
|
|
Das ist die Zusage, die auf der Kachel steht („nur du siehst
|
|
ihn"), und die einzige, deren Bruch man nicht bemerken würde:
|
|
Eine Notiz, die jemand Fremdes liest, sieht für den Schreiber
|
|
genauso aus wie eine, die niemand liest.
|
|
|
|
3. DIE DECKSCHICHT LIEGT AUF DEN PIXEL GENAU. Das Blatt besteht
|
|
aus einem unsichtbaren Schreibfeld und einer sichtbaren
|
|
Deckschicht darüber. Ändert eine Auszeichnung die BREITE eines
|
|
Zeichens (Fettdruck, andere Schriftgröße, Sperrung), brechen
|
|
beide Schichten an verschiedenen Stellen um — und der Cursor
|
|
steht neben dem Text. Das sieht aus wie ein Browserfehler und
|
|
wäre einer in notizen.css. Gemessen wird deshalb am echten
|
|
Umbruch, nicht an der Absicht.
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM DIESE PRÜFUNG ÜBER https SPRICHT
|
|
|
|
Derselbe Grund wie bei pruef-handy-teamdogi: `dogfather-universe.com`
|
|
steht in der fest eingebauten HSTS-Liste von Chromium, und ohne die
|
|
Team-Adresse sieht man die Trennung nicht: Auf einer Prüfadresse
|
|
(127.0.0.1) ist `haus` weder „crew" noch „agentur" — dort ist der
|
|
Block offen (so wie jedes andere Modul auch), und nur auf
|
|
`workspace.` wird er abgewiesen. Beides wird unten gemessen.
|
|
|
|
Aufrufen mit: node server/pruef-notizen.mjs
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { request } from "node:http";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-notiz-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
import { zertifikatBauen } from "./helfer-openssl.mjs";
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die Notizblockpruefung");
|
|
|
|
process.env.PORT = `${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 }); };
|
|
await import("./index.js");
|
|
notbremse(260_000, "pruef-notizen");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
const melde = (t) => console.log(t);
|
|
process.on("exit", () => { try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } });
|
|
|
|
const CREW = "crew.dogfather-universe.com";
|
|
const AGENTUR = "workspace.dogfather-universe.com";
|
|
|
|
/* ---------- Zugänge anlegen -------------------------------------------- */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const nun = new Date().toISOString();
|
|
const kennSchluessel = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", kennSchluessel, nun);
|
|
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", kennSchluessel).update(code).digest("hex"), nun);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
const ROLLEN = [["admin", "Filipe"], ["hand", "VanVan"], ["linke", "Funny"],
|
|
["modi", "Miss"], ["gast", "Zuschauerin"], ["creator", "Luna"]];
|
|
const CODES = {};
|
|
const IDS = {};
|
|
for (const [i, [rolle, name]] of ROLLEN.entries()) {
|
|
CODES[rolle] = `CODE-N${String(i).padStart(2, "0")}-000001`;
|
|
IDS[rolle] = anlegen(name, rolle, CODES[rolle]);
|
|
}
|
|
d.close();
|
|
|
|
/* ---------- Sprechen mit Hausadresse ------------------------------------
|
|
`fetch` kann die Host-Kopfzeile nicht setzen (verbotene Kopfzeile),
|
|
und an ihr entscheidet das Haus alles. Deshalb node:http. */
|
|
function roh(host, pfad, { method = "GET", keks = "", rumpf = null, origin } = {}) {
|
|
return new Promise((fertig, schief) => {
|
|
const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf));
|
|
const a = request({
|
|
host: "127.0.0.1", port: PORT, path: pfad, method,
|
|
headers: {
|
|
Host: host,
|
|
Origin: origin === undefined ? `https://${host}` : origin,
|
|
...(keks ? { Cookie: keks } : {}),
|
|
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
|
|
},
|
|
}, (antwort) => {
|
|
const teile = [];
|
|
antwort.on("data", (s) => teile.push(s));
|
|
antwort.on("end", () => {
|
|
const text = Buffer.concat(teile).toString("utf8");
|
|
let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ }
|
|
fertig({ code: antwort.statusCode, text, j, kopf: antwort.headers });
|
|
});
|
|
});
|
|
a.on("error", schief);
|
|
if (daten) a.write(daten);
|
|
a.end();
|
|
});
|
|
}
|
|
|
|
async function anmelden(rolle, host) {
|
|
const a = await roh(host, "/workspace/api/anmelden", {
|
|
method: "POST", rumpf: { rolle, code: CODES[rolle] },
|
|
});
|
|
const keks = []
|
|
.concat(a.kopf["set-cookie"] || [])
|
|
.map((z) => z.split(";")[0]).join("; ");
|
|
return a.code === 200 && keks ? keks : null;
|
|
}
|
|
|
|
/* =======================================================================
|
|
1. WER DARF — und woher die Liste kommt
|
|
======================================================================= */
|
|
melde("=== 1. Wer einen Block hat ===");
|
|
|
|
const { ROLLEN_MIT_BLOCK } = await import("./workspace-notizen.js");
|
|
const { SEITEN } = await import("./rechte.js");
|
|
{
|
|
const ausTafel = [...(SEITEN["/workspace/notizen.html"] || [])].sort();
|
|
const ausProgramm = [...ROLLEN_MIT_BLOCK].sort();
|
|
ok(ausTafel.length === 4,
|
|
`die Rechtetafel nennt vier Rollen (${ausTafel.join(", ") || "keine"})`);
|
|
/* BEIDE SEITEN GEGENEINANDER. Die Tafel bewacht die Seite, die
|
|
Menge im Modul die Daten. Wer nur eine pflegt, baut entweder eine
|
|
Tuer ohne Raum oder einen Raum ohne Tuer -- und beides faellt
|
|
niemandem auf. */
|
|
ok(JSON.stringify(ausTafel) === JSON.stringify(ausProgramm),
|
|
`Tafel und Schnittstelle nennen dieselben Rollen (${ausProgramm.join(", ")})`,
|
|
);
|
|
ok(ausTafel.includes("modi") && ausTafel.includes("hand")
|
|
&& ausTafel.includes("linke") && ausTafel.includes("admin"),
|
|
"und es sind genau die vier aus dem Auftrag");
|
|
}
|
|
|
|
const kekse = {};
|
|
for (const [rolle] of ROLLEN) kekse[rolle] = await anmelden(rolle, CREW);
|
|
ok(["admin", "hand", "linke", "modi"].every((r) => kekse[r]),
|
|
"die vier melden sich auf der Team-Adresse an");
|
|
|
|
melde("");
|
|
melde("=== 2. Und wer nicht ===");
|
|
for (const rolle of ["gast", "creator"]) {
|
|
if (!kekse[rolle]) { ok(true, `${rolle}: kommt auf dieser Adresse gar nicht herein`); continue; }
|
|
const a = await roh(CREW, "/workspace/api/notizblock", { keks: kekse[rolle] });
|
|
ok(a.code === 404, `${rolle}: bekommt 404 an der Schnittstelle (${a.code})`);
|
|
}
|
|
{
|
|
/* DIE ANDERE ADRESSE. DogFather arbeitet in beiden Haeusern -- und
|
|
genau deshalb ist er hier der richtige Pruefstein: Bei ihm
|
|
scheitert es nicht an der Rolle, sondern an der Adresse. */
|
|
const keksAgentur = await anmelden("admin", AGENTUR);
|
|
ok(!!keksAgentur, "DogFather meldet sich auch auf der Agenturadresse an");
|
|
const a = await roh(AGENTUR, "/workspace/api/notizblock", { keks: keksAgentur });
|
|
ok(a.code === 404, `und bekommt dort 404 auf seinen Block (${a.code})`);
|
|
const s = await roh(AGENTUR, "/workspace/notizen.html", { keks: keksAgentur });
|
|
ok(s.code === 404, `auch die Seite selbst gibt es dort nicht (${s.code})`);
|
|
const s2 = await roh(CREW, "/workspace/notizen.html", { keks: kekse.admin });
|
|
ok(s2.code === 200, `auf der Team-Adresse schon (${s2.code})`);
|
|
}
|
|
|
|
/* ---- Und die Pruefadresse ist das ANDERE Ende derselben Regel --------
|
|
(27.09.2026, nachgetragen, nachdem pruef-handy einen 404 gemeldet
|
|
hatte.)
|
|
|
|
Auf 127.0.0.1 hat niemand ein Haus. Die erste Fassung der Schranke
|
|
fragte „ist das Haus crew?" und wies damit auch die Pruefadresse ab
|
|
-- die Seite lud, blieb leer und schrieb einen Fehler in die
|
|
Konsole. Gefragt wird jetzt „ist es das ANDERE Haus?", so wie es
|
|
hausWo() im ganzen Haus tut.
|
|
|
|
Beide Enden gehoeren in dieselbe Pruefung: Wer nur das eine misst,
|
|
kann die Schranke jederzeit wieder zu scharf stellen, ohne dass
|
|
etwas rot wird. */
|
|
{
|
|
const keksProbe = await anmelden("admin", "127.0.0.1");
|
|
ok(!!keksProbe, "auf der Pruefadresse meldet sich DogFather an");
|
|
const a = await roh("127.0.0.1", "/workspace/api/notizblock", { keks: keksProbe });
|
|
ok(a.code === 200 && Array.isArray(a.j?.notizen),
|
|
`und bekommt dort seinen Block (${a.code}) -- ohne Haus wird nicht gesperrt`);
|
|
const s3 = await roh("127.0.0.1", "/workspace/notizen.html", { keks: keksProbe });
|
|
ok(s3.code === 200, `auch die Seite selbst (${s3.code})`);
|
|
}
|
|
{
|
|
const ohne = await roh(CREW, "/workspace/api/notizblock");
|
|
ok(ohne.code === 401, `ohne Anmeldung: 401 (${ohne.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. SCHREIBEN, LESEN, ÄNDERN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Ein Blatt ===");
|
|
let blattId = null;
|
|
{
|
|
const leer = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
ok(leer.code === 200 && Array.isArray(leer.j?.notizen) && leer.j.notizen.length === 0,
|
|
`am Anfang ist der Block leer (${leer.j?.notizen?.length})`);
|
|
ok(Array.isArray(leer.j?.farben) && leer.j.farben.length >= 4,
|
|
`die Papierfarben kommen vom Server (${leer.j?.farben?.length})`);
|
|
|
|
const neu = await roh(CREW, "/workspace/api/notizblock", {
|
|
method: "POST", keks: kekse.modi, rumpf: { text: "Erste Zeile\nZweite Zeile" },
|
|
});
|
|
blattId = neu.j?.notiz?.id;
|
|
ok(neu.code === 201 && blattId > 0, `ein Blatt anlegen geht (${neu.code})`);
|
|
ok(neu.j?.notiz?.titel === "Erste Zeile",
|
|
`der Titel wird aus der ersten Zeile abgeleitet ("${neu.j?.notiz?.titel}")`);
|
|
|
|
const aend = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { text: "# Einkauf\n[ ] Milch\n[x] Brot" },
|
|
});
|
|
ok(aend.code === 200 && typeof aend.j?.geaendert === "string",
|
|
`aendern geht und gibt den Zeitstempel zurueck (${aend.code})`);
|
|
|
|
const nach = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
ok(nach.j?.notizen?.[0]?.text?.includes("[x] Brot"), "und der Text steht drin");
|
|
ok(nach.j?.notizen?.[0]?.titel === "Einkauf",
|
|
`die Raute zaehlt nicht zum Titel ("${nach.j?.notizen?.[0]?.titel}")`);
|
|
}
|
|
{
|
|
const falsch = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { farbe: "knallorange" },
|
|
});
|
|
ok(falsch.code === 400, `eine erfundene Papierfarbe wird abgelehnt (${falsch.code})`);
|
|
const echt = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { farbe: "rosa" },
|
|
});
|
|
ok(echt.code === 200, `eine echte nicht (${echt.code})`);
|
|
}
|
|
{
|
|
const lang = "x".repeat(30000);
|
|
const a = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { text: lang },
|
|
});
|
|
const nach = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
const drin = nach.j?.notizen?.find((n) => n.id === blattId)?.text || "";
|
|
/* ABGESCHNITTEN UND NICHT ABGELEHNT. Wer zwanzigtausend Zeichen
|
|
getippt hat, soll nicht alles verlieren, weil er hundert zu viel
|
|
hatte -- er soll merken, dass hier Schluss ist. */
|
|
ok(a.code === 200 && drin.length === nach.j.text_max,
|
|
`zu langer Text wird gekuerzt statt abgelehnt (${drin.length} von ${nach.j?.text_max})`);
|
|
await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { text: "# Einkauf\n[ ] Milch\n[x] Brot" },
|
|
});
|
|
}
|
|
{
|
|
const fremd = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { text: "x" }, origin: "https://boese.example",
|
|
});
|
|
ok(fremd.code === 403, `eine Anfrage von einer fremden Seite wird abgewiesen (${fremd.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. NIEMAND SIEHT DIE NOTIZEN EINES ANDEREN
|
|
|
|
Die Zusage steht auf der Kachel. Sie ist auch die einzige, deren
|
|
Bruch der Schreiber nicht bemerken würde.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Niemand sonst ===");
|
|
for (const rolle of ["admin", "hand", "linke"]) {
|
|
const liste = await roh(CREW, "/workspace/api/notizblock", { keks: kekse[rolle] });
|
|
const fremdeDrin = (liste.j?.notizen || []).some((n) => n.id === blattId);
|
|
ok(liste.code === 200 && !fremdeDrin,
|
|
`${rolle}: sieht das Blatt des Modis nicht in seiner Liste`);
|
|
const holen = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse[rolle], rumpf: { text: "hier war ich" },
|
|
});
|
|
ok(holen.code === 404, `${rolle}: kann es auch nicht aendern (${holen.code})`);
|
|
const weg = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "DELETE", keks: kekse[rolle],
|
|
});
|
|
ok(weg.code === 404, `${rolle}: und nicht wegwerfen (${weg.code})`);
|
|
}
|
|
{
|
|
/* GEGENPROBE. Ohne sie wuerde die Messung oben auch dann gruen, wenn
|
|
das Blatt gar nicht existiert. */
|
|
const eigen = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { angeheftet: true },
|
|
});
|
|
ok(eigen.code === 200, `Gegenprobe: dem Modi selbst gelingt genau dasselbe (${eigen.code})`);
|
|
const nach = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
ok(nach.j?.notizen?.[0]?.angeheftet === true, "und das Anheften kommt an");
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. DER PAPIERKORB
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5. Der Papierkorb ===");
|
|
{
|
|
const weg = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "DELETE", keks: kekse.modi,
|
|
});
|
|
ok(weg.code === 200, `wegtun geht (${weg.code})`);
|
|
const block = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
ok(!(block.j?.notizen || []).some((n) => n.id === blattId), "und es ist vom Block verschwunden");
|
|
ok(block.j?.papierkorb === 1, `der Papierkorb zaehlt es (${block.j?.papierkorb})`);
|
|
|
|
const korb = await roh(CREW, "/workspace/api/notizblock/papierkorb", { keks: kekse.modi });
|
|
const drin = (korb.j?.notizen || []).find((n) => n.id === blattId);
|
|
ok(!!drin, "es liegt im Papierkorb");
|
|
ok(typeof drin?.endgueltig_am === "string"
|
|
&& Date.parse(drin.endgueltig_am) > Date.now(),
|
|
`und es steht dabei, wann es von selbst geht (${drin?.endgueltig_am?.slice(0, 10)})`);
|
|
|
|
const zurueck = await roh(CREW, `/workspace/api/notizblock/${blattId}/zurueck`, {
|
|
method: "POST", keks: kekse.modi,
|
|
});
|
|
ok(zurueck.code === 200, `zurueckholen geht (${zurueck.code})`);
|
|
const block2 = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
ok((block2.j?.notizen || []).some((n) => n.id === blattId), "und es ist wieder da");
|
|
}
|
|
{
|
|
/* ENDGUELTIG NUR AUS DEM PAPIERKORB. Vom Block aus gibt es diesen
|
|
Weg nicht -- wer danebentippt, soll nicht alles verlieren. */
|
|
const zuFrueh = await roh(CREW, `/workspace/api/notizblock/${blattId}/endgueltig`, {
|
|
method: "DELETE", keks: kekse.modi,
|
|
});
|
|
ok(zuFrueh.code === 404,
|
|
`endgueltig loeschen geht vom Block aus NICHT (${zuFrueh.code})`);
|
|
await roh(CREW, `/workspace/api/notizblock/${blattId}`, { method: "DELETE", keks: kekse.modi });
|
|
const jetzt = await roh(CREW, `/workspace/api/notizblock/${blattId}/endgueltig`, {
|
|
method: "DELETE", keks: kekse.modi,
|
|
});
|
|
ok(jetzt.code === 200, `aus dem Papierkorb schon (${jetzt.code})`);
|
|
const korb = await roh(CREW, "/workspace/api/notizblock/papierkorb", { keks: kekse.modi });
|
|
ok((korb.j?.notizen || []).length === 0, "und dann ist es wirklich weg");
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. DAS AUFRÄUMEN HÄLT SEINE FRIST
|
|
|
|
Eine Frist ohne Werkzeug ist ein Satz in einem Kommentar. Geprüft
|
|
wird deshalb an echten Daten, in beide Richtungen.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 6. Dreissig Tage ===");
|
|
{
|
|
const { notizenAufraeumen } = await import("./workspace-notizen.js");
|
|
const { PAPIERKORB_TAGE } = await import("./notiz-tabellen.js");
|
|
const { db } = await import("./workspace.js");
|
|
const alt = new Date(Date.now() - (PAPIERKORB_TAGE + 2) * 86400000).toISOString();
|
|
const frisch = new Date(Date.now() - 86400000).toISOString();
|
|
const einfuegen = (weg) => db().prepare(
|
|
"INSERT INTO notizen (person_id, text, farbe, angeheftet, erstellt, geaendert, weg_am)"
|
|
+ " VALUES (?,?,?,0,?,?,?) RETURNING id")
|
|
.get(IDS.modi, "Probe", "block", nun, nun, weg).id;
|
|
const idAlt = einfuegen(alt);
|
|
const idNeu = einfuegen(frisch);
|
|
const idOffen = db().prepare(
|
|
"INSERT INTO notizen (person_id, text, farbe, angeheftet, erstellt, geaendert)"
|
|
+ " VALUES (?,?,?,0,?,?) RETURNING id").get(IDS.modi, "Auf dem Block", "block", nun, nun).id;
|
|
|
|
const weg = notizenAufraeumen();
|
|
const da = (id) => !!db().prepare("SELECT 1 FROM notizen WHERE id = ?").get(id);
|
|
ok(weg === 1, `genau eines wird geraeumt (${weg})`);
|
|
ok(!da(idAlt), `das alte ist weg (nach ${PAPIERKORB_TAGE} Tagen)`);
|
|
ok(da(idNeu), "das von gestern liegt noch da");
|
|
/* DIE WICHTIGERE HAELFTE: Was auf dem Block liegt, wird NIE
|
|
geraeumt. Ohne diese Zeile waere ein Aufraeumer, der alles
|
|
loescht, hier gruen. */
|
|
ok(da(idOffen), "und was gar nicht weggelegt wurde, bleibt unberuehrt");
|
|
db().prepare("DELETE FROM notizen WHERE id IN (?,?)").run(idNeu, idOffen);
|
|
}
|
|
{
|
|
/* Steht die Frist auch in der Aufbewahrungsliste? Sie ist die
|
|
einzige Stelle im Haus, die wirklich raeumt -- am 24.09. war
|
|
genau dieser Eintrag beim Support vergessen worden, und die
|
|
Meldungen waeren fuer immer liegen geblieben. */
|
|
const { FRISTEN } = await import("./workspace-aufbewahrung.js");
|
|
const eintrag = (FRISTEN || []).find((e) => e.schluessel === "notizen_papierkorb");
|
|
ok(!!eintrag, "der Papierkorb steht in der Aufbewahrungsliste");
|
|
ok(typeof eintrag?.raeumen === "function",
|
|
"und hat ein Werkzeug, das ihn wirklich raeumt");
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. DAS BLATT AUF DEM BILDSCHIRM
|
|
|
|
Jetzt der Teil, den keine Schnittstelle beantworten kann.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 7. Das Blatt auf dem Bildschirm ===");
|
|
|
|
const { execFileSync } = await import("node:child_process");
|
|
const { createServer: httpsServer } = await import("node:https");
|
|
const { request: httpAnfrage } = await import("node:http");
|
|
|
|
const schluesselDatei = join(ordner, "pruef.key");
|
|
const zertDatei = join(ordner, "pruef.crt");
|
|
zertifikatBauen(schluesselDatei, zertDatei, CREW);
|
|
|
|
const HTTPS_PORT = PORT + 1;
|
|
const vorbau = httpsServer({
|
|
key: readFileSync(schluesselDatei), cert: readFileSync(zertDatei),
|
|
}, (von, nach) => {
|
|
const weiter = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
|
|
headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` },
|
|
}, (antwort) => {
|
|
nach.writeHead(antwort.statusCode || 502, antwort.headers);
|
|
antwort.pipe(nach);
|
|
});
|
|
weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } });
|
|
von.pipe(weiter);
|
|
});
|
|
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
|
|
const BASIS = `https://${CREW}:${HTTPS_PORT}`;
|
|
|
|
const pw = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch({
|
|
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`,
|
|
"--ignore-certificate-errors"],
|
|
});
|
|
const kontext = await browser.newContext({
|
|
viewport: { width: 1440, height: 900 }, ignoreHTTPSErrors: true,
|
|
});
|
|
await kontext.addCookies((kekse.modi || "").split("; ").filter(Boolean).map((k) => {
|
|
const [n, ...r] = k.split("=");
|
|
return { name: n, value: r.join("="), domain: CREW, path: "/" };
|
|
}));
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("pageerror", (f) => konsole.push(String(f?.message || f)));
|
|
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
|
|
|
await seite.goto(`${BASIS}/workspace/notizen.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(700);
|
|
|
|
ok(await seite.locator("#block").count() === 1, "die Seite kommt an");
|
|
|
|
/* Ein Blatt anlegen und Text hineinschreiben, der alle Auszeichnungen
|
|
enthaelt -- und eine SEHR lange Zeile, die umbrechen MUSS. Genau an
|
|
ihr faellt eine Breitenaenderung auf: Brechen die zwei Schichten an
|
|
verschiedenen Stellen um, sind sie verschieden hoch. */
|
|
const PROBE = [
|
|
"# Eine Ueberschrift",
|
|
"[ ] Offener Punkt",
|
|
"[x] Erledigter Punkt",
|
|
"- Ein Aufzaehlungspunkt",
|
|
"> Ein Zitat",
|
|
"Ein Link: https://example.org/pfad",
|
|
"Eine sehr lange Zeile, die auf jeden Fall umbrechen muss, damit man sieht, "
|
|
+ "ob beide Schichten an derselben Stelle umbrechen, und deshalb ist sie so "
|
|
+ "lang wie sie ist, mit vielen Woertern hintereinander ohne Punkt und Komma "
|
|
+ "damit der Umbruch wirklich mitten im Satz passiert und nicht am Ende.",
|
|
].join("\n");
|
|
|
|
await seite.click("#neu");
|
|
await seite.waitForTimeout(400);
|
|
await seite.fill("#feld", PROBE);
|
|
await seite.waitForTimeout(400);
|
|
|
|
{
|
|
const m = await seite.evaluate(() => {
|
|
const f = document.getElementById("feld");
|
|
const dk = document.getElementById("deck");
|
|
const cf = getComputedStyle(f), cd = getComputedStyle(dk);
|
|
const gleich = (n) => cf[n] === cd[n];
|
|
return {
|
|
hoeheFeld: f.scrollHeight, hoeheDeck: dk.scrollHeight,
|
|
breiteFeld: f.clientWidth, breiteDeck: dk.clientWidth,
|
|
schriftGleich: ["fontFamily", "fontSize", "fontWeight", "fontStyle",
|
|
"letterSpacing", "wordSpacing", "lineHeight", "whiteSpace",
|
|
"overflowWrap", "tabSize", "paddingLeft", "paddingRight",
|
|
"paddingTop"].filter((n) => !gleich(n)),
|
|
/* Und dasselbe fuer JEDES ausgezeichnete Stueck im Deck: Wenn
|
|
eine Auszeichnung die Schrift veraendert, steht es hier. */
|
|
stuecke: [...dk.querySelectorAll("span, a")].map((s) => {
|
|
const c = getComputedStyle(s);
|
|
return ["fontFamily", "fontSize", "fontWeight", "fontStyle",
|
|
"letterSpacing", "wordSpacing"]
|
|
.filter((n) => c[n] !== cf[n])
|
|
.map((n) => `${s.className || s.tagName}.${n}=${c[n]}`);
|
|
}).flat(),
|
|
haken: dk.querySelectorAll(".n-hk").length,
|
|
hakenAn: dk.querySelectorAll('.n-hk[data-an="ja"]').length,
|
|
links: dk.querySelectorAll("a.n-link").length,
|
|
ueber: dk.querySelectorAll(".n-h").length,
|
|
text: dk.textContent,
|
|
};
|
|
});
|
|
|
|
ok(m.schriftGleich.length === 0,
|
|
`beide Schichten schreiben gleich (${m.schriftGleich.join(", ") || "alles gleich"})`);
|
|
ok(m.stuecke.length === 0,
|
|
`keine Auszeichnung aendert die Schrift (${m.stuecke.slice(0, 3).join(", ") || "keine"})`);
|
|
ok(m.breiteFeld === m.breiteDeck,
|
|
`beide sind gleich breit (${m.breiteFeld} / ${m.breiteDeck})`);
|
|
/* DIE EIGENTLICHE MESSUNG. Zwei Bildpunkte Spielraum fuers Runden;
|
|
ein verschobener Umbruch kostet eine ganze Zeile (30 px). */
|
|
ok(Math.abs(m.hoeheFeld - m.hoeheDeck) <= 2,
|
|
`und gleich hoch -- der Umbruch sitzt an derselben Stelle `
|
|
+ `(${m.hoeheFeld} / ${m.hoeheDeck})`);
|
|
ok(m.text.replace(/\s+$/, "") === PROBE,
|
|
"die Deckschicht zeigt genau den Text, Zeichen fuer Zeichen");
|
|
ok(m.haken === 2 && m.hakenAn === 1,
|
|
`zwei Kaestchen, eines angehakt (${m.haken}/${m.hakenAn})`);
|
|
ok(m.links === 1, `die Adresse ist anklickbar (${m.links})`);
|
|
ok(m.ueber === 1, `die Ueberschrift ist ausgezeichnet (${m.ueber})`);
|
|
}
|
|
|
|
/* GEGENPROBE ZUR HOEHENMESSUNG. Ohne sie wuerde die Zeile oben auch
|
|
dann gruen, wenn die Messung gar nichts ansieht. Ein Fettdruck auf
|
|
der Deckschicht MUSS sie rot machen. */
|
|
{
|
|
const unterschied = await seite.evaluate(() => {
|
|
const f = document.getElementById("feld");
|
|
const dk = document.getElementById("deck");
|
|
const vorher = Math.abs(f.scrollHeight - dk.scrollHeight);
|
|
dk.style.fontWeight = "800";
|
|
dk.style.letterSpacing = "0.4px";
|
|
const nachher = Math.abs(f.scrollHeight - dk.scrollHeight);
|
|
dk.style.fontWeight = ""; dk.style.letterSpacing = "";
|
|
return { vorher, nachher };
|
|
});
|
|
ok(unterschied.nachher > 2,
|
|
`Gegenprobe: Fettdruck auf der Deckschicht bricht die Deckung `
|
|
+ `(${unterschied.vorher} -> ${unterschied.nachher} px Unterschied)`);
|
|
}
|
|
|
|
/* Ein Klick auf ein Kaestchen schaltet es um. */
|
|
{
|
|
const vorher = await seite.inputValue("#feld");
|
|
await seite.locator("#deck .n-hk").first().click();
|
|
await seite.waitForTimeout(250);
|
|
const nachher = await seite.inputValue("#feld");
|
|
ok(vorher.includes("[ ] Offener Punkt") && nachher.includes("[x] Offener Punkt"),
|
|
"ein Klick auf das Kaestchen setzt den Haken");
|
|
ok(nachher.length === vorher.length,
|
|
`und der Text bleibt zeichengenau gleich lang (${nachher.length}/${vorher.length})`);
|
|
await seite.locator("#deck .n-hk").first().click();
|
|
await seite.waitForTimeout(250);
|
|
ok((await seite.inputValue("#feld")).includes("[ ] Offener Punkt"),
|
|
"noch ein Klick nimmt ihn wieder weg");
|
|
}
|
|
|
|
/* Die Tastatur fuehrt die Liste fort. */
|
|
{
|
|
await seite.fill("#feld", "- Erster Punkt");
|
|
await seite.click("#feld");
|
|
await seite.keyboard.press("Control+End");
|
|
await seite.keyboard.press("Enter");
|
|
await seite.keyboard.type("Zweiter");
|
|
await seite.waitForTimeout(200);
|
|
const t = await seite.inputValue("#feld");
|
|
ok(t === "- Erster Punkt\n- Zweiter",
|
|
`Enter setzt die Aufzaehlung fort ("${t.replace(/\n/g, "\\n")}")`);
|
|
|
|
await seite.fill("#feld", "[x] Fertig");
|
|
await seite.click("#feld");
|
|
await seite.keyboard.press("Control+End");
|
|
await seite.keyboard.press("Enter");
|
|
await seite.keyboard.type("Noch offen");
|
|
await seite.waitForTimeout(200);
|
|
const t2 = await seite.inputValue("#feld");
|
|
/* DER HAKEN WIRD NICHT MITGENOMMEN -- die naechste Aufgabe ist noch
|
|
nicht erledigt. Das ist die Sorte Kleinigkeit, die den
|
|
Unterschied macht zwischen „funktioniert" und „denkt mit". */
|
|
ok(t2 === "[x] Fertig\n[ ] Noch offen",
|
|
`und uebernimmt einen gesetzten Haken NICHT ("${t2.replace(/\n/g, "\\n")}")`);
|
|
}
|
|
|
|
/* Es sichert sich von selbst -- gemessen am Neuladen, nicht an der
|
|
Anzeige. Eine Anzeige, die „gesichert" sagt, ist eine Behauptung. */
|
|
{
|
|
const wort = `Selbstgesichert-${Date.now()}`;
|
|
await seite.fill("#feld", wort);
|
|
await seite.waitForTimeout(1400);
|
|
const stand = (await seite.locator("#stand").textContent() || "").trim();
|
|
/* GEFRAGT WIRD DER SERVER, nicht die Seite nach dem Neuladen.
|
|
|
|
Der erste Anlauf lud neu und sah ins Schreibfeld -- und wurde
|
|
rot, obwohl alles richtig war: Die Seite schlaegt nach dem Laden
|
|
das OBERSTE Blatt auf, und das war ein anderes (ein angeheftetes
|
|
aus Abschnitt 4). Gemessen wurde also ein Blatt, in das nie
|
|
jemand getippt hat. Eine Messung, die das Falsche ansieht, ist
|
|
schlimmer als keine -- sie schickt einen los, einen Fehler zu
|
|
suchen, den es nicht gibt. */
|
|
const drueben = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
const gefunden = (drueben.j?.notizen || []).some((n) => String(n.text).includes(wort));
|
|
ok(gefunden, `der Text ist ohne Knopfdruck beim Server angekommen `
|
|
+ `(Anzeige sagte "${stand}")`);
|
|
ok(stand === "gesichert", `und die Anzeige sagt es auch ("${stand}")`);
|
|
}
|
|
|
|
/* Und auf dem Handy: Das Blatt legt sich ueber die Liste, der
|
|
Zurueck-Knopf holt sie wieder. */
|
|
{
|
|
/* EIN EIGENER KONTEXT MIT FINGER (29.09.2026).
|
|
|
|
Hier stand `kontext.newPage()` plus `setViewportSize`. Das
|
|
aendert nur die Groesse: `hasTouch` gehoert zum KONTEXT und
|
|
laesst sich danach nicht mehr setzen. Ohne ihn meldet der
|
|
Browser `pointer: fine`, und KEINE Regel aus
|
|
`@media (pointer: coarse)` greift -- dort stehen die
|
|
44-Pixel-Beruehrziele. Ein Handy-Bild mit Mauszeiger zeigt eine
|
|
Seite, die es auf keinem Handy gibt.
|
|
|
|
Die Anmeldung wandert als Keks mit, statt sie zu wiederholen --
|
|
eine zweite Anmeldung waere eine zweite Stelle, die veralten
|
|
kann. */
|
|
const handyKontext = await browser.newContext({
|
|
viewport: { width: 390, height: 780 }, hasTouch: true, isMobile: true });
|
|
await handyKontext.addCookies(await kontext.cookies());
|
|
const handy = await handyKontext.newPage();
|
|
await handy.goto(`${BASIS}/workspace/notizen.html`, { waitUntil: "networkidle" });
|
|
await handy.waitForTimeout(700);
|
|
const listeDa = await handy.locator(".n-zettel").count();
|
|
ok(listeDa >= 1, `auf dem Handy steht zuerst der Stapel (${listeDa} Blaetter)`);
|
|
ok(await handy.locator("#blatt").isHidden(),
|
|
"und kein Blatt liegt davor -- sonst kaeme man nie an die Liste");
|
|
await handy.locator(".n-zettel").first().click();
|
|
await handy.waitForTimeout(400);
|
|
ok(await handy.locator("#blatt").isVisible(), "ein Tipp schlaegt das Blatt auf");
|
|
ok(await handy.locator("#zurueck").isVisible(),
|
|
"und der Zurueck-Knopf erscheint");
|
|
/* KEIN WAAGERECHTES SCHIEBEN. Die Hausgrenze ist die Breite des
|
|
Schirms; ein Blatt, das darueber hinausragt, macht die ganze
|
|
Seite schiebbar. */
|
|
const breit = await handy.evaluate(() =>
|
|
document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
ok(breit <= 1, `nichts ragt seitlich hinaus (${breit} px)`);
|
|
await handy.locator("#zurueck").click();
|
|
await handy.waitForTimeout(400);
|
|
ok(await handy.locator("#blatt").isHidden(), "der Zurueck-Knopf schliesst es wieder");
|
|
await handyKontext.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
8. ZWEI WEGE MIT DEMSELBEN NAMEN -- EINE HAUSWEITE PRUEFUNG
|
|
|
|
Sie steht in dieser Datei, weil sie hier gefunden wurde, und sie
|
|
gilt fuer das ganze Haus.
|
|
|
|
BEIM BAUEN DIESES BLOCKS: Die Route DELETE /workspace/api/notizen/:id
|
|
gab es schon -- in workspace-aufgaben.js, fuer die Notizen AN einer
|
|
Aufgabe. Weil aufgabenRouter frueher eingehaengt wird, hat sie jeden
|
|
Loeschversuch des Blocks abgefangen und mit 404 beantwortet.
|
|
|
|
VON AUSSEN SAH DAS AUS WIE EIN FEHLER IM NEUEN MODUL: Anlegen ging,
|
|
Aendern ging, Loeschen nicht. Man sucht dann im eigenen Code, und
|
|
dort ist nichts. Express meldet so etwas nicht -- es nimmt die
|
|
erste Route, die passt, und schweigt ueber die zweite.
|
|
|
|
Geprueft wird der Routenbaum von express selbst, nicht eine Liste.
|
|
Findet sich ein Paar aus Methode und Pfad zweimal, steht hier, wo.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 8. Kein Weg heisst zweimal gleich ===");
|
|
{
|
|
const app = (await import("./index.js")).default;
|
|
const wege = new Map();
|
|
const doppelt = [];
|
|
const sammeln = (schicht, vorn = "") => {
|
|
if (schicht.route) {
|
|
const pfad = vorn + (schicht.route.path || "");
|
|
for (const m of Object.keys(schicht.route.methods || {})) {
|
|
if (m === "_all") continue;
|
|
const name = `${m.toUpperCase()} ${pfad}`;
|
|
if (wege.has(name)) doppelt.push(name); else wege.set(name, true);
|
|
}
|
|
} else if (schicht.handle?.stack) {
|
|
for (const s of schicht.handle.stack) sammeln(s, vorn);
|
|
}
|
|
};
|
|
const stapel = app?._router?.stack || app?.router?.stack || [];
|
|
for (const s of stapel) sammeln(s);
|
|
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Faende diese Messung gar keine
|
|
Wege (weil express seinen Baum eines Tages anders ablegt), waere
|
|
„keine Dubletten" trivial wahr -- und bliebe es fuer immer. */
|
|
ok(wege.size > 200, `${wege.size} Wege im Baum gefunden`);
|
|
ok(doppelt.length === 0,
|
|
doppelt.length ? `doppelt vergeben: ${[...new Set(doppelt)].join(", ")}`
|
|
: "kein Paar aus Methode und Pfad kommt zweimal vor");
|
|
/* GEGENPROBE: Die Messung MUSS eine Dublette finden koennen. */
|
|
const probe = new Map(); const gefunden = [];
|
|
for (const n of ["GET /x", "GET /x"]) {
|
|
if (probe.has(n)) gefunden.push(n); else probe.set(n, true);
|
|
}
|
|
ok(gefunden.length === 1, "Gegenprobe: zwei gleiche Wege wuerden auffallen");
|
|
}
|
|
|
|
ok(konsole.length === 0, `keine Fehler in der Konsole (${konsole.slice(0, 2).join(" | ") || "keine"})`);
|
|
|
|
await browser.close();
|
|
await new Promise((r) => vorbau.close(r));
|
|
|
|
melde("");
|
|
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
melde(fehler ? "FEHLGESCHLAGEN" : "ALLES IN ORDNUNG");
|
|
process.exit(fehler ? 1 : 0);
|