From 6811848a47361ebc856aecae4aa7c1f111a2263a Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Wed, 10 Jun 2026 18:19:46 -0300 Subject: [PATCH] fix: derive isComplete from isRealtime+pct instead of ponder_sync_is_complete (COW-1008) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ponder_sync_is_complete never reaches 1 for chains with live block handlers (C1–C5 run indefinitely). isComplete is now computed locally as isRealtime && pct >= 100, which correctly reflects sync completion. Co-Authored-By: Claude Sonnet 4.6 --- src/api/endpoints/sync-progress.ts | 4 +++- tests/api/sync-progress.test.ts | 28 ++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/api/endpoints/sync-progress.ts b/src/api/endpoints/sync-progress.ts index 4caf985..7678fdf 100644 --- a/src/api/endpoints/sync-progress.ts +++ b/src/api/endpoints/sync-progress.ts @@ -74,7 +74,9 @@ export const syncProgressHandler: RouteHandler = processedBlocks: processed, historicalBlocksFetchedPct: pct, isRealtime: (isRealtime.get(chain) ?? 0) === 1, - isComplete: (isComplete.get(chain) ?? 0) === 1, + // ponder_sync_is_complete never reaches 1 for chains with live block handlers + // (C1–C5 run indefinitely). Derive locally: synced means realtime + all blocks processed. + isComplete: (isRealtime.get(chain) ?? 0) === 1 && pct >= 100, }; } diff --git a/tests/api/sync-progress.test.ts b/tests/api/sync-progress.test.ts index b39e401..97e0727 100644 --- a/tests/api/sync-progress.test.ts +++ b/tests/api/sync-progress.test.ts @@ -95,14 +95,38 @@ describe("GET /api/sync-progress", () => { expect(body["gnosis"]!.historicalBlocksFetchedPct).toBe(14.1); }); - it("sets isRealtime and isComplete from metrics flags", async () => { + it("sets isRealtime from ponder_sync_is_realtime metric", async () => { mockFetch(SAMPLE_METRICS); const app = buildApp(); const res = await app.request("http://localhost/api/sync-progress"); const body = (await res.json()) as Record; expect(body["mainnet"]!.isRealtime).toBe(false); - expect(body["mainnet"]!.isComplete).toBe(false); expect(body["gnosis"]!.isRealtime).toBe(true); + }); + + it("isComplete requires both isRealtime=true and pct>=100 (ignores ponder_sync_is_complete)", async () => { + // gnosis is realtime but only 14.1% processed — must NOT be complete + mockFetch(SAMPLE_METRICS); + const app = buildApp(); + const res = await app.request("http://localhost/api/sync-progress"); + const body = (await res.json()) as Record; + expect(body["gnosis"]!.isRealtime).toBe(true); + expect(body["gnosis"]!.isComplete).toBe(false); + }); + + it("isComplete is true when isRealtime=true and all blocks processed", async () => { + // Regression: ponder_sync_is_complete=0 must not block isComplete when realtime+100% + const fullSyncMetrics = ` +ponder_historical_total_blocks{chain="gnosis"} 1000 +ponder_historical_completed_blocks{chain="gnosis"} 600 +ponder_historical_cached_blocks{chain="gnosis"} 400 +ponder_sync_is_realtime{chain="gnosis"} 1 +ponder_sync_is_complete{chain="gnosis"} 0 +`.trim(); + mockFetch(fullSyncMetrics); + const app = buildApp(); + const res = await app.request("http://localhost/api/sync-progress"); + const body = (await res.json()) as Record; expect(body["gnosis"]!.isComplete).toBe(true); });