MAINT-15 round 2: in-repo differential check, full-hold with-reading check
checks / unit (pull_request) Successful in 35m8s
infra-pin / fresh (pull_request) Successful in 1s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 00:42:01 +02:00
co-authored by Claude Opus 5.5
parent cd1bf2a694
commit 35b2a699de
2 changed files with 56 additions and 8 deletions
@@ -37,7 +37,11 @@ line is true without a reading: it names minutes of hold, not a level.
- It does not check that the pack is still full at the end of the hold.
- It does not say why the reading was lost.
- With a reading, nothing changes (proved by a differential trace against
release/1.0 in `test_maint15.py`).
- With a reading, nothing changes. `test_maint15.py` section 7 compares the new
HOLD tick by tick with a frozen copy of the release/1.0 HOLD branch (phase,
setpoint, owner, events, `last_completed`) at four `hold_min` values. A wider
trace of the whole cycle (800 seeded runs against db9e231) was run outside the
repo; its script and log are kept under `scratch/MAINT-15/` of the dev
workspace, not in this repository.
- A state file written by an older version with `phase: hold` loads and ends on
time; the file is not changed in format.
+50 -6
View File
@@ -16,9 +16,7 @@ import tempfile
from datetime import datetime, timedelta, timezone
from app.maintenance import (
CHARGE, CHARGE_NEAR_FULL_PCT, CHARGE_TIMEOUT_MIN, CHARGE_WALL_TIMEOUT_MIN,
DRAIN, DRAIN_TIMEOUT_MIN, FAIL_BACKOFF_DAYS, HOLD, IDLE, NOSOC_NOTE_MIN,
MaintConfig, Maintenance, arbitrate,
CHARGE, DRAIN, HOLD, IDLE, NOSOC_NOTE_MIN, MaintConfig, Maintenance,
)
from app.store import DEFAULTS, Store
@@ -241,10 +239,56 @@ m = in_charge()
res, _, out, _ = run(m, T0, 600, None)
check("CHARGE with no SoC still keeps charging", out is None
and res.phase == CHARGE and res.charge_only)
def old_hold_branch(phase_started, last_completed, now, hold_min):
"""FROZEN copy of release/1.0 (db9e231) HOLD branch of step(), with a
reading: returns (phase, owns_setpoint, setpoint_w, events,
last_completed). Kept as a reference for the differential check below."""
elapsed = (now - phase_started).total_seconds() / 60.0
if elapsed >= hold_min:
return (IDLE, False, None,
[f"maintenance: cycle COMPLETE ({elapsed:.0f} min hold) "
"- handing back to the loop"], now)
return (HOLD, True, 0.0, [], last_completed)
print("7. with a reading: the new HOLD against the frozen release/1.0 branch")
for hm in (0, 5, 60, 120):
cfg = MaintConfig(enabled=True, interval_days=28, start_hour=19,
discharge_w=2000, charge_w=2000, soc_floor=11,
soc_target=100, hold_min=hm)
m = Maintenance(cfg, Mem())
m.force_start(T0)
m.tick(T0, 11.0)
m.tick(H0, 100.0)
started, last, same, end = H0, None, True, None
for i in range(0, hm + 15):
t = H0 + i * MIN
if m.phase != HOLD:
break
want = old_hold_branch(started, last, t, hm)
r = m.tick(t, 100.0 - (i % 3))
got = (r.phase, r.owns_setpoint, r.setpoint_w, r.events,
m.store.get_time("last_completed"))
same = same and got == want
last = want[4]
if r.phase == IDLE:
end = t
check(f"hold_min {hm}: every tick identical (phase, owns, W, events, "
"last_completed)", same)
check("...ends at hold_min, last_completed = that tick, one COMPLETE line",
end == H0 + hm * MIN and m.store.get_time("last_completed") == end
and m.phase == IDLE)
m = in_hold()
res, _, out, _ = run(m, H0, 30, 100.0)
check("HOLD with a reading is unchanged: 0 W, ends at hold_min", out is None
and res.setpoint_w == 0.0)
res, _, out, ev = run(m, H0, HOLD_MIN + 5, 100.0)
check("HOLD with a reading: 0 W until the end, then IDLE at hold_min",
out is not None and out[1] == H0 + HOLD_MIN * MIN and out[0].phase == IDLE)
check("...last_completed is that tick and the COMPLETE line is said once",
m.store.get_time("last_completed") == H0 + HOLD_MIN * MIN
and ev == ["maintenance: cycle COMPLETE (120 min hold) "
"- handing back to the loop"])
print()
if fails: