"""t14: the adb_command ACTION is selectable — the contract HA reads. The user's report was that the Shield could not be selected for the ``nvidia_shield.adb_command`` action, so he ran his diagnostics through the deprecated ``androidtv`` integration instead. The cause was measured against the installed Home Assistant (2026.10.1), not guessed: * ``ServiceRegistry.async_register`` takes no ``target`` — a target CANNOT be declared at registration; * the frontend's target picker comes from the service DESCRIPTION, which HA builds from ``services.yaml`` or from ``homeassistant.helpers.service.async_set_service_schema``, and the runtime description WINS; * therefore THIS integration must publish a description carrying ``target: {entity: [{domain: media_player}]}`` — that is the picker. This file pins the published contract itself, without any HA installed: the description, and the copy in BOTH translation files. The registration and the resolution are exercised in ``test_command_routing.py::TestAdbCommandService`` (stubs) and in ``tests/test_adb_command_real_ha.py`` (real HA). """ from __future__ import annotations import json from pathlib import Path import pytest _ROOT = Path(__file__).resolve().parent.parent _SHIELD = _ROOT / "custom_components" / "nvidia_shield" @pytest.fixture(name="const") def const_fixture(): """The integration's const module, loaded by path like every other test.""" import sys import conftest if "nsh.const" not in sys.modules: conftest._load_module("nsh.const", conftest._SHIELD_DIR / "const.py") return sys.modules["nsh.const"] def _strings(name: str) -> dict: return json.loads((_SHIELD / name).read_text(encoding="utf-8")) class TestThePublishedTarget: """What HA is told the action accepts.""" def test_the_target_is_the_shields_media_player_entity(self, const): assert const.ADB_COMMAND_SERVICE_TARGET == { "entity": [{"domain": const.ADB_COMMAND_TARGET_DOMAIN}] } assert const.ADB_COMMAND_TARGET_DOMAIN == "media_player" def test_the_description_carries_the_target_and_the_two_fields(self, const): description = const.ADB_COMMAND_SERVICE_DESCRIPTION # the picker: without this key the UI offers no Shield at all assert description["target"] is const.ADB_COMMAND_SERVICE_TARGET assert set(description["fields"]) == {"command", "entity_id"} assert description["fields"]["command"]["required"] is True # a plain (non-required) field: the TARGET is what selects the Shield, and # an entity_id given anyway still has to point at a participant assert "required" not in description["fields"]["entity_id"] def test_the_response_key_is_the_one_the_old_integration_returned(self, const): # the user's captures read `adb_response`; keeping the name means a template # written against `androidtv.adb_command` keeps working assert const.ADB_COMMAND_RESPONSE_KEY == "adb_response" class TestTheCopyMatchesBothTranslationFiles: """The description's prose is duplicated in the translations — so it cannot drift. ``strings.json``/``translations/en.json`` are where HA's translations live and the description is what the frontend receives; the two copies must stay identical, and every raised translation_key needs copy in BOTH files or the user sees a bare key (t29's lesson, applied to t14's two new keys). """ @pytest.mark.parametrize("name", ["strings.json", "translations/en.json"]) def test_the_service_copy_matches_the_description(self, const, name): service = _strings(name)["services"]["adb_command"] description = const.ADB_COMMAND_SERVICE_DESCRIPTION assert service["name"] == description["name"] assert service["description"] == description["description"] for field, copy in description["fields"].items(): assert service["fields"][field]["name"] == copy["name"] assert service["fields"][field]["description"] == copy["description"] @pytest.mark.parametrize("name", ["strings.json", "translations/en.json"]) def test_the_new_exception_keys_have_copy(self, const, name): exceptions = _strings(name)["exceptions"] for key in ("no_target", "ambiguous_target", "unsupported_target"): assert exceptions[key]["title"], (name, key) assert exceptions[key]["message"], (name, key) # the placeholders the handler fills in are the ones the copy names assert "{entity_id}" in exceptions["ambiguous_target"]["message"] assert "{entity_id}" in exceptions["unsupported_target"]["message"] assert "{service}" in exceptions["no_target"]["message"] def test_both_files_are_identical(self): # they are two copies of the same English copy — a divergence would mean one # of them is dead weight assert (_SHIELD / "strings.json").read_text( encoding="utf-8" ) == (_SHIELD / "translations" / "en.json").read_text(encoding="utf-8") def test_the_entity_field_copy_names_the_picker(self): # the field the YAML caller passes has to say what to pass, now that the # picker is the primary way in for name in ("strings.json", "translations/en.json"): copy = _strings(name)["services"]["adb_command"]["fields"]["entity_id"] assert "media_player" in copy["description"] assert "device_id" in copy["description"]