Skip to content

fix: cascades follow-ups (power-off ops mirror, AST coverage, manual-slider race) - #241

Merged
hughobrien merged 1 commit into
mainfrom
fix/cascade-followups
May 12, 2026
Merged

fix: cascades follow-ups (power-off ops mirror, AST coverage, manual-slider race)#241
hughobrien merged 1 commit into
mainfrom
fix/cascade-followups

Conversation

@hughobrien

Copy link
Copy Markdown
Owner

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 0x0007 when 0x0001 goes 1→0. Encoded in pkg/breezy/ops.go::Power(false) so the daemon's 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 no longer relies on a hand-maintained whitelist. It now parses the templates package's .go files, walks every clickAction(...) call, and asserts each literal 2nd-argument signal name appears in the cascades map. Mutation-verified: changing "heater" to "unregisteredSig" in controls_block.templ produces a precise file:line error.

3. Manual slider phantom-POST race

The manual slider drag posts dragged value Playwright test flaked 40-60% under heavy load. Stack-trace probing (2026-05-12) pointed at u.rx in datastar-1.0.1.min.js:4:3121: 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 have 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.

Fix: replace data-bind with 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 mirror
  • data-on:change={ manualChangeExpr } — POST on release (NO __debounce.200ms — that 200ms window let SSE pushes reset el.value via data-effect between 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 ci green
  • 28/28 Playwright tests pass
  • New pkg/breezy.TestPowerOff_ClearsTimer and TestPowerOn_NoTimerClear
  • Coverage test mutation-verified (changing a signal name in a click handler triggers a precise CI failure)
  • Live deployment at breezy.wg now serves the cascade markup (effPower function present, const __next patterns in SSE-pushed cards)

🤖 Generated with Claude Code

…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
hughobrien enabled auto-merge (squash) May 12, 2026 06:51
@hughobrien
hughobrien merged commit 7c83262 into main May 12, 2026
6 checks passed
@hughobrien
hughobrien deleted the fix/cascade-followups branch May 12, 2026 06:53
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant