CFG-04 (#45) merged first and moved the base. Two conflicts, both resolved as the union of the two sides: - app/main.py: an import-block collision. CFG-04 added validate_unit_ranges to the .validate import; SAFETY-30 added AGAINST_METER, AGAINST_OWN_POWER, SIGN_OPTION_DEFAULTS and SignWatchdog to the .watchdog import. Neither side dropped anything, so the resolution keeps both lists whole. - CHANGELOG.md: two adjacent new entries under ## Unreleased. Both kept. Both sides also independently rewrote large parts of test_peakguard.py (60 and 47 insertions against the merge base) and git merged them textually, so the full suite was run on the MERGE RESULT rather than inferred from the two parents: 19 passed, 0 failed, twice. One caveat recorded rather than hidden: test_peakguard.py failed once during the first sequential full-suite run and then passed 10/10 in isolation and in two further full runs on this same tree. It passes on both parents too, so this is the pre-existing flakiness CFG-04 already disclosed, not a merge defect - it is timing- or load-sensitive. It sits on the required CI context and can therefore block an unrelated merge at random, which wants a card of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
@@ -190,10 +190,26 @@ check("...and the refusal names the actuator and every writer aimed at it, so "
|
||||
# inconveniences, and then none of this is enforced. Two inverters with two
|
||||
# entities are two writers on TWO actuators, which is not the non-goal - it is
|
||||
# the whole point of the per-inverter roster.
|
||||
#
|
||||
# ⚠️ THE ENTRIES BELOW GAINED `name` AND `max_w` WHEN CFG-04 LANDED, AND THE
|
||||
# HAZARD FIXTURE ABOVE DELIBERATELY DID NOT. SAFETY-28 wrote both against a
|
||||
# shape that did not exist yet and guessed `id`; CFG-04 chose `name`, and made
|
||||
# `name`/`setpoint_entity`/`max_w` required of every entry (rule 9). The hazard
|
||||
# fixture is left in the OLD guessed shape on purpose - the claim it makes is
|
||||
# that `actuator_writers()` finds writers STRUCTURALLY, under a container and a
|
||||
# key spelling this file was never told, and rewriting it into today's shape
|
||||
# would quietly turn it into a check that the census knows about `inverters`.
|
||||
# This one is the opposite claim - that a real, valid, two-inverter config is
|
||||
# ACCEPTED - so it has to be a config the shipped validator actually accepts.
|
||||
_PER_INVERTER_OK = {k: v for k, v in base().items() if k != "setpoint_entity"}
|
||||
_PER_INVERTER_OK["inverters"] = [
|
||||
{"id": "inverter-a", "setpoint_entity": "number.goodwe_setpoint"},
|
||||
{"id": "inverter-b", "setpoint_entity": "number.goodwe_setpoint_b"}]
|
||||
{"name": "inverter-a", "setpoint_entity": "number.goodwe_setpoint",
|
||||
"max_w": 2000},
|
||||
# ⚠️ INVERTER B DOES NOT EXIST. 2000 W here is the SHIPPED default repeated,
|
||||
# not a rating measured off a second device - SPIKE-04 measures that when
|
||||
# there is one to measure. Nothing in this file depends on the number.
|
||||
{"name": "inverter-b", "setpoint_entity": "number.goodwe_setpoint_b",
|
||||
"max_w": 2000}]
|
||||
check("two inverters with their own entities are ACCEPTED - one writer each, "
|
||||
"no shared actuator, and that is CFG-04's whole purpose",
|
||||
validate_startup_config(_PER_INVERTER_OK, -5000, 5000) == [])
|
||||
@@ -219,11 +235,28 @@ check("...and entity ids are folded to lower case, so a second writer cannot "
|
||||
why(validate_startup_config(base(inverters=[{"setpoint_entity":
|
||||
"NUMBER.GOODWE_SETPOINT"}]),
|
||||
-5000, 5000), "one actuator"))
|
||||
# ⚠️ THIS CHECK CHANGED SHAPE IN CFG-04, AND THE REASON IS A DECISION RATHER
|
||||
# THAN AN ADJUSTMENT. It used to assert that a blank per-inverter entity left
|
||||
# the whole config ACCEPTED - "blank is how an operator leaves a slot
|
||||
# unconfigured". CFG-04 decided that is not true of a ROSTER ENTRY: an entry
|
||||
# with no `setpoint_entity` is a unit that `allocator.plan()` still gives a
|
||||
# share of the site target to and that nothing can command, on a status page
|
||||
# reporting a site at full strength. Rule 9 refuses it by name. The claim about
|
||||
# the CENSUS is untouched and is now asserted directly on the census, where it
|
||||
# always belonged - a blank contributes no writer, so it cannot manufacture a
|
||||
# two-writer refusal out of two unconfigured slots.
|
||||
check("a BLANK per-inverter entity is not a writer - the same rule the scalar "
|
||||
"key gets, since write_setpoint() returns immediately on a blank entity "
|
||||
"and blank is how an operator leaves a slot unconfigured",
|
||||
validate_startup_config(base(inverters=[{"setpoint_entity": " "}]),
|
||||
-5000, 5000) == [])
|
||||
"key gets, since write_setpoint() returns immediately on a blank entity",
|
||||
actuator_writers(base(inverters=[{"setpoint_entity": " "},
|
||||
{"setpoint_entity": ""}]))
|
||||
== {"number.goodwe_setpoint": ["setpoint_entity"]})
|
||||
check("...but the ENTRY is refused all the same, by rule 9 and not by the "
|
||||
"census: a roster slot nothing can write still takes a share of the site "
|
||||
"target every cycle and delivers none of it",
|
||||
why(validate_startup_config(base(inverters=[{"name": "inverter-b",
|
||||
"max_w": 2000,
|
||||
"setpoint_entity": " "}]),
|
||||
-5000, 5000), "has no setpoint_entity"))
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("3. a meter that can never read")
|
||||
|
||||
Reference in New Issue
Block a user