Skip to content

Shard the backend suite across three runners - #547

Merged
Babissimo merged 1 commit into
mainfrom
ci/shard-backend-suite
Sep 23, 2026
Merged

Babissimo merged 1 commit into
mainfrom
ci/shard-backend-suite

Conversation

@Babissimo

@Babissimo Babissimo commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Shards backend-tests across three runners and applies the coverage threshold once, in a new backend-coverage job behind them.

ClickUp: Shard the backend suite across two runners and split the dashboard tests from its lint and build

Measured effect, across five runs

gate satisfied at
main #514 start+134 s
main #539 start+138 s
main #534 start+142 s
this PR, run 1 start+121 s
this PR, run 2 start+105 s

About 25 s off the median, not the ~55 s the ticket projected. Single runs vary by 15 s or more either way, so the two sharded runs are a range rather than a figure.

The projection assumed the 107 s pytest step divides by the shard count. It does not: every shard collects all 4656 tests, imports the app in four xdist workers and starts coverage, and none of that divides. Fitting the two measured points, 107 s unsharded against 67 s for a third of the tests, puts the per-shard fixed cost near 47 s and the divisible work near 60 s. A fourth shard would buy about 5 s and no number of shards takes the pytest step below ~47 s, so the remaining lever is that fixed cost, not more runners.

One consequence worth recording: in run 2 the backend path finished at 94 s and Web (dashboard) set the gate at 105 s. The two are now comparable, so the ticket's other half is on the critical path and does have something to buy, which was not true before this change.

Why

backend-tests is the longest gate on every PR and the first thing every deploy waits for. Three shards rather than two because the combine job runs serially after them and costs about 16 s whatever the count; past three the per-shard fixed cost dominates and a fourth buys very little.

The coverage seam

A shard measures a third of the suite, so it cannot apply --cov-fail-under to its own figure. Each overrides it to 0 and uploads its data fragment; backend-coverage combines the three and applies the 55 once. pyproject.toml keeps the number, so a local pytest enforces the gate exactly as before.

The failure mode here is quiet rather than red: a combine that reads fewer fragments than ran reports a plausible figure over part of the suite. Hence the fragment count in the job, and tests/test_backend_coverage_gate.py, which pins that the shards disclaim the threshold, that the combine applies the number addopts names, that the number is not zero, that a deploy waits on it, and that the temp-path omit still matches a real path.

The reported coverage figure moves, 88% to 93%

This looks like the gate weakening and is the opposite. main reports 15460 statements at 88%; this PR reports 14171 at 93%. Every one of the 1289 statements in that gap is pytest temp-directory content that main counts and this PR omits:

  • 18 throwaway app.py files from tests/test_deploy_scope.py's temp git repos.
  • 28 files under tests/test_migrations.py's old_image, which shutil.copytrees core/ and migrations/ into a tmp_path. main therefore counts a second, almost entirely uncovered copy of the codebase, which is what was holding the figure down.

Nothing appears in this PR's report that is absent from main's, and the per-file statement counts agree for every shared file except the two #534 grew.

Why the omit was needed at all

tests/test_deploy_scope.py and tests/test_migrations.py run subprocesses whose cwd is a tmp_path. pytest-cov exports COVERAGE_PROCESS_START, so those subprocesses measure under the same config, and source = ["."] resolves against their cwd. Harmless while the report runs in the same job, where the paths still exist. Fatal to a combine job on another runner: coverage report stops at No source for code and exits non-zero. It failed exactly this way before the fix.

Two details that are easy to get wrong, and why the test checks the glob against a real filename rather than by string match:

  • * does not cross a path separator, so *pytest-of-* does not match.
  • It has to be in [report], not [run]. [run] omit filters what is measured, and these files are added back by the source walk at report time. [report] omit is additive, so the existing [run] omissions still apply.

Verification

The shards partition the suite exactly: full collection 4656, groups of 1636 / 1277 / 1743 summing to 4656, no pairwise overlap, nothing unassigned. On CI the three shards ran 4716 tests between them against main's 4710, the difference being the six added here.

Locally, an unsharded run and the three shards combined both report 14171 statements and 4716 passed; only the missed count differs, 981 against 984, being three lines covered solely because the whole suite shared one session.

