From 88ebf3a5a6dff08c94a9b0b0c311ee318273eb57 Mon Sep 17 00:00:00 2001 From: SAFETY-97 review Date: Sun, 4 Oct 2026 14:29:27 +0200 Subject: [PATCH] SAFETY-97 review: tests for four surviving mutants The N=1 stale-path refusal and SAFETY-42's parked test are centred on the idle reading; learning stops while control is stopped or the sign watchdog has latched. Each had no check (review mutants X7, X8, X13, X14). Co-Authored-By: Claude Opus 5.5 --- goodwe_controller/test_safety97.py | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/goodwe_controller/test_safety97.py b/goodwe_controller/test_safety97.py index f3bf095..003b3d8 100644 --- a/goodwe_controller/test_safety97.py +++ b/goodwe_controller/test_safety97.py @@ -141,6 +141,37 @@ check("...and at a 5 s meter the stale trip on B's held -110 W is a refusal " "of B (one trip), not a repeating site stop", len(st) == 1 and ref == st) +# Review: the two centrings with no check of their own. +# N=1, 5 s meter: the stale trip on a held -110 W idle reading is a refusal. +saved = dict(rig.OFFSET) +rig.OFFSET["a"] = -110.0 +try: + r = rig.run(1, lambda t: 2500.0 if t >= 600 else 0.0, 800.0, tick=5.0, + publish_s=1.0, dither=True, freeze_at=lambda u: 300.0) +finally: + rig.OFFSET.clear() + rig.OFFSET.update(saved) +st = [t for t, _u in r["stale"]] +ref = [t for t, _u in r["refused"]] +print(f" N=1, 5 s meter, register frozen on -110 W: stale {st}, refused " + f"{ref}") +check("...and at N=1 the same stale trip is a refusal (one trip), not a " + "repeating site stop", len(st) == 1 and ref == st) + +# SAFETY-42: B asked under 200 W while giving nothing (its idle reading) is +# parked, not named as not delivering; A carries the house. +def small_ask(): + return rig.run(2, lambda t: 150.0, 700.0, tick=1.0, noise=4.0, + store={unit_key("inverter-b", "idle_w"): -100.0}, + cut=lambda u, t: u == "b")["c"].inverters[1] + + +b, b_old = small_ask(), without_fix(small_ask) +check("SAFETY-42: B giving nothing at a small ask, idle -100 W, is parked " + "(not latched as not delivering in 700 s; with idle 0 W it is)", + not b.delivery_wd.tripped and b.idle_w == -100.0 + and b_old.delivery_wd.tripped) + # ============================================================================ # print("\n2. a healthy B with its offset is never refused or flagged") SHAPES = ((10.0, False), (1.0, True)) @@ -224,6 +255,25 @@ check("⭐ not learned while refused: 800 s held at 0 W reading -160 W leaves " "B's idle reading at -100 W", _t is not None and b.refused and abs(b.idle_w + 100.0) < 1.0) +# Not learned while control is stopped (another owner may drive the battery) +# or while the unit's sign watchdog has latched. +class _Stopped(app_main.Controller): + auto = property(lambda self: False, lambda self, v: None) + + +rig.Controller = _Stopped +try: + r = rig.run(2, lambda t: 0.0, 600.0, tick=1.0) +finally: + rig.Controller = app_main.Controller +r2 = rig.run(2, lambda t: 0.0, 600.0, tick=1.0, + store={unit_key("inverter-b", "sign_tripped"): True}) +check("not learned while control is stopped, nor on a unit whose sign " + "watchdog has latched (600 s at 0 W); its sibling learns", + all(i.idle.value is None for i in r["c"].inverters) + and r2["c"].inverters[1].idle.value is None + and r2["c"].inverters[0].idle.value is not None) + # A battery capped at 150 W under a big ask is not idle. x = idle.IdleReading() for t in range(0, 3600):