From 157b9fe20c779cc5dbf58eacbec46b188ed3b2d1 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Fri, 18 Sep 2026 09:30:31 +0200 Subject: [PATCH] fix(feed): reflect content updates in source health confirmation and freshness Ensure lastConfirmedAt never precedes contentChangedAt for fetched snapshots and calculate game-level lastConfirmedAt using effective pull times. --- scripts/build-feed.ts | 12 ++++++++++-- src/ingest/health.ts | 7 ++++++- src/shared/feed.ts | 16 +++++++++++++--- test/feed.test.ts | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/scripts/build-feed.ts b/scripts/build-feed.ts index c10ec88..7f19d13 100644 --- a/scripts/build-feed.ts +++ b/scripts/build-feed.ts @@ -47,12 +47,20 @@ async function latestFixture(adapterId: string, game: GameId) { async function documentFor(adapterId: string, game: GameId) { const cached = await snapshots.read(adapterId); if (cached !== null) { + const rawConfirmed = cached.state.lastConfirmedAt; + const contentChangedAt = cached.meta.contentChangedAt; + const lastConfirmedAt = + rawConfirmed !== null && + contentChangedAt !== null && + Date.parse(rawConfirmed) >= Date.parse(contentChangedAt) + ? rawConfirmed + : (contentChangedAt ?? rawConfirmed); return { file: snapshots.bodyPath(adapterId), html: cached.html, at: freshnessAt(cached), - lastConfirmedAt: cached.state.lastConfirmedAt, - contentChangedAt: cached.meta.contentChangedAt, + lastConfirmedAt, + contentChangedAt, }; } const { file, html } = await latestFixture(adapterId, game); diff --git a/src/ingest/health.ts b/src/ingest/health.ts index 566af16..6e6aa6c 100644 --- a/src/ingest/health.ts +++ b/src/ingest/health.ts @@ -86,7 +86,12 @@ export function sourceHealth( // When the bytes were last confirmed live; a fixture's capture date when // this source has never been refreshed. lastSuccessAt: at, - lastConfirmedAt: timestamps?.lastConfirmedAt ?? null, + lastConfirmedAt: + timestamps?.lastConfirmedAt != null && + (timestamps.contentChangedAt ?? at) != null && + Date.parse(timestamps.lastConfirmedAt) < Date.parse(timestamps.contentChangedAt ?? at!) + ? (timestamps.contentChangedAt ?? at) + : (timestamps?.lastConfirmedAt ?? null), contentChangedAt: timestamps?.contentChangedAt ?? at, eventCount, parsedCount, diff --git a/src/shared/feed.ts b/src/shared/feed.ts index 1d5dc01..2924f83 100644 --- a/src/shared/feed.ts +++ b/src/shared/feed.ts @@ -167,11 +167,21 @@ export function freshness( let lastConfirmedAt: string | null = null; let hasConfirmed = false; for (const s of sourcesInfo) { + const pullAt = + s.lastConfirmedAt !== null && + s.contentChangedAt !== null && + Date.parse(s.lastConfirmedAt) >= Date.parse(s.contentChangedAt) + ? s.lastConfirmedAt + : (s.lastConfirmedAt ?? + (s.contentChangedAt && now - Date.parse(s.contentChangedAt) <= STALE_AFTER_MS + ? s.contentChangedAt + : null)); + if (!hasConfirmed) { - lastConfirmedAt = s.lastConfirmedAt; + lastConfirmedAt = pullAt; hasConfirmed = true; - } else if (lastConfirmedAt !== null && (s.lastConfirmedAt === null || s.lastConfirmedAt < lastConfirmedAt)) { - lastConfirmedAt = s.lastConfirmedAt; + } else if (lastConfirmedAt !== null && (pullAt === null || pullAt < lastConfirmedAt)) { + lastConfirmedAt = pullAt; } } diff --git a/test/feed.test.ts b/test/feed.test.ts index 798d5b1..c372683 100644 --- a/test/feed.test.ts +++ b/test/feed.test.ts @@ -202,6 +202,31 @@ describe("freshness", () => { expect(staleEndfield.sources[0]?.contentChangedAt).toBe(OLD_SITE); }); + test("differentiates blame across multiple sources when one updated recently and another confirmed 304", () => { + const PULL_TIME = new Date(NOW - 11 * HOUR).toISOString(); + const OLD_SITE = new Date(NOW - 60 * HOUR).toISOString(); + const RECENT_CONTENT = new Date(NOW - 1 * HOUR).toISOString(); + const result = freshness( + [ + { + ...source("endfield", OLD_SITE, "endfield-wikigg-events"), + lastConfirmedAt: PULL_TIME, + contentChangedAt: OLD_SITE, + }, + { + ...source("endfield", RECENT_CONTENT, "endfield-game8-events"), + lastConfirmedAt: null, + contentChangedAt: RECENT_CONTENT, + }, + ], + NOW, + ); + expect(result.stale).toHaveLength(1); + const staleEndfield = result.stale[0]!; + expect(staleEndfield.lastConfirmedAt).toBe(PULL_TIME); + expect(staleEndfield.contentChangedAt).toBe(OLD_SITE); + }); + test("an empty feed reports nothing rather than throwing", () => { expect(freshness([], NOW)).toEqual({ refreshedAt: null, stale: [] }); }); @@ -394,4 +419,14 @@ describe("recording a source's health at build time", () => { expect(health.statesNoEvents).toBe(false); expect(brokenSources([health])).toEqual([]); }); + + test("lastConfirmedAt is never older than contentChangedAt", () => { + const OLD_CONFIRM = "2026-09-12T00:00:00.000Z"; + const NEW_CONTENT = "2026-09-18T00:00:00.000Z"; + const health = sourceHealth(adapter(), FULL, NEW_CONTENT, 1, { + lastConfirmedAt: OLD_CONFIRM, + contentChangedAt: NEW_CONTENT, + }); + expect(health.lastConfirmedAt).toBe(NEW_CONTENT); + }); });