UX-08 2c: short refusals, error texts and option texts
Validator refusals name the setting by its label and key, say what is wrong and what to change, and keep the rejected value and limit. Option names and descriptions are short in en and nl; citations moved to DOCS "For installers" and the test_translations citation check now pins the instruction on the tab and the record in DOCS. No rule, key, schema, default or exit code changed. Allowlist 167 -> 43 (log lines only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e1d0970b23
commit
2c516a032c
@@ -2123,7 +2123,7 @@ check("...and so does a configured provider on its own",
|
||||
-5000, 5000) == [])
|
||||
check("an unknown provider id is refused rather than defaulted to 'off'",
|
||||
why(validate_startup_config(vbase(price_provider="nordpool"), -5000, 5000),
|
||||
"not a shipped plugin"))
|
||||
"which does not exist"))
|
||||
|
||||
# ⚠️ TARIFF-07: A RETIRED ID IS REFUSED BY A DIFFERENT MESSAGE, AND EVERY CHECK
|
||||
# HERE GRADES THE REASON RATHER THAN THE VERDICT. The failure this section
|
||||
@@ -2144,26 +2144,21 @@ check(f"every retired id is refused at startup {sorted(_retired)}",
|
||||
len(_retired) == 3 and all(len(v) == 1 for v in _retired.values()))
|
||||
check("...and NOT by the unknown-id message, which would tell an operator they "
|
||||
"mistyped an id this add-on offered them and accepted for months",
|
||||
all("not a shipped plugin" not in v[0] for v in _retired.values())
|
||||
and "WITHDRAWN" not in _unknown[0])
|
||||
all("which does not exist" not in v[0] for v in _retired.values())
|
||||
and "was withdrawn" not in _unknown[0])
|
||||
check("...and each message NAMES ITS OWN ID, so the operator can tell it is "
|
||||
"about their configuration and not a generic notice",
|
||||
all(repr(pid) in v[0] for pid, v in _retired.items()))
|
||||
check("...and (a) says the plugin never fetched a price, naming the endpoint "
|
||||
"that does not exist",
|
||||
all("NEVER FETCHED A PRICE" in v[0] for v in _retired.values())
|
||||
and "api.engie.be/dynamic/be" in _retired["engie_dynamic"][0]
|
||||
and "api.totalenergies.be/prices/dynamic"
|
||||
in _retired["totalenergies_dynamic"][0]
|
||||
and "api.eneco.be/eddy/prices" in _retired["eneco_eddy"][0])
|
||||
# UX-08 2c: the endpoints that never existed are in DOCS.md under "Energy price
|
||||
# providers"; the refusal keeps what the operator acts on.
|
||||
check("...and (a) says the plugin never fetched a price",
|
||||
all("never fetched a price" in v[0] for v in _retired.values()))
|
||||
check("...and (b) says what the site has ACTUALLY been running instead - the "
|
||||
"half §11.4 made invisible",
|
||||
all("SILENT" in v[0] and "static_profile" in v[0]
|
||||
for v in _retired.values()))
|
||||
check("...and (c) says what to do, naming a live plugin and refusing to "
|
||||
"pretend an alias could exist",
|
||||
all("This site ran on" in v[0] for v in _retired.values()))
|
||||
check("...and (c) says what to do, naming a live plugin",
|
||||
all("generic_dayahead" in v[0] and "manual_csv" in v[0]
|
||||
and "NO alias" in v[0] for v in _retired.values()))
|
||||
for v in _retired.values()))
|
||||
|
||||
# ⚠️ AND THE SENTENCE CHANGES WITH THE SITE. "You have been on the static
|
||||
# profile" is FALSE where no profile was ever set, and that site is in a WORSE
|
||||
@@ -2177,11 +2172,12 @@ _with_profile = validate_startup_config(
|
||||
_without = _retired["eneco_eddy"][0]
|
||||
check("a retired id on a site WITH a static profile is told it has been "
|
||||
"running that profile",
|
||||
"the static day/night tariff in static_profile" in _with_profile)
|
||||
"ran on the “Fixed or day/night tariff” (static_profile)"
|
||||
in _with_profile)
|
||||
check("...and a site WITHOUT one is told it has been running nothing at all, "
|
||||
"which is a different and worse state",
|
||||
"NOTHING AT ALL" in _without
|
||||
and "the static day/night tariff in static_profile" not in _without)
|
||||
"ran on no tariff at all" in _without
|
||||
and "(static_profile)" not in _without)
|
||||
check("...so the two renderings are not the same sentence",
|
||||
_with_profile != _without)
|
||||
|
||||
@@ -2193,7 +2189,7 @@ _p = validate_startup_config(
|
||||
check("a provider AND the Home Assistant price entities is refused - two price "
|
||||
"sources on one site", len(_p) == 1)
|
||||
check("...and the message says which one actually wins",
|
||||
why(_p, "HOME ASSISTANT ENTITIES WIN"))
|
||||
why(_p, "are also set and would win"))
|
||||
# TARIFF-03 added a second source with the same silent-loser shape.
|
||||
_ps = validate_startup_config(
|
||||
vbase(static_profile='{"periods": [{"from":"00:00","to":"24:00",'
|
||||
@@ -2202,7 +2198,7 @@ _ps = validate_startup_config(
|
||||
-5000, 5000)
|
||||
check("a static_profile AND the Home Assistant price entities is refused too - "
|
||||
"main.py reads one of them for `cheap` and the loser is silent",
|
||||
len(_ps) == 1 and why(_ps, "HOME ASSISTANT"))
|
||||
len(_ps) == 1 and why(_ps, "Home Assistant price entities would count"))
|
||||
check("the Home Assistant entities alone still start, so nothing is taken away "
|
||||
"from a live install",
|
||||
validate_startup_config(vbase(price_now_entity="sensor.now",
|
||||
@@ -2945,10 +2941,10 @@ check("...and BOTH translations tell an operator which switch does which - "
|
||||
"the provider option says it is what starts the fetch, and the trading "
|
||||
"option says turning it off does not turn the feed off. One language "
|
||||
"updated and not the other is how this surface rots",
|
||||
"STARTS THE PRICE FETCH" in _T10_TR["en"]
|
||||
and "DOES NOT TURN THE PRICE FEED OFF" in _T10_TR["en"]
|
||||
and "HET OPHALEN VAN PRIJZEN START" in _T10_TR["nl"]
|
||||
and "ZET DE PRIJSFEED NIET UIT" in _T10_TR["nl"])
|
||||
"starts the price fetch" in _T10_TR["en"]
|
||||
and "does not turn the price feed off" in _T10_TR["en"]
|
||||
and "start het ophalen van prijzen" in _T10_TR["nl"]
|
||||
and "zet de prijsfeed niet uit" in _T10_TR["nl"])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -3431,8 +3427,8 @@ check(f"network charges folded into `offset` move the card's own price to "
|
||||
isinstance(_double_price, float) and abs(_double_price - 15.55) > 10)
|
||||
check(f"...and it is refused, by the CARD rule, naming the discrepancy and "
|
||||
f"the network-charge cause ({_double[:60]!r})",
|
||||
"does not reproduce its own card" in _double and "OUT BY" in _double
|
||||
and "folded into `offset`" in _double)
|
||||
"does not match its own card" in _double and "the card says" in _double
|
||||
and "network charges are not in the offset" in _double)
|
||||
|
||||
_CORRUPTIONS = {
|
||||
"VAT left out (vat_pct 0)": spoilt(vat_pct=0),
|
||||
@@ -3451,7 +3447,7 @@ for _name, _blob in sorted(_CORRUPTIONS.items()):
|
||||
_why = refusal(_blob)
|
||||
check(f"{_name}: refused, and by the CARD rule rather than by another one "
|
||||
f"({_why[:48]!r})",
|
||||
"does not reproduce its own card" in _why and "OUT BY" in _why)
|
||||
"does not match its own card" in _why and "the card says" in _why)
|
||||
|
||||
# ⚠️ THE TOLERANCE IS A REAL EDGE AND IT IS GRADED FROM BOTH SIDES, or
|
||||
# "within 0.01" and "within anything" would be the same check.
|
||||
@@ -3463,7 +3459,7 @@ _outside = refusal(spoilt(card_expect_incl_vat=round(
|
||||
+ CARD_TOLERANCE * 2.0, 6)))
|
||||
check(f"half a tolerance out passes and twice a tolerance out does not, so "
|
||||
f"{CARD_TOLERANCE} is the number being enforced rather than a shape",
|
||||
_inside == "" and "does not reproduce its own card" in _outside)
|
||||
_inside == "" and "does not match its own card" in _outside)
|
||||
|
||||
# A card that is absent is refused, and NOT by the generic unknown-field rule.
|
||||
check(f"a formula with NO card block is refused, and the message says why the "
|
||||
@@ -3569,32 +3565,31 @@ check("...and the hourly series' coverage is still DERIVED from the "
|
||||
print()
|
||||
print("15e. every refusal in the table, and the two that are WARNINGS")
|
||||
_TABLE = {
|
||||
"units absent": (spoilt(units=""), "units", "is REQUIRED"),
|
||||
"units absent": (spoilt(units=""), "units", "Choose one of"),
|
||||
"units not one of the enumerated ones":
|
||||
(spoilt(units="cents"), "units", "must be one of"),
|
||||
(spoilt(units="cents"), "units", "Choose one of"),
|
||||
"factor == 0 (every spread becomes 0, arbitrage never fires)":
|
||||
(spoilt(factor=0), "factor", "static_profile"),
|
||||
"factor < 0 (the dearest hour becomes the cheapest)":
|
||||
(spoilt(factor=-0.102), "factor", "inverts the market"),
|
||||
(spoilt(factor=-0.102), "factor", "turns the market upside down"),
|
||||
"vat_pct above 30 (the 6 / 1.06 / 106 confusion)":
|
||||
(spoilt(vat_pct=106), "vat_pct", "between 0 and 30"),
|
||||
"vat_pct below 0": (spoilt(vat_pct=-6), "vat_pct", "between 0 and 30"),
|
||||
"a non-numeric coefficient": (spoilt(factor="0.102"), "factor",
|
||||
"not a number"),
|
||||
"a coefficient that is an EXPRESSION rather than a number":
|
||||
(spoilt(factor="0.1*x"), "factor", "evaluates nothing"),
|
||||
(spoilt(factor="0.1*x"), "factor", "Write a plain number"),
|
||||
"a non-finite coefficient": (spoilt(offset=float("inf")), "offset",
|
||||
"not finite"),
|
||||
"an unknown index": (spoilt(index="belpex_h"), "index", "cannot get it"),
|
||||
"an unknown index": (spoilt(index="belpex_h"), "index", "can only fetch"),
|
||||
"an unknown resolution": (spoilt(resolution="daily"), "resolution",
|
||||
"must be one of"),
|
||||
"Choose one of"),
|
||||
"an unknown top-level field (which is how 'expr' is answered)":
|
||||
(spoilt(expr="0.102*spot+1"), "expr", "NO field that takes an "
|
||||
"expression"),
|
||||
(spoilt(expr="0.102*spot+1"), "expr", "It takes only"),
|
||||
"an unknown field inside the card": ({**ENECO_CARD,
|
||||
"card": dict(ENECO_CARD["card"],
|
||||
vat="6")},
|
||||
"vat", "no field(s)"),
|
||||
"vat", "unknown field(s)"),
|
||||
}
|
||||
for _name, (_blob, _names, _needle) in sorted(_TABLE.items()):
|
||||
_why = refusal(_blob)
|
||||
@@ -3649,7 +3644,7 @@ check("...and it is warned about, naming the month and saying why it is not "
|
||||
"refused",
|
||||
"2019-01" in (stale_card(parse_price_formula(json.dumps(_old_card)))
|
||||
or "")
|
||||
and "Not refused" in (stale_card(
|
||||
and "still starts" in (stale_card(
|
||||
parse_price_formula(json.dumps(_old_card))) or ""))
|
||||
check("...while a card from this month draws no warning, so the rule is the "
|
||||
"AGE and not the presence of the field",
|
||||
@@ -3850,13 +3845,13 @@ check("custom_formula with NO formula is refused, naming the option and "
|
||||
"showing the shape",
|
||||
why(validate_startup_config(vbase(price_provider="custom_formula"),
|
||||
-5000, 5000),
|
||||
"price_formula is empty"))
|
||||
"(price_formula) is empty"))
|
||||
check("a formula that does not reproduce its card is refused AT STARTUP, "
|
||||
"with the discrepancy named",
|
||||
why(validate_startup_config(
|
||||
vbase(price_provider="custom_formula",
|
||||
price_formula=json.dumps(spoilt(offset=13.0))),
|
||||
-5000, 5000), "does not reproduce its own card"))
|
||||
-5000, 5000), "does not match its own card"))
|
||||
check("...and a formula that is not JSON at all is refused with the shape it "
|
||||
"wanted, rather than with a traceback",
|
||||
why(validate_startup_config(
|
||||
@@ -5362,18 +5357,17 @@ check("...and none of those rows is vacuous: every one really did refuse on "
|
||||
_T16_DIFFER = [
|
||||
("factor 0 -> where a FLAT price of THIS leg lives",
|
||||
spoilt(factor=0.0), "static_profile", "export_price_eur_kwh"),
|
||||
("a negative factor -> which market it inverts",
|
||||
spoilt(factor=-0.102), "No consumption tariff",
|
||||
"No Belgian injection tariff"),
|
||||
("a card miss -> what a wrong `offset` costs on THIS leg",
|
||||
spoilt(factor=0.120), "bills you for it twice",
|
||||
"nothing is added to this leg"),
|
||||
# UX-08 2c: a negative factor gets one sentence on both legs now ("put the
|
||||
# minus sign on the offset"), so it left this table.
|
||||
("a card miss -> what else to check on THIS leg",
|
||||
spoilt(factor=0.120), "network charges are not in the offset",
|
||||
"vat_pct is 0 for injection"),
|
||||
("invalid JSON -> WHEN this option is read at all, which is not the "
|
||||
"same answer on the two legs: `price_formula` decides nothing unless "
|
||||
"price_provider is 'custom_formula', and the injection leg is read "
|
||||
"whatever the consumption provider is",
|
||||
"{not json", "unless price_provider is 'custom_formula'",
|
||||
"read whatever price_provider is set to"),
|
||||
"{not json", "(price_provider) is 'custom_formula'",
|
||||
"used whatever the price source is"),
|
||||
]
|
||||
for _what, _blob, _cons_mark, _inj_mark in _T16_DIFFER:
|
||||
_a, _b = ((_reason(lambda b=_blob: parse_price_formula(b)),
|
||||
@@ -5490,10 +5484,10 @@ check(f"a factor 100x out is refused BY THE PASS-THROUGH RUNG, naming the "
|
||||
f"share of the wholesale price it would pay ({_T16_PT_WHY[:60]!r})",
|
||||
"passes through" in _T16_PT_WHY
|
||||
and f"{PASSTHROUGH_LO}-{PASSTHROUGH_HI}x" in _T16_PT_WHY
|
||||
and "|offset| above" not in _T16_PT_WHY)
|
||||
and "larger than" not in _T16_PT_WHY)
|
||||
check(f"an offset 100x out is refused BY THE OFFSET RUNG, naming the EUR/kWh "
|
||||
f"it works out at ({_T16_OFF_WHY[:60]!r})",
|
||||
"|offset| above" in _T16_OFF_WHY
|
||||
"larger than" in _T16_OFF_WHY
|
||||
and "passes through" not in _T16_OFF_WHY)
|
||||
check(f"...and the WHOLE card 100x out - which trips both - is refused, with "
|
||||
f"the pass-through rung answering first ({_T16_BOTH_WHY[:44]!r})",
|
||||
@@ -5555,9 +5549,8 @@ check(f"a card block ON THIS LEG is graded by the same rung at the same "
|
||||
f"15.55) is refused, naming the option the operator typed "
|
||||
f"({_T16_SPOILT_CARD[:64]!r})",
|
||||
_T16_WITH_CARD == ""
|
||||
and "does not reproduce its own card" in _T16_SPOILT_CARD
|
||||
and _T16_SPOILT_CARD.startswith(EXPORT_FORMULA_OPTION)
|
||||
and f"the tolerance is {CARD_TOLERANCE}" in _T16_SPOILT_CARD)
|
||||
and "does not match its own card" in _T16_SPOILT_CARD
|
||||
and _T16_SPOILT_CARD.startswith(EXPORT_FORMULA_OPTION))
|
||||
check(f"...and half a tolerance out still passes, so {CARD_TOLERANCE} is "
|
||||
f"enforced on this leg too rather than 'something'",
|
||||
_reason(lambda: parse_export_formula(json.dumps(spoilt(
|
||||
|
||||
Reference in New Issue
Block a user