Merge pull request 'TEST-17: grade the status page's peak row at a stated month, not at today's' (#26) from TEST-17 into release/1.0

This commit was merged in pull request #26.
This commit is contained in:
2026-09-01 01:32:44 +02:00
3 changed files with 110 additions and 5 deletions
+35
View File
@@ -2,6 +2,41 @@
## Unreleased
**`release/1.0` went red at midnight on a commit nobody had touched (TEST-17).**
`Controller.checks()` read the capacity-tariff peak with `self.peak.peak()` - no
argument - so `tariff.py` resolved the month from `datetime.now()`, while
`test_tariff.py` section 10 banks its quarter at the fixed fixture
`2026-08-30`. From 1 September the page was correctly asked for September,
correctly answered `NO_RECORD`, and the row read *"no completed quarter recorded
yet"* - a string containing no `"peak"`, which the section's filter required.
Four checks failed. The suite had been a latent time bomb since TARIFF-01
merged on 2026-08-30 and every PR that ran it ran in August.
Isolated by holding the commit and the image constant and moving only the clock
under `libfaketime`: green at 2026-08-30, four failures at 2026-09-01, identical
at `f4914a1`, `04292c4~1` and `04292c4`. The calendar, not a regression.
**The product was right and the test was wrong**, so the fix does not change what
the live page says. `checks()` takes `when: datetime | None = None` and threads
it into `peak()` and `warning_frac()` - the convention every other peak query in
`tariff.py` already follows. The single production caller passes nothing and
still means today. Section 10 now grades at its own fixture's month.
Three new checks make the defect detectable rather than seasonal: the same
controller read at two different reference months must produce two different
rows, which no clock can satisfy; the later month must read `NO_RECORD` and stay
distinguishable from both an observed peak and an unreadable store; and calling
`checks()` with no argument must still mean today. Verified by mutation - a
`checks()` that drops `when` is now red at every clock tried, where the old
section went green in August. Full suite green, 15 files, at the real clock and
at faked clocks in four other months.
One thing left alone deliberately: only the UNREADABLE branch prefixes its row
with `capacity-tariff peak:`. The `NO_RECORD` row carries neither that phrase
nor the word `peak`, which is what made the original filter brittle and what
broke the first draft of the new checks too. It is product text with other
suites reading it, so it is noted here rather than changed.
**Thirty days of the real meter, and the acceptance criterion three cards
deferred is now a number (DATA-01).** TARIFF-01 and SAFETY-07 both merged with
AC 3 - *"monthly peak held under the soft limit in >= 95 % of intervals over a
+16 -3
View File
@@ -1270,7 +1270,20 @@ class Controller:
self.mqtt.close()
# -- UI ------------------------------------------------------------------
def checks(self) -> list:
def checks(self, when: datetime | None = None) -> list:
"""The status rows. `when` is the reference time for anything billed by
the calendar - today's month when it is None, which is what the live
page means; `status_payload()` calls this with no argument.
⚠️ IT EXISTS SO THE PEAK ROW CAN BE GRADED (TEST-17). Every other peak
query already takes it - `PeakStore.peak()`, `PeakTracker.peak()`,
`headroom_w()`, `warning_frac()` - and the row built from them was the
one thing that could only be read against `datetime.now()`. A suite
that banks an August quarter then asks this page what the peak is goes
green in August and red in September, which is what happened at 00:00
on 2026-09-01: TARIFF-01's own section 10, red on a calendar boundary,
on a commit nobody had touched for a day.
"""
o = self.o
out = []
if self.p1_enabled:
@@ -1331,14 +1344,14 @@ class Controller:
# a normal state on the 1st; "unreadable" is a broken instrument, and
# a status page that spelled them the same way would let a 0 W read
# look like a quiet month right up until SAFETY-07 shaved nothing.
_pk = self.peak.peak()
_pk = self.peak.peak(when)
if _pk.is_unreadable:
out.append({"ok": False, "warn": True,
"text": "capacity-tariff peak: " + _pk.explain()})
else:
# §9.1's "warning entity at 80 %", as a status row rather than
# a second entity - see the constructor on not doubling up.
_frac = self.peak.warning_frac() or 0.0
_frac = self.peak.warning_frac(when) or 0.0
_ok = _frac < 0.8
out.append({"ok": _ok, "warn": not _ok,
"text": _pk.explain()
+59 -2
View File
@@ -863,7 +863,12 @@ check("...and syncing the same quarter again banks nothing new, so the count "
check("...and the peak is unchanged by the repeat",
near(c.peak.peak(B).peak_w, 4998.0, 1e-6))
rows = [r for r in c.checks() if "capacity-tariff" in r["text"] or "peak" in r["text"]]
# ⚠️ `when=B`, AND THAT ARGUMENT IS THE TICKET (TEST-17). Without it `checks()`
# reads the peak for `datetime.now()`'s month while this fixture banked its
# quarter in August, so every check below passed in August and failed from
# 2026-09-01 - on a commit nobody had touched. The suite must grade the month
# it created, not the month the machine happens to be running in.
rows = [r for r in c.checks(when=B) if "capacity-tariff" in r["text"] or "peak" in r["text"]]
peak_rows = [r for r in rows if "peak" in r["text"] and "quarter(s) banked" in r["text"]]
print(" status row: " + (peak_rows[0]["text"] if peak_rows else "(none)"))
check("the status page carries exactly one capacity-tariff peak row",
@@ -879,7 +884,7 @@ check("...and it is FLAGGED, because 4998 W is over §9.1's 80 % warning point "
# The half that matters: the same page with a store that cannot answer.
c2 = built(TMP / "no-such-dir" / "nope.db")
u_rows = [r for r in c2.checks() if "capacity-tariff peak" in r["text"]]
u_rows = [r for r in c2.checks(when=B) if "capacity-tariff peak" in r["text"]]
print(" status row (unreadable store): "
+ (u_rows[0]["text"] if u_rows else "(none)"))
check("an unreadable store gets its OWN row, saying UNREADABLE", len(u_rows) == 1)
@@ -891,6 +896,58 @@ check("...and the healthy row and the unreadable row are different text",
bool(u_rows) and bool(peak_rows)
and u_rows[0]["text"] != peak_rows[0]["text"])
# --- TEST-17: the page must be graded at a stated time, and prove it ----------
# Three ways this can regress, so three checks. The first two would both go
# green again if `checks()` quietly went back to reading the clock, which is
# exactly the mutation this section could not previously detect.
# ⚠️ SELECTED ON "quarter(s) banked", NOT ON "peak". The NO_RECORD row reads
# "2026-10: no completed quarter recorded yet - ..." and contains neither the
# word "peak" nor "capacity-tariff" - only the UNREADABLE branch prefixes the
# family name. Filtering these rows the way the checks above do would drop the
# row and then grade its absence, which is the very mistake this ticket fixes.
# (That asymmetry in the row's own text is worth a look on its own; it is what
# made the original filter brittle. Not changed here - it is product text with
# other suites reading it.)
_peak_rows = lambda rows: [r for r in rows if "quarter(s) banked" in r["text"]]
_later = B + timedelta(days=45) # same fixture, a later billing month
_other_peak = _peak_rows(c.checks(when=_later))
print(f" the SAME controller read at {_later.date()}: "
+ (_other_peak[0]["text"] if _other_peak else "(no peak row)"))
check("asked for a LATER month, the same controller reports that month's "
"absence of a quarter, not August's 4998 W - the row follows `when`",
bool(_other_peak) and "4998 W" not in _other_peak[0]["text"]
and "no completed quarter recorded yet" in _other_peak[0]["text"])
check("...and that is NO_RECORD, distinguishable from BOTH an observed peak and "
"an unreadable store - a month with no quarter is not a broken instrument",
bool(_other_peak) and bool(peak_rows)
and "UNREADABLE" not in _other_peak[0]["text"]
and _other_peak[0]["text"] != peak_rows[0]["text"])
check("...and it is not FLAGGED, because an empty month is not an exceedance",
bool(_other_peak) and _other_peak[0]["ok"] and not _other_peak[0]["warn"])
# The mutation guard. `checks()` ignoring `when` is the whole defect, and it is
# invisible to every check above whenever the machine's clock happens to sit in
# the fixture's own month - which is how this shipped. Grading two DIFFERENT
# reference months against each other cannot be satisfied by a clock at all.
check("the peak row differs between two reference months, so a `checks()` that "
"dropped `when` and read datetime.now() would fail HERE regardless of "
"what day the suite is run on",
bool(peak_rows) and bool(_other_peak)
and peak_rows[0]["text"] != _other_peak[0]["text"])
# And the production meaning is unchanged: no argument still means today.
_now_peak = _peak_rows(c.checks())
_today_is_fixture_month = month_key(datetime.now(timezone.utc)) == month_key(B)
check("calling checks() with NO argument still reads the month being billed "
"today - the live status page's meaning is untouched",
bool(_now_peak) and (bool(peak_rows)
and _now_peak[0]["text"] == peak_rows[0]["text"]
if _today_is_fixture_month
else "no completed quarter recorded yet"
in _now_peak[0]["text"]))
# THE MEASUREMENT IS OF DELIVERED POWER (SAFETY-20). Graded on the parsed
# SYNTAX, not on the text: the module docstring argues this at length and names
# every forbidden word while doing so, so a grep over the source would be