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
982 lines
47 KiB
JavaScript
982 lines
47 KiB
JavaScript
/* =====================================================================
|
|
pruef-bewerbung-aufgaben.mjs — sich bewerben statt selbst nehmen
|
|
=====================================================================
|
|
|
|
Filipe, 22.09.2026: „die modis und linke hand sollen da nichts
|
|
uebernehmen koennen von aufgaben, ueberhaupt ueberall sollen die
|
|
keine aufgaben selber uebernehmen die ihnen nicht zugetragen sind.
|
|
ich will dass die sich fuer aufgaben bewerben koennen aber die rechte
|
|
hand oder dogfather muessen annehmen oder ablehnen koennen und das
|
|
mit einem kommentar als moeglichkeit sogar noch zum hinzufuegen."
|
|
|
|
Dazu sein Satz vom selben Tag, der NICHT widerspricht, sondern eine
|
|
andere Frage beantwortet: „rechte hand linke hand und dogfather sehen
|
|
alle aufgaben und koennen alle verteilen."
|
|
|
|
ZWEI FRAGEN, DIE MAN AUSEINANDERHALTEN MUSS:
|
|
|
|
verteilen eine Aufgabe an ANDERE geben
|
|
entscheiden bestimmen, wer sie am Ende macht
|
|
|
|
Die linke Hand darf das erste und nicht das zweite. Sie bewirbt sich
|
|
wie ein Modi.
|
|
|
|
WAS HIER GEMESSEN WIRD — und warum gerade das:
|
|
|
|
(1) DASS DAS VERBOT KEINEN UMWEG HAT. Drei Wege fuehren zu „ich habe
|
|
jetzt diese Aufgabe": aus dem Pool uebernehmen, sich beim
|
|
Verteilen selbst eintragen, und (neu) eine Bewerbung annehmen.
|
|
Alle drei werden geprueft. Ein Verbot mit einem offenen Umweg
|
|
ist keins.
|
|
|
|
(2) DASS DER ERLAUBTE WEG WIRKLICH OFFEN IST. Ein Test, der nur
|
|
Verbote misst, ist auch dann gruen, wenn gar nichts mehr geht.
|
|
|
|
(3) DASS DIE WORTE DER PERSON UND DER KOMMENTAR DER LEITUNG IN ZWEI
|
|
FELDERN LANDEN. In dasselbe Feld waere bequemer und wuerde die
|
|
Frage mit der Antwort ueberschreiben.
|
|
|
|
AUFRUF: node server/pruef-bewerbung-aufgaben.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { request as httpAnfrage } from "node:http";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
import { zertifikatBauen } from "./helfer-openssl.mjs";
|
|
|
|
const PORT = await eigenerPort(import.meta, "pruef-bewerbung-aufgaben");
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-bew-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = String(PORT);
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
await import("./index.js");
|
|
notbremse(240_000, "pruef-bewerbung-aufgaben");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
|
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const melde = (t) => console.log(t);
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const CREW = "crew.dogfather-universe.com";
|
|
|
|
/* =====================================================================
|
|
1. WER ENTSCHEIDET — die Regel selbst
|
|
===================================================================== */
|
|
melde("\n=== 1. Wer entscheidet, wer eine Aufgabe macht ===");
|
|
const w = await import("./workspace.js");
|
|
{
|
|
const soll = { admin: true, hand: true, linke: false, modi: false, gast: false };
|
|
for (const [rolle, erwartet] of Object.entries(soll)) {
|
|
const ist = w.entscheidetUeberAufgaben({ rolle, haus: "crew", id: 1 });
|
|
ok(ist === erwartet,
|
|
`${rolle}: entscheidet = ${ist} (erwartet ${erwartet})`);
|
|
}
|
|
/* DIE ZWEITE FRAGE BLEIBT, WIE SIE WAR. Sein Satz „rechte hand linke
|
|
hand und dogfather koennen alle verteilen" darf durch diese
|
|
Aenderung nicht mit weggehen. */
|
|
for (const [rolle, erwartet] of Object.entries(
|
|
{ admin: true, hand: true, linke: true, modi: false, gast: false })) {
|
|
const ist = w.darfAufgabenVerteilen({ rolle, haus: "crew", id: 1 });
|
|
ok(ist === erwartet,
|
|
`${rolle}: darf verteilen = ${ist} (erwartet ${erwartet}) — unveraendert`);
|
|
}
|
|
/* GEGENPROBE: Die zwei Fragen sind wirklich verschieden. Waeren sie
|
|
dasselbe, haette die ganze Unterscheidung keinen Sinn. */
|
|
const p = { rolle: "linke", haus: "crew", id: 1 };
|
|
ok(w.darfAufgabenVerteilen(p) && !w.entscheidetUeberAufgaben(p),
|
|
"die linke Hand verteilt, entscheidet aber nicht — genau der Unterschied");
|
|
}
|
|
|
|
/* =====================================================================
|
|
2. AM LAUFENDEN SERVER
|
|
===================================================================== */
|
|
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 idChef = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
|
|
const idLinke = anlegen("Lino", "linke", "CODE-LINK-0001");
|
|
const idModi = anlegen("Frida", "modi", "CODE-MODI-0001");
|
|
const idModi2 = anlegen("Nele", "modi", "CODE-MODI-0002");
|
|
d.close();
|
|
|
|
function roh(pfad, wie = {}) {
|
|
return new Promise((fertig, schief) => {
|
|
const daten = wie.body === undefined ? null : Buffer.from(JSON.stringify(wie.body));
|
|
const a = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: pfad,
|
|
method: wie.methode || (daten ? "POST" : "GET"),
|
|
headers: {
|
|
Host: CREW,
|
|
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
|
|
...(wie.keks ? { Cookie: wie.keks } : {}),
|
|
Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/`,
|
|
},
|
|
}, (antwort) => {
|
|
const st = [];
|
|
antwort.on("data", (x) => st.push(x));
|
|
antwort.on("end", () => {
|
|
const text = Buffer.concat(st).toString("utf8");
|
|
fertig({ code: antwort.statusCode, text,
|
|
json: (() => { try { return JSON.parse(text); } catch { return null; } })() });
|
|
});
|
|
});
|
|
a.on("error", schief);
|
|
if (daten) a.write(daten);
|
|
a.end();
|
|
});
|
|
}
|
|
async function anmelden(rolle, code) {
|
|
const r = await roh("/workspace/api/anmelden", { body: { rolle, code } });
|
|
return { keks: [].concat(r.kopfSet || []).join("; "), code: r.code, roh: r };
|
|
}
|
|
/* Die Kekse kommen aus dem Antwortkopf -- dafuer braucht es ihn. */
|
|
function anmeldenMitKeks(rolle, code) {
|
|
return new Promise((fertig, schief) => {
|
|
const daten = Buffer.from(JSON.stringify({ rolle, code }));
|
|
const a = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden", method: "POST",
|
|
headers: { Host: CREW, "Content-Type": "application/json", "Content-Length": daten.length,
|
|
Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/` },
|
|
}, (antwort) => {
|
|
const st = [];
|
|
antwort.on("data", (x) => st.push(x));
|
|
antwort.on("end", () => {
|
|
const kekse = [].concat(antwort.headers["set-cookie"] || []);
|
|
fertig({ code: antwort.statusCode, keks: kekse.map((c) => c.split(";")[0]).join("; ") });
|
|
});
|
|
});
|
|
a.on("error", schief);
|
|
a.write(daten); a.end();
|
|
});
|
|
}
|
|
|
|
const chef = await anmeldenMitKeks("admin", "CODE-DOGI-0001");
|
|
const hand = await anmeldenMitKeks("hand", "CODE-HAND-0001");
|
|
const linke = await anmeldenMitKeks("linke", "CODE-LINK-0001");
|
|
const modi = await anmeldenMitKeks("modi", "CODE-MODI-0001");
|
|
const modi2 = await anmeldenMitKeks("modi", "CODE-MODI-0002");
|
|
|
|
melde("");
|
|
melde("=== 2. Alle fuenf kommen herein ===");
|
|
for (const [wer, s] of [["DogFather", chef], ["rechte Hand", hand],
|
|
["linke Hand", linke], ["Frida (Modi)", modi], ["Nele (Modi)", modi2]]) {
|
|
ok(s.code === 200 && s.keks.length > 0, `${wer} ist angemeldet (${s.code})`);
|
|
}
|
|
|
|
const aufgabeAnlegen = (keks, titel, zuteilung, verteilart) =>
|
|
roh("/workspace/api/aufgaben", {
|
|
keks, body: { titel, ...(zuteilung ? { zuteilung, verteilart } : {}) },
|
|
});
|
|
const liste = (keks) => roh("/workspace/api/aufgaben", { keks });
|
|
const finde = async (keks, titel) => {
|
|
const r = await liste(keks);
|
|
const alle = Array.isArray(r.json) ? r.json : (r.json?.aufgaben || []);
|
|
return alle.find((a) => a.titel === titel) || null;
|
|
};
|
|
|
|
/* =====================================================================
|
|
3. DER VERBOTENE WEG: selbst nehmen
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 3. Aus dem Pool nimmt sich niemand mehr selbst etwas ===");
|
|
let poolId = null;
|
|
{
|
|
const r = await aufgabeAnlegen(chef.keks, "Pool fuer alle",
|
|
[idModi, idModi2, idLinke], "pool");
|
|
poolId = r.json?.id;
|
|
ok(r.code === 201 && Number.isInteger(poolId),
|
|
`DogFather legt eine Pool-Aufgabe fuer drei an (${r.code})`);
|
|
|
|
for (const [wer, s] of [["Frida (Modi)", modi], ["die linke Hand", linke]]) {
|
|
const u = await roh(`/workspace/api/aufgaben/${poolId}/uebernehmen`, { methode: "POST", keks: s.keks });
|
|
ok(u.code === 403 && u.json?.fehler === "erst_bewerben",
|
|
`${wer} kann sie nicht uebernehmen (${u.code}/${u.json?.fehler})`);
|
|
/* UND AUCH NICHT DURCH DIE ZWEITE TUER. Beim Pool heisst
|
|
„annehmen" dasselbe wie „uebernehmen": Wer zusagt, nimmt sie
|
|
den anderen weg. Diese Zeile fehlte im ersten Entwurf, und
|
|
genau daran haette das Verbot vorbeigegriffen. */
|
|
const an = await roh(`/workspace/api/aufgaben/${poolId}/annehmen`, { methode: "POST", keks: s.keks });
|
|
ok(an.code === 403 && an.json?.fehler === "erst_bewerben",
|
|
`${wer} kann sie auch nicht einfach annehmen (${an.code}/${an.json?.fehler})`);
|
|
}
|
|
}
|
|
|
|
melde("");
|
|
melde("=== 3b. Und niemand traegt sich beim Verteilen selbst ein ===");
|
|
{
|
|
const r = await aufgabeAnlegen(linke.keks, "Die linke Hand fuer sich selbst", [idLinke], "einzeln");
|
|
const zu = r.json?.zuteilung;
|
|
ok(r.code === 201 && zu?.fehler === "nicht_an_dich_selbst",
|
|
`die linke Hand kann sich nicht selbst eintragen (${zu?.fehler})`);
|
|
|
|
/* GEGENPROBE: An JEMAND ANDEREN darf sie weiterhin verteilen. Sein
|
|
Satz „rechte hand linke hand und dogfather koennen alle verteilen"
|
|
bleibt damit gueltig. */
|
|
const r2 = await aufgabeAnlegen(linke.keks, "Die linke Hand verteilt an Frida", [idModi], "einzeln");
|
|
ok(r2.code === 201 && Array.isArray(r2.json?.zuteilung?.gesetzt)
|
|
&& r2.json.zuteilung.gesetzt.includes(idModi),
|
|
`aber an jemand anderen schon (${JSON.stringify(r2.json?.zuteilung?.gesetzt)})`);
|
|
|
|
/* Und DogFather darf sich selbst eintragen -- er entscheidet ja. */
|
|
const r3 = await aufgabeAnlegen(chef.keks, "DogFather fuer sich selbst", [idChef], "einzeln");
|
|
ok(r3.code === 201 && r3.json?.zuteilung?.gesetzt?.includes(idChef),
|
|
"DogFather darf sich selbst eintragen — er entscheidet");
|
|
}
|
|
|
|
/* =====================================================================
|
|
4. DER ERLAUBTE WEG: sich bewerben
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 4. Bewerben ===");
|
|
{
|
|
const b = await roh(`/workspace/api/aufgaben/${poolId}/bewerben`, {
|
|
methode: "POST", keks: modi.keks, body: { text: "Ich habe Freitag Zeit." },
|
|
});
|
|
ok(b.code === 200 && b.json?.zustand === "beworben",
|
|
`Frida bewirbt sich (${b.code}/${b.json?.zustand})`);
|
|
|
|
const nochmal = await roh(`/workspace/api/aufgaben/${poolId}/bewerben`, {
|
|
methode: "POST", keks: modi.keks, body: {} });
|
|
ok(nochmal.code === 409 && nochmal.json?.fehler === "schon_beworben",
|
|
`ein zweites Mal geht nicht (${nochmal.code}/${nochmal.json?.fehler})`);
|
|
|
|
const bLinke = await roh(`/workspace/api/aufgaben/${poolId}/bewerben`, {
|
|
methode: "POST", keks: linke.keks, body: { text: "Kann ich uebernehmen." } });
|
|
ok(bLinke.code === 200, `die linke Hand bewirbt sich auch (${bLinke.code})`);
|
|
|
|
/* WER ENTSCHEIDET, BEWIRBT SICH NICHT -- er teilt sich zu. Eine
|
|
Bewerbung an sich selbst waere ein Umweg mit demselben Ergebnis. */
|
|
const bChef = await roh(`/workspace/api/aufgaben/${poolId}/bewerben`, {
|
|
methode: "POST", keks: chef.keks, body: {} });
|
|
ok(bChef.code === 409 && bChef.json?.fehler === "du_entscheidest_selbst",
|
|
`DogFather bewirbt sich nicht (${bChef.code}/${bChef.json?.fehler})`);
|
|
|
|
const a = await finde(chef.keks, "Pool fuer alle");
|
|
ok((a?.bewerbungen || []).length === 2,
|
|
`die Aufgabe zeigt ${a?.bewerbungen?.length} Bewerbungen`);
|
|
ok((a?.bewerbungen || []).some((z) => z.grund === "Ich habe Freitag Zeit."),
|
|
"und Fridas Worte stehen dabei");
|
|
}
|
|
|
|
/* =====================================================================
|
|
5. ENTSCHEIDEN — nur die rechte Hand und DogFather
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 4b. Wer zuerst Zeit hat, steht zuerst ===");
|
|
{
|
|
/* Filipe, nachdem er meinen Satz „wer zuerst Zeit hat — genau das
|
|
geht ja nicht mehr" gelesen hatte: „falls das nicht mehr geht mach
|
|
das es geht wenn es wieder geht ist alles gut."
|
|
|
|
„Wer zuerst Zeit hat" heisst nicht zwingend „man bedient sich
|
|
selbst" -- es kann genauso heissen, dass die ERSTE BEWERBUNG zuerst
|
|
drankommt. Damit gilt beides gleichzeitig.
|
|
|
|
GEPRUEFT WIRD DIE REIHENFOLGE, NICHT DER SATZ. Die Abfrage
|
|
sortiert sonst nach NAMEN; nach Alphabet haette nicht der
|
|
Schnelle den Vorteil, sondern Frida. Genau deshalb steht hier
|
|
eine Messung und kein Blick in den Quelltext. */
|
|
const r = await aufgabeAnlegen(chef.keks, "Wer zuerst kommt", [idModi, idModi2], "pool");
|
|
const id = r.json?.id;
|
|
ok(r.code === 201, `eine neue Pool-Aufgabe (${r.code})`);
|
|
|
|
/* NELE ZUERST -- und sie steht im Alphabet HINTER Frida. Waere die
|
|
Liste nach Namen sortiert, faellt genau das auf. */
|
|
const n1 = await roh(`/workspace/api/aufgaben/${id}/bewerben`, {
|
|
methode: "POST", keks: modi2.keks, body: { text: "Ich kann sofort." } });
|
|
ok(n1.code === 200, `Nele bewirbt sich zuerst (${n1.code})`);
|
|
await new Promise((w) => setTimeout(w, 1100));
|
|
const n2 = await roh(`/workspace/api/aufgaben/${id}/bewerben`, {
|
|
methode: "POST", keks: modi.keks, body: { text: "Ich auch." } });
|
|
ok(n2.code === 200, `Frida danach (${n2.code})`);
|
|
|
|
const a = await finde(chef.keks, "Wer zuerst kommt");
|
|
const reihe = (a?.bewerbungen || []).map((z) => z.person_name);
|
|
ok(reihe.length === 2, `zwei Bewerbungen liegen vor (${reihe.length})`);
|
|
ok(reihe[0] === "Nele",
|
|
`Nele steht vorn, weil sie zuerst da war (${reihe.join(" vor ")})`);
|
|
/* GEGENPROBE: Nach Namen sortiert stuende Frida vorn. Die Messung
|
|
kann den Unterschied also sehen -- ohne diese Zeile waere
|
|
„Nele steht vorn" auch bei einer zufaelligen Ordnung mal richtig. */
|
|
ok([...reihe].sort()[0] === "Frida",
|
|
"nach Alphabet staende Frida vorn — die Reihenfolge ist also wirklich die des Eingangs");
|
|
}
|
|
|
|
melde("");
|
|
melde("=== 5. Entscheiden ===");
|
|
{
|
|
/* Die linke Hand darf nicht -- sie hat sich ja selbst beworben. */
|
|
const versuch = await roh(`/workspace/api/aufgaben/${poolId}/bewerbung/annehmen`, {
|
|
methode: "POST", keks: linke.keks, body: { person_id: idModi } });
|
|
ok(versuch.code === 403,
|
|
`die linke Hand entscheidet nicht (${versuch.code}/${versuch.json?.fehler})`);
|
|
|
|
const modiVersuch = await roh(`/workspace/api/aufgaben/${poolId}/bewerbung/annehmen`, {
|
|
methode: "POST", keks: modi.keks, body: { person_id: idModi } });
|
|
ok(modiVersuch.code === 403,
|
|
`ein Modi auch nicht (${modiVersuch.code})`);
|
|
|
|
/* Die rechte Hand nimmt Frida an, mit Kommentar. */
|
|
const ja = await roh(`/workspace/api/aufgaben/${poolId}/bewerbung/annehmen`, {
|
|
methode: "POST", keks: hand.keks,
|
|
body: { person_id: idModi, kommentar: "Danke dir, mach das gern." } });
|
|
ok(ja.code === 200 && ja.json?.zustand === "angenommen",
|
|
`die rechte Hand nimmt Frida an (${ja.code}/${ja.json?.zustand})`);
|
|
|
|
const a = await finde(chef.keks, "Pool fuer alle");
|
|
const fridas = (a?.zuteilung || []).find((z) => z.person_id === idModi);
|
|
ok(fridas?.zustand === "angenommen", `Fridas Stand ist "${fridas?.zustand}"`);
|
|
/* ZWEI FELDER, NICHT EINS. Der Kommentar der Leitung darf die Worte
|
|
der Person nicht ueberschreiben -- sonst weiss hinterher niemand
|
|
mehr, worum jemand gebeten hat. */
|
|
ok(fridas?.grund === "Ich habe Freitag Zeit.",
|
|
`ihre eigenen Worte stehen noch da ("${fridas?.grund}")`);
|
|
ok(fridas?.entscheid_text === "Danke dir, mach das gern.",
|
|
`und der Kommentar daneben ("${fridas?.entscheid_text}")`);
|
|
ok(fridas?.entschieden_von_name === "Rieke",
|
|
`mit Namen, wer entschieden hat (${fridas?.entschieden_von_name})`);
|
|
|
|
/* Die Bewerbung der linken Hand ist damit erledigt -- Pool. */
|
|
const linkes = (a?.zuteilung || []).find((z) => z.person_id === idLinke);
|
|
ok(linkes?.zustand === "abgelehnt",
|
|
`die andere Bewerbung faellt weg, weil Pool (${linkes?.zustand})`);
|
|
/* UND IHRE WORTE BLEIBEN STEHEN. Der Hinweis „von jemand anderem
|
|
uebernommen" gehoert in das Feld der Leitung, nicht in ihres --
|
|
sonst loescht die Antwort die Frage. */
|
|
ok(linkes?.grund === "Kann ich uebernehmen.",
|
|
`ihre eigenen Worte bleiben ("${linkes?.grund}")`);
|
|
ok(/jemand anderem/i.test(linkes?.entscheid_text || ""),
|
|
`und der Grund steht daneben ("${linkes?.entscheid_text}")`);
|
|
}
|
|
|
|
melde("");
|
|
melde("=== 5b. Ablehnen, auch mit Kommentar ===");
|
|
{
|
|
const r = await aufgabeAnlegen(chef.keks, "Etwas fuer Nele", [idModi2], "einzeln");
|
|
const id = r.json?.id;
|
|
/* Nele ist zugeteilt -- sie bewirbt sich nicht, sie hat sie schon. */
|
|
const b = await roh(`/workspace/api/aufgaben/${id}/bewerben`, {
|
|
methode: "POST", keks: modi2.keks, body: {} });
|
|
ok(b.code === 409 && b.json?.fehler === "schon_zugeteilt",
|
|
`wer sie schon hat, bewirbt sich nicht (${b.code}/${b.json?.fehler})`);
|
|
|
|
/* Frida ist nicht zugeteilt und kann sich bewerben. */
|
|
const b2 = await roh(`/workspace/api/aufgaben/${id}/bewerben`, {
|
|
methode: "POST", keks: modi.keks, body: { text: "Ich kenne mich da aus." } });
|
|
ok(b2.code === 200, `Frida bewirbt sich darauf (${b2.code})`);
|
|
|
|
const nein = await roh(`/workspace/api/aufgaben/${id}/bewerbung/ablehnen`, {
|
|
methode: "POST", keks: chef.keks,
|
|
body: { person_id: idModi, kommentar: "Nele macht das schon." } });
|
|
ok(nein.code === 200 && nein.json?.zustand === "abgelehnt",
|
|
`DogFather lehnt ab (${nein.code}/${nein.json?.zustand})`);
|
|
|
|
const a = await finde(chef.keks, "Etwas fuer Nele");
|
|
const fridas = (a?.zuteilung || []).find((z) => z.person_id === idModi);
|
|
ok(fridas?.entscheid_text === "Nele macht das schon.",
|
|
`mit Kommentar ("${fridas?.entscheid_text}")`);
|
|
|
|
/* OHNE KOMMENTAR GEHT AUCH -- „als moeglichkeit", nicht als Pflicht. */
|
|
const b3 = await roh(`/workspace/api/aufgaben/${id}/bewerben`, {
|
|
methode: "POST", keks: modi.keks, body: {} });
|
|
ok(b3.code === 200, "Frida bewirbt sich erneut, diesmal ohne Text");
|
|
const nein2 = await roh(`/workspace/api/aufgaben/${id}/bewerbung/ablehnen`, {
|
|
methode: "POST", keks: chef.keks, body: { person_id: idModi } });
|
|
ok(nein2.code === 200, "und wird ohne Kommentar abgelehnt — der ist freiwillig");
|
|
}
|
|
|
|
melde("");
|
|
melde("=== 5c. Eine Bewerbung laesst sich zurueckziehen ===");
|
|
{
|
|
const r = await aufgabeAnlegen(chef.keks, "Noch etwas", null, null);
|
|
const id = r.json?.id;
|
|
const b = await roh(`/workspace/api/aufgaben/${id}/bewerben`, {
|
|
methode: "POST", keks: modi.keks, body: { text: "Mache ich." } });
|
|
ok(b.code === 200, `Frida bewirbt sich (${b.code})`);
|
|
|
|
const zurueck = await roh(`/workspace/api/aufgaben/${id}/bewerbung-zurueck`, {
|
|
methode: "POST", keks: modi.keks });
|
|
ok(zurueck.code === 200, `und zieht sie zurueck (${zurueck.code})`);
|
|
|
|
const a = await finde(chef.keks, "Noch etwas");
|
|
ok((a?.bewerbungen || []).length === 0,
|
|
`danach steht keine mehr da (${a?.bewerbungen?.length})`);
|
|
/* GANZ WEG, nicht „zurueckgezogen": keine Zeile, die nur noch sagt,
|
|
dass hier mal eine war. */
|
|
ok((a?.zuteilung || []).every((z) => z.person_id !== idModi),
|
|
"und auch keine Zeile mehr, die davon uebrig ist");
|
|
|
|
const nochmal = await roh(`/workspace/api/aufgaben/${id}/bewerbung-zurueck`, {
|
|
methode: "POST", keks: modi.keks });
|
|
ok(nochmal.code === 409, `ein zweites Zurueckziehen geht nicht (${nochmal.code})`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
6. AM ECHTEN BILDSCHIRM
|
|
=====================================================================
|
|
Die Abschnitte davor messen die Wege. Keiner von ihnen haette
|
|
gemerkt, wenn der Knopf „Ich bewerbe mich" gar nicht erscheint oder
|
|
wenn ein Modi weiterhin „Ich uebernehme das" sieht.
|
|
|
|
Genau diese Luecke ist heute frueh beim Aufgaben-Formular
|
|
aufgefallen: Die Pruefung war gruen und hat den Zustand, in dem der
|
|
Fehler steckt, nie hergestellt.
|
|
|
|
WARUM EIN HTTPS-VORBAU: Team Dogi wohnt auf
|
|
crew.dogfather-universe.com, und welche Wand gilt, entscheidet der
|
|
Host-Kopf. Chromium setzt den selbst und hebt diese Domain
|
|
ausserdem von sich aus auf https.
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 6. Am echten Bildschirm ===");
|
|
{
|
|
const { execFileSync } = await import("node:child_process");
|
|
const { createServer: httpsServer } = await import("node:https");
|
|
const { readFileSync } = await import("node:fs");
|
|
|
|
const schluessel = join(ordner, "pruef.key");
|
|
const zertifikat = join(ordner, "pruef.crt");
|
|
zertifikatBauen(schluessel, zertifikat, CREW);
|
|
|
|
const HTTPS_PORT = PORT + 1;
|
|
const vorbau = httpsServer({
|
|
key: readFileSync(schluessel), cert: readFileSync(zertifikat),
|
|
}, (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 SEITE = `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"],
|
|
});
|
|
|
|
/* Eine frische Aufgabe fuer den Bildschirm -- die aus den
|
|
Abschnitten davor sind schon entschieden. */
|
|
const frisch = await aufgabeAnlegen(chef.keks, "Fuer den Bildschirm",
|
|
[idModi, idLinke], "pool");
|
|
const frischId = frisch.json?.id;
|
|
ok(Number.isInteger(frischId), `eine frische Pool-Aufgabe liegt (${frisch.code})`);
|
|
|
|
const meckern = [];
|
|
async function seiteAls(rolle, code, breite = 1280) {
|
|
const ctx = await browser.newContext({ viewport: { width: breite, height: 1000 } });
|
|
const s2 = await ctx.newPage();
|
|
s2.on("pageerror", (e) => meckern.push(`${rolle}: ${String(e).slice(0, 110)}`));
|
|
s2.on("console", (m) => {
|
|
if (m.type() === "error" && !/favicon|ERR_CERT|net::ERR/i.test(m.text())) {
|
|
meckern.push(`${rolle}: ${m.text().slice(0, 110)}`);
|
|
}
|
|
});
|
|
await s2.goto(`${SEITE}/workspace/`, { waitUntil: "networkidle" });
|
|
await s2.click(`.rolle[data-rolle="${rolle}"]`);
|
|
await s2.fill("#code", code);
|
|
await s2.click("button[type=submit]");
|
|
await s2.waitForTimeout(900);
|
|
await s2.goto(`${SEITE}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
|
|
await s2.waitForTimeout(1600);
|
|
return { ctx, s: s2 };
|
|
}
|
|
const knoepfeAn = (s2, titel) => s2.evaluate((t) => {
|
|
const karten = [...document.querySelectorAll(".karte, .aufgabe, li, article")];
|
|
const k = karten.find((x) => (x.textContent || "").includes(t)
|
|
&& x.querySelector(".z-karte"));
|
|
if (!k) return null;
|
|
return {
|
|
knoepfe: [...k.querySelectorAll(".z-karte button")].map((b) => b.textContent.trim()),
|
|
bewerbungen: k.querySelectorAll(".z-bewerbung").length,
|
|
text: (k.querySelector(".z-karte")?.textContent || "").replace(/\s+/g, " ").trim().slice(0, 200),
|
|
};
|
|
}, titel);
|
|
|
|
try {
|
|
/* ---- Ein Modi ------------------------------------------------- */
|
|
const m = await seiteAls("modi", "CODE-MODI-0001", 390);
|
|
try {
|
|
const b = await knoepfeAn(m.s, "Fuer den Bildschirm");
|
|
ok(!!b, "der Modi sieht die Aufgabe mit ihrem Zuteilungsblock");
|
|
ok(!!b && b.knoepfe.some((t) => /bewerbe mich/i.test(t)),
|
|
`und den Knopf „Ich bewerbe mich" (${b?.knoepfe.join(" | ")})`);
|
|
ok(!!b && !b.knoepfe.some((t) => /übernehme/i.test(t)),
|
|
"und NICHT mehr „Ich übernehme das“");
|
|
ok(!!b && !b.knoepfe.some((t) => /^Ablehnen$/i.test(t)),
|
|
"und kein „Ablehnen“ — eine Absage auf eine Frage, die niemand gestellt hat");
|
|
} finally { await m.ctx.close(); }
|
|
|
|
/* ---- Die linke Hand: darf verteilen, aber nicht entscheiden --- */
|
|
const l = await seiteAls("linke", "CODE-LINK-0001", 1280);
|
|
try {
|
|
const b = await knoepfeAn(l.s, "Fuer den Bildschirm");
|
|
ok(!!b && b.knoepfe.some((t) => /bewerbe mich/i.test(t)),
|
|
`die linke Hand sieht denselben Knopf (${b?.knoepfe.join(" | ")})`);
|
|
ok(!!b && !b.knoepfe.some((t) => /übernehme/i.test(t)),
|
|
"und auch sie nicht „Ich übernehme das“");
|
|
} finally { await l.ctx.close(); }
|
|
|
|
/* ---- Jetzt bewirbt sich der Modi wirklich --------------------- */
|
|
const bew = await roh(`/workspace/api/aufgaben/${frischId}/bewerben`, {
|
|
methode: "POST", keks: modi.keks, body: { text: "Kann ich machen." } });
|
|
ok(bew.code === 200, `Frida bewirbt sich (${bew.code})`);
|
|
|
|
/* ---- Die rechte Hand sieht die Bewerbung ---------------------- */
|
|
const h = await seiteAls("hand", "CODE-HAND-0001", 1280);
|
|
try {
|
|
const b = await knoepfeAn(h.s, "Fuer den Bildschirm");
|
|
ok(!!b && b.bewerbungen === 1,
|
|
`die rechte Hand sieht 1 Bewerbung (${b?.bewerbungen})`);
|
|
ok(!!b && b.knoepfe.some((t) => /^Annehmen$/i.test(t))
|
|
&& b.knoepfe.some((t) => /^Ablehnen$/i.test(t)),
|
|
`mit Annehmen und Ablehnen (${b?.knoepfe.join(" | ")})`);
|
|
ok(!!b && /Kann ich machen/.test(b.text),
|
|
"und Fridas Worte stehen dabei");
|
|
} finally { await h.ctx.close(); }
|
|
|
|
/* ---- Der Modi sieht seinen eigenen Stand ---------------------- */
|
|
const m2 = await seiteAls("modi", "CODE-MODI-0001", 390);
|
|
try {
|
|
const b = await knoepfeAn(m2.s, "Fuer den Bildschirm");
|
|
ok(!!b && /beworben/i.test(b.text),
|
|
`Frida sieht, dass sie sich beworben hat ("${(b?.text || "").slice(0, 70)}…")`);
|
|
ok(!!b && b.knoepfe.some((t) => /zurückziehen/i.test(t)),
|
|
`und kann sie zurueckziehen (${b?.knoepfe.join(" | ")})`);
|
|
/* GEGENPROBE: Sie sieht KEINE Entscheidungsknoepfe an ihrer
|
|
eigenen Bewerbung -- sonst haette sie sich selbst annehmen
|
|
koennen, und das ganze Verbot waere ein Knopf weiter offen. */
|
|
ok(!!b && b.bewerbungen === 0,
|
|
`und keinen Block zum Entscheiden (${b?.bewerbungen})`);
|
|
} finally { await m2.ctx.close(); }
|
|
|
|
/* =================================================================
|
|
7. AUFGABEN DORT ANLEGEN, WO DIE PERSON STEHT (A5 und B6)
|
|
=================================================================
|
|
Filipe: „dieser buttion da soll nicht einen zu der seite
|
|
aufgaben fuehren sondern da in dieser seite die aufgaben
|
|
erstellen und vergeben koennen. plus man soll die aufgaben hier
|
|
in dieser seite auch sehen." Und zum Aufgabenbrett: „dieser
|
|
button kann da jetzt doch endlich verschwinden."
|
|
|
|
DER ORT WIRD GEMESSEN, NICHT ANGENOMMEN: Auf der Agenturadresse
|
|
gibt es die Entwicklungsseite nicht, und vier Rollen dort
|
|
duerfen sehr wohl Aufgaben anlegen. Der Knopf verschwindet also
|
|
genau dort, wo es einen anderen Ort gibt -- und nur dort.
|
|
================================================================= */
|
|
const e = await seiteAls("admin", "CODE-DOGI-0001", 1180);
|
|
try {
|
|
/* B6: auf dem Brett ist der Knopf weg. */
|
|
const b6 = await e.s.evaluate(() => {
|
|
const k2 = document.getElementById("neu-oeffnen");
|
|
return { da: !!k2, versteckt: k2 ? k2.hidden : null,
|
|
ort: window.__ich?.aufgaben_anlegen_auf };
|
|
});
|
|
ok(b6.ort === "entwicklung",
|
|
`der Server nennt die Entwicklungsseite als Ort (${b6.ort})`);
|
|
ok(b6.da && b6.versteckt === true,
|
|
`und der Knopf auf dem Aufgabenbrett ist weg (${b6.versteckt})`);
|
|
|
|
/* A5: auf der Entwicklungsseite geht es jetzt. */
|
|
await e.s.goto(`${SEITE}/workspace/entwicklung.html`, { waitUntil: "networkidle" });
|
|
await e.s.waitForTimeout(1800);
|
|
|
|
const vorher = await e.s.evaluate(() => {
|
|
const k2 = document.getElementById("verteilen-knopf");
|
|
return { istLink: k2?.tagName === "A", aus: k2?.disabled,
|
|
aufgabenBlock: !document.getElementById("e-aufgaben-block")?.hidden };
|
|
});
|
|
ok(vorher.istLink === false,
|
|
"der Knopf fuehrt nicht mehr auf eine andere Seite (kein Link mehr)");
|
|
/* HIER STAND `vorher.aus === true` (bis 22.09.2026 abends).
|
|
---------------------------------------------------------------
|
|
Die Pruefung hat damit MEINEN Entwurf bestaetigt, nicht die
|
|
Sache: Der Knopf war abgeschaltet, solange niemand gewaehlt
|
|
war, und daneben stand „Waehl links jemanden aus".
|
|
|
|
Gemessen steht die Auswahl aber gar nicht links, sondern
|
|
206 px WEITER UNTEN, bei derselben linken Kante (130 px). Wer
|
|
der Anweisung folgte, sah eine leere Flaeche neben einem
|
|
toten Knopf.
|
|
|
|
Filipe: „weil gerade hängt das und weiß nicht wieso."
|
|
|
|
Das ist die unangenehmste Sorte gruener Haken: Er lief, er war
|
|
gruen, und er hat den Fehler festgeschrieben. Jetzt wird das
|
|
Gegenteil verlangt -- der Knopf ist IMMER benutzbar, und die
|
|
Auswahl steht im Formular. */
|
|
ok(vorher.aus === false,
|
|
"und er ist benutzbar, auch ohne vorherige Auswahl — die Frage"
|
|
+ " nach der Person wird im Formular selbst gefragt");
|
|
ok(vorher.aufgabenBlock === false,
|
|
"ohne Person steht auch keine Aufgabenliste da");
|
|
|
|
await e.s.click("#personen .e-person");
|
|
await e.s.waitForTimeout(1400);
|
|
const nachher = await e.s.evaluate(() => ({
|
|
knopf: document.getElementById("verteilen-knopf")?.textContent.trim(),
|
|
aus: document.getElementById("verteilen-knopf")?.disabled,
|
|
block: !document.getElementById("e-aufgaben-block")?.hidden,
|
|
titel: document.getElementById("e-aufgaben-titel")?.textContent.trim(),
|
|
}));
|
|
ok(nachher.aus === false && /Aufgabe für/.test(nachher.knopf || ""),
|
|
`nach der Auswahl heisst er „${nachher.knopf}"`);
|
|
ok(nachher.block === true && /Aufgaben von/.test(nachher.titel || ""),
|
|
`und die Aufgabenliste steht da („${nachher.titel}")`);
|
|
|
|
await e.s.click("#verteilen-knopf");
|
|
await e.s.waitForTimeout(500);
|
|
const form = await e.s.evaluate(() => ({
|
|
offen: !document.getElementById("e-neu")?.hidden,
|
|
felder: document.querySelectorAll("#e-neu input, #e-neu textarea").length,
|
|
}));
|
|
ok(form.offen === true && form.felder >= 3,
|
|
`das Formular oeffnet sich auf derselben Seite (${form.felder} Felder)`);
|
|
|
|
const titel = "Auf der Entwicklungsseite angelegt";
|
|
await e.s.fill("#e-titel", titel);
|
|
await e.s.click("#e-neu-speichern");
|
|
await e.s.waitForTimeout(1800);
|
|
const fertig = await e.s.evaluate((t) => ({
|
|
zu: document.getElementById("e-neu")?.hidden,
|
|
steht: [...document.querySelectorAll(".e-aufgabe__titel")].some((x) => x.textContent === t),
|
|
gut: document.getElementById("fehler")?.dataset.gut,
|
|
}), titel);
|
|
ok(fertig.zu === true, "nach dem Anlegen ist das Formular zu");
|
|
ok(fertig.steht === true, "und die Aufgabe steht sofort in der Liste darunter");
|
|
/* EINE BESTAETIGUNG DARF NICHT AUSSEHEN WIE EIN FEHLER. Auf dem
|
|
ersten Bildschirmfoto stand sie in Warnrot -- `melde` schreibt
|
|
immer in denselben Absatz, und der heisst `.fehler`. */
|
|
ok(fertig.gut === "ja",
|
|
`und die Bestaetigung ist als gute Nachricht gekennzeichnet (${fertig.gut})`);
|
|
|
|
/* GEGENPROBE: Die Aufgabe ist wirklich da, nicht nur im Bild. */
|
|
const ausDerListe = await finde(chef.keks, titel);
|
|
ok(!!ausDerListe, "sie steht auch wirklich in der Aufgabenliste des Servers");
|
|
|
|
/* =================================================================
|
|
6b. VERTEILEN OHNE VORHERIGE AUSWAHL -- und als Bewerbung
|
|
|
|
Filipe am 22.09.2026 abends: „dogfather, die rechte hand und
|
|
die linke hand sollen immer noch aufgaben werden teilen
|
|
können plus die option dass die modis sich für aufgaben
|
|
bewerben können. weil gerade hängt das und weiß nicht wieso."
|
|
|
|
Der Weg, der vorher tot war: Seite aufmachen, auf „Aufgabe
|
|
anlegen" druecken -- ohne unten jemanden angeklickt zu haben.
|
|
Frueher passierte dabei NICHTS.
|
|
|
|
Und im selben Formular muss die Bewerbung zu finden sein.
|
|
Vorher steckte sie hinter „Noch jemanden dazunehmen" und
|
|
erschien erst, wenn man dort jemanden ankreuzte -- eine
|
|
Moeglichkeit, die man nicht findet, gibt es nicht.
|
|
================================================================= */
|
|
await e.s.goto(`${SEITE}/workspace/entwicklung.html`, { waitUntil: "networkidle" });
|
|
await e.s.waitForTimeout(1800);
|
|
await e.s.click("#verteilen-knopf");
|
|
await e.s.waitForTimeout(600);
|
|
|
|
const direkt = await e.s.evaluate(() => ({
|
|
offen: !document.getElementById("e-neu")?.hidden,
|
|
zurWahl: document.querySelectorAll("#e-fuer-leute input[type=checkbox]").length,
|
|
artSichtbar: document.getElementById("e-art")
|
|
? !document.getElementById("e-art").hidden : null,
|
|
/* Die alte, zugeklappte zweite Liste darf es nicht mehr geben --
|
|
sonst stehen wieder zwei Bedienungen fuer dieselbe Frage da. */
|
|
alteZweiteListe: !!document.getElementById("e-mehr-auf"),
|
|
}));
|
|
ok(direkt.offen === true,
|
|
"das Formular geht auch ohne vorherige Auswahl auf — genau der Weg, der haengen blieb");
|
|
ok(direkt.zurWahl >= 2,
|
|
`und die Frage nach der Person steht darin, mit ${direkt.zurWahl} Personen zur Wahl`);
|
|
ok(direkt.alteZweiteListe === false,
|
|
"die zweite, zugeklappte Personenliste gibt es nicht mehr");
|
|
ok(direkt.artSichtbar === false,
|
|
"bei niemandem oder einer Person wird nicht nach der Verteilart gefragt");
|
|
|
|
/* Zwei anhaken -> jetzt MUSS die Frage kommen, und die
|
|
Bewerbung muss eine der Antworten sein. */
|
|
const angehakt = await e.s.evaluate(() => {
|
|
const k = [...document.querySelectorAll("#e-fuer-leute input[type=checkbox]")];
|
|
k[0].click(); k[1].click();
|
|
return k.length;
|
|
});
|
|
await e.s.waitForTimeout(500);
|
|
const zuZweit = await e.s.evaluate(() => ({
|
|
artSichtbar: !document.getElementById("e-art").hidden,
|
|
antworten: [...document.querySelectorAll('#e-art input[name="e-verteilart"]')]
|
|
.map((x) => x.value),
|
|
knopf: document.getElementById("verteilen-knopf")?.textContent.trim(),
|
|
}));
|
|
ok(angehakt >= 2 && zuZweit.artSichtbar === true,
|
|
"bei zwei Personen wird gefragt, wie es laufen soll");
|
|
ok(zuZweit.antworten.includes("pool"),
|
|
`und der Pool-Weg steht als Antwort da (${zuZweit.antworten.join(", ")})`);
|
|
ok(/2 Personen/.test(zuZweit.knopf || ""),
|
|
`der Knopf zaehlt mit („${zuZweit.knopf}")`);
|
|
|
|
const poolTitel = "Pool von der Entwicklungsseite";
|
|
await e.s.check('#e-art input[value="pool"]');
|
|
await e.s.fill("#e-titel", poolTitel);
|
|
await e.s.click("#e-neu-speichern");
|
|
await e.s.waitForTimeout(1800);
|
|
const poolFertig = await e.s.evaluate(() => ({
|
|
zu: document.getElementById("e-neu")?.hidden,
|
|
meldung: document.getElementById("fehler")?.textContent.trim(),
|
|
gut: document.getElementById("fehler")?.dataset.gut,
|
|
}));
|
|
ok(poolFertig.zu === true && poolFertig.gut === "ja",
|
|
`angelegt und als gute Nachricht gemeldet (${poolFertig.gut})`);
|
|
/* DIE MELDUNG DARF NICHT „steht jetzt bei ihm" SAGEN. Bei einem
|
|
Pool sitzt noch niemand daran -- das ist der ganze Punkt. */
|
|
ok(/bewirbt|bereit/i.test(poolFertig.meldung || "")
|
|
&& !/bei ihm/.test(poolFertig.meldung || ""),
|
|
`und die Meldung sagt, dass eine Bewerbung kommt („${poolFertig.meldung}")`);
|
|
|
|
const poolAufgabe = await finde(chef.keks, poolTitel);
|
|
ok(!!poolAufgabe, "die Pool-Aufgabe steht in der Liste des Servers");
|
|
ok(poolAufgabe?.verteilart === "pool",
|
|
`und sie ist wirklich ein Pool (${poolAufgabe?.verteilart})`);
|
|
ok((poolAufgabe?.zuteilung || []).length === 2,
|
|
`mit zwei Leuten darin (${(poolAufgabe?.zuteilung || []).length})`);
|
|
} finally { await e.ctx.close(); }
|
|
|
|
/* ===================================================================
|
|
6c. ALLE DREI, DIE VERTEILEN DUERFEN -- am Bildschirm
|
|
|
|
Filipe: „dogfather, die rechte hand und die linke hand sollen
|
|
immer noch aufgaben werden teilen können."
|
|
|
|
Der Server sagt das seit dem 22.09. ueber `darfAufgabenVerteilen`,
|
|
und Abschnitt 1 misst das dort auch. Hier geht es um die andere
|
|
Haelfte: ob es auf der SEITE ankommt. Genau diese Luecke war der
|
|
Fehler -- die Regel stimmte, der Knopf war trotzdem tot.
|
|
|
|
DIE DREI STEHEN IN EINER LISTE, und die Zahl der geprueften
|
|
Rollen steht in der Bedingung: Eine leere Schleife darf nicht
|
|
„in Ordnung" heissen.
|
|
=================================================================== */
|
|
const verteiler = [
|
|
["admin", "CODE-DOGI-0001", "DogFather"],
|
|
["hand", "CODE-HAND-0001", "die rechte Hand"],
|
|
["linke", "CODE-LINK-0001", "die linke Hand"],
|
|
];
|
|
let konnten = 0;
|
|
for (const [rolle, code, wer] of verteiler) {
|
|
const v = await seiteAls(rolle, code, 1180);
|
|
try {
|
|
await v.s.goto(`${SEITE}/workspace/entwicklung.html`, { waitUntil: "networkidle" });
|
|
await v.s.waitForTimeout(1700);
|
|
const stand = await v.s.evaluate(() => ({
|
|
band: document.getElementById("verteilen-band")
|
|
? !document.getElementById("verteilen-band").hidden : null,
|
|
aus: document.getElementById("verteilen-knopf")?.disabled,
|
|
darf: window.__ich?.darf_verteilen,
|
|
}));
|
|
if (stand.band === true && stand.aus === false && stand.darf === true) {
|
|
/* Nicht nur hinsehen -- einmal aufmachen. Ein Knopf, der
|
|
nicht abgeschaltet ist, muss noch lange nichts tun. */
|
|
await v.s.click("#verteilen-knopf");
|
|
await v.s.waitForTimeout(500);
|
|
const auf = await v.s.evaluate(() => ({
|
|
offen: !document.getElementById("e-neu")?.hidden,
|
|
leute: document.querySelectorAll("#e-fuer-leute input[type=checkbox]").length,
|
|
}));
|
|
if (auf.offen && auf.leute > 0) konnten++;
|
|
}
|
|
ok(stand.band === true && stand.aus === false,
|
|
`${wer} sieht das Verteilband und kann es bedienen`
|
|
+ ` (sichtbar=${stand.band}, aus=${stand.aus})`);
|
|
} finally { await v.ctx.close(); }
|
|
}
|
|
ok(konnten === verteiler.length,
|
|
`und alle ${verteiler.length} bekommen das Formular mit der Personenliste auf (${konnten})`);
|
|
|
|
/* GEGENPROBE: Ein Modi darf es NICHT. Ohne diese Zeile waere nicht
|
|
bewiesen, dass die drei oben etwas Besonderes koennen -- eine
|
|
Pruefung, die bei jedem gruen wird, misst keine Grenze. */
|
|
{
|
|
const m = await seiteAls("modi", "CODE-MODI-0001", 1180);
|
|
try {
|
|
await m.s.goto(`${SEITE}/workspace/entwicklung.html`, { waitUntil: "networkidle" });
|
|
await m.s.waitForTimeout(1700);
|
|
const stand = await m.s.evaluate(() => ({
|
|
band: document.getElementById("verteilen-band")
|
|
? !document.getElementById("verteilen-band").hidden : null,
|
|
darf: window.__ich?.darf_verteilen,
|
|
}));
|
|
ok(stand.band === false && stand.darf === false,
|
|
`Gegenprobe: ein Modi sieht das Verteilband nicht`
|
|
+ ` (sichtbar=${stand.band}, darf=${stand.darf})`);
|
|
} finally { await m.ctx.close(); }
|
|
}
|
|
|
|
/* =================================================================
|
|
DIE SPALTE „BEWERBUNGEN" IM AUFGABENBRETT (25.09.2026)
|
|
=================================================================
|
|
|
|
Filipe: „ich will in dieser seite auch eine eigene kategorie fuer
|
|
die aufgaben wo die modis sich selbst bewerben. ich will dass man
|
|
die getrennt sieht. mach es richtig uebersichtlich."
|
|
|
|
BISHER STANDEN BEWERBUNGEN NUR AUF DEM VORLAGENBRETT, an der
|
|
jeweiligen Karte. Wer morgens das Aufgabenbrett oeffnete, sah
|
|
nicht, dass jemand auf eine Antwort wartet.
|
|
|
|
DREI DINGE WERDEN GEMESSEN, und das dritte ist das, was ohne
|
|
Pruefung irgendwann kippt:
|
|
1. Wer entscheidet, sieht die Spalte MIT den zwei Knoepfen.
|
|
2. Der Modi sieht seine eigene -- ohne Knoepfe, mit dem Satz
|
|
„wartet auf Antwort". Ein Knopf, der eine Absage holt,
|
|
waere schlimmer als keiner.
|
|
3. Ohne Bewerbung gibt es die Spalte GAR NICHT. Eine leere
|
|
Spalte stuende 360 Tage im Jahr im Weg.
|
|
================================================================= */
|
|
{
|
|
/* ERST DER ZUSTAND OHNE: Ohne diese Messung waere „die Spalte ist
|
|
da" auch dann gruen, wenn sie immer da waere. */
|
|
const vorher = await seiteAls("admin", "CODE-DOGI-0001", 1400);
|
|
let ohne = null;
|
|
try {
|
|
await vorher.s.goto(`${SEITE}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
|
|
await vorher.s.waitForTimeout(1700);
|
|
ohne = await vorher.s.evaluate(() =>
|
|
document.querySelectorAll(".spalte--bewerbung").length);
|
|
} finally { await vorher.ctx.close(); }
|
|
ok(ohne === 0, `ohne Bewerbung gibt es die Spalte nicht (${ohne})`);
|
|
|
|
/* JETZT BEWIRBT SICH EIN MODI -- ueber die echte Schnittstelle
|
|
der Seite, damit Sitzung und Herkunftspruefung mitlaufen. */
|
|
const m = await seiteAls("modi", "CODE-MODI-0001", 1400);
|
|
let beworben = null, beiModi = null;
|
|
try {
|
|
await m.s.goto(`${SEITE}/workspace/entwicklung.html`, { waitUntil: "networkidle" });
|
|
await m.s.waitForTimeout(1500);
|
|
beworben = await m.s.evaluate(async () => {
|
|
const k = await fetch("/workspace/api/vorlagen", { credentials: "same-origin" });
|
|
const d = await k.json();
|
|
const v = (d.katalog?.aufgaben || [])[0];
|
|
if (!v) return { fehler: "kein Katalog" };
|
|
const a = await fetch("/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", credentials: "same-origin",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ vorlage: v.schluessel, text: "ich habe Freitag Zeit" }),
|
|
});
|
|
return { code: a.status, titel: v.titel };
|
|
});
|
|
/* UND DER MODI SIEHT SEINE EIGENE -- auf dem Aufgabenbrett. */
|
|
await m.s.goto(`${SEITE}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
|
|
await m.s.waitForTimeout(1800);
|
|
beiModi = await m.s.evaluate(() => {
|
|
const sp = document.querySelector(".spalte--bewerbung");
|
|
if (!sp) return { spalte: false };
|
|
return {
|
|
spalte: true,
|
|
karten: sp.querySelectorAll(".karte--bewerbung").length,
|
|
knoepfe: sp.querySelectorAll(".karte--bewerbung button").length,
|
|
warte: !!sp.querySelector(".karte__warte"),
|
|
titel: sp.querySelector(".karte__titel")?.textContent || "",
|
|
wort: sp.querySelector(".karte__bwort")?.textContent || "",
|
|
};
|
|
});
|
|
} finally { await m.ctx.close(); }
|
|
|
|
ok(beworben?.code === 201,
|
|
`ein Modi bewirbt sich (HTTP ${beworben?.code} ${beworben?.fehler || ""})`);
|
|
ok(beiModi?.spalte === true && beiModi.karten === 1,
|
|
`er sieht sie im Aufgabenbrett in einer eigenen Spalte (${beiModi?.karten})`);
|
|
ok(beiModi?.titel === beworben?.titel,
|
|
`mit dem TITEL der Aufgabe, nicht dem Schluessel ("${beiModi?.titel}")`);
|
|
ok((beiModi?.wort || "").includes("Freitag"),
|
|
`und mit seinem eigenen Satz dazu ("${beiModi?.wort}")`);
|
|
ok(beiModi?.knoepfe === 0 && beiModi?.warte === true,
|
|
`Gegenprobe: er bekommt KEINE Entscheidungsknoepfe (${beiModi?.knoepfe}), `
|
|
+ `sondern „wartet auf Antwort" (${beiModi?.warte})`);
|
|
|
|
/* UND DER, DER ENTSCHEIDET. */
|
|
const e2 = await seiteAls("admin", "CODE-DOGI-0001", 1400);
|
|
let beiDogi = null;
|
|
try {
|
|
await e2.s.goto(`${SEITE}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
|
|
await e2.s.waitForTimeout(1800);
|
|
beiDogi = await e2.s.evaluate(() => {
|
|
const sp = document.querySelector(".spalte--bewerbung");
|
|
if (!sp) return { spalte: false };
|
|
return {
|
|
spalte: true,
|
|
karten: sp.querySelectorAll(".karte--bewerbung").length,
|
|
knoepfe: [...sp.querySelectorAll(".karte--bewerbung button")]
|
|
.map((b) => b.textContent.trim()),
|
|
/* Die Spalte muss die ERSTE sein: Was auf eine Antwort
|
|
wartet, gehoert an die Stelle, die man zuerst ansieht. */
|
|
erste: document.querySelector("#brett .spalte")?.classList
|
|
.contains("spalte--bewerbung"),
|
|
wer: sp.querySelector(".karte__wer")?.textContent || "",
|
|
};
|
|
});
|
|
} finally { await e2.ctx.close(); }
|
|
|
|
ok(beiDogi?.spalte === true && beiDogi.karten === 1,
|
|
`DogFather sieht dieselbe Bewerbung (${beiDogi?.karten})`);
|
|
ok(beiDogi?.erste === true,
|
|
`und die Spalte steht ganz vorn (${beiDogi?.erste})`);
|
|
ok(JSON.stringify(beiDogi?.knoepfe) === JSON.stringify(["Annehmen", "Ablehnen"]),
|
|
`mit beiden Knoepfen (${JSON.stringify(beiDogi?.knoepfe)})`);
|
|
ok((beiDogi?.wer || "").includes("möchte das übernehmen"),
|
|
`und dem Namen dessen, der wartet ("${beiDogi?.wer}")`);
|
|
}
|
|
|
|
ok(meckern.length === 0, meckern.length
|
|
? `der Browser meldet: ${meckern.slice(0, 3).join(" | ")}`
|
|
: "kein Absturz und keine Fehlermeldung im Browser");
|
|
} finally {
|
|
await browser.close();
|
|
await new Promise((r) => vorbau.close(r));
|
|
}
|
|
}
|
|
|
|
/* =====================================================================
|
|
ERGEBNIS
|
|
===================================================================== */
|
|
melde("");
|
|
if (geprueft < 70) {
|
|
console.log(` FEHL nur ${geprueft} Pruefungen gelaufen -- erwartet waren 70+`);
|
|
fehler++;
|
|
}
|
|
melde(fehler === 0
|
|
? `Alles in Ordnung. ${geprueft} Pruefungen, 0 Fehler.`
|
|
: `${fehler} FEHLER bei ${geprueft} Pruefungen.`);
|
|
melde("");
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler ? 1 : 0);
|