Make the strip preview show the effect it claims to (#14) #134

Merged
claude-bot merged 1 commit from feat/14-simulator into main 2026-09-05 01:26:10 +00:00
Contributor

Closes #14.

Two of the three acceptance criteria already passed before this PR — the side panel animated a virtual strip, and it worked with no WLED_HOST because nothing in it touches the controller. What was missing was any relationship between the animation and the effect.

The mapping was wrong, and unfalsifiable by looking

It keyed on effect IDs 57, 65, 66 and 9 believing them to be a comet, a wave, traffic lanes and a chase. They are Lightning, Palette, Fire 2012 and Rainbow.

Nothing caught it because the only way to check a render loop is to look at it, and looking at a shimmering strip tells you nothing about which effect it claims to be. (The same class of error in the backend is #132, fixed just before this.)

Effects are now grouped into twelve families — solid, breathe, wipe, chase, twinkle, sparkle, fire, gradient, rainbow, comet, strobe, aurora — built from the names a real 16.0.0 controller reports, and pinned by a test against the captured catalogue exactly as the backend's guard is.

It approximates; it does not emulate

WLED ships 220 effects, many 2D or audio-reactive. Reproducing them would be a second implementation of someone else's firmware, one that drifted the moment they changed anything. The goal is narrower and achievable: a reviewer approving a year of schemes can tell them apart and see roughly what each will look like.

An unrecognised ID animates as a generic shimmer — deliberately moving, because a still frame would read as "solid", which is a specific claim it cannot make.

Extracting it found two real bugs immediately

The maths moved out of the component's draw loop into a pure function, which is the only reason any of it is testable. It had no tests at all before. Within minutes of there being some:

  • The wipe was invisible. It sampled the palette at 0.25 and 0.75, which on a two-colour scheme both land mid-blend and return the same purple — so the wipe showed only as a step in brightness, and the comment claiming it "leaves the new colour behind it" was false. Both sides now sample a slot exactly.
  • The twinkle was a breathe with holes. Every live LED shared one fade, because the fade depended only on time. Each LED now carries its own phase offset.

One of my test premises was also wrong

I asserted every non-solid family renders differently at t=0 and t=1.3. Strobe is periodic and happened to be lit at both, so an honest implementation failed the test. It now samples six instants and asserts the frames aren't all identical, which is the property actually worth having.

The duplicated fixture

vitest can't read a file outside the frontend package, so the captured catalogue exists in both trees. Two copies is exactly the arrangement that rots — the backend's guard would keep passing against a stale frontend copy while the simulator's mapping quietly drifted. So a backend test asserts the two files are identical, checked from there because that run can see both trees and the frontend's cannot.

Verification

26 new vitest cases asserting what each family visibly does — solid must not animate, breathe must swing brightness without shifting hue, strobe must light the whole strip together, rainbow must sweep hue regardless of the scheme's colours, twinkle must differ per LED — because "it produced a colour" is true of every possible implementation including a broken one.

Frontend in node:22: tsc -b clean, eslint clean, 107 vitest passing, npm run build succeeds. Backend ruff/mypy clean, 761 tests passing.

🤖 Generated with Claude Code

Closes #14. **Two of the three acceptance criteria already passed** before this PR — the side panel animated a virtual strip, and it worked with no `WLED_HOST` because nothing in it touches the controller. What was missing was any *relationship between the animation and the effect*. ## The mapping was wrong, and unfalsifiable by looking It keyed on effect IDs 57, 65, 66 and 9 believing them to be a comet, a wave, traffic lanes and a chase. They are **Lightning, Palette, Fire 2012 and Rainbow**. Nothing caught it because the only way to check a render loop is to look at it, and looking at a shimmering strip tells you nothing about which effect it claims to be. (The same class of error in the backend is #132, fixed just before this.) Effects are now grouped into **twelve families** — solid, breathe, wipe, chase, twinkle, sparkle, fire, gradient, rainbow, comet, strobe, aurora — built from the names a real 16.0.0 controller reports, and pinned by a test against the captured catalogue exactly as the backend's guard is. ## It approximates; it does not emulate WLED ships 220 effects, many 2D or audio-reactive. Reproducing them would be a second implementation of someone else's firmware, one that drifted the moment they changed anything. The goal is narrower and achievable: a reviewer approving a year of schemes can tell them apart and see roughly what each will look like. An unrecognised ID animates as a generic shimmer — deliberately *moving*, because a still frame would read as "solid", which is a specific claim it cannot make. ## Extracting it found two real bugs immediately The maths moved out of the component's draw loop into a pure function, which is the only reason any of it is testable. It had **no tests at all** before. Within minutes of there being some: - **The wipe was invisible.** It sampled the palette at 0.25 and 0.75, which on a two-colour scheme both land mid-blend and return the same purple — so the wipe showed only as a step in brightness, and the comment claiming it "leaves the new colour behind it" was false. Both sides now sample a slot exactly. - **The twinkle was a breathe with holes.** Every live LED shared one fade, because the fade depended only on time. Each LED now carries its own phase offset. ## One of my test premises was also wrong I asserted every non-solid family renders differently at t=0 and t=1.3. **Strobe is periodic and happened to be lit at both**, so an honest implementation failed the test. It now samples six instants and asserts the frames aren't all identical, which is the property actually worth having. ## The duplicated fixture vitest can't read a file outside the frontend package, so the captured catalogue exists in both trees. Two copies is exactly the arrangement that rots — the backend's guard would keep passing against a stale frontend copy while the simulator's mapping quietly drifted. So a **backend test asserts the two files are identical**, checked from there because that run can see both trees and the frontend's cannot. ## Verification 26 new vitest cases asserting what each family visibly *does* — solid must not animate, breathe must swing brightness without shifting hue, strobe must light the whole strip together, rainbow must sweep hue regardless of the scheme's colours, twinkle must differ per LED — because "it produced a colour" is true of every possible implementation including a broken one. Frontend in `node:22`: `tsc -b` clean, `eslint` clean, **107 vitest passing**, `npm run build` succeeds. Backend `ruff`/`mypy` clean, **761 tests passing**. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Make the strip preview show the effect it claims to (#14)
All checks were successful
CI / Dockerfile lint (pull_request) Successful in 8s
CI / Pre-commit hooks (pull_request) Successful in 1m2s
CI / Alembic migration check (pull_request) Successful in 44s
CI / Python lint & type-check (pull_request) Successful in 1m19s
CI / Frontend lint, test & build (pull_request) Successful in 1m15s
CI / Python tests (pull_request) Successful in 4m37s
CI / Docker build, health smoke & E2E (pull_request) Successful in 2m49s
22292e5127
Selecting a scheme already animated a virtual LED strip; what was missing
was any relationship between the animation and the effect. The mapping
keyed on IDs 57, 65, 66 and 9 believing them to be a comet, a wave,
traffic lanes and a chase. They are Lightning, Palette, Fire 2012 and
Rainbow. Nothing caught it because the only way to check a render loop is
to look at it, and looking at a shimmering strip tells you nothing about
which effect it claims to be.

Effects are now grouped into twelve families -- solid, breathe, wipe,
chase, twinkle, sparkle, fire, gradient, rainbow, comet, strobe, aurora
-- built from the names a real 16.0.0 controller reports rather than from
memory, and pinned by a test against the captured catalogue exactly as
the backend's #132 guard is. Anything unrecognised animates as a generic
shimmer, deliberately moving: a still frame would read as "solid", which
is a specific claim it cannot make.

It approximates; it does not emulate. WLED ships 220 effects, many 2D or
audio-reactive, and reproducing them would be a second implementation of
someone else's firmware that drifted the moment they changed anything.
The goal is that a reviewer approving a year of schemes can tell them
apart and see roughly what each will look like.

The maths moved out of the component's draw loop into a pure function,
which is the only reason any of this is testable, and it had no tests at
all before. Two real bugs surfaced as soon as there were:

The wipe sampled the palette at 0.25 and 0.75, which on a two-colour
scheme lands mid-blend and returns the same purple on both sides -- so
the wipe showed only as a step in brightness, and the comment claiming it
"leaves the new colour behind it" was false. Both sides now sample a slot
exactly.

Every twinkling LED shared one fade, since the fade depended only on
time. That is a breathe with holes, not a twinkle. Each LED now carries
its own phase offset.

One test premise of mine was also wrong: I asserted every non-solid
family renders differently at t=0 and t=1.3. Strobe is periodic and
happened to be lit at both, so an honest implementation failed. The test
now samples six instants and asserts the frames are not all identical,
which is the property actually worth having.

The captured catalogue is duplicated into the frontend because vitest
cannot read a file outside the package. Two copies is exactly the
arrangement that rots, so a backend test asserts they are identical --
checked from there because that run can see both trees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-bot deleted branch feat/14-simulator 2026-09-05 01:26:11 +00:00
Sign in to join this conversation.
No description provided.