Store an empty parse when the page itself says it lists nothing
Refusing every zero-event body is right when the alternative is silently emptying a calendar, and wrong when the page has told us it is empty. A gacha game goes quiet between versions, and read strictly that is a source failing every cycle until the next patch ships: three of them reach the broken tier and fail the workflow over a lane that is correctly empty, while the snapshot being held ages out of date. Infinity Nikki sat in exactly that state for four cycles. So an empty parse is stored when the source's `statesNoEvents` vouches for it, and the cycle counts as confirmed rather than failed. Everything else is unchanged: a body that parses to nothing on its own still keeps the previous snapshot, and a first fetch that yields nothing still stores nothing. The gate turns on the page's statement, never on the adapter merely being able to make one — otherwise implementing `statesNoEvents` would quietly switch the zero-events gate off for that source. A test pins that. The run says which of the two empties it saw. "0 events — down from 6" reads as the shape change it is not, and that note is what somebody checking on a quiet lane actually sees. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a73f35b7cd
commit
4b2b3d0958
@@ -516,6 +516,44 @@ describe("a source being down never blanks the feed", () => {
|
||||
expect(await store.read("genshin-game8-events")).toBeNull();
|
||||
});
|
||||
|
||||
test("a page that states it lists no events is stored, not rejected", async () => {
|
||||
// Infinity Nikki, 2026-09-03: 2.7's events had ended, 2.8 was not listed
|
||||
// yet, and the wiki replaced both tables with "There are no Events in this
|
||||
// category." Rejecting that costs the source its snapshot every cycle until
|
||||
// the next version ships, and three cycles of it reach the `broken` tier —
|
||||
// a build failing over a game that is merely between patches.
|
||||
await seed("<html><event></event></html>", "2026-08-01T00:00:00.000Z", 1);
|
||||
const { opts } = options({
|
||||
adapters: [
|
||||
adapter({ statesNoEvents: (html: string) => html.includes("<nothing-on>") }),
|
||||
],
|
||||
responder: () => new Response("<html><nothing-on></nothing-on></html>"),
|
||||
});
|
||||
const summary = await runRefresh(opts);
|
||||
|
||||
expect(summary.outcomes[0]?.result).toBe("fetched");
|
||||
expect(summary.outcomes[0]?.eventCount).toBe(0);
|
||||
expect((await store.read("genshin-game8-events"))?.meta.eventCount).toBe(0);
|
||||
expect(summary.broken).toEqual([]);
|
||||
});
|
||||
|
||||
test("an empty parse the page does not vouch for is still rejected", async () => {
|
||||
// The gate turns on the page's statement, never on the adapter merely being
|
||||
// able to make one — otherwise implementing `statesNoEvents` would quietly
|
||||
// switch off the zero-events gate for that source.
|
||||
await seed("<html><event></event></html>", "2026-08-01T00:00:00.000Z", 1);
|
||||
const { opts } = options({
|
||||
adapters: [
|
||||
adapter({ statesNoEvents: (html: string) => html.includes("<nothing-on>") }),
|
||||
],
|
||||
responder: () => new Response("<html>redesigned</html>"),
|
||||
});
|
||||
const summary = await runRefresh(opts);
|
||||
|
||||
expect(summary.outcomes[0]?.result).toBe("rejected");
|
||||
expect((await store.read("genshin-game8-events"))?.meta.eventCount).toBe(1);
|
||||
});
|
||||
|
||||
test("a steep drop is stored but flagged", async () => {
|
||||
await seed("<html>" + "<event></event>".repeat(10) + "</html>", "2026-08-01T00:00:00.000Z", 10);
|
||||
const { opts } = options({
|
||||
|
||||
Reference in New Issue
Block a user