fix: cascades follow-ups (power-off ops mirror, AST coverage, manual-slider race) - #241
Merged
Conversation
…slider race) Four targeted follow-ups to the optimistic-UI cascades PR (#240): 1. **Power-off → timer-clear at the ops layer.** Probed the office device 2026-05-12 (with night-timer to avoid the inconclusive turbo-self-clear from the original Test 4): firmware DOES clear 0x0007 when 0x0001 goes 1→0. Encode that in pkg/breezy/ops.go::Power so the daemon cache stays coherent for ALL callers (UI handler, /v1 JSON, HomeKit, scheduler) — not just the /power UI handler, which used to do its own explicit SetTimer(off) and is now simplified. 2. **AST-walking coverage test.** TestCascadeTable_AllWritableSignalsCovered used to enumerate signal names by hand. Now it parses the .go files in the templates package, walks every clickAction(...) call, extracts the 2nd argument when literal, and asserts each signal appears in cascades. Verified by mutation: changing "heater" to "unregisteredSig" in the .templ produces a precise file:line error. 3. **Manual slider phantom-POST race.** Under heavy load, the "manual slider drag" Playwright test flaked 40-60%. Stack-trace probing (2026-05-12) showed datastar's data-bind plugin fires the change handler synchronously during its signal→input syncs — both initial and on every SSE-pushed signal update. The synthetic events had isTrusted=false and the input's current value, producing no-op {manual:50} POSTs that pre-empted the test's deliberate drag-to-75 assertion. Replaced data-bind with: data-effect (signal → el.value, no event dispatch), data-on:input (input → signal, live drag mirror), data-on:change without debounce (POST on release). Removing the 200ms debounce also closes a second race where SSE pushes could reset el.value via data-effect between event dispatch and debounced handler-fire, causing the handler to read the server value instead of the dragged value. 15 full-suite runs under heavy load: all green. 4. **Spec updated** with the settled Test 4 result and reorganized Layer B server-side mirrors table around ops-layer placement. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
hughobrien
enabled auto-merge (squash)
May 12, 2026 06:51
hughobrien
added a commit
that referenced
this pull request
Jun 29, 2026
…slider race) (#241) Four targeted follow-ups to the optimistic-UI cascades PR (#240): 1. **Power-off → timer-clear at the ops layer.** Probed the office device 2026-05-12 (with night-timer to avoid the inconclusive turbo-self-clear from the original Test 4): firmware DOES clear 0x0007 when 0x0001 goes 1→0. Encode that in pkg/breezy/ops.go::Power so the daemon cache stays coherent for ALL callers (UI handler, /v1 JSON, HomeKit, scheduler) — not just the /power UI handler, which used to do its own explicit SetTimer(off) and is now simplified. 2. **AST-walking coverage test.** TestCascadeTable_AllWritableSignalsCovered used to enumerate signal names by hand. Now it parses the .go files in the templates package, walks every clickAction(...) call, extracts the 2nd argument when literal, and asserts each signal appears in cascades. Verified by mutation: changing "heater" to "unregisteredSig" in the .templ produces a precise file:line error. 3. **Manual slider phantom-POST race.** Under heavy load, the "manual slider drag" Playwright test flaked 40-60%. Stack-trace probing (2026-05-12) showed datastar's data-bind plugin fires the change handler synchronously during its signal→input syncs — both initial and on every SSE-pushed signal update. The synthetic events had isTrusted=false and the input's current value, producing no-op {manual:50} POSTs that pre-empted the test's deliberate drag-to-75 assertion. Replaced data-bind with: data-effect (signal → el.value, no event dispatch), data-on:input (input → signal, live drag mirror), data-on:change without debounce (POST on release). Removing the 200ms debounce also closes a second race where SSE pushes could reset el.value via data-effect between event dispatch and debounced handler-fire, causing the handler to read the server value instead of the dragged value. 15 full-suite runs under heavy load: all green. 4. **Spec updated** with the settled Test 4 result and reorganized Layer B server-side mirrors table around ops-layer placement. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four follow-up fixes for gaps I flagged after PR #240 landed.
1. Power-off → timer-clear at the ops layer
Probed the office device 2026-05-12 with the night timer (the original Test 4 used turbo, whose short configured duration self-cleared mid-test and made the result inconclusive). Firmware DOES clear
0x0007when0x0001goes 1→0. Encoded inpkg/breezy/ops.go::Power(false)so the daemon's cache stays coherent for ALL callers — UI handler,/v1JSON, HomeKit, scheduler — not just the/powerUI handler, which used to do its own explicitSetTimer(off)and is now simplified.2. AST-walking coverage test
TestCascadeTable_AllWritableSignalsCoveredno longer relies on a hand-maintained whitelist. It now parses the templates package's.gofiles, walks everyclickAction(...)call, and asserts each literal 2nd-argument signal name appears in thecascadesmap. Mutation-verified: changing"heater"to"unregisteredSig"incontrols_block.templproduces a precise file:line error.3. Manual slider phantom-POST race
The
manual slider drag posts dragged valuePlaywright test flaked 40-60% under heavy load. Stack-trace probing (2026-05-12) pointed atu.rxindatastar-1.0.1.min.js:4:3121: datastar'sdata-bindplugin fires the change handler synchronously during its signal→input syncs — both initial and on every SSE-pushed signal update. The synthetic events haveisTrusted=falseand the input's current value, producing no-op{manual:50}POSTs that pre-empted the test's deliberate drag-to-75 assertion.Fix: replace
data-bindwith the explicit split that doesn't synthesize change events:data-effect="el.value = $_manualPct.X"— signal → input.value (direct property assignment, no DOM event)data-on:input="$_manualPct.X = parseInt(evt.target.value, 10)"— input → signal, live drag mirrordata-on:change={ manualChangeExpr }— POST on release (NO__debounce.200ms— that 200ms window let SSE pushes resetel.valueviadata-effectbetween dispatch and handler-fire, making the handler read the server value instead of the dragged value)15+ full-suite runs under heavy load (concurrent
go test -race): all green.4. Spec updated
Test 4 result settled. Layer B server-side mirrors table reorganized around ops-layer placement now that both speed and power use the same pattern.
Test plan
just cigreenpkg/breezy.TestPowerOff_ClearsTimerandTestPowerOn_NoTimerClearconst __nextpatterns in SSE-pushed cards)🤖 Generated with Claude Code