e2e: CI drives the built bundle, a missing await fails lint, and the suite's budgets become decisions #204
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
spec
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
amaurycoudr/chess-project#204
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem Statement
The browser-seam suite is the only oracle for the Room refactor (#191), and two things about it are not true today.
CI is not testing what it claims to test. The workflow builds the packages and sets
E2E_TARGET=builtso the suite drives the bundle a Player actually receives. It doesn't. Turbo 2 runs in strict env mode by default, and thetest:slowtask declares no passthrough for that variable, so it arrives at the runner asundefinedand the suite silently falls back to the dev servers. Three separate comments in the repo promise the opposite, and the CI build step exists to serve a guarantee that is not being delivered. A regression that only appears in the production bundle would pass CI.A dropped
awaitwould silently disable an assertion. Every helper in the suite returns a promise. Omit oneawaitand the assertion never runs, the test still reports green, and nothing objects —typescript/no-floating-promisesis a type-aware rule, and type-aware linting is not enabled in this repo, sopnpm lintdoes not catch it. Every assertion is correctly awaited today; the exposure is the next edit.Beyond those two defects, several of the suite's most important numbers are unstated defaults rather than decisions. The budget for a Move to cross from one Player's browser to the other's is the built-in five seconds, and it appears nowhere. The poll that holds a request open while asserting local-first application inherits that same five seconds, and everything it guards must finish inside it. There is no ceiling on a wedged run. On CI there is no HTML report, so triage starts from a raw trace with no index. A seven-Move Game that fails on the fifth reports as a locator error on a square, with the Ply recoverable only from the trace.
Solution
One hardening pass over the browser-seam suite that makes CI test what it says it tests, makes a missing
awaita lint failure, and converts the suite's remaining implicit budgets into stated decisions.Two of the eight changes fix defects. The rest are cheap, and each one replaces a silent default with something a reader can see and argue with.
After this, a Player-visible regression that exists only in the built bundle fails CI; a dropped
awaitfails the five-command gate; every timeout that governs cross-browser propagation is named in the config next to the reason for its value; and a failing run on CI arrives with an indexed report, a trace, a recording per Player, and a step name identifying which Ply broke.User Stories
awaitbefore an assertion to fail lint, so that the suite cannot go green while silently asserting nothing.pnpm lint,pnpm typecheckandpnpm format:check, so that the hardening pass does not itself break the gate.Implementation Decisions
Scope is
apps/e2e, the Turbo task graph, the CI workflow, and the root lint configuration. No front-end, API or engine code changes. No test is added, removed or renamed; one existing test gains step boundaries.The built-artifact guarantee is enforced twice, deliberately. The Turbo
test:slowtask declares a passthrough for the target env var, which is the actual fix — it makes the workflow's existing declaration work. The runner config then throws when it is running under CI and has not been told to serve the built artifact. The first makes it correct; the second makes the whole class of failure loud rather than silent, and it follows the precedent already set in that file, where an unset port throws with a message naming the fix rather than defaulting. This shape came from checking the option against the running task graph:The task sets
cache: false, sopassThroughEnvis correct here andenv— which would additionally fold the variable into a cache key — is not needed.The missing-
awaitgap is closed with type-aware linting, workspace-wide. This requires adding theoxlint-tsgolintpackage as a dev dependency and enabling type-aware rules in the root lint configuration. Verified against the pinned oxlint: with the package installed and type-aware rules on,typescript/no-floating-promisesreports an unawaited promise-returning call; without it, the same code reports clean. Prefer enabling it through the root configuration'stypeAwareoption rather than adding a flag to each package's lint script, so the setting lives in one place; if that does not apply to per-package runs, fall back to the flag and note why. Expect the gate to get slower — that is the cost being accepted, and it buys the same protection for the engine and API.Timeouts become named constants next to the ones already there. The runner config already names its per-test and server-start budgets as constants with comments. The cross-browser propagation budget (the assertion timeout) and the total suite ceiling (the global timeout) join them in the same style. The propagation budget is the one that governs how long a Move has to reach the other Player's browser, and its comment should say so — it is the number that decides whether a slow fan-out reads as a failure.
The poll holding the Move request open gets an explicit timeout, comfortably larger than the assertions it guards, so that the test's own budget is stated rather than inherited from the assertion default. This is the test asserting that a Move is applied locally before the Room has answered.
The CI job gets a time limit, and the reporter becomes environment-dependent: the existing terminal reporter locally, and that reporter plus an HTML report that does not try to open a browser on CI. The report directory joins the existing artifact upload so a failure's report, trace and per-Player recordings arrive together. The report directory is already ignored by version control.
Each Move in the checkmate Game becomes a named step, labelled by its Ply and the squares it joins, so a failure names the Move. This pulls a runner import into a spec that currently takes everything from the suite's fixtures module; that is acceptable and should be the only such import.
The worker count becomes explicit with a comment recording what it balances. The value matters less than the fact that it is chosen: a test here holds two browser contexts, so the real cost per worker is double what the default assumes. Note that the official guidance is genuinely self-contradictory on this point — one page recommends a single worker on CI for reproducibility, another recommends parallelism — so the comment should record the local reasoning rather than cite a rule.
Explicitly unchanged: the retry count stays at zero, and the reason already recorded in the config still holds — a retry lets a flake report green, and this suite is the refactor's only oracle. The suite stays Chromium-only. Setup continues to go through the user interface, per this package's own standards. None of these is revisited here.
Testing Decisions
A good test here asserts what a Player can perceive and nothing about how the app is built — this package's standards already govern that, and this ticket does not relax them. The suite's existing tests are the prior art and the shape to preserve.
This ticket adds no new test, and that is the decision, not an omission. Seven of the eight changes are configuration; their correctness is observable in the gate and in a single suite run, and a test asserting that a config literal holds a particular value would assert the implementation rather than a behaviour.
The eighth change is itself the test. The guard in the runner config is the verification for the built-artifact guarantee: it converts a silent misconfiguration into a startup failure on every CI run, which is stronger and cheaper than a test that would have to reproduce the task graph to observe the same thing. It follows the existing precedent in that file, where a missing port throws rather than defaulting.
The floating-promise change is verified by the gate. Its correctness claim is "lint now fails on an unawaited assertion", which
pnpm lintdemonstrates directly. The implementer should confirm both directions before considering it done — that a deliberately unawaited call fails, and that the suite as it stands passes — and should not leave the deliberately-broken version behind.No new seam is introduced. The browser suite remains the only browser-driving seam in the repo, and it stays outside the five-command gate and outside the pre-commit hook, so none of this starts a browser during a commit.
pnpm test:slowremains the way to run it.Acceptance is the five commands green, plus one full browser-suite run passing with the new timeouts and step boundaries in place, and the report and artifacts landing where the workflow expects them.
Out of Scope
merge-reports. The suite is three tests; sharding solves a problem it does not have.retryStrategy, which is inert while retries are zero.Further Notes
This spec comes from a full audit of the suite against Playwright's official guidance for the pinned version and against seven open-source reference suites. The audit is committed alongside this work and holds the criteria checklist, the benchmark table of contested practices, and the per-criterion assessment with evidence. It scored the suite 42 compliant, 9 partial, 7 violated.
Three facts behind this ticket were established by experiment rather than read from documentation, because the documentation is silent or secondary sources disagree:
CIvariable does pass through, which is why the local-versus-CI server decision is correct today and only the target is lost.Two things the suite already does are better than the common advice and must not be "tidied" while working nearby. Recording is requested per browser context rather than through the runner's video option, because that option reaches only contexts the runner creates itself — setting it in the config would record nothing and report no error. And the rule that a behaviour spanning two Players is asserted in both browsers is what makes the suite able to catch a broken fan-out at all. Both are documented in this package's standards; leave them alone.
The audit also flagged a conflict worth knowing about while working here: the single broadest consensus in the research — seed state through the API, not the interface — runs against this package's written standard, which mandates interface-driven setup so that setup and assertion share one seam. That standard stands and this ticket does not touch it, but an implementer who "improves" setup by adding a back door would be violating it.
mentioned in commit
0f6e3d5460mentioned in commit
9579723a3fmentioned in commit
ddc04aee28