Eine TikTok-Kampagnenseite lesen -- Etappe 1, nur lesen
Etappe 1 des Bauplans "Kampagnen-Kachel aus TikTok-Link": das Auslesen
einer Kampagnenseite, nachweisbar richtig, ohne dass irgendetwas davon
schon eine Kachel erzeugt. Keine Route, keine Spalte, keine Oberflaeche,
keine Netzanfrage im Betrieb. index.js, workspace.js und
workspace-bereiche.js sind unberuehrt; beide Haeuser verhalten sich
unveraendert.
DER WEG NACH DRAUSSEN WIRD GETEILT
helfer-tiktok.mjs nimmt istTikTok und holeMitFrist aus
workspace-video.js auf. Zwei Fassungen von istTikTok waeren die
abgeschriebene Liste aus CLAUDE.md -- und gerade dort faellt es am
teuersten aus: Es ist die eine Stelle, die entscheidet, welchen
fremden Rechner unser Server anfragt. Die Frist ist jetzt ein
Zusatz mit 8000 ms als Vorgabe; das Video bleibt damit beim alten
Verhalten, die Kampagnenseite braucht mehr. pruef-video: 74
Pruefungen, 0 Fehler.
GEMESSEN, NICHT ABGESCHRIEBEN -- und der Bauplan irrte zweimal
1. pageInfo.title IST der Kampagnenname ("Gipfelstuermer"). Nur das
Kopfbild und der Untertitel kommen aus der Vorlage (einem
goldenen Loewen aus einer Nahost-Kampagne vom August 2024). Das
Banner wird deshalb aus props.imageUrl[0].url genommen.
2. Eine BEENDETE Kampagne liefert
activityInfo.ac_schema_with_interaction_rules gar nicht mehr. Das
Geruest steht dann nur unter value.schema, mit Bausteinnamen auf
_rep_remove. Beide Quellen werden gelesen, die Endung wird
abgeschnitten -- sonst liesse sich keine abgelaufene Kampagne
nachtragen.
DAS SCHEMA IST DIE WAHRHEIT, DAS WOERTERBUCH IST NUR DAS WOERTERBUCH
134 Platzhalter im Seitengeruest, 135 Eintraege im Woerterbuch. Der
eine Ueberzaehlige lautet "1 schenkende Person = 100 Punkte" und
gehoert zu einer frueheren Fassung der Kampagne. Wer das Woerterbuch
durchliest, schreibt eine Regel in die Kachel, die nicht gilt, und
das Team richtet seinen Stream danach aus. Nachgeschlagen wird
deshalb nur, nie durchgelaufen.
VIER ECHTE SEITEN ALS PRUEFDATEN
Am 07.10.2026 unangemeldet geholt (userInfo.uid = "0", anchor_id
leer) -- es steckt keine Person darin. Brotli gepackt, 150 KB je
Seite statt 1 MB, beim Anlegen sofort zurueckgelesen und Byte fuer
Byte verglichen. Jede ist mit ihrer SHA-256 festgenagelt: eine
Pruefung, deren Eingabe sich aendern kann, beweist nichts. Woher sie
kommen und was an EINER von ihnen veraendert wurde, steht in
server/pruefdaten/LIESMICH.md.
.gitattributes: server/pruefdaten/** -text. Ohne das schriebe git
kampagne-kaputt.html beim Auschecken auf CRLF um (core.autocrlf=true),
369 Bytes wuerden 378, und die Pruefung meldete einen Schaden, den es
nicht gibt -- die Sorte Fehlalarm, nach der man eine Pruefung
abschaltet. git hat es beim Hinzufuegen selbst angesagt.
pruef-kampagne-lesen.mjs: 108 Pruefungen, 0 Fehler, ohne Server, ohne
Port, ohne Netz, ohne Datenbank. Drei Ausgaenge: gelesen /
Pflichtfeld fehlt / Aufbau unbekannt -- fehlt eine Pruefdatei, endet
sie mit "KONNTE NICHT NACHSEHEN" und Rueckgabewert 2, nicht mit einem
uebersprungenen Abschnitt.
SIEBEN SABOTAGEN, SIEBEN TREFFER
Woerterbuch durchlesen, Tag mit toISOString bilden, auf time_zone
ausweichen, Vorlagenbild als Banner, _rep_remove stehenlassen, eine
Netzanfrage einschmuggeln, ein Byte in einer Pruefdatei kippen --
jede wurde bemerkt, jede in genau dem Abschnitt, in dem sie erwartet
war. Eine Pruefung, die immer bestaetigt, bestaetigt nichts.
ZWEI FUNDE AM RANDE, BEIDE BEIM MESSEN AUFGEFALLEN
mess-fokus.mjs war seit dem 01.10.2026 KAPUTT: Die Einfuhr von
eigenerPort stand INNERHALB eines Blockkommentars (Zeile 27 oeffnet,
Zeile 34 schliesst). Die Datei brach beim Start mit ReferenceError
ab. Niemandem aufgefallen, weil sie von Hand gestartet wird.
Gefunden hat das pruef-struktur.mjs -- aber erst, nachdem seine
Quellenliste ABGELEITET wird statt aufgezaehlt. Dort standen drei
Namen von Hand, obwohl der Kommentar darueber seit immer
"ABGELEITET, NICHT AUFGEZAEHLT" verspricht. Gemessen: 98 Namen und
508 Aufrufe vorher, 148 und 1025 jetzt. In der Luecke dazwischen lag
genau dieser Fehler.
NICHT ANGEFASST, WEIL AUSSERHALB DIESER ETAPPE (vorbestehend, belegt
mit einem Lauf ohne meine Dateien): pruef-struktur meldet weiterhin
vier Befunde -- willkommen.html ohne apple-touch-icon, ohne Manifest
und ohne theme-color, sowie zwei Stellen, die ihren Kalendertag aus
UTC bilden (workspace-anleitung.js:194, pruef-anleitung.mjs:1124).
Ebenso pruef-portnummern: mess-anleitung-crew.mjs rechnet seine zweite
Portnummer als PORT + 1 statt sie abzuleiten.
pruef-eventkarte 94/0 und pruef-agentur 62/0 -- beide gleich wie vor
dem Umbau.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,734 @@
|
||||
/* =====================================================================
|
||||
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",
|
||||
},
|
||||
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. */
|
||||
{
|
||||
const erwartet = new Set([...Object.values(SEITEN).map((s) => s.datei), "LIESMICH.md"]);
|
||||
const gefunden = readdirSync(DATEN);
|
||||
const fremd = gefunden.filter((n) => !erwartet.has(n));
|
||||
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,
|
||||
} = 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.)");
|
||||
{
|
||||
const rohHtml = "<ol start=\"3\"><li><strong>Liga </strong></li></ol>";
|
||||
ok(textAusHtml(rohHtml) === "3. Liga",
|
||||
`Gegenprobe am Baustein selbst: ${JSON.stringify(textAusHtml(rohHtml))}`);
|
||||
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) => h.includes("Preise")),
|
||||
`und das steht als Hinweis da: „${p.hinweise.find((h) => h.includes("Preise"))}“`);
|
||||
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(regelnText(p.kuer, p.hinweise).includes("Bitte nachtragen"),
|
||||
"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) => h.includes("Preise")),
|
||||
"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 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");
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
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 === 3 && geworfen === 1,
|
||||
`${gelesen} der vier Seiten lesen sich, ${geworfen} wirft (die kaputte)`);
|
||||
}
|
||||
|
||||
melde("");
|
||||
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
||||
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
||||
process.exit(fehler ? 1 : 0);
|
||||
Reference in New Issue
Block a user