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."
WO SIE VORHER STANDEN, und warum das nicht reichte: nur auf dem
Vorlagenbrett, an der jeweiligen Karte. Das ist der richtige Ort zum
ENTSCHEIDEN -- man liest Aufgabe und Name nebeneinander. Es ist der
falsche Ort zum SEHEN: Wer morgens das Aufgabenbrett oeffnet, sieht
nicht, dass drei Leute auf eine Antwort warten, und das Vorlagenbrett
ist eine Seite weiter. Fuer den Modi gab es ueberhaupt keine Stelle,
an der stand "dein Wunsch ist angekommen".
DIE SPALTE STEHT GANZ VORN. Sie ist die einzige, in der jemand auf eine
ANTWORT wartet -- alle anderen zeigen Arbeit, die laeuft. Und sie
VERSCHWINDET, wenn nichts wartet: Eine leere Spalte "Bewerbungen"
stuende 360 Tage im Jahr im Weg, um an fuenf Tagen etwas zu sagen. Die
vier festen Spalten haben auch leer eine Aussage ("hier landet, was
fertig ist"); diese nicht.
WAS AUF DER KARTE STEHT: der TITEL der Vorlage (nicht ihr Schluessel),
wer sie uebernehmen moechte, und sein eigener Satz dazu -- als Zitat
gesetzt, mit Strich davor. Er gehoert ihm, nicht dem Brett. Ohne ihn
entscheidet man ueber einen Namen.
DER TITEL WIRD NACHGESCHLAGEN, NICHT MITGESPEICHERT. Er gehoert dem
Katalog; stuende er in der Bewerbungszeile, gaebe es zwei Wahrheiten,
und die aeltere gewinnt still, sobald jemand eine Vorlage umbenennt.
EIN EIGENER, KLEINER WEG statt eines Mitschleppens:
`/workspace/api/vorlagen/bewerbungen` liefert nur die Bewerbungen --
nicht den ganzen Katalog, der an `/workspace/api/vorlagen` haengt. Das
Brett zeichnet sich bei jedem Statuswechsel neu; der Katalog ist um ein
Vielfaches groesser als die Handvoll Bewerbungen.
WER ENTSCHEIDEN DARF, SAGT DER SERVER (`darf_entscheiden`) -- nicht der
Rollenname im Browser. `assets/js` bekommt jeder, der die Seite
oeffnet, und ein Rollenvergleich dort ist in diesem Haus allein diese
Woche dreimal veraltet. Bis die Antwort da ist, gilt `false`: lieber
einen Knopf zu spaet zeigen als einen, der eine Absage holt.
DIE FARBE IST NEU IM BRETT. Die vier vorhandenen stehen fuer einen
Arbeitsstand (grau, blau, gelb, gruen); hier wartet niemand auf Arbeit,
sondern auf eine Entscheidung. Kein Rot ("kaputt"), kein Gelb (heisst
schon "zur Freigabe") -- Violett, das im Chat seit gestern fuer "an
dich gerichtet" steht. Eine Sprache im Haus.
Geprueft: pruef-bewerbung-aufgaben 101 -> 111 Punkte. Darunter die drei
Gegenproben, ohne die "die Spalte ist da" nichts bewiese: ohne
Bewerbung gibt es sie NICHT, der Modi bekommt KEINE
Entscheidungsknoepfe (sondern "wartet auf Antwort"), und die Spalte
steht wirklich an erster Stelle. Dazu pruef-aufgabenbrett und
pruef-vorlagen (24), beide unveraendert gruen.
Co-Authored-By: Claude Opus 5 <[email protected]>
984 lines
47 KiB
JavaScript
984 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";
|
|
|
|
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");
|
|
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
|
"-keyout", schluessel, "-out", zertifikat, "-days", "2",
|
|
"-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`],
|
|
{ stdio: "ignore" });
|
|
|
|
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);
|