Skip to content

Commit 10596fd

Browse files
authored
fix: raise on parse error for a single explicit ha_config_get_yaml target (#1959)
A malformed single (non-glob) target soft-degraded to a warning with success:true and an empty match list, making a real parse failure indistinguishable from "key absent". The two sibling failure branches already gate on is_glob and raise for a single target; the parse_error branch did not. Extract the failure triage into a helper that raises a structured CONFIG_INVALID error for the single-target case and keeps the per-file warning under a glob, plus a regression test for the non-glob path. Closes #1948
1 parent 1f6b103 commit 10596fd

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

src/ha_mcp/tools/tools_yaml_read.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,28 @@ def _failure_text(payload: dict[str, Any]) -> str:
8282
return str(error or "read failed")
8383

8484

85+
def _parse_error_result(
86+
target: str, yaml_path: str, parse_error: str, *, is_glob: bool
87+
) -> tuple[None, str]:
88+
"""Decide a file the component could not parse: raise for a single target.
89+
90+
A single explicit target has no siblings to salvage, so a parse failure
91+
raises rather than soft-degrading to a warning that reads as "key absent".
92+
Under a glob it stays a per-file warning so one broken file does not sink
93+
the whole search.
94+
"""
95+
if not is_glob:
96+
raise_tool_error(
97+
create_error_response(
98+
ErrorCode.CONFIG_INVALID,
99+
f"{target} could not be parsed as YAML: {parse_error}",
100+
suggestions=[f"Fix the YAML syntax error in {target} and retry."],
101+
context={"file": target, "yaml_path": yaml_path},
102+
)
103+
)
104+
return None, f"{target} was not searched: {parse_error}."
105+
106+
85107
def _evaluate_read(
86108
response: Any,
87109
target: str,
@@ -125,7 +147,7 @@ def _evaluate_read(
125147

126148
parse_error = unwrapped.get("parse_error")
127149
if parse_error:
128-
return None, f"{target} was not searched: {parse_error}."
150+
return _parse_error_result(target, yaml_path, str(parse_error), is_glob=is_glob)
129151

130152
if unwrapped.get("subtree") is None:
131153
return None, None

tests/src/unit/test_yaml_read_tool.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,28 @@ def read(payload):
470470
]
471471

472472

473+
async def test_single_file_parse_error_raises_instead_of_reporting_no_match():
474+
"""A single explicit target that will not parse has no siblings to salvage.
475+
476+
Soft-degrading to a warning would make a real parse failure indistinguishable
477+
from "key absent" at the success/count level, so it raises instead.
478+
"""
479+
fn, _ = await _make_tool(
480+
{
481+
"read_file": {
482+
"success": True,
483+
"path": "configuration.yaml",
484+
"content": "...",
485+
"subtree": None,
486+
"parse_error": "not valid YAML at line 3, column 5",
487+
}
488+
}
489+
)
490+
491+
with pytest.raises(ToolError):
492+
await fn(yaml_path="rest", file="configuration.yaml")
493+
494+
473495
async def test_no_warnings_key_when_nothing_degraded():
474496
"""`warnings` is omitted when empty, per the tool return contract."""
475497
fn, _ = await _make_tool({"read_file": _read_ok("method: GET\n")})

0 commit comments

Comments
 (0)