A4: 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."
ZWEI FRAGEN, DIE MAN AUSEINANDERHALTEN MUSS -- und das war der Schluessel:
verteilen eine Aufgabe an ANDERE geben
entscheiden bestimmen, wer sie am Ende macht
Sein Satz vom selben Tag („rechte hand linke hand und dogfather ...
koennen alle verteilen") widerspricht dem nicht, er beantwortet die
erste Frage. Die linke Hand verteilt, entscheidet aber nicht -- sie
bewirbt sich wie ein Modi. `entscheidetUeberAufgaben` steht neben
`darfAufgabenVerteilen`, und drei Stellen fragen ab jetzt dieselbe
Funktion.
DAS VERBOT HATTE DREI TUEREN, und die zweite und dritte waren beim
Planen nicht zu sehen:
1. aus dem Pool „uebernehmen" -- die offensichtliche
2. beim Pool „annehmen" -- dasselbe unter anderem Namen: Wer
zusagt, nimmt sie den anderen weg.
Bei „einzeln"/„mehrere" bleibt es
erlaubt -- dort wurde sie ihm
ZUGETRAGEN, und genau das Wort
steht in seinem Satz.
3. beim Verteilen sich selbst eintragen -- die linke Hand darf
verteilen, haette sich also selbst
nehmen koennen. Abgelehnt statt
still gefiltert: Wer sich eintraegt
und sich danach nicht findet, sucht
den Fehler bei sich.
EIN FUND, OHNE DEN A4 GAR NICHT FUNKTIONIERT HAETTE
Die rechte Hand soll entscheiden -- und sah die Aufgabe nicht, um die
es ging. Gemessen an einer Pool-Aufgabe, die DogFather fuer zwei Modis
angelegt hat:
DogFather sieht sie 1 Bewerbung
Modi sieht sie 1 Bewerbung
rechte Hand SIEHT SIE NICHT (Liste leer)
Ihre Sichtregel sammelte Menschen mit einer TEAM_DOGI_ROLLE und fragte,
ob einer als Creator, Verantwortlicher oder Ersteller eingetragen ist.
Bei einer Pool-Aufgabe bleibt `verantwortlich_id` leer (das ist ihr
Sinn), und der Ersteller war DogFather -- der in dieser Liste nicht
steht. Alle drei Bedingungen liefen ins Leere.
Auf der Adresse von Team Dogi sehen die Haende jetzt alles. Das ist
zugleich sein Satz „sehen alle aufgaben". Die Haustrennung bleibt:
`sichtbar()` haengt fuer crew weiterhin `AND ohneAgentur(...)` davor.
WIE DIE BEWERBUNG GEBAUT IST
Kein zweiter Tisch, sondern ein weiterer Zustand derselben Zeile
(`beworben`). Die Frage „wie steht diese Aufgabe bei DIESEM Menschen"
wird dort schon beantwortet; eine zweite Tabelle haette zwei Wahrheiten
ueber dieselbe Beziehung.
Die Worte der Person (`grund`) und der Kommentar der Leitung
(`entscheid_text`) stehen in ZWEI Feldern. In dasselbe waere bequemer
und wuerde die Frage mit der Antwort ueberschreiben -- danach wuesste
niemand mehr, worum jemand gebeten hat. Dasselbe gilt, wenn eine
Bewerbung wegfaellt, weil jemand anders die Aufgabe bekommt: Der
Hinweis kommt in das Feld der Leitung, ihre Worte bleiben stehen.
Der Kommentar ist FREIWILLIG. Die Begruendung beim Ablehnen einer
zugeteilten Aufgabe bleibt Pflicht -- dort sagt jemand ab, der gefragt
wurde. Hier bittet jemand; ein „bitte" braucht keine Begruendung.
Filipes Wort ist „als moeglichkeit".
Die CHECK-Regel wurde ueber `checkListeErweitern` erweitert -- den Weg,
der seit dem 09.09. an einer Stelle steht, mit Sicherung vorher,
Spaltenliste aus PRAGMA und Indizes, die mitgehen.
UND EIN SATZ, DER NICHT MEHR STIMMTE
„Frei fuer 3 Leute — wer zuerst Zeit hat" war die Beschreibung des
Pools, solange sich jeder selbst bedienen durfte. Er sagt jetzt jedem,
was FUER IHN gilt.
GEPRUEFT
Neu: server/pruef-bewerbung-aufgaben.mjs, 66/0 -- die Regel selbst,
alle drei Tueren, der erlaubte Weg, die zwei Felder, das Zurueckziehen,
und ein Abschnitt am echten Bildschirm (ein Modi sieht „Ich bewerbe
mich" statt „Ich uebernehme das"; die rechte Hand sieht die Bewerbung
mit Annehmen und Ablehnen; die Bewerberin sieht KEINEN Block zum
Entscheiden -- sonst waere das Verbot einen Knopf weiter offen).
pruef-zuteilung auf die neue Regel umgestellt (sie hielt „Bea
uebernimmt sie" fest und wurde zu Recht rot) -- misst jetzt denselben
Effekt ueber den neuen Weg. Dazu gruen: pruef-aufgabenbrett,
pruef-meldungen, pruef-sicht, pruef-css-klassen.
ALTLAST, nicht von hier: pruef-rollen meldet einen Fehler an der
Kachel „Zu Team Dogi" (absolute Adresse). Mit `git stash` nachgemessen
-- vorher und nachher identisch.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,566 @@
|
||||
/* =====================================================================
|
||||
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("=== 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(); }
|
||||
|
||||
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 < 55) {
|
||||
console.log(` FEHL nur ${geprueft} Pruefungen gelaufen -- erwartet waren 55+`);
|
||||
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);
|
||||
Reference in New Issue
Block a user