From 2ce428e926190cef6ea7aaf8d221bc0f8cbdf867 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Sat, 29 Aug 2026 06:03:12 -0300 Subject: [PATCH] test(quality): split the #11500 fetch-failure-dedup tests into their own file tests/unit/arena-elo-sync.test.ts crossed the 1000-line new-test-file cap (file-size gate, PR mode). The two new tests don't need the file's DB fixture (fetchArenaLeaderboards() never touches the DB), so they move to a self-contained sibling file instead of growing the frozen suite. --- .../arena-elo-fetch-failure-log-dedup.test.ts | 154 ++++++++++++++++++ tests/unit/arena-elo-sync.test.ts | 93 ----------- 2 files changed, 154 insertions(+), 93 deletions(-) create mode 100644 tests/unit/arena-elo-fetch-failure-log-dedup.test.ts diff --git a/tests/unit/arena-elo-fetch-failure-log-dedup.test.ts b/tests/unit/arena-elo-fetch-failure-log-dedup.test.ts new file mode 100644 index 0000000000..e95e4a3827 --- /dev/null +++ b/tests/unit/arena-elo-fetch-failure-log-dedup.test.ts @@ -0,0 +1,154 @@ +/** + * Unit tests for the fetchArenaLeaderboards() consecutive-failure log-dedup + * added in src/lib/arenaEloSync.ts (#11500 sub-issue 3). + * + * Split out of arena-elo-sync.test.ts (#11500) — that file needs a full + * SQLite/DB fixture the sync path requires, which this fetch-only surface + * does not; keeping these two tests self-contained keeps the split honest + * and avoids pushing arena-elo-sync.test.ts's own (pre-existing) size past + * the file-size gate's new-test-file cap. + */ + +import { describe, it, afterEach } from "node:test"; +import assert from "node:assert/strict"; + +const { fetchArenaLeaderboards, resetArenaEloFetchFailureStreaksForTests } = await import( + "../../src/lib/arenaEloSync.ts" +); +import type { ArenaLeaderboardData, ArenaModelEntry } from "../../src/lib/arenaEloSync.ts"; + +const originalFetch = globalThis.fetch; + +function mockFetch(impl: (url: string, opts?: RequestInit) => Promise): void { + globalThis.fetch = impl as typeof fetch; +} + +function restoreFetch(): void { + globalThis.fetch = originalFetch; +} + +function jsonResponse(data: unknown, status = 200): Response { + return new Response(JSON.stringify(data), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +function makeModelEntry(overrides: Partial = {}): ArenaModelEntry { + return { + rank: 1, + model: "anthropic/claude-sonnet", + vendor: "Anthropic", + score: 1350, + ci: 10, + votes: 5000, + license: "proprietary", + ...overrides, + }; +} + +function makeLeaderboardData( + models: ArenaModelEntry[] = [], + category = "text" +): ArenaLeaderboardData { + return { + meta: { leaderboard: category, model_count: models.length }, + models, + }; +} + +afterEach(() => { + restoreFetch(); + resetArenaEloFetchFailureStreaksForTests(); +}); + +describe("fetchArenaLeaderboards() — consecutive-failure log dedup (#11500)", () => { + it("rate-limits the per-category fetch-failure warning across many consecutive timeouts", async () => { + resetArenaEloFetchFailureStreaksForTests(); + const originalWarn = console.warn; + const warnCalls: string[] = []; + console.warn = ((...args: unknown[]) => { + warnCalls.push(args.map(String).join(" ")); + }) as typeof console.warn; + + try { + mockFetch(async () => { + throw new Error("The operation was aborted due to timeout"); + }); + + const ATTEMPTS = 25; + for (let i = 0; i < ATTEMPTS; i++) { + await assert.rejects(() => fetchArenaLeaderboards()); + } + + const fetchFailureWarnings = warnCalls.filter((line) => + line.includes('Failed to fetch "text" leaderboard') + ); + + // One category × 25 consecutive-failure attempts would be 25 raw warns — + // the rate limiter must keep the emitted count far below that. + assert.ok( + fetchFailureWarnings.length < ATTEMPTS, + `expected fewer than ${ATTEMPTS} warnings, got ${fetchFailureWarnings.length}` + ); + assert.ok( + fetchFailureWarnings.length <= 5, + `expected the streak-gated warning to stay tightly bounded, got ${fetchFailureWarnings.length}` + ); + assert.ok(fetchFailureWarnings.length >= 1, "the first failure must still be logged"); + } finally { + console.warn = originalWarn; + resetArenaEloFetchFailureStreaksForTests(); + } + }); + + it("resets the fetch-failure streak after a successful fetch so the next outage warns again", async () => { + resetArenaEloFetchFailureStreaksForTests(); + const originalWarn = console.warn; + const warnCalls: string[] = []; + console.warn = ((...args: unknown[]) => { + warnCalls.push(args.map(String).join(" ")); + }) as typeof console.warn; + + try { + mockFetch(async () => { + throw new Error("timeout"); + }); + await assert.rejects(() => fetchArenaLeaderboards()); + await assert.rejects(() => fetchArenaLeaderboards()); + + const textData = makeLeaderboardData( + [makeModelEntry({ model: "recovered-model", score: 1200, votes: 5000, rank: 1 })], + "text" + ); + const codeData = makeLeaderboardData( + [makeModelEntry({ model: "recovered-code", score: 1200, votes: 5000, rank: 1 })], + "code" + ); + mockFetch(async (url: string) => { + if (url.includes("name=text")) return jsonResponse(textData); + if (url.includes("name=code")) return jsonResponse(codeData); + return new Response("Not found", { status: 404 }); + }); + await fetchArenaLeaderboards(); + + mockFetch(async () => { + throw new Error("timeout again"); + }); + warnCalls.length = 0; + await assert.rejects(() => fetchArenaLeaderboards()); + + const fetchFailureWarnings = warnCalls.filter((line) => + line.includes('Failed to fetch "text" leaderboard') + ); + assert.strictEqual( + fetchFailureWarnings.length, + 1, + "streak reset by the success must re-arm the first-failure warning" + ); + } finally { + console.warn = originalWarn; + resetArenaEloFetchFailureStreaksForTests(); + } + }); +}); diff --git a/tests/unit/arena-elo-sync.test.ts b/tests/unit/arena-elo-sync.test.ts index d498ff0b5a..0fc7b07ae9 100644 --- a/tests/unit/arena-elo-sync.test.ts +++ b/tests/unit/arena-elo-sync.test.ts @@ -37,7 +37,6 @@ const { getArenaEloSyncStatus, initArenaEloSync, stopArenaEloSync, - resetArenaEloFetchFailureStreaksForTests, } = await import("../../src/lib/arenaEloSync.ts"); const { setFeatureFlagOverride, removeFeatureFlagOverride } = await import("../../src/lib/db/featureFlags.ts"); @@ -631,98 +630,6 @@ describe("fetchArenaLeaderboards()", () => { } ); }); - - // #11500 sub-issue (3): repeated consecutive timeouts must not spam one - // console.warn per category per attempt — the per-category fetch-failure - // warning is rate-limited the same way warnEmptyAutoPoolOnce dedupes. - it("rate-limits the per-category fetch-failure warning across many consecutive timeouts", async () => { - resetArenaEloFetchFailureStreaksForTests(); - const originalWarn = console.warn; - const warnCalls: string[] = []; - console.warn = ((...args: unknown[]) => { - warnCalls.push(args.map(String).join(" ")); - }) as typeof console.warn; - - try { - mockFetch(async () => { - throw new Error("The operation was aborted due to timeout"); - }); - - const ATTEMPTS = 25; - for (let i = 0; i < ATTEMPTS; i++) { - await assert.rejects(() => fetchArenaLeaderboards()); - } - - const fetchFailureWarnings = warnCalls.filter((line) => - line.includes('Failed to fetch "text" leaderboard') - ); - - // One category × 25 consecutive-failure attempts would be 25 raw warns — - // the rate limiter must keep the emitted count far below that. - assert.ok( - fetchFailureWarnings.length < ATTEMPTS, - `expected fewer than ${ATTEMPTS} warnings, got ${fetchFailureWarnings.length}` - ); - assert.ok( - fetchFailureWarnings.length <= 5, - `expected the streak-gated warning to stay tightly bounded, got ${fetchFailureWarnings.length}` - ); - assert.ok(fetchFailureWarnings.length >= 1, "the first failure must still be logged"); - } finally { - console.warn = originalWarn; - resetArenaEloFetchFailureStreaksForTests(); - } - }); - - it("resets the fetch-failure streak after a successful fetch so the next outage warns again", async () => { - resetArenaEloFetchFailureStreaksForTests(); - const originalWarn = console.warn; - const warnCalls: string[] = []; - console.warn = ((...args: unknown[]) => { - warnCalls.push(args.map(String).join(" ")); - }) as typeof console.warn; - - try { - mockFetch(async () => { - throw new Error("timeout"); - }); - await assert.rejects(() => fetchArenaLeaderboards()); - await assert.rejects(() => fetchArenaLeaderboards()); - - const textData = makeLeaderboardData( - [makeModelEntry({ model: "recovered-model", score: 1200, votes: 5000, rank: 1 })], - "text" - ); - const codeData = makeLeaderboardData( - [makeModelEntry({ model: "recovered-code", score: 1200, votes: 5000, rank: 1 })], - "code" - ); - mockFetch(async (url: string) => { - if (url.includes("name=text")) return jsonResponse(textData); - if (url.includes("name=code")) return jsonResponse(codeData); - return new Response("Not found", { status: 404 }); - }); - await fetchArenaLeaderboards(); - - mockFetch(async () => { - throw new Error("timeout again"); - }); - warnCalls.length = 0; - await assert.rejects(() => fetchArenaLeaderboards()); - - const fetchFailureWarnings = warnCalls.filter((line) => - line.includes('Failed to fetch "text" leaderboard') - ); - assert.strictEqual( - fetchFailureWarnings.length, - 1, - "streak reset by the success must re-arm the first-failure warning" - ); - } finally { - console.warn = originalWarn; - resetArenaEloFetchFailureStreaksForTests(); - } - }); }); // ═══════════════════════════════════════════════════════════