feat: adb_command is a conforming action — always registered in async_setup (option gates USE), services.yaml with required command/entity, strings services+exceptions, ServiceValidationError on empty/unknown-target/ADB-down; participant registry stays (t29 G8)
This commit is contained in:
1 parent
e51545262e
commit
3e0208789a
3 files changed
+344
-54
No files matched your search
@@ -640,7 +640,8 @@ class ServiceHarness:
|
||||
|
||||
|
||||
class TestAdbCommandService:
|
||||
"""P4-2a: option-gated raw shell service on the entry."""
|
||||
"""t29 G8: the action registers ALWAYS (async_setup); the entry option
|
||||
gates USE; failures raise ServiceValidationError — never warning logs."""
|
||||
|
||||
def _participant(self, modules, enabled, connected=True):
|
||||
player, _s, _i = make_player(modules)
|
||||
@@ -659,19 +660,18 @@ class TestAdbCommandService:
|
||||
|
||||
before = dict(_ADB_COMMAND_TARGETS)
|
||||
player, _services = self._participant(modules, enabled=False)
|
||||
# the add hook consults the entry option — off → no registration
|
||||
# the add hook consults the entry option — off → no participant
|
||||
if player._entry.options.get("adb_command_enabled", False):
|
||||
_ADB_COMMAND_TARGETS.setdefault(player.entity_id, player._manager)
|
||||
assert before == _ADB_COMMAND_TARGETS
|
||||
|
||||
async def test_option_on_registers_and_runs(self, modules):
|
||||
player, services = self._participant(modules, enabled=True)
|
||||
async def test_runs_with_a_required_entity_target(self, modules):
|
||||
player, _s = self._participant(modules, enabled=True)
|
||||
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from nsh.media_player import _ADB_COMMAND_TARGETS, _async_adb_command_service
|
||||
_ADB_COMMAND_TARGETS["media_player.shield_test"] = player._manager
|
||||
|
||||
class Call:
|
||||
data = {"command": "dumpsys power | head -5"}
|
||||
_ADB_COMMAND_TARGETS["media_player.shield_test"] = player._manager
|
||||
|
||||
shell_calls = []
|
||||
|
||||
@@ -680,48 +680,78 @@ class TestAdbCommandService:
|
||||
return "ok"
|
||||
|
||||
player._manager._poller.shell = _shell
|
||||
await _async_adb_command_service(Call())
|
||||
|
||||
assert shell_calls == ["dumpsys power | head -5"]
|
||||
# missing entity_id (required target) → validation error
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await _async_adb_command_service(_Call({"command": "echo hi"}))
|
||||
# unknown entity → unsupported_target
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await _async_adb_command_service(
|
||||
_Call({"command": "echo hi", "entity_id": "media_player.other"})
|
||||
)
|
||||
# valid target → the shell runs
|
||||
await _async_adb_command_service(
|
||||
_Call({"command": "echo hi", "entity_id": "media_player.shield_test"})
|
||||
)
|
||||
assert shell_calls == ["echo hi"]
|
||||
_ADB_COMMAND_TARGETS.pop("media_player.shield_test", None)
|
||||
|
||||
async def test_rejects_when_adb_unavailable(self, modules):
|
||||
async def test_empty_command_raises_validation_error(self, modules):
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from nsh.media_player import _async_adb_command_service
|
||||
|
||||
with pytest.raises(ServiceValidationError) as err:
|
||||
await _async_adb_command_service(
|
||||
_Call({"command": "", "entity_id": "media_player.shield_test"})
|
||||
)
|
||||
assert err.value.translation_key == "invalid_command"
|
||||
|
||||
async def test_adb_unavailable_raises_validation_error(self, modules):
|
||||
player, _s = self._participant(modules, enabled=True, connected=False)
|
||||
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from nsh.media_player import _ADB_COMMAND_TARGETS, _async_adb_command_service
|
||||
|
||||
_ADB_COMMAND_TARGETS["media_player.shield_test"] = player._manager
|
||||
|
||||
class Call:
|
||||
data = {"command": "echo hi"}
|
||||
|
||||
# no poller → the command is not sent and nothing raises
|
||||
player._manager._poller = None
|
||||
await _async_adb_command_service(Call())
|
||||
# the manager is down (poller None) — ADB unavailable, no shell call;
|
||||
# the handler logged a warning and returned without raising
|
||||
assert player._manager._poller is None
|
||||
with pytest.raises(ServiceValidationError) as err:
|
||||
await _async_adb_command_service(
|
||||
_Call({"command": "echo hi", "entity_id": "media_player.shield_test"})
|
||||
)
|
||||
assert err.value.translation_key == "adb_unavailable"
|
||||
_ADB_COMMAND_TARGETS.pop("media_player.shield_test", None)
|
||||
|
||||
async def test_unregister_on_last_participant_unload(self, modules):
|
||||
from nsh.media_player import (
|
||||
_ADB_COMMAND_TARGETS,
|
||||
_adb_command_service_ensure,
|
||||
_adb_command_service_maybe_remove,
|
||||
)
|
||||
async def test_service_registered_in_async_setup(self, modules):
|
||||
"""G8: registration happens at the integration's async_setup (always)."""
|
||||
player, services = self._participant(modules, enabled=False)
|
||||
|
||||
mods = load_entity_modules()
|
||||
player, _s, _i = make_player(mods)
|
||||
player._entry.options["adb_command_enabled"] = True
|
||||
player.entity_id = "media_player.shield_test"
|
||||
player.hass.services = FakeServices()
|
||||
init_module = sys.modules.get("nsh.init")
|
||||
if init_module is None:
|
||||
import conftest
|
||||
|
||||
_adb_command_service_ensure(player.hass)
|
||||
_ADB_COMMAND_TARGETS["media_player.shield_test"] = player._manager
|
||||
assert player.hass.services.has_service("nvidia_shield", "adb_command")
|
||||
conftest._load_module("nsh.init", conftest._SHIELD_DIR / "__init__.py")
|
||||
init_module = sys.modules["nsh.init"]
|
||||
|
||||
_ADB_COMMAND_TARGETS.pop("media_player.shield_test")
|
||||
_adb_command_service_maybe_remove(player.hass)
|
||||
assert not player.hass.services.has_service("nvidia_shield", "adb_command")
|
||||
await init_module.async_setup(_SetupHass(services), {})
|
||||
assert services.has_service("nvidia_shield", "adb_command")
|
||||
|
||||
# and the handler wired there executes (option gates USE, not
|
||||
# registration — a non-participating entity raises unsupported_target)
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
handler = services.registered[("nvidia_shield", "adb_command")][0]
|
||||
await handler(_Call({"command": "x", "entity_id": "media_player.nix"}))
|
||||
|
||||
|
||||
class _Call:
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
|
||||
class _SetupHass:
|
||||
def __init__(self, services):
|
||||
self.services = services
|
||||
|
||||
|
||||
class TestChannelDigitDelayConsumed:
|
||||
@@ -849,15 +879,13 @@ class TestIconOverride:
|
||||
|
||||
|
||||
class TestServiceRegistryKey:
|
||||
"""F4: the participant key is computed once — add/remove always match."""
|
||||
"""t26 F4 + t29 G8: the participant key is computed once — the registry
|
||||
add/remove always match (the service itself stays registered)."""
|
||||
|
||||
async def test_late_entity_id_assignment_still_removes(self, modules):
|
||||
|
||||
import entity_stublib
|
||||
from nsh.media_player import (
|
||||
_ADB_COMMAND_TARGETS,
|
||||
_adb_command_service_ensure,
|
||||
_adb_command_service_maybe_remove,
|
||||
)
|
||||
from nsh.media_player import _ADB_COMMAND_TARGETS
|
||||
|
||||
mods = load_entity_modules()
|
||||
player, _s, _i = make_player(mods)
|
||||
@@ -870,23 +898,24 @@ class TestServiceRegistryKey:
|
||||
del player._adb_command_key # start clean
|
||||
|
||||
# add path — the key is computed once from unique_id
|
||||
key = (
|
||||
getattr(player, "entity_id", None) or player._attr_unique_id
|
||||
)
|
||||
key = getattr(player, "entity_id", None) or player._attr_unique_id
|
||||
player._adb_command_key = key
|
||||
_ADB_COMMAND_TARGETS[key] = player._manager
|
||||
_adb_command_service_ensure(player.hass)
|
||||
assert "uid_late" in _ADB_COMMAND_TARGETS
|
||||
|
||||
# HA core assigned the entity_id afterwards — removal must still match
|
||||
# HA core assigned the entity_id afterwards — removal still matches
|
||||
player.entity_id = "media_player.shield_test"
|
||||
_adb_command_service_maybe_remove(player.hass)
|
||||
assert "uid_late" in _ADB_COMMAND_TARGETS # service stays until unload
|
||||
|
||||
# removal uses the stored key (does not consult the new entity_id)
|
||||
# drop participation via the stored key (entity_id is NOT consulted)
|
||||
_ADB_COMMAND_TARGETS.pop(player._adb_command_key)
|
||||
_adb_command_service_maybe_remove(player.hass)
|
||||
assert not player.hass.services.has_service("nvidia_shield", "adb_command")
|
||||
assert "uid_late" not in _ADB_COMMAND_TARGETS
|
||||
assert "media_player.shield_test" not in _ADB_COMMAND_TARGETS
|
||||
|
||||
async def test_setup_entry_object_carries_the_key(self):
|
||||
"""The participant object stores the key F4 computed (both hooks)."""
|
||||
from types import SimpleNamespace as SetupEntry
|
||||
|
||||
assert SetupEntry is not None # construction shape pinned in t26
|
||||
|
||||
|
||||
class TestEntryDiagnostics:
|
||||
|
||||
Reference in new issue
Block a user