Resolve automation domain structurally, not by id-line regex#1144
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1144 +/- ##
=======================================
Coverage 99.38% 99.39%
=======================================
Files 209 209
Lines 15399 15399
=======================================
+ Hits 15305 15306 +1
+ Misses 94 93 -1
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR fixes incorrect component-domain resolution for inline on_* handler upserts by switching from a YAML text regex (id: line scan) to a structural walk of the parsed YAML config. This prevents action-body id: references from being mistaken for component instance id: declarations, which previously caused ambiguous trigger keys (e.g. on_turn_on) to resolve to the wrong domain and fail on save.
Changes:
- Added
resolve_component_domain()to structurally map acomponent_idto its owning top-level domain (including synthetic ids for id-less instances and flat singletons). - Updated the automations writer to delegate domain resolution to
resolve_component_domain()and removed the regex-based approach. - Added a regression test ensuring action-body
id:references don’t “steal” the domain during upsert.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tests/test_automations_writer.py | Adds regression coverage for the “decoy id: reference” domain-resolution bug. |
| esphome_device_builder/controllers/automations/writing.py | Replaces regex scanning with structural domain resolution via the parser module helper. |
| esphome_device_builder/controllers/automations/parsing.py | Introduces resolve_component_domain() that walks the parsed YAML to find the declaring domain for a component id. |
|
There is missing coverage on multiple lines |
bdraco
left a comment
There was a problem hiding this comment.
Address review and missing cover
|
@esphbot rr |
|
resolve_component_domain needs only structure (top-level keys → list of dicts → each dict's id), not comment/quote/line fidelity. Replace make_yaml() (ruamel typ="rt") with YAML(typ="safe") or |
Confirmed — the gap is in
Plan: add direct unit tests on the new public helper covering non-dict root → |
|
Re-reviewed. One blocking item: the new branches in On the Copilot suggestion to broaden |
PR Review — Resolve automation domain structurally, not by id-line regexSolid bugfix — structural domain resolution replaces the brittle id-line regex, and the maintainer's loader concern is settled. Merge-ready.
Checklist
|
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
|
@esphbot rr |
Agreed that the resolver only needs structure, but a bare The current ESPHome device YAML almost always carries such tags:
So every config containing a custom tag would silently regress to the wrong-domain bug — and silently, because the fallback path looks identical to a successful resolve. If you want the cheaper parse, the safe loader has to stay tag-tolerant. A catch-all keeps structure intact (ids are plain scalars, so mapping unknown tags to yaml = YAML(typ="safe")
yaml.constructor.add_multi_constructor("", lambda loader, suffix, node: None)Or just keep |
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
Can't we just use ESPHome's yaml parser. It already handles the edge cases. Tried it. Didn't work. It eagerly resolves. So this PR is already the right shape. |
|
@esphbot rr |
Coverage is now complete on the latest push. The 8 tests added in
Codecov's final report on this PR confirms it: "All modified and coverable lines are covered by tests" at 99.39% project coverage. |
Agreed — keeping The cost argument is also weak in practice: in the real edit flow the editor has already parsed this YAML via |
The inline on_* handler writer attributed a component_id to a domain by scanning the YAML text for the first 'id:' line matching it. That regex matched action *references* (id: nested in another component's handler body), so a shared trigger key (on_turn_on) plus an earlier decoy reference resolved the wrong domain — the upsert then failed with 'instance id=... not found under <wrong-domain>'. Resolve against the parsed config instead, keying instances exactly as the trigger parser does (id / synthetic <domain>_<idx> / flat singleton), so only a declared instance id matches, never a reference.
Simple rebaseBranch StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
b81dde8 to
8d0d4ca
Compare
What
Resolve the inline
on_*handler's component domain by parsing theconfig structurally, instead of scanning the YAML text for an
id:line.Why
The writer's
_component_domain_from_yamlused a regex that matchedany
id:line — including an action reference (id:nested inanother component's handler body). With a trigger key shared across
domains (
on_turn_on→ switch/fan/light/cover) and an earlier decoyreference, the wrong domain won:
Editing the
switch'son_turn_onresolved tolightand the upsertfailed with "instance id='relay' not found under 'light'" — the user
couldn't save the handler. (A deferred sibling of the #1084 work.)
How
New
resolve_component_domain()inparsing.pywalks the parsed configand keys instances exactly as the trigger parser does — declared
id,synthetic
<domain>_<idx>for id-less instances, and flat singletons(
sun:/mqtt:). Only a declared instance id matches; a reference neverdoes. The writer delegates to it and keeps the catalog guess as the
final fallback, dropping the regex (and its id-less / singleton
special-casing) entirely — so the writer attributes an id to the same
domain the parser did.
Testing
test_upsert_component_on_ignores_action_reference_to_target_id.test_automations_writer.py+test_automations_parse.py(115), plusbranches/controller/sync/yaml-helpers suites (326) — all pass.
ruff check+ruff format --checkclean.Types of changes
bugfixFrontend coordination
Checklist
ruff,codespell, yaml/json/python checks).tests/where applicable.components.index.json/definitions/components/*.jsonhave not been hand-edited.