From a32d6667517aab5417fea674b421cec56b28d870 Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 24 Jun 2026 10:07:31 -0700 Subject: [PATCH] fix(e2e): reset cycle-gate games in place instead of 240 page reloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cycle regression gate did a fresh page.goto() for each of 240 games in one browser context. Around game ~100 the accumulated resources made page.waitForFunction time out (30s), failing the gate on ~88% of runs — a long-standing flaky-CI issue, not a product regression (the 18 e2e tests always pass). Load the page once and reset each game via a new __FERROUS_DEBUG__.newGame(seed, drawThree) bridge method (added to game.js — it was already in play.html). cycle_metrics.js now navigates once, then loops newGame() + runAutoplay with no per-game reload, so the run stays fast and stable. Co-Authored-By: Claude Opus 4.8 (1M context) --- solitaire_server/e2e/scripts/cycle_metrics.js | 49 ++++++++++--------- solitaire_server/web/game.js | 8 +++ 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/solitaire_server/e2e/scripts/cycle_metrics.js b/solitaire_server/e2e/scripts/cycle_metrics.js index 71149a5..40d2156 100644 --- a/solitaire_server/e2e/scripts/cycle_metrics.js +++ b/solitaire_server/e2e/scripts/cycle_metrics.js @@ -142,10 +142,32 @@ async function main() { const page = await context.newPage(); const results = []; + // Load the page once, then reset each game in place via the bridge's + // newGame(). A fresh page.goto() per game (hundreds of navigations in a + // single browser context) accumulates resources and eventually makes + // waitForFunction time out around game ~100. One load stays fast. + await page.goto(`${baseUrl}/${route}`, { waitUntil: "domcontentloaded" }); + if (route === "play-classic") { + const resumeVisible = await page + .locator("#resume-overlay:not(.hidden)") + .isVisible() + .catch(() => false); + if (resumeVisible) { + await page.evaluate(() => localStorage.removeItem("fs_game_save")); + await page.reload({ waitUntil: "domcontentloaded" }); + } + } + await page.waitForFunction( + () => + typeof window.__FERROUS_DEBUG__ === "object" && + typeof window.__FERROUS_DEBUG__.newGame === "function", + null, + { timeout: 30_000 } + ); + for (let i = 0; i < games; i++) { const seed = i; const draw3 = i % 2 === 1; - const suffix = draw3 ? "&draw3=" : ""; const pageErrors = []; const consoleErrors = []; @@ -158,27 +180,10 @@ async function main() { } }); - await page.goto(`${baseUrl}/${route}?seed=${seed}${suffix}`, { - waitUntil: "domcontentloaded", - }); - - if (route === "play-classic") { - const resumeVisible = await page - .locator("#resume-overlay:not(.hidden)") - .isVisible() - .catch(() => false); - if (resumeVisible) { - await page.evaluate(() => localStorage.removeItem("fs_game_save")); - await page.reload({ waitUntil: "domcontentloaded" }); - } - } - - await page.waitForFunction( - () => - typeof window.__FERROUS_DEBUG__ === "object" && - window.__FERROUS_DEBUG__.seed() !== null, - null, - { timeout: 30_000 } + // Reset to a fresh seeded game without navigating. + await page.evaluate( + ({ seed, draw3 }) => window.__FERROUS_DEBUG__.newGame(seed, draw3), + { seed, draw3 } ); const run = await page.evaluate(({ stepCap, policyName, maxVisits }) => { diff --git a/solitaire_server/web/game.js b/solitaire_server/web/game.js index 7270052..9a24aee 100644 --- a/solitaire_server/web/game.js +++ b/solitaire_server/web/game.js @@ -994,6 +994,14 @@ window.__FERROUS_DEBUG__ = { serialize() { return game ? game.serialize() : null; }, + // Reset to a fresh seeded game in place (no page reload). Lets the cycle + // regression harness reuse one page across hundreds of games instead of + // navigating per game. + newGame(seed, drawThreeMode) { + drawThree = !!drawThreeMode; + startGame(seed ?? randomSeed()); + return game ? game.state() : null; + }, applyLegalMove(index) { if (!game) return { ok: false, error: "game_not_ready" }; const result = game.debug_apply_legal_move(index);