Resolve an id-less script by the id its listing shows - #2821
Conversation
The parser lists an id-less top-level script under script_<index>, but the writer matched only a literal id, so a replace aimed at that row appended a duplicate on the draft path and a delete answered not found. Both writers now resolve the row through the parser's own identity rule, and a replace writes the listed id onto the item, which esphome requires anyway.
|
@esphbot review |
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 #2821 +/- ##
==========================================
- Coverage 99.75% 99.75% -0.01%
==========================================
Files 294 294
Lines 25364 25363 -1
==========================================
- Hits 25301 25300 -1
Misses 63 63
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
|
esphbot
left a comment
There was a problem hiding this comment.
Tip
No blocking issues found — ready to merge.
A declared id is the user's stated name; the positional label is only a fallback for a row without one. The new writer tests apply the diff the editor applies, and the guard's replace arm keeps a case: the collision above, where the write lands on the declared row and the guard fails closed.
|
241cbb7 takes all three:
@esphbot review |
|
Confirmed all three at 241cbb7. Two-pass lookup correct. Writer tests now apply diffs via Two nits left, both non-blocking: |
PR Review — Resolve an id-less script by the id its listing showsAll three findings from the prior review are genuinely resolved in 241cbb7 — merge-ready, with two non-blocking notes. Specific strengths of the follow-up commit:
Note: I could not execute the test suite (the review shell is read-only, no interpreter), so the pass/fail claim is not independently verified here — the reasoning above is from reading the code and tests. ✅ Resolved since last review (2)Previously-flagged issues verified fixed
Checklist
ℹ️ Triage summary1 pre-existing finding(s) on unchanged code suppressed (freeze). Automated review by Kōan (Claude) |
esphbot
left a comment
There was a problem hiding this comment.
Tip
No blocking issues found — ready to merge.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Guarded deletion can validate one script row but delete another when an id-less row precedes a declared script_0.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Fixes automation writes so id-less top-level scripts can be replaced or deleted using their parser-generated script_<index> identity.
Changes:
- Aligns script lookup with parser identity resolution.
- Assigns IDs when replacing id-less scripts.
- Adds regression tests and API/model documentation.
| File | Summary |
|---|---|
tests/test_automations_writer.py |
Tests synthetic-ID replacement and deletion. |
tests/test_automations_delete_save.py |
Tests guarded saves for id-less scripts. |
esphome_device_builder/models/automations.py |
Documents synthetic script IDs. |
esphome_device_builder/controllers/automations/writing.py |
Resolves scripts by declared or synthetic identity. |
esphome_device_builder/controllers/automations/parsing.py |
Shares the parser identity rule. |
docs/API.md |
Documents id-less script replacement behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| idx = _top_level_item_index(yaml_text, domain, item_id) | ||
| if idx is None: | ||
| msg = f"{domain}:[id={item_id!r}] not present" | ||
| raise CommandError(ErrorCode.NOT_FOUND, msg) | ||
| for idx, raw in enumerate(items): | ||
| if isinstance(raw, dict) and str(raw.get(id_key, "")) == item_id: | ||
| return _delete_list_item_lines(yaml_text, domain, idx) | ||
| msg = f"{domain}:[{id_key}={item_id!r}] not present" | ||
| raise CommandError(ErrorCode.NOT_FOUND, msg) | ||
| return _delete_list_item_lines(yaml_text, domain, idx) |
There was a problem hiding this comment.
This looks right from the code: the check takes the first row at the location and the writer prefers the item that declares the id. It came in after the merge, so it is tracked in #2851 with the YAML that would show it. Not reproduced yet.

What does this implement/fix?
The parser lists an id-less top-level
script:item under a syntheticscript_<index>location, but the writer only matched a literalid. A replace aimed at that row appended a second script on the editor's draft path, and a delete answerednot_found; the guarded path from #2788 caught the append only after the fact.Both writers now find the row through the parser's own identity rule (
instance_id), so the replace and the delete land on the row the listing showed. A replace writes the listed id onto the item; esphome requires a script id, so the row had never loaded before.Related issue or feature (if applicable):
Types of changes
bugfixnew-featureenhancementbreaking-changerefactordocsmaintenancecidependenciesFrontend coordination
Checklist
ruff,codespell, yaml/json/python checks).tests/where applicable.components.index.json/definitions/components/*.jsonhave not been hand-edited (regenerate viascript/sync_components.pyif a sync is needed).docs/ARCHITECTURE.mdand/ordocs/API.md.