test: centralize vitest testTimeout so packages can't inherit the 5s default #78

Merged
ullrich merged 1 commit from fix/centralize-vitest-timeout into main 2026-08-18 15:36:29 +00:00
Owner

Why

Renovate PRs keep going red for reasons unrelated to the dependency they bump. #75 is a pure lockfile bump (esbuild 0.28.1 → 0.28.2) and its only CI failure is packages/fit's gpx-to-fit-course test timing out at 5000ms — something a bundler patch release cannot cause.

The cost is one-time module loading, not test work. parseGpxAsync lazily await import("linkedom") and caches the parser in _LinkedDOMParser, so the whole import lands on whichever test runs first. That is why the smallest fixture is the one that fails:

✓ short-flat.gpx  (559 B)   61ms   ← pays the linkedom import
✓ alpine.gpx      (861 B)    3ms
✓ multi-day.gpx   (1.1 K)    3ms

A 20× penalty on iteration one only. Locally 61ms is invisible; on a contended runner the same one-time cost crosses 5s.

This was already fixed three times, once per package

Each as --testTimeout=20000 in a package.json test script, each after a renovate PR went red:

commit package stated reason
ee19578 packages/db cold drizzle/postgres import
805ac86 packages/gpx cold fit-parser import
f31eb26 apps/journal cold fedify/drizzle/react-router import

packages/fit never got the flag and stayed at the 5s default — even though 805ac86 was prompted by the gpx→fit round-trip, i.e. the same code path packages/fit tests. A fourth per-package flag just moves the whack-a-mole along.

So set it once in vitest.shared.ts, which every package's vitest.config.ts extends, and drop the three scattered copies. A new package can no longer be born at the 5s default and rediscover this.

Also set in the root vitest.config.ts: that file does not extend the shared one and is what pnpm test:watch uses, so without it watch mode sits at 5s while CI is at 20s — the same divergence pointing the other way.

Testing

Verified the mechanism rather than assuming it:

  • Reproduced CI's exact signature locally. --testTimeout=30 (short-flat needs 61ms, the others 3ms) yields 1 failed | 7 passed (8) with short-flat as the failure — the same shape as run 638. Confirms the timeout is the binding constraint and nothing else is wrong.
  • Proved the new value is inherited, with a probe test sleeping 6s — which the old default would have killed — passing in packages/fit. Removed afterwards.
  • Proved no regression for the three packages that lost their CLI flag: the same 6s probe passes in db, gpx and journal from the shared config alone.
  • Verified against #75 itself: cherry-picked onto renovate/esbuild-=0.27.3-0.28.1-0.x, installed its lockfile (esbuild 0.28.2 confirmed present), and ran the previously-failing test — 8/8 passed. Full suite on that branch: test 12/12, typecheck 14/14, lint 14/14, build 2/2.
  • On this branch: test 12/12, typecheck 14/14, lint 14/14.

What this does not fix

runs-on: playwright resolves to a runner-pinned image. When renovate bumps the @playwright/test package past it, visual tests die with:

browserType.launch: Executable doesn't exist at
/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell

Confirmed on #66 (job ran image=mcr.microsoft.com/playwright:v1.61.1-noble, PR installs playwright 1.62.1). Renovate can never make such a PR green while the image version lives in runner config and the package version in package.json — which is what #62 is probing. #66 needs both that and this.

After this lands, #75 should go green on a rebase. #66 needs #62 as well.

