Commit 6cdb9ad
authored
* refactor: validate only new entries on convenience-mode writes (#1086)
Convenience-mode writes (_add_device, _add_source, _remove_device) reach
_set_prefs through _mutate_atomic, which builds partial_config equal to
the FULL existing list ± one entry. _set_prefs ran _shape_check on that
union — re-validating pre-existing (HA-validated) siblings on every
add/remove. If _shape_check is ever tightened past HA's voluptuous
schema, an unrelated add would fail because a SIBLING entry would
suddenly fail the local check.
Scope _shape_check on the convenience-mode write path to the appended
tail only:
- _shape_check accepts validate_only: dict[str, set[int]] | None. None
preserves the original full-validation contract. An empty dict skips
the per-entry pass entirely. Structural "must be a dict" / "must be a
list" checks still fire for listed keys.
- _set_prefs forwards validate_only as a kwarg-only param.
- _mutate_atomic computes the appended-tail indices (range(existing_count,
new_count)) and passes them on both the real-run write and the dry-run
backstop. For add_* this is the new entry only; for remove_* this is
set() so nothing is re-validated.
- Direct mode='set' callers unchanged (validate_only defaults to None).
Assumes append-only/remove-only mutator semantics; revisit for any
future in-place mutator.
No reachable bug today (the local check is currently a strict subset of
HA's voluptuous schema). Forward-looking robustness — see issue body.
Closes #1086
* docs(energy): clarify {key: set()} vs {} semantics in _shape_check
Gemini G1 (PR #1100, medium): the prior docstring conflated the two
empty-validate_only forms. _mutate_atomic for remove operations passes
{target_key: set()} (preserves the structural "must be a list" check on
target_key, skips its per-entry pass), not {} (which would skip all keys
including the structural check). Update the docstring to reflect both
shapes accurately.
No code-behaviour change; pytest 1463/1 grün, mypy clean.
* docs(energy): start helper docstrings with action verbs
Gemini G2 (PR #1100, medium): suggested starting _shape_check's
docstring with an action verb. Empirical norm in tools_energy.py is
11/15 internal helpers action-verb-first; spot-check tools_addons.py
and tools_integrations.py shows the same pattern across the project.
Three noun-phrase outliers in this file aligned for consistency:
- _shape_check: "Cheap" -> "Perform a cheap"
- _is_no_prefs_error: "True if" -> "Return True if"
- _mutate_atomic: "Read-modify-write loop" -> "Run the read-modify-write loop"
Note: the .gemini/styleguide.md rule itself is scoped to public @tool
docstrings (verb whitelist Get/List/Search/Create/Update/Delete/Remove/
Execute/Call/Manage), so adoption here is empirical-pattern-consistency
rather than literal styleguide-compliance.
No code-behaviour change; pytest 1463/1, mypy + ruff clean.
* fix: remove leftover merge-conflict markers in tools_energy docstring
The post-#1098 rebase left three conflict markers (<<<<<<< HEAD,
=======, >>>>>>> 0825a15) inside the _set_prefs docstring at
lines 677/680/689. They survived ruff, mypy, AST parsing, and the full
unit suite because they sat inside a triple-quoted string literal —
valid Python content, no static-analysis trigger.
Resolved by merging the two halves into a single coherent paragraph
covering both the str/dict config_hash form contract and the
validate_only forwarding semantics.
No behaviour change; pytest 1479/1, mypy + ruff still clean.
* docs(energy): tighten helper docstrings and heuristic warnings
Self-Review Boy-Scout sweep on the lines this PR already touches:
- _shape_check first line: "Perform a cheap local shape check..." had
redundant "cheap" + "shape check" wording; replaced with "Validate
config shape locally..." (action-verb-first, drops the redundancy).
Note: ".gemini/styleguide.md" verb whitelist is scoped to public
@tool docstrings, not internal helpers — so neither "Perform" nor
"Validate" sit on the whitelist; this rewording is empirical-pattern-
consistency only.
- _mutate_atomic docstring lead: was a one-liner "Run the read-modify-
write loop for convenience modes."; expanded to "Run convenience-mode
read-modify-write with dry-run backstop and hash-conflict retry." to
match what the function actually owns (per body L1313-1336).
- Heuristic warning at the appended_indices computation (real-run +
dry-run): made the in-place-mutator failure mode explicit. An
in-place mutator with len(new) == len(existing) would yield an empty
appended_indices and silently skip the per-entry pass entirely.
Dropping just "revisit if added" was too oblique.
No code-behaviour change; pytest 1479/1, mypy + ruff clean.
* refactor(energy): extract _appended_tail_indices helper; harden invariants
Addresses kingpanther13's #1100 review:
Requested:
1. Convert the in-place-mutator assumption into a runtime guard. The
appended-tail formula yields an empty index set when len(new) ==
len(existing) regardless of content, which would silently bypass
per-entry validation if a future _replace_*/_update_* mutator were
added. The new `_appended_tail_indices` helper raises an
INTERNAL_ERROR when same-length non-equal mutation is detected, so
the wrong shape fails loudly at test time rather than silently in
production.
2. Replace the bare AssertionError in the unreachable retry-loop exit
with a structured raise_tool_error(INTERNAL_ERROR), matching the
pattern used elsewhere in this file. The previous AssertionError
fell through `except Exception` and surfaced as a context-less
generic INTERNAL_ERROR.
While you're in there:
3. Extract the duplicated 'append-only/shrink-only' rationale and
index computation from `_mutate_atomic` (dry_run + real-run
branches) into the new module-level helper. Both call sites now
share one source of truth; the rationale lives in the helper
docstring and the call-site comments collapse to a one-line
reference.
4. Rename test_add_device_still_rejects_a_genuinely_invalid_new_entry
to test_direct_set_mode_still_validates_full_config_when_validate_only_is_none
so the boundary it pins is in the name (it actually exercises
mode='set' with the default validate_only=None, not add_device).
5. Add a direct `_shape_check` test for validate_only={key: set()}
pinning the docstring's distinction between {} (skip everything,
structural list-check skipped for unlisted keys) and {key: set()}
(key listed, list-shape still checked, per-entry skipped). The
latter is what `_remove_*` mutators emit via _appended_tail_indices.
6. Parametrize test_*_dry_run_succeeds_with_invalid_pre_existing
across add_device / add_source / remove_device. The dry_run path
in _mutate_atomic is shared but parametrizing locks symmetry with
the real-run regression block above, so future divergence between
the two _mutate_atomic branches breaks loudly.
All 101 tests in test_tools_energy.py pass; ruff and mypy clean.
1 parent 0e9b18d commit 6cdb9ad
2 files changed
Lines changed: 406 additions & 13 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
| 99 | + | |
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| |||
143 | 143 | | |
144 | 144 | | |
145 | 145 | | |
146 | | - | |
147 | | - | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
148 | 151 | | |
149 | 152 | | |
150 | 153 | | |
151 | 154 | | |
152 | 155 | | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
153 | 170 | | |
154 | 171 | | |
155 | 172 | | |
| |||
159 | 176 | | |
160 | 177 | | |
161 | 178 | | |
| 179 | + | |
| 180 | + | |
162 | 181 | | |
163 | 182 | | |
164 | 183 | | |
165 | 184 | | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
166 | 188 | | |
| 189 | + | |
| 190 | + | |
167 | 191 | | |
168 | 192 | | |
169 | 193 | | |
| |||
217 | 241 | | |
218 | 242 | | |
219 | 243 | | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
220 | 285 | | |
221 | 286 | | |
222 | 287 | | |
| |||
629 | 694 | | |
630 | 695 | | |
631 | 696 | | |
| 697 | + | |
632 | 698 | | |
633 | 699 | | |
634 | 700 | | |
| |||
650 | 716 | | |
651 | 717 | | |
652 | 718 | | |
653 | | - | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
654 | 727 | | |
655 | 728 | | |
656 | 729 | | |
657 | | - | |
| 730 | + | |
658 | 731 | | |
659 | 732 | | |
660 | 733 | | |
| |||
1176 | 1249 | | |
1177 | 1250 | | |
1178 | 1251 | | |
1179 | | - | |
| 1252 | + | |
1180 | 1253 | | |
1181 | 1254 | | |
1182 | 1255 | | |
| |||
1205 | 1278 | | |
1206 | 1279 | | |
1207 | 1280 | | |
1208 | | - | |
1209 | | - | |
1210 | | - | |
| 1281 | + | |
| 1282 | + | |
| 1283 | + | |
| 1284 | + | |
| 1285 | + | |
| 1286 | + | |
| 1287 | + | |
| 1288 | + | |
1211 | 1289 | | |
1212 | 1290 | | |
1213 | 1291 | | |
| |||
1241 | 1319 | | |
1242 | 1320 | | |
1243 | 1321 | | |
| 1322 | + | |
| 1323 | + | |
| 1324 | + | |
| 1325 | + | |
| 1326 | + | |
1244 | 1327 | | |
1245 | 1328 | | |
1246 | 1329 | | |
1247 | 1330 | | |
1248 | 1331 | | |
| 1332 | + | |
1249 | 1333 | | |
1250 | 1334 | | |
1251 | 1335 | | |
| |||
1289 | 1373 | | |
1290 | 1374 | | |
1291 | 1375 | | |
1292 | | - | |
1293 | | - | |
1294 | | - | |
1295 | | - | |
| 1376 | + | |
| 1377 | + | |
| 1378 | + | |
| 1379 | + | |
| 1380 | + | |
| 1381 | + | |
| 1382 | + | |
| 1383 | + | |
| 1384 | + | |
| 1385 | + | |
| 1386 | + | |
| 1387 | + | |
| 1388 | + | |
| 1389 | + | |
| 1390 | + | |
1296 | 1391 | | |
1297 | 1392 | | |
1298 | 1393 | | |
| |||
0 commit comments