Identify the Infinity Nikki page by its headings, not by finding a row

The wiki emptied both its Current and Upcoming tables on 2026-09-03 — 2.7's
events had ended and 2.8 was not listed yet — and said so in words: "There
are no Events in this category". Nothing else moved; Past Events still
carries twenty tables of the same shape.

`isInfinityNikkiEventPage` looked for a *populated* table, so it read that
as a redesign. The source spent four cycles reporting "the source has
likely been redesigned" at a page nobody had touched, reached the broken
tier, and went on serving a snapshot whose every row expired on 27 August.

A check that reads data cannot tell a rewrite from a quiet week. It now
reads the section headings, which an empty table does not take with it —
either heading rather than both, since requiring the pair would fail the
source over a renamed heading it does not even read, and one already
separates this page from the other three Fandom templates. docs/INGESTION.md
asked for structural checks all along; this one had drifted into content.

`statesNoEvents` then reports the page's own declaration, so the runner can
tell an answer from a failure. It insists on the statement rather than an
empty table, because a redesign yields an empty table too.

The emptied page is pinned as a fixture: an empty answer is a shape, and it
is the one that used to read as a redesign.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-09-03 18:45:24 +02:00
co-authored by Claude Opus 5
parent 0809fa1122
commit a73f35b7cd
4 changed files with 116 additions and 3 deletions
+56
View File
@@ -51,6 +51,11 @@ const CASES: Array<{ adapter: Adapter; fixture: string }> = [
{ adapter: adapter("uma-game8-events"), fixture: "fixtures/uma/game8-events-2026-08-19" },
{ adapter: adapter("nikke-fandom-events"), fixture: "fixtures/nikke/fandom-events-2026-08-19" },
{ adapter: adapter("nikki-fandom-events"), fixture: "fixtures/nikki/fandom-events-2026-08-19" },
// The same page a week later, listing nothing: 2.7's events had ended and 2.8
// was not up yet, so both tables read "There are no Events in this category."
// Pinned because an empty answer is a shape too — it is what the detector
// used to read as a redesign.
{ adapter: adapter("nikki-fandom-events"), fixture: "fixtures/nikki/fandom-events-2026-09-03" },
// Honkai Impact 3rd. Every boundary in this expected file is a week-bucket
// edge the source labels `ESTIMATED WEEK`, not an announced date — see
// src/ingest/parsers/arustats.ts and docs/SOURCES.md § 14.
@@ -1767,3 +1772,54 @@ describe("Infinity Nikki wiki (the fourth Fandom template)", () => {
expect(shop?.summary).toBeTruthy();
});
});
describe("Infinity Nikki wiki between versions (the page states it is empty)", () => {
const nikki = adapter("nikki-fandom-events");
const emptied = "fixtures/nikki/fandom-events-2026-09-03";
const populated = "fixtures/nikki/fandom-events-2026-08-19";
function parse(html: string) {
return nikki.parse(html, {
now: NOW,
sourceUrl: nikki.url,
sourceId: nikki.id,
game: nikki.game,
});
}
test("still recognises the template when both tables have been emptied", async () => {
// 2.7's events ended on 27 August and 2.8 was not listed yet, so the wiki
// replaced both tables with "There are no Events in this category." The
// page is otherwise untouched — `Past Events` still carries twenty tables
// of the same shape. A detector that needs a populated row reads that as a
// redesign and the runner reports a broken source that is merely quiet.
const html = await Bun.file(`${emptied}.html`).text();
expect(fandomParser.canParse(html)).toBe(true);
expect(() => parse(html)).not.toThrow();
expect(parse(html)).toEqual([]);
});
test("says the page states no events, and says it only when the page does", async () => {
// The distinction the refresh gate turns on: a page that tells us it lists
// nothing is a source we read correctly, not one that broke. Anything
// weaker than the page's own words would let a real redesign through as an
// empty calendar, which is the failure the zero-events gate exists for.
const empty = await Bun.file(`${emptied}.html`).text();
const full = await Bun.file(`${populated}.html`).text();
expect(fandomParser.statesNoEvents?.(empty)).toBe(true);
expect(fandomParser.statesNoEvents?.(full)).toBe(false);
});
test("does not claim emptiness for the other three Fandom templates", async () => {
// `statesNoEvents` is asked of every Fandom source, so a loose check would
// let a redesigned FGO or Nikke page store an empty snapshot.
for (const fixture of [
"fixtures/fgo/fandom-events-2026-08-18",
"fixtures/nikke/fandom-events-2026-08-19",
"fixtures/r1999/fandom-events-2026-08-17",
]) {
const html = await Bun.file(`${fixture}.html`).text();
expect(fandomParser.statesNoEvents?.(html)).toBe(false);
}
});
});