/* ===================================================================== pruef-portnummern.mjs — Zwei Pruefungen, eine Nummer WAS HIER SCHIEFGING (20.09.2026 gemessen): 164 Pruefdateien, 129 verschiedene Nummern -- NEUNZEHN waren doppelt vergeben, 4186 und 4188 und 4196 und 4198 sogar dreifach. Niemand hatte das gewollt; jede neue Pruefung wurde von einer vorhandenen abgeschrieben, und die Nummer kam mit. WAS DAS ANRICHTET: Der Waechter bricht ab, statt gegen einen fremden Server zu messen -- so weit, so richtig. Aber zwei Pruefungen mit derselben Nummer koennen nie gleichzeitig laufen, und ein liegengebliebener Prozess der einen laesst die andere mit einer Meldung abbrechen, die wie ein Befund aussieht. Genau das ist am 20.09. zweimal passiert, beide Male mit Fehlersuche an der falschen Stelle. WAS DIESE PRUEFUNG SICHERT: Dass die Nummer ABGELEITET bleibt. Eine von Hand eingetragene Zahl ist der Anfang der naechsten Doppelung -- sie faellt nur niemandem auf, bis zwei Laeufe kollidieren. ===================================================================== */ import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { eigenerPort, portNummer, MESS_BASIS, GESPERRT } from "./helfer-port.mjs"; let ok_ = 0, fehl = 0; const ok = (b, text) => { if (b) { ok_++; console.log(" ok " + text); } else { fehl++; console.log(" FEHL " + text); } }; const ORDNER = new URL(".", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"); const dateien = readdirSync(ORDNER) .filter((f) => f.startsWith("pruef-") && f.endsWith(".mjs")).sort(); /* DIE MESSDATEIEN GEHOEREN DAZU (01.10.2026). Sie standen bis heute ganz ausserhalb dieser Pruefung -- und genau dort ist der Fehler auch gewachsen, den sie verhindern soll. */ const messen = readdirSync(ORDNER) .filter((f) => f.startsWith("mess-") && f.endsWith(".mjs")).sort(); console.log("\n=== 1. Niemand traegt eine Nummer von Hand ein ==="); const vonHand = []; const ohneWaechter = []; for (const f of dateien) { const s = readFileSync(ORDNER + "/" + f, "utf8"); /* Kommentare zaehlen nicht: In helfer-port.mjs und hier stehen alte Nummern als Beleg, und ein Beleg ist kein Eintrag. */ const code = s.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); if (/portMussFreiSein\(\s*\d/.test(code)) vonHand.push(f); /* ==== WER STARTET WIRKLICH EINEN SERVER? (25.09.2026) ========== Hier stand `listen(` -- und das trifft auch den, der nur NACHSIEHT, ob eine Nummer frei ist: `pruef-ports.mjs` macht einen Port auf, schliesst ihn sofort wieder und startet dabei gar nichts. Sie wurde deshalb angemahnt, den Waechter nicht zu benutzen, den sie selbst prueft. DAS ECHTE MERKMAL IST DAS HAUS: Wer `index.js` importiert oder `process.env.PORT` setzt, laesst den Workspace laufen und braucht eine eigene, eindeutige Nummer. Wer nur `createServer().listen()` aufruft, um das Betriebssystem zu fragen, braucht sie nicht -- und darf es auch gar nicht, sonst verschoebe sich die ganze Nummernfolge. DIE GEGENPROBE UNTEN BLEIBT GUELTIG und wird sogar schaerfer: Sie baut eine Datei, die `index.js` importiert und keinen Waechter hat -- also genau den Fall, um den es geht. */ const startetServer = /import\(\s*["'`]\.\/index\.js["'`]\s*\)/.test(code) || /process\.env\.PORT\s*=/.test(code); if (startetServer && !/eigenerPort\(\s*import\.meta/.test(code)) ohneWaechter.push(f); } ok(dateien.length > 100, `${dateien.length} Pruefdateien angesehen`); ok(vonHand.length === 0, `keine traegt eine feste Nummer im Waechter (${vonHand.slice(0, 4).join(", ") || "—"})`); ok(ohneWaechter.length === 0, `jede, die einen Server startet, leitet ihre Nummer ab (${ohneWaechter.slice(0, 4).join(", ") || "—"})`); /* ==== KANN DIESE ERKENNUNG AUCH NEIN SAGEN? (25.09.2026) ============ Die Zeile darueber meldet „alles in Ordnung", solange sie NICHTS findet -- und genau so sieht auch eine Erkennung aus, die nichts mehr findet, weil man sie zu eng gefasst hat. Am 25.09. wurde sie enger gefasst (vorher schlug jedes `listen(` an, auch das blosse Nachsehen, ob ein Port frei ist). Ohne diese Probe waere aus der Verbesserung ein blinder Fleck geworden. Gebaut wird der Fall im Speicher, nicht als Datei: Eine echte Datei im Ordner wuerde die Nummernfolge aller anderen verschieben -- und das ist genau das, wovor diese Pruefung schuetzt. */ { const wieErkannt = (code) => (/import\(\s*["'`]\.\/index\.js["'`]\s*\)/.test(code) || /process\.env\.PORT\s*=/.test(code)) && !/eigenerPort\(\s*import\.meta/.test(code); ok(wieErkannt('await import("./index.js");'), "Gegenprobe: ein Serverstart ohne Waechter faellt auf"); ok(wieErkannt('process.env.PORT = "5000";'), " und eine von Hand gesetzte Nummer ebenfalls"); ok(!wieErkannt('const PORT = await eigenerPort(import.meta, "x");\n' + 'process.env.PORT = `${PORT}`;\nawait import("./index.js");'), " wer den Waechter benutzt, faellt NICHT auf"); ok(!wieErkannt('const s = createServer(); s.listen(port, "127.0.0.1");'), " und wer nur nachsieht, ob ein Port frei ist, auch nicht"); } console.log("\n=== 2. Die abgeleiteten Nummern sind eindeutig ==="); const belegt = new Map(); let doppelt = 0; dateien.forEach((f, i) => { for (let n = 0; n < 2; n++) { const p = portNummer(i * 2 + n); if (belegt.has(p)) { doppelt++; console.log(` ${p}: ${belegt.get(p)} und ${f}`); } belegt.set(p, f); } }); ok(belegt.size === dateien.length * 2 && doppelt === 0, `${belegt.size} Nummern fuer ${dateien.length} Dateien, ${doppelt} doppelt`); console.log("\n=== 2b. Keine davon ist eine, die `fetch` verweigert ==="); /* DIE TEUERSTE HALBE STUNDE DIESES ABENDS. Nach der Umstellung bekam pruef-chat-kanaele die 5060 und stuerzte ab -- obwohl der Server nachweislich lief und die Anmeldung ueber `http.request` geklappt hatte. 5060 ist SIP und steht auf der "bad ports"-Liste der Fetch-Spezifikation, die Node uebernommen hat: `fetch` baut dorthin keine Verbindung auf. Die Meldung lautet "fetch failed" -- genau wie bei einem Server, der nicht laeuft. Wer sie liest, sucht am Server. Deshalb steht sie jetzt hier als eigene Pruefung: Die naechste Datei, die in so eine Nummer rutscht, soll es HIER erfahren und nicht in einer Fehlersuche. */ const BOESE = [1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 138, 139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, 6669, 6679, 6697, 10080]; const getroffen = [...belegt.keys()].filter((x) => BOESE.includes(x)); ok(getroffen.length === 0, `keine der ${belegt.size} Nummern steht auf der Sperrliste (${getroffen.join(", ") || "—"})`); /* GEGENPROBE AM ECHTEN VERHALTEN, nicht an der Liste: Ein Zuhoerer auf einer gesperrten Nummer muss fuer `fetch` unerreichbar sein, einer daneben erreichbar. Ohne das waere die Liste oben nur eine Behauptung ueber Node -- und Behauptungen altern. */ { const { createServer } = await import("node:http"); const probe = async (nummer) => { const dienst = createServer((q, a) => { a.writeHead(200); a.end("da"); }); await new Promise((r) => dienst.listen(nummer, "127.0.0.1", r)); let grund = "erreichbar"; try { await fetch(`http://127.0.0.1:${nummer}/`); } catch (f) { grund = f?.cause?.message || f?.message || "?"; } await new Promise((r) => dienst.close(r)); return grund; }; const gesperrt = await probe(5060); const erlaubt = await probe(portNummer(0)); ok(/bad port/i.test(gesperrt), `eine gesperrte Nummer ist fuer fetch tot, obwohl jemand lauscht (5060: ${gesperrt})`); ok(erlaubt === "erreichbar", `eine abgeleitete dagegen nicht (${portNummer(0)}: ${erlaubt})`); } console.log("\n=== 3. Die Ableitung stimmt wirklich (gemessen, nicht gerechnet) ==="); /* NICHT die Formel gegen sich selbst pruefen -- das waere eine Bestaetigung ohne Aussage. Gefragt wird die Funktion, die die Pruefungen auch benutzen, und verglichen wird mit der Stelle im Alphabet. */ const ersteDatei = dateien[0]; const meine = dateien.indexOf("pruef-portnummern.mjs"); const p0 = await eigenerPort(import.meta, "pruef-portnummern (Selbsttest)", 0); ok(p0 === portNummer(meine * 2), `meine eigene Nummer ist ${p0} — Stelle ${meine} im Alphabet, erste Datei ${ersteDatei}`); const p1 = await eigenerPort(import.meta, "pruef-portnummern (Selbsttest)", 1); ok(p1 === p0 + 1, `und die zweite ist ${p1}, also genau eine daneben`); console.log("\n=== 4. Gegenprobe: kann sie auch NICHT IN ORDNUNG sagen? ==="); /* Eine Pruefung, die immer bestaetigt, bestaetigt nichts. */ let drittesAusgangGriff = false; try { /* Eine Datei, die es nicht gibt -- die Ableitung darf dann NICHT raten, sondern muss abbrechen. */ await eigenerPort({ url: new URL("./gibt-es-nicht.mjs", import.meta.url).href }, "Gegenprobe"); } catch (f) { drittesAusgangGriff = /steht nicht in der Liste/.test(f?.message || ""); } ok(drittesAusgangGriff, "eine unbekannte Datei bekommt keine geratene Nummer, sondern einen Abbruch"); let zuHoheNummer = false; try { await eigenerPort(import.meta, "Gegenprobe", 7); } catch (f) { zuHoheNummer = /Nummern je Datei/.test(f?.message || ""); } ok(zuHoheNummer, "und eine dritte Nummer je Datei gibt es nicht"); console.log("\n=== 5. Die Messdateien leiten ihre Nummer auch ab (01.10.2026) ==="); /* WAS HIER AM 01.10. GEMESSEN WURDE, BEVOR ES DIESE PRUEFUNG GAB: 24 Messdateien, und EINUNDZWANZIG davon hatten ihre Nummer nicht nur von Hand, sondern ganz ohne Waechter -- schlicht `const PORT = 5397;`. Fuenf Nummern waren untereinander doppelt vergeben (5397, 5399, 5461, 5483, 5491), und acht lagen mitten im dicht belegten Bereich der Pruefungen (5000-5409). Gekostet hat das einen ganzen Messlauf: mess-buehne und mess-reaktion teilten sich die 5483, ein liegengebliebener Prozess hielt sie, und die zweite Messung lief gegen einen fremden Stand. Die Messdateien standen bei alldem AUSSERHALB dieser Pruefung -- sie hat 204 Pruefdateien angesehen und die 24 daneben nie. Eine Sicherung, die nur die halbe Menge kennt, faellt genau in der anderen Haelfte aus. */ { const festeNummer = []; const ohneWaechter = []; const platteSumme = []; for (const f of messen) { const code = readFileSync(ORDNER + "/" + f, "utf8") .replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); if (/const\s+\w*PORT\w*\s*=\s*\d/.test(code) || /portMussFreiSein\(\s*\d/.test(code)) festeNummer.push(f); /* `PORT + 1` IST KEINE ABLEITUNG. `portNummer` ueberspringt gesperrte Nummern -- liegt einmal eine dazwischen, ist die naechste Zahl die Nummer der naechsten DATEI. Heute liegt dort keine; das ist Glueck, kein Entwurf. */ if (/PORT\s*\+\s*1\b/.test(code)) platteSumme.push(f); const startetServer = /import\(\s*["'`]\.\/index\.js["'`]\s*\)/.test(code) || /process\.env\.PORT\s*=/.test(code); if (startetServer && !/eigenerPort\(\s*import\.meta/.test(code)) ohneWaechter.push(f); } ok(messen.length > 10, `${messen.length} Messdateien angesehen`); ok(festeNummer.length === 0, `keine traegt eine Nummer von Hand (${festeNummer.slice(0, 4).join(", ") || "—"})`); ok(ohneWaechter.length === 0, `jede, die einen Server startet, leitet ihre Nummer ab (${ohneWaechter.slice(0, 4).join(", ") || "—"})`); ok(platteSumme.length === 0, `keine rechnet ihre zweite Nummer als PORT + 1 (${platteSumme.slice(0, 4).join(", ") || "—"})`); /* ---- KANN DIESE ERKENNUNG AUCH NEIN SAGEN? ----------------------- Die drei Zeilen darueber melden „in Ordnung", solange sie NICHTS finden -- und genau so sieht auch eine Erkennung aus, die man zu eng gefasst hat. Am 01.10. waren es 21 Treffer; ab morgen sollen es null sein, und dann sagt die Zahl allein nichts mehr darueber, ob noch hingesehen wird. */ const alsFest = (c) => /const\s+\w*PORT\w*\s*=\s*\d/.test(c) || /portMussFreiSein\(\s*\d/.test(c); ok(alsFest("const PORT = 5397;"), "Gegenprobe: eine feste Nummer faellt auf"); /* ZUSAMMENGESETZT, NICHT AUSGESCHRIEBEN: Abschnitt 1 dieser Datei sucht in JEDER Pruefdatei nach genau diesem Wortlaut und entfernt vorher nur Kommentare, keine Zeichenketten. Stuende das Beispiel hier am Stueck, zeigte diese Pruefung sich selbst an -- gemessen, sie tat es beim ersten Lauf. */ ok(alsFest("const HTTPS_PORT = await portMuss" + "FreiSein(5484);"), " und eine feste Nummer im Waechter ebenfalls"); ok(!alsFest("const PORT = await eigenerPort(import.meta);"), " wer ableitet, faellt NICHT auf"); ok(/PORT\s*\+\s*1/.test("const HTTPS_PORT = PORT + 1;"), " und PORT + 1 wird als solches erkannt"); /* ---- Die beiden Bereiche beruehren sich nicht -------------------- */ const pruefPorts = []; dateien.forEach((f, i) => pruefPorts.push(portNummer(i * 2), portNummer(i * 2 + 1))); const messPorts = []; messen.forEach((f, i) => messPorts.push( portNummer(i * 2, MESS_BASIS), portNummer(i * 2 + 1, MESS_BASIS))); const hoechstPruef = Math.max(...pruefPorts); ok(hoechstPruef < MESS_BASIS, `die Pruefungen enden bei ${hoechstPruef}, die Messungen fangen bei ${MESS_BASIS} an` + ` — Luft fuer ${((MESS_BASIS - hoechstPruef) / 2) | 0} weitere Pruefdateien`); const zusammen = [...pruefPorts, ...messPorts]; ok(new Set(zusammen).size === zusammen.length, `alle ${zusammen.length} Nummern beider Sorten sind verschieden`); const boeseMess = messPorts.filter((p) => GESPERRT.has(p)); ok(boeseMess.length === 0, `keine Messnummer steht auf der Sperrliste (${boeseMess.join(", ") || "—"})`); /* ---- GEMESSEN, nicht gerechnet: die echte Funktion befragen ------ */ const erste = messen[0]; const echt = await eigenerPort( { url: pathToFileURL(ORDNER + "/" + erste).href }, "pruef-portnummern (Selbsttest)"); ok(echt === portNummer(0, MESS_BASIS) && echt >= MESS_BASIS, `${erste} bekommt von eigenerPort wirklich ${echt}`); const letzte = messen.at(-1); const echt2 = await eigenerPort( { url: pathToFileURL(ORDNER + "/" + letzte).href }, "pruef-portnummern (Selbsttest)", 1); ok(echt2 === portNummer((messen.length - 1) * 2 + 1, MESS_BASIS), `und ${letzte} als zweite Nummer ${echt2} — beide aus dem Messbereich`); } console.log("\n=== 6. Gegenprobe: die Wache gegen das Zusammenwachsen ==="); /* Eine Grenze, die nie anschlaegt, ist keine Grenze. Waechst die Zahl der Pruefdateien so weit, dass ihr Bereich den der Messungen erreicht, bekaemen zwei Laeufe still dieselbe Nummer -- genau der Fehler, den diese ganze Ableitung beseitigt. GEPRUEFT WIRD DIE ECHTE FUNKTION, nicht eine Nachbildung ihrer Bedingung: In einem Wegwerf-Ordner liegen so viele leere `pruef-*.mjs`, dass die letzte ueber MESS_BASIS hinauskaeme. Ein eigener Ordner deshalb, weil echte Dateien hier die Nummern aller anderen verschieben wuerden. */ { const wegwerf = mkdtempSync(join(tmpdir(), "portwache-")); const noetig = Math.ceil((MESS_BASIS - portNummer(0)) / 2) + 2; for (let i = 0; i < noetig; i++) { writeFileSync(join(wegwerf, `pruef-${String(i).padStart(5, "0")}.mjs`), ""); } const letzte = join(wegwerf, `pruef-${String(noetig - 1).padStart(5, "0")}.mjs`); let sagte = ""; try { await eigenerPort({ url: pathToFileURL(letzte).href }, "Gegenprobe"); } catch (f) { sagte = f?.message || ""; } ok(/stoesst an den der Messdateien/.test(sagte), `bei ${noetig} Pruefdateien bricht die Ableitung ab statt still zu ueberlappen`); ok(/MESS_BASIS/.test(sagte) && /hochsetzen/.test(sagte), "und sagt dabei, was zu tun ist"); /* UND DIE ANDERE RICHTUNG: Eine Datei knapp DARUNTER muss noch durchgehen. Ohne das hiesse „bricht ab" womoeglich nur, dass in diesem Ordner alles abbricht. */ const knapp = join(wegwerf, `pruef-${String(noetig - 30).padStart(5, "0")}.mjs`); let ging = 0; try { ging = await eigenerPort({ url: pathToFileURL(knapp).href }, "Gegenprobe"); } catch { ging = 0; } ok(ging > 0 && ging < MESS_BASIS, `eine Datei knapp darunter bekommt weiter ihre Nummer (${ging})`); rmSync(wegwerf, { recursive: true, force: true }); } console.log("\n=== 7. Die uebrigen Werkzeuge, die einen Server starten ==="); /* BEIM UMBAU AM 01.10. AUFGEFALLEN: Es gibt eine dritte Sorte. Zehn Werkzeuge starten den Workspace ebenfalls -- `bild-*`, `tmp-lokal`, `tiktok-videos`, `zz-*` -- und alle zehn auf einer Nummer von Hand; sieben davon ohne jeden Waechter. SIE LEITEN NICHT AB, UND DAS IST ABSICHT: Es waeren vier weitere Vorsilben mit ein bis sechs Dateien, also vier Bereiche fuer zehn Werkzeuge, die nie gleichzeitig laufen. Der saubere Weg waere, sie `mess-` zu nennen -- ein eigener Schritt. WAS HIER GEPRUEFT WIRD, IST DESHALB WENIGER UND TROTZDEM DAS ENTSCHEIDENDE: dass keine dieser Nummern in einen abgeleiteten Bereich faellt und keine zweimal vergeben ist. Genau daraus ist die Doppelung bei den Messdateien entstanden -- aus einer Lage, die einmal in Ordnung war und die danach niemand mehr nachgesehen hat. */ { const uebrige = readdirSync(ORDNER) .filter((f) => f.endsWith(".mjs") && !f.startsWith("pruef-") && !f.startsWith("mess-") && !f.startsWith("helfer-")) .filter((f) => /process\.env\.PORT\s*=/.test(readFileSync(ORDNER + "/" + f, "utf8"))) .sort(); ok(uebrige.length > 0, `${uebrige.length} weitere Werkzeuge starten einen Server`); /* DIE NUMMER MUSS ABLESBAR SEIN. Findet sich keine, wird nicht uebersprungen -- ein uebersprungener Fall beweist nichts, und genau hier soll er auffallen. */ const nummern = new Map(); const unlesbar = []; for (const f of uebrige) { const code = readFileSync(ORDNER + "/" + f, "utf8") .replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); const t = code.match(/process\.env\.PORT\s*=\s*[`"']?(\d{3,5})/) || code.match(/const PORT\s*=\s*(\d{3,5})/) || code.match(/portMussFreiSein\(\s*(\d{3,5})/); if (!t) { unlesbar.push(f); continue; } nummern.set(f, Number(t[1])); } ok(unlesbar.length === 0, `bei jedem steht die Nummer im Quelltext (${unlesbar.join(", ") || "—"})`); const werte = [...nummern.values()]; const doppelte = werte.filter((p, i) => werte.indexOf(p) !== i); ok(werte.length > 0 && doppelte.length === 0, `keine ihrer ${werte.length} Nummern ist doppelt (${[...new Set(doppelte)].join(", ") || "—"})`); const pruefPorts2 = []; dateien.forEach((f, i) => pruefPorts2.push(portNummer(i * 2), portNummer(i * 2 + 1))); const messPorts2 = []; messen.forEach((f, i) => messPorts2.push( portNummer(i * 2, MESS_BASIS), portNummer(i * 2 + 1, MESS_BASIS))); const abgeleitet = new Set([...pruefPorts2, ...messPorts2]); const drin = [...nummern].filter(([, p]) => abgeleitet.has(p)); ok(werte.length > 0 && drin.length === 0, "keine liegt in einem abgeleiteten Bereich" + ` (${drin.map(([f, p]) => `${f}:${p}`).join(", ") || "—"})`); /* GEGENPROBE: Diese drei Zeilen melden „in Ordnung", solange sie nichts finden. Also einmal zeigen, dass sie etwas finden KOENNEN. */ const probe = [...abgeleitet][0]; ok(abgeleitet.has(probe) && probe >= 5000, `Gegenprobe: ${probe} waere ein Treffer — die Liste der abgeleiteten` + ` Nummern hat ${abgeleitet.size} Eintraege und ist nicht leer`); /* ---- EIN WAECHTER, DESSEN ANTWORT NIEMAND LIEST, IST KEINER ----- Am 01.10. in allen sechs `bild-*.mjs` gefunden: await portMussFreiSein(4315, "das Bildwerkzeug"); process.env.PORT = "4315"; const BASIS = "http://127.0.0.1:4315"; Der Waechter lief, meldete nichts und wirkte nicht. Gibt das System den Port dauerhaft nicht her, weicht er auf Port + 4000 aus und GIBT DIE NEUE NUMMER ZURUECK -- diese Werkzeuge hoerten danach trotzdem auf der alten und stuerzten mit `listen EACCES` ab, also mit genau dem Fehler, gegen den er gebaut wurde. Dazu stand die Zahl dreimal je Datei. Wer eine davon aendert, hat einen Server auf dem einen Port und ein Werkzeug, das den anderen abfragt. */ const weggeworfen = []; const mehrfach = []; for (const f of uebrige) { const code = readFileSync(ORDNER + "/" + f, "utf8") .replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); if (/^\s*await\s+(portMussFreiSein|eigenerPort)\(/m.test(code)) weggeworfen.push(f); const n = nummern.get(f); if (n !== undefined) { const wieOft = (code.match(new RegExp(String(n), "g")) || []).length; if (wieOft > 1) mehrfach.push(`${f}:${wieOft}x`); } } ok(weggeworfen.length === 0, `jedes benutzt die Antwort des Waechters (${weggeworfen.join(", ") || "—"})`); ok(mehrfach.length === 0, `und schreibt seine Nummer nur einmal hin (${mehrfach.join(", ") || "—"})`); /* GEGENPROBE zu beiden: Erkennen sie den Rueckschritt auch? */ const wirftWeg = (c) => /^\s*await\s+(portMussFreiSein|eigenerPort)\(/m.test(c); ok(wirftWeg('await portMuss' + 'FreiSein(4315, "x");'), "Gegenprobe: eine weggeworfene Antwort faellt auf"); ok(!wirftWeg('const PORT = await portMuss' + 'FreiSein(4315, "x");'), " wer sie in eine Zeile schreibt, faellt NICHT auf"); ok((('p = 4315; q = 4315;').match(/4315/g) || []).length === 2, " und eine zweimal genannte Nummer wird gezaehlt"); console.log(` ${[...nummern].map(([f, p]) => `${f.replace(/\.mjs$/, "")} ${p}`).join(", ")}`); } console.log(`\n${ok_} Pruefungen, ${fehl} Fehler`); console.log(fehl ? "NICHT IN ORDNUNG\n" : "ALLES IN ORDNUNG\n"); process.exit(fehl ? 1 : 0);