what_to_wear/tests/test_blueprints.py
Nora 8effa251cf feat(2.5): Blueprints Push & TTS — Epic 2 komplett
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>
2026-07-13 14:55:01 +00:00

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://")