and passes every time it is run on its own. It has now failed three
scheduled runs, including PR #218, whose diff was entirely backend/riff, backend/metadata and backend/tagwriter — no frontend file touched. That
is the expensive part: make ui-test is a required check, so this sends
authors looking at their own diff.
What it actually is
A test file does not get its own origin.@vitest/browser-playwright
opens one BrowserContext per session (createContext(sessionId) → browser.newContext()) and runs several files in it one after another, so localStorage survives from file to file. track-list restores its sort in connectedCallback; aria-tail.test.ts:100 activates the Title column
header from the keyboard, which persists track-list-sort-field. Any file
that mounts a track list afterwards in that tab opens sorted by title —
where track-11 precedes track-3.
Nothing about it is specific to that pair. A temporary probe (a root beforeEach that throws when localStorage is not empty) failed 24
test-starts in a single full run on main — 11 with ["track-list-sort-field","track-list-sort-direction"], 8 with cover-grid-size, 5 with track-list-column-widths — and cascaded into 248
failures. What varies run to run is only which files share a tab and in what
order, which is the whole of why this reads as a 1-in-3 flake.
That also explains why the reporter's sequential subset did not reproduce
it: two files named on the command line rarely land in the tab, or the
order, that matters. The full suite has ~99 chances to.
What changed, and why there
Both halves of the issue's own Direction, and nothing else.
frontend/test/setup.ts — localStorage.clear() in the root beforeEach, beside resetHarness(). It belongs here rather than in the
specs that write, because the spec that reads is never the one that
knows. It is safe for exactly the reason the leak exists: files within a
session are sequential, so it cannot wipe storage a concurrently-running
file is in the middle of using.
frontend/test/components/play-in-context.test.ts — the spec states
the order it asserts (sortField = null after mount) instead of
inheriting a default, and checks the row it is about to double-click
carries the path it expects, so a stray sort fails by naming itself rather
than as an off-by-eight file path.
CLAUDE.md — the trap and the reasoning, in the make ui-test
section.
No production code is touched. That was also this run's constraint:
eight PRs are open against an unmoved main and between them already edit
most of frontend/src.
Verification
Tier
Result
make ui-test
1092 passed, eight consecutive full runs
The probe above, re-run on top of the fix
0 polluted test-starts, against 24 before
npx tsc --noEmit
clean
make ui-visual
2 screenshot cases fail — identically on origin/main with the change stashed (see below)
make commit-check
well-formed
Proved non-vacuous. With the two sort keys injected into setup.ts and both halves of the fix defeated, the spec fails at the new first
assertion: expected '/music/track-11.mp3' to be '/music/track-3.mp3'. With
the pollution injected and only the setup clear defeated, it passes; with
only the spec's own statement defeated, it passes. Each half stands alone.
Pre-existing and not this.make ui-visual fails chrome.test.ts <app-sidebar> and now-playing.test.ts <now-playing> — byte-identical on origin/main with the change stashed. These are the machine-specific
screenshot baselines CLAUDE.md describes; CI runs make ui-test, not make ui-visual.
Tiers deliberately not run. No Go, no .sql, no .templ, no bound
signature and no CSS, so make lint, make test, make generate, make bindings and make css-check do not apply. No user-visible flow
changed, so make e2e was not run.
Deliberately not done
The four specs that clear one key by hand (play-in-context, track-list-width, track-list-phone, library-store) keep doing so. The
lines are redundant now but harmless, and deleting them would widen the diff
across four files for nothing.
## What the issue was
`test/components/play-in-context.test.ts` fails about one full `make ui-test`
run in three, on a clean `main`, with
```
AssertionError: expected [ 12, '/music/track-11.mp3' ]
to deeply equal [ 12, '/music/track-3.mp3' ]
```
and passes every time it is run on its own. It has now failed three
scheduled runs, including PR #218, whose diff was entirely `backend/riff`,
`backend/metadata` and `backend/tagwriter` — no frontend file touched. That
is the expensive part: `make ui-test` is a required check, so this sends
authors looking at their own diff.
## What it actually is
**A test file does not get its own origin.** `@vitest/browser-playwright`
opens one `BrowserContext` per session (`createContext(sessionId)` →
`browser.newContext()`) and runs several files in it one after another, so
`localStorage` survives from file to file. `track-list` restores its sort in
`connectedCallback`; `aria-tail.test.ts:100` activates the Title column
header from the keyboard, which persists `track-list-sort-field`. Any file
that mounts a track list afterwards **in that tab** opens sorted by title —
where `track-11` precedes `track-3`.
Nothing about it is specific to that pair. A temporary probe (a root
`beforeEach` that throws when `localStorage` is not empty) failed **24**
test-starts in a single full run on `main` — 11 with
`["track-list-sort-field","track-list-sort-direction"]`, 8 with
`cover-grid-size`, 5 with `track-list-column-widths` — and cascaded into 248
failures. What varies run to run is only which files share a tab and in what
order, which is the whole of why this reads as a 1-in-3 flake.
That also explains **why the reporter's sequential subset did not reproduce
it**: two files named on the command line rarely land in the tab, or the
order, that matters. The full suite has ~99 chances to.
## What changed, and why there
Both halves of the issue's own Direction, and nothing else.
- **`frontend/test/setup.ts`** — `localStorage.clear()` in the root
`beforeEach`, beside `resetHarness()`. It belongs here rather than in the
specs that write, because the spec that *reads* is never the one that
knows. It is safe for exactly the reason the leak exists: files within a
session are sequential, so it cannot wipe storage a concurrently-running
file is in the middle of using.
- **`frontend/test/components/play-in-context.test.ts`** — the spec states
the order it asserts (`sortField = null` after mount) instead of
inheriting a default, and checks the row it is about to double-click
carries the path it expects, so a stray sort fails by naming itself rather
than as an off-by-eight file path.
- **`CLAUDE.md`** — the trap and the reasoning, in the `make ui-test`
section.
**No production code is touched.** That was also this run's constraint:
eight PRs are open against an unmoved `main` and between them already edit
most of `frontend/src`.
## Verification
| Tier | Result |
| --- | --- |
| `make ui-test` | **1092 passed, eight consecutive full runs** |
| The probe above, re-run on top of the fix | **0** polluted test-starts, against 24 before |
| `npx tsc --noEmit` | clean |
| `make ui-visual` | 2 screenshot cases fail — **identically on `origin/main` with the change stashed** (see below) |
| `make commit-check` | well-formed |
**Proved non-vacuous.** With the two sort keys injected into `setup.ts` and
*both* halves of the fix defeated, the spec fails at the new first
assertion: `expected '/music/track-11.mp3' to be '/music/track-3.mp3'`. With
the pollution injected and only the setup clear defeated, it passes; with
only the spec's own statement defeated, it passes. Each half stands alone.
**Pre-existing and not this.** `make ui-visual` fails `chrome.test.ts
<app-sidebar>` and `now-playing.test.ts <now-playing>` — byte-identical on
`origin/main` with the change stashed. These are the machine-specific
screenshot baselines `CLAUDE.md` describes; CI runs `make ui-test`, not
`make ui-visual`.
**Tiers deliberately not run.** No Go, no `.sql`, no `.templ`, no bound
signature and no CSS, so `make lint`, `make test`, `make generate`,
`make bindings` and `make css-check` do not apply. No user-visible flow
changed, so `make e2e` was not run.
## Deliberately not done
The four specs that clear one key by hand (`play-in-context`,
`track-list-width`, `track-list-phone`, `library-store`) keep doing so. The
lines are redundant now but harmless, and deleting them would widen the diff
across four files for nothing.
Closes #138
A test file does not get its own origin. `@vitest/browser-playwright`
opens one BrowserContext per session and runs several files in it, one
after another, so everything a component persists survives from file to
file. `track-list` restores its sort in `connectedCallback` and
`aria-tail.test.ts` activates the Title column header, so any file that
mounts a track list later in that tab opens sorted by title — where
`track-11` precedes `track-3`, which is #138's failure exactly.
Nothing about it is specific to that pair: a probe that throws when a
test starts with a non-empty `localStorage` failed 24 test-starts in one
full run (11 with the two sort keys, 8 with `cover-grid-size`, 5 with
`track-list-column-widths`) and cascaded into 248 failures. Which files
share a tab, and in what order, changes run to run, which is the whole
of why this reads as a 1-in-3 flake and passes in isolation.
The clear belongs in `setup.ts` rather than in the specs that write,
because the spec that reads is never the one that knows — and it is
safe for the same reason the leak exists: files within a session are
sequential, so it cannot wipe storage a concurrent file is using.
The spec now also states the order it asserts rather than inheriting a
default, and checks the row it is about to double-click carries the path
it expects, so a stray sort fails by naming itself instead of as an
off-by-eight file path.
Closes#138
CI green. Run 554: check success, e2e success (chromium and WebKit).
Worth noting for this PR specifically — the check job'''s Component and store suite step is the one the issue is about, and it passed first attempt. That is one sample, not a proof; the eight consecutive local full runs in the description are the evidence, and the probe result (0 polluted test-starts against 24 before) is the one that does not depend on how many times anything was run.
**CI green.** Run 554: `check` success, `e2e` success (chromium and WebKit).
Worth noting for this PR specifically — the `check` job'''s `Component and store suite` step is the one the issue is about, and it passed first attempt. That is one sample, not a proof; the eight consecutive local full runs in the description are the evidence, and the probe result (0 polluted test-starts against 24 before) is the one that does not depend on how many times anything was run.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What the issue was
test/components/play-in-context.test.tsfails about one fullmake ui-testrun in three, on a clean
main, withand passes every time it is run on its own. It has now failed three
scheduled runs, including PR #218, whose diff was entirely
backend/riff,backend/metadataandbackend/tagwriter— no frontend file touched. Thatis the expensive part:
make ui-testis a required check, so this sendsauthors looking at their own diff.
What it actually is
A test file does not get its own origin.
@vitest/browser-playwrightopens one
BrowserContextper session (createContext(sessionId)→browser.newContext()) and runs several files in it one after another, solocalStoragesurvives from file to file.track-listrestores its sort inconnectedCallback;aria-tail.test.ts:100activates the Title columnheader from the keyboard, which persists
track-list-sort-field. Any filethat mounts a track list afterwards in that tab opens sorted by title —
where
track-11precedestrack-3.Nothing about it is specific to that pair. A temporary probe (a root
beforeEachthat throws whenlocalStorageis not empty) failed 24test-starts in a single full run on
main— 11 with["track-list-sort-field","track-list-sort-direction"], 8 withcover-grid-size, 5 withtrack-list-column-widths— and cascaded into 248failures. What varies run to run is only which files share a tab and in what
order, which is the whole of why this reads as a 1-in-3 flake.
That also explains why the reporter's sequential subset did not reproduce
it: two files named on the command line rarely land in the tab, or the
order, that matters. The full suite has ~99 chances to.
What changed, and why there
Both halves of the issue's own Direction, and nothing else.
frontend/test/setup.ts—localStorage.clear()in the rootbeforeEach, besideresetHarness(). It belongs here rather than in thespecs that write, because the spec that reads is never the one that
knows. It is safe for exactly the reason the leak exists: files within a
session are sequential, so it cannot wipe storage a concurrently-running
file is in the middle of using.
frontend/test/components/play-in-context.test.ts— the spec statesthe order it asserts (
sortField = nullafter mount) instead ofinheriting a default, and checks the row it is about to double-click
carries the path it expects, so a stray sort fails by naming itself rather
than as an off-by-eight file path.
CLAUDE.md— the trap and the reasoning, in themake ui-testsection.
No production code is touched. That was also this run's constraint:
eight PRs are open against an unmoved
mainand between them already editmost of
frontend/src.Verification
make ui-testnpx tsc --noEmitmake ui-visualorigin/mainwith the change stashed (see below)make commit-checkProved non-vacuous. With the two sort keys injected into
setup.tsandboth halves of the fix defeated, the spec fails at the new first
assertion:
expected '/music/track-11.mp3' to be '/music/track-3.mp3'. Withthe pollution injected and only the setup clear defeated, it passes; with
only the spec's own statement defeated, it passes. Each half stands alone.
Pre-existing and not this.
make ui-visualfailschrome.test.ts <app-sidebar>andnow-playing.test.ts <now-playing>— byte-identical onorigin/mainwith the change stashed. These are the machine-specificscreenshot baselines
CLAUDE.mddescribes; CI runsmake ui-test, notmake ui-visual.Tiers deliberately not run. No Go, no
.sql, no.templ, no boundsignature and no CSS, so
make lint,make test,make generate,make bindingsandmake css-checkdo not apply. No user-visible flowchanged, so
make e2ewas not run.Deliberately not done
The four specs that clear one key by hand (
play-in-context,track-list-width,track-list-phone,library-store) keep doing so. Thelines are redundant now but harmless, and deleting them would widen the diff
across four files for nothing.
Closes #138
CI green. Run 554:
checksuccess,e2esuccess (chromium and WebKit).Worth noting for this PR specifically — the
checkjob'''sComponent and store suitestep is the one the issue is about, and it passed first attempt. That is one sample, not a proof; the eight consecutive local full runs in the description are the evidence, and the probe result (0 polluted test-starts against 24 before) is the one that does not depend on how many times anything was run.