From a4a04d203c516e9d631bd48ce1d7eb085d264365 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 11:07:43 +0200 Subject: [PATCH] SAFETY-87 review: grade the three surviving review mutants A battery refusing only to charge stays able; the reserve alarm is graded on the real limit, not the aim; 'control still runs' is set on the two peak rows only. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019DBUfvndgHwaV339Fvd2gr --- goodwe_controller/test_safety87.py | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/goodwe_controller/test_safety87.py b/goodwe_controller/test_safety87.py index 2d5bede..b354775 100644 --- a/goodwe_controller/test_safety87.py +++ b/goodwe_controller/test_safety87.py @@ -656,6 +656,52 @@ check("⭐ a row that stops control outranks the running peak alarm: the " and "Meter down" in _both["banner"] and "until the alarm clears" in json.dumps(_both, ensure_ascii=False)) +for _inv, _s, _r in zip(_ctl2.inverters, (15.0, 60.0), (0, -1)): + _inv.soc, _inv.refused = _s, _r # B refuses only to CHARGE +_chg = _ctl2._peak_on_able(_lim_all, [MaintResult(IDLE, False, None, False, + [])] * 2) +_ctl2.inverters[1].refused = 0 +check("⭐ a battery that refuses only to charge can still give: it stays " + "able and carries the floor (2000 W of 2500 W)", + not _chg.floor_limited and _chg.floor_w == 2000.0 + and _chg.able_w == 2500.0 and _chg.held == (A,)) + + +def _short_for(held_w, secs=30.0): + """`_reserve_short()` over `secs` of a steady quarter: the meter at + `held_w` with the able batteries already at their full 2500 W.""" + s = types.SimpleNamespace(_reserve_batt_prev=None, + _reserve_high_since=None, + _peak_floor_limited=False, site_batt_w=2500.0) + g = dataclasses.replace(_lim, floor_w=2500.0, able_w=2500.0) + out = [] + for k in range(int(secs / 5.0) + 1): + s._peak_proj = types.SimpleNamespace( + block_start=rig.BASE, elapsed_s=450.0 + 5.0 * k, + projected_w=held_w, held_w=held_w) + out.append(Controller._reserve_short(s, g)) + return out + + +_aim_miss, _limit_miss = _short_for(3480.0), _short_for(3600.0) +check("⭐ the reserve alarm is graded on the real limit, not the aim: a " + "quarter ending at 3480 W (over the 3450 W aim, under 3500 W) never " + "alarms; one ending at 3600 W alarms once it holds ALARM_HOLD_S", + not any(_aim_miss) and not any(_limit_miss[:2]) + and all(_limit_miss[2:])) + +_runs_src = {} +for _f in ("main.py", "homeview.py"): + with open(os.path.join(HERE, "app", _f), encoding="utf-8") as _fh: + _src = _fh.read() + for _part in _src.split('out.append({')[1:]: + if '"runs": True' in _part.split("})", 1)[0]: + _runs_src.setdefault(_f, []).append( + _part.split('"id": "', 1)[-1].split('"', 1)[0]) +check("⭐ 'control still runs' is set on the two peak rows only (saturated " + "and reserve); no row that stops control carries it", + _runs_src == {"main.py": ["peak_guard", "peak_guard"]}) + print() if fails: print(f"{len(fails)} FAILED: " + ", ".join(fails))