diff --git a/.pi/skills/yellowjacket-dev/SKILL.md b/.pi/skills/yellowjacket-dev/SKILL.md index 63c26d2..761cde5 100644 --- a/.pi/skills/yellowjacket-dev/SKILL.md +++ b/.pi/skills/yellowjacket-dev/SKILL.md @@ -56,11 +56,12 @@ reference, because you need them *before* the failure, not after. `--browser=webkit` is CI-only; local work is Chromium. CI runs it with `if: !cancelled()` so a chromium failure does not silently skip it, which it did for two sessions. -- **CI's `e2e` job is red for a reason that is not yours.** Three - playback specs fail in the container on both engines (48 pass on - each) because the position never advances there — an audio-device - problem, not a renderer or app one. Read the per-step status before - assuming your change did it. +- **CI's `e2e` job is green on both engines** (54 specs each) since the + container got an audio device that keeps time. If playback specs + start failing there again, check the **`The sink plays at real time`** + step first: ALSA's `null` plugin consumes 3000 ms of audio in 2.96 ms, + so every track finishes instantly and the clock never moves — which + reads as an app bug and cost two sessions of that suspicion. - **`make e2e` needs `SEED=default`.** Its specs assert on fixture content — unicode tracks, the fixture artists, a known playable file. Run against the `bulk` seed a measurement session left behind and 13 diff --git a/.planning/NOTES.md b/.planning/NOTES.md index 5caa8cb..7a28bc2 100644 --- a/.planning/NOTES.md +++ b/.planning/NOTES.md @@ -1529,3 +1529,57 @@ Seven more things worth keeping: removed. Nothing is paid for it — the slot's light-DOM children are in the DOM either way; a conditional `` only stops projecting them. + +## A dependency with a rate has to be checked at its rate + +Same pass, after the plan's three landings: the `e2e` job's red history +turned out to be one measurement being wrong, and the fix was worth the +generalisation. + +`ci.yml` had made ALSA's `null` plugin the container's default device, +with a comment saying it "advances its pointer on a timer, so playback +is consumed at real-time rate". It does not. Measured in the CI image +under Docker, through beep and oto with the same `speaker.Init` +arguments `player.InitSpeaker` uses: + +| default device | 3000 ms of audio consumed in | +|---|---| +| `type null` | **2.96 ms** | +| PulseAudio null sink | **3762 ms** | + +A thousand times too fast. Every track finished instantly, so the +position reset to zero while the UI kept interpolating, and three specs +failed on a clock that never moved — 17–19 s adrift, which reads +exactly like the `H-3` bug Phase 2 fixed. `check` and `e2e` are both +green now, 54 specs on Chromium and 54 on WebKit. + +Five things worth keeping: + +- **A dependency with a *rate* needs a check at its rate.** Installing + a sound device and asserting it exists is not the same as asserting + it plays. The job now plays three seconds and fails if they take + under two, in a step called "The sink plays at real time" — so the + next regression names itself instead of surfacing three steps later + as an app bug. +- **A failure that succeeds quietly is the expensive kind.** + `InitSpeaker` returns nil in ~3 ms against both devices. Nothing + logged, nothing errored; the only symptom was arithmetic in three + specs. Two sessions read that as a flake and one as a possible + WebKit regression. +- **The CI container is reproducible locally, and nobody had tried.** + `docker run --rm ubuntu:24.04` reproduced the whole thing in four + minutes and let the fix be verified — including under the private + session bus and Xvfb `dev-headless.sh` runs the app in — before it + was pushed. Every previous attempt to reason about this job reasoned + from the *commits* instead, because the log looked unreachable + (which it was not either). +- **Test the stack you ship, not one that resembles it.** `aplay` + showed the same 1000× gap and would have been enough to *diagnose*. + It would not have shown that oto opens the pulse-backed device at + all, which is the thing that had to be true for the fix to work; a + fifteen-line Go program using the app's own `speaker.Init` did. +- **The comment was the bug's hiding place.** "Measured: InitSpeaker + succeeds in ~36 ms and all six playback specs pass" was true when + written and had been carried forward through every subsequent read of + that file, including two this session. A measurement in a comment + needs the same expiry as one in a plan. diff --git a/.planning/plans/active/007-ui-reconciliation.md b/.planning/plans/active/007-ui-reconciliation.md index 353d62e..ce15948 100644 --- a/.planning/plans/active/007-ui-reconciliation.md +++ b/.planning/plans/active/007-ui-reconciliation.md @@ -1924,12 +1924,26 @@ route from the albums grid to `track-details`. And two things this pass found rather than inherited: - **Name the dialogs.** One helper, eight call sites. -- **CI's e2e job cannot go green until the container's audio clock - does.** Three specs assert on a position that does not advance - there. Either the container gets a sink that really consumes, or - those three specs learn to skip when it does not — but silently - loosening a tolerance would delete the only assertions this repo has - that the player tells the truth. + +**CI is green, and the audio clock was the last thing between it and +green.** Fixed the same pass: `ci.yml` used ALSA's `null` plugin on the +belief that it paces, and it does not — measured in the CI image +through beep and oto with `player.InitSpeaker`'s own arguments, +**3000 ms of audio consumed in 2.96 ms**, against **3762 ms** through a +PulseAudio null sink. Every track finished instantly, so the position +reset to zero and three specs failed on a clock that never moved. It +looked like a flake because `InitSpeaker` succeeds either way, in ~3 ms +either way. `check` and `e2e` now both pass, **54 specs on Chromium and +54 on WebKit** — the first fully green run this plan has had. + +Two things about how that was found are worth carrying forward. The fix +was **verified in `ubuntu:24.04` under Docker before it was pushed**, +including under the private session bus and Xvfb `dev-headless.sh` uses +— the CI container is reproducible locally, which nothing had tried. +And the sink is now **checked like the dependency-with-a-rate that it +is**: a step plays three seconds and fails if they take under two, +because otherwise the failure surfaces three steps later as "the +elapsed clock is 19 s adrift" and reads as an app bug. ## Phase 6 — Explore starts the conversation diff --git a/CLAUDE.md b/CLAUDE.md index 11e2ae6..3684daf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -834,20 +834,18 @@ per-step status (which is how "WebKit was skipped" was found) and `GET /api/v1/repos/{owner}/{repo}/actions/jobs/{job_id}/logs` returns the whole log, with `Authorization: token $GITEA_TOKEN`. -**The `e2e` job is currently red on both engines, for one reason.** -48 specs pass under Chromium and 48 under WebKit; the same three fail -— `playback.spec`'s elapsed clock and two in `player-truth.spec` — -because playback does not advance in the container: the UI -interpolates while the backend position stays at zero, 17–18 s adrift. -It is the audio device, not the renderer and not the app. Which means -the paragraph below was measured once and is no longer true. - -Two things the container needs that a developer machine does not. It -has no PulseAudio socket, so `/etc/asound.conf` makes ALSA's `null` -plugin the default device — that plugin is *supposed* to advance its -pointer on a timer, so playback is consumed at real-time rate and the -elapsed clock moves, which `e2e/specs/playback.spec.ts` asserts; it no -longer does. And +Two things the container needs that a developer machine does not. +**It needs an audio device that keeps time**, because the player's +position is derived from what has been consumed — so a device that +accepts audio instantly makes every track finish instantly and the +clock never move. That is what ALSA's `null` plugin does, contrary to +a year of this file and `ci.yml` saying it paces on a timer: measured +through beep and oto with `player.InitSpeaker`'s own arguments, +**3000 ms of audio consumed in 2.96 ms**. It is a PulseAudio null sink +now (the same 3000 ms takes 3762 ms), started in system mode because +the job runs as root, with a step that plays three seconds and fails +if they take under two — a dependency with a rate, checked like one. +And `YJ_CORE_INDEX_URL` points at a dead address so no run fetches the real explore artifact, matching what `scripts/seed-sandbox.sh` already does.