diff --git a/scripts/build-feed.ts b/scripts/build-feed.ts index efc3f2b..d216542 100644 --- a/scripts/build-feed.ts +++ b/scripts/build-feed.ts @@ -12,7 +12,7 @@ import { ADAPTERS } from "../src/ingest/adapters/index.ts"; import { sourceHealth } from "../src/ingest/health.ts"; import { mergeEvents } from "../src/ingest/merge.ts"; -import { SnapshotStore, freshnessAt } from "../src/ingest/snapshots.ts"; +import { SnapshotStore, freshnessAt, pickFixture } from "../src/ingest/snapshots.ts"; import { EventFeed, SCHEMA_VERSION, type SourceHealth } from "../src/shared/feed.ts"; import type { GachaEvent, GameId } from "../src/shared/schema.ts"; @@ -22,17 +22,16 @@ const snapshots = new SnapshotStore(process.env["SNAPSHOT_DIR"] ?? "snapshots"); /** * Newest fixture for one *source*, not one game. * - * A game can have several sources, and fixtures are named `-events-` - * against adapter ids of `--events`. Globbing by game alone hands - * one site's page to another site's parser. + * A game can have several sources, and fixtures are named `-` (`fandom-events-2026-09-12` for `genshin-fandom-events`). + * Globbing by game alone, or by site, hands one page to another page's parser — + * see `pickFixture`. */ async function latestFixture(adapterId: string, game: GameId) { - const site = adapterId.replace(`${game}-`, "").replace(/-events$/, ""); - const pattern = `fixtures/${game}/${site}-*.html`; - const files = [...new Bun.Glob(pattern).scanSync(".")].sort(); - const file = files.at(-1); + const files = [...new Bun.Glob(`fixtures/${game}/*.html`).scanSync(".")]; + const file = pickFixture(adapterId, game, files); if (file === undefined) { - throw new Error(`no fixture found for ${adapterId} (${pattern})`); + throw new Error(`no fixture found for ${adapterId} (fixtures/${game}/${adapterId.slice(game.length + 1)}-.html)`); } return { file, html: await Bun.file(file).text() }; } diff --git a/src/ingest/snapshots.ts b/src/ingest/snapshots.ts index 8ef4371..9a16979 100644 --- a/src/ingest/snapshots.ts +++ b/src/ingest/snapshots.ts @@ -404,3 +404,18 @@ export function freshnessAt(snapshot: Snapshot): string { ? confirmed : snapshot.meta.contentChangedAt; } + +/** + * The newest checked-in fixture for one *source*, from `files`, or undefined. + * + * Fixtures are named `-.html`: `fandom-events-2026-09-12` + * for `genshin-fandom-events`. The name must be followed directly by the date — + * a glob of `fandom-*` would hand `genshin-fandom-miliastra-pass`'s page to the + * events parser, a second page on the same site being exactly the case that + * shares a prefix. Sorted by name, so the newest date is last. + */ +export function pickFixture(adapterId: string, game: string, files: readonly string[]): string | undefined { + const name = adapterId.slice(game.length + 1); + const own = new RegExp(`(^|/)${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}-\\d{4}-\\d{2}-\\d{2}\\.html$`); + return files.filter((f) => own.test(f)).sort().at(-1); +} diff --git a/test/snapshots.test.ts b/test/snapshots.test.ts index 3ce770f..2c1e4be 100644 --- a/test/snapshots.test.ts +++ b/test/snapshots.test.ts @@ -1,3 +1,4 @@ +import { pickFixture } from "../src/ingest/snapshots.ts"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { statSync } from "node:fs"; import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; @@ -454,3 +455,29 @@ describe("freshnessAt", () => { ).toBe(T0); }); }); + +describe("pickFixture: a source's own fixture, never a sibling's", () => { + const files = [ + "fixtures/genshin/fandom-events-2026-09-12.html", + "fixtures/genshin/fandom-miliastra-pass-2026-10-16.html", + "fixtures/genshin/game8-events-2026-08-14.html", + ]; + + test("an -events source keeps its old names", () => { + expect(pickFixture("genshin-fandom-events", "genshin", files)).toBe( + "fixtures/genshin/fandom-events-2026-09-12.html", + ); + }); + + test("a second page on the same site does not take the events page's fixture, or give it its own", () => { + expect(pickFixture("genshin-fandom-miliastra-pass", "genshin", files)).toBe( + "fixtures/genshin/fandom-miliastra-pass-2026-10-16.html", + ); + }); + + test("the newest date wins, and nothing matches nothing", () => { + expect(pickFixture("genshin-game8-events", "genshin", [...files, "fixtures/genshin/game8-events-2026-09-01.html"])) + .toBe("fixtures/genshin/game8-events-2026-09-01.html"); + expect(pickFixture("genshin-fandom-other", "genshin", files)).toBeUndefined(); + }); +});