pre-commit run --all-files green, and the workflow-reading tests at 96 passed, including the cases #546 added.

🤖 Generated with Claude Code

@claude

This comment has been minimized.

`backend-tests` is the longest gate on every PR and the first thing every
deploy waits for: 129s, of which a single pytest invocation is 107s. Its
setup is only 18s, so splitting the tests across runners divides nearly all
of it, and public-repo runners are free.

Three shards rather than two because the combine job runs serially after
them and costs about 18s whatever the count, which would leave two at ~98s.
Past three the other gate jobs set the run's floor and a fourth buys
nothing.

The threshold is the awkward part. A shard measures a third of the suite,
so it cannot apply `--cov-fail-under` to its own figure; each overrides it
away and uploads its coverage fragment, and the new `backend-coverage` job
combines the three and applies the 55 once. `pyproject.toml` keeps the
number, so a local `pytest` still enforces the gate exactly as it did. The
failure mode here is quiet rather than red: a combine that reads fewer
fragments than ran reports a plausible figure over part of the suite. Hence
the fragment count in the job, and `tests/test_backend_coverage_gate.py`
pinning that the shards disclaim the threshold, that the combine applies
the number `addopts` names, that the number is not zero, that a deploy waits
on it, that the omit below still matches a real temp path, and that the
combine job reads with the coverage version the shards wrote with.

Reporting on a different runner from the one that measured is what the new
`[tool.coverage.report] omit` entry is for. pytest-cov exports
`COVERAGE_PROCESS_START`, so a subprocess a test spawns measures under this
config, and `source = ["."]` resolves against that subprocess's cwd:
`tests/test_deploy_scope.py` runs `deploy-scope.py` inside a throwaway git
repo and its `app.py` lands in the data as an unrun file. That was harmless
while the report ran beside the tests that made it, and fatal once it does
not, because `coverage report` stops at "No source for code".

pytest-split cuts the tests into contiguous chunks of equal recorded
duration rather than bin-packing them into balanced ones, so each file's
tests stay together and in collection order. This suite has tests that pass
or fail on what shares their process, and the least disturbance to that is
the split worth having.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Babissimo
Babissimo force-pushed the ci/shard-backend-suite branch from 749e1db to 66cc8d2 Compare September 23, 2026 09:55
@claude

claude Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Claude finished @Babissimo's task in 22m 28s —— View job


Tasks

  • Gather context (PR diff, CLAUDE.md, custom instructions)
  • Run /code-review skill against this PR (8 review angles: correctness, removed behavior, cross-file tracing, reuse, simplification, efficiency, altitude/design, CLAUDE.md conventions)
  • Compile and post findings

Review of PR #547 (updated pass, commit 66cc8d2)

Since the previous review on this branch, test_the_combine_job_reads_with_the_coverage_the_shards_wrote_with was added to backend/tests/test_backend_coverage_gate.py — the coverage-version-pin drift gap flagged last time is now closed. Good.

1. Still open from the previous pass: required status checks can't be verified from the repo, and this diff makes it easy to get wrong
backend-tests used to be a single check name; it's now a 3-leg matrix (backend-tests (1)/(2)/(3)) plus a brand-new backend-coverage check (.github/workflows/ci.yml:47-232). staging's needs: correctly lists both (ci.yml:670), but GitHub's branch-protection "required status checks" list lives outside this repo and can't be confirmed from the diff. If it still names only the old bare backend-tests, either merges stall waiting on a context that never reports again, or — if GitHub tolerates a missing required context — a coverage regression could merge to main via the button and only get caught later when staging blocks the deploy. Worth a direct look at the ruleset before merging.

2. .github/workflows/ci.yml:63, 182, 226 — shard count 3 is hardcoded independently three times
matrix: shard: [1, 2, 3], --splits 3, and -ne 3 all have to agree, with no GH Actions expression to derive one from another. test_every_shard_that_runs_is_a_shard_the_combine_job_waits_for in backend/tests/test_backend_coverage_gate.py:64-69 does cross-check these against each other, so drift is caught in CI today — but it depends on that specific test staying in the suite (e.g. never quarantined/marked xfail). --splits ${{ strategy.job-total }} would remove one of the three hardcodes structurally rather than via a test.
Fix this →

