Skip to content

test(web): deterministic seek-acceptance and auth-geometry browser tests - #79

Merged
sayed710 merged 3 commits into
mainfrom
claude/browser-acceptance-stability
Sep 30, 2026
Merged

sayed710 merged 3 commits into
mainfrom
claude/browser-acceptance-stability

Conversation

@sayed710

@sayed710 sayed710 commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

Two backend Playwright failures reproduced on untouched main (edacc07) under four-worker load. Both are test defects, not product regressions. This PR changes only the two specs and appends PROJECT_STATE Increment 77. No production code changes.

1. seek-acceptance.spec.ts: deterministic lobby refresh

  • Cause: the creator learns its seek was accepted only on LobbyController's 10 s setInterval refresh (lobby-controller.ts:118-122,166). The acceptor is routed by the accept response (:279). The old page1.waitForURL(/\/game\/.+/, { timeout: 10_000 }) gave the creator one poll interval to poll and navigate.

  • Fix: install Playwright's clock (1.61.1) before navigation, then fast-forward exactly one refresh interval:

    • on the acceptor, to surface the seek;
    • on the creator, only after the acceptor's game URL commits. The server has committed the match by then.
    • Both pages must then reach the same /game/:id. The product's 10 s interval is unchanged.
  • Falsification: three disposable variants each first aligned the creator's poll to its worst phase (a tick had just answered "unmatched" before accept).

    Variant Result
    Old 10 s contract failed 3/3 at the originally reported line
    New flow, creator fastForward removed, 5 s probe failed 3/3 (the natural tick can't satisfy it)
    New flow unchanged, same 5 s probe passed 3/3 (the clock drives the creator's refresh)

2. auth-responsive.spec.ts: atomic geometry

  • Cause: Chromium layout-shift entries show that at 768px #lobby sits above #auth. When /v1/seeks answers, the seek-list empty state (164px) pushes the whole card down 163px about 100 ms after #auth is visible. This happened in 4/5 backend loads and never in static mode, where the request fails.
    • Register and passkey share a row in every frame. The old spec's separate boundingBox() round trips straddled the shift and read them a row apart.
  • Fix: boxesOf() reads every related rect in one page.evaluate, so all of them come from the same layout. It throws on missing or hidden elements, just as boundingBox() === null was handled. No assertion was changed or removed.
  • Falsification:
    • A verbatim copy of the old spec failed 1/20 on a single backend worker at sameRow(register, passkey). The new spec passed 20/20 alongside it.
    • A temporary .auth-actions mutation to minmax(20rem, 1fr) made the new spec fail sameRow(register, passkey) at 1440, 1024 and 768px. The stylesheet was restored from a hashed backup and rebuilt.
  • Follow-up (out of scope): the late-rendering seek-list empty state is a small real front-door layout shift (CLS ≈ 0.05).

Validation

  • npm run build, npm run lint: pass.
  • Web unit tests: 1200/1200. test:scripts: 308/308.
  • Guards: check:test-topology, check:ci-parity.
  • ci:local --quick: pass. Its Postgres, gateway/Redis, Nginx and POSIX jobs were not run locally.
  • Both changed specs typecheck under the web package's strict compiler options.
  • Focused repeats (both specs): 100/100 on 1 worker and 100/100 on 4 workers, retries=0.
  • Full backend Chromium suite: 197/197, twice, 4 workers, retries=0, zero skips. Avast shields were off, toggled by the owner.
  • Independent read-only review (Gemini 3.8 Flash High): APPROVE, no findings.

Note

PR #76 has its own unmerged PROJECT_STATE increment. This PR follows current main, so whichever merges second must renumber.

Test plan

  • CI green
  • Qodo / Greptile exact-head reviews clean
  • Owner merges manually

Summary by CodeRabbit

  • Tests
    • Improved reliability of browser acceptance tests by controlling refresh timing and measuring related layout elements from a consistent page state.
    • Kept existing responsive-layout assertions and the 10-second refresh interval unchanged.
  • Documentation
    • Updated the project status record with the latest validation results and an unresolved layout issue.

…'s clock

The creator learns its seek was accepted only on LobbyController's 10 s
setInterval refresh, but the test gave it a 10 s waitForURL after the
accept, so under four-worker load the poll phase plus navigation lost the
race. Install Playwright's clock before navigation and fast-forward exactly
one refresh interval: on the acceptor to show the seek, and on the creator
only after the acceptor's game URL commits. Both must reach the same game.
The product's 10 s interval is unchanged.
At tablet width the lobby sits above the sign-in card, and when /v1/seeks
answers its empty state pushes the card 163px down shortly after #auth is
visible. Separate boundingBox() round trips straddled that shift and read
Register and the passkey button a row apart. Read all related boxes in one
page.evaluate so they come from the same layout; assertions are unchanged.
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: bb259de2-955e-419e-a43e-befefe25fabc

📥 Commits

Reviewing files that changed from the base of the PR and between edacc07 and ac162d6.

📒 Files selected for processing (3)
  • docs/PROJECT_STATE.md
  • packages/web/e2e/auth-responsive.spec.ts
  • packages/web/e2e/seek-acceptance.spec.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The pull request updates two browser tests. The auth responsive test batches layout measurements. The seek acceptance test uses Playwright clocks to control lobby refresh timing. The project state records the test changes and validation results.

Changes

Browser test reliability

Layer / File(s) Summary
Batch auth layout measurements
packages/web/e2e/auth-responsive.spec.ts
The responsive test reads related element bounds in one browser evaluation. Its existing layout assertions remain unchanged.
Control seek acceptance timing
packages/web/e2e/seek-acceptance.spec.ts, docs/PROJECT_STATE.md
The acceptance test advances each page clock by the 10-second refresh interval and waits for both pages to reach the same game URL. The project state records the test updates, validation results, and unresolved layout shift follow-up.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Other

Suggested reviewers: edwardnewgate710

Merge Risk: ⚪ Minimal · up to ac162

This change makes two browser tests more deterministic and records the work in project documentation. It does not alter product behavior, and no actionable merge-blocking risk was identified.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: deterministic seek-acceptance testing and auth-geometry browser testing.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. (1 skipped: 1 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sayed710

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Stabilize seek-acceptance and auth-layout browser tests

🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Drive lobby refreshes with Playwright’s clock so seek acceptance no longer races polling timeouts.
• Measure related auth elements in one layout snapshot to avoid false failures during page shifts.
• Document the reproduced failures, falsification checks, and validation results in PROJECT_STATE.
Diagram

graph TD
  SeekSpec["Seek test"] --> Clock["Playwright clock"] --> Lobby["Lobby refresh"] --> GameURL["Same game URL"]
  AuthSpec["Auth test"] --> DOM["Auth elements"] --> Snapshot["Atomic snapshot"] --> Assertions["Layout assertions"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Wait for natural lobby polling
  • ➕ Requires no clock control
  • ➖ Retains poll-phase dependence and slow, load-sensitive waits
2. Wait for auth layout to settle
  • ➕ Could avoid measurements during the observed shift
  • ➖ Adds backend-dependent synchronization and does not guarantee related measurements share one layout

Recommendation: Keep the PR’s approach: it controls the test’s polling phase without changing product behavior, and it makes geometry comparisons internally consistent without weakening assertions.

Files changed (3) +82 / -31

Tests (2) +71 / -30
auth-responsive.spec.tsRead related auth bounds from one layout +48/-20

Read related auth bounds from one layout

• Adds a single-evaluation geometry helper and uses it for responsive and RTL comparisons. Existing layout assertions remain unchanged; standalone bounds checks use the same helper.

packages/web/e2e/auth-responsive.spec.ts

seek-acceptance.spec.tsControl lobby refreshes during seek acceptance +23/-10

Control lobby refreshes during seek acceptance

• Installs Playwright clocks before navigation and advances each player’s refresh when needed. Waits for the acceptor’s game navigation before advancing the creator, then requires the creator to reach that exact URL.

packages/web/e2e/seek-acceptance.spec.ts

Documentation (1) +11 / -1
PROJECT_STATE.mdRecord browser-test stability findings +11/-1

Record browser-test stability findings

• Adds Increment 77 with the reproduced failure causes, falsification experiments, validation results, and the remaining product layout-shift follow-up.

docs/PROJECT_STATE.md

@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Low risk] Updates browser test infrastructure and documentation.

The PR appears safe to merge.

Summary

The PR makes the seek-acceptance browser test drive lobby refreshes with Playwright clocks and makes auth layout comparisons use an atomic set of element bounds. It also records the test changes and validation in PROJECT_STATE.

Reviews (1) · Last reviewed commit: "docs: record the browser-acceptance stab..."

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can route each severity your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@sayed710
sayed710 merged commit 553751c into main Sep 30, 2026
11 checks passed
@sayed710
sayed710 deleted the claude/browser-acceptance-stability branch September 30, 2026 10:01
sayed710 added a commit that referenced this pull request Sep 30, 2026
Incorporates PR #79 (553751c). The only conflict was docs/PROJECT_STATE.md:
main's Increment 77 is kept verbatim and this PR's entry is appended as
Increment 78.
sayed710 added a commit that referenced this pull request Sep 30, 2026
…ls (P0) (#76)

* feat: durable, exactly-once ratings with explicit variant x speed pools (P0)

Ratings were keyed by (user, variant) and never written at runtime. Rated
results now update both players exactly once in their variant x speed pool,
applied from the committed event log.

- Migration 0044: speed joins the ratings key with finite-value checks,
  a leaderboard index, the rating checkpoint, the rating_applications
  ledger (primary key game_id) and rating_blocked_games; it fails closed
  with operator instructions if legacy variant-only rows exist. 0045
  builds the ordered GameEnded index online.
- PgRatingsApplier walks GameEnded rows in (xact_id, server_ts, game_id)
  order below pg_snapshot_xmin (the ADR-0147 committed prefix), so live
  processing and replay apply one order. Per game: both pool rows are
  locked in player-id order, both new ratings come from the pre-game rows,
  and ledger, updates and checkpoint commit together.
- Eligibility is folded from the stream only; bot, casual, aborted and
  every no-show ending are never rated; unprovable streams are blocked.
- Consumers: GET /v1/leaderboard/:variant/:speed (no default speed),
  profiles list every pool, seek ranges read the seek's own pool, and the
  web leaderboard asks for a time control before loading.

ADR-0150 records the owner decisions and design.

* fix: block impossible endings and malformed creations; update load scenario

Review findings on PR #76:
- Qodo: decideRating now takes the committed stream and blocks any ending
  the authority could not have written: a result that termination cannot
  have (e.g. a decisive stalemate), a winner contradicting the result, a
  non-boolean rated flag, or an unknown time-control kind.
- Qodo: the k6 API baseline and load README use the pooled leaderboard
  route; FEATURE_PARITY_AUDIT describes it.
- Greptile: test that endings sharing one transaction id (the migration
  0040 upgrade case) apply in ending-time order across pages.

* fix: harden durable ratings decisions and operations

* test(web): choose a rating pool in the RTL leaderboard fixture

PR #76 loads no leaderboard until a time control is chosen, so #78's RTL
containment fixture waited for rows that never mounted. The fixture now
selects blitz after navigation and serves only /v1/leaderboard/standard/blitz
with speed-bearing entries, so rows prove the real pool load.

Falsified: removing the selection, or choosing bullet, fails all four
leaderboard viewports; the fixed spec passes 4/4 and the RTL suite 36/36.

* fix(persistence): guard rating_ineligible_games only after the 0046 backfill

The decision-exclusion trigger takes one advisory lock per inserted row. With
it installed before the pre_upgrade backfill, a database with many historical
unrated endings could exhaust max_locks_per_transaction during the upgrade.
0046 is unpublished, so it now backfills first and installs the trigger after.
The backfill needs no guard: its NOT EXISTS clauses exclude every other
decision and the held checkpoint row lock keeps appliers out.

The new migration test backfills 40 earlier skips inside a rolled-back run of
0046 and requires zero advisory locks (the old order held 40), then migrates
for real and requires every skip frozen, the pending ending untouched, and
the guard installed. Removing the trigger fails it.

* fix(observability): alert on failing ratings batches and give them a runbook

A ratings batch that fails on anything other than stream data rolls back
without advancing the single checkpoint and retries with backoff, so a
repeating failure stops every pool. Only the backlog-age alert noticed, and
no runbook said what to do.

GambitRatingsBatchFailing fires when ratings_batch_failures_total keeps
increasing for ten minutes. The new "Ratings batch failures" runbook section
separates transient database errors from a repeating one, gives a read-only
query for the ending being retried, forbids hand edits and forced decisions,
and escalates to the owner. No skip or mark-ineligible command is added.

scripts/test/alert-runbooks.test.mjs requires every alert's runbook link to
resolve to a RUNBOOKS.md heading and the failure counter to have an alert.
Renaming the heading or repointing the expression fails it; promtool check
passes, and a scratch promtool unit test confirmed a failure every 30 s fires
by 16 min while a single failure never fires.

* docs: record the PR #76 integration fixes and validation in Increment 77

* docs: record the validation of the PR #79 integration in Increment 78

* fix(persistence): keep ratings retries retryable and blocks deterministic

Four review findings on the ratings applier (Qodo 1 and 3, CodeRabbit 1
and 2), each with a regression test that fails on the old code:

- Judge a checkpoint rewind by a horizon read after the checkpoint lock.
  A checkpoint another applier committed while this one waited looked
  like a logical restore and replayed history from the origin.
- Report a block found from before as already_blocked, so a replay is
  not counted, listed or logged as a new block.
- Load the stream outside the block decision and block only on the new
  CorruptGameStreamError. An unreadable event version, or any loader,
  database or runtime failure, now aborts the batch for retry instead of
  permanently blocking a valid game.
- Block a game whose variant is not in the variants catalog before any
  rating write, instead of failing ratings_variant_fkey on every retry.

* fix(observability): alert when a rating game is newly blocked

GambitRatingsGameBlocked fires on increase(ratings_games_total
{outcome="blocked"}[15m]), so a new block fires it and an old counter
value or a restart reset does not. A promtool rule test proves both and
now runs in CI; the runbook guard pins the metric, label and anchor.

* docs: record the ratings review fixes in ADR-0150, the runbook and Increment 78

* fix(persistence): block a non-string variant instead of coercing it

requireCatalogVariant looked up String(game.variant), so ['standard']
passed while the array itself reached ratings.variant, failed
ratings_variant_fkey outside the block path and stalled every pool.
A non-string variant is now a CorruptGameStreamError, and the lookup
uses the string itself.

* fix(observability): alert on a block recorded before a gateway's first scrape

increase() needs two samples, so a block written during a gateway's
startup backfill, before Prometheus first scraped it, never fired
GambitRatingsGameBlocked. A second term fires for a blocked series first
seen non-zero within 15 minutes; promtool cases cover it, a new series
at zero, a reset of an existing series and a flat counter. The drift
guard now strips offset durations as it does range selectors.

* docs: record the exact-head ratings fixes in ADR-0150, the runbook and Increment 78
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant