from __future__ import annotations from typing import Any from core.models import Bounds, Scene, SceneElement from semantic.config import SemanticConfig, load_config from semantic.enricher import enrich_scene from semantic.llm_client import EnrichmentUnavailable class FakeClient: def __init__( self, *, response: dict[str, Any] | None = None, error: Exception | None = None, ) -> None: self.response = response self.error = error self.calls: list[tuple[dict[str, Any], float]] = [] def enrich(self, scene_json: dict[str, Any], *, timeout: float) -> dict[str, Any]: self.calls.append((scene_json, timeout)) if self.error: raise self.error assert self.response is not None return self.response def _scene() -> Scene: return Scene( width=10, height=20, elements=[ SceneElement( id="input", type="input", bounds=Bounds(1, 2, 3, 4), text="Message", ), SceneElement( id="send", type="button", bounds=Bounds(5, 2, 3, 4), text="Send", ), ], ) def _semantic_payload() -> dict[str, Any]: return { "page": "Chat", "intents": ["send a message"], "widgets": [ {"element_id": "input", "purpose": "message text input"}, {"element_id": "send", "purpose": "send message"}, ], } def test_enrichment_config_is_disabled_by_default() -> None: assert load_config({}).enabled is False def test_enrich_scene_returns_semantic_scene_when_enabled() -> None: client = FakeClient(response=_semantic_payload()) result = enrich_scene( _scene(), client=client, config=SemanticConfig(enabled=True, timeout=2.5), ) assert result is not None assert result.to_dict() == _semantic_payload() assert len(client.calls) == 1 assert client.calls[0][1] == 2.5 def test_enrich_scene_disabled_short_circuits_without_client_call() -> None: client = FakeClient(response=_semantic_payload()) result = enrich_scene( _scene(), client=client, config=SemanticConfig(enabled=False), ) assert result is None assert client.calls == [] def test_enrich_scene_degrades_to_none_on_client_failure() -> None: client = FakeClient(error=EnrichmentUnavailable("rate limited")) result = enrich_scene( _scene(), client=client, config=SemanticConfig(enabled=True), ) assert result is None def test_enrich_scene_filters_dangling_element_ids() -> None: payload = _semantic_payload() payload["widgets"] = [ {"element_id": "send", "purpose": "send message"}, {"element_id": "missing", "purpose": "unknown control"}, ] client = FakeClient(response=payload) result = enrich_scene( _scene(), client=client, config=SemanticConfig(enabled=True), ) assert result is not None assert result.to_dict()["widgets"] == [ {"element_id": "send", "purpose": "send message"} ] def test_enrich_scene_returns_empty_widgets_when_all_widgets_are_dangling() -> None: client = FakeClient( response={ "page": "Chat", "intents": ["send a message"], "widgets": [{"element_id": "missing", "purpose": "unknown control"}], } ) result = enrich_scene( _scene(), client=client, config=SemanticConfig(enabled=True), ) assert result is not None assert result.page == "Chat" assert result.intents == ["send a message"] assert result.widgets == []