diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 397fa3f..de4557a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,12 +81,17 @@ jobs: // and it was not: it read eventCount, which is counted after // expiry, so a page whose events had all simply ended reddened the // build. Behaviour belongs where behaviour can be exercised. - const { brokenSources, staleSources } = await import("./src/shared/feed.ts"); + const { brokenSources, quietSources, staleSources } = await import("./src/shared/feed.ts"); for (const s of staleSources(feed.sources)) { console.log( ` note: ${s.sourceId} parsed ${s.parsedCount} events, all of them ended — stale page, not a fault`, ); } + for (const s of quietSources(feed.sources)) { + console.log( + ` note: ${s.sourceId} parsed nothing and the page says so itself — between versions, not a fault`, + ); + } const broken = brokenSources(feed.sources); if (broken.length > 0) { throw new Error( diff --git a/AGENTS.md b/AGENTS.md index 2724bd5..71a162e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,7 +145,7 @@ src/client/ React app, service worker, manifest theme.ts — dark or light, and what a game hue reads as on each scripts/ build-feed.ts, build-static.ts, parse-fixture.ts (offline), refresh-sources.ts (fetches) serve.ts static server + /api/health -test/ 898 tests +test/ 1,071 tests fixtures// raw HTML + .expected.json per source — pinned, kept forever snapshots/ current page per source, rewritten by refresh — see its README ``` @@ -658,7 +658,11 @@ and both generalise past this wiki: that file for a string, which proved the check existed and never that it was right. An empty Nikki lane is therefore now the truth rather than a gap, exactly as GFL2's thin weeks are. -The lane refills on its own when 2.8 is listed, with no parser change. +The lane refills on its own when 2.8 is listed, with no parser change. CI says so out loud rather +than silently — `quietSources` prints a note beside the count, because an unexplained `0` in the log +reads as exactly the fault the gate just declined to call it, and it is the only line that would ever +prompt somebody to ask whether a month-long quiet lane is a quiet game or a wording change under a +`statesNoEvents` that still matches. **The second Fandom source's page is chosen, not obvious.** `fategrandorder.fandom.com` publishes two schedules: `Event_List` opens "This page lists all Events diff --git a/src/shared/feed.ts b/src/shared/feed.ts index 5286b4d..c1c7089 100644 --- a/src/shared/feed.ts +++ b/src/shared/feed.ts @@ -165,3 +165,22 @@ export function staleSources(sources: readonly SourceHealth[]): SourceHealth[] { (s) => s.parsedCount !== null && s.parsedCount > 0 && s.eventCount === 0, ); } + +/** + * Sources whose page says it currently lists no events. + * + * The third empty, and the only one that is neither a fault nor a gap: a gacha + * calendar goes quiet between versions and this page said so in words. Not + * thrown, for the reason `statesNoEvents` exists at all. + * + * Reported, though, and that is the half worth defending. Excusing this zero + * from the build is not the same as saying nothing about it — the build log + * prints a count per source, and an unexplained `0` reads as exactly the fault + * this distinction denies. It is also the only thing that would ever prompt + * somebody to check whether a lane has been quiet for a month because the game + * is between patches or because the page's wording changed under a + * `statesNoEvents` that still matches. + */ +export function quietSources(sources: readonly SourceHealth[]): SourceHealth[] { + return sources.filter((s) => s.statesNoEvents); +} diff --git a/test/feed.test.ts b/test/feed.test.ts index 4db9a6c..c7c61f6 100644 --- a/test/feed.test.ts +++ b/test/feed.test.ts @@ -3,6 +3,7 @@ import { brokenSources, freshness, staleSources, + quietSources, STALE_AFTER_MS, SourceHealth, } from "../src/shared/feed.ts"; @@ -175,10 +176,26 @@ describe("telling a broken source from a stale one", () => { expect(brokenSources([health({})])).toEqual([]); }); - test("a healthy source is neither", () => { + test("a healthy source is none of the three", () => { const ok = health({ eventCount: 5, parsedCount: 5 }); expect(brokenSources([ok])).toEqual([]); expect(staleSources([ok])).toEqual([]); + expect(quietSources([ok])).toEqual([]); + }); + + test("a source whose page states it lists none is reported as quiet", () => { + // Excusing it from the build is not the same as saying nothing about it. + // The build log prints a count per source, and an unexplained 0 reads as + // the fault this whole distinction exists to deny — so the one empty we + // are content with is the one that has to say why. + expect( + quietSources([health({ parsedCount: 0, statesNoEvents: true })]).map( + (s) => s.sourceId, + ), + ).toEqual(["nikki-fandom-events"]); + expect(staleSources([health({ parsedCount: 0, statesNoEvents: true })])).toEqual( + [], + ); }); test("a source with nothing current left is reported as stale", () => { diff --git a/test/refresh.test.ts b/test/refresh.test.ts index a17f72c..c4d4552 100644 --- a/test/refresh.test.ts +++ b/test/refresh.test.ts @@ -1096,6 +1096,17 @@ describe("the workflows that drive the refresh", () => { expect(ci).toContain("brokenSources"); expect(ci).not.toContain("eventCount === 0"); }); + + test("ci.yml says which empty a source's zero was", async () => { + // Three sources can read zero and only one is a fault, so the two that are + // not have to say so where the count is printed — an unexplained 0 in the + // log reads as the failure the check just declined to call it. Whether the + // rules are right is exercised in test/feed.test.ts; this only pins that + // both are wired up. + const ci = await read("ci.yml"); + expect(ci).toContain("staleSources"); + expect(ci).toContain("quietSources"); + }); }); describe("flags", () => {