3. Still open from the previous pass: omit = ["*/pytest-of-*/*"] (backend/pyproject.toml:51) works around the subprocess-measurement problem at report time rather than stopping it at the source
The two subprocess.run(...) calls in tests/test_deploy_scope.py inherit COVERAGE_PROCESS_START from pytest-cov, so deploy-scope.py — never meant to count toward the backend coverage gate — gets measured anyway, and the tmp-path glob patches over the resulting "No source for code" failure in backend-coverage. Clearing that env var for those two subprocess calls would stop the measurement at its origin instead of pattern-matching it away later, and would no longer depend on every future tmp-path shape (a --basetemp override, a tmp_path_factory.mktemp() call, a self-hosted runner) matching a 3-segment glob. test_a_tmp_path_fixture_is_omitted_from_the_report (backend/tests/test_backend_coverage_gate.py:97) also only pins the shallow test_deploy_scope.py shape, not test_migrations.py's deeper old_image/core/... nesting — a narrowing edit to the glob could pass that test and CI's own suite while still breaking the combine job on a test_migrations.py-exercised PR.

4. Non-blocking: real but unaddressed duplicated compute in backend-tests
Each of the 3 shards repeats checkout+submodules+dependency install (~14s) and full-suite collection (~16.6s before any of that shard's own tests start) — roughly 42s and 50s of triplicated work respectively, measured against actual run timings. This doesn't lengthen the PR gate's wall-clock (shards run concurrently), and the PR's own comments already address the combine job's cost, but neither the checkout/install duplication nor pytest-split's post-collection filtering is discussed. Not a correctness issue, just worth knowing if a future pass wants to shave shard startup further (e.g. a shared setup step, or filtering to each shard's file list before invoking pytest instead of collecting all 4656 items and discarding two-thirds).

5. Minor: backend/tests/test_backend_coverage_gate.py quality nits

  • _declared_threshold() (line 41) re-reads and re-parses pyproject.toml from disk on every call — it's invoked from two tests, and a third test parses it again independently at line 103. Turning it into a module-scoped fixture (mirroring the jobs fixture at lines 19-21) would parse once.
  • Line 69, assert f"-ne {len(shards)} " in counted[0], is a whitespace-sensitive raw substring match instead of reusing the file's own _threshold() regex helper (line 31) that every other numeric check uses — a harmless reformat of the bash guard in ci.yml would break it for no reason connected to what the test claims to guard.
  • Five call sites (lines 26-28, 59-61, 67-69, 75-77, 89-91) repeat the same "filter steps, assert exactly one match" shape; a small _one_step(steps, predicate, label) helper would remove the duplication (no actual bug found across the five — each predicate correctly targets its own job/field).
  • .github/workflows/ci.yml:189, path: backend/.coverage.${{ matrix.shard }}, restates the COVERAGE_FILE env var already defined at line 76 instead of referencing backend/${{ env.COVERAGE_FILE }}.

Checked and correct, not flagging:

  • Coverage figure moving 88%→93% (explained fully in the PR body; verified the [run] omit vs [report] omit split is a legitimate coverage.py distinction, not duplication).
  • fail-fast: false + no if: always() on upload-artifact means a failed shard can't contribute a fragment, so "3 fragments found" can't pass on an incomplete run.
  • needs:/skip-propagation from backend-tests → backend-coverage → staging, and the uses:-step path-prefixing around defaults.run.working-directory, are wired correctly.
  • No CLAUDE.md convention violations in the changed files.
    · Branch: ci/shard-backend-suite

@Babissimo

Copy link
Copy Markdown
Contributor Author

Thanks. Taken one, answered three.

1. Duplicated coverage pin, unguarded. Agreed, fixed. It was the one seam in that file left unpinned while the file's whole purpose is pinning seams. test_the_combine_job_reads_with_the_coverage_the_shards_wrote_with now reads the version out of the backend-coverage install step and compares it to requirements-dev.txt. Mutation-checked: bumping the requirements pin alone turns it red.

2. Clearing COVERAGE_PROCESS_START at the call sites. Not taking this, on two grounds.

The suggestion is based on an incomplete picture of where the measurement comes from. It names the subprocess.run(...) calls in tests/test_deploy_scope.py, but the larger source is tests/test_migrations.py:307, which runs alembic with env = os.environ | {...} and cwd=old_image after shutil.copytreeing core/ and migrations/ there. That one accounts for 28 of the 46 affected files and roughly 1200 of the 1289 statements. Between the two files there are five subprocess.run call sites, not two, and a sixth added later would silently reintroduce the problem. A report-time denylist covers call sites nobody remembers to update; a source-level fix has to be repeated at each.

On the glob being "anchored to one directory depth": it isn't, and this run demonstrates it. Locally it matches /private/var/folders/.../pytest-of-al/pytest-31/test_x0/app.py, three segments deep, and on this PR's CI run it matched /tmp/pytest-of-runner/pytest-0/popen-gw1/test_x0/app.py, four segments deep, which is why backend-coverage passed at all. The residual risk you describe is real but needs someone to set --basetemp, and it announces itself loudly: the combine job fails with No source for code and blocks the deploy. That is the failure mode worth having.

3. Required status checks. Checked before writing the change: there are none. repos/offworldlabs/retina-server/branches/main/protection returns 404 and repos/offworldlabs/retina-server/rulesets is empty, so no check name is required for merge and renaming backend-tests into matrix legs breaks nothing. staging's needs is the only gate that matters, and it lists backend-coverage.

4. Lighter toolchain in backend-coverage. Keeping setup-python, and here is why it is load-bearing. The job does not only merge data files: coverage report re-parses every source file to count statements, using the running interpreter's tokeniser. Pinning it to 3.12 keeps the reporter's parse identical to the shards' and stops the reported total drifting with whatever the runner image ships. setup-uv costs about 2 s and matches how every other job here installs. Not worth the couple of seconds.

@Babissimo
Babissimo merged commit 61e3b94 into main Sep 23, 2026
33 checks passed
@Babissimo
Babissimo deleted the ci/shard-backend-suite branch September 23, 2026 10:20
Babissimo added a commit that referenced this pull request Sep 23, 2026
With the backend suite sharded (#547), Web (dashboard) became the job that sets the gate: in #547's second run the backend path finished at 94 s and the dashboard at 105 s. It runs lint (13 s), typecheck (7 s), tests (39-62 s) and build (5 s) one after another, so the tests wait on 25 s of work they do not depend on.

The tests now have their own matrix entry, beside one for lint, typecheck and build. On a slow runner that entry is about 13 s of setup plus the 60 s test step, which puts it below the backend path in both of #547's runs and takes the dashboard off the critical path. The other workspaces keep one entry each; their scripts take 20 s together and splitting them would buy nothing.

Splitting rather than sharding with vitest --shard, because the split alone is enough while the backend path stays above ~75 s, and sharding would add a runner for no gain today. Unlike pytest, vitest's cost does divide by shard: its reported environment, import and test times are summed over three workers (about 162 s against a 61 s wall), and a shard loads only its own files. So --shard is the next step if the backend path ever drops below this entry.

The job keeps its name, web-build, so staging's needs list is unchanged. Which entry runs which script is now decided by step conditions, and a mistake there fails nothing: a script left out runs nowhere, and one run twice silently undoes the split. tests/test_web_build_matrix.py pins that every workspace in package.json has an entry, that each script it defines runs in exactly one, that the dashboard's test entry runs nothing else, and that a split entry's name says which half it is. All nine mutations tried against the workflow turn it red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Babissimo added a commit that referenced this pull request Sep 23, 2026
With the backend suite sharded (#547), Web (dashboard) became the job that
sets the gate: on main's run for #547 the backend path finished at 95 s and
the dashboard at 119 s. It runs lint (13 s), typecheck (7 s), tests (39-75 s)
and build (5 s) one after another, so the tests wait on 25 s of work they do
not depend on.

The tests now have their own matrix entry, beside one for lint, typecheck
and build. On a slow runner that entry is about 13 s of setup plus the test
step, which is below the backend path and takes the dashboard off the
critical path. The other workspaces keep one entry each; their scripts take
20 s together and splitting them would buy nothing.

Splitting rather than sharding with vitest --shard, because the split alone
is enough while the backend path stays above this entry, and sharding would
add a runner for no gain today. Unlike pytest, vitest's cost does divide by
shard: timed at three workers, the whole suite took 100 s and each third
37-41 s, which fits about 8 s fixed against 92 s divisible. So --shard is
the next step if the backend path ever drops below this entry.

The job keeps its name, web-build, so staging's needs list is unchanged.
Which entry runs which script is now decided by step conditions, and a
mistake there fails nothing: a script left out runs nowhere, one run twice
silently undoes the split, and the two halves trading conditions keeps
every count right while the tests land back on the checks entry.
tests/test_web_build_matrix.py pins that every workspace in package.json has
an entry, that each lint, typecheck, test and build script it defines runs
in exactly one, that the dashboard is split in two, that each half runs only
its own scripts, and that a split entry's name says which half it is. All
eleven mutations tried against the workflow turn it red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Babissimo added a commit that referenced this pull request Sep 23, 2026
Three follow-ups to #547, where the shards' coverage is combined and the threshold applied on a runner that measured nothing.

The omit test checked only test_deploy_scope.py's shallow tmp_path shape, a repo's top-level app.py. test_migrations.py also puts measured files in the report, a copied core/ two levels deeper and under an xdist worker's popen-gwN directory. A glob narrowed to a fixed depth, or to the serial basetemp layout, still matched the shallow path and would have passed while backend-coverage died on "No source for code". The test now asserts both shapes; both of those narrowings turn it red.

--splits named the shard count as a literal beside the matrix list it has to equal. It now reads strategy.job-total, so the matrix alone sets the count, and the test pins that --splits and --group follow the matrix rather than comparing two literals.

The fragment count was checked by a whitespace-sensitive substring, where the rest of the file parses numbers with _threshold(). It uses that now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Babissimo added a commit that referenced this pull request Sep 23, 2026
Three follow-ups to #547, where the shards' coverage is combined and the threshold applied on a runner that measured nothing.

The omit test checked only test_deploy_scope.py's shallow tmp_path shape, a repo's top-level app.py. test_migrations.py also puts measured files in the report, a copied core/ two levels deeper and under an xdist worker's popen-gwN directory. A glob narrowed to a fixed depth, or to the serial basetemp layout, still matched the shallow path and would have passed while backend-coverage died on "No source for code". The test now asserts both shapes; both of those narrowings turn it red.

--splits named the shard count as a literal beside the matrix list it has to equal. It now reads strategy.job-total, so the matrix alone sets the count, and the test pins that --splits and --group follow the matrix. job-total counts every combination, so the test also pins that shard is the matrix's only axis: a second one would multiply --splits while --group still spanned only the shards, leaving part of the suite unrun.

The fragment count was checked by a whitespace-sensitive substring, where the rest of the file parses numbers with _threshold(). It uses that now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Babissimo added a commit that referenced this pull request Sep 23, 2026
With the backend suite sharded (#547), Web (dashboard) became the job that
sets the gate: on main's run for #547 the backend path finished at 95 s and
the dashboard at 119 s. It runs lint (13 s), typecheck (7 s), tests (39-75 s)
and build (5 s) one after another, so the tests wait on 25 s of work they do
not depend on.

The tests now have their own matrix entry, beside one for lint, typecheck
and build. On a slow runner that entry is about 13 s of setup plus the test
step, which is below the backend path and takes the dashboard off the
critical path. The other workspaces keep one entry each; their scripts take
20 s together and splitting them would buy nothing.

Splitting rather than sharding with vitest --shard, because the split alone
is enough while the backend path stays above this entry, and sharding would
add a runner for no gain today. Unlike pytest, vitest's cost does divide by
shard: timed at three workers, the whole suite took 100 s and each third
37-41 s, which fits about 8 s fixed against 92 s divisible. So --shard is
the next step if the backend path ever drops below this entry.

The job keeps its name, web-build, so staging's needs list is unchanged.
Which entry runs which script is now decided by step conditions, and a
mistake there fails nothing: a script left out runs nowhere, one run twice
silently undoes the split, and the two halves trading conditions keeps
every count right while the tests land back on the checks entry.
tests/test_web_build_matrix.py pins that every workspace in package.json has
an entry, that each lint, typecheck, test and build script it defines runs
in exactly one, that the dashboard is split in two, that each half runs only
its own scripts, and that a split entry's name says which half it is. All
eleven mutations tried against the workflow turn it red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Babissimo added a commit that referenced this pull request Sep 23, 2026
Three follow-ups to #547, where the shards' coverage is combined and the threshold applied on a runner that measured nothing.

The omit test checked only test_deploy_scope.py's shallow tmp_path shape, a repo's top-level app.py. test_migrations.py also puts measured files in the report, a copied core/ two levels deeper and under an xdist worker's popen-gwN directory. A glob narrowed to a fixed depth, or to the serial basetemp layout, still matched the shallow path and would have passed while backend-coverage died on "No source for code". The test now asserts both shapes; both of those narrowings turn it red.

--splits named the shard count as a literal beside the matrix list it has to equal. It now reads strategy.job-total, so the matrix alone sets the count, and the test pins that --splits and --group follow the matrix. job-total counts every combination, so the test also pins that shard is the matrix's only axis: a second one would multiply --splits while --group still spanned only the shards, leaving part of the suite unrun.

The fragment count was checked by a whitespace-sensitive substring, where the rest of the file parses numbers with _threshold(). It uses that now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Babissimo added a commit that referenced this pull request Sep 23, 2026
… gaps (#549)

* ci: run the dashboard's tests in a web-build entry of their own

With the backend suite sharded (#547), Web (dashboard) became the job that
sets the gate: on main's run for #547 the backend path finished at 95 s and
the dashboard at 119 s. It runs lint (13 s), typecheck (7 s), tests (39-75 s)
and build (5 s) one after another, so the tests wait on 25 s of work they do
not depend on.

The tests now have their own matrix entry, beside one for lint, typecheck
and build. On a slow runner that entry is about 13 s of setup plus the test
step, which is below the backend path and takes the dashboard off the
critical path. The other workspaces keep one entry each; their scripts take
20 s together and splitting them would buy nothing.

Splitting rather than sharding with vitest --shard, because the split alone
is enough while the backend path stays above this entry, and sharding would
add a runner for no gain today. Unlike pytest, vitest's cost does divide by
shard: timed at three workers, the whole suite took 100 s and each third
37-41 s, which fits about 8 s fixed against 92 s divisible. So --shard is
the next step if the backend path ever drops below this entry.

The job keeps its name, web-build, so staging's needs list is unchanged.
Which entry runs which script is now decided by step conditions, and a
mistake there fails nothing: a script left out runs nowhere, one run twice
silently undoes the split, and the two halves trading conditions keeps
every count right while the tests land back on the checks entry.
tests/test_web_build_matrix.py pins that every workspace in package.json has
an entry, that each lint, typecheck, test and build script it defines runs
in exactly one, that the dashboard is split in two, that each half runs only
its own scripts, and that a split entry's name says which half it is. All
eleven mutations tried against the workflow turn it red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: close three gaps in the backend coverage gate's pins

Three follow-ups to #547, where the shards' coverage is combined and the threshold applied on a runner that measured nothing.

The omit test checked only test_deploy_scope.py's shallow tmp_path shape, a repo's top-level app.py. test_migrations.py also puts measured files in the report, a copied core/ two levels deeper and under an xdist worker's popen-gwN directory. A glob narrowed to a fixed depth, or to the serial basetemp layout, still matched the shallow path and would have passed while backend-coverage died on "No source for code". The test now asserts both shapes; both of those narrowings turn it red.

--splits named the shard count as a literal beside the matrix list it has to equal. It now reads strategy.job-total, so the matrix alone sets the count, and the test pins that --splits and --group follow the matrix. job-total counts every combination, so the test also pins that shard is the matrix's only axis: a second one would multiply --splits while --group still spanned only the shards, leaving part of the suite unrun.

The fragment count was checked by a whitespace-sensitive substring, where the rest of the file parses numbers with _threshold(). It uses that now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Babissimo added a commit that referenced this pull request Sep 23, 2026
The shard split reads .test_durations, last written at #547, and group 3 now runs about 15% lighter than the other two. Serial, so no xdist worker's first test absorbs the session fixture setup, and under coverage as the shards run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Babissimo added a commit that referenced this pull request Sep 23, 2026
The shard split reads .test_durations, last written at #547, and group 3 now runs about 15% lighter than the other two. Serial, so no xdist worker's first test absorbs the session fixture setup, and under coverage as the shards run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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