Skip to content

ci: give Jest shards CI-only timeout headroom to unblock promotion - #2079

Merged
steilerDev merged 2 commits into
betafrom
fix/2076-searchpicker-fake-timers
Sep 7, 2026
Merged

ci: give Jest shards CI-only timeout headroom to unblock promotion#2079
steilerDev merged 2 commits into
betafrom
fix/2076-searchpicker-fake-timers

Conversation

@steilerDev

@steilerDev steilerDev commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Summary

Unblocks the v2.15.0 promotion (#2075), whose Quality Gates has been failing with 60-second test timeouts in picker-family suites. This is a stopgap, not a root-cause fix — see "What this does not do".

Three hypotheses were falsified by measurement before the cause was found. Recording them so they don't get re-proposed:

Hypothesis How it died
The dev-dependencies bump (user-event 14.6.1 -> 14.6.7) Bisection on #2070 reproduced identical failures on the old versions; a binary diff of the two tarballs found no hot-path change (wait.js byte-identical, delay: 0 unchanged)
Fake timers beta already ships SearchPicker.test.tsx and HouseholdItemPicker.test.tsx fully converted — both still timed out
Worker contention In shard 5, the other 77 suites finished at 19:23:29 and SearchPicker.test.tsx then ran alone on the runner until 19:49:23 — 25m54s with zero contention — and still blew the ceiling

Actual cause: a machine-speed gap

GitHub-hosted runners are ~1.8x slower single-threaded than the dev boxes the 60 s ceiling was calibrated on:

Suite CI Local Ratio
SearchPicker.test.tsx 2326 s 1330 s 1.75x
WorkItemPicker.test.tsx 436 s 224 s 1.94x

At ~39 s/test in CI, the heaviest tests cross 60 s and the rest don't. That also explains why the failing set looked arbitrary — near-identical tests split across the boundary (line 307 failed while line 323 passed), and why it worsened monotonically (8 -> 26 -> 45 min) with no code change: it tracks runner speed, not the tree.

Changes

  • .github/workflows/ci.yml — append --testTimeout=240000 to the shard invocation only. Verified via --showConfig that the CLI flag lands in globalConfig, dodging the projects[].testTimeout-is-silently-ignored trap that jest.config.ts documents. jest.config.ts stays at 60000 so it remains a tight dev-side regression detector. Also adds a runner-capacity diagnostic step and a comment block recording all rejected levers with evidence.
  • scripts/ci-wait.sh — raise defaults to 2400 s (beta) / 3600 s (main). The old 600/900 s were below even a healthy run: Quality Gates on release: promote beta to main #2075 took 40m33s, so the script reported TIMEOUT ~25 min before a verdict existed, reading as a CI fault rather than "still running".
  • WorkItemPicker.test.tsx — convert all 10 real-timer userEvent.setup() call sites to fake timers.
  • HouseholdItemPicker.breadcrumb.test.tsx — convert the 6 sites a468c0cf left behind (it did 4 of 10). Partial conversion is what let failures migrate between files.
  • jest.config.ts + agent memory — correct text that asserted a mechanism the measurements refuted. The memory file had over-claimed twice already; this stops a third.

SearchPicker.test.tsx is not in this diff — a468c0cf converted it on beta and that version was kept during the rebase.

Verification

  • WorkItemPicker.test.tsx: 14/14 passing, twice (221.9 s, 220.5 s)
  • HouseholdItemPicker.breadcrumb.test.tsx: 10/10 passing, twice
  • No assertion weakened — every expect(...) byte-identical to before; test counts unchanged; no .skip/.only; verified mechanically, not by eye
  • No production code touched
  • Lint/format clean on all touched files
  • scripts/check-trailers.sh passes

What this does not do

It does not make anything faster. Shard 5 will still take ~40 min. The ~21 s per-test CPU cost is exactly as unexplained as before — two mechanism guesses were falsified by measurement (pointerEventsCheck: Never removes only ~9%; coverage instrumentation only 0.3%).

Do not read a green Quality Gates as evidence the slowness was solved. It was only given room. #2078 owns the root cause; next step is node --cpu-prof on a single dropdown test.

pointerEventsCheck: Never was also rejected on correctness grounds independent of its measurement: it disables user-event's pointer-events: none check, so a test could pass against a control a real user cannot click.

Refs #2076
Refs #2078

🤖 Generated with Claude Code

Converts all 57 real-timer `userEvent.setup()` call sites in
SearchPicker.test.tsx (47) and HouseholdItemPicker.breadcrumb.test.tsx (10)
to the fake-timer idiom via a local `setupUser()` helper, targeting the 11
60-second timeouts that block the v2.15.0 promotion.

Applied as an experiment with a verified-safe payload, not as a proven fix.
A local unloaded A/B on the full SearchPicker file showed no speedup
(1331.6s unconverted vs 1290.7s / 1352.0s converted) and the unconverted
file passes locally, so the sandbox never reproduced the CI failure. The
~21s per-dropdown-test cost is CPU-bound and remains unexplained; the
"in-file fake-timer control group proves immunity" inference is withdrawn
and recorded as withdrawn in agent memory.

The dependency bump named in #2076 is ruled out twice: a pre-bump
jest/testing-library bisection on #2070 reproduced identical failures on
the old versions, and a binary diff of the user-event 14.6.1/14.6.7
tarballs found no hot-path change (utils/misc/wait.js byte-identical,
delay:0 unchanged).

No production code changed. All 173 assertions and both test counts are
byte-identical; `testTimeout: 60000` is unchanged (comment-only edit),
since ~592 real-timer call sites remain across the client suite.

Fixes #2076

Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
… tests

Quality Gates was failing on the v2.15.0 promotion (#2075) with 60s test
timeouts in picker-family suites. Three hypotheses were falsified by
measurement before landing on the cause:

- The dev-dependencies bump: bisection on #2070 reproduced identical
  failures on the OLD jest/testing-library versions, and a binary diff of
  the two user-event tarballs found no hot-path change.
- Fake timers: beta already ships SearchPicker.test.tsx and
  HouseholdItemPicker.test.tsx fully converted, and BOTH still timed out.
- Worker contention: in shard 5 the other 77 suites finished at 19:23:29
  and SearchPicker.test.tsx then ran alone on the runner until 19:49:23 --
  25m54s with zero contention -- and still blew the ceiling.

The actual cause is a machine-speed gap: GitHub-hosted runners are ~1.8x
slower single-threaded than the dev boxes the 60s ceiling was calibrated
on (SearchPicker 2326s CI vs 1330s local; WorkItemPicker 436s vs 224s).
At ~39s/test the heaviest tests cross 60s and the rest do not.

- Raise --testTimeout to 240000 in the CI shard invocation only. Verified
  via --showConfig that the CLI flag lands in globalConfig, dodging the
  projects[].testTimeout trap. jest.config.ts stays at 60000 so it remains
  a tight dev-side regression detector.
- Record every rejected lever with its evidence in ci.yml so they are not
  re-proposed, and add a runner-capacity diagnostic step.
- Raise scripts/ci-wait.sh defaults (2400s beta / 3600s main). The old
  600s/900s were below even a healthy run and reported TIMEOUT ~25min
  before a verdict existed.
- Convert WorkItemPicker.test.tsx (10 sites) and the remaining 6 sites in
  HouseholdItemPicker.breadcrumb.test.tsx to fake timers. This removes a
  real wall-clock dependency but is explicitly NOT the fix.
- Correct jest.config.ts and agent memory, which asserted a mechanism the
  measurements refuted.

This is a stopgap. The ~21s per-test CPU cost is unexplained and stays
open in #2078; next step is node --cpu-prof on a single dropdown test. A
green Quality Gates is not evidence the slowness was solved.

Refs #2076
Refs #2078

Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@steilerDev
steilerDev force-pushed the fix/2076-searchpicker-fake-timers branch from 36b12c3 to d3ed005 Compare September 7, 2026 20:10
@steilerDev steilerDev changed the title test(searchpicker): convert real-timer userEvent tests to fake timers ci: give Jest shards CI-only timeout headroom to unblock promotion Sep 7, 2026
@steilerDev
steilerDev merged commit e3ccf55 into beta Sep 7, 2026
33 checks passed
@steilerDev
steilerDev deleted the fix/2076-searchpicker-fake-timers branch September 7, 2026 20:54
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.15.0-beta.7 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.15.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant