OPT-34 review: test that a battery leaving the plan leaves the latch
checks / unit (pull_request) Failing after 19m32s
infra-pin / fresh (pull_request) Successful in 1s

test_opt34.py part 4 drives Controller._latch_charge() directly: a battery
that leaves the plan is dropped at once, does not rejoin while nothing
charges, an empty latch is cleared from the store, and the screen says the
charge goes on only while the latched battery is below its level. Catches
two reviewer mutants that survived the round-2 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Claude (OPT-34 review)
2026-10-31 20:50:02 +01:00
parent eee7616022
commit 8f0af9f318
+59
View File
@@ -36,6 +36,8 @@ DECISIONS note and the dev report.
and before a charge has started, up and down;
e. the last hour, after the kept figure has ended at 06:00: the charge under
way goes on to its level, also over a restart; the screens say so.
3. The text, EN and NL.
4. `_latch_charge()` alone: a battery that leaves the plan leaves the latch.
"""
import asyncio
@@ -566,6 +568,63 @@ check("'kept for tonight' is unchanged (EN and NL)",
and nl["home_words"]["solar"]["kept"]
== "Zonneprognose van {at}, bewaard voor vannacht.")
# --------------------------------------------------------------------------- #
print("\n4. The latch alone: a battery that leaves the plan leaves the latch")
from types import SimpleNamespace # noqa: E402
from app import modetrade, priceruns # noqa: E402
_tmp = tempfile.mkdtemp(prefix="o34-")
lc = Controller(dict(opts()), rp.Hass(("a", "b"), lambda t: 300.0,
{"a": 40.0, "b": 40.0}),
rp._Store(), rp._Pub(), stop=asyncio.Event(),
peak_db_path=os.path.join(_tmp, "tariff.db"),
price_history_path=os.path.join(_tmp, "ph.json"))
n0 = at(4, 1, 0)
lruns = SimpleNamespace(cheap=priceruns.Run(at(3, 22, 0), at(4, 7, 0), 0.26))
charging = SimpleNamespace(action=modetrade.CHARGE,
roles={A: modetrade.CHARGE, B: modetrade.CHARGE})
resting = SimpleNamespace(action=None, roles={A: modetrade.HOLD,
B: modetrade.HOLD})
def latch_step(fc, tick, targets, now=n0):
lc.solar_fc, lc.mode_tick = fc, tick
lc._latch_charge(now, lruns, targets)
return lc._charge_latch, lc.store.data.get(m.CHARGE_LATCH_KEY)
FC = object() # a forecast is read (fresh or kept)
lt, saved = latch_step(FC, charging, {A: 80.0, B: 70.0})
check("both batteries charge with a forecast: both are latched at their "
"levels, to the run's end + one slot, and saved",
lt is not None and lt[1] == {A: 80.0, B: 70.0}
and lt[0] == at(4, 7, 15) and saved is not None
and saved["levels"] == {A: 80.0, B: 70.0})
lt, saved = latch_step(FC, charging, {B: 70.0})
check("A leaves the plan (its mode or use changed): A is dropped from the "
"latch at once, B stays", lt is not None and lt[1] == {B: 70.0}
and saved["levels"] == {B: 70.0})
lc.solar_fc = None
check("... and with the forecast then gone, A has no latched level (it is "
"not charged back to 80 %)", lc.plan_targets() == {B: 70.0})
lt, saved = latch_step(FC, resting, {A: 80.0, B: 70.0})
check("A back in the plan while nothing charges: it does not rejoin",
lt is not None and lt[1] == {B: 70.0})
lt, saved = latch_step(FC, resting, {A: 80.0})
check("B leaves the plan too: the latch is empty, so it is gone and "
"cleared from the store", lt is None and saved is None
and lc.plan_targets() is None)
latch_step(FC, charging, {A: 80.0})
lc.solar_fc = None
ia = next(i for i in lc.inverters if i.name == A)
ia.soc = 50.0
st_below = homeview.plan_state(lc)
ia.soc = 80.0
st_at = homeview.plan_state(lc)
check("the screen says the charge goes on only while the latched battery is "
f"below its level ({st_below!r} at 50 %, {st_at!r} at 80 %)",
st_below == "latched" and st_at == "no_forecast")
print()
if fails:
print(f"{len(fails)} FAILED:")