Filipe: "ich will dass wenn ich es teile im discord der agentur, dass
ein banner und ein kompletter fertiger text fuer die creator entsteht
und nicht einfach der link kopiert wird." Den fertigen Text gibt es
seit fdadd5d5; was fehlte, war der letzte Schritt -- bisher musste er
ihn selbst einfuegen.
NEU AN DER KACHEL: ein Knopf "In den Agentur-Kanal stellen", sobald ein
Kanal eingerichtet ist. Der Browser schickt dabei KEINEN Text mit --
der Server stellt ihn aus denselben Daten noch einmal zusammen. Sonst
koennte jemand mit einem veraenderten Text unter unserem Namen posten.
EINGERICHTET WIRD IM WORKSPACE, NICHT AUF DER KOMMANDOZEILE. Die
Webhook-Adresse IST der Schluessel zum Kanal; ueber einen Befehl
eingetragen landet sie im Chatverlauf, im Bildschirmfoto und in
~/.bash_history -- am 03.09.2026 ist genau das mit einem Token
passiert. Gemessen am 08.10.2026 kann `claudian` die Datei ohnehin
nicht anlegen: /home/dogiweb/workspace-daten ist drwxr-xr-x
dogiweb:dogiweb, und `sudo -l` deckt `dogiweb` nicht ab. Der Dienst
kann es. Damit braucht der Weg keine Rechteaenderung, kein root und
keinen Neustart.
DAS GEHEIMNIS liegt in /home/dogiweb/workspace-daten/discord-webhook.txt
mit 600, wird bei JEDEM Aufruf gelesen (ein Austausch wirkt sofort) und
kommt nie wieder heraus: GET beantwortet nur "ist eingerichtet" und
"seit wann", das Protokoll nennt sie nicht, und die Teilen-Antwort
auch nicht.
DIE SCHRANKEN, jede einzeln gemessen:
- nur discord.com/discordapp.com/ptb./canary., nur https, nur Wege
unter /api/webhooks/ -- sonst waere das Feld ein Weg, den Server zu
beliebigen Zielen sprechen zu lassen
- nur die Leitung, nur die Agentur-Adresse; von crew.* aus 404 statt
403, damit man nicht erahnen kann, was es woanders gibt
- allowed_mentions leer: im Beitrag steht TikToks Text, und ein
"@everyone" darin wuerde sonst den ganzen Server anpingen
- keine flags: die Vorschau bleibt an, denn genau sie ist das Banner
- ohne Teilen-Link keine Nachricht
- zwoelf je Stunde und Person, gezaehlt im Protokoll (Erfolge UND
Fehlschlaege -- sonst bremst sie den nicht, der in einer Schleife
haengt; derselbe Fehler wie am 07.10. an der Kampagnen-Bremse)
SIEBEN NEUE KENNUNGEN, sieben Saetze in meldung.js -- verlangt hat sie
`pruef-meldungen`, nicht mein Gedaechtnis. Jeder Satz sagt auch, was
stattdessen geht: Der Beitrag liegt in jedem Fall zum Kopieren da.
ZWEI FEHLER FANDEN NUR DIE PRUEFUNGEN:
- teilenRouter.use("/workspace/api/eintrag", angemeldet) deckte die
neuen Kanal-Wege nicht ab. Ohne req.person ist istLeitung falsch,
also antworteten sie JEDEM mit 403 "nicht_erlaubt", auch
DogFather. Es ist ZU gescheitert, nicht auf -- aber die Auskunft
schickte in die falsche Richtung.
- Das Datum im Dateikopf kam aus UTC (pruef-struktur): zwischen 00:00
und 02:00 stuende dort der Vortag, in einer Zeile, die spaeter
jemand liest, um zu wissen, seit wann der Kanal gilt.
Geprueft: pruef-kampagne 190 -> 226 (Abschnitte 12c und 12d), mit einer
ehrlichen Luecke -- die Dateirechte lassen sich auf Windows nicht
messen, auf dem Server schon; die Schlusszeile sagt jetzt "IN ORDNUNG,
mit Luecke" statt "ALLES IN ORDNUNG". bild-kampagne 53 -> 83
(Kanalfeld vermessen und abgebildet, Eingabefeld 45/46 px auf beiden
Breiten). Dazu gruen: meldungen 10, struktur, css-klassen, zeichen,
deutsche-texte, alle-wege, schranke, ports, portnummern, tippziele,
lesbarkeit, agentur 67, eventkarte 190, workspace-seiten 37,
haus-trennung 107, formulare 23, fingermass.
SECHS GEGENPROBEN am Weg in den Kanal, alle sechs schlagen an:
Rechnerliste uebergangen, Erwaehnungen wieder erlaubt, Vorschau
unterdrueckt, Bremse aus, ohne Teilen-Link gesendet, Adresse in die
Antwort gelegt.
NEBENBEI, UND ES WAR MEIN EIGENER REST: pruef-kampagne-lesen war seit
698c23e2 rot, also seit heute Vormittag. Mit dem Bildleser kamen drei
Pruefdaten in den Ordner, und die Wache dort verlangt zu Recht, dass
nichts Unerklaertes liegenbleibt. Ich hatte sie VOR dem Commit
gemessen und danach nicht noch einmal -- ein Pruefstand von vorher ist
keiner. Die drei sind jetzt benannt, mit Grund, und eine Gegenwache
prueft, dass es jede noch gibt. 186 -> 187, zwei Gegenproben: eine
unbekannte Datei faellt auf, ein toter Eintrag in der Liste auch.
1226 lines
59 KiB
JavaScript
1226 lines
59 KiB
JavaScript
/* =====================================================================
|
|
EINE KAMPAGNENSEITE LESEN — gegen GESPEICHERTE Seiten (07.10.2026)
|
|
|
|
Etappe 1 des Bauplans „Kampagnen-Kachel aus TikTok-Link“. Geprüft
|
|
wird `server/kampagne-lesen.mjs`, und zwar an vier echten Seiten,
|
|
die am 07.10.2026 von TikTok geholt wurden und unter
|
|
`server/pruefdaten/` liegen. Woher sie kommen und was an einer von
|
|
ihnen verändert wurde, steht dort in `LIESMICH.md`.
|
|
|
|
KEIN SERVER, KEIN PORT, KEIN NETZ, KEINE DATENBANK. Diese Prüfung
|
|
startet nichts und fragt niemanden. Sie liest vier Dateien und
|
|
rechnet. Deshalb läuft sie in Sekunden statt in Minuten, und
|
|
deshalb kann sie nicht rot werden, weil TikTok einen schlechten
|
|
Nachmittag hat.
|
|
|
|
---------------------------------------------------------------------
|
|
DER DRITTE AUSGANG
|
|
|
|
Fehlt eine der Prüfdateien oder lässt sie sich nicht entpacken, dann
|
|
ist diese Prüfung nicht DURCHFÜHRBAR — und das muss SCHEITERN, nicht
|
|
schweigen (CLAUDE.md, 03.09.2026). Sie endet dann mit Rückgabewert 2
|
|
und den Worten KONNTE NICHT NACHSEHEN, mit Grund und Anleitung.
|
|
|
|
Ein übersprungener Abschnitt wäre das Schlimmste von allem: Die Zahl
|
|
der Prüfungen sänke still, der Lauf bliebe grün, und ab da wäre das
|
|
Auslesen dauerhaft ungeprüft.
|
|
|
|
---------------------------------------------------------------------
|
|
DIE PRÜFDATEIEN SIND MIT IHRER PRÜFSUMME FESTGENAGELT
|
|
|
|
Eine Prüfung, deren Eingabe sich ändern kann, beweist nichts. Hier
|
|
steht deshalb die SHA-256-Summe des ENTPACKTEN Inhalts. Wird eine
|
|
Datei bewusst erneuert, wird diese Prüfung rot und nennt die neue
|
|
Summe — dann ist die Summe hier nachzutragen, mit einem Satz dazu,
|
|
WARUM die Datei neu ist.
|
|
|
|
Aufrufen mit: node server/pruef-kampagne-lesen.mjs
|
|
===================================================================== */
|
|
import { readFileSync, existsSync, mkdtempSync, rmSync, readdirSync } from "node:fs";
|
|
import { brotliDecompressSync } from "node:zlib";
|
|
import { createHash } from "node:crypto";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { tmpdir } from "node:os";
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
|
|
/* Auch eine Prüfung ohne Netz braucht die Notbremse: `index.js` hängt
|
|
globale Auffangnetze ein, und ein Absturz würde sonst nur notiert.
|
|
Eine Prüfung, die HÄNGT, ist schlimmer als eine, die scheitert. */
|
|
notbremse(60_000, "pruef-kampagne-lesen");
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
const DATEN = join(HIER, "pruefdaten");
|
|
|
|
let fehler = 0;
|
|
let geprueft = 0;
|
|
const melde = (t) => console.log(t);
|
|
const ok = (b, t) => {
|
|
geprueft++;
|
|
console.log((b ? " ok " : " FEHL ") + t);
|
|
if (!b) fehler++;
|
|
};
|
|
|
|
/** Der dritte Ausgang: nicht „in Ordnung“, nicht „nicht in Ordnung“,
|
|
* sondern „konnte nicht nachsehen“ — mit Grund und Anleitung. */
|
|
function nichtNachsehbar(grund, rat) {
|
|
console.log("");
|
|
console.log("KONNTE NICHT NACHSEHEN");
|
|
console.log(" Grund: " + grund);
|
|
console.log(" Was tun: " + rat);
|
|
console.log("");
|
|
console.log(" Das ist KEIN Befund — es heißt nur, dass gerade niemand");
|
|
console.log(" nachsehen konnte. Es ist aber auch kein Erfolg.");
|
|
process.exit(2);
|
|
}
|
|
|
|
/* =====================================================================
|
|
0. DIE VORAUSSETZUNG
|
|
===================================================================== */
|
|
melde("=== 0. Sind die gespeicherten Seiten da und unverändert? ===");
|
|
|
|
/* Die Summen stammen vom 07.10.2026, dem Tag, an dem die Seiten geholt
|
|
wurden. Erste 16 Stellen der SHA-256 des entpackten Inhalts. */
|
|
const SEITEN = {
|
|
gipfel: {
|
|
datei: "kampagne-gipfelstuermer.html.br",
|
|
sha: "f8486096ff4e1b96", gross: 1_109_700,
|
|
was: "Gipfelstürmer, läuft, unverändert",
|
|
},
|
|
beendet: {
|
|
datei: "kampagne-beendet.html.br",
|
|
sha: "e068e2c9bcfc37de", gross: 961_831,
|
|
was: "Sonne im Gepäck, beendet, unverändert",
|
|
},
|
|
ohnePreise: {
|
|
datei: "kampagne-ohne-preise.html.br",
|
|
sha: "28e6a24ff3f6b726", gross: 986_560,
|
|
was: "Stern Stunde, Preisbaustein entfernt",
|
|
},
|
|
aufgabengruppe: {
|
|
datei: "kampagne-aufgabengruppe.html.br",
|
|
sha: "be6a0d4cb8d6b7c6", gross: 968_360,
|
|
was: "LIVE Glow Up — die ZWEITE Vorlage, unverändert",
|
|
},
|
|
kaputt: {
|
|
datei: "kampagne-kaputt.html", packt: false,
|
|
sha: "cf40f347d49d3e03", gross: 369,
|
|
was: "Seite ohne Datenpaket, selbst geschrieben",
|
|
},
|
|
};
|
|
|
|
const seite = {};
|
|
for (const [name, s] of Object.entries(SEITEN)) {
|
|
const pfad = join(DATEN, s.datei);
|
|
if (!existsSync(pfad)) {
|
|
nichtNachsehbar(
|
|
`die Prüfdatei ${s.datei} fehlt in ${DATEN}`,
|
|
"Sie gehört ins Verzeichnis und ist in Gitea versioniert — "
|
|
+ "`git status server/pruefdaten/` zeigt, ob sie nur lokal fehlt.");
|
|
}
|
|
let roh;
|
|
try {
|
|
roh = s.packt === false
|
|
? readFileSync(pfad)
|
|
: brotliDecompressSync(readFileSync(pfad));
|
|
} catch (f) {
|
|
nichtNachsehbar(
|
|
`${s.datei} lässt sich nicht entpacken (${f.message})`,
|
|
"Die Datei ist beschädigt. Aus Gitea neu holen: "
|
|
+ `git checkout -- server/pruefdaten/${s.datei}`);
|
|
}
|
|
const sha = createHash("sha256").update(roh).digest("hex").slice(0, 16);
|
|
ok(roh.length === s.gross && sha === s.sha,
|
|
`${s.datei}: ${roh.length} Bytes, sha ${sha} — ${s.was}`
|
|
+ (sha === s.sha ? "" : ` (ERWARTET ${s.sha}/${s.gross})`));
|
|
seite[name] = roh.toString("utf8");
|
|
}
|
|
|
|
/* Und: es liegt nichts Unerklärtes dort. Eine Prüfdatei, die niemand
|
|
benutzt, ist entweder ein Rest oder eine vergessene Absicht. */
|
|
{
|
|
/* WAS SONST NOCH DORT LIEGEN DARF -- benannt, mit Grund.
|
|
|
|
Diese drei gehören nicht zu `SEITEN`: Das sind die TikTok-Seiten,
|
|
die `leseKampagne` liest. Hier geht es um den Bildleser, der eine
|
|
ganz andere Eingabe hat -- den erkannten Text eines
|
|
Bildschirmfotos. Gelesen werden sie von `pruef-kampagne-bild.mjs`.
|
|
|
|
SIE WURDEN AM 08.10.2026 MIT 698c23e2 ANGELEGT, und von da an war
|
|
diese Prüfung rot: Ich hatte sie vor dem Commit gemessen und
|
|
danach nicht noch einmal. Ein Prüfstand von vorher ist keiner. */
|
|
const AUCH_ERLAUBT = new Map([
|
|
["bild-agentur-nachbau.png", "das nachgebaute Backstage-Bild -- Eingabe der Texterkennung"],
|
|
["bild-agentur-nachbau.html", "woraus es entsteht, damit es sich nachbauen laesst"],
|
|
["bild-agentur-erkannt.txt", "was Tesseract daraus liest -- die Eingabe von leseBild"],
|
|
]);
|
|
|
|
const erwartet = new Set([...Object.values(SEITEN).map((s) => s.datei), "LIESMICH.md",
|
|
...AUCH_ERLAUBT.keys()]);
|
|
const gefunden = readdirSync(DATEN);
|
|
const fremd = gefunden.filter((n) => !erwartet.has(n));
|
|
|
|
/* GEGENWACHE: Jede benannte Datei muss es noch geben. Sonst bleibt
|
|
ein Freibrief stehen, nachdem die Stelle aufgeräumt ist -- und
|
|
deckt beim nächsten Mal etwas ab, das niemand entschieden hat. */
|
|
const toteNamen = [...AUCH_ERLAUBT.keys()].filter((n) => !gefunden.includes(n));
|
|
ok(AUCH_ERLAUBT.size > 0 && toteNamen.length === 0,
|
|
`jede benannte Zusatzdatei gibt es noch (${AUCH_ERLAUBT.size})`
|
|
+ (toteNamen.length ? ` -- fehlen: ${toteNamen.join(", ")}` : ""));
|
|
ok(gefunden.length === erwartet.size && fremd.length === 0,
|
|
`${gefunden.length} Dateien im Ordner, alle erklärt`
|
|
+ (fremd.length ? ` — unerklärt: ${fremd.join(", ")}` : ""));
|
|
}
|
|
|
|
const {
|
|
leseKampagne, regelnText, aufgabenText, tagInZone, textAusHtml,
|
|
KampagneUnlesbar, datenAusSeite, gliederung, deutschesDatum, zeitOderNull,
|
|
geruestVon, bausteine,
|
|
} = await import("./kampagne-lesen.mjs");
|
|
|
|
/* =====================================================================
|
|
1. DER KALENDERTAG — Fallstrick 1 und 2 des Bauplans
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 1. Ein Tag in der Zone der Kampagne, nicht in UTC ===");
|
|
|
|
const START_GIPFEL = 1_791_064_800_000; /* so steht es in der Seite */
|
|
|
|
ok(tagInZone(START_GIPFEL, "Europe/Berlin") === "2026-10-04",
|
|
`Beginn von Gipfelstürmer: ${tagInZone(START_GIPFEL, "Europe/Berlin")}`);
|
|
|
|
/* GEGENPROBE: Der bequeme Weg liefert den VORTAG. Steht hier, damit
|
|
niemand die Zeile oben für Zierde hält. */
|
|
const naiv = new Date(START_GIPFEL).toISOString().slice(0, 10);
|
|
ok(naiv === "2026-10-03" && naiv !== tagInZone(START_GIPFEL, "Europe/Berlin"),
|
|
`Gegenprobe: toISOString() sagt ${naiv} — einen Tag zu früh`);
|
|
|
|
ok(tagInZone(START_GIPFEL, "UTC") === "2026-10-03",
|
|
"die Zone entscheidet wirklich (UTC ergibt den 03.)");
|
|
|
|
/* WINTERZEIT: Der in der Seite gespeicherte Versatz „+02:00“ gilt nur
|
|
bis zum 25.10.2026. Eine Kampagne im November, damit gerechnet,
|
|
liegt über Mitternacht um einen TAG daneben. */
|
|
{
|
|
const spaet = Date.parse("2026-11-15T23:30:00+01:00"); /* Berliner Winterzeit */
|
|
const mitZone = tagInZone(spaet, "Europe/Berlin");
|
|
const mitVersatz = new Date(spaet + 2 * 3600_000).toISOString().slice(0, 10);
|
|
ok(mitZone === "2026-11-15",
|
|
`im November rechnet die Zone richtig: ${mitZone}`);
|
|
ok(mitVersatz === "2026-11-16" && mitVersatz !== mitZone,
|
|
`Gegenprobe: mit dem gespeicherten +02:00 wäre es der ${mitVersatz}`);
|
|
}
|
|
|
|
ok(tagInZone(START_GIPFEL, "Gibt/EsNicht") === null,
|
|
"eine unbekannte Zone ergibt NICHTS — kein stilles Ausweichen auf UTC");
|
|
ok(tagInZone("keine Zahl", "Europe/Berlin") === null,
|
|
"und ein Nicht-Zeitpunkt ebenfalls nichts");
|
|
|
|
/* =====================================================================
|
|
2. DIE VOLLSTÄNDIGE SEITE
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 2. Gipfelstürmer — alles gelesen ===");
|
|
|
|
const g = leseKampagne(seite.gipfel);
|
|
|
|
ok(g.pflicht.titel === "Gipfelstürmer", `Titel: ${g.pflicht.titel}`);
|
|
ok(g.pflicht.id === "7691493175936699143", `Nummer: ${g.pflicht.id}`);
|
|
ok(g.pflicht.zone === "Europe/Berlin", `Zone: ${g.pflicht.zone}`);
|
|
ok(g.pflicht.start === "2026-10-04", `Beginn: ${g.pflicht.start}`);
|
|
ok(g.pflicht.ende === "2026-10-07", `Ende: ${g.pflicht.ende}`);
|
|
|
|
/* Die Zahl gehört in die BEDINGUNG, nicht nur in den Meldetext. */
|
|
ok(g.hinweise.length === 0,
|
|
`keine Lücke zu melden (${g.hinweise.length} Hinweise)`
|
|
+ (g.hinweise.length ? ": " + g.hinweise.join(" | ") : ""));
|
|
|
|
ok(g.auszug.gelesen_aus === "activityInfo",
|
|
`Gerüst kam aus ${g.auszug.gelesen_aus}`);
|
|
ok(g.auszug.woerterbuch_eintraege === 135,
|
|
`Wörterbuch: ${g.auszug.woerterbuch_eintraege} Einträge`);
|
|
ok(g.auszug.platzhalter_ohne_eintrag === 0,
|
|
`jeder Platzhalter im Gerüst hatte seinen Text (${g.auszug.platzhalter_ohne_eintrag} ohne)`);
|
|
ok(g.auszug.bausteine.length === 13 && g.auszug.bausteine.includes("live_rule_introduction"),
|
|
`${g.auszug.bausteine.length} Bausteine am Namen erkannt`);
|
|
|
|
/* ---- Die Aufgaben, wörtlich -------------------------------------- */
|
|
const ERWARTETE_AUFGABEN = [
|
|
"1 Diamant = 1 Punkt",
|
|
"Dreifache Punkte für Kampagnengeschenke",
|
|
"Tägliche \"Gipfelzeit\" (Bonuszeit) von 20-22 Uhr",
|
|
"Gehe täglich mindestens 25 min LIVE um 10.000 Extrapunkte zu erhalten",
|
|
"Gewinn ein neues Fanclub Mitglied für 100 Extrapunkte",
|
|
];
|
|
ok(g.kuer.aufgaben.length === 5
|
|
&& ERWARTETE_AUFGABEN.every((t, i) => g.kuer.aufgaben[i] === t),
|
|
`5 Aufgaben, wörtlich wie auf der Seite`);
|
|
ok(g.kuer.aufgabenTitel === "Wie gewinne ich?",
|
|
`ihre Überschrift kommt von der Seite: „${g.kuer.aufgabenTitel}“`);
|
|
|
|
/* Kein Aufzählungszeichen davor: `punkteAusText` würde es wieder
|
|
abschneiden, aber es stünde im Speichertext, und DogFather fände
|
|
beim Bearbeiten etwas vor, das er nie getippt hat. */
|
|
const AUFZAEHLZEICHEN = /^[\s]*[-–—•·*▪◦‣>+]/u;
|
|
ok(g.kuer.aufgaben.length > 0 && !g.kuer.aufgaben.some((z) => AUFZAEHLZEICHEN.test(z)),
|
|
"keine Zeile beginnt mit einem Aufzählungszeichen");
|
|
|
|
/* ---- Die Geschenke ----------------------------------------------- */
|
|
ok(g.kuer.geschenke.length === 5, `5 Kampagnengeschenke`);
|
|
ok(g.kuer.geschenke[0].name === "Herbstliches Herz"
|
|
&& g.kuer.geschenke[0].preis === 1
|
|
&& g.kuer.geschenke[0].id === "12877",
|
|
`das erste: ${g.kuer.geschenke[0].name}, ${g.kuer.geschenke[0].preis} Coin, #${g.kuer.geschenke[0].id}`);
|
|
ok(g.kuer.geschenke[4].name === "Ahorn-Welt" && g.kuer.geschenke[4].preis === 12000,
|
|
`das teuerste: ${g.kuer.geschenke[4].name}, ${g.kuer.geschenke[4].preis} Coins`);
|
|
|
|
/* ---- Die Preise -------------------------------------------------- */
|
|
ok(g.kuer.preise.length === 6, `6 Preisgruppen`);
|
|
ok(g.kuer.preise[0].liga === "Superstars"
|
|
&& g.kuer.preise[0].plaetze[0].platz === "Top 1"
|
|
&& g.kuer.preise[0].plaetze[0].preise[0] === "60.000 Diamanten",
|
|
`${g.kuer.preise[0].liga} / ${g.kuer.preise[0].plaetze[0].platz}`
|
|
+ ` = ${g.kuer.preise[0].plaetze[0].preise[0]}`);
|
|
{
|
|
const plaetze = g.kuer.preise.reduce((s, p) => s + p.plaetze.length, 0);
|
|
/* 41, NACHGERECHNET: Superstars 3 + 1. Liga 6 + 2. bis 5. Liga je 8.
|
|
Hier stand zuerst 46 -- geraten, nicht gezaehlt. Die Prueferin war
|
|
richtig, die Erwartung falsch. */
|
|
ok(plaetze === 41, `${plaetze} Plätze über alle Ligen`);
|
|
}
|
|
|
|
/* ---- Länder und Banner ------------------------------------------- */
|
|
ok(g.kuer.laender.join(",") === "DE,CH,AT,LI,LU", `Länder: ${g.kuer.laender.join(", ")}`);
|
|
|
|
/* DAS BANNER IST NICHT DAS VORLAGENBILD. `pageInfo.topImageInfo.url`
|
|
zeigt bei Gipfelstürmer auf einen goldenen Löwen aus einer
|
|
Nahost-Kampagne vom August 2024 — sichtbar ist er nirgends. */
|
|
{
|
|
const wert = datenAusSeite(seite.gipfel);
|
|
const vorlage = wert.globalData.pageInfo.topImageInfo?.url;
|
|
ok(typeof g.kuer.bannerUrl === "string" && g.kuer.bannerUrl.startsWith("https://"),
|
|
`Banner: ${g.kuer.bannerUrl.slice(0, 72)}…`);
|
|
ok(!!vorlage && g.kuer.bannerUrl !== vorlage,
|
|
"und es ist NICHT pageInfo.topImageInfo.url (das Vorlagenbild)");
|
|
ok(wert.globalData.pageInfo.pageVisualEnName === "MENA Black Gold PK",
|
|
`Gegenprobe: die Vorlage heißt „${wert.globalData.pageInfo.pageVisualEnName}“`
|
|
+ " — genau das stünde sonst in der Kachel");
|
|
}
|
|
|
|
/* =====================================================================
|
|
3. DER WICHTIGSTE NACHWEIS — das Schema, nicht das Wörterbuch
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 3. „1 schenkende Person“ darf NICHT in der Kachel stehen ===");
|
|
|
|
{
|
|
const wert = datenAusSeite(seite.gipfel);
|
|
const woerterbuch = wert.sourceTextMap;
|
|
const imWoerterbuch = Object.values(woerterbuch)
|
|
.filter((t) => typeof t === "string" && t.includes("schenkende Person"));
|
|
|
|
/* GEGENPROBE ZUERST: Wer das Wörterbuch durchliest, FINDET den Satz.
|
|
Ohne diese Zeile wäre die nächste nur die Feststellung, dass ein
|
|
Wort irgendwo nicht vorkommt — und das wäre auch wahr, wenn der
|
|
ganze Leser nichts täte. */
|
|
ok(imWoerterbuch.length === 1,
|
|
`Gegenprobe: im Wörterbuch steht er sehr wohl — „${imWoerterbuch[0]?.trim()}“`);
|
|
|
|
/* Und er steht an KEINER Stelle im Seitengerüst. */
|
|
const geruest = JSON.parse(wert.globalData.activityInfo.ac_schema_with_interaction_rules);
|
|
const kopie = { ...geruest };
|
|
delete kopie.sourceTextMap;
|
|
const imGeruest = new Set(JSON.stringify(kopie).match(/\$\$__activity_[^"]*?__\$\$/g) || []);
|
|
const nurImWoerterbuch = Object.keys(woerterbuch).filter((k) => !imGeruest.has(k));
|
|
ok(imGeruest.size === 134 && Object.keys(woerterbuch).length === 135
|
|
&& nurImWoerterbuch.length === 1
|
|
&& woerterbuch[nurImWoerterbuch[0]].includes("schenkende Person"),
|
|
`${imGeruest.size} Platzhalter im Gerüst, ${Object.keys(woerterbuch).length} im Wörterbuch`
|
|
+ ` — der eine Überzählige ist genau dieser Satz`);
|
|
|
|
/* Und nun das Ergebnis: nirgends in der Kachel. */
|
|
const allesLesbare = JSON.stringify(g.kuer) + "\n" + regelnText(g.kuer, g.hinweise)
|
|
+ "\n" + aufgabenText(g.kuer) + "\n" + JSON.stringify(g.pflicht);
|
|
ok(allesLesbare.length > 1000 && !allesLesbare.includes("schenkende"),
|
|
`in ${allesLesbare.length} Zeichen Kachelinhalt kommt er nicht vor`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
4. DIE TEXTE FÜR DIE KACHEL
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 4. Was in event_aufgaben und event_regeln landet ===");
|
|
|
|
{
|
|
const auf = aufgabenText(g.kuer);
|
|
const zeilen = auf.split("\n").filter((z) => z.trim());
|
|
ok(zeilen.length === 5 && auf === ERWARTETE_AUFGABEN.join("\n"),
|
|
`event_aufgaben: ${zeilen.length} Zeilen, eine je Haken`);
|
|
|
|
/* MIT DER ECHTEN ZERLEGUNG DES HAUSES, nicht mit einer Abschrift.
|
|
Dafür muss `workspace-bereiche.js` eingeführt werden, und das
|
|
braucht einen Datenbankpfad — deshalb ein Wegwerfordner. Es wird
|
|
nichts geschrieben; dass dort hinterher keine Datenbank liegt,
|
|
wird gleich nachgewiesen. */
|
|
const wegwerf = mkdtempSync(join(tmpdir(), "ws-kampagne-lesen-"));
|
|
process.env.WORKSPACE_DB = join(wegwerf, "workspace.db");
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
const { punkteAusText } = await import("./workspace-bereiche.js");
|
|
|
|
const haken = punkteAusText(auf);
|
|
ok(haken.length === 5 && haken.every((h, i) => h.text === ERWARTETE_AUFGABEN[i]),
|
|
`punkteAusText macht daraus ${haken.length} Haken, wörtlich gleich`);
|
|
|
|
/* GEGENPROBE: eine Zeile weniger ergibt einen Haken weniger. Sonst
|
|
bewiese die Zeile darüber nur, dass irgendeine Zahl 5 ist. */
|
|
const weniger = punkteAusText(ERWARTETE_AUFGABEN.slice(0, 4).join("\n"));
|
|
ok(weniger.length === 4, `Gegenprobe: vier Zeilen ergeben ${weniger.length} Haken`);
|
|
|
|
/* GEGENPROBE: ein vorangestelltes Aufzählungszeichen würde
|
|
abgeschnitten — deshalb setzt der Leser keines. */
|
|
const mitPunkt = punkteAusText("· " + ERWARTETE_AUFGABEN[0]);
|
|
ok(mitPunkt.length === 1 && mitPunkt[0].text === ERWARTETE_AUFGABEN[0],
|
|
"Gegenprobe: ein Aufzaehlungszeichen davor wuerde beim Anzeigen"
|
|
+ " verschwinden -- deshalb setzt der Leser keines");
|
|
|
|
const db = join(wegwerf, "workspace.db");
|
|
ok(!existsSync(db),
|
|
"und es ist dabei KEINE Datenbank entstanden — Etappe 1 schreibt nichts");
|
|
try { rmSync(wegwerf, { recursive: true, force: true }); } catch { /* Rest */ }
|
|
}
|
|
|
|
{
|
|
const r = regelnText(g.kuer, g.hinweise);
|
|
ok(r.length > 3000, `event_regeln: ${r.length} Zeichen`);
|
|
|
|
/* Die Reihenfolge ist fest — Geschenke, Preise, Klapptexte,
|
|
Freitext, Teilnahme. Nicht die Reihenfolge, in der etwas gelesen
|
|
wurde, sondern die, in der es jemand braucht. */
|
|
/* „TEILNAHME" steht auch am Anfang von „TEILNAHMEBEDINGUNGEN" --
|
|
`indexOf` faende dann zweimal dieselbe Stelle und die Reihenfolge
|
|
waere nie aufsteigend. Der letzte Abschnitt wird deshalb an seiner
|
|
eigenen Zeile erkannt, nicht an seinem Anfang. */
|
|
const reihe = ["KAMPAGNENGESCHENKE", "BELOHNUNGEN", "WELCHE LIGA BIN ICH?",
|
|
"TEILNAHMEBEDINGUNGEN", "TEILNAHME\nLänder:"];
|
|
const stellen = reihe.map((w) => r.indexOf(w));
|
|
ok(stellen.every((s) => s >= 0)
|
|
&& stellen.every((s, i) => i === 0 || s > stellen[i - 1]),
|
|
`alle fünf Abschnitte da und in dieser Reihenfolge: ${reihe.join(" → ")}`);
|
|
|
|
ok(r.includes("· Herbstliches Herz — 1 Coin\n"),
|
|
"Einzahl bei genau einem Coin („1 Coin“, nicht „1 Coins“)");
|
|
ok(r.includes("Ahorn-Welt — 12000 Coins"), "und Mehrzahl darüber");
|
|
ok(r.includes("Superstars: Top 1 = 60.000 Diamanten · Top 2 = 55.000 Diamanten"),
|
|
"die Preise stehen in einer Zeile je Liga");
|
|
ok(r.includes("Länder: DE, CH, AT, LI, LU"), "die Länder stehen drin");
|
|
|
|
/* DIE LIGANUMMER STECKT IM MERKMAL, NICHT IM TEXT. Auf der Seite
|
|
steht `<ol start="2"><li><strong>Liga</strong></li></ol>`. Wer nur
|
|
die Marken wegwirft, bekommt viermal das Wort „Liga“. */
|
|
ok(r.includes("1. Liga") && r.includes("2. Liga") && r.includes("5. Liga"),
|
|
"die Ligen sind durchnummeriert (1. bis 5.)");
|
|
{
|
|
/* `### ` DAVOR SEIT DEM 07.10.2026 -- und das ist der Zweck.
|
|
|
|
Hier stand `=== "3. Liga"`. Auf der Seite ist ein
|
|
Listenpunkt, dessen GANZER Inhalt hervorgehoben ist, eine
|
|
Überschrift mit Nummer: Darunter steht jeweils die Bedingung
|
|
für diese Liga. Flach ausgegeben standen zwölf
|
|
gleichrangige Zeilen da, und die Frage „ab wie vielen
|
|
Diamanten bin ich in der 2. Liga" war daran nicht mehr zu
|
|
beantworten -- obwohl die Antwort danebenstand.
|
|
|
|
DIE NUMMER BLEIBT DABEI (`<ol start>`), und das
|
|
` ` hinter `</strong>` darf nicht stören -- die fünfte
|
|
Liga hat eines, die ersten vier nicht. */
|
|
const rohHtml = "<ol start=\"3\"><li><strong>Liga </strong></li></ol>";
|
|
ok(textAusHtml(rohHtml) === "### 3. Liga",
|
|
`Gegenprobe am Baustein selbst: ${JSON.stringify(textAusHtml(rohHtml))}`);
|
|
const mitNbsp = "<ol start=\"5\"><li><strong>Liga </strong> </li></ol>";
|
|
ok(textAusHtml(mitNbsp) === "### 5. Liga",
|
|
`auch mit geschütztem Leerzeichen dahinter: ${JSON.stringify(textAusHtml(mitNbsp))}`);
|
|
/* GEGENPROBE: Ein Listenpunkt mit gewöhnlichem Text wird KEINE
|
|
Überschrift. Ohne sie wäre unbelegt, dass die Hervorhebung
|
|
entscheidet und nicht einfach jede Zeile eine Überschrift
|
|
wird. */
|
|
const schlicht = "<ol start=\"2\"><li>Nur Text, nicht fett</li></ol>";
|
|
ok(textAusHtml(schlicht) === "2. Nur Text, nicht fett",
|
|
`ein gewöhnlicher Punkt bleibt ein Punkt: ${JSON.stringify(textAusHtml(schlicht))}`);
|
|
/* Und ein Absatz, dessen ganzer Inhalt hervorgehoben ist,
|
|
ebenfalls -- so stehen die Ligennamen auf der Seite. */
|
|
const absatz = "<p style=\"text-align: center;\"><strong>Superstars</strong></p>"
|
|
+ "<p>Du kannst teilnehmen, wenn ...</p>";
|
|
/* MIT LEERZEILE DAZWISCHEN -- so soll es sein. Die
|
|
Überschrift bekommt ihren eigenen Umbruch, der Absatz
|
|
seinen; `gliederung()` wirft leere Zeilen ohnehin weg, und
|
|
im Rohtext, den DogFather beim Bearbeiten sieht, liest sich
|
|
das sauber. Meine Erwartung von EINEM Umbruch war
|
|
geraten. */
|
|
ok(textAusHtml(absatz) === "### Superstars\n\nDu kannst teilnehmen, wenn ...",
|
|
`ein Absatz nur aus Hervorhebung wird Überschrift: ${JSON.stringify(textAusHtml(absatz))}`);
|
|
/* GEGENPROBE dazu: Eine Frage MIT hervorgehobenem Wort ist auch
|
|
der Sache nach eine Überschrift -- aber das zu behaupten wäre
|
|
geraten. Sie bleibt ein Absatz. */
|
|
const gemischt = "<p>Um was geht es bei <strong>Gipfelstürmer?</strong></p>";
|
|
ok(textAusHtml(gemischt) === "Um was geht es bei Gipfelstürmer?",
|
|
`eine Frage mit fettem Wort bleibt ein Absatz: ${JSON.stringify(textAusHtml(gemischt))}`);
|
|
ok(rohHtml.replace(/<[^>]+>/g, "").trim() === "Liga ",
|
|
"Gegenprobe: bloßes Markenentfernen verliert die Nummer");
|
|
}
|
|
|
|
ok(!/<[a-z/][^>]*>/i.test(r), "keine einzige HTML-Marke ist übrig");
|
|
ok(!r.includes(" ") && !r.includes("&"),
|
|
"und keine unaufgelöste Zeichenfolge");
|
|
ok(!r.includes("$$__activity_"), "und kein unaufgelöster Platzhalter");
|
|
}
|
|
|
|
/* =====================================================================
|
|
5. TEXT AUS HTML, einzeln
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 5. Der HTML-zu-Text-Wandler für sich ===");
|
|
|
|
ok(textAusHtml("<p>eins</p><p>zwei</p>") === "eins\n\nzwei", "Absätze trennen");
|
|
ok(textAusHtml("a<br>b") === "a\nb", "ein Umbruch bricht um");
|
|
ok(textAusHtml("<ul><li>x</li><li>y</li></ul>") === "· x\n· y", "Punkte bekommen ihr Zeichen");
|
|
ok(textAusHtml("<ol start=\"7\"><li>a</li><li>b</li></ol>") === "7. a\n8. b",
|
|
"nummerierte Listen zählen von `start` weiter");
|
|
ok(textAusHtml("Tom & Jerry <3 €") === "Tom & Jerry <3 €",
|
|
"Zeichenfolgen werden zurückübersetzt");
|
|
ok(textAusHtml("<script>boese()</script><p>gut</p>") === "gut", "Skripte fallen weg");
|
|
ok(textAusHtml("") === "" && textAusHtml(null) === "" && textAusHtml(undefined) === "",
|
|
"nichts hinein ergibt nichts heraus");
|
|
ok(textAusHtml("<p>a</p>\n\n\n\n<p>b</p>") === "a\n\nb",
|
|
"mehr als eine Leerzeile wird eine");
|
|
|
|
/* =====================================================================
|
|
6. DIE BEENDETE KAMPAGNE — der Fall, den der Bauplan nicht kannte
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 6. Eine abgelaufene Kampagne hat kein Interaktionsschema ===");
|
|
|
|
{
|
|
const wert = datenAusSeite(seite.beendet);
|
|
/* GEGENPROBE/VORAUSSETZUNG: Das Feld ist wirklich weg. Sonst würde
|
|
der Rückfallweg gar nicht geprüft, und die Zeilen danach wären
|
|
grün, ohne etwas zu messen. */
|
|
ok(!("ac_schema_with_interaction_rules" in wert.globalData.activityInfo),
|
|
"activityInfo.ac_schema_with_interaction_rules fehlt in dieser Seite");
|
|
ok(wert.globalData.activityInfo.activity_status === 16,
|
|
`ihr Zustand ist „beendet“ (${wert.globalData.activityInfo.activity_status})`);
|
|
ok(!!wert.schema, "aber value.schema ist da");
|
|
|
|
const b = leseKampagne(seite.beendet);
|
|
ok(b.auszug.gelesen_aus === "value.schema",
|
|
`gelesen wurde aus ${b.auszug.gelesen_aus}`);
|
|
ok(b.pflicht.titel === "Sonne im Gepäck", `Titel: ${b.pflicht.titel}`);
|
|
ok(b.pflicht.id === "7657456323453405959", `Nummer: ${b.pflicht.id}`);
|
|
ok(b.pflicht.start === "2026-07-03" && b.pflicht.ende === "2026-07-07",
|
|
`Zeitraum: ${b.pflicht.start} bis ${b.pflicht.ende}`);
|
|
ok(b.hinweise.length === 0,
|
|
`und vollständig gelesen (${b.hinweise.length} Hinweise)`);
|
|
|
|
/* Die Bausteine heißen dort `…_rep_remove`. Dass der Name trotzdem
|
|
erkannt wird, ist die eigentliche Leistung. */
|
|
ok(JSON.stringify(wert.schema).includes("_rep_remove"),
|
|
"ihre Bausteinnamen tragen die Endung _rep_remove");
|
|
ok(b.auszug.bausteine.length === 10
|
|
&& b.auszug.bausteine.every((n) => !n.endsWith("_rep_remove")),
|
|
`${b.auszug.bausteine.length} Bausteine erkannt, Endung abgeschnitten`);
|
|
ok(b.kuer.aufgaben.length > 0 && b.kuer.preise.length > 0 && !!b.kuer.bannerUrl,
|
|
`Aufgaben (${b.kuer.aufgaben.length}), Preise (${b.kuer.preise.length}) und Banner da`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
7. OHNE PREISE — 201 mit Hinweis, nicht 502
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 7. Eine Kampagne ohne Preise ist unvollständig, nicht unlesbar ===");
|
|
|
|
{
|
|
const p = leseKampagne(seite.ohnePreise);
|
|
ok(p.pflicht.titel === "Stern Stunde" && !!p.pflicht.start && !!p.pflicht.ende,
|
|
`sie wird gelesen: ${p.pflicht.titel}, ${p.pflicht.start} bis ${p.pflicht.ende}`);
|
|
ok(p.kuer.preise.length === 0, `keine Preise gelesen (${p.kuer.preise.length})`);
|
|
ok(p.hinweise.some((h) => /Preis/.test(h)),
|
|
`und das steht als Hinweis da: „${p.hinweise.find((h) => /Preis/.test(h))}“`);
|
|
ok(p.hinweise.length === 1,
|
|
`genau ein Hinweis, nicht mehr (${p.hinweise.length}) — nur die Preise fehlen`);
|
|
ok(p.kuer.aufgaben.length > 0 && p.kuer.geschenke.length > 0 && !!p.kuer.bannerUrl,
|
|
`alles andere ist da: ${p.kuer.aufgaben.length} Aufgaben,`
|
|
+ ` ${p.kuer.geschenke.length} Geschenke, Banner ja`);
|
|
ok(/Preis/.test(regelnText(p.kuer, p.hinweise)),
|
|
"die Kachel sagt selbst, dass sie unvollständig ist");
|
|
ok(!regelnText(p.kuer, p.hinweise).includes("BELOHNUNGEN"),
|
|
"und behauptet keinen Preisabschnitt, den es nicht gibt");
|
|
|
|
/* DER MILDERE FALL: Baustein da, Liste leer. Hier aus Gipfelstürmer
|
|
im Speicher hergestellt — beide Wege müssen denselben Hinweis
|
|
ergeben, sonst hängt das Verhalten daran, WIE TikTok nichts
|
|
konfiguriert hat. */
|
|
const leer = umbauen(seite.gipfel, (w) => {
|
|
const s = JSON.parse(w.globalData.activityInfo.ac_schema_with_interaction_rules);
|
|
let getroffen = 0;
|
|
(function lauf(k, t = 0) {
|
|
if (!k || typeof k !== "object" || t > 20) return;
|
|
if (typeof k.type === "string" && k.type.includes("live_reward_introduction")
|
|
&& k.props?.rewardConfig) { k.props.rewardConfig.list = []; getroffen++; }
|
|
for (const v of Object.values(k)) {
|
|
if (Array.isArray(v)) { for (const x of v) lauf(x, t + 1); }
|
|
else if (v && typeof v === "object") lauf(v, t + 1);
|
|
}
|
|
})(s);
|
|
if (getroffen !== 1) throw new Error(`erwartete 1 Preisbaustein, fand ${getroffen}`);
|
|
w.globalData.activityInfo.ac_schema_with_interaction_rules = JSON.stringify(s);
|
|
});
|
|
const l = leseKampagne(leer);
|
|
ok(l.kuer.preise.length === 0 && l.hinweise.some((h) => /Preis/.test(h)),
|
|
"auch ein leerer Preisbaustein ergibt denselben Hinweis");
|
|
ok(l.kuer.aufgaben.length === 5,
|
|
`und der Rest bleibt unberührt (${l.kuer.aufgaben.length} Aufgaben)`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
8. DER DRITTE AUSGANG DES LESERS — Aufbau unbekannt
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 8. Wenn TikTok die Seite umbaut ===");
|
|
|
|
/** Macht das Datenpaket auf, lässt daran ändern, schreibt es zurück.
|
|
* Steht hier und nicht im Leser: Der Leser liest, er baut nicht. */
|
|
function umbauen(html, aendern) {
|
|
const marke = html.indexOf("id=\"__MODERN_ROUTER_DATA__\"");
|
|
const a = html.indexOf(">", marke) + 1;
|
|
const e = html.indexOf("</script>", a);
|
|
const daten = JSON.parse(html.slice(a, e));
|
|
aendern(daten.loaderData.home_page.COSMIC_D_K.value, daten);
|
|
return html.slice(0, a) + JSON.stringify(daten) + html.slice(e);
|
|
}
|
|
|
|
/** Fängt den Fehler und gibt ihn zurück — oder `null`, wenn keiner kam. */
|
|
function faengt(tun) {
|
|
try { tun(); return null; } catch (f) { return f; }
|
|
}
|
|
|
|
{
|
|
const f = faengt(() => leseKampagne(seite.kaputt));
|
|
ok(f instanceof KampagneUnlesbar, `es gibt einen Fehler, und zwar ${f?.name}`);
|
|
ok(f?.art === "aufbau", `Sorte „${f?.art}“ — die Seite, nicht das Feld`);
|
|
ok(/__MODERN_ROUTER_DATA__/.test(f?.grund || ""),
|
|
`der Grund nennt, was fehlt: „${f?.grund}“`);
|
|
|
|
/* GEGENPROBE: Die vollständige Seite wirft NICHT. Ohne diese Zeile
|
|
wäre eine Lesefunktion, die immer wirft, hier grün. */
|
|
ok(faengt(() => leseKampagne(seite.gipfel)) === null,
|
|
"Gegenprobe: die vollständige Seite wirft nicht");
|
|
|
|
/* DIE LEERE HUELLE IST ETWAS ANDERES ALS EIN UMBAU
|
|
(nachgetragen 07.10.2026, nachdem genau das passiert ist).
|
|
|
|
Gemessen an diesem Tag: TikTok schickt auf einen gewoehnlichen
|
|
Abruf nur noch 17 KB -- Titel "Campaign", leerer Koerper, zwei
|
|
Skripte. Von jedem Weg aus dasselbe: ehrliche Kennung,
|
|
Browser-Kennung, mit Keksen, sogar aus einem echten Chromium.
|
|
Die Seite baut sich erst im Browser auf.
|
|
|
|
Fuer den Menschen davor ist der Unterschied alles: Bei einem
|
|
Umbau ist etwas zu reparieren, bei einer Huelle kann er nichts
|
|
machen -- und soll das hoeren statt zu suchen. */
|
|
{
|
|
const huelle = [
|
|
"<!doctype html><html><head><meta charset=\"utf-8\">",
|
|
"<title>Campaign</title>",
|
|
'<script id="tiktok-environment" type="application/json">{"regionInfo":{}}</script>',
|
|
'<script id="gfdatav1" type="application/json">{"env":"prod"}</script>',
|
|
"</head><body><div id=\"root\"></div></body></html>",
|
|
].join("");
|
|
const fh = faengt(() => leseKampagne(huelle));
|
|
ok(fh?.art === "aufbau", `eine Hülle wirft ebenfalls (${fh?.art})`);
|
|
ok(/Hülle/.test(fh?.grund || "") && /(KB|Bytes)/.test(fh?.grund || ""),
|
|
`und nennt sie beim Namen: „${fh?.grund}“`);
|
|
ok(!/__MODERN_ROUTER_DATA__/.test(fh?.grund || ""),
|
|
" ohne mit einer Marke um sich zu werfen, die hier niemandem hilft");
|
|
|
|
/* GEGENPROBE: Eine Seite OHNE TikToks Kennzeichen bleibt beim
|
|
alten Satz -- sonst hiesse jeder Unsinn "Hülle". */
|
|
const fremd = faengt(() => leseKampagne("<html><body>nichts</body></html>"));
|
|
ok(/__MODERN_ROUTER_DATA__/.test(fremd?.grund || ""),
|
|
"Gegenprobe: irgendeine fremde Seite gilt nicht als Hülle");
|
|
}
|
|
|
|
/* Die kaputte Seite enthält bewusst ein ANDERES JSON-Skript und eine
|
|
Zeile mit der Kampagnennummer. Wer „das erste Skript“ nimmt oder
|
|
„irgendwo die Nummer“, wird hier grün und im Betrieb falsch. */
|
|
ok(seite.kaputt.includes("__NEXT_DATA__")
|
|
&& seite.kaputt.includes("7691493175936699143"),
|
|
"sie enthält einen Lockvogel: ein anderes JSON und die Nummer");
|
|
|
|
/* Paket da, aber ohne unseren Ast. */
|
|
const ohneAst = seite.gipfel.replace(/"COSMIC_D_K"/, "\"COSMIC_ANDERS\"");
|
|
const f2 = faengt(() => leseKampagne(ohneAst));
|
|
ok(f2?.art === "aufbau" && /COSMIC_D_K/.test(f2?.grund || ""),
|
|
`ein umbenannter Ast wird benannt: „${f2?.grund}“`);
|
|
|
|
/* Paket da, aber kein JSON. */
|
|
const kaputtesJson = seite.gipfel.replace(
|
|
"id=\"__MODERN_ROUTER_DATA__\">{", "id=\"__MODERN_ROUTER_DATA__\">{{");
|
|
const f3 = faengt(() => leseKampagne(kaputtesJson));
|
|
ok(f3?.art === "aufbau" && /kein lesbares JSON/.test(f3?.grund || ""),
|
|
`unlesbares JSON wird als solches gemeldet: „${(f3?.grund || "").slice(0, 70)}…“`);
|
|
|
|
/* Paket und Ast da, aber beide Gerüstquellen weg. */
|
|
const ohneGeruest = umbauen(seite.gipfel, (w) => {
|
|
delete w.globalData.activityInfo.ac_schema_with_interaction_rules;
|
|
delete w.schema;
|
|
});
|
|
const f4 = faengt(() => leseKampagne(ohneGeruest));
|
|
ok(f4?.art === "aufbau" && /Seitengeruest fehlt/.test(f4?.grund || ""),
|
|
`fehlt BEIDES, sagt der Grund das: „${(f4?.grund || "").slice(0, 60)}…“`);
|
|
|
|
/* Und: die zweite Quelle rettet die erste. */
|
|
const nurZweite = umbauen(seite.gipfel, (w) => {
|
|
w.schema = w.globalData.activityInfo.ac_schema_with_interaction_rules;
|
|
delete w.globalData.activityInfo.ac_schema_with_interaction_rules;
|
|
});
|
|
const z = leseKampagne(nurZweite);
|
|
ok(z.auszug.gelesen_aus === "value.schema" && z.kuer.aufgaben.length === 5,
|
|
"fällt die erste Quelle weg, liest die zweite dasselbe");
|
|
}
|
|
|
|
/* =====================================================================
|
|
9. DER ZWEITE AUSGANG — Pflichtfeld fehlt, KEINE Kachel
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 9. Ohne Zeitraum entsteht keine Kachel ===");
|
|
|
|
{
|
|
const ohneEnde = umbauen(seite.gipfel, (w) => { delete w.globalData.activityInfo.end_time; });
|
|
const f = faengt(() => leseKampagne(ohneEnde));
|
|
ok(f instanceof KampagneUnlesbar && f.art === "pflicht",
|
|
`ohne Enddatum: ${f?.name}, Sorte „${f?.art}“`);
|
|
ok(/Ende/.test(f?.grund || ""), `und der Grund sagt welches Feld: „${f?.grund}“`);
|
|
|
|
const ohneStart = umbauen(seite.gipfel, (w) => { w.globalData.activityInfo.start_time = null; });
|
|
ok(faengt(() => leseKampagne(ohneStart))?.art === "pflicht", "ohne Beginn ebenso");
|
|
|
|
const ohneTitel = umbauen(seite.gipfel, (w) => { w.globalData.pageInfo.title = " "; });
|
|
ok(faengt(() => leseKampagne(ohneTitel))?.art === "pflicht", "ohne Titel ebenso");
|
|
|
|
/* DIE ZONE IST PFLICHT, und sie wird NICHT ersetzt. `time_zone`
|
|
bleibt in der Seite stehen — wer darauf ausweicht, rechnet nach
|
|
dem 25.10.2026 falsch. */
|
|
const ohneZone = umbauen(seite.gipfel, (w) => {
|
|
delete w.globalData.activityInfo.tz_location_name;
|
|
});
|
|
const fz = faengt(() => leseKampagne(ohneZone));
|
|
ok(fz?.art === "pflicht" && /tz_location_name/.test(fz?.grund || ""),
|
|
`ohne Zeitzone: „${fz?.grund}“`);
|
|
ok(datenAusSeite(ohneZone).globalData.activityInfo.time_zone === "+02:00",
|
|
"Gegenprobe: der feste Versatz +02:00 stand bereit und wurde NICHT genommen");
|
|
|
|
/* Sekunden statt Millisekunden wären das Jahr 1970 — und ein Datum
|
|
von 1970 sieht aus wie ein Datum. */
|
|
const sekunden = umbauen(seite.gipfel, (w) => {
|
|
w.globalData.activityInfo.end_time = Math.floor(1_791_410_399_000 / 1000);
|
|
});
|
|
const fs = faengt(() => leseKampagne(sekunden));
|
|
ok(fs?.art === "pflicht",
|
|
"ein Zeitstempel in Sekunden wird abgelehnt, nicht ins Jahr 1970 gerechnet");
|
|
|
|
/* Und zwei fehlende Felder nennt er BEIDE. */
|
|
const zwei = umbauen(seite.gipfel, (w) => {
|
|
delete w.globalData.activityInfo.end_time;
|
|
w.globalData.pageInfo.title = "";
|
|
});
|
|
const f2 = faengt(() => leseKampagne(zwei));
|
|
ok(/Ende/.test(f2?.grund || "") && /Titel/.test(f2?.grund || ""),
|
|
`fehlen zwei, nennt er zwei: „${f2?.grund}“`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
10. EIN PLATZHALTER OHNE TEXT FÄLLT AUF
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 10. Ein Wörterbuch mit Lücken wird gemeldet, nicht verschwiegen ===");
|
|
|
|
{
|
|
const luecke = umbauen(seite.gipfel, (w) => {
|
|
const s = JSON.parse(w.globalData.activityInfo.ac_schema_with_interaction_rules);
|
|
/* Beide Wörterbücher anfassen -- der Leser mischt sie. */
|
|
/* AUS BEIDEN WOERTERBUECHERN DIESELBEN Schluessel.
|
|
Beim ersten Versuch wurden sechs Eintraege nur aus
|
|
`value.sourceTextMap` geloescht -- und nichts fehlte, weil der
|
|
Leser beide mischt und das Woerterbuch IM SCHEMA einspringt.
|
|
Das ist richtiges Verhalten und hier gleich mitbewiesen: Erst
|
|
wenn der Schluessel in BEIDEN fehlt, fehlt der Text. */
|
|
const treffer = Object.keys(w.sourceTextMap || {})
|
|
.filter((k) => k.includes("rewardIntroFormData")).slice(0, 6);
|
|
if (treffer.length < 2) throw new Error("keine Preis-Einträge zum Löschen gefunden");
|
|
for (const k of treffer) { delete w.sourceTextMap[k]; delete (s.sourceTextMap || {})[k]; }
|
|
const raus = treffer.length;
|
|
void raus;
|
|
w.globalData.activityInfo.ac_schema_with_interaction_rules = JSON.stringify(s);
|
|
});
|
|
const l = leseKampagne(luecke);
|
|
ok(l.auszug.platzhalter_ohne_eintrag > 0,
|
|
`${l.auszug.platzhalter_ohne_eintrag} Platzhalter ohne Text — und das steht im Auszug`);
|
|
ok(l.hinweise.some((h) => h.includes("Wörterbuch")),
|
|
`und als Hinweis: „${l.hinweise.find((h) => h.includes("Wörterbuch"))}“`);
|
|
const text = regelnText(l.kuer, l.hinweise) + aufgabenText(l.kuer);
|
|
ok(!text.includes("$$__activity_"),
|
|
"und kein Platzhalter steht dafür in der Kachel");
|
|
}
|
|
|
|
/* =====================================================================
|
|
10b. DIE ZWEITE VORLAGE — nicht jede Kampagne ist wie Gipfelstürmer
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 10b. „LIVE Glow Up“ ist ganz anders gebaut ===");
|
|
|
|
{
|
|
const g = leseKampagne(seite.aufgabengruppe);
|
|
|
|
/* GEGENPROBE ZUERST: Die Bausteine, auf denen Gipfelstürmer
|
|
beruht, gibt es hier WIRKLICH nicht. Ohne diese Zeile bewiese
|
|
der Rest nur, dass irgendeine Seite gelesen wurde. */
|
|
ok(!g.auszug.bausteine.includes("live_rule_introduction")
|
|
&& !g.auszug.bausteine.includes("live_reward_introduction")
|
|
&& !g.auszug.bausteine.includes("live_campaign_intro"),
|
|
`ihr fehlen drei Bausteine, die Gipfelstürmer trägt`);
|
|
ok(g.auszug.bausteine.includes("live_task_group")
|
|
&& g.auszug.bausteine.includes("live_secondary_gift"),
|
|
`dafür hat sie live_task_group und live_secondary_gift`);
|
|
|
|
/* Und das Pflichtprogramm steht trotzdem. */
|
|
ok(g.pflicht.titel === "LIVE Glow Up", `Titel: ${g.pflicht.titel}`);
|
|
ok(g.pflicht.start === "2026-10-05" && g.pflicht.ende === "2026-10-11",
|
|
`Zeitraum: ${g.pflicht.start} bis ${g.pflicht.ende}`);
|
|
ok(g.pflicht.zone === "Europe/Berlin" && !!g.kuer.bannerUrl,
|
|
`Zone und Banner da`);
|
|
|
|
/* Aufgaben aus der Aufgabengruppe -- und OHNE Doppelte. Die
|
|
Vorlage legt je Reiter zwei Einträge an (Creator und Zuschauer);
|
|
zweimal derselbe Haken wäre ein Haken an zwei Stellen. */
|
|
ok(g.kuer.aufgaben.length === 2,
|
|
`${g.kuer.aufgaben.length} Aufgaben aus der Aufgabengruppe`);
|
|
ok(new Set(g.kuer.aufgaben).size === g.kuer.aufgaben.length,
|
|
"keine davon doppelt");
|
|
ok(g.kuer.aufgaben[0].includes("Missionen"),
|
|
`die erste heißt „${g.kuer.aufgaben[0]}“`);
|
|
|
|
ok(g.kuer.geschenke.length === 1 && !!g.kuer.geschenke[0].name,
|
|
`${g.kuer.geschenke.length} Geschenk aus live_secondary_gift`
|
|
+ ` („${g.kuer.geschenke[0]?.name}“)`);
|
|
ok(g.kuer.klapptexte.length === 2,
|
|
`${g.kuer.klapptexte.length} Klapptexte`);
|
|
ok(g.kuer.klapptexte.some((k) => /Woche 1/.test(k.text)),
|
|
" der Zeitplan steht darin (Woche 1 …)");
|
|
|
|
/* WAS ES NICHT GIBT, HEISST NICHT „konnte nicht gelesen werden“.
|
|
Das sind zwei verschiedene Nachrichten: Die eine schickt
|
|
jemanden suchen, die andere sagt ihm, dass nichts fehlt. */
|
|
ok(g.hinweise.length === 2,
|
|
`${g.hinweise.length} Hinweise: ${g.hinweise.join(" | ")}`);
|
|
ok(g.hinweise.every((h) => /hat keine|hat keinen/.test(h)),
|
|
" beide sagen „hat keine“, keiner „konnte nicht gelesen werden“");
|
|
|
|
/* GEGENPROBE: Bei Gipfelstürmer sagt derselbe Leser gar nichts --
|
|
dort ist alles da. */
|
|
ok(leseKampagne(seite.gipfel).hinweise.length === 0,
|
|
"Gegenprobe: Gipfelstürmer meldet weiterhin nichts");
|
|
|
|
/* Und der Regeltext trägt den Zeitplan, nicht nur Überschriften. */
|
|
const r = regelnText(g.kuer, g.hinweise);
|
|
ok(/Woche 1/.test(r) && /Woche 4/.test(r),
|
|
`der Regeltext enthält den ganzen Zeitplan (${r.length} Zeichen)`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
11. DER LESER LIEST NUR — nachgewiesen am Quelltext
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 11. Kein Netz, keine Datenbank, keine Uhr — im Quelltext ===");
|
|
|
|
{
|
|
const quelle = readFileSync(join(HIER, "kampagne-lesen.mjs"), "utf8");
|
|
/* Kommentare weg: Der Kopf der Datei SPRICHT über fetch und
|
|
Datenbank, und ein Wort in einer Begründung ist kein Aufruf. */
|
|
const code = quelle
|
|
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
|
|
.replace(/^\s*\/\/.*$/gm, "");
|
|
|
|
const verboten = [
|
|
[/\bfetch\s*\(/, "fetch("],
|
|
[/node:sqlite/, "node:sqlite"],
|
|
[/node:fs|node:http|node:https|node:net|node:dns|node:child_process/, "ein Zugriff nach draußen"],
|
|
[/require\s*\(/, "require("],
|
|
[/\bexpress\b/, "express"],
|
|
[/Date\s*\.\s*now\s*\(/, "Date.now()"],
|
|
[/new\s+Date\s*\(\s*\)/, "new Date() ohne Argument"],
|
|
];
|
|
const gefunden = verboten.filter(([muster]) => muster.test(code)).map(([, n]) => n);
|
|
ok(code.length > 5000 && gefunden.length === 0,
|
|
`${code.length} Zeichen Code geprüft, nichts davon verboten`
|
|
+ (gefunden.length ? ` — gefunden: ${gefunden.join(", ")}` : ""));
|
|
|
|
const einfuhren = (code.match(/^import\s/gm) || []).length;
|
|
ok(einfuhren === 0,
|
|
`die Datei führt NICHTS ein (${einfuhren}) — sie hängt an nichts`);
|
|
|
|
/* GEGENPROBE: Derselbe Prüfer findet `fetch(` dort, wo es hingehört.
|
|
Ohne sie wäre die Zeile oben auch grün, wenn das Muster nie passt. */
|
|
const helfer = readFileSync(join(HIER, "helfer-tiktok.mjs"), "utf8")
|
|
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
|
|
ok(/\bfetch\s*\(/.test(helfer),
|
|
"Gegenprobe: derselbe Prüfer findet fetch( in helfer-tiktok.mjs");
|
|
}
|
|
|
|
/* =====================================================================
|
|
12. ZWEIMAL LESEN GIBT ZWEIMAL DASSELBE
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 12. Gleiche Eingabe, gleiches Ergebnis ===");
|
|
|
|
{
|
|
const a = JSON.stringify(leseKampagne(seite.gipfel));
|
|
const b = JSON.stringify(leseKampagne(seite.gipfel));
|
|
ok(a.length > 2000 && a === b,
|
|
`zwei Läufe, ${a.length} Zeichen, Zeichen für Zeichen gleich`);
|
|
|
|
/* Und alle vier Seiten zusammen, damit keine davon still ausfällt. */
|
|
let gelesen = 0;
|
|
let geworfen = 0;
|
|
for (const [name, text] of Object.entries(seite)) {
|
|
try { leseKampagne(text); gelesen++; }
|
|
catch { geworfen++; void name; }
|
|
}
|
|
ok(gelesen === 4 && geworfen === 1,
|
|
`${gelesen} der fünf Seiten lesen sich, ${geworfen} wirft (die kaputte)`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
13. DIE RANGLISTE, DER STECKBRIEF UND DIE MISSIONSGRUPPEN
|
|
|
|
Drei Bausteine, die in allen vier gespeicherten Seiten dastanden
|
|
und bis zum 07.10.2026 nie gelesen wurden. Filipe: „ich will alle
|
|
informationen die möglich sind wenn man den link hochlädt."
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 13. Rangliste, Steckbrief, Missionsgruppen ===");
|
|
|
|
{
|
|
const g = leseKampagne(seite.gipfel);
|
|
const r = g.kuer.rangliste;
|
|
ok(!!r, "die Rangliste wird gelesen");
|
|
|
|
/* DER WERTUNGSZEITRAUM IST NICHT DER KAMPAGNENZEITRAUM -- genau
|
|
deshalb lohnt das Lesen. Bei „Gipfelstürmer" sind beide gleich,
|
|
bei „LIVE Glow Up" nicht; die Gegenprobe steht unten. */
|
|
ok(r.von_tag === "2026-10-04" && r.bis_tag === "2026-10-07",
|
|
`gewertet wird ${r.von_tag} bis ${r.bis_tag}`);
|
|
ok(r.reiter.length === 6
|
|
&& r.reiter[0] === "Superstars" && r.reiter[5] === "5. Liga",
|
|
`sechs Gruppen, aus dem Wörterbuch aufgelöst: ${JSON.stringify(r.reiter)}`);
|
|
ok(r.punkteVerborgen === false,
|
|
`die Punktzahl ist hier sichtbar (${r.punkteVerborgen})`);
|
|
ok(typeof r.beschreibung === "string" && r.beschreibung.length > 0,
|
|
`und die Wertung ist beschriftet: ${JSON.stringify(r.beschreibung)}`);
|
|
|
|
/* Der Steckbrief: nur Felder, die man ohne Deutung zeigen kann. */
|
|
const s = g.kuer.steckbrief;
|
|
ok(s.kurznummer === "1140116740", `Kurznummer ${s.kurznummer}`);
|
|
ok(s.region === "EU-TTP2", `Markt ${s.region}`);
|
|
ok(s.sprache === "de-DE", `Sprache ${s.sprache}`);
|
|
ok(s.zeitzone === "Europe/Berlin", `Zeitzone ${s.zeitzone}`);
|
|
/* LEER IST NICHT „—": `union_name` steht in allen vier gemessenen
|
|
Seiten leer und wird deshalb zu null. */
|
|
ok(s.verbund === null, `der leere Verbund wird null, nicht "" (${JSON.stringify(s.verbund)})`);
|
|
/* ANMELDUNG UND ERGEBNIS NUR BEI ABWEICHUNG. Sie sind hier gleich
|
|
dem Kampagnenzeitraum; sie trotzdem zu zeigen hieße, dieselbe
|
|
Zahl dreimal hinzuschreiben. */
|
|
ok(s.anmeldung_ab === null && s.ergebnis_bis === null,
|
|
"Anmeldung und Ergebnis fallen weg, weil sie nichts Neues sagen");
|
|
/* GEGENPROBE: Die Rohwerte sind sehr wohl da -- sonst wäre
|
|
„fällt weg" nicht von „wurde nie gelesen" zu unterscheiden. */
|
|
const rohAktion = datenAusSeite(seite.gipfel).globalData.activityInfo;
|
|
ok(zeitOderNull(rohAktion.warm_up_start_time) === g.auszug.start_ms,
|
|
`Gegenprobe: warm_up_start_time steht da und ist gleich dem Beginn `
|
|
+ `(${rohAktion.warm_up_start_time})`);
|
|
|
|
/* Kein Rohfeld, das niemand lesen kann, rutscht in den
|
|
Steckbrief -- die stehen weiter im Auszug, nicht auf der
|
|
Kachel. */
|
|
ok(!Object.keys(s).some((k) => /activity_type|kind_flag|platform|sdlc|biz/.test(k)),
|
|
`nur erklärbare Felder im Steckbrief: ${JSON.stringify(Object.keys(s))}`);
|
|
|
|
/* ---- Die zweite Vorlage: Missionsgruppen mit Beschreibung ---- */
|
|
const h = leseKampagne(seite.aufgabengruppe);
|
|
const gr = h.kuer.aufgabenGruppen;
|
|
/* ZWEI, NICHT VIER -- und das ist der Zweck der Entdopplung.
|
|
|
|
In `taskItemList` stehen VIER Einträge; die Vorlage legt je
|
|
Reiter zwei an (einen für Creator, einen für Zuschauer), und
|
|
beide zeigen auf denselben Wörterbucheintrag. Nachgemessen:
|
|
die Platzhalter `..._7_...` und `..._8_...` enden auf
|
|
derselben Prüfsumme, also auf demselben Text. Zweimal
|
|
derselbe Haken wäre ein Haken an zwei Stellen.
|
|
|
|
Meine Erwartung von vier war die ROHZAHL, nicht das
|
|
Ergebnis. Gemessen wird deshalb beides: vier hinein, zwei
|
|
heraus -- sonst wäre „zwei" auch dann grün, wenn die Hälfte
|
|
gar nicht gelesen würde. */
|
|
/* GEZÄHLT WIRD AM BAUSTEIN, nicht im Text gesucht (berichtigt
|
|
07.10.2026). Mein erster Versuch suchte den Platzhalternamen
|
|
`taskFormData_displayInfo_title` im ganzen Schema und fand ihn
|
|
ACHTMAL -- er steht dort noch an anderen Stellen. Eine Zahl aus
|
|
einer Textsuche ist keine Messung der Liste; gezählt wird
|
|
`taskItemList` selbst, also genau das, was der Leser liest. */
|
|
const rohTeile = bausteine(geruestVon(datenAusSeite(seite.aufgabengruppe)).geruest);
|
|
const rohListe = (rohTeile.get("live_task_group") || [])[0]?.props?.taskItemList || [];
|
|
ok(rohListe.length === 4, `vier Einträge stehen in der Seite (${rohListe.length})`);
|
|
ok(gr.length === 2, `und zwei verschiedene Missionsgruppen bleiben (${gr.length})`);
|
|
ok(new Set(gr.map((x) => x.titel)).size === gr.length,
|
|
`keine zwei mit demselben Titel: ${JSON.stringify(gr.map((x) => x.titel))}`);
|
|
ok(gr.every((x) => typeof x.beschreibung === "string" && x.beschreibung.length > 3),
|
|
`jede mit eigener Beschreibung: ${JSON.stringify(gr.map((x) => x.beschreibung.slice(0, 28)))}`);
|
|
ok(gr.every((x) => /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(x.von_tag || "")),
|
|
`und mit eigenem Zeitraum: ${JSON.stringify(gr.map((x) => `${x.von_tag}..${x.bis_tag}`))}`);
|
|
|
|
/* DIE HAKENLISTE IST ENTDOPPELT, DIE GRUPPENLISTE DECKUNGSGLEICH.
|
|
Die Vorlage legt je Reiter zwei Einträge an (Creator und
|
|
Zuschauer); zweimal derselbe Haken wäre ein Haken an zwei
|
|
Stellen. */
|
|
ok(h.kuer.aufgaben.length === gr.length
|
|
&& h.kuer.aufgaben.every((a, i) => a === gr[i].titel),
|
|
`Aufgaben und Gruppen passen Zeile für Zeile zusammen `
|
|
+ `(${h.kuer.aufgaben.length} / ${gr.length})`);
|
|
|
|
/* ---- WAS DIE RANGLISTE WIRKLICH BEITRÄGT ------------------
|
|
|
|
Hier stand „bei LIVE Glow Up beginnt die Wertung später als
|
|
die Kampagne". Das ist nachgemessen FALSCH: In allen drei
|
|
Seiten mit Rangliste ist der Wertungszeitraum auf die
|
|
Millisekunde gleich dem Kampagnenzeitraum. Ich hatte zwei
|
|
verschiedene Kampagnen verglichen. Die ausführliche
|
|
Berichtigung steht bei `rangBaustein` in kampagne-lesen.mjs.
|
|
|
|
GEMESSEN WIRD JETZT, WAS STIMMT -- und zwar beides: dass die
|
|
Zeiträume hier gleich sind (damit niemand wieder etwas
|
|
anderes behauptet) UND dass der Baustein trotzdem etwas
|
|
beiträgt, das sonst nirgends steht: die Gruppen. */
|
|
const gleich = [seite.gipfel, seite.ohnePreise, seite.aufgabengruppe]
|
|
.map((x) => leseKampagne(x))
|
|
.filter((x) => x.kuer.rangliste)
|
|
.map((x) => x.kuer.rangliste.von === x.auszug.start_ms
|
|
&& x.kuer.rangliste.bis === x.auszug.ende_ms);
|
|
ok(gleich.length === 3 && gleich.every(Boolean),
|
|
`in allen ${gleich.length} Seiten mit Rangliste ist die Wertung `
|
|
+ "gleich dem Kampagnenzeitraum -- gemessen, nicht angenommen");
|
|
/* DAS IST DER GEWINN: zwei Gruppen hier, sechs bei
|
|
Gipfelstürmer. „Gegen wen trete ich an" stand vorher
|
|
nirgends auf der Kachel. */
|
|
ok(h.kuer.rangliste.reiter.length === 2
|
|
&& g.kuer.rangliste.reiter.length === 6,
|
|
`die Gruppen unterscheiden sich sehr wohl: `
|
|
+ `${JSON.stringify(h.kuer.rangliste.reiter)} gegen `
|
|
+ `${g.kuer.rangliste.reiter.length} Ligen`);
|
|
/* UND IM REGELTEXT STEHT ES AUCH. Ohne diese Zeile wäre
|
|
geprüft, dass gelesen wird -- nicht, dass es ankommt. */
|
|
const rt = regelnText(h.kuer, h.hinweise);
|
|
ok(/## RANGLISTE/.test(rt) && /Gruppen: /.test(rt),
|
|
"und die Rangliste steht als eigener Abschnitt im Regeltext");
|
|
|
|
/* Der zweite Bildbaustein. */
|
|
ok(h.auszug.weitere_bilder.length === 2,
|
|
`zwei weitere Bilder aus live_banner (${h.auszug.weitere_bilder.length})`);
|
|
ok(h.auszug.weitere_bilder.every((u) => /^https:\/\//.test(u)),
|
|
"und beide mit absoluter Adresse");
|
|
/* GEGENPROBE: Geholt wird trotzdem nur EINES -- das Banner bleibt
|
|
das aus live_top_image. */
|
|
ok(h.kuer.bannerUrl !== h.auszug.weitere_bilder[0],
|
|
"das Banner kommt weiterhin aus live_top_image, nicht von dort");
|
|
|
|
/* Und das deutsche Datum. */
|
|
ok(deutschesDatum("2026-11-04") === "04.11.2026",
|
|
`deutschesDatum: ${deutschesDatum("2026-11-04")}`);
|
|
ok(deutschesDatum("Unsinn") === null, "und Unsinn gibt nichts");
|
|
}
|
|
|
|
/* =====================================================================
|
|
14. DIE GLIEDERUNG DES REGELTEXTES
|
|
|
|
Filipe, mit Bildschirmfoto: „man soll die titel in den texten
|
|
besser erkennen und sehen was zu was passt."
|
|
|
|
Geprüft wird hier die REGEL, nicht die Anzeige -- was auf dem
|
|
Bildschirm daraus wird, misst pruef-eventkarte.mjs, Abschnitt 4b.
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 14. Der Regeltext, gegliedert ===");
|
|
|
|
{
|
|
/* ZEICHENGLEICH MIT DEM, WAS AM 07.10.2026 WIRKLICH IN DER
|
|
DATENBANK STAND (Eintrag 89, „LIVE Glow Up"). Erfundene
|
|
Testdaten hätten genau das verdeckt, worum es geht. */
|
|
const echt = [
|
|
"* Diese Kampagne hat keinen Einleitungstext — der Inhalt steht in den Klapptexten.",
|
|
"* Diese Kampagne hat keine Preisliste auf der Seite.",
|
|
"",
|
|
"SAMMLE DEIN FEUERWERK — Erhalte doppelte Punkte für die Rangliste",
|
|
"· Feuerwerk (Sammle Feuerwerk in deinem LIVE!)",
|
|
"",
|
|
"INFOS ZUR KAMPAGNE",
|
|
"LIVE Glow Up ist eine exklusive 4-wöchige Wachstum Challenge.",
|
|
"",
|
|
"KAMPAGNENZEITPLAN",
|
|
"· Woche 1: 5. - 11.10.",
|
|
"",
|
|
"TEILNAHME",
|
|
"Länder: DE, CH, AT, LI, LU",
|
|
].join("\n");
|
|
|
|
const ab = gliederung(echt, {
|
|
geschenke: [{ name: "Feuerwerk", preis: null, hinweis: "Sammle Feuerwerk in deinem LIVE!" }],
|
|
});
|
|
const titel = ab.map((a) => a.titel);
|
|
ok(ab.length === 5,
|
|
`fünf Abschnitte aus dem echten Text (${ab.length}): ${JSON.stringify(titel)}`);
|
|
|
|
/* ---- Die Überschrift wird an der Großschreibung erkannt ---- */
|
|
ok(titel[1] === "SAMMLE DEIN FEUERWERK",
|
|
`die Überschrift ohne ihren Untertitel: ${JSON.stringify(titel[1])}`);
|
|
ok(ab[1].unter === "Erhalte doppelte Punkte für die Rangliste",
|
|
`der Untertitel getrennt dahinter: ${JSON.stringify(ab[1].unter)}`);
|
|
|
|
/* ---- Die vier Zeilensorten ---------------------------------- */
|
|
const arten = ab.flatMap((a) => a.zeilen.map((z) => z.art));
|
|
ok(new Set(arten).size >= 3 && arten.includes("hinweis")
|
|
&& arten.includes("geschenk") && arten.includes("punkt") && arten.includes("text"),
|
|
`alle vier Zeilensorten kommen vor: ${JSON.stringify([...new Set(arten)])}`);
|
|
|
|
/* ---- Die Hinweise stehen für sich, nicht zwischen den Regeln - */
|
|
ok(ab[0].titel === null && ab[0].art === "hinweise" && ab[0].zeilen.length === 2,
|
|
`die zwei Hinweise bilden den ersten, titellosen Abschnitt`);
|
|
ok(!ab.slice(1).some((a) => a.zeilen.some((z) => z.art === "hinweis")),
|
|
"und keiner steckt in einem anderen Abschnitt -- das war der Mangel");
|
|
|
|
/* ---- Das Geschenk kommt AUS DER LISTE, nicht aus dem Text --- */
|
|
const gabe = ab[1].zeilen.find((z) => z.art === "geschenk");
|
|
ok(gabe?.text === "Feuerwerk", `der Name: ${JSON.stringify(gabe?.text)}`);
|
|
/* `Number(null)` IST 0 -- und „0 Coins" wäre eine Preisangabe,
|
|
und zwar eine falsche. Dieser Fehler war am 07.10.2026 wirklich
|
|
da, an drei Stellen. */
|
|
ok(gabe && gabe.coins === null,
|
|
`ohne Preis bleibt ohne Preis, nicht 0: ${JSON.stringify(gabe?.coins)}`);
|
|
ok(gabe?.zusatz === "Sammle Feuerwerk in deinem LIVE!",
|
|
`stattdessen steht da, wie man es bekommt: ${JSON.stringify(gabe?.zusatz)}`);
|
|
ok(ab[1].art === "geschenke",
|
|
`der Abschnitt mit einem Geschenk ist ein Geschenkabschnitt (${ab[1].art})`);
|
|
|
|
/* GEGENPROBE: Ohne Geschenkliste ist dieselbe Zeile ein
|
|
gewöhnlicher Punkt. Sonst wäre unbelegt, dass die LISTE
|
|
entscheidet. */
|
|
const ohne = gliederung(echt);
|
|
const z1 = ohne[1].zeilen.find((z) => z.text.startsWith("Feuerwerk"));
|
|
ok(z1?.art === "punkt",
|
|
`ohne Liste ist es ein Punkt, kein Geschenk (${z1?.art})`);
|
|
|
|
/* Und der Coinpreis aus dem TEXT greift, wo es keine Liste gibt --
|
|
für von Hand getippte Zeilen. */
|
|
const vonHand = gliederung("## GESCHENKE\n· Rose — 1 Coin\n· Löwe — 29999 Coins");
|
|
const rose = vonHand[0].zeilen.find((z) => z.text === "Rose");
|
|
const loewe = vonHand[0].zeilen.find((z) => z.text === "Löwe");
|
|
ok(rose?.coins === 1 && loewe?.coins === 29999,
|
|
`aus dem Text gelesen: ${JSON.stringify([rose?.coins, loewe?.coins])}`);
|
|
|
|
/* DIE LÄNGSTEN NAMEN ZUERST: „Rose" und „Rose Gold" in derselben
|
|
Kampagne -- ein Vergleich von vorne träfe bei „Rose Gold" den
|
|
Eintrag „Rose" und schriebe dessen Preis an. */
|
|
const zwei = gliederung("## GESCHENKE\n· Rose Gold\n· Rose", {
|
|
geschenke: [{ name: "Rose", preis: 10 }, { name: "Rose Gold", preis: 500 }],
|
|
});
|
|
const gold = zwei[0].zeilen.find((z) => z.text === "Rose Gold");
|
|
ok(gold?.coins === 500,
|
|
`„Rose Gold" bekommt 500, nicht die 10 von „Rose" (${gold?.coins})`);
|
|
|
|
/* ---- Die Gegenprobe zur Überschriftenerkennung -------------- */
|
|
/* Eine Zeile aus nichts als Ländercodes IST großgeschrieben und
|
|
würde zur Überschrift. `regelnText` schreibt deshalb „Länder:
|
|
..." -- die Kleinbuchstaben im Wort davor sind die Sicherung. */
|
|
ok(ab[4].zeilen[0].art === "text",
|
|
`„Länder: DE, CH, ..." bleibt ein Absatz (${ab[4].zeilen[0].art})`);
|
|
const nackt = gliederung("DE, CH, AT");
|
|
ok(nackt[0]?.titel === "DE, CH, AT" || nackt.length === 0,
|
|
"ohne Wort davor wäre es tatsächlich eine Überschrift -- "
|
|
+ "deshalb schreibt regelnText nie nackte Codes");
|
|
|
|
/* Keine Überschrift ohne Buchstaben, keine mit Satzzeichen am
|
|
Ende, keine über 80 Zeichen. */
|
|
const proben = [
|
|
["12.10.", "eine Zahl ist keine Überschrift"],
|
|
["DAS IST EIN GESCHRIENER SATZ.", "ein Satz mit Punkt am Ende auch nicht"],
|
|
["Gemischte Schreibweise", "und Kleinbuchstaben erst recht nicht"],
|
|
["A".repeat(90), "und nichts über 80 Zeichen"],
|
|
];
|
|
for (const [zeile, was] of proben) {
|
|
const p = gliederung(zeile);
|
|
ok(p.length === 1 && p[0].titel === null, `${was}: ${JSON.stringify(zeile.slice(0, 20))}`);
|
|
}
|
|
|
|
/* ---- Zwei Stufen ------------------------------------------- */
|
|
const stufen = gliederung("## OBEN\nText.\n### Unten\nMehr Text.");
|
|
ok(stufen.length === 2 && stufen[0].stufe === 2 && stufen[1].stufe === 3,
|
|
`## ist Stufe 2, ### ist Stufe 3: ${JSON.stringify(stufen.map((a) => [a.titel, a.stufe]))}`);
|
|
|
|
/* EINE ÜBERSCHRIFT ÜBER LAUTER UNTERABSCHNITTEN BLEIBT STEHEN.
|
|
Das war beim ersten Lauf ein echter Verlust: „WELCHE LIGA BIN
|
|
ICH?" hat keine eigene Zeile, nur sechs Unterabschnitte -- und
|
|
verschwand deshalb. */
|
|
const ueber = gliederung("## WELCHE LIGA BIN ICH?\n### Superstars\nBedingung.");
|
|
ok(ueber.length === 2 && ueber[0].titel === "WELCHE LIGA BIN ICH?",
|
|
`die Überschrift über Unterabschnitten bleibt: ${JSON.stringify(ueber.map((a) => a.titel))}`);
|
|
/* GEGENPROBE: Eine Überschrift über NICHTS fällt weg -- sie
|
|
entsteht, wenn jemand den Inhalt löscht und den Titel stehen
|
|
lässt, und sieht aus wie ein Ladefehler. */
|
|
ok(gliederung("## LEER").length === 0,
|
|
"eine Überschrift über nichts fällt weg");
|
|
/* Und sie ist kein Hinweisblock -- `every()` auf einem leeren
|
|
Feld ist `true` (CLAUDE.md, 01.09.2026). */
|
|
ok(ueber[0].art !== "hinweise",
|
|
`der leere Abschnitt ist kein stiller Hinweiskasten (${ueber[0].art})`);
|
|
|
|
/* ---- Die Frage mit Antwort darunter ------------------------ */
|
|
const frage = gliederung("## INFO\nWie geht das?\nSo geht das.\nHast du Fragen?");
|
|
const fz = frage[0].zeilen;
|
|
ok(fz[0].art === "frage", `eine Frage MIT Antwort darunter: ${JSON.stringify(fz[0])}`);
|
|
/* GEGENPROBE: Eine Frage, auf die NICHTS folgt, ist keine
|
|
Überschrift, sondern eine Frage. */
|
|
ok(fz[2].art === "text",
|
|
`die letzte Zeile bleibt ein Absatz: ${JSON.stringify(fz[2])}`);
|
|
|
|
/* ---- Nichts geht verloren ---------------------------------- */
|
|
/* DIE WICHTIGSTE ZEILE DIESES ABSCHNITTS: Die Gliederung ist eine
|
|
Heuristik. Sie darf sich irren -- aber sie darf nichts
|
|
verschlucken. Gezählt wird gegen den Eingabetext. */
|
|
const eingabe = echt.split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
|
|
const ausgabe = ab.flatMap((a) => [
|
|
...(a.titel ? [1] : []), ...a.zeilen.map(() => 1),
|
|
]).length;
|
|
ok(ausgabe === eingabe.length,
|
|
`${eingabe.length} Zeilen hinein, ${ausgabe} Überschriften und Zeilen heraus`);
|
|
|
|
/* Und am echten, langen Text dasselbe. */
|
|
const lang = regelnText(leseKampagne(seite.gipfel).kuer, []);
|
|
const langAb = gliederung(lang, { geschenke: leseKampagne(seite.gipfel).kuer.geschenke });
|
|
const langEin = lang.split(/\r?\n/).map((x) => x.trim()).filter(Boolean).length;
|
|
const langAus = langAb.reduce((s, a) => s + (a.titel ? 1 : 0) + a.zeilen.length, 0);
|
|
ok(langAus === langEin,
|
|
`am echten Regeltext ebenfalls: ${langEin} hinein, ${langAus} heraus `
|
|
+ `(${langAb.length} Abschnitte)`);
|
|
}
|
|
|
|
melde("");
|
|
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
process.exit(fehler ? 1 : 0);
|