feat(refresh): record a lapsed source instead of failing every run on it

holodori.wiki still answers 200 and still parses, but its editors stopped
adding events after 19 Sep 2026, so every cycle parsed to zero, was rejected,
and after three cycles turned the refresh workflow red over something no code
here can fix. No fetchable replacement exists.

A registry entry can now carry `lapsed: { recorded, reason }`, written by a
person. Refresh reports such a source's empty 200s as a warning rather than
broken; any other status is still broken, and the first cycle that lists
events again warns that the record should come out. Readers see no change:
the held snapshot keeps ageing and the staleness footer still says so.

The 2026-10-14 page is kept as a fixture, pinning that the template still
matches and only the current table is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-14 02:34:45 +02:00
1 parent 3d32ab8d21
commit ae701de74e
6 files changed
+1029 -2

No files matched your search

+59
View File
@@ -900,6 +900,65 @@ describe("a source that has stopped answering", () => {
});
});
describe("a source recorded as lapsed", () => {
// hololive Dreams, 2026-10-14: holodori.wiki answers 200 every cycle with an
// intact page whose editors stopped adding events, and no other source can
// be fetched. A person recorded that; the run should say so without turning
// red every cycle over something nobody here can fix.
const LAPSED = { recorded: "2026-10-14", reason: "editors stopped adding events" };
async function emptyCycles(lapsedAdapter: Adapter, body: string, status = 200) {
await seed("<html><event></event></html>", NOW.toISOString(), 1);
let summary = await runRefresh(
options({ adapters: [lapsedAdapter], responder: () => new Response(body, { status }) }).opts,
);
for (let i = 1; i <= BROKEN_AFTER_FAILURES; i += 1) {
const later = new Date(NOW.getTime() + i * SIX_HOURS_MS);
summary = await runRefresh({
...options({
adapters: [lapsedAdapter],
responder: () => new Response(body, { status }),
}).opts,
now: () => later,
});
}
return summary;
}
test("an empty page it keeps answering with is a warning, not broken", async () => {
const summary = await emptyCycles(adapter({ lapsed: LAPSED }), "<html></html>");
expect(summary.outcomes[0]?.result).toBe("rejected");
expect(summary.broken).toEqual([]);
expect(summary.warnings.join("\n")).toInclude(
"genshin-game8-events: lapsed (recorded 2026-10-14, editors stopped adding events)",
);
});
test("it is still broken when it stops answering", async () => {
const summary = await emptyCycles(adapter({ lapsed: LAPSED }), "denied", 403);
expect(summary.broken).toHaveLength(1);
});
test("a source with no such record is broken as before", async () => {
const summary = await emptyCycles(adapter(), "<html></html>");
expect(summary.broken).toHaveLength(1);
});
test("events coming back say the record is out of date", async () => {
const { opts } = options({
adapters: [adapter({ lapsed: LAPSED })],
responder: () => new Response("<html><event></event><event></event></html>"),
});
const summary = await runRefresh(opts);
expect(summary.outcomes[0]?.result).toBe("fetched");
expect(summary.warnings.join("\n")).toInclude(
"genshin-game8-events is listing events again; remove `lapsed` from its registry entry",
);
});
});
describe("what the runner reports to the runner", () => {
// Warnings on stdout are invisible unless someone opens the log, which is how
// six of seven sources failed every cycle for three days under a green tick.