TEST-06 review follow-ups: two notification surfaces, the real ordering boundary, the schema bounds
1. notes_of() could not tell this guard's alarm from anybody else's. It selected by SERVICE, so against #12's app/ (which adds goodwe_output_clamp) eight assertions went red, AC1's core check among them. Blanket id-keying is not the repair and was measured, not guessed: selector vs #12's app/ id renamed by service (as committed) 8 red 1 red by id alone 3 red 6 red by id or title (shipped) 3 red 1 red all_notes_of() is the whole surface; notes_of() is this guard's, recognised by the id it should carry OR the title it writes - the `or` is what keeps the one assertion whose subject IS the id able to fail on its own. The three surface-wide claims stay surface-wide, each with a comment naming which surface it is about and why the next guard's author must decide rather than narrow them. They are the three that go red against #12, which is the correct signal. 2. The ordering comment named the wrong boundary. N3 - moving the call to immediately before write_setpoint() - is green and correctly so: self.target is assigned two lines earlier, write_setpoint() clamps only a local and can skip the write entirely. So the invariant is "after `self.target = resolution.target_w`", and the detector grades pre-clamp INTENT, not the wire. Corrected in test_freeze.py and in main.py's own note above the call (comment-only; I merge second and own the conflict). N8 - moving the call above the assignment - is RED 4. 3. The schema range's own bounds are now pinned. int(60,3600) -> int(1,86400) survived green: an operator could set meter_freeze_s: 86400 and the guard is dead with nothing red. Both bounds are re-derived rather than restated. 4. find_hwsim() crashed the whole suite on a checkout one level below the root (HERE.parents[1] -> IndexError, reproduced at C:/goodwe_controller). Guarded. And part 3 now names its transport in the summary: the rig hwsim and the stub are not equivalent - the rig path failed 1 run in 3 - so a green log has to say which one backed it. Also: the "revived transport re-rails" check is labelled a PRECONDITION like its sibling; freeze_rows() records that flipping p1_enabled exercises the row's content and never its gate; and the note about the deleted assertion is corrected - it was REDUNDANT, not undecidable. The reviewer falsified it in one line (move the hass.call above `if not fired: return`, RED 9). "I could not construct a falsifier, therefore it cannot fail" was exactly the inference this file exists to distrust. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC
This commit is contained in:
co-authored by
Claude Opus 5
parent
c34b5a2922
commit
611d3dbcdf
@@ -406,12 +406,22 @@ class Controller:
|
||||
self.reason = resolution.explain()
|
||||
await self.write_setpoint(self.target)
|
||||
|
||||
# --- SAFETY-18, and it runs LAST on purpose --------------------------
|
||||
# Both terms of the conjunction describe the state that now exists: the
|
||||
# command that has just gone on the wire, and how long the meter that
|
||||
# chose it has been standing still. Evaluated before write_setpoint()
|
||||
# it would be reasoning about the PREVIOUS command against THIS cycle's
|
||||
# meter - the one combination that is true of neither moment.
|
||||
# --- SAFETY-18, and it runs AFTER `self.target` is assigned ----------
|
||||
# ⚠️ THE BOUNDARY IS THE ASSIGNMENT ABOVE, NOT write_setpoint(). This
|
||||
# comment used to say "the command that has just gone on the wire",
|
||||
# which is wrong twice over and was corrected by TEST-06's reviewer:
|
||||
# `check_meter_frozen()` grades `self.target` - the arbiter's resolved
|
||||
# intent - and `write_setpoint()` clamps only a local `value` and can
|
||||
# skip the write entirely, so that number may never have gone anywhere.
|
||||
#
|
||||
# What both terms of the conjunction must describe is the SAME moment:
|
||||
# the command this cycle resolved, against how long the meter that
|
||||
# chose it has been standing still. Evaluated before
|
||||
# `self.target = resolution.target_w` it would grade the PREVIOUS
|
||||
# cycle's command against THIS cycle's meter - the one combination that
|
||||
# was true of neither moment. Moving it between the assignment and
|
||||
# write_setpoint() is therefore harmless; moving it above the
|
||||
# assignment is not, and test_freeze.py part 5 fails on exactly that.
|
||||
await self.check_meter_frozen()
|
||||
|
||||
async def check_meter_frozen(self) -> None:
|
||||
|
||||
@@ -349,11 +349,19 @@ check("...and the same replay DOES alarm once both terms are made reachable",
|
||||
# 3. a genuinely frozen meter, over real HTTP, through the real transport
|
||||
# --------------------------------------------------------------------------- #
|
||||
def find_hwsim():
|
||||
"""The rig's own fake meter, if the infra repo is reachable."""
|
||||
"""The rig's own fake meter, if the infra repo is reachable.
|
||||
|
||||
⚠️ `HERE.parents[1]` only exists when this package sits at least two levels
|
||||
below the filesystem root - it raised IndexError and took the WHOLE suite
|
||||
down on a checkout at `C:/goodwe_controller`, which is a failure of the
|
||||
lookup, not of the thing being tested. Guarded, so a shallow checkout falls
|
||||
through to the stub like any other missing sibling repo.
|
||||
"""
|
||||
cands = []
|
||||
if os.environ.get("GOODWE_SIM"):
|
||||
cands.append(Path(os.environ["GOODWE_SIM"]) / "hwsim.py")
|
||||
cands.append(HERE.parents[1] / "goodwe" / "sim" / "hwsim.py")
|
||||
if len(HERE.parents) > 1:
|
||||
cands.append(HERE.parents[1] / "goodwe" / "sim" / "hwsim.py")
|
||||
for c in cands:
|
||||
if c.exists():
|
||||
return c
|
||||
@@ -451,7 +459,15 @@ def with_server(argv, port, fn):
|
||||
|
||||
|
||||
hw = find_hwsim()
|
||||
print(f"frozen meter over HTTP - using {'the rig hwsim at ' + str(hw) if hw else 'the local stub'}")
|
||||
# ⚠️ WHICH TRANSPORT BACKED PART 3 IS PART OF THE RESULT, and it is repeated in
|
||||
# the summary at the end of the run. The two are not equivalent: pointed at the
|
||||
# real `sim/hwsim.py` the rig path failed 1 run in 3 (a 16 s window against a
|
||||
# 10 s hold, when hwsim.serve() is slow to start), while the stub is tuned never
|
||||
# to fail. A green log that does not say which one ran is a green log that can
|
||||
# have quietly substituted the weaker stand-in - the same shape as an assertion
|
||||
# that cannot fail, from the other end.
|
||||
PART3_TRANSPORT = f"the rig hwsim at {hw}" if hw else "the local stub"
|
||||
print(f"frozen meter over HTTP - using {PART3_TRANSPORT}")
|
||||
if hw:
|
||||
def argv_for(mode):
|
||||
return [HWSIM_DRIVER, "8791" if mode == "freeze" else "8792", mode,
|
||||
@@ -574,12 +590,65 @@ def _opts(**over):
|
||||
return o
|
||||
|
||||
|
||||
def notes_of(hass):
|
||||
"""The HA persistent notifications that have left the process so far."""
|
||||
# ⚠️ TWO SURFACES, AND EVERY ASSERTION BELOW SAYS WHICH ONE IT IS ABOUT.
|
||||
# The first cut of this helper selected by SERVICE, which measured "any guard's
|
||||
# notification" while every assertion built on it was named for THIS guard's.
|
||||
# That was true only because `check_meter_frozen()` was the sole
|
||||
# `persistent_notification.create` in `app/`; SAFETY-03 (#12) adds a second one
|
||||
# (`goodwe_output_clamp`) raised by scenarios that rail the command on purpose,
|
||||
# and eight assertions here went red against it - AC1's core check included.
|
||||
#
|
||||
# Blanket id-keying is NOT the repair. Three of those eight are claims about the
|
||||
# WHOLE notification surface, and narrowing them to this guard's id would have
|
||||
# silently shrunk a claim rather than fixed a conflation - which is the defect
|
||||
# class this card exists to close. So both surfaces are available by name, and
|
||||
# each assertion picks one deliberately.
|
||||
FREEZE_NOTE_ID = "goodwe_meter_frozen"
|
||||
|
||||
|
||||
def all_notes_of(hass):
|
||||
"""EVERY HA persistent notification raised this run, whoever raised it."""
|
||||
return [d for dom, svc, d in hass.calls
|
||||
if (dom, svc) == ("persistent_notification", "create")]
|
||||
|
||||
|
||||
def _is_freeze_note(d):
|
||||
"""Is this notification the freeze detector's?
|
||||
|
||||
⚠️ THE ID **OR** THE TITLE, and the `or` is measured rather than tidy.
|
||||
Keyed on the id alone, renaming the id empties every list in this file and
|
||||
SIX assertions collapse together - so the one check whose subject IS the id
|
||||
could no longer fail on its own, in the file about assertions that cannot
|
||||
fail. Recognising the guard by what it says as well leaves the rename
|
||||
exactly one red: the id's own check. Measured both ways:
|
||||
|
||||
selector vs #12's app/ id renamed
|
||||
by service (the first cut) 8 red 1 red
|
||||
by id alone 3 red 6 red
|
||||
by id or title (this) 3 red 1 red
|
||||
|
||||
The union cannot re-admit another guard's alarm unless that guard also
|
||||
calls itself frozen, which would be a confusable alarm on its own account.
|
||||
"""
|
||||
return (d.get("notification_id") == FREEZE_NOTE_ID
|
||||
or "frozen" in str(d.get("title", "")).lower())
|
||||
|
||||
|
||||
def notes_of(hass):
|
||||
"""Only THIS guard's - see _is_freeze_note()."""
|
||||
return [d for d in all_notes_of(hass) if _is_freeze_note(d)]
|
||||
|
||||
|
||||
def notes_by_text_of(hass):
|
||||
"""This guard's, recognised by what it SAYS and never by its id.
|
||||
|
||||
Exists for the two assertions whose subject IS the id: selected by the id,
|
||||
both would be true by construction.
|
||||
"""
|
||||
return [d for d in all_notes_of(hass)
|
||||
if "frozen" in str(d.get("title", "")).lower()]
|
||||
|
||||
|
||||
def wired(unchanged_s, grid, cycles=60, **over):
|
||||
"""Returns (hass, controller, trace).
|
||||
|
||||
@@ -604,7 +673,8 @@ def wired(unchanged_s, grid, cycles=60, **over):
|
||||
|
||||
|
||||
hass_f, ctl_f, trace_f = wired(3600.0, 4000.0)
|
||||
notes = notes_of(hass_f)
|
||||
notes = notes_of(hass_f) # THIS guard's
|
||||
all_notes = all_notes_of(hass_f) # the whole surface
|
||||
# The status row lives behind `p1_enabled`, which is what a homewizard_local
|
||||
# install has. Flipped here rather than plumbing a whole P1 transport in - the
|
||||
# row's own content is what is under test.
|
||||
@@ -612,9 +682,22 @@ ctl_f.p1_enabled = True
|
||||
rows = [c for c in ctl_f.checks() if "METER MAY BE FROZEN" in c["text"]]
|
||||
print(f" frozen at +4000 W: command ran to {ctl_f.target:.0f} W, "
|
||||
f"{len(notes)} notification(s), {len(rows)} red status row(s)")
|
||||
check("the shipped cycle() raises exactly one HA notification", len(notes) == 1)
|
||||
# SURFACE-WIDE, deliberately. "The controller alarmed once" is a different and
|
||||
# stronger claim than "the freeze guard alarmed once", and it is the one this
|
||||
# row has always made. When a second guard lands (SAFETY-03's
|
||||
# `goodwe_output_clamp` is the first), this assertion is where somebody decides
|
||||
# whether the scenario is still meant to raise exactly one alarm in total - a
|
||||
# decision, made here, rather than a claim quietly narrowed to keep it green.
|
||||
check("the whole notification surface carried exactly one alarm this run",
|
||||
len(all_notes) == 1)
|
||||
# THIS GUARD'S, keyed on the id.
|
||||
check("...and the freeze detector is the one that raised it", len(notes) == 1)
|
||||
# ⚠️ Selected by TEXT, not by id - see notes_by_text_of(). This is the only
|
||||
# assertion whose subject is the id, so it is the one that must survive being
|
||||
# the only thing a rename breaks.
|
||||
check("...with a fixed notification_id so repeats replace, not stack",
|
||||
notes and notes[0].get("notification_id") == "goodwe_meter_frozen")
|
||||
[d.get("notification_id") for d in notes_by_text_of(hass_f)]
|
||||
== [FREEZE_NOTE_ID])
|
||||
check("...and it says the controller is still running",
|
||||
bool(notes) and "STILL RUNNING" in notes[0]["message"])
|
||||
check("the status page carries a failed row saying so",
|
||||
@@ -625,39 +708,66 @@ check("the event log names it", any("METER FROZEN" in e for e in ctl_f.events))
|
||||
check("and it does NOT command 0 W - this cut is an alarm only",
|
||||
abs(ctl_f.target) >= rail_w(LIVE_MAX_W))
|
||||
|
||||
# ⚠️ THESE THREE ARE SURFACE-WIDE ON PURPOSE, and they are the three that go red
|
||||
# against #12's `app/`. That is the correct signal and not a bug in them: each
|
||||
# says "this scenario is quiet", which is a stronger claim than "this guard is
|
||||
# quiet", and it is the claim a reader of a converged install wants. Whoever
|
||||
# lands a second notifying guard has to come here and decide, per scenario,
|
||||
# whether the new alarm belongs in it - `notes_of()` is one word away for the
|
||||
# narrower reading, and choosing it must be a decision with a reason.
|
||||
# (They also used to match `svc == "create"` in ANY domain, which was looser
|
||||
# still.)
|
||||
hass_n, ctl_n, _ = wired(3600.0, -14.2)
|
||||
check("wired up, TEL-05's converged case still raises nothing",
|
||||
not [1 for dom, svc, _ in hass_n.calls if svc == "create"])
|
||||
check("wired up, TEL-05's converged case leaves the whole surface silent",
|
||||
not all_notes_of(hass_n))
|
||||
hass_m, ctl_m, _ = wired(2.0, 8000.0)
|
||||
check("wired up, a real railed load against a moving meter raises nothing",
|
||||
not [1 for dom, svc, _ in hass_m.calls if svc == "create"]
|
||||
and abs(ctl_m.target) >= rail_w(LIVE_MAX_W))
|
||||
check("an uncovered transport (no unchanged_s) raises no notification",
|
||||
not [1 for dom, svc, _ in wired(None, 4000.0)[0].calls if svc == "create"])
|
||||
check("wired up, a real railed load against a moving meter leaves the whole "
|
||||
"surface silent",
|
||||
not all_notes_of(hass_m) and abs(ctl_m.target) >= rail_w(LIVE_MAX_W))
|
||||
check("an uncovered transport (no unchanged_s) leaves the whole surface silent",
|
||||
not all_notes_of(wired(None, 4000.0)[0]))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 5. WHERE in cycle() the detector is evaluated (TEST-06 / AC1)
|
||||
# --------------------------------------------------------------------------- #
|
||||
# ⚠️ WHY THIS IS NOT STYLE. `check_meter_frozen()` runs LAST, after
|
||||
# `write_setpoint()`, and moving it to the top of `cycle()` used to leave this
|
||||
# whole file green. It is not a tidiness preference, it is the reason two
|
||||
# separate compositions are safe, and each is asserted here by an OBSERVABLE
|
||||
# CONSEQUENCE rather than by the source order - so a refactor that keeps the
|
||||
# semantics (renaming it, inlining it, calling it from a helper that runs at the
|
||||
# end) stays green, and one that changes WHICH command it judges goes red:
|
||||
# ⚠️ WHERE THE BOUNDARY ACTUALLY IS - AND IT IS NOT write_setpoint().
|
||||
# The first version of this comment said the detector runs "after
|
||||
# write_setpoint()" and grades "the command that has just gone on the wire".
|
||||
# TEST-06's reviewer disproved both halves by mutation:
|
||||
#
|
||||
# N3 move check_meter_frozen() to immediately BEFORE write_setpoint()
|
||||
# -> green, and CORRECTLY green
|
||||
#
|
||||
# because `self.target = resolution.target_w` is assigned two lines above the
|
||||
# write, `write_setpoint()` clamps only a local `value`, and it can skip the
|
||||
# write entirely (`if not (changed or force): return`). So what the detector
|
||||
# grades is `self.target` - the arbiter's resolved intent, PRE device clamp and
|
||||
# possibly never emitted - and the invariant these checks pin is:
|
||||
#
|
||||
# check_meter_frozen() runs AFTER `self.target = resolution.target_w`
|
||||
#
|
||||
# That distinction is not pedantry. A check more robust than its own
|
||||
# explanation is how the next person moves the call ABOVE the assignment
|
||||
# believing they had preserved the stated rule.
|
||||
#
|
||||
# Both properties below are asserted by an OBSERVABLE CONSEQUENCE rather than by
|
||||
# source order, so semantic-preserving refactors stay green (measured by the
|
||||
# reviewer: renaming the method, and calling it from a helper that itself runs
|
||||
# last, both survive - the suite drives `await c.cycle()` and never names the
|
||||
# method):
|
||||
#
|
||||
# (a) BOTH TERMS MUST DESCRIBE THE SAME MOMENT. The command it grades has to
|
||||
# be the one that just went on the wire, judged against how long the meter
|
||||
# that chose it has stood still. Evaluated at the top it grades the
|
||||
# be the one THIS cycle resolved, judged against how long the meter that
|
||||
# chose it has stood still. Evaluated above the assignment it grades the
|
||||
# PREVIOUS cycle's command against THIS cycle's meter - a pairing that was
|
||||
# never true of any moment - and the alarm arrives a whole cycle late.
|
||||
# (b) THE DEAD-POLL-TASK COMPOSITION. When the poll task dies the inputs go
|
||||
# stale, the arbiter commands 0 W, and the detector - evaluated after that
|
||||
# write - correctly sees an unrailed command and CLEARS. At the top of the
|
||||
# cycle it would still be looking at the railed command from before the
|
||||
# failsafe, and would keep the alarm standing over a controller that is
|
||||
# already commanding 0 W.
|
||||
# stale, the arbiter resolves 0 W, and the detector - evaluated after that
|
||||
# resolution lands in self.target - correctly sees an unrailed command and
|
||||
# CLEARS. Evaluated above the assignment it would still be looking at the
|
||||
# railed command from before the failsafe, and would keep the alarm
|
||||
# standing over a controller that is already commanding 0 W.
|
||||
print("WHERE the detector is evaluated in cycle() - the ordering AC1 protects")
|
||||
|
||||
_railed = [i for i, (t, _, _) in enumerate(trace_f)
|
||||
@@ -674,9 +784,9 @@ check("the command takes several cycles to reach the rail, so 'same cycle' and "
|
||||
_first_railed is not None and _first_railed >= 1
|
||||
and len(trace_f) > _first_railed + 1)
|
||||
check("the alarm lands on the SAME cycle the command reaches the rail - "
|
||||
"evaluated after write_setpoint() it grades the command now on the wire; "
|
||||
"moved to the top of cycle() it would grade the previous one and arrive a "
|
||||
"cycle late",
|
||||
"evaluated after `self.target = resolution.target_w` it grades the "
|
||||
"command THIS cycle resolved; evaluated above that assignment it grades "
|
||||
"the previous one and arrives a cycle late",
|
||||
_first_alarm is not None and _first_alarm == _first_railed)
|
||||
|
||||
|
||||
@@ -684,7 +794,7 @@ def dead_poll(after_cycles=20, dead_cycles=4, revived_cycles=20):
|
||||
"""Alarm on a railed command, then kill the poll task and keep cycling.
|
||||
|
||||
The real thing: the transport task dies, every input reads None, and after
|
||||
`stale_input_s` the arbiter's P_SAFETY stop commands 0 W. `inputs_bad_since`
|
||||
`stale_input_s` the arbiter's P_SAFETY stop resolves 0 W. `inputs_bad_since`
|
||||
is backdated rather than slept through - the clock is wall clock, and the
|
||||
mechanism rather than the wait is what is under test.
|
||||
"""
|
||||
@@ -716,11 +826,14 @@ def dead_poll(after_cycles=20, dead_cycles=4, revived_cycles=20):
|
||||
await c.read_inputs()
|
||||
await c.cycle()
|
||||
revived.append((c.target, c.freeze_wd.active, len(notes_of(hass))))
|
||||
return before, trace, revived, notes_of(hass), c
|
||||
# ⚠️ by TEXT: "the second episode REPLACED the first" is a claim about
|
||||
# the id, so the list it is checked against must not be selected by the
|
||||
# id - that would make it true by construction.
|
||||
return before, trace, revived, notes_by_text_of(hass), c
|
||||
return asyncio.run(go())
|
||||
|
||||
|
||||
_before, _dead, _revived, _all_notes, _ctl_d = dead_poll()
|
||||
_before, _dead, _revived, _freeze_notes, _ctl_d = dead_poll()
|
||||
_zeroed = [i for i, (t, _, _) in enumerate(_dead) if t == 0.0]
|
||||
print(f" poll task killed: alarming at {_before[0]:+.0f} W "
|
||||
f"(active={_before[1]}, {_before[2]} notification(s)) -> "
|
||||
@@ -732,15 +845,21 @@ check("the episode really was standing before the poll task died - otherwise "
|
||||
check("a dead poll task drives the command to 0 W",
|
||||
bool(_zeroed) and _zeroed[0] == 0)
|
||||
check("...and on THAT SAME cycle the alarm is already clear, because the "
|
||||
"detector ran after the 0 W write and not before it",
|
||||
"detector ran after 0 W was resolved into self.target, not before it",
|
||||
bool(_zeroed) and not _dead[_zeroed[0]][1])
|
||||
check("...and it stays clear for as long as the failsafe holds it there",
|
||||
_dead and not any(a for _, a, _ in _dead))
|
||||
# ⚠️ "and it raises nothing new while parked" is NOT asserted here, and that is
|
||||
# deliberate: with the alarm cleared there is no mutation of this code that
|
||||
# could make a notification appear during those cycles, so the assertion could
|
||||
# not fail and would have been decoration. The real claim - that the outage did
|
||||
# not add notifications - is carried by the exact count below, which CAN fail.
|
||||
# ⚠️ "and it raises nothing new while parked" is NOT asserted here, and the
|
||||
# reason matters more than the deletion. It was dropped as REDUNDANT, not as
|
||||
# undecidable: TEST-06's reviewer falsified it in one line - move the
|
||||
# `hass.call` above `if not fired: return` so the detector notifies every cycle,
|
||||
# and notifications appear in exactly these parked cycles (that mutation takes
|
||||
# this file RED 9). My first note here claimed no mutation could break it, which
|
||||
# was the very inference this file exists to distrust, made about this file.
|
||||
# The deletion stands because the exact count below catches that same mutation
|
||||
# nine times over - no coverage was lost - and the honest form of the claim is
|
||||
# "I tried to break it and here is what happened", never "I could not, so it
|
||||
# cannot".
|
||||
|
||||
# ⚠️ AND THE OTHER HALF, or "no second notification" is a claim no mutation can
|
||||
# break: when the transport comes back against a meter that is STILL frozen the
|
||||
@@ -750,17 +869,28 @@ _re_railed = [i for i, (t, _, _) in enumerate(_revived)
|
||||
if abs(t) >= rail_w(LIVE_MAX_W)]
|
||||
print(f" transport revived: command re-rails on cycle "
|
||||
f"{_re_railed[0] if _re_railed else None}, "
|
||||
f"{_ctl_d.freeze_wd.episodes} episode(s), {len(_all_notes)} notification(s), "
|
||||
f"ids {sorted({n.get('notification_id') for n in _all_notes})}")
|
||||
check("a revived transport against a still-frozen meter re-rails the command",
|
||||
f"{_ctl_d.freeze_wd.episodes} episode(s), {len(_freeze_notes)} "
|
||||
f"notification(s) from this guard, "
|
||||
f"ids {sorted({n.get('notification_id') for n in _freeze_notes})}")
|
||||
# A PRECONDITION, not a discriminating check - the same role as "the command
|
||||
# takes several cycles to reach the rail" above, and labelled the same way:
|
||||
# without it the two checks below are satisfied by a controller that never came
|
||||
# back. The crudest falsifier (never file the loop claim) takes most of this
|
||||
# file red, which is the signature of a precondition rather than a check; the
|
||||
# narrowest one measured is a stale-input stop that never releases.
|
||||
check("PRECONDITION: a revived transport against a still-frozen meter re-rails "
|
||||
"the command",
|
||||
bool(_re_railed))
|
||||
check("...which is a SECOND episode, and it alarms again on the cycle it rails",
|
||||
_ctl_d.freeze_wd.episodes == 2
|
||||
and bool(_re_railed) and _revived[_re_railed[0]][2] == _before[2] + 1)
|
||||
# ⚠️ STABILITY, not the value - the value is pinned once, above. Renaming the
|
||||
# id must fail that check and NOT this one; the two say different things and
|
||||
# each has to be able to go red on its own.
|
||||
check("...on the same notification_id, so the second REPLACES the first "
|
||||
"instead of stacking a second one beside it",
|
||||
len(_all_notes) == 2
|
||||
and len({n.get("notification_id") for n in _all_notes}) == 1)
|
||||
len(_freeze_notes) == 2
|
||||
and len({n.get("notification_id") for n in _freeze_notes}) == 1)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -776,7 +906,16 @@ print("what the status page SAYS about coverage")
|
||||
|
||||
|
||||
def freeze_rows(unchanged_s, grid, **over):
|
||||
"""The freeze-detector rows of the real checks(), on a p1 install."""
|
||||
"""The freeze-detector rows of the real checks(), on a p1 install.
|
||||
|
||||
⚠️ WHAT THIS DOES NOT COVER. The rows live behind `if self.p1_enabled:`, and
|
||||
the flag is set here by hand rather than by building a real P1 transport -
|
||||
inherited from part 4's fixture. So everything below proves "WHEN the row is
|
||||
produced it says the right thing", and nothing below proves "the row is
|
||||
produced on homewizard_local and not otherwise". That gate is uncovered;
|
||||
AC2 is about the string, and widening the fixture belongs to whoever covers
|
||||
the gate.
|
||||
"""
|
||||
_h, ctl, _t = wired(unchanged_s, grid, cycles=3, **over)
|
||||
# p1_enabled is what a homewizard_local install has; the row's own content
|
||||
# is what is under test, not the transport that feeds it.
|
||||
@@ -829,6 +968,7 @@ check("a Controller built with the key absent lands on the SHIPPED hold, so "
|
||||
_ctl_c.freeze_wd.cfg.hold_s == SHIPPED_HOLD_S)
|
||||
|
||||
print()
|
||||
print(f"part 3 (the live HTTP transport) ran against: {PART3_TRANSPORT}")
|
||||
if fails:
|
||||
print(f"{len(fails)} FAILED: {', '.join(fails)}")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -308,6 +308,36 @@ check("...and a healthy meter that looks unchanged for it is still inside the "
|
||||
_slowest_healthy_w is not None and _db is not None
|
||||
and _slowest_healthy_w < _db)
|
||||
|
||||
# ⚠️ AND THE RANGE'S OWN BOUNDS. "300 sits inside the range" is worth nothing
|
||||
# while the range itself is unpinned: widened to `int(1,86400)` an operator can
|
||||
# configure `meter_freeze_s: 86400` and the guard is dead for a day at a time,
|
||||
# with every check in this repo still green. That mutation survived the first
|
||||
# cut of this PR and is what this block closes. Both numbers are argued for in
|
||||
# config.yaml's own comment and in freeze.py, and each is re-derived here:
|
||||
#
|
||||
# 60 s the floor freeze.py calls NOT A RECOMMENDATION - there a healthy
|
||||
# meter can look unchanged at 60 W, four times the shipped deadband, so
|
||||
# the conjunction's two terms stop being mutually exclusive and only
|
||||
# the margin is left;
|
||||
# 3600 s the ceiling - already four times the billing quarter, so the top of
|
||||
# the range has given up detecting a freeze inside the quarter that
|
||||
# bills it. It is the last bound between a supported configuration and
|
||||
# a disabled guard, and moving it is a decision rather than a tidy-up.
|
||||
print(f" schema range {_lo}-{_hi} s: at the floor a healthy meter that looks "
|
||||
f"unchanged is "
|
||||
+ (f"{3600.0 / _lo:.0f} W" if _lo else "?")
|
||||
+ f" against a {_db} W deadband; the ceiling is "
|
||||
+ (f"{_hi / _QUARTER_S:.1f}x" if _hi else "?")
|
||||
+ " the billing quarter")
|
||||
check(f"the schema range itself is still the argued 60-3600 s (found "
|
||||
f"{_lo}-{_hi})", (_lo, _hi) == (60.0, 3600.0))
|
||||
check("...a floor that gives up the mutual exclusion and keeps only the "
|
||||
"margin, which is why config.yaml calls it not a recommendation",
|
||||
None not in (_lo, _db) and 3600.0 / _lo > _db)
|
||||
check("...and a ceiling already past the billing quarter, so no value the "
|
||||
"schema still accepts can alarm inside the quarter that bills it",
|
||||
_hi is not None and _hi > _QUARTER_S)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("the KNOWN cross-file default divergence, pinned rather than commented")
|
||||
# ⚠️ WHAT THIS GUARDS, AND WHY IT ASSERTS THE BUG RATHER THAN THE FIX.
|
||||
|
||||
Reference in New Issue
Block a user