Files
pl_mail/.github
Paul LütznerandClaude Opus 5 af7414683f Run the browser suite in quarters instead of in a queue
One job, one two-core runner, PHPStan then PHPUnit then ~690 browser
tests in that order. Everything waited for everything, and the part that
takes the time waited on two static checks that say nothing about
whether a page renders.

It is six jobs now: `image` builds once so the rest start from a warm
layer cache and no two of them race to write the same scope, `static`
runs PHPStan and PHPUnit beside the others rather than in front of them,
four `browser` shards divide the suite with Playwright's own `--shard`,
`browser-exclusive` takes the install-wide specs, and `report` merges
their blobs into the one HTML report a person downloads.

A STACK PER JOB IS THE POINT, not a side effect. A single app container
was already the ceiling on worker count, so eight browsers against one
of them would have bought nothing. It is also what frees the four specs
that own install-wide state — the AiSettings singleton, the integration
provider rows, the Mercure hub one of them restarts. They ran last,
alone, in one worker, behind everything else; they were never slow, they
were serialised, because there was one installation to collide over. On
a runner of their own there is nothing left to collide with, which is
why the job passes --no-deps: the project's `dependencies` on the main
one is exactly right for a developer running against ONE stack, and here
would only mean running the whole suite twice.

The setup those six jobs each repeat is now one composite action rather
than six copies of eighty lines, which is the argument compose.test.yaml
already makes about owning DATABASE_URL.

  measured, 4 workers, whole suite 4m55s

    1/4  168 tests  2.8m   account-*, admin-*, appearance*, calendar*
    2/4  169 tests  1.3m
    3/4  155 tests  1.4m
    4/4  137 tests  0.7m
    exclusive  23   1.1m

The numbers are in the matrix comment because they are the argument for
whatever the count becomes next: Playwright divides by test COUNT and
the counts are even, while the durations are not — the first quarter
holds the specs that navigate and measure layout. A run is as fast as
its slowest shard, so that block is where the next gain is, and more
shards is the only lever since there is no duration-aware split.

Two things fell out.

A pull request stops paying for superseded runs — a second commit
cancels the first one's. Tags are never cancelled; a release is the run
whose answer gets kept.

And admin-card-insets.spec.ts has been failing since v0.2.15, taking two
minutes and a CI retry to do it. It waits for a section to finish
loading by counting spinners left in the frame, and counts elements
rather than visible ones — so when the AI card grew a "Warm up now"
button whose spinner sits beside its icon, rendered and hidden, waiting
for a click, the wait started looking for something that is never
removed. Ten sections, each spending the timeout on a frame that had
loaded perfectly. Asking for visible spinners takes that test from a
120-second failure to 4.7 seconds passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 22:47:32 +02:00
..