Files
agentic-mobile-control/tests/test_semantic_enricher.py
q792602257 ca5e300289 fix(semantic-scene): preserve page/intents when all widgets are dangling, drop dead exception-name set
- _with_known_widgets_only used to discard the entire SemanticScene
  (returning None) when filtering dangling-element-id widgets left an
  empty list, losing valid page/intents. Now returns a SemanticScene
  with widgets=[] instead, matching design.md's 'dropped, not a hard
  failure' intent.
- Removed _SDK_UNAVAILABLE_EXCEPTION_NAMES, dead code left over from an
  abandoned name-based exception-matching approach; the blanket except
  Exception already maps every SDK failure correctly.

openspec: semantic-scene capability, archived change semantic-scene-runtime
2026-07-07 08:31:03 +08:00

145 lines
3.7 KiB
Python

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 == []