Merge remote-tracking branch 'origin/release/1.0' into TEST-16
checks / unit (pull_request) Successful in 3m41s

# Conflicts:
#	goodwe_controller/CHANGELOG.md
This commit is contained in:
glenn schrooyen
2026-09-01 07:54:52 +02:00
3 changed files with 284 additions and 1 deletions
+35
View File
@@ -29,6 +29,41 @@ along**; `app/` is untouched. `MUTATION.md` gains a section on masking as a
second way a survivor lies, and `test_arbitrage.py`'s SAFETY-26 AC4 note is
marked closed rather than left reading as an open finding.
**`check_output_clamp()` could be moved above `write_setpoint()` in `cycle()`
with test_clamp.py, test_freeze.py and test_loop.py all staying GREEN — the
guard's write-path evidence, `self.clamp_last`, had nothing watching it.**
Measured before the fix, on the move itself: all three suites green. The guard
has two evidence sources and only one of them was covered. `self.loop_clamped`
is set from the `Decision` well above the write, so it survives the move — and
on the §10.3 runaway replay it is the source of every engagement the alarm
reports (0 write-path engagements against 2 loop-side ones), which is precisely
what made the move nearly invisible. `self.clamp_last` is assigned by
`write_setpoint()` and by nothing else (`main.py`), is `None` until it has run,
and is what `engaged_write` reads; moved above the write, that consumer goes
quietly false and a DEVICE-bound clamp — `max_w` above the inverter's own
rating, SAFETY-20's install — stops being reported by anything.
test_clamp.py gains part 6, and it grades the CONSEQUENCE rather than the
position: a spy subclassed from the shipped `ClampWatchdog` records what
`self.clamp_last` held each time the guard consulted it, over a slew-limited
ramp where every cycle's command is a different number, and asserts the
evidence is always the write of THAT cycle. Hooking `clamp_wd.note()` rather
than the guard's name is what keeps a rename legitimate; the same assertions
are re-run through a controller whose guard is renamed and whose write lives in
a helper, and were verified against `main.py` itself — renaming the method
throughout and moving both calls into a new `_emit_and_guard()` helper keeps
part 6 green, while swapping the two calls turns it red on a line that names
`clamp_last`. AC 3 needed a stimulus of its own: through the real `cycle()` the
write-path half can never engage, because the arbiter files the device range
and `max_w` as P_SAFETY LIMIT claims and `resolution.target_w` is already
inside both bounds before `write_setpoint()` re-applies them — that duplication
is deliberate defence in depth, and part 6 now measures the 0 engagements
rather than leaving it as a claim in a comment. So the write path is driven
directly at `max_w` 5000 against a ±2500 W device: the wire carries 2500 W,
every loop-side flag stays clean, and the alarm fires on `clamp_last` alone —
with the two calls swapped, the same stimulus clamps the same value and the
alarm is silent. `main.py` gains one comment sentence pointing at the check,
mirroring the one SAFETY-18's block already carries for test_freeze.py part 5.
**`max_w` and the `LIVE_MAX_W` literal beside it were unpinned in test_freeze.py
and test_tariff.py, the same gap TEST-06 closed one key over for
`meter_freeze_s` (TEST-10).** Measured before the fix: `max_w: 2000 -> 4500`