Merge remote-tracking branch 'origin/release/1.0' into TEST-16
checks / unit (pull_request) Successful in 3m41s
checks / unit (pull_request) Successful in 3m41s
# Conflicts: # goodwe_controller/CHANGELOG.md
This commit is contained in:
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user