From dd5eb6fdc8840a2f7e35050017702f9d19afb339 Mon Sep 17 00:00:00 2001 From: glenn schrooyen Date: Sun, 4 Oct 2026 01:17:52 +0200 Subject: [PATCH] SAFETY-94 review: tests for three surviving mutants; record two gaps - test_safety94 part 4: time under 250 W is not counted; a command that swaps direction every cycle is never refusal evidence; a reading moving away from the ask does not restart the evidence. These kill review mutants R1, R3 and R6, which survived the PR's tests. - DECISIONS: at N=1 a battery with B's standby offset no longer latches a refusal (SAFETY-97 now covers N=1); a part-time ask is caught after 35/d s of wall time. Co-Authored-By: Claude Opus 5.5 --- .../DECISIONS/SAFETY-94-lagging-battery.md | 12 +++++++-- goodwe_controller/test_safety94.py | 25 ++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/goodwe_controller/DECISIONS/SAFETY-94-lagging-battery.md b/goodwe_controller/DECISIONS/SAFETY-94-lagging-battery.md index fce3371..1059e22 100644 --- a/goodwe_controller/DECISIONS/SAFETY-94-lagging-battery.md +++ b/goodwe_controller/DECISIONS/SAFETY-94-lagging-battery.md @@ -88,7 +88,15 @@ the held number for one 35 s window of asking, so 35 < 75 < 85 still holds. ## Known gaps - B's standby offset (-95 to -118 W at idle) puts a refusing B outside the - 100 W band some of the time, so a refusing B may not latch. Pre-existing, - not touched here. + 100 W band some of the time, so a refusing B may not latch (SAFETY-97). + At N=2 this is unchanged. **At N=1 it is new:** the old one-instant check + caught such a battery in 17-105 s; the shared rule needs 12 cycles in the + band in a row and, on the review probe, never latched in 1200 s. +- A command that asks only part of the time (a fraction d) is caught after + 35 s of asking, which is 35/d s of wall time (0/1500 W every 5 s: 69 s). + SAFETY-30's per-unit check restarts when its command stops opposing the + held reading, which a lapsing ask normally does, so 35 < 85 holds there. + SAFETY-31's circulation check (75 s) ignores the command, so its margin + holds only while d >= 0.47. - A register frozen under a command that stays near the 250 W travel edge is caught late or not at all (survivor 2). diff --git a/goodwe_controller/test_safety94.py b/goodwe_controller/test_safety94.py index 40baf86..b7a2e9e 100644 --- a/goodwe_controller/test_safety94.py +++ b/goodwe_controller/test_safety94.py @@ -286,6 +286,18 @@ for t in range(0, 600): check("a command that never asks 250 W is never an ask (SAFETY-52's 200 W " "probe cannot arm the gate)", not f.stale and f.obliged_s() == 0.0) +# Review: time under 250 W is not counted, even after the command travelled. +f = Freshness(BOUNDS["batt"]) +for t in range(0, 61): + f.submit(-300.0, -300.0 if t == 0 else 200.0, float(t)) +for t in range(61, 131): + f.submit(-300.0, 260.0, float(t)) + if t == 90: + early = f.stale +check("a command that has travelled 500 W but stands at 200 W for a minute " + "adds nothing: 30 s after it reaches 260 W the register is not stale, " + "and by 70 s it is", + not early and f.stale) # compute(): `gave_nothing` gates the N=1 engage; None keeps the old law. T = control.Tuning(max_w=4000.0, slew_w=400.0) @@ -307,20 +319,21 @@ class _Inv: def __init__(self): self.batt, self.ref_count, self.ref_since, self.ref_sign = 0.0, 0, None, 0 self.ref_from = None + self.ask = 800.0 def standing_w(self): - return 800.0 + return self.ask class _C: _now_mono = 0.0 -def evidence(readings, tick=1.0): +def evidence(readings, tick=1.0, ask=lambda i: 800.0): c, inv = _C(), _Inv() got = [] for i, b in enumerate(readings): - c._now_mono, inv.batt = i * tick, b + c._now_mono, inv.batt, inv.ask = i * tick, b, ask(i) got.append(app_main.Controller._gave_nothing(c, inv)) return got @@ -330,6 +343,12 @@ climb = evidence([-95.0 + 4.0 * i for i in range(48)]) check("a flat reading (+-1 W) under an 800 W ask is evidence after 22 s; one " "climbing 4 W/s through the band is never", 1 in flat and flat.index(1) == 22 and not any(climb)) +flap = evidence([-4.0] * 60, ask=lambda i: 800.0 if i % 2 else -800.0) +away = evidence([40.0] * 5 + [-40.0] * 25) +check("a command that swaps direction every cycle is never evidence, and a " + "reading moving 80 W away from the ask does not restart it (latched at " + "22 s, counted from the first reading)", + not any(flap) and 1 in away and away.index(1) == 22) check("REFUSED_PROGRESS_W is under the band's width and over the live " "readings' rest wander (25 W)", 25.0 < app_main.REFUSED_PROGRESS_W < 2 * control.REFUSED_BELOW_W)