TEL-04: make a missing build() guard fail legibly instead of aborting
Review point from the non-vacuity run. Deleting build()'s "is the net entity cached at all" guard makes build() raise KeyError rather than return False. That still failed the suite, but by aborting it with a traceback at whichever check ran first - a red that costs the next person ten minutes deciding whether the suite is broken or the code is. New `built()` helper in test_p1.py wraps build() and turns an escaping exception into a returned value, so the comparison against True/False fails by name. Applied only to the ha_signed section; TEL-01's own checks are untouched. Mutation 4 before: 0 named checks red, aborted at check 123 of 174. Mutation 4 after: 2 named checks red - "nothing cached yet builds nothing" and "an unavailable entity does not build a sample" - all 174 reached. Still 174 checks, all green, and the other nine mutations are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa
This commit is contained in:
co-authored by
Claude Opus 5
parent
8b51a51e20
commit
632be44f6c
@@ -57,6 +57,25 @@ def raises(name, fn):
|
||||
fails.append(name)
|
||||
|
||||
|
||||
def built(src):
|
||||
"""`src.build()`, with any escaping exception turned into a visible value.
|
||||
|
||||
⚠️ Legibility of a RED, not leniency. build() is contracted to return a bool
|
||||
and to funnel every bad telegram through ingest.reject() - a guard that goes
|
||||
missing (say the "is the net entity cached at all" one) makes it raise
|
||||
instead. That still fails the suite, but by aborting it with a traceback at
|
||||
whichever check happened to run first, which costs the next person ten
|
||||
minutes deciding whether the suite is broken or the code is. Returning the
|
||||
exception makes it compare unequal to True/False, so the NAMED check goes red
|
||||
and says which rule died.
|
||||
"""
|
||||
try:
|
||||
return src.build()
|
||||
except Exception as err: # noqa: BLE001 - a raise here is itself the failure
|
||||
print(f" build() raised {type(err).__name__}: {err}")
|
||||
return err
|
||||
|
||||
|
||||
def sample(net_import, net_export=0.0, at=BASE, phases=1, pi=None, pe=None):
|
||||
return make_sample(SOURCE_HA, net_import, net_export, phases=phases,
|
||||
phase_import_w=pi, phase_export_w=pe,
|
||||
@@ -614,20 +633,20 @@ NET = {"net": "sensor.p1_meter_active_power", "phase_net": []}
|
||||
ing = P1Ingest(phases=1, max_age_s=30.0)
|
||||
sig = HaSignedSource(None, ing, NET, token="x")
|
||||
|
||||
check("nothing cached yet builds nothing", sig.build() is False and ing.last is None)
|
||||
check("nothing cached yet builds nothing", built(sig) is False and ing.last is None)
|
||||
sig._absorb("sensor.p1_meter_active_power", "1000")
|
||||
check("one signed entity is a complete telegram on its own",
|
||||
sig.build() is True and ing.net_w == 1000.0)
|
||||
built(sig) is True and ing.net_w == 1000.0)
|
||||
check("the sample is tagged with its own transport",
|
||||
ing.last.source == SOURCE_HA_SIGNED)
|
||||
sig._absorb("sensor.p1_meter_active_power", "-2500")
|
||||
sig.build()
|
||||
built(sig)
|
||||
check("a negative state lands as a negative net", ing.net_w == -2500.0)
|
||||
|
||||
before = ing.last
|
||||
sig._absorb("sensor.p1_meter_active_power", "unavailable")
|
||||
check("an unavailable signed entity is a parse error", ing.parse_errors == 1)
|
||||
check("an unavailable entity does not build a sample", sig.build() is False)
|
||||
check("an unavailable entity does not build a sample", built(sig) is False)
|
||||
# ⚠️ The rule the whole ticket turns on: a missing reading is MISSING. Resolving
|
||||
# it to 0 W would read as a perfectly balanced house and defeat the staleness
|
||||
# trigger that FW-01's watchdog is built on.
|
||||
@@ -640,7 +659,7 @@ check("a non-numeric signed state is a parse error, not 0 W",
|
||||
ing.parse_errors == 3 and ing.net_w == -2500.0)
|
||||
sig._absorb("sensor.p1_meter_active_power", "64954")
|
||||
check("64954 is refused at the signed transport too",
|
||||
sig.build() is False and ing.parse_errors == 4)
|
||||
built(sig) is False and ing.parse_errors == 4)
|
||||
sig._absorb("sensor.not_ours", "123")
|
||||
check("an unsubscribed entity is never cached by the signed transport",
|
||||
"sensor.not_ours" not in sig.cache)
|
||||
@@ -652,7 +671,7 @@ sig = HaSignedSource(None, ing, dict(NET), token="x")
|
||||
ing.submit(make_sample(SOURCE_HA_SIGNED, 1200, 0, phases=1,
|
||||
ingest_mono=time.monotonic() - 20.0))
|
||||
sig._absorb("sensor.p1_meter_active_power", "unavailable")
|
||||
sig.build()
|
||||
built(sig)
|
||||
check("a rejected reading does not reset the published age",
|
||||
ing.published_age_s > 19 and ing.net_w == 1200.0)
|
||||
ing.submit(make_sample(SOURCE_HA_SIGNED, 1200, 0, phases=1,
|
||||
@@ -669,10 +688,10 @@ NET3 = {"net": "sensor.p1_meter_active_power",
|
||||
sig3 = HaSignedSource(None, ing3, NET3, token="x")
|
||||
for eid, val in (("sensor.p1_meter_active_power", "187"), ("sensor.p1_l1", "2301")):
|
||||
sig3._absorb(eid, val)
|
||||
check("an incomplete signed phase set waits instead of guessing", sig3.build() is False)
|
||||
check("an incomplete signed phase set waits instead of guessing", built(sig3) is False)
|
||||
sig3._absorb("sensor.p1_l2", "468")
|
||||
sig3._absorb("sensor.p1_l3", "-2582")
|
||||
check("a complete signed three-phase set builds", sig3.build() is True)
|
||||
check("a complete signed three-phase set builds", built(sig3) is True)
|
||||
check("signed per-phase entities keep the exporting phase negative",
|
||||
ing3.last.per_phase_w == (2301.0, 468.0, -2582.0))
|
||||
check("per-phase IMPORT clamps the exporting phase to zero",
|
||||
@@ -688,7 +707,7 @@ sig_bad = HaSignedSource(None, P1Ingest(phases=3, max_age_s=30.0),
|
||||
for eid in ("sensor.net", "sensor.a", "sensor.b"):
|
||||
sig_bad._absorb(eid, "100")
|
||||
check("two phases delivered against meter_phases 3 is rejected, not padded",
|
||||
sig_bad.build() is False and sig_bad.ingest.last is None
|
||||
built(sig_bad) is False and sig_bad.ingest.last is None
|
||||
and sig_bad.ingest.parse_errors == 1)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user