Story 2.5 (deklaratives Artefakt, Struktur-Validierung, Suite gruen 130/130): - blueprints/automation/what_to_wear/notify_push.yaml: device(mobile_app)+time+ boolean, Bedingung changed/gaps_count/alert, notify.mobile_app_ via slugify. - announce_tts.yaml: tts+media_player+trigger-Selector, tts.speak. - Beide: what_to_wear.recommend via response_variable, rec.llm_text || full_text, min_version 2025.3.0, gehaertete Templates (.get()+Defaults), continue_on_error. - luna-pro-Story-Review: 5/6 Findings uebernommen, 1 verworfen (flat-response-Evidenz). Epic 2 (15-Minuten-Wow) komplett: Kleiderschrank->Karte->Service->Push/TTS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
78 lines
2.7 KiB
Python
78 lines
2.7 KiB
Python
"""Story 2.5 — blueprint validity (declarative artifact, verifiable criterion).
|
|
|
|
Blueprints are not runtime-tested with a red test (DEV-METHOD: a declarative
|
|
artifact gets a pre-noted, verifiable acceptance criterion instead). Here we
|
|
assert they parse and carry the required structure, min_version, selectors, and
|
|
the response_variable-based call into what_to_wear.recommend.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
|
|
import yaml
|
|
|
|
BP_DIR = pathlib.Path(__file__).parent.parent / "blueprints" / "automation" / "what_to_wear"
|
|
|
|
|
|
class _BlueprintLoader(yaml.SafeLoader):
|
|
pass
|
|
|
|
|
|
def _passthrough(loader, node):
|
|
if isinstance(node, yaml.ScalarNode):
|
|
return {"__tag__": node.tag, "value": loader.construct_scalar(node)}
|
|
if isinstance(node, yaml.SequenceNode):
|
|
return loader.construct_sequence(node)
|
|
return loader.construct_mapping(node)
|
|
|
|
|
|
# Handle HA's custom !input tag so PyYAML can parse the blueprints.
|
|
_BlueprintLoader.add_constructor("!input", _passthrough)
|
|
|
|
|
|
def _load(name: str) -> dict:
|
|
with open(BP_DIR / name, encoding="utf-8") as fh:
|
|
return yaml.load(fh, Loader=_BlueprintLoader)
|
|
|
|
|
|
def _dump(obj) -> str:
|
|
return yaml.dump(obj, default_flow_style=False)
|
|
|
|
|
|
def test_push_blueprint_structure() -> None:
|
|
bp = _load("notify_push.yaml")
|
|
meta = bp["blueprint"]
|
|
assert meta["domain"] == "automation"
|
|
assert meta["min_version"] == "2025.3.0"
|
|
inputs = meta["input"]
|
|
assert inputs["notify_device"]["selector"]["device"]["integration"] == "mobile_app"
|
|
assert "time" in inputs["push_time"]["selector"]
|
|
assert "boolean" in inputs["only_if_relevant"]["selector"]
|
|
# calls the service with a response_variable and uses the response, not the event
|
|
text = _dump(bp)
|
|
assert "what_to_wear.recommend" in text
|
|
assert "response_variable" in text
|
|
assert "llm_text" in text and "full_text" in text
|
|
# the "only when relevant" logic uses changed/gaps_count/alert
|
|
assert "changed" in text and "gaps_count" in text and "alert" in text
|
|
|
|
|
|
def test_tts_blueprint_structure() -> None:
|
|
bp = _load("announce_tts.yaml")
|
|
meta = bp["blueprint"]
|
|
assert meta["domain"] == "automation"
|
|
assert meta["min_version"] == "2025.3.0"
|
|
inputs = meta["input"]
|
|
assert inputs["media_player"]["selector"]["entity"]["domain"] == "media_player"
|
|
assert "trigger" in inputs["announce_trigger"]["selector"]
|
|
text = _dump(bp)
|
|
assert "what_to_wear.recommend" in text
|
|
assert "response_variable" in text
|
|
assert "tts.speak" in text
|
|
assert "llm_text" in text and "full_text" in text
|
|
|
|
|
|
def test_both_have_source_url_for_import() -> None:
|
|
for name in ("notify_push.yaml", "announce_tts.yaml"):
|
|
bp = _load(name)
|
|
assert bp["blueprint"]["source_url"].startswith("https://")
|