feat: add wiki.gg as a second Endfield source
First non-Game8 parser, and the first source that states region-scoped ends: wiki.gg emits ISO timestamps with one timer per server region, so Endfield events now carry exact times and separate Asia / Americas-Europe ends. That gap is up to 13 hours, which is what regionEnds was built for. Two sources for one game also exercised merge for real. It caught both overlaps — including "Bedazzling Dawnstar" against Game8's "Bedazzling Dawnstar Sign-In" — and flagged that the two disagree on the end date by 70 hours rather than averaging them into a date neither states. Matching an appended qualifier needed a prefix test, not a subset test: a subset would also fuse "Gold Clash" with "Gold Rush Clash Royale". Also fixes build-feed picking fixtures by game rather than by source, which handed the wiki.gg page to the Game8 parser once Endfield had two. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f4b133274a
commit
dd596b5a43
@@ -26,6 +26,7 @@ const CASES: Array<{ adapter: Adapter; fixture: string }> = [
|
||||
{ adapter: adapter("wuwa-game8-events"), fixture: "fixtures/wuwa/game8-events-2026-08-14" },
|
||||
{ adapter: adapter("zzz-game8-events"), fixture: "fixtures/zzz/game8-events-2026-08-14" },
|
||||
{ adapter: adapter("endfield-game8-events"), fixture: "fixtures/endfield/game8-events-2026-08-14" },
|
||||
{ adapter: adapter("endfield-wikigg-events"), fixture: "fixtures/endfield/wikigg-events-2026-08-14" },
|
||||
];
|
||||
|
||||
async function runAdapter(adapter: Adapter, fixture: string) {
|
||||
@@ -224,3 +225,34 @@ describe("endfield", () => {
|
||||
expect(rooted?.summary).not.toMatch(/^Period:/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("wiki.gg parser", () => {
|
||||
test("reads exact per-region timers", async () => {
|
||||
// The first source that states region-scoped ends. Asia and the Americas
|
||||
// differ by hours, which is precisely what regionEnds exists to carry.
|
||||
const events = await runAdapter(
|
||||
adapter("endfield-wikigg-events"),
|
||||
"fixtures/endfield/wikigg-events-2026-08-14",
|
||||
);
|
||||
expect(events).toHaveLength(6);
|
||||
|
||||
const heat = events.find((e) => e.title === "HEAT RAGE! MEGA ARENA!");
|
||||
expect(heat?.startPrecision).toBe("exact");
|
||||
expect(heat?.regionScoped).toBe(true);
|
||||
expect(heat?.regionEnds?.asia).toBe("2026-08-12T20:00:00.000Z");
|
||||
expect(heat?.regionEnds?.america).toBe("2026-08-13T09:00:00.000Z");
|
||||
// endsAt is the fallback for a region the source did not list, so it takes
|
||||
// the earliest — never promise more time than some region actually gets.
|
||||
expect(heat?.endsAt).toBe("2026-08-12T20:00:00.000Z");
|
||||
});
|
||||
|
||||
test("links each event to its own wiki page", async () => {
|
||||
const events = await runAdapter(
|
||||
adapter("endfield-wikigg-events"),
|
||||
"fixtures/endfield/wikigg-events-2026-08-14",
|
||||
);
|
||||
for (const e of events) {
|
||||
expect(e.sourceUrl).toStartWith("https://endfield.wiki.gg/wiki/");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+58
-1
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mergeEvents, titleSimilarity } from "../src/ingest/merge.ts";
|
||||
import {
|
||||
mergeEvents,
|
||||
titleExtends,
|
||||
titleSimilarity,
|
||||
} from "../src/ingest/merge.ts";
|
||||
import type { GachaEvent } from "../src/shared/schema.ts";
|
||||
|
||||
const NOW = "2026-08-14T00:00:00.000Z";
|
||||
@@ -134,3 +138,56 @@ describe("titleSimilarity", () => {
|
||||
expect(titleSimilarity("Gold Clash", "Fishing Frenzy")).toBeLessThan(0.3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("titleExtends", () => {
|
||||
test("matches a title with an appended qualifier", () => {
|
||||
// Real case: Game8 says "Bedazzling Dawnstar Sign-In", wiki.gg says
|
||||
// "Bedazzling Dawnstar". Token overlap scores 0.67 — under any safe
|
||||
// threshold — but it is plainly one event.
|
||||
expect(titleExtends("Bedazzling Dawnstar", "Bedazzling Dawnstar Sign-In")).toBe(true);
|
||||
});
|
||||
|
||||
test("refuses a single shared word", () => {
|
||||
// A one-word title would otherwise swallow everything containing it.
|
||||
expect(titleExtends("Rerun", "Overflowing Abundance Rerun")).toBe(false);
|
||||
});
|
||||
|
||||
test("refuses a shared-word subset that is not a prefix", () => {
|
||||
// Subset matching would fuse these; they are different events.
|
||||
expect(titleExtends("Gold Clash", "Gold Rush Clash Royale")).toBe(false);
|
||||
});
|
||||
|
||||
test("refuses two titles of equal length", () => {
|
||||
expect(titleExtends("Gold Clash", "Gold Rush")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("merging two sources for one game", () => {
|
||||
test("collapses an appended-qualifier duplicate and flags the date gap", () => {
|
||||
const g8 = event({
|
||||
id: "endfield:bedazzling-dawnstar-sign-in:2026-08-09",
|
||||
game: "endfield",
|
||||
title: "Bedazzling Dawnstar Sign-In",
|
||||
startsAt: "2026-08-09T00:00:00.000Z",
|
||||
endsAt: "2026-08-30T00:00:00.000Z",
|
||||
sourceId: "endfield-game8-events",
|
||||
confidence: 0.85,
|
||||
});
|
||||
const wiki = event({
|
||||
id: "endfield:bedazzling-dawnstar:2026-08-09",
|
||||
game: "endfield",
|
||||
title: "Bedazzling Dawnstar",
|
||||
startsAt: "2026-08-09T04:00:00.000Z",
|
||||
endsAt: "2026-09-01T22:00:00.000Z",
|
||||
sourceId: "endfield-wikigg-events",
|
||||
confidence: 0.95,
|
||||
});
|
||||
const { events, conflicts } = mergeEvents([[g8], [wiki]]);
|
||||
expect(events).toHaveLength(1);
|
||||
// The better-sourced copy wins, and the disagreement is surfaced rather
|
||||
// than averaged into a date neither source states.
|
||||
expect(events[0]?.sourceId).toBe("endfield-wikigg-events");
|
||||
expect(conflicts).toHaveLength(1);
|
||||
expect(conflicts[0]?.field).toBe("endsAt");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user