Beim Weiterarbeiten am Vorlagenbrett nachgemessen und gefunden:
`benachrichtige` kam in workspace-zuteilung.js KEIN EINZIGES MAL vor,
in workspace-vorlagen.js auch nicht.
Beide Bewerbungswege waren gebaut, beide funktionierten -- und beide
waren stumm:
* Bewirbt sich Frida, erfaehrt DogFather es nur, wenn er von sich
aus das Brett aufmacht.
* Antwortet er, erfaehrt Frida es nur, wenn SIE von sich aus
nachsieht.
Das ist keine Kleinigkeit, das ist die Funktion. Wer sich bewirbt,
wartet -- und Warten ohne Rueckmeldung fuehlt sich nach zwei Tagen an
wie "interessiert keinen". Genau das soll eine Bewerbung verhindern.
WER ES ERFAEHRT -- ABGELEITET, NICHT AUFGEZAEHLT
------------------------------------------------
Die naheliegende Zeile waere `rolle IN ('admin','hand')` gewesen; so
steht sie in workspace-hilfe.js. Das ist eine Abschrift, und
Abschriften altern: Kaeme morgen eine Rolle dazu, die entscheiden
darf, bekaeme sie keine einzige Meldung -- und niemand merkte es, weil
ja alles funktioniert.
Gefragt wird deshalb die Regel selbst (entscheidetUeberAufgaben),
Person fuer Person. Und zusaetzlich darfSchreibenMit: Wer den Bewerber
gar nicht sehen darf, bekommt auch keine Meldung ueber ihn. Das ist
keine Vorsicht um ihrer selbst willen -- ohne diese Zeile erfuehre die
Agentur ueber eine Push-Nachricht, dass es Team Dogi ueberhaupt gibt.
Gemessen: Die Bewerbung eines Modis erreicht genau zwei Leute
(admin, hand) von sechs Aktiven. Nicht die linke Hand (sie entscheidet
hier nicht mit), niemand aus dem anderen Haus, und nicht der Bewerber
selbst.
ZWEI SCHALTER, ZWEI ENTSCHEIDUNGEN
----------------------------------
"bewerbung_neu" trifft den, der antwortet -- an einem lebhaften Tag
mehrfach, das kann man stumm stellen wollen. "bewerbung_antwort"
trifft den, der wartet; sie kommt einmal, und niemand will sie stumm
stellen. Eine gemeinsame Art hiesse: beides zusammen abschalten oder
beides zusammen ertragen. Dieselbe Ueberlegung wie beim Chat
(Nachricht / Erwaehnung).
Beide von sich aus an. Keine Ausnahme von der Ruhezeit: Eine Bewerbung
wartet, ein Anruf nicht.
DIE NOTIZ STEHT IN DER MELDUNG
------------------------------
Filipe hat sie ausdruecklich verlangt ("mit einem text als notiz").
Sie erst zu verlangen und dann an genau der Stelle zu verschweigen, an
der man sie liest, waere die halbe Funktion. Und das Ergebnis steht im
TITEL -- "angenommen" oder "diesmal nicht" -- damit man es lesen kann,
ohne zu oeffnen. Auch die gute Nachricht.
Der Wortlaut steht in zwei reinen Funktionen (bewerbungText,
antwortText), exportiert, damit eine Pruefung sie lesen kann, ohne
einen Push-Dienst nachzubauen. Genau an so einer Stelle steckte am
18.09. der Fehler "Nachricht von [object Object]", der von aussen
nicht messbar war.
EINE STELLE FUER BEIDE WEGE
---------------------------
workspace-bewerbung-melden.js. Zwei Fassungen waeren zwei
Gelegenheiten, dass eine davon die Ruhezeit, die Abschaltbarkeit oder
die Haeusertrennung vergisst -- und dieselbe Person laese zweimal
etwas Verschiedenes ueber denselben Vorgang.
Die Meldung wird NICHT abgewartet (`void`): Ob sie durchgeht, haengt
am Push-Dienst, an der Ruhezeit und an den Einstellungen des
Empfaengers. Nichts davon darf entscheiden, ob die Bewerbung
gespeichert ist -- die ist es laengst.
NOCH EINE ROTE PRUEFUNG, DIE NIEMAND GESEHEN HAT
-------------------------------------------------
pruef-push-ziel meldete: "aber nicht auf eine Seite, die es fuer ihn
nicht gibt (/workspace/calls.html)". Das sah aus wie ein Befund und
war eine erfuellte Bestellung -- Filipe hatte am 22.09. genau das
Gegenteil bestellt ("jeder der einen kalender hat soll auch sowas
haben"). Nachgemessen: Modi, rechte und linke Hand haben je eine
Calls-Kachel.
Die Pruefung steht jetzt andersherum: Die Calls-Seite MUSS stehen
bleiben. Dieselbe Zeile schuetzt damit das, was sie vorher verboten
hat -- und wird rot, wenn die Kachel je wieder verschwindet. Das
Umlenken selbst bleibt geprueft (Scouting, zweimal).
Das ist die DRITTE stille rote Pruefung an einem Tag (nach
pruef-modi-wortleck und pruef-zuteilung). Die Frage an Filipe, ob ein
naechtlicher Lauf sie selbst anstossen soll, steht in der Vault-Notiz
und wird nicht von mir allein entschieden.
GEPRUEFT
--------
pruef-modi-katalog: 116 Pruefungen, 0 Fehler (vorher 95).
Neu: die beiden Schalter, wer es erfaehrt (samt Gegenprobe, dass es
nicht einfach alle sind: 2 von 6), und der Wortlaut an acht Proben.
Dabei war meine eigene erste Messung falsch -- sie erwartete eine
Kuerzung bei 50 Zeichen, die nur gilt, wenn eine Notiz danebensteht.
Steht als Begruendung in der Pruefung.
pruef-push-ziel: 11 von 11 (vorher 1 Fehler).
pruef-zuteilung, pruef-push, pruef-push-weg: gruen.
Co-Authored-By: Claude Opus 5 <[email protected]>
1024 lines
52 KiB
JavaScript
1024 lines
52 KiB
JavaScript
/* =====================================================================
|
|
DER AUFGABENKATALOG AUS TEIL 2 DES ANFORDERUNGSDOKUMENTS
|
|
|
|
Kapitel 14: "Alle Aufgaben dieses Katalogs koennen 1:1 als Vorlagen in
|
|
die App importiert werden -- inklusive Kategorie, empfohlener Rolle
|
|
und Frequenz. So ist das Aufgaben-Board ab dem ersten Tag vollstaendig
|
|
befuellt, statt leer zu starten."
|
|
|
|
GEPRUEFT WIRD NICHT NUR, DASS ER DA IST. Ein Katalog, den man ansehen,
|
|
aber nicht uebernehmen kann, ist eine Speisekarte ohne Kueche. Und ein
|
|
uebernommener Eintrag, bei dem die Kategorie unterwegs verlorengeht,
|
|
ist die Haelfte wert -- dann sortiert man 60-mal von Hand ein, was im
|
|
Dokument bereits danebensteht.
|
|
|
|
DAZU DIE VERBORGENHEIT: Wer den Katalog nicht bekommt, darf auch nicht
|
|
erfahren, dass es ihn gibt. Eine Absage muss deshalb wortgleich mit
|
|
der fuer eine erfundene Art sein.
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-kat2-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
/* DER PORT MUSS MEINER SEIN (nachgeruestet 19.09.2026).
|
|
Ohne diese Zeile startet der eigene Server bei belegtem Port
|
|
STILL nicht -- und alles Folgende misst gegen einen fremden
|
|
Stand. Das ist der dritte Ausgang: belegt ist etwas anderes
|
|
als kaputt. */
|
|
const PORT = await eigenerPort(import.meta, "pruef-modi-katalog");
|
|
process.env.PORT = `${PORT}`;
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
|
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";
|
|
import { ersatzschreibungen } from "./helfer-deutsch.mjs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
await import("./index.js");
|
|
notbremse(180_000, "pruef-modi-katalog");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
let fehler = 0;
|
|
let geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
let offen = 0;
|
|
const unklar = (t) => { offen++; console.log(" -- " + t); };
|
|
|
|
/* ---------- Personen ------------------------------------------------- */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
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 idMan = anlegen("Schulle", "manager", "CODE-MAN-0001");
|
|
const idCreat = anlegen("Tili", "creator", "CODE-ANNA-0001");
|
|
const idModi = anlegen("Marina", "modi", "CODE-MODI-0001");
|
|
/* DIE BEIDEN HAENDE (23.09.2026). Ohne sie laesst sich die Grenze, die
|
|
Filipe gezogen hat, gar nicht messen: "nur dogfather und die rechte
|
|
hand sollen annehmen oder ablehnen koennen". Die linke Hand darf
|
|
verteilen -- und genau deshalb ist sie die schaerfste Gegenprobe:
|
|
Wer nur "darf verteilen" prueft, laesst sie mitentscheiden. */
|
|
const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
|
|
const idLinke = anlegen("Funny", "linke", "CODE-LINK-0001");
|
|
void idHand; void idLinke;
|
|
d.close();
|
|
void idDogi;
|
|
void idMan;
|
|
|
|
async function anmelden(rolle, code) {
|
|
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle, code }),
|
|
});
|
|
const keks = (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
|
return { status: a.status, keks };
|
|
}
|
|
async function hole(keks, pfad, aufruf = {}) {
|
|
const a = await fetch(BASIS + pfad, {
|
|
...aufruf, headers: { ...(aufruf.headers || {}), cookie: keks },
|
|
});
|
|
return { status: a.status, daten: await a.json().catch(() => null) };
|
|
}
|
|
const jsonKopf = { "Content-Type": "application/json" };
|
|
|
|
const modiAn = await anmelden("creator", "CODE-MODI-0001");
|
|
const dogiAn = await anmelden("admin", "CODE-DOGI-0001");
|
|
const manAn = await anmelden("manager", "CODE-MAN-0001");
|
|
const creatAn = await anmelden("creator", "CODE-ANNA-0001");
|
|
/* Ueber den stillen Zugang -- welche Kachel man anfasst, ist
|
|
gleichgueltig, der Code allein entscheidet (siehe die Anmelderoute). */
|
|
const handAn = await anmelden("creator", "CODE-HAND-0001");
|
|
const linkeAn = await anmelden("creator", "CODE-LINK-0001");
|
|
|
|
/* ---------- 1. Ist er vollstaendig da? ------------------------------- */
|
|
console.log("\n=== 1. Der Katalog, wie ihn das Dokument beschreibt ===\n");
|
|
|
|
let katalog = null;
|
|
{
|
|
const a = await hole(modiAn.keks, "/workspace/api/vorlagen");
|
|
katalog = a.daten?.katalog || null;
|
|
/* SEIT DEM 16.09.2026 NACH KATEGORIE UND STUFE.
|
|
|
|
Vorher standen hier neun PHASEN mit 60 Eintraegen -- und
|
|
nachgezaehlt gehoerten 39 davon DogFather und der rechten Hand.
|
|
Es war ein Aufbauplan, kein Arbeitskatalog. Der Aufbauplan gibt es
|
|
weiterhin, unter `aufbauplan`; hier steht jetzt, was ein Mensch im
|
|
Team TUT.
|
|
|
|
ZAHLEN FEST HINGESCHRIEBEN, wie vorher: Eine Zahl, die sich selbst
|
|
nachzaehlt, merkt nicht, wenn etwas herausfaellt. */
|
|
ok(katalog && katalog.gruppen.length === 14,
|
|
`${katalog?.gruppen?.length ?? 0} Kategorien (erwartet 14)`);
|
|
ok(katalog && katalog.stufen.length === 3,
|
|
`${katalog?.stufen?.length ?? 0} Stufen (erwartet 3)`);
|
|
ok(katalog && katalog.aufgaben.length === 101,
|
|
`${katalog?.aufgaben?.length ?? 0} Aufgaben (erwartet 101)`);
|
|
|
|
/* JEDE KATEGORIE MUSS BELEGT SEIN. Vorher hatten vier von vierzehn
|
|
eine Aufgabe oder keine (Events 1, Wachstum 1, Branding 1,
|
|
Sonstiges 0) -- eine Kategorie ohne Inhalt ist ein leeres
|
|
Versprechen im Auswahlfeld. */
|
|
const je = {};
|
|
for (const v of katalog?.aufgaben || []) je[v.gruppe] = (je[v.gruppe] || 0) + 1;
|
|
const leer = (katalog?.gruppen || []).filter((g) => !je[g.schluessel]);
|
|
ok(leer.length === 0,
|
|
`jede Kategorie hat Aufgaben${leer.length ? ": leer sind " + leer.map((g) => g.name).join(", ") : ""}`);
|
|
ok(Math.min(...Object.values(je)) >= 3,
|
|
`auch die kleinste hat mindestens drei (${Math.min(...Object.values(je))})`);
|
|
|
|
/* UND JEDE STUFE. Eine Stufe ohne Aufgaben waere ein Knopf, der eine
|
|
leere Liste oeffnet. */
|
|
const jeStufe = {};
|
|
for (const v of katalog?.aufgaben || []) jeStufe[v.stufe] = (jeStufe[v.stufe] || 0) + 1;
|
|
ok((katalog?.stufen || []).every((s) => jeStufe[s.schluessel] > 0),
|
|
`jede Stufe ist belegt (${Object.entries(jeStufe).map(([k, n]) => `${k}:${n}`).join(" ")})`);
|
|
|
|
ok((katalog?.aufgaben || []).every((v) => v.titel && v.text && v.gruppe && v.stufe && v.tage),
|
|
"jede Aufgabe hat Titel, Text, Kategorie, Stufe und Frist");
|
|
/* DER TEXT SAGT WARUM, nicht was. Ein Text, der kuerzer ist als der
|
|
Titel, ist eine Wiederholung -- und die haette man sich sparen
|
|
koennen. */
|
|
const duenn = (katalog?.aufgaben || []).filter((v) => v.text.length < v.titel.length + 20);
|
|
ok(duenn.length === 0,
|
|
`jeder Text begruendet${duenn.length ? ": zu duenn bei " + duenn[0].schluessel : ""}`);
|
|
ok(new Set((katalog?.aufgaben || []).map((v) => v.schluessel)).size
|
|
=== (katalog?.aufgaben || []).length, "keine Kennung doppelt");
|
|
|
|
/* DER AUFBAUPLAN IST NICHT VERLORENGEGANGEN. */
|
|
const plan = a.daten?.aufbauplan;
|
|
ok(plan && plan.aufgaben.length === 60 && plan.gruppen.length === 9,
|
|
`der Aufbauplan steht weiter zur Verfuegung (${plan?.aufgaben?.length ?? 0} in `
|
|
+ `${plan?.gruppen?.length ?? 0} Etappen)`);
|
|
}
|
|
|
|
/* Jede Kategorie im Katalog muss es auch wirklich geben -- sonst nimmt
|
|
der Server sie beim Uebernehmen an und lehnt sie beim Aendern ab. */
|
|
{
|
|
const a = await hole(modiAn.keks, "/workspace/api/aufgaben");
|
|
const gueltig = new Set((a.daten?.kategorien || []).map((k) => k.wert));
|
|
/* Die Kategorie heisst im ausgelieferten Katalog `gruppe` -- sie ist
|
|
zugleich die Gruppe, nach der die Oberflaeche sortiert. Ein
|
|
zweiter Name fuer dasselbe waere die Gelegenheit, dass beide
|
|
auseinanderlaufen. */
|
|
const unbekannt = [...new Set((katalog?.aufgaben || []).map((v) => v.gruppe))]
|
|
.filter((k) => !gueltig.has(k));
|
|
ok(gueltig.size > 0 && unbekannt.length === 0,
|
|
unbekannt.length
|
|
? `unbekannte Kategorie(n) im Katalog: ${unbekannt.join(", ")}`
|
|
: `alle ${gueltig.size} Kategorien passen zum Katalog`);
|
|
}
|
|
|
|
/* ---------- 2. Wer bekommt ihn nicht? -------------------------------- */
|
|
console.log("\n=== 2. Und wer ihn nicht bekommt ===\n");
|
|
for (const [name, an] of [["Manager", manAn], ["Creator", creatAn]]) {
|
|
const a = await hole(an.keks, "/workspace/api/vorlagen");
|
|
ok(a.status === 200 && a.daten?.katalog === null,
|
|
`${name}: kein Katalog (${JSON.stringify(a.daten?.katalog)})`);
|
|
/* Und der ANDERE Katalog ist bei ihnen sehr wohl da -- sonst waere
|
|
oben auch gruen, wenn die ganze Antwort leer kaeme. */
|
|
ok(a.daten?.aufgaben && Object.keys(a.daten.aufgaben).length > 0,
|
|
`${name}: der Creator-Katalog dagegen schon`);
|
|
}
|
|
{
|
|
const a = await hole(modiAn.keks, "/workspace/api/vorlagen");
|
|
ok(a.daten?.aufgaben === null,
|
|
"und der Modi bekommt umgekehrt den Creator-Katalog nicht");
|
|
}
|
|
|
|
/* ---------- 3. Ein Modi kann sich nur BEWERBEN ------------------------
|
|
|
|
Filipe, 23.09.2026: "die modis sollen bei all diesen voschlaegen
|
|
auch nur bewerben koennen. die aufgaben aus der vorlage, da sollen
|
|
die modis sich nur bewerben koennen und nur dogfather und die rechte
|
|
hand sollen annehmen oder ablehnen koennen, mit einem text als
|
|
notiz."
|
|
|
|
BIS HEUTE STAND HIER DAS GEGENTEIL: "uebernommen (200)", "und er
|
|
selbst ist verantwortlich". Diese Pruefung ist beim Umbau ROT
|
|
geworden -- richtig so, sie hat die Aenderung bemerkt.
|
|
|
|
DREI DINGE WERDEN GEMESSEN, und das dritte ist das, was Filipe
|
|
ausdruecklich wollte:
|
|
1. Der Modi kommt an die alte Tuer nicht mehr heran.
|
|
2. Die Bewerbung kommt an -- und legt NOCH KEINE Aufgabe an.
|
|
3. Erst die Zusage macht daraus Arbeit, mit Notiz daran.
|
|
--------------------------------------------------------------------- */
|
|
console.log("\n=== 3. Ein Modi kann sich nur bewerben ===\n");
|
|
|
|
let bewerbungsNr = null, bewerbungsVorlage = null;
|
|
{
|
|
const nr = katalog.aufgaben.findIndex((v) => v.gruppe === "chat");
|
|
const vorlage = katalog.aufgaben[nr];
|
|
bewerbungsVorlage = vorlage;
|
|
|
|
/* ---- 1. Die alte Tuer ist zu -------------------------------------- */
|
|
const alt = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog", nr, gruppe: "chat" }),
|
|
});
|
|
ok(alt.status === 403, `selbst uebernehmen geht nicht mehr (${alt.status})`);
|
|
/* DER SATZ IST EIN WEGWEISER, KEINE ABSAGE. Wer "Das darfst du
|
|
nicht" liest, haelt sich fuer ausgeschlossen -- es gibt aber einen
|
|
Weg, er fuehrt nur ueber jemanden. */
|
|
ok(alt.daten?.fehler === "erst_bewerben",
|
|
` und sagt, was stattdessen geht (${alt.daten?.fehler})`);
|
|
|
|
const keine = await hole(modiAn.keks, "/workspace/api/aufgaben");
|
|
const gibtsNicht = (keine.daten?.aufgaben || [])
|
|
.some((x) => x.vorlage === vorlage.schluessel);
|
|
ok(!gibtsNicht, " und es liegt nichts auf seinem Brett");
|
|
|
|
/* ---- 2. Die Bewerbung ---------------------------------------------- */
|
|
const b = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: vorlage.schluessel, text: "ich habe Freitag Zeit" }),
|
|
});
|
|
ok(b.status === 201, `die Bewerbung kommt an (${b.status})`);
|
|
|
|
/* DAS IST DER KERN DER BAUWEISE: Bis jemand ja sagt, entsteht keine
|
|
Arbeit. Waere die Aufgabe schon da, stuenden nach zwoelf Absagen
|
|
zwoelf herrenlose Aufgaben auf dem Brett -- und das Ablehnen
|
|
muesste Aufgaben LOESCHEN, um das wieder einzufangen. */
|
|
const nochNicht = await hole(modiAn.keks, "/workspace/api/aufgaben");
|
|
ok(!(nochNicht.daten?.aufgaben || []).some((x) => x.vorlage === vorlage.schluessel),
|
|
" und legt noch KEINE Aufgabe an — das tut erst die Zusage");
|
|
|
|
const meine = await hole(modiAn.keks, "/workspace/api/vorlagen");
|
|
const meineB = (meine.daten?.bewerbungen || [])
|
|
.find((x) => x.vorlage === vorlage.schluessel);
|
|
ok(!!meineB, "er sieht seine eigene Bewerbung");
|
|
ok(meineB?.selbst === true && meineB?.text === "ich habe Freitag Zeit",
|
|
` samt seinem Wort dazu ("${meineB?.text}")`);
|
|
ok(meine.daten?.darf_entscheiden === false,
|
|
"und der Server sagt ihm, dass er nicht entscheidet");
|
|
bewerbungsNr = meineB?.id ?? null;
|
|
|
|
/* Zweimal dieselbe Bitte ist eine Bitte. */
|
|
const doppelt = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: vorlage.schluessel }),
|
|
});
|
|
ok(doppelt.status === 409 && doppelt.daten?.fehler === "schon_beworben",
|
|
`zweimal dieselbe Bewerbung wird abgewiesen (${doppelt.status})`);
|
|
}
|
|
|
|
/* ---------- 3b. Wer entscheidet -- und wer nicht ----------------------
|
|
|
|
DIE LINKE HAND IST HIER DIE SCHAERFSTE PROBE. Sie darf verteilen und
|
|
gehoert sonst ueberall dazu; Filipe hat bei der Entscheidung aber
|
|
genau zwei genannt. Wer statt "darf entscheiden" nur "darf
|
|
verteilen" prueft, laesst sie durch -- und niemand merkt es, weil
|
|
alles funktioniert.
|
|
--------------------------------------------------------------------- */
|
|
console.log("\n=== 3b. Wer über die Bewerbung entscheidet ===\n");
|
|
{
|
|
ok(Number.isInteger(bewerbungsNr), `die Bewerbung hat eine Nummer (${bewerbungsNr})`);
|
|
|
|
const alsLinke = await hole(linkeAn.keks, "/workspace/api/vorlagen");
|
|
ok(alsLinke.daten?.darf_verteilen === true,
|
|
"die linke Hand darf verteilen (sie bekommt den Übernehmen-Weg)");
|
|
ok(alsLinke.daten?.darf_entscheiden === false,
|
|
" aber NICHT entscheiden — woertlich nach Filipes Aufzaehlung");
|
|
const versuch = await hole(linkeAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: bewerbungsNr, kommentar: "mach ich" }),
|
|
});
|
|
ok(versuch.status === 403,
|
|
` und am Knopf vorbei geht es auch nicht (${versuch.status})`);
|
|
|
|
/* GEGENPROBE: Der Modi selbst erst recht nicht -- sonst naehme er
|
|
seine eigene Bewerbung an. */
|
|
const selbst = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: bewerbungsNr }),
|
|
});
|
|
ok(selbst.status === 403, `und der Bewerber selbst schon gar nicht (${selbst.status})`);
|
|
|
|
/* Die rechte Hand sieht sie -- mit Namen, denn sie entscheidet ueber
|
|
einen Menschen und nicht ueber einen Listeneintrag. */
|
|
const alsHand = await hole(handAn.keks, "/workspace/api/vorlagen");
|
|
const sieht = (alsHand.daten?.bewerbungen || []).find((x) => x.id === bewerbungsNr);
|
|
ok(alsHand.daten?.darf_entscheiden === true, "die rechte Hand entscheidet");
|
|
ok(!!sieht && sieht.person_name === "Marina",
|
|
` und sieht, wer sich beworben hat (${sieht?.person_name})`);
|
|
ok(sieht?.selbst === false, " als fremde Bewerbung, nicht als eigene");
|
|
}
|
|
|
|
/* ---------- 3c. Die Zusage macht daraus Arbeit ------------------------ */
|
|
console.log("\n=== 3c. Annehmen und ablehnen — mit Notiz ===\n");
|
|
{
|
|
const a = await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: bewerbungsNr, kommentar: "danke dir, mach das gern" }),
|
|
});
|
|
ok(a.status === 200 && a.daten?.zustand === "angenommen",
|
|
`die rechte Hand sagt zu (${a.status})`);
|
|
ok(Number.isInteger(a.daten?.aufgabe_id),
|
|
` und JETZT entsteht die Aufgabe (#${a.daten?.aufgabe_id})`);
|
|
|
|
const brett = await hole(modiAn.keks, "/workspace/api/aufgaben");
|
|
const neu = (brett.daten?.aufgaben || [])
|
|
.find((x) => x.vorlage === bewerbungsVorlage.schluessel);
|
|
ok(!!neu, "sie liegt auf seinem Brett");
|
|
/* SIE MUSS AUSSEHEN WIE EINE VERTEILTE. Ein zweiter Weg zum Anlegen
|
|
waere ein zweiter Satz Regeln -- deshalb geht die Zusage durch
|
|
dieselbe Funktion wie das Uebernehmen. */
|
|
ok(neu?.kategorie === bewerbungsVorlage.gruppe,
|
|
`mit ihrer Kategorie (${neu?.kategorie} / erwartet ${bewerbungsVorlage.gruppe})`);
|
|
ok(!!neu?.frist, `und mit Frist (${neu?.frist})`);
|
|
ok(neu?.verantwortlich_id === idModi, "und der Bewerber ist verantwortlich");
|
|
|
|
/* Die Bewerbung ist damit vom Tisch -- sonst entschiede gleich noch
|
|
jemand ein zweites Mal darueber. */
|
|
const danach = await hole(handAn.keks, "/workspace/api/vorlagen");
|
|
ok(!(danach.daten?.bewerbungen || []).some((x) => x.id === bewerbungsNr),
|
|
"und sie wartet nicht mehr auf eine Antwort");
|
|
const nochmal = await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: bewerbungsNr }),
|
|
});
|
|
ok(nochmal.status === 409 && nochmal.daten?.fehler === "keine_bewerbung",
|
|
`zweimal entscheiden geht nicht (${nochmal.status})`);
|
|
|
|
/* ---- UND DIE ABSAGE, mit Notiz ------------------------------------ */
|
|
const nr2 = katalog.aufgaben.findIndex((v) => v.gruppe === "events");
|
|
const v2 = katalog.aufgaben[nr2];
|
|
await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: v2.schluessel }),
|
|
});
|
|
const offen2 = (await hole(handAn.keks, "/workspace/api/vorlagen"))
|
|
.daten?.bewerbungen?.find((x) => x.vorlage === v2.schluessel);
|
|
ok(!!offen2, "eine zweite Bewerbung liegt vor");
|
|
ok(offen2?.text === null,
|
|
" auch ohne Wort dazu — der Text ist freiwillig, wie bei den Aufgaben");
|
|
|
|
const ab = await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/ablehnen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: offen2.id, kommentar: "das macht diesmal jemand anders" }),
|
|
});
|
|
ok(ab.status === 200 && ab.daten?.zustand === "abgelehnt",
|
|
`und wird abgelehnt (${ab.status})`);
|
|
ok(ab.daten?.aufgabe_id === null,
|
|
" ohne dass Arbeit entsteht, die niemand bestellt hat");
|
|
|
|
/* DIE NOTIZ BLEIBT STEHEN -- das ist der Teil, den Filipe
|
|
ausdruecklich genannt hat ("mit einem text als notiz"). Gelesen
|
|
wird direkt aus der Datenbank: Ein Feld, das der Server zwar
|
|
annimmt, aber nirgends speichert, saehe von aussen genauso aus. */
|
|
const { DatabaseSync: DB2 } = await import("node:sqlite");
|
|
const d2 = new DB2(process.env.WORKSPACE_DB, { readOnly: true });
|
|
const zeilen = d2.prepare(`SELECT zustand, entscheid_text, entschieden_von, aufgabe_id
|
|
FROM vorlagen_bewerbungen ORDER BY id`).all();
|
|
d2.close();
|
|
const zu = zeilen.find((z) => z.zustand === "angenommen");
|
|
const weg = zeilen.find((z) => z.zustand === "abgelehnt");
|
|
ok(zu?.entscheid_text === "danke dir, mach das gern",
|
|
`die Notiz der Zusage steht in der Datenbank ("${zu?.entscheid_text}")`);
|
|
ok(weg?.entscheid_text === "das macht diesmal jemand anders",
|
|
`und die der Absage auch ("${weg?.entscheid_text}")`);
|
|
ok(zu?.entschieden_von === idHand && weg?.entschieden_von === idHand,
|
|
"bei beiden steht, WER entschieden hat");
|
|
ok(Number.isInteger(zu?.aufgabe_id) && weg?.aufgabe_id === null,
|
|
"und nur an der Zusage haengt eine Aufgabe");
|
|
|
|
/* ---- Nach einer Absage darf man es wieder versuchen ---------------- */
|
|
const nochmalBewerben = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: v2.schluessel, text: "doch noch Zeit" }),
|
|
});
|
|
ok(nochmalBewerben.status === 201,
|
|
`nach einer Absage kann man sich wieder bewerben (${nochmalBewerben.status})`);
|
|
/* GEGENPROBE zur Sperre oben: Waere der Index ueber ALLE Zustaende
|
|
eindeutig, kaeme hier 409 -- und eine Absage waere ein Bann. */
|
|
const wiederOffen = (await hole(handAn.keks, "/workspace/api/vorlagen"))
|
|
.daten?.bewerbungen?.filter((x) => x.vorlage === v2.schluessel).length;
|
|
ok(wiederOffen === 1, `und sie wartet wieder auf Antwort (${wiederOffen})`);
|
|
|
|
/* Und auf etwas, das man schon hat, bewirbt man sich nicht. */
|
|
const doppelt = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: bewerbungsVorlage.schluessel }),
|
|
});
|
|
ok(doppelt.status === 409 && doppelt.daten?.fehler === "liegt_schon_bei_dir",
|
|
`auf etwas, das schon auf dem Brett liegt, bewirbt man sich nicht (${doppelt.status})`);
|
|
}
|
|
|
|
/* ---------- 4. DogFather traegt fuer jemanden ein -------------------- */
|
|
console.log("\n=== 4. DogFather trägt für einen Modi ein ===\n");
|
|
{
|
|
const nr = katalog.aufgaben.findIndex((v) => v.gruppe === "analyse");
|
|
const a = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog", nr, gruppe: "analyse", person_id: idModi }),
|
|
});
|
|
ok(a.status === 200 && a.daten?.angelegt === 1, `für den Modi geht es (${a.status})`);
|
|
|
|
/* Aber nicht an einen Creator -- sonst laege eine Aufgabe mit
|
|
Modi-Kategorie an jemandem, der die Kategorien gar nicht kennt, und
|
|
die Trennung waere von innen aufgeweicht. */
|
|
const b = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog", nr, gruppe: "monat", person_id: idCreat }),
|
|
});
|
|
ok(b.status === 404, `an einen Creator dagegen nicht (${b.status})`);
|
|
|
|
/* Und ohne Person gar nicht -- eine Aufgabe ohne Adressaten ist ein
|
|
Zettel. */
|
|
const c = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog", nr, gruppe: "monat" }),
|
|
});
|
|
ok(c.status === 400 && /Person/i.test(c.daten?.fehler || ""),
|
|
`ohne Person wird gefragt (${c.status} ${c.daten?.fehler || ""})`);
|
|
}
|
|
|
|
/* ---------- 5. Der heikelste Fall ------------------------------------ */
|
|
console.log("\n=== 5. Ein Manager versucht es trotzdem ===\n");
|
|
{
|
|
const a = await hole(manAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog", nr: 0, gruppe: "aufbau", person_id: idModi }),
|
|
});
|
|
const b = await hole(manAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "gibtesnicht", nr: 0 }),
|
|
});
|
|
/* WORTGLEICH. Ein eigener Fehlertext waere die Auskunft, dass es die
|
|
Art gibt -- und damit, dass es den Katalog gibt. */
|
|
ok(a.status === b.status && JSON.stringify(a.daten) === JSON.stringify(b.daten),
|
|
`klingt wie eine erfundene Art (${a.status} ${JSON.stringify(a.daten)})`);
|
|
}
|
|
|
|
/* ---------- 6. Im Browser -------------------------------------------- */
|
|
console.log("\n=== 6. Das Brett im echten Browser ===\n");
|
|
{
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
try {
|
|
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); });
|
|
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click('.rolle[data-rolle="creator"]');
|
|
await seite.fill("#code", "CODE-MODI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
|
await seite.goto(BASIS + "/workspace/aufgaben.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1200);
|
|
|
|
/* Aufklappen, falls es zugeklappt startet. */
|
|
const zu = await seite.evaluate(() =>
|
|
document.getElementById("vorlagenbrett")?.dataset.auf !== "ja");
|
|
if (zu) {
|
|
await seite.click(".vorlagenbrett__kopf").catch(() => {});
|
|
await seite.waitForTimeout(700);
|
|
}
|
|
|
|
const bild = await seite.evaluate(() => ({
|
|
sichtbar: !document.getElementById("vorlagenbrett")?.hidden,
|
|
/* ZWEI REIHEN SEIT DEM 16.09.2026: oben die Kategorien, darunter
|
|
die Stufen. Getrennt gezaehlt -- eine gemeinsame Zahl haette
|
|
nicht gesagt, WELCHE der beiden fehlt. */
|
|
/* UEBER DIE BESCHRIFTUNG UNTERSCHIEDEN, nicht ueber eine
|
|
Hilfsklasse (16.09.2026). Seit die Stufenreihe genauso aussieht
|
|
wie die Kategorienreihe -- so wollte Filipe es -- gibt es im
|
|
Aussehen keinen Unterschied mehr. Den gibt es aber im SINN, und
|
|
der steht ohnehin da: in dem Wort, das ein Vorleseprogramm
|
|
ansagt. Eine Klasse nur zum Wiederfinden waere eine zweite
|
|
Wahrheit ueber dieselbe Sache. */
|
|
kategorien: [...document.querySelectorAll(
|
|
'[aria-label="Kategorie wählen"] .stufenleiter__name')]
|
|
.map((z) => z.textContent.trim()),
|
|
stufen: [...document.querySelectorAll(
|
|
'[aria-label="Stufe wählen"] .stufenleiter__name')]
|
|
.map((z) => z.textContent.trim()),
|
|
karten: document.querySelectorAll(".vorlagenbrett__karte").length,
|
|
/* WIE VIELE ZEILEN BRAUCHT DIE FRIST? Gemessen ueber einen
|
|
Bereich um den TEXT, nicht ueber das Element.
|
|
|
|
ERST FALSCH GEMESSEN, und die Gegenprobe hat es verraten:
|
|
`element.getClientRects()` liefert hier IMMER genau ein
|
|
Rechteck, weil ein Flex-Kind zu einem Block wird -- die
|
|
Pruefung blieb gruen, auch mit dem Fehler wieder eingebaut.
|
|
Ein Bereich ueber den Textknoten dagegen gibt pro Zeilenkasten
|
|
ein Rechteck, und genau das ist die Frage: Steht "in 2 Tagen"
|
|
auf einer Zeile oder als "in / 2 / Tagen" untereinander? */
|
|
fristZeilen: [...document.querySelectorAll(".vorlagenbrett__frist")]
|
|
.map((z) => {
|
|
const b = document.createRange();
|
|
b.selectNodeContents(z);
|
|
return b.getClientRects().length;
|
|
}),
|
|
/* Und wie viele Karten tragen ueberhaupt den DRITTEN Eintrag, an
|
|
dem es eng wurde. Ohne diese Zahl wuerde die Zeile darueber
|
|
auch dann gruen, wenn der enge Fall gar nicht auf dem Schirm
|
|
war. */
|
|
engeKarten: document.querySelectorAll(".vorlagenbrett__schon").length,
|
|
text: document.body.innerText,
|
|
kopf: (document.querySelector(".kopfleiste .marke__text") || {}).textContent || "",
|
|
}));
|
|
ok(bild.sichtbar, "das Brett steht da");
|
|
ok(bild.kategorien.length === 14,
|
|
`mit vierzehn Kategorien (${bild.kategorien.join(" · ")})`);
|
|
/* DREI STUFEN PLUS "Alle" -- der vierte Knopf ist die Ruecknahme.
|
|
Ohne ihn kaeme man aus einer Stufe nicht wieder heraus, ohne die
|
|
Kategorie zu wechseln. */
|
|
ok(bild.stufen.length === 4 && bild.stufen[0] === "Alle",
|
|
`und drei Stufen daneben (${bild.stufen.join(" · ")})`);
|
|
ok(bild.karten > 0, `und ${bild.karten} Karten in der offenen Etappe`);
|
|
/* Der Katalog des ANDEREN darf nirgends durchschlagen. */
|
|
ok(!/vier Bereichen und vier Stufen/.test(bild.text),
|
|
"kein Wort vom Creator-Katalog");
|
|
/* DIE KOPFLEISTE. Auf jeder Seite steht dort fest "Creator
|
|
Workspace" -- fuer jemanden, der moderiert statt einen Kanal
|
|
aufzubauen, ist das schlicht der falsche Name. Aufgefallen auf
|
|
dem Bildschirmfoto, wie schon die Zierzeile auf der Startseite. */
|
|
ok(bild.kopf.includes("Team") && !bild.kopf.includes("Creator"),
|
|
`die Kopfleiste nennt seinen Betrieb ("${bild.kopf.trim()}")`);
|
|
/* Und der Seitenname dahinter steht noch da -- ihn beim Austausch
|
|
zu verlieren waere lautlos passiert. */
|
|
ok(/Aufgaben/.test(bild.kopf), "der Seitenname dahinter blieb erhalten");
|
|
/* DIE FUSSZEILE DER KARTEN (16.09.2026, auf dem Bildschirmfoto
|
|
gefunden). Bei einer schon uebernommenen Aufgabe stehen dort
|
|
drei Dinge statt zwei, und "Frist: in 2 Tagen" brach mitten im
|
|
Wort auf drei Zeilen um. Die Karten sind alle gleich breit --
|
|
eine einzige gequetschte reicht, um den Fehler zu zeigen. */
|
|
ok(bild.engeKarten > 0,
|
|
`${bild.engeKarten} Karten tragen den dritten Fusseintrag (der enge Fall)`);
|
|
const zuViel = bild.fristZeilen.filter((n) => n !== 1).length;
|
|
ok(bild.fristZeilen.length > 0 && zuViel === 0,
|
|
`jede der ${bild.fristZeilen.length} Fristangaben steht auf einer Zeile`
|
|
+ (zuViel ? ` -- ${zuViel} umgebrochen` : ""));
|
|
|
|
/* UMLAUTE AUF DEM FERTIGEN BILDSCHIRM (16.09.2026).
|
|
Am 16.09. stand unter jeder Kategorie eine Erklaerung in ASCII:
|
|
"Was im Livechat passiert, waehrend gesendet wird. Loeschen,
|
|
stummschalten, begruessen". Alles gruen, niemand hat es gemerkt.
|
|
|
|
HIER und nicht im Quelltext, weil hier der Unterschied sichtbar
|
|
wird, auf den es ankommt: `innerText` ist, was ein Mensch liest.
|
|
Feldnamen, Stilklassen und Adressen -- die ASCII sein MUESSEN --
|
|
stehen nicht drin. Im Quelltext derselben Skripte schlaegt
|
|
dieselbe Liste 71 Mal an, und kein einziges Mal zu Recht.
|
|
|
|
Die Kataloge und die festen Seitentexte prueft
|
|
pruef-deutsche-texte, ohne Browser. */
|
|
const ersatz = ersatzschreibungen(bild.text);
|
|
ok(bild.text.length > 400 && ersatz.length === 0,
|
|
ersatz.length
|
|
? `Ersatzschreibungen auf dem Brett: ${ersatz.join(", ")}`
|
|
: `der Text auf dem Brett traegt Umlaute (${bild.text.length} Zeichen geprueft)`);
|
|
/* GEGENPROBE, sonst hiesse "nichts gefunden" nur, dass nichts
|
|
gesucht wurde -- etwa wenn `innerText` leer bliebe. */
|
|
ok(ersatzschreibungen(`${bild.text} waehrend`).length === 1,
|
|
"und ein eingeschmuggeltes waehrend wuerde auffallen");
|
|
/* ==== AM BILDSCHIRM: BEWERBEN STATT UEBERNEHMEN (23.09.2026) ===
|
|
|
|
Filipe: „die modis sollen bei all diesen voschlaegen auch nur
|
|
bewerben koennen."
|
|
|
|
ALLES BISHERIGE HAT DEN SERVER GEPRUEFT. Hier geht es um die
|
|
andere Haelfte: Steht der richtige Knopf da? Ein Modi, der
|
|
„Uebernehmen" liest und dann eine Absage bekommt, haelt die
|
|
Seite fuer kaputt. */
|
|
const knoepfe = await seite.evaluate(() =>
|
|
[...document.querySelectorAll(".vorlagenbrett__karte .knopf")]
|
|
.map((k) => k.textContent.trim()));
|
|
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Bei null Karten waere
|
|
`every()` wahr, ohne einen Knopf angesehen zu haben -- genau der
|
|
Fehler, der am 01.09. dreimal gruen dastand. */
|
|
ok(knoepfe.length >= 10 && knoepfe.every((t) => t === "Bewerben"),
|
|
`alle ${knoepfe.length} Knoepfe heissen "Bewerben" (${[...new Set(knoepfe)].join(", ")})`);
|
|
/* GEGENPROBE: „Uebernehmen" kommt nirgends mehr vor -- weder als
|
|
Knopf noch als „Alle 12 uebernehmen" darunter. */
|
|
const uebernahme = await seite.evaluate(() =>
|
|
[...document.querySelectorAll("#vorlagenbrett button")]
|
|
.filter((k) => /übernehmen/i.test(k.textContent)).length);
|
|
ok(uebernahme === 0,
|
|
`Gegenprobe: kein einziger Uebernehmen-Knopf mehr (${uebernahme})`);
|
|
|
|
/* Die schon beantragte Karte sagt, dass sie wartet -- und WER
|
|
antwortet. Sonst wartet man auf niemanden.
|
|
|
|
ERST ZUR RICHTIGEN KATEGORIE. Die offene Bewerbung liegt bei
|
|
„Events", das Brett startet bei der ersten Kategorie. Ohne
|
|
diesen Klick maesse die Pruefung eine Karte, die gar nicht auf
|
|
dem Bildschirm steht -- und waere rot, ohne dass etwas kaputt
|
|
ist. (Beim Bauen genau so passiert.) */
|
|
await seite.evaluate(() => {
|
|
const reihe = document.querySelector('.vorlagenbrett .stufenleiter');
|
|
const k = [...(reihe?.querySelectorAll('button') || [])]
|
|
.find((b) => /Events/i.test(b.textContent));
|
|
if (k) k.click();
|
|
});
|
|
await seite.waitForTimeout(700);
|
|
const wartet = await seite.evaluate(() => {
|
|
const w = document.querySelector(".vorlagenbrett__warte");
|
|
const karte = w?.closest(".vorlagenbrett__karte");
|
|
return w ? {
|
|
satz: w.textContent.trim(),
|
|
zurueck: !![...karte.querySelectorAll("button")]
|
|
.find((b) => /zurückziehen/i.test(b.textContent)),
|
|
} : null;
|
|
});
|
|
ok(!!wartet, "die Karte, auf die er sich beworben hat, sagt es");
|
|
ok(/rechte hand/i.test(wartet?.satz || ""),
|
|
` und nennt, wer antwortet ("${wartet?.satz}")`);
|
|
ok(wartet?.zurueck === true, " mit einem Weg zurueck");
|
|
|
|
ok(konsole.length === 0,
|
|
konsole.length ? `Fehler in der Konsole: ${konsole[0]}` : "keine Fehler in der Konsole");
|
|
/* DER PFAD GEHOERT AN DIESE DATEI, nicht ans Arbeitsverzeichnis.
|
|
Vorher stand hier ein blosser Dateiname: Das Bild landete dort,
|
|
von wo aus gestartet wurde, waehrend die Zeile darunter
|
|
"server/..." meldete. Man sucht es dann an der falschen Stelle
|
|
-- oder findet ein altes und haelt es fuer das neue. */
|
|
const bildPfad = fileURLToPath(new URL("pruef-modi-katalog.png", import.meta.url));
|
|
await seite.screenshot({ path: bildPfad, fullPage: false });
|
|
console.log(" -- Bildschirmfoto: server/pruef-modi-katalog.png");
|
|
await kontext.close();
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
/* =======================================================================
|
|
6b. AN WEN? — die Auswahl steht dort, wo entschieden wird (16.09.2026)
|
|
|
|
Filipe: „perfektionniere die kategorien wo ich aufgaben an die modis
|
|
verteile und so."
|
|
|
|
Bis heute kam die Person aus dem Formular GANZ OBEN auf der Seite:
|
|
dort wählen, herunterscrollen, „Übernehmen" drücken. Wer das nicht
|
|
weiß, bekommt „Bitte zuerst eine Person wählen" und sucht.
|
|
|
|
Und man sah beim Vergeben nicht, wer schon wie viel offen hat. Zu
|
|
wenig Zeit ist der meistgenannte Grund, warum Moderatoren aufhören;
|
|
die Person, die verteilt, ist die einzige, die das verhindern kann.
|
|
Dafür muss die Zahl dort stehen, wo entschieden wird — nicht auf
|
|
einer Auswertung, die man hinterher aufruft.
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("=== 6b. An wen die Aufgabe geht ===");
|
|
console.log("");
|
|
{
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
try {
|
|
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1100 } });
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); });
|
|
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click('.rolle[data-rolle="admin"]');
|
|
await seite.fill("#code", "CODE-DOGI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
|
await seite.goto(BASIS + "/workspace/aufgaben.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1400);
|
|
const zu = await seite.evaluate(() =>
|
|
document.getElementById("vorlagenbrett")?.dataset.auf !== "ja");
|
|
if (zu) {
|
|
await seite.click(".vorlagenbrett__kopf").catch(() => {});
|
|
await seite.waitForTimeout(800);
|
|
}
|
|
|
|
const wahl = await seite.evaluate(() => {
|
|
const reihen = [...document.querySelectorAll(".stufenleiter")];
|
|
const letzte = document.querySelector('[aria-label="Person wählen"]');
|
|
const zahl = /^[0-9]+$/;
|
|
return {
|
|
reihen: reihen.length,
|
|
namen: [...(letzte?.querySelectorAll(".stufenleiter__name") || [])]
|
|
.map((z) => z.textContent.trim()),
|
|
zahlenOk: [...(letzte?.querySelectorAll(".stufenleiter__zahl") || [])]
|
|
.every((z) => zahl.test(z.textContent.trim())),
|
|
zahlen: [...(letzte?.querySelectorAll(".stufenleiter__zahl") || [])]
|
|
.map((z) => z.textContent.trim()),
|
|
/* Der Satz steht seit dem 16.09.2026 in `stufenleiter__hilfe`
|
|
-- derselbe Baustein wie beim anderen Brett. Vorher war es
|
|
`vorlagenbrett__stand`, und das ist die Zeile fuer Zahlen. */
|
|
satz: [...document.querySelectorAll(".stufenleiter__hilfe")]
|
|
.map((z) => z.textContent.trim())
|
|
.find((t) => t.includes("Formular oben") || t.includes("gehen an")) || "",
|
|
};
|
|
});
|
|
/* ZWEI FEINE REIHEN: die Stufen und die Personen. Eine einzelne
|
|
Zahl haette nicht gesagt, welche der beiden fehlt. */
|
|
/* DREI REIHEN: Kategorie, Stufe, Person. Die ersten beiden sehen
|
|
jetzt aus wie drueben beim anderen Brett. */
|
|
ok(wahl.reihen === 3, "drei Auswahlreihen (Kategorie, Stufe, Person): " + wahl.reihen);
|
|
ok(wahl.namen.includes("Marina"),
|
|
"der Modi steht zur Auswahl (" + wahl.namen.join(", ") + ")");
|
|
ok(!wahl.namen.includes("Tili") && !wahl.namen.includes("Schulle"),
|
|
"ein Creator und eine Managerin nicht — sie arbeiten im anderen Haus");
|
|
ok(wahl.zahlenOk && wahl.zahlen.length === wahl.namen.length,
|
|
"neben jedem Namen steht, wie viel offen ist (" + wahl.zahlen.join(", ") + ")");
|
|
ok(wahl.satz.includes("Formular oben"),
|
|
"und solange niemand gewählt ist, sagt die Seite es");
|
|
|
|
/* JETZT WIRKLICH DRUECKEN -- und nachsehen, ob die Aufgabe bei
|
|
DIESEM Menschen landet. Ein Knopf, den niemand betaetigt hat,
|
|
ist kein geprueter Knopf. */
|
|
await seite.evaluate(() => {
|
|
const reihen = [...document.querySelectorAll(".stufenleiter")];
|
|
const letzte = document.querySelector('[aria-label="Person wählen"]');
|
|
[...letzte.querySelectorAll(".stufenleiter__knopf")]
|
|
.find((k) => k.textContent.includes("Marina"))?.click();
|
|
});
|
|
await seite.waitForTimeout(600);
|
|
const nachWahl = await seite.evaluate(() =>
|
|
[...document.querySelectorAll(".stufenleiter__hilfe")]
|
|
.map((z) => z.textContent.trim()).find((t) => t.includes("gehen an")) || "");
|
|
ok(nachWahl.includes("Marina"), "nach dem Klick steht es da: " + nachWahl);
|
|
|
|
const vorher = await seite.evaluate(() => document.querySelectorAll(".karte").length);
|
|
await seite.evaluate(() => {
|
|
[...document.querySelectorAll(".vorlagenbrett__karte")][0]
|
|
?.querySelector("button")?.click();
|
|
});
|
|
await seite.waitForTimeout(1800);
|
|
const danach = await seite.evaluate(() => ({
|
|
karten: document.querySelectorAll(".karte").length,
|
|
fuerMarina: [...document.querySelectorAll(".karte")]
|
|
.filter((k) => k.textContent.includes("Marina")).length,
|
|
}));
|
|
ok(danach.karten === vorher + 1,
|
|
"die Aufgabe liegt auf dem Brett (" + vorher + " -> " + danach.karten + ")");
|
|
ok(danach.fuerMarina > 0, "und zwar bei Marina (" + danach.fuerMarina + ")");
|
|
|
|
/* ==== DIE BEWERBUNG AM BILDSCHIRM ENTSCHEIDEN (23.09.2026) ======
|
|
|
|
Filipe: „nur dogfather und die rechte hand sollen annehmen oder
|
|
ablehnen koennen, mit einem text als notiz."
|
|
|
|
DER SERVERTEIL IST OBEN GEPRUEFT. Hier geht es um das, was ein
|
|
Mensch tut: Sieht er die Bewerbung, steht der Name dabei, und
|
|
bekommt er ein Feld fuer seine Notiz?
|
|
|
|
ZUR RICHTIGEN KATEGORIE -- die offene Bewerbung liegt bei
|
|
„Events". Ohne den Klick maesse die Pruefung eine Karte, die gar
|
|
nicht auf dem Bildschirm steht. */
|
|
await seite.evaluate(() => {
|
|
const reihe = document.querySelector(".vorlagenbrett .stufenleiter");
|
|
const k = [...(reihe?.querySelectorAll("button") || [])]
|
|
.find((b) => /Events/i.test(b.textContent));
|
|
if (k) k.click();
|
|
});
|
|
await seite.waitForTimeout(800);
|
|
|
|
const bw = await seite.evaluate(() => {
|
|
const reihe = document.querySelector(".vorlagenbrett__bewerbung");
|
|
if (!reihe) return null;
|
|
const karte = reihe.closest(".vorlagenbrett__karte");
|
|
return {
|
|
name: reihe.querySelector(".vorlagenbrett__bname")?.textContent.trim() || "",
|
|
wort: reihe.querySelector(".vorlagenbrett__bwort")?.textContent.trim() || "",
|
|
schild: karte.querySelector(".vorlagenbrett__bschild")?.textContent.trim() || "",
|
|
knoepfe: [...reihe.querySelectorAll("button")].map((b) => b.textContent.trim()),
|
|
auffaellig: karte.dataset.beworben === "ja",
|
|
titel: karte.querySelector(".vorlagenbrett__titel")?.textContent.trim() || "",
|
|
};
|
|
});
|
|
ok(!!bw, "DogFather sieht die Bewerbung an der Karte");
|
|
ok(bw?.name === "Marina", `mit dem Namen dessen, der sie geschrieben hat (${bw?.name})`);
|
|
/* MAN ENTSCHEIDET UEBER EINEN MENSCHEN, nicht ueber einen
|
|
Listeneintrag -- deshalb stehen beide Knoepfe an SEINER Zeile. */
|
|
ok(JSON.stringify(bw?.knoepfe) === JSON.stringify(["Annehmen", "Ablehnen"]),
|
|
`und beiden Wegen daneben (${(bw?.knoepfe || []).join(", ")})`);
|
|
ok(bw?.wort.includes("doch noch Zeit"),
|
|
`sein Wort dazu steht dabei (${bw?.wort})`);
|
|
/* Eine Karte mit Bewerbung ist der einzige Grund, dieses Brett
|
|
gerade zu oeffnen -- sie darf auffallen. */
|
|
ok(bw?.auffaellig === true, "und die Karte faellt auf");
|
|
|
|
/* ---- Jetzt wirklich druecken, samt Notiz ---------------------- */
|
|
const vorherK = await seite.evaluate(() => document.querySelectorAll(".karte").length);
|
|
await seite.evaluate(() => {
|
|
[...document.querySelectorAll(".vorlagenbrett__bewerbung button")]
|
|
.find((b) => b.textContent.trim() === "Annehmen")?.click();
|
|
});
|
|
await seite.waitForTimeout(600);
|
|
/* DER KASTEN MUSS BEANTWORTET WERDEN. Beim ersten Anlauf einer
|
|
aehnlichen Pruefung habe ich ihn stehen lassen und dann gemeldet,
|
|
der Weg sei kaputt -- er war es nie, ich hatte nur nicht geklickt. */
|
|
const kastenDa = await seite.evaluate(() =>
|
|
!!document.querySelector("dialog.nachfrage[open]"));
|
|
ok(kastenDa, "es kommt ein Kasten für die Notiz — kein stilles Durchdrücken");
|
|
const feldDa = await seite.evaluate(() => {
|
|
const f = document.getElementById("nachfrage-grund");
|
|
return !!f && !f.closest("[hidden]");
|
|
});
|
|
ok(feldDa, " mit einem Feld für den Text — „mit einem text als notiz“");
|
|
await seite.fill("#nachfrage-grund", "danke dir, mach das gern");
|
|
await seite.click('dialog.nachfrage [data-teil="ja"]');
|
|
await seite.waitForTimeout(2200);
|
|
|
|
const nachher = await seite.evaluate(() => ({
|
|
karten: document.querySelectorAll(".karte").length,
|
|
nochBeworben: document.querySelectorAll(".vorlagenbrett__bewerbung").length,
|
|
}));
|
|
ok(nachher.karten === vorherK + 1,
|
|
`die Aufgabe entsteht mit der Zusage (${vorherK} -> ${nachher.karten})`);
|
|
/* UND DIE BEWERBUNG IST VOM TISCH. Bliebe sie stehen, entschiede
|
|
gleich noch jemand ein zweites Mal ueber dieselbe Bitte. */
|
|
ok(nachher.nochBeworben === 0,
|
|
`und die Bewerbung wartet nicht mehr (${nachher.nochBeworben})`);
|
|
|
|
ok(konsole.length === 0,
|
|
konsole.length ? "Fehler in der Konsole: " + konsole[0].slice(0, 80)
|
|
: "keine Fehler in der Konsole");
|
|
} finally { await browser.close(); }
|
|
}
|
|
|
|
/* =======================================================================
|
|
6c. EINE BEWERBUNG, DIE NIEMAND SIEHT, IST KEINE (23.09.2026)
|
|
=======================================================================
|
|
|
|
Beide Bewerbungswege waren gebaut und beide funktionierten -- und
|
|
beide waren STUMM. Nachgemessen vor dem Bauen: `benachrichtige` kam
|
|
weder in workspace-zuteilung.js noch in workspace-vorlagen.js ein
|
|
einziges Mal vor.
|
|
|
|
Wer sich bewirbt, wartet. Warten ohne Rueckmeldung fuehlt sich nach
|
|
zwei Tagen an wie "interessiert keinen" -- und genau das soll eine
|
|
Bewerbung verhindern.
|
|
|
|
GEPRUEFT WIRD, WAS SICH OHNE PUSH-DIENST PRUEFEN LAESST, und das ist
|
|
fast alles: WER etwas erfaehrt (abgeleitet, nicht aufgezaehlt), WAS
|
|
darin steht (reine Funktionen), und DASS es zwei getrennte Schalter
|
|
gibt.
|
|
======================================================================= */
|
|
console.log("\n=== 6c. Erfaehrt es auch jemand? ===\n");
|
|
{
|
|
const melder = await import("./workspace-bewerbung-melden.js");
|
|
const { ARTEN } = await import("./workspace-push.js");
|
|
|
|
/* ---- DIE SCHALTER --------------------------------------------- */
|
|
const neu2 = ARTEN.find((a) => a.schluessel === "bewerbung_neu");
|
|
const antwort = ARTEN.find((a) => a.schluessel === "bewerbung_antwort");
|
|
ok(!!neu2 && !!antwort,
|
|
"beide Arten stehen in den Benachrichtigungs-Einstellungen");
|
|
ok(neu2?.vorgabe === true && antwort?.vorgabe === true,
|
|
"und sind von sich aus an");
|
|
/* ZWEI SCHALTER, ZWEI ENTSCHEIDUNGEN. Wer an einem lebhaften Tag
|
|
viele Bitten bekommt, will die stumm stellen koennen -- die
|
|
Antwort auf die EIGENE Bewerbung will niemand stumm stellen. Eine
|
|
gemeinsame Art hiesse: beides zusammen abschalten oder beides
|
|
zusammen ertragen. */
|
|
ok(neu2?.schluessel !== antwort?.schluessel,
|
|
"getrennt voneinander — die eine trifft den, der antwortet, die andere den, der wartet");
|
|
|
|
/* ---- WER ES ERFAEHRT ------------------------------------------- */
|
|
const wer = melder.entscheiderFuer({ id: idModi, name: "Marina" });
|
|
const rollen = wer.map((x) => x.rolle).sort();
|
|
ok(wer.length > 0, `die Bewerbung eines Modis erreicht ${wer.length} Leute`);
|
|
ok(rollen.includes("admin") && rollen.includes("hand"),
|
|
`DogFather und die rechte Hand sind dabei (${rollen.join(", ")})`);
|
|
/* DIE LINKE HAND NICHT -- sie darf verteilen, aber nicht ueber
|
|
Bewerbungen entscheiden. Eine Meldung ueber etwas, das man nicht
|
|
beantworten kann, ist Laerm. */
|
|
ok(!rollen.includes("linke"),
|
|
"die linke Hand nicht — sie entscheidet hier nicht mit");
|
|
/* UND DIE AGENTUR ERST RECHT NICHT. Ohne die Haeuser-Bedingung
|
|
erfuehre ein Manager ueber eine Push-Nachricht, dass es Team Dogi
|
|
ueberhaupt gibt. */
|
|
ok(!rollen.includes("manager") && !rollen.includes("creator"),
|
|
`niemand aus dem anderen Haus (${rollen.join(", ") || "niemand"})`);
|
|
/* Und der Bewerber nicht sich selbst. */
|
|
ok(!wer.some((x) => x.id === idModi), "und der Bewerber nicht sich selbst");
|
|
|
|
/* GEGENPROBE: Die Liste darf nicht einfach "alle Aktiven" sein --
|
|
sonst waere jede Zeile oben zufaellig richtig. */
|
|
const alleAktiven = (await import("node:sqlite")).DatabaseSync;
|
|
const d3 = new alleAktiven(process.env.WORKSPACE_DB, { readOnly: true });
|
|
const anzahl = d3.prepare("SELECT COUNT(*) AS n FROM personen WHERE aktiv = 1").get().n;
|
|
d3.close();
|
|
ok(wer.length < anzahl,
|
|
`Gegenprobe: es sind nicht einfach alle (${wer.length} von ${anzahl} Aktiven)`);
|
|
|
|
/* ---- WAS DRINSTEHT --------------------------------------------- */
|
|
const b1 = melder.bewerbungText({
|
|
name: "Marina", titel: "Die Regeln des Rudels einmal ganz lesen",
|
|
wort: "ich habe Freitag Zeit" });
|
|
ok(b1.titel === "Marina bewirbt sich", `der Titel nennt den Menschen (${b1.titel})`);
|
|
ok(b1.text.includes("Regeln des Rudels"), "und die Zeile darunter die Aufgabe");
|
|
/* DAS WORT IST OFT DIE GANZE AUSKUNFT. Wer es schon auf dem
|
|
Sperrbildschirm liest, kann entscheiden, ohne zu oeffnen. */
|
|
ok(b1.text.includes("ich habe Freitag Zeit"),
|
|
`samt dem Wort des Bewerbers (${b1.text})`);
|
|
|
|
const b2 = melder.bewerbungText({ name: "Marina", titel: "Kurz", wort: "" });
|
|
ok(!b2.text.includes("„"), `ohne Wort bleibt es sauber (${b2.text})`);
|
|
ok(b2.text === "Kurz", "und nur die Aufgabe steht da");
|
|
|
|
/* NAMENLOS DARF NICHT "undefined bewirbt sich" ergeben. */
|
|
const b3 = melder.bewerbungText({ name: "", titel: "X", wort: "" });
|
|
ok(b3.titel === "Jemand bewirbt sich", `ohne Namen steht ein Wort da (${b3.titel})`);
|
|
|
|
const a1 = melder.antwortText({ titel: "Die Regeln", ja: true, notiz: "mach das gern" });
|
|
const a2 = melder.antwortText({ titel: "Die Regeln", ja: false, notiz: "diesmal jemand anders" });
|
|
/* DAS ERGEBNIS STEHT IM TITEL -- man soll es lesen koennen, ohne zu
|
|
oeffnen. Auch die gute Nachricht. */
|
|
ok(/angenommen/.test(a1.titel), `die Zusage sagt es im Titel (${a1.titel})`);
|
|
ok(/nicht/.test(a2.titel) && a2.titel !== a1.titel,
|
|
`die Absage auch, und anders (${a2.titel})`);
|
|
/* DIE NOTIZ IST DER GRUND, WARUM FILIPE SIE VERLANGT HAT ("mit
|
|
einem text als notiz"). Sie hier wegzulassen hiesse, sie an genau
|
|
der Stelle zu verstecken, an der man sie liest. */
|
|
ok(a1.text.includes("mach das gern") && a2.text.includes("diesmal jemand anders"),
|
|
"und die Notiz steht in beiden");
|
|
|
|
/* LANGES WIRD GEKUERZT, ABER NICHT MITTEN IM WORT.
|
|
|
|
DIE ERSTE FASSUNG DIESER ZEILE WAR FALSCH und hat mich das
|
|
Richtige gelehrt: Sie nahm einen 68 Zeichen langen Titel und
|
|
erwartete eine Kuerzung bei 50 -- die gilt aber nur, wenn eine
|
|
Notiz danebensteht und Platz wegnimmt. Ohne Notiz darf der Titel
|
|
die ganzen 90 haben. Die Pruefung war rot, das Programm richtig.
|
|
Gemessen wird jetzt mit etwas, das JEDE Grenze reisst. */
|
|
const langerSatz = "Eine ganze Sendung neben einem Neuen sitzen und im Zweifel "
|
|
+ "eingreifen und danach noch drei Zeilen aufschreiben, damit am naechsten "
|
|
+ "Tag jemand weiss, was eigentlich los war";
|
|
const lang = melder.antwortText({ titel: langerSatz, ja: true, notiz: "" });
|
|
ok(lang.text.length < langerSatz.length && /…$/.test(lang.text),
|
|
`zu Langes endet mit Auslassung (${lang.text})`);
|
|
/* KEIN SCHNITT MITTEN IM WORT -- ein "eingrei…" liest sich wie ein
|
|
Fehler, nicht wie eine Kuerzung. */
|
|
ok(/\s…$/.test(lang.text) || /[a-zA-Zäöüß]{3,}\s?…$/.test(lang.text),
|
|
`und nicht mitten im Wort (${lang.text.slice(-24)})`);
|
|
/* GEGENPROBE: Kurzes bleibt unangetastet -- sonst bewiese die Zeile
|
|
darueber nur, dass immer gekuerzt wird. */
|
|
const kurzerSatz = melder.antwortText({ titel: "Kurzer Titel", ja: true, notiz: "" });
|
|
ok(kurzerSatz.text === "Kurzer Titel",
|
|
`Gegenprobe: Kurzes bleibt unangetastet (${kurzerSatz.text})`);
|
|
}
|
|
|
|
/* ---------- 7. Gegenprobe -------------------------------------------- */
|
|
console.log("\n=== 7. Gegenprobe: kann das hier ueberhaupt scheitern? ===\n");
|
|
{
|
|
/* Eine Vorlagennummer, die es nicht gibt. Kaeme dafuer ein 200,
|
|
wuerden die Haken oben nichts beweisen -- dann sagt der Server zu
|
|
allem ja. */
|
|
/* MIT DEM ZUGANG VON DOGFATHER, nicht mit dem des Modis: Seit dem
|
|
23.09. bekommt ein Modi auf dieser Route ohnehin 403 -- die
|
|
Gegenprobe haette also ab da nur noch bewiesen, dass die
|
|
Rechtepruefung greift, und nichts mehr ueber erfundene Nummern
|
|
gesagt. Genau so verliert eine Pruefung still ihren Sinn. */
|
|
const a = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog", nr: 9999, gruppe: "aufbau", person_id: idModi }),
|
|
});
|
|
ok(a.status === 400, `eine erfundene Vorlagennummer wird abgelehnt (${a.status})`);
|
|
const b = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog_alle", gruppe: "gibtesnicht", person_id: idModi }),
|
|
});
|
|
ok(b.status === 400, `und eine erfundene Etappe ebenso (${b.status})`);
|
|
/* Und eine erfundene Bewerbungsnummer. */
|
|
const c = await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: 999999 }),
|
|
});
|
|
ok(c.status === 409, `eine erfundene Bewerbung ebenso (${c.status})`);
|
|
const e = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: "gibt-es-nicht" }),
|
|
});
|
|
ok(e.status === 400, `und eine erfundene Vorlage in der Bewerbung (${e.status})`);
|
|
}
|
|
|
|
console.log(`\n${fehler ? "FEHLGESCHLAGEN" : "BESTANDEN"} -- `
|
|
+ `${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`);
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler ? 1 : 0);
|