DEV Community

pm25coder
pm25coder

Posted on

Undefined CSS variables fail silently: two failures in one evening, and the guard that checks reality

The agent harness I work on has an Electron GUI that shares a renderer with a web shell. Last night it broke twice in one evening. The second break was caused by the first fix. Both were silent. The first one I could explain. The second one was the interesting one, because it exposed something the first fix's test suite could not see — and the fix was a guard that checks reality instead of checking the guard's own arithmetic.

Failure one: the light-theme regression.

The React shell used CSS custom properties for theming, but a chunk of the migration hardcoded dark-palette hexes directly in component CSS. In light mode the UI looked wrong: dark text on light cards, bad contrast, the exact shape of a half-finished theme refactor. The fix was to route everything through theme variables (the release shipped that as v0.2.84). Straightforward.

Failure two: the fix had a hole, and the hole was invisible.

After the theme-variable fix landed, a second round of breakage showed up: the task-form background rendered transparent, file-tab hover was dead, badge font sizes and radii were wrong. Nothing threw. No console error, no crash, no failing test. The cause: the fix consumed four variables — --fs-small, --radius-sm, --bg-1, --bg-hover — that did not exist in tokens.css. A bare var(--x) with no fallback is not an error. At computed-value time the declaration becomes invalid at computed-value time, and the property is treated as if it were never specified. The element just falls back to the default — transparent background, no hover style, default font metrics. The failure mode of an undefined CSS variable is silence.

This is the part I want to keep: the bug was not a wrong value. It was a value that was never there, consumed as if it were. The tests passed because the tests asserted behavior, and the behavior was "whatever the browser does with an invalid declaration".

The guard that checks definedness.

The fix was a guard, not just a value: a static test that walks every CSS file in the renderer and asserts that every bare var(--x) is defined in tokens.css — every theme block (light default + forced, dark media + forced). A second rule asserts no dark-palette hexes live outside tokens.css, comment-stripped. Negative-state verified: the guard fails on the pre-fix tokens and passes on the fixed ones. From now on, an undefined variable is a red build, not a transparent form.

The meta-failure: the guard that checked arithmetic, not reality.

While fixing that, we found a third silent drift. The project doc lists a per-file count of renderer test cases; the count had drifted 445 to 448. The doc-count guard validated each line's internal sum — parts equal headline — but never compared the total to what the test runner actually executes. The pytest CI job has no node_modules, so vitest never runs there; nothing checked the number against reality. The fix was a static guard that counts test-case definitions per source file and asserts the total equals the executed total, runnable in plain pytest — so any future drift goes red immediately.

The lesson: self-consistency is not verification.

All three failures share a shape. The light-theme bug: a claim (theme works) with no artifact tying it to reality. The undefined-variable bug: consumption with no definedness check — the reference looked fine because nothing validated the referent. The doc-count drift: a check that verified its own arithmetic instead of the world. None of them were caught by tests that compared the system to itself. All of them were caught — or are now guarded — by checks that compare against something outside the thing being checked: the actual tokens file, the actual executed test count, the actual consumption path.

Silent failures are the recurring enemy in this system, and we have now seen the same disease four times: a counter that drifted from reality, an anchor that went quiet without saying so, a same-day revert, and now an undefined variable that declared itself invalid at computed-value time. The pattern in the fix is consistent too: fail loudly at the boundary where the artifact meets the thing it claims to represent.

If you take one thing from this: when you write a guard, ask what ground truth it compares against. A check that compares the system to itself will drift quietly. A check that compares the system to reality goes red — and red is a feature.

Top comments (0)