## Why Renovate PRs keep going red for reasons unrelated to the dependency they bump. **#75** is a pure lockfile bump (esbuild 0.28.1 → 0.28.2) and its only CI failure is `packages/fit`'s `gpx-to-fit-course` test timing out at 5000ms — something a bundler patch release cannot cause. The cost is one-time module loading, not test work. `parseGpxAsync` lazily `await import("linkedom")` and caches the parser in `_LinkedDOMParser`, so the whole import lands on whichever test runs first. That is why the **smallest** fixture is the one that fails: ``` ✓ short-flat.gpx (559 B) 61ms ← pays the linkedom import ✓ alpine.gpx (861 B) 3ms ✓ multi-day.gpx (1.1 K) 3ms ``` A 20× penalty on iteration one only. Locally 61ms is invisible; on a contended runner the same one-time cost crosses 5s. ## This was already fixed three times, once per package Each as `--testTimeout=20000` in a `package.json` test script, each after a renovate PR went red: | commit | package | stated reason | |---|---|---| | `ee19578` | `packages/db` | cold drizzle/postgres import | | `805ac86` | `packages/gpx` | cold fit-parser import | | `f31eb26` | `apps/journal` | cold fedify/drizzle/react-router import | `packages/fit` never got the flag and stayed at the 5s default — even though `805ac86` was prompted by the **gpx→fit round-trip**, i.e. the same code path `packages/fit` tests. A fourth per-package flag just moves the whack-a-mole along. So set it once in `vitest.shared.ts`, which every package's `vitest.config.ts` extends, and drop the three scattered copies. A new package can no longer be born at the 5s default and rediscover this. Also set in the root `vitest.config.ts`: that file does not extend the shared one and is what `pnpm test:watch` uses, so without it watch mode sits at 5s while CI is at 20s — the same divergence pointing the other way. ## Testing Verified the mechanism rather than assuming it: - **Reproduced CI's exact signature locally.** `--testTimeout=30` (short-flat needs 61ms, the others 3ms) yields `1 failed | 7 passed (8)` with short-flat as the failure — the same shape as run 638. Confirms the timeout is the binding constraint and nothing else is wrong. - **Proved the new value is inherited**, with a probe test sleeping 6s — which the old default would have killed — passing in `packages/fit`. Removed afterwards. - **Proved no regression** for the three packages that lost their CLI flag: the same 6s probe passes in `db`, `gpx` and `journal` from the shared config alone. - **Verified against #75 itself**: cherry-picked onto `renovate/esbuild-=0.27.3-0.28.1-0.x`, installed its lockfile (esbuild 0.28.2 confirmed present), and ran the previously-failing test — 8/8 passed. Full suite on that branch: test 12/12, typecheck 14/14, lint 14/14, build 2/2. - On this branch: test 12/12, typecheck 14/14, lint 14/14. ## What this does not fix `runs-on: playwright` resolves to a **runner-pinned** image. When renovate bumps the `@playwright/test` package past it, visual tests die with: ``` browserType.launch: Executable doesn't exist at /ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell ``` Confirmed on **#66** (job ran `image=mcr.microsoft.com/playwright:v1.61.1-noble`, PR installs `playwright 1.62.1`). Renovate can never make such a PR green while the image version lives in runner config and the package version in `package.json` — which is what **#62** is probing. #66 needs both that and this. After this lands, #75 should go green on a rebase. #66 needs #62 as well.
test: centralize vitest testTimeout so packages can't inherit the 5s default
All checks were successful
CD Staging / Build & Push Docker Images (pull_request) Has been skipped
CD Staging / Build & Push Docker Images-1 (pull_request) Has been skipped
CD Staging / Deploy Staging (pull_request) Has been skipped
CD Staging / Deploy PR Preview (pull_request) Has been skipped
CI / Security Scan (pull_request) Successful in 1m2s
CI / Dockerfile Package Check (pull_request) Successful in 14s
CI / Visual Tests (pull_request) Successful in 2m46s
CI / Checks (pull_request) Successful in 6m2s
CI / E2E Tests (pull_request) Successful in 6m52s
CI / Journal Image Smoke Test (pull_request) Successful in 19m43s
Cancel superseded CI / Cancel in-flight CI (pull_request) Successful in 19s
CD Staging / Tear Down PR Preview (pull_request) Successful in 45s
43c61b9b5c
Renovate PRs keep going red for reasons unrelated to the dependency they
bump. #75 is a pure lockfile bump (esbuild 0.28.1 -> 0.28.2) and its only
CI failure is packages/fit's gpx-to-fit-course test timing out at 5000ms.

The cost is one-time module loading, not test work. parseGpxAsync lazily
`await import("linkedom")` and caches the parser, so the entire import lands
on whichever test runs first — which is why the *smallest* fixture,
short-flat.gpx (559 B), is the one that fails while alpine.gpx and
multi-day.gpx pass right after it. Locally that first test takes 61ms against
3ms for the others; on a contended runner the same 20x penalty crosses 5s.

This was already known and fixed three times, once per package, each time as
`--testTimeout=20000` in a package.json test script:

  ee19578  packages/db     cold drizzle/postgres import
  805ac86  packages/gpx    cold fit-parser import
  f31eb26  apps/journal    cold fedify/drizzle/react-router import

packages/fit never got the flag and stayed at the default — even though
805ac86 was prompted by the gpx->fit round-trip, i.e. the same code path the
fit package tests. A fourth per-package flag would just move the whack-a-mole
along, so set it once in vitest.shared.ts, which every package's
vitest.config.ts extends, and drop the three scattered copies.

Also set it in the root vitest.config.ts. That file does not extend the shared
one and is what `pnpm test:watch` uses, so without it watch mode sits at 5s
while CI is at 20s — the same divergence in the other direction.

Verified locally: reproduced CI's exact signature by constraining the timeout
(`--testTimeout=30` yields "1 failed | 7 passed (8)", the failure being
short-flat, matching run 638); confirmed fit now inherits 20000 with a 6s
probe test that the old default would have killed; confirmed db, gpx and
journal still inherit it after losing their CLI flag. typecheck 14/14,
lint 14/14, test 12/12.

Does not address the other reason these PRs fail: `runs-on: playwright`
resolves to a runner-pinned image, so bumping the @playwright/test package
past it breaks visual tests with "Executable doesn't exist at
/ms-playwright/chromium_headless_shell-<rev>/...". That is #66's second
failure and what #62 is probing.
ullrich deleted branch fix/centralize-vitest-timeout 2026-08-18 15:36:29 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
trails-cool/trails!78
No description provided.