Files
ha-nvidia-shield/tests/test_intents_registration.py
T
James Mackay 8f064b3247 fix(intents): an ADB-off entry never scans, and a truncated detail is never believed (t21)
The t18 review's three findings, closed with the reviewer's own repro.

F1 (HIGH — the harm was real shells, not a wrong label). `async_scan_intents`
decided between `adb_disabled` and `adb_unavailable` INSIDE `if self._poller is
None:`, so a manager that HELD a poller fell through to the shell loop: with
`adb_enabled: False` and a connected poller the review measured status `fresh`,
one candidate CACHED, and `pm dump com.x …` really sent. The option is a fact
about the ENTRY, so it is now checked once at the top and returns before
anything else — no cache read, no cache write, no shell. The no-poller branch
keeps the OTHER honest answer: with the option ON and nobody to ask, the status
is `adb_unavailable` ("wait or reconnect"), never `adb_disabled` ("go turn the
option on"), because the two have different fixes. Five tests pin it, including
a cache-leak case (a manager with a cached answer whose option is then switched
off answers `adb_disabled`, not the cache) and both statuses with a FORCED scan.

F2 (low, but it fabricated a value that looked real). A capture cut mid-detail
produced `Scheme: "ht` → scheme `"ht` and `Authority: "www.exam` → that
authority, and `_candidate_data` would then prefill the form with a URL no app
can match. Not an injection (everything stays quoted when used), which is
exactly why nothing caught it. `_unquote` now DROPS a value that opens a quote
and never closes it (a lone quote too), so the callers turn it into None: the
candidate survives with its intact action/component and simply carries no
scheme/authority/path — and therefore prefills NO data instead of a broken URL.
A complete quoted value is still read (the anti-overcorrection pin), and the
consequence is pinned at the consumption end too, through the real options-flow
helper.

F3 (low). The floor comment now answers the review in writing, where the next
reader looks: the t18 review's measurement (awaited_calls 6 against a floor of 5,
three other counters within 2) is named, and the verdict is recorded — those ARE
the policy margins (2 for owner-counters because an owner really can be
refactored away, 1 for awaited_calls because six sites cannot carry a percentage
band). The counters were genuinely updated, not dodged: raised to the real sweep
when the code grew (20→24, 70→75, 235→275, 55→63, 1400→1620) and re-derived
twice since (t20 1620→1615, t21 1615→1617 for this unit's blocks).

EVIDENCE THAT THE TESTS PIN THE FIX (not merely pass): the same files were run
against the PRE-fix source (git archive HEAD, a throwaway copy under /tmp) — 9 of
the 11 new tests FAIL there, including the reviewer's repro, which fails with
`assert 'fresh' == 'adb_disabled'`. The two that pass are the deliberate
anti-overcorrection pins: a COMPLETE quoted value is still parsed, and the
no-poller control keeps its own status.

NO REGRESSION: 900 passed / 5 skipped, 901 / 4 with androidtvremote2; the
injection proof, the fixture's candidates with dedup/flagging/order/bound
disclosures, both cache outcomes, the registration round trip through the real
FlowManager, button lifecycle and defaults, the YAML seed rule and §7.1
(supports_response optional, a real return_response call, a no-response call) all
still pass. The strings files are byte-identical (untouched here). Real-HA pattern
collected and RAN 3 files → 74 passed / 0 skipped. Drift exit 0 (1650 blocks).

NOT VERIFIED HERE: the device side remains a scripted double — no real Shield was
asked for its resolver table, so the shell COUNT and the status vocabulary are
what is proven, not real firmware behaviour. No browser either: the form prefill
is asserted on the values the helper returns.
2026-10-12 13:14:34 +01:00

1373 lines
54 KiB
Python

"""tests/test_intents_registration.py — t17: the registered-intent feature.
Four things are pinned here, in the order the user meets them:
1. THE MODEL AND THE COMMAND (`media_player.py`): what a registered intent is,
that an unusable row never becomes a button, and — the part that matters —
that the ``am start`` command is BUILT from validated data with plain text
substitution and quoted values, so nothing a variable contains can become
shell syntax.
2. THE SCREEN (`config_flow.py`): the Intents sub-menu, the selector with its
Add-new sentinel, the per-intent form, removal, and the scan that offers a
discovered candidate — plus the seed validator, which must produce a row
IDENTICAL to one typed into the form.
3. THE BUTTON (`button.py`): one entity per registration, sending the declared
defaults, failing loudly (the uninstalled-app case is the device's own
message, not a swallowed no-op).
4. THE ACTION (`media_player._async_send_intent_service`): the intent lookup,
the variables, and the output returned under ``intent_response``.
The flow and the entity modules are loaded in ONE stub environment on purpose:
point 1's model exists twice (the runtime's reader and the screen's), and the
test at the bottom of ``TestTheModel`` drives both over the same rows so they
cannot drift into meaning different things.
"""
from __future__ import annotations
import asyncio
import importlib
import shlex
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent))
import entity_stublib # noqa: E402
from entity_stublib import load_entity_modules # noqa: E402
from nsh.const import ( # noqa: E402
CONF_REGISTERED_INTENTS,
INTENT_FIELD_ACTION,
INTENT_FIELD_ACTIVITY,
INTENT_FIELD_CANDIDATE,
INTENT_FIELD_CATEGORIES,
INTENT_FIELD_CHOICE,
INTENT_FIELD_DATA,
INTENT_FIELD_DECLARED,
INTENT_FIELD_LIMIT,
INTENT_FIELD_NAME,
INTENT_FIELD_PACKAGE,
INTENT_FIELD_SCAN_PACKAGE,
INTENT_NEW_SENTINEL,
INTENT_REGISTRATION_LIMIT,
INTENT_RESPONSE_KEY,
)
from test_standalone_setup import FakeHass, _form_module, _install_stubs # noqa: E402
#: A registration as the plan describes it: a component, a data template with a
#: declared placeholder, and a required field the caller must supply.
NETFLIX_INTENT = {
"id": "netflix_title",
"name": "Netflix: open a title",
"package": "com.netflix.ninja",
"activity": "com.netflix.ninja/.MainActivity",
"action": "android.intent.action.VIEW",
"categories": ["android.intent.category.DEFAULT"],
"data": "https://www.netflix.com/watch/{id}",
"fields": [{"key": "id", "type": "string", "required": True}],
}
@pytest.fixture(autouse=True)
def scoped_stubs():
"""Keep the import stubs and the loaded modules inside this file (t7)."""
snapshot = entity_stublib.snapshot_modules()
_install_stubs()
yield
entity_stublib.restore_modules(snapshot)
@pytest.fixture()
def modules(tmp_path):
"""The flow module AND the entity modules, in one environment."""
flow = _form_module()
media = load_entity_modules()
return SimpleNamespace(flow=flow, media=media["media_player"])
class _RecordingPoller:
"""A poller that answers with what a test puts on the wire."""
def __init__(self, answer='Starting: Intent { act=android.intent.action.VIEW }'):
self.answer = answer
self.commands: list[str] = []
async def shell(self, command):
self.commands.append(command)
if isinstance(self.answer, Exception):
raise self.answer
return self.answer
def _manager(poller, *, connected=True):
return SimpleNamespace(adb_poller=poller, connected=connected, entry=None)
# ── 1. the model and the command ───────────────────────────────────────────
class TestTheCommand:
"""The `am start` command: built, quoted, and never evaluated."""
def test_the_plan_s_mapping_is_exactly_what_is_sent(self, modules):
command = modules.media.intent_am_start_command(
NETFLIX_INTENT, {"id": "80100172"}
)
assert command == (
"am start -n com.netflix.ninja/.MainActivity "
"-a android.intent.action.VIEW -c android.intent.category.DEFAULT "
"-d https://www.netflix.com/watch/80100172 --es id 80100172"
)
def test_every_declared_type_becomes_its_own_flag(self, modules):
intent = {
**NETFLIX_INTENT,
"data": "",
"fields": [
{"key": "title", "type": "string", "required": True},
{"key": "season", "type": "int", "required": True},
{"key": "resume", "type": "bool", "required": True},
{"key": "extra", "type": "raw", "required": True},
],
}
command = modules.media.intent_am_start_command(
intent,
{"title": "Sky", "season": "2", "resume": "YES", "extra": "--user 0"},
)
assert "--es title Sky" in command
assert "--ei season 2" in command
assert "--ez resume true" in command # normalised to the device's boolean
assert "--es extra '--user 0'" in command, "raw is quoted like everything else"
def test_a_hostile_variable_is_one_literal_argument(self, modules):
hostile = '"; rm -rf /"'
command = modules.media.intent_am_start_command(
{**NETFLIX_INTENT, "data": ""}, {"id": hostile}
)
tokens = shlex.split(command)
# the shell sees the value as ONE argument, exactly as typed
assert tokens == ["am", "start", "-n", NETFLIX_INTENT["activity"],
"-a", NETFLIX_INTENT["action"],
"-c", "android.intent.category.DEFAULT",
"--es", "id", hostile]
assert "rm -rf" not in command.split("--es")[0]
def test_a_hostile_data_value_is_quoted_too(self, modules):
command = modules.media.intent_am_start_command(
NETFLIX_INTENT, {"id": "a; reboot"}
)
assert shlex.split(command)[-1] == "a; reboot"
assert "https://www.netflix.com/watch/a; reboot" in shlex.split(command)
def test_substitution_is_plain_text_and_runs_once(self, modules):
"""A value that LOOKS like a placeholder is data, never re-substituted."""
intent = {
**NETFLIX_INTENT,
"data": "https://example.test/{id}",
"fields": [{"key": "id", "type": "string", "required": True}],
}
command = modules.media.intent_am_start_command(intent, {"id": "{name}"})
assert "https://example.test/{name}" in command
assert shlex.split(command)[-1] == "{name}"
def test_attribute_style_placeholders_are_left_alone(self, modules):
"""No eval, no format(): ``{id.__class__}`` is not a placeholder at all."""
intent = {**NETFLIX_INTENT, "data": "https://example.test/{id.__class__}"}
command = modules.media.intent_am_start_command(intent, {"id": "1"})
assert "https://example.test/{id.__class__}" in command
def test_a_missing_required_field_names_the_field(self, modules):
err = pytest.raises(
modules.media.ServiceValidationError if hasattr(modules.media, "ServiceValidationError")
else Exception
)
with err as caught:
modules.media.intent_am_start_command(NETFLIX_INTENT, {})
assert caught.value.translation_key == "missing_intent_field"
assert caught.value.translation_placeholders["field"] == "id"
assert "id" in str(caught.value)
def test_an_undeclared_variable_is_refused_not_dropped(self, modules):
import homeassistant.exceptions as ha_exceptions
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
modules.media.intent_am_start_command(
NETFLIX_INTENT, {"id": "1", "sneaky": "x"}
)
assert caught.value.translation_key == "unknown_intent_variable"
assert caught.value.translation_placeholders["fields"] == "sneaky"
def test_a_value_that_cannot_become_its_type_names_field_and_value(self, modules):
import homeassistant.exceptions as ha_exceptions
intent = {
**NETFLIX_INTENT,
"data": "",
"fields": [{"key": "season", "type": "int", "required": True}],
}
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
modules.media.intent_am_start_command(intent, {"season": "winter"})
assert caught.value.translation_key == "invalid_intent_field"
assert caught.value.translation_placeholders["field"] == "season"
assert "winter" in str(caught.value)
def test_an_unfilled_placeholder_raises_instead_of_sending_braces(self, modules):
import homeassistant.exceptions as ha_exceptions
intent = {**NETFLIX_INTENT, "fields": []} # nothing declares {id}
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
modules.media.intent_am_start_command(intent, {})
assert caught.value.translation_key == "unresolved_intent_placeholder"
assert caught.value.translation_placeholders["placeholder"] == "id"
def test_defaults_satisfy_the_template(self, modules):
intent = {
**NETFLIX_INTENT,
"fields": [{"key": "id", "type": "string", "default": "42"}],
}
assert "watch/42" in modules.media.intent_am_start_command(intent, {})
assert modules.media.intent_default_variables(intent) == {"id": "42"}
def test_an_optional_field_without_a_value_is_simply_not_sent(self, modules):
intent = {
**NETFLIX_INTENT,
"data": "",
"fields": [{"key": "quality", "type": "string"}],
}
command = modules.media.intent_am_start_command(intent, {})
assert "--es" not in command
assert "quality" not in command
class TestRunningAnIntent:
"""What actually reaches the device, and what happens when it refuses."""
async def test_the_device_gets_exactly_one_built_command(self, modules):
poller = _RecordingPoller()
manager = _manager(poller)
output = await modules.media.run_registered_intent(
manager, "media_player.shield", NETFLIX_INTENT, {"id": "1"}
)
assert output.startswith("Starting: Intent")
assert poller.commands == [
modules.media.intent_am_start_command(NETFLIX_INTENT, {"id": "1"})
]
async def test_adb_down_says_the_protocol_cannot_carry_an_intent(self, modules):
import homeassistant.exceptions as ha_exceptions
for manager in (_manager(None), _manager(_RecordingPoller(), connected=False)):
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
await modules.media.run_registered_intent(
manager, "media_player.shield", NETFLIX_INTENT, {"id": "1"}
)
assert caught.value.translation_key == "intents_need_adb"
assert "protocol" in str(caught.value)
async def test_a_transport_that_never_answered_is_an_error(self, modules):
import homeassistant.exceptions as ha_exceptions
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
await modules.media.run_registered_intent(
_manager(_RecordingPoller(None)),
"media_player.shield",
NETFLIX_INTENT,
{"id": "1"},
)
assert caught.value.translation_key == "intent_no_answer"
async def test_a_refused_intent_keeps_the_device_s_own_words(self, modules):
import homeassistant.exceptions as ha_exceptions
refusal = (
"Starting: Intent { act=android.intent.action.VIEW }\n"
"Error: Activity class {com.netflix.ninja/.MainActivity} does not exist."
)
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
await modules.media.run_registered_intent(
_manager(_RecordingPoller(refusal)),
"media_player.shield",
NETFLIX_INTENT,
{"id": "1"},
)
assert caught.value.translation_key == "intent_failed"
assert "does not exist" in str(caught.value)
assert "com.netflix.ninja/.MainActivity" in str(caught.value)
def test_the_failure_detector_knows_a_success_from_a_refusal(self, modules):
detect = modules.media.intent_failure_message
assert detect("Starting: Intent { cmp=com.a/.B }") is None
assert detect(None) is None
assert "unable to resolve" in detect(
"Error: Unable to resolve Intent { act=x }"
).lower()
assert detect("Permission Denial: starting Intent requires permission") is not None
# ── 2. the model as READ, and the screen's copy of it ──────────────────────
class TestTheModel:
"""What counts as a usable registration, and the two readers agreeing."""
def test_a_row_that_names_nothing_to_launch_is_dropped(self, modules):
rows = modules.media.registered_intents(
{
CONF_REGISTERED_INTENTS: [
NETFLIX_INTENT,
{"id": "broken", "name": "Broken", "package": "com.x"},
{"id": "nolabel", "package": "com.x", "action": "a.B"},
]
}
)
assert [row["id"] for row in rows] == ["netflix_title"]
def test_categories_and_fields_are_normalised(self, modules):
rows = modules.media.registered_intents(
{
CONF_REGISTERED_INTENTS: [
{
**NETFLIX_INTENT,
"categories": ["", " android.intent.category.DEFAULT "],
"fields": [
{"key": "id", "type": "STRING", "required": 1},
{"key": "9bad", "type": "string"},
{"key": "ok", "type": "nope"},
],
}
]
}
)
assert rows[0]["categories"] == ["android.intent.category.DEFAULT"]
assert rows[0]["fields"] == [{"key": "id", "type": "string", "required": True}]
def test_duplicate_ids_keep_the_first_and_the_cap_holds(self, modules):
rows = modules.media.registered_intents(
{
CONF_REGISTERED_INTENTS: [
{**NETFLIX_INTENT, "name": "First"},
{**NETFLIX_INTENT, "name": "Second"},
]
+ [
{**NETFLIX_INTENT, "id": f"i{n}", "name": f"n{n}"}
for n in range(INTENT_REGISTRATION_LIMIT + 5)
]
}
)
assert rows[0]["name"] == "First"
assert len(rows) == INTENT_REGISTRATION_LIMIT
def test_a_mapping_shaped_blob_still_reads(self, modules):
rows = modules.media.registered_intents(
{CONF_REGISTERED_INTENTS: {"netflix_title": NETFLIX_INTENT}}
)
assert [row["id"] for row in rows] == ["netflix_title"]
def test_lookup_by_id_and_by_name(self, modules):
options = {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]}
assert modules.media.intent_by_id(options, "netflix_title")["name"] == (
NETFLIX_INTENT["name"]
)
assert modules.media.intent_by_id(options, NETFLIX_INTENT["name"]) is not None
assert modules.media.intent_by_id(options, "nope") is None
def test_the_screen_and_the_runtime_read_the_same_rows(self, modules):
"""The anti-drift pin: the flow's reader and the runtime's must agree.
They live in different modules because the options flow must stay
importable without Home Assistant's media_player platform. This drives
both over one battery — valid rows, junk, a duplicate id, an over-long
list, a mapping-shaped blob — and requires the same ids, names, packages,
components, actions, categories and fields from each.
"""
battery = [
NETFLIX_INTENT,
{**NETFLIX_INTENT, "id": "second"},
{"id": "junk", "name": "Junk", "package": "com.x"},
"not-a-dict",
{**NETFLIX_INTENT},
{**NETFLIX_INTENT, "activity": "no-slash", "action": "a.B"},
{**NETFLIX_INTENT, "id": "fields", "fields": [{"key": "bad key"}]},
]
runtime = modules.media.registered_intents({CONF_REGISTERED_INTENTS: battery})
screen = modules.flow.registered_intent_rows({CONF_REGISTERED_INTENTS: battery})
assert runtime == screen
# ── 3. the screen ──────────────────────────────────────────────────────────
def _flow(modules, tmp_path, options=None):
flow = modules.flow.NvidiaShieldOptionsFlow()
flow.config_entry = SimpleNamespace(
options=dict(options or {}), entry_id="e1", title="Shield"
)
flow.hass = FakeHass(str(tmp_path))
flow.async_show_form = lambda **kwargs: {"type": "form", **kwargs}
flow.async_show_menu = lambda **kwargs: {"type": "menu", **kwargs}
flow.async_create_entry = lambda **kwargs: {"type": "create_entry", **kwargs}
return flow
def _form_payload(**overrides):
payload = {
INTENT_FIELD_NAME: NETFLIX_INTENT["name"],
INTENT_FIELD_PACKAGE: NETFLIX_INTENT["package"],
INTENT_FIELD_ACTIVITY: NETFLIX_INTENT["activity"],
INTENT_FIELD_ACTION: NETFLIX_INTENT["action"],
INTENT_FIELD_CATEGORIES: "android.intent.category.DEFAULT",
INTENT_FIELD_DATA: NETFLIX_INTENT["data"],
INTENT_FIELD_DECLARED: "id:string:required",
}
payload.update(overrides)
return payload
class TestTheIntentsScreen:
"""The menu, the sentinel, the form, removal — and nothing half-stored."""
async def test_the_menu_offers_the_screen_and_labels_it(self, modules, tmp_path):
flow = _flow(modules, tmp_path)
menu = await flow.async_step_intents(None)
assert menu["type"] == "menu"
assert list(menu["menu_options"]) == ["intents_edit", "intents_scan", "menu_again"]
assert "no intents registered yet" in menu["description_placeholders"]["intents"]
async def test_remove_appears_only_with_something_to_remove(self, modules, tmp_path):
flow = _flow(modules, tmp_path)
menu = await flow.async_step_intents(None)
assert "intents_remove" not in menu["menu_options"]
flow._options_dict[CONF_REGISTERED_INTENTS] = [NETFLIX_INTENT]
menu = await flow.async_step_intents(None)
assert "intents_remove" in menu["menu_options"]
assert "1 intent registered" in menu["description_placeholders"]["intents"]
async def test_the_selector_lists_every_intent_and_the_add_new_sentinel(
self, modules, tmp_path
):
flow = _flow(modules, tmp_path, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]})
result = await flow.async_step_intents_edit(None)
assert result["type"] == "form" and result["step_id"] == "intents_edit"
# the selector is the field the form declares; its options carry the
# registered intent AND the explicit Add-new row
choices = flow._intent_choices()
assert [row["value"] for row in choices] == ["netflix_title", INTENT_NEW_SENTINEL]
assert choices[-1]["label"].startswith("+ Add a new intent")
async def test_add_new_opens_a_blank_form(self, modules, tmp_path):
flow = _flow(modules, tmp_path, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]})
form = await flow.async_step_intents_edit(
{INTENT_FIELD_CHOICE: INTENT_NEW_SENTINEL}
)
assert form["type"] == "form" and form["step_id"] == "intents_form"
assert "new intent" in form["description_placeholders"]["intent_note"]
async def test_an_unknown_choice_is_refused(self, modules, tmp_path):
flow = _flow(modules, tmp_path, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]})
result = await flow.async_step_intents_edit({INTENT_FIELD_CHOICE: "ghost"})
assert result["errors"] == {"base": "unknown_intent_row"}
async def test_a_new_intent_is_stored_with_a_slug_id(self, modules, tmp_path):
flow = _flow(modules, tmp_path)
result = await flow.async_step_intents_form(_form_payload())
assert result["type"] == "menu" # back at the Intents editor
stored = flow._options_dict[CONF_REGISTERED_INTENTS]
assert len(stored) == 1
assert stored[0]["id"] == "netflix_open_a_title"
assert stored[0]["fields"] == [{"key": "id", "type": "string", "required": True}]
assert stored[0]["categories"] == ["android.intent.category.DEFAULT"]
async def test_stored_rows_are_json_safe(self, modules, tmp_path):
"""Options are persisted as JSON: a set or a tuple here would break storage."""
import json
flow = _flow(modules, tmp_path)
await flow.async_step_intents_form(_form_payload())
json.dumps(flow._options_dict) # raises if a row is not serialisable
async def test_editing_one_row_leaves_the_others_alone(self, modules, tmp_path):
other = {**NETFLIX_INTENT, "id": "other", "name": "Another"}
flow = _flow(
modules, tmp_path, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT, other]}
)
result = await flow.async_step_intents_edit(
{INTENT_FIELD_CHOICE: "netflix_title"}
)
assert result["step_id"] == "intents_form"
assert result["description_placeholders"]["intent_note"].startswith("editing ")
await flow.async_step_intents_form(
_form_payload(**{INTENT_FIELD_NAME: "Renamed"})
)
stored = flow._options_dict[CONF_REGISTERED_INTENTS]
assert [row["id"] for row in stored] == ["netflix_title", "other"]
assert stored[0]["name"] == "Renamed" # the rename landed on THAT row
assert stored[1] == other # …and this one is byte-identical to before
async def test_a_refused_submission_stores_nothing_and_says_why(
self, modules, tmp_path
):
flow = _flow(modules, tmp_path, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]})
result = await flow.async_step_intents_form(
_form_payload(
**{
INTENT_FIELD_NAME: "Nothing to launch",
INTENT_FIELD_ACTIVITY: "",
INTENT_FIELD_ACTION: "",
}
)
)
assert result["type"] == "form"
assert result["errors"] == {"base": "invalid_intent_row"}
note = result["description_placeholders"]["intent_note"]
assert "component" in note and "action" in note
assert len(flow._options_dict[CONF_REGISTERED_INTENTS]) == 1 # unchanged
async def test_each_refusal_names_its_own_problem(self, modules, tmp_path):
cases = [
({INTENT_FIELD_PACKAGE: "has spaces"}, "not a usable package id"),
({INTENT_FIELD_ACTIVITY: "com.x.MainActivity"}, "not a component"),
({INTENT_FIELD_DATA: "https://x/{nope}"}, "no declared field"),
({INTENT_FIELD_DECLARED: "id:weird"}, "is not one of"),
({INTENT_FIELD_NAME: NETFLIX_INTENT["name"]}, "already called"),
]
for overrides, expected in cases:
flow = _flow(modules, tmp_path, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]})
payload = _form_payload(**overrides)
if INTENT_FIELD_NAME in overrides:
payload[INTENT_FIELD_CHOICE] = INTENT_NEW_SENTINEL
else:
flow._options_dict[CONF_REGISTERED_INTENTS] = [NETFLIX_INTENT]
payload[INTENT_FIELD_NAME] = "New name"
result = await flow.async_step_intents_form(payload)
assert result["errors"] == {"base": "invalid_intent_row"}, overrides
assert expected in result["description_placeholders"]["intent_note"], overrides
async def test_removal_takes_a_confirmation_and_drops_only_that_row(
self, modules, tmp_path
):
other = {**NETFLIX_INTENT, "id": "other", "name": "Another"}
flow = _flow(
modules, tmp_path, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT, other]}
)
confirm = await flow.async_step_intents_remove(
{INTENT_FIELD_CHOICE: "netflix_title"}
)
assert confirm["step_id"] == "intents_remove_confirm"
assert confirm["description_placeholders"]["intent"] == (
"Netflix: open a title (com.netflix.ninja)"
)
assert confirm["description_placeholders"]["button"] == NETFLIX_INTENT["name"]
assert "intents_remove_yes" in confirm["menu_options"], "it takes a second act"
# nothing is dropped until the user confirms
assert len(flow._options_dict[CONF_REGISTERED_INTENTS]) == 2
await flow.async_step_intents_remove_yes(None)
stored = flow._options_dict[CONF_REGISTERED_INTENTS]
assert [row["id"] for row in stored] == ["other"]
async def test_cancelling_a_removal_changes_nothing(self, modules, tmp_path):
flow = _flow(modules, tmp_path, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]})
await flow.async_step_intents_remove({INTENT_FIELD_CHOICE: "netflix_title"})
result = await flow.async_step_intents_remove_no(None)
assert result["step_id"] == "intents"
assert flow._options_dict[CONF_REGISTERED_INTENTS] == [NETFLIX_INTENT]
async def test_a_registration_survives_a_new_flow_on_the_same_options(
self, modules, tmp_path
):
"""The options ARE the store: a fresh flow reads what the last one wrote."""
options = {}
first = _flow(modules, tmp_path, options)
await first.async_step_intents_form(_form_payload())
options.update(first._options_dict)
second = _flow(modules, tmp_path, options)
assert [row["id"] for row in second._registered_intents()] == [
"netflix_open_a_title"
]
class TestTheDeclaredFieldsText:
"""The `key:type[:required][=value]` grammar, one line at a time."""
def test_the_grammar_reads_every_shape(self, modules):
fields, problems = modules.flow.intent_fields_from_text(
"# a comment\nid:string:required\nseason:int:required\n"
"quality:string=high\nresume:bool\nblob:raw\n"
)
assert problems == []
assert fields == [
{"key": "id", "type": "string", "required": True},
{"key": "season", "type": "int", "required": True},
{"key": "quality", "type": "string", "required": False, "default": "high"},
{"key": "resume", "type": "bool", "required": False},
{"key": "blob", "type": "raw", "required": False},
]
def test_every_problem_names_its_line(self, modules):
fields, problems = modules.flow.intent_fields_from_text(
"id:string\nnoline\n9bad:string\nid:string:maybe\nboth:int:required=1\nid:string\n"
)
assert fields is None
assert [problem.split(":")[0] for problem in problems] == [
"fields line 2",
"fields line 3",
"fields line 4",
"fields line 5",
"fields line 6",
]
def test_the_text_round_trips_through_the_form(self, modules):
fields = [
{"key": "id", "type": "string", "required": True},
{"key": "quality", "type": "string", "required": False, "default": "high"},
]
text = modules.flow.intent_fields_to_text(fields)
assert text == "id:string:required\nquality:string=high"
assert modules.flow.intent_fields_from_text(text)[0] == fields
def test_the_cap_is_refused_rather_than_truncated(self, modules):
text = "\n".join(f"k{n}:string" for n in range(INTENT_FIELD_LIMIT + 2))
fields, problems = modules.flow.intent_fields_from_text(text)
assert fields is None
assert any("cap" in problem for problem in problems)
class _FakeScan:
"""A stand-in for `manager.async_scan_intents` (t16's API)."""
def __init__(self, result=None, exc=None):
self.result = result
self.exc = exc
self.packages: list[str] = []
async def __call__(self, package):
self.packages.append(package)
if self.exc is not None:
raise self.exc
return self.result
def _candidate(**overrides):
"""One discovered candidate, with the dedup KEY the real one carries.
``candidate_value`` is built from ``IntentCandidate.key`` (t16), so the fake
has to have one — and two fakes differing only in a field must differ in it,
exactly as the real candidates do.
"""
base = {
"component": "com.netflix.ninja/.MainActivity",
"action": "android.intent.action.VIEW",
"categories": ("android.intent.category.DEFAULT",),
"scheme": "https",
"authority": "www.netflix.com",
"path": "/browse",
"path_kind": "LITERAL",
"likely_internal": False,
}
base.update(overrides)
base["key"] = (
base["component"],
base["action"],
base["categories"],
base["scheme"],
base["authority"],
base["path"],
base["path_kind"],
)
return SimpleNamespace(**base)
def _scan_result(candidates=(), *, status="fresh", package="com.netflix.ninja"):
return SimpleNamespace(
package=package,
status=status,
candidates=tuple(candidates),
entry_state="candidates" if candidates else "empty",
age_seconds=0.0,
truncated=False,
)
class TestTheScanStep:
"""The scan answers honestly, and a candidate prefills only what it can."""
async def test_no_manager_anywhere_is_reported_not_crashed(self, modules, tmp_path):
flow = _flow(modules, tmp_path)
flow.config_entry = SimpleNamespace(options={}, entry_id="e1", title="Shield")
flow.hass.data = {} # no runtime_data, no domain store
result = await flow.async_step_intents_scan(
{INTENT_FIELD_SCAN_PACKAGE: "com.netflix.ninja"}
)
assert result["step_id"] == "intents_scan_result"
note = result["description_placeholders"]["scan_note"]
assert "no intent discovery" in note
assert "Nothing was registered" in note
async def test_a_bad_package_never_reaches_the_manager(self, modules, tmp_path):
flow = _flow(modules, tmp_path)
scan = _FakeScan(_scan_result())
flow.config_entry.runtime_data = SimpleNamespace(async_scan_intents=scan)
result = await flow.async_step_intents_scan({INTENT_FIELD_SCAN_PACKAGE: "not one"})
assert result["errors"] == {"base": "invalid_intent_package"}
assert scan.packages == []
async def test_one_package_is_scanned_and_its_candidates_offered(
self, modules, tmp_path
):
flow = _flow(modules, tmp_path)
scan = _FakeScan(
_scan_result(
[_candidate(), _candidate(path="/title.*", path_kind="GLOB")]
)
)
flow.config_entry.runtime_data = SimpleNamespace(async_scan_intents=scan)
result = await flow.async_step_intents_scan(
{INTENT_FIELD_SCAN_PACKAGE: "com.netflix.ninja"}
)
assert scan.packages == ["com.netflix.ninja"], "ONE package per request"
note = result["description_placeholders"]["scan_note"]
assert "2 candidate intent(s)" in note
assert "Nothing has been registered yet" in note
async def test_each_status_reads_differently(self, modules, tmp_path):
cases = {
"fresh_empty": "advertises no intents",
"adb_disabled": "switched OFF",
"adb_unavailable": "did not answer",
"unreadable": "did not answer",
"cached": "cache",
}
for status, expected in cases.items():
note = modules.flow.intent_scan_note(
_scan_result(status=status), "com.netflix.ninja"
)
assert expected in note, status
async def test_a_literal_candidate_prefills_a_real_url(self, modules, tmp_path):
candidate = _candidate() # https + authority + LITERAL /browse
flow = _flow(modules, tmp_path)
scan = _FakeScan(_scan_result([candidate]))
flow.config_entry.runtime_data = SimpleNamespace(async_scan_intents=scan)
await flow.async_step_intents_scan(
{INTENT_FIELD_SCAN_PACKAGE: "com.netflix.ninja"}
)
form = await flow.async_step_intents_scan_result(
{INTENT_FIELD_CANDIDATE: modules.flow.candidate_value(candidate)}
)
assert form["step_id"] == "intents_form"
assert form["type"] == "form" # the schema is rendered, not submitted
assert form["description_placeholders"]["intent_note"] == "new intent"
assert flow._intent_prefill[INTENT_FIELD_DATA] == "https://www.netflix.com/browse"
assert flow._intent_prefill[INTENT_FIELD_ACTIVITY] == candidate.component
assert flow._intent_prefill[INTENT_FIELD_ACTION] == candidate.action
async def test_a_path_pattern_prefills_no_url_and_says_why(self, modules, tmp_path):
candidate = _candidate(path="/title.*", path_kind="GLOB")
flow = _flow(modules, tmp_path)
scan = _FakeScan(_scan_result([candidate]))
flow.config_entry.runtime_data = SimpleNamespace(async_scan_intents=scan)
await flow.async_step_intents_scan(
{INTENT_FIELD_SCAN_PACKAGE: "com.netflix.ninja"}
)
form = await flow.async_step_intents_scan_result(
{INTENT_FIELD_CANDIDATE: modules.flow.candidate_value(candidate)}
)
assert flow._intent_prefill[INTENT_FIELD_DATA] == ""
assert "pattern" in form["description_placeholders"]["intent_note"]
async def test_an_internal_candidate_is_offered_and_marked(self, modules, tmp_path):
candidate = _candidate(action="com.netflix.action.DEBUG_FORCE_EXIT", likely_internal=True)
assert "[internal-looking]" in modules.flow.candidate_label(candidate)
flow = _flow(modules, tmp_path)
flow.config_entry.runtime_data = SimpleNamespace(
async_scan_intents=_FakeScan(_scan_result([candidate]))
)
await flow.async_step_intents_scan(
{INTENT_FIELD_SCAN_PACKAGE: "com.netflix.ninja"}
)
form = await flow.async_step_intents_scan_result(
{INTENT_FIELD_CANDIDATE: modules.flow.candidate_value(candidate)}
)
assert "internal hook" in form["description_placeholders"]["intent_note"]
async def test_a_scan_that_cannot_run_registers_nothing(self, modules, tmp_path):
flow = _flow(modules, tmp_path)
flow.config_entry.runtime_data = SimpleNamespace(
async_scan_intents=_FakeScan(exc=RuntimeError("adb died"))
)
result = await flow.async_step_intents_scan(
{INTENT_FIELD_SCAN_PACKAGE: "com.netflix.ninja"}
)
assert result["step_id"] == "intents_scan_result"
assert CONF_REGISTERED_INTENTS not in flow._options_dict
class TestTheSeed:
"""`registered_intents:` in configuration.yaml — validated, or named and dropped."""
def test_a_valid_seed_produces_the_form_s_row(self, modules):
seeded = modules.flow._seeded_intent_rows([NETFLIX_INTENT])
assert len(seeded) == 1
# …and it is the SAME row the form would have built from those values
typed, problems = modules.flow.intent_row_from_form(
{
INTENT_FIELD_NAME: NETFLIX_INTENT["name"],
INTENT_FIELD_PACKAGE: NETFLIX_INTENT["package"],
INTENT_FIELD_ACTIVITY: NETFLIX_INTENT["activity"],
INTENT_FIELD_ACTION: NETFLIX_INTENT["action"],
INTENT_FIELD_CATEGORIES: ", ".join(NETFLIX_INTENT["categories"]),
INTENT_FIELD_DATA: NETFLIX_INTENT["data"],
INTENT_FIELD_DECLARED: "id:string:required",
},
existing_id=NETFLIX_INTENT["id"],
taken_ids=set(),
taken_names=set(),
)
assert problems == []
assert seeded[0] == typed
def test_a_broken_row_is_dropped_with_a_warning(self, modules, caplog):
rows = [
NETFLIX_INTENT,
{"name": "No target", "package": "com.x"},
{"name": "Bad package", "package": "has spaces", "action": "a.B"},
"not a mapping",
]
with caplog.at_level("WARNING"):
seeded = modules.flow._seeded_intent_rows(rows)
assert [row["id"] for row in seeded] == [NETFLIX_INTENT["id"]]
assert len(caplog.records) == 3, [r.message for r in caplog.records]
assert all("registered_intents row" in r.message for r in caplog.records)
def test_an_explicit_id_is_kept_and_a_duplicate_is_skipped(self, modules, caplog):
with caplog.at_level("WARNING"):
seeded = modules.flow._seeded_intent_rows(
[NETFLIX_INTENT, {**NETFLIX_INTENT, "name": "Duplicate"}]
)
assert [row["id"] for row in seeded] == [NETFLIX_INTENT["id"]]
assert "repeats the id" in caplog.records[0].message
def test_every_documented_row_key_is_honoured(self, modules):
"""The README's documented keys and the seed validator must agree (t17).
A key the docs show but the validator drops would be worse than a typo:
it would look configured and do nothing.
"""
from test_readme import INTENT_ROW_KEYS
row = {
"id": "example",
"name": "Example",
"package": "com.example.app",
"activity": "com.example.app/.MainActivity",
"action": "android.intent.action.VIEW",
"categories": ["android.intent.category.DEFAULT"],
"data": "https://example.test/{slug}",
"fields": [{"key": "slug", "type": "string"}],
}
assert set(row) == INTENT_ROW_KEYS, "the fixture must cover the docs"
seeded = modules.flow._seeded_intent_rows([row])
assert set(seeded[0]) == INTENT_ROW_KEYS, "every documented key survives"
def test_a_non_list_block_is_ignored_with_a_warning(self, modules, caplog):
with caplog.at_level("WARNING"):
seeded = modules.flow._seeded_intent_rows("nope")
assert seeded == []
assert "must be a LIST" in caplog.records[0].message
# ── 4. the buttons ─────────────────────────────────────────────────────────
class TestTheButtons:
"""One entity per registration, sending the defaults, failing loudly."""
async def _setup(self, modules, options):
entry = SimpleNamespace(
entry_id="e1", options=options, title="Shield", runtime_data=None
)
added: list = []
button = importlib.import_module("nsh.button")
await button.async_setup_entry(None, entry, added.extend)
return entry, added
async def test_one_button_per_registered_intent(self, modules):
_entry, added = await self._setup(
modules, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]}
)
assert len(added) == 1
assert added[0]._attr_name == NETFLIX_INTENT["name"]
assert added[0]._attr_unique_id.endswith("_intent_netflix_title")
async def test_no_registration_means_no_button(self, modules):
_entry, added = await self._setup(modules, {})
assert added == []
async def test_a_rename_keeps_the_entity_identity(self, modules):
_entry, added = await self._setup(
modules, {CONF_REGISTERED_INTENTS: [{**NETFLIX_INTENT, "name": "Renamed"}]}
)
assert added[0]._attr_name == "Renamed"
assert added[0]._attr_unique_id.endswith("_intent_netflix_title")
async def test_the_button_shows_what_it_will_send(self, modules):
_entry, added = await self._setup(
modules, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]}
)
attrs = added[0].extra_state_attributes
assert attrs["intent_id"] == "netflix_title"
assert attrs["activity"] == NETFLIX_INTENT["activity"]
assert attrs["fields"] == NETFLIX_INTENT["fields"]
async def test_pressing_sends_the_declared_defaults(self, modules):
entry, added = await self._setup(
modules, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]}
)
poller = _RecordingPoller()
entry.runtime_data = _manager(poller)
button = added[0]
button.entity_id = "button.shield_netflix_open_a_title"
# with no default for the required field, the press is an honest error
import homeassistant.exceptions as ha_exceptions
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
await button.async_press()
assert caught.value.translation_key == "missing_intent_field"
assert poller.commands == [], "nothing was sent"
# give the field a default and the same press works
intent = {**NETFLIX_INTENT, "fields": [{"key": "id", "type": "string", "default": "7"}]}
_entry, added = await self._setup(modules, {CONF_REGISTERED_INTENTS: [intent]})
added[0]._manager = _manager(poller)
added[0].entity_id = "button.shield_netflix_open_a_title"
await added[0].async_press()
assert poller.commands == [
modules.media.intent_am_start_command(intent, {"id": "7"})
]
async def test_an_uninstalled_app_fails_the_press_honestly(self, modules):
"""The plan's acceptance: a package that is gone must not press silently.
The device is the authority here — its own sentence is what the error
carries, so the user learns WHICH component vanished instead of seeing a
button that did nothing.
"""
import homeassistant.exceptions as ha_exceptions
intent = {
**NETFLIX_INTENT,
"fields": [{"key": "id", "type": "string", "default": "7"}],
}
entry, added = await self._setup(
modules, {CONF_REGISTERED_INTENTS: [intent]}
)
poller = _RecordingPoller(
"Starting: Intent { act=android.intent.action.VIEW }\n"
"Error: Activity class {com.netflix.ninja/.MainActivity} does not exist."
)
entry.runtime_data = _manager(poller)
added[0]._manager = _manager(poller)
added[0].entity_id = "button.shield_netflix_open_a_title"
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
await added[0].async_press()
assert caught.value.translation_key == "intent_failed"
assert "does not exist" in str(caught.value)
assert "com.netflix.ninja/.MainActivity" in str(caught.value)
assert len(poller.commands) == 1, "attempted once, never retried"
async def test_a_button_with_no_manager_fails_loudly(self, modules):
"""A press that cannot reach the Shield RAISES — it never no-ops.
The field gets a default here on purpose: that leaves ADB as the only
thing wrong, so the assertion can pin WHICH error the user sees.
"""
_entry, added = await self._setup(
modules,
{
CONF_REGISTERED_INTENTS: [
{
**NETFLIX_INTENT,
"fields": [{"key": "id", "type": "string", "default": "7"}],
}
]
},
)
import homeassistant.exceptions as ha_exceptions
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
await added[0].async_press()
assert caught.value.translation_key == "intents_need_adb"
# ── 5. the action ──────────────────────────────────────────────────────────
class _Call:
"""The shape `ServiceCall` has for the handler (data + hass)."""
def __init__(self, data, hass=None):
self.data = data
self.hass = hass
class TestTheAction:
"""`nvidia_shield.send_intent`: the lookup, the variables, the response."""
def _wire(self, modules, options, poller=None, entity_id="media_player.shield"):
manager = _manager(poller or _RecordingPoller())
manager.entry = SimpleNamespace(options=options)
modules.media._INTENT_TARGETS[entity_id] = manager
return manager
def _cleanup(self, modules, entity_id="media_player.shield"):
modules.media._INTENT_TARGETS.pop(entity_id, None)
async def test_the_output_comes_back_under_the_response_key(self, modules):
poller = _RecordingPoller()
self._wire(modules, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]}, poller)
try:
result = await modules.media._async_send_intent_service(
_Call(
{
"intent": NETFLIX_INTENT["name"],
"variables": {"id": "1"},
"entity_id": "media_player.shield",
}
)
)
finally:
self._cleanup(modules)
assert set(result) == {INTENT_RESPONSE_KEY}
assert result[INTENT_RESPONSE_KEY].startswith("Starting: Intent")
assert poller.commands == [
modules.media.intent_am_start_command(NETFLIX_INTENT, {"id": "1"})
]
async def test_an_unknown_intent_lists_the_known_names(self, modules):
import homeassistant.exceptions as ha_exceptions
self._wire(modules, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]})
try:
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
await modules.media._async_send_intent_service(
_Call({"intent": "nope", "entity_id": "media_player.shield"})
)
finally:
self._cleanup(modules)
assert caught.value.translation_key == "unknown_intent"
assert caught.value.translation_placeholders["known"] == NETFLIX_INTENT["name"]
async def test_no_registered_intents_yet_is_said_plainly(self, modules):
import homeassistant.exceptions as ha_exceptions
self._wire(modules, {})
try:
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
await modules.media._async_send_intent_service(
_Call({"intent": "anything", "entity_id": "media_player.shield"})
)
finally:
self._cleanup(modules)
assert caught.value.translation_placeholders["known"] == "none yet"
async def test_variables_must_be_a_mapping(self, modules):
import homeassistant.exceptions as ha_exceptions
self._wire(modules, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]})
try:
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
await modules.media._async_send_intent_service(
_Call(
{
"intent": NETFLIX_INTENT["name"],
"variables": "id=1",
"entity_id": "media_player.shield",
}
)
)
finally:
self._cleanup(modules)
assert caught.value.translation_key == "invalid_intent_variables"
async def test_the_target_errors_are_the_shared_ones(self, modules):
import homeassistant.exceptions as ha_exceptions
self._wire(modules, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]})
try:
# nothing selected
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
await modules.media._async_send_intent_service(
_Call({"intent": NETFLIX_INTENT["name"]})
)
assert caught.value.translation_key == "no_target"
assert "send_intent" in caught.value.translation_placeholders["service"]
# an entity that does not participate in THIS action
with pytest.raises(ha_exceptions.ServiceValidationError) as caught:
await modules.media._async_send_intent_service(
_Call({"intent": "x", "entity_id": "media_player.other"})
)
assert caught.value.translation_key == "unsupported_target"
finally:
self._cleanup(modules)
async def test_the_key_a_template_reads_is_not_adb_response(self, modules):
from nsh.const import INTENT_RESPONSE_KEY
assert INTENT_RESPONSE_KEY == "intent_response"
class TestTheServiceDescription:
"""The dropdown the frontend renders comes from the runtime description."""
def test_the_intent_field_is_a_dropdown_of_registered_names(self, modules):
description = modules.media.send_intent_service_description(
["Netflix: open a title", "TiviMate: channel"]
)
intent_field = description["fields"]["intent"]
assert intent_field["required"] is True
selector = intent_field["selector"]["select"]
assert sorted(selector["options"]) == [
"Netflix: open a title",
"TiviMate: channel",
]
assert selector["custom_value"] is True
assert selector["mode"] == "dropdown"
def test_the_description_declares_the_target_and_the_variables_field(self, modules):
description = modules.media.send_intent_service_description([])
assert description["target"] == {"entity": [{"domain": "media_player"}]}
assert "variables" in description["fields"]
assert description["fields"]["variables"]["selector"] == {"object": {}}
assert description["fields"]["intent"]["selector"]["select"]["options"] == []
def test_the_strings_files_carry_the_same_service_and_exception_keys(self):
"""The runtime description and the translation cannot disagree."""
import json
base = Path(__file__).resolve().parent.parent / "custom_components"
shield = base / "nvidia_shield"
for name in ("strings.json", "translations/en.json"):
data = json.loads((shield / name).read_text(encoding="utf-8"))
assert set(data["services"]["send_intent"]["fields"]) == {
"intent",
"variables",
"entity_id",
}
for key in (
"unknown_intent",
"missing_intent_field",
"unknown_intent_variable",
"invalid_intent_field",
"invalid_intent_variables",
"unresolved_intent_placeholder",
"intents_need_adb",
"intent_no_answer",
"intent_failed",
):
assert key in data["exceptions"], (name, key)
class TestNothingRunsByItself:
"""The plan's non-negotiable: registration is inert until someone calls it."""
async def test_loading_the_options_screen_touches_no_device(self, modules, tmp_path):
poller = _RecordingPoller()
flow = _flow(modules, tmp_path, {CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]})
flow.config_entry.runtime_data = SimpleNamespace(
async_scan_intents=_FakeScan(_scan_result()),
adb_poller=poller,
)
for step in ("intents", "intents_edit", "intents_remove", "intents_scan"):
result = await getattr(flow, f"async_step_{step}")(None)
assert result["type"] in ("form", "menu")
assert poller.commands == [], "rendering a screen never runs anything"
async def test_a_scan_never_registers_anything(self, modules, tmp_path):
flow = _flow(modules, tmp_path)
flow.config_entry.runtime_data = SimpleNamespace(
async_scan_intents=_FakeScan(_scan_result([_candidate()]))
)
await flow.async_step_intents_scan(
{INTENT_FIELD_SCAN_PACKAGE: "com.netflix.ninja"}
)
assert flow._options_dict.get(CONF_REGISTERED_INTENTS) in (None, [])
async def test_a_registered_intent_is_inert_until_pressed(self, modules):
_entry, added = await self._setup_intents(modules)
assert added, "the button exists"
assert added[0]._manager is None or added[0]._manager.adb_poller is not None
async def _setup_intents(self, modules):
entry = SimpleNamespace(
entry_id="e1",
options={CONF_REGISTERED_INTENTS: [NETFLIX_INTENT]},
title="Shield",
runtime_data=None,
)
added: list = []
button = importlib.import_module("nsh.button")
await button.async_setup_entry(None, entry, added.extend)
return entry, added
def test_the_text_helpers_do_not_run_in_a_thread():
"""A guard against a future rewrite reaching for `eval`/`exec` (t17)."""
source = (
Path(__file__).resolve().parent.parent
/ "custom_components"
/ "nvidia_shield"
/ "media_player.py"
).read_text(encoding="utf-8")
assert "eval(" not in source
assert "exec(" not in source
assert ".format(" not in source.split("def intent_am_start_command")[1].split("\ndef ")[0]
def test_asyncio_is_still_needed_by_the_module_under_test():
assert asyncio is not None # keeps the import honest if the tests shrink
def test_a_truncated_capture_prefills_no_url(modules):
"""t18/F2 at the CONSUMPTION end: a cut detail must not become a form URL.
The parser drops an unterminated quoted value (see
tests/test_intents.py::TestATruncatedValueIsNotFabricated); this is the
consequence the review named — `_candidate_data` must not build
``https://www.exam`` out of a host it only half-read.
"""
from nsh.ha_adb_common import parse_activity_resolver_table
capture = (
"Activity Resolver Table:\n"
" Schemes:\n"
" https:\n"
" deca7fa com.netflix.ninja/.MainActivity filter fc1\n"
' Action: "android.intent.action.VIEW"\n'
' Scheme: "ht\n'
' Authority: "www.exam\n'
"Service Resolver Table:\n"
)
[candidate] = parse_activity_resolver_table(capture)
values, _note = modules.flow.intent_prefill_from_candidate("com.netflix.ninja", candidate)
assert values[INTENT_FIELD_DATA] == "", "a cut scheme/authority prefills NO data"
assert "www.exam" not in values[INTENT_FIELD_DATA]
assert values[INTENT_FIELD_ACTION] == "android.intent.action.VIEW"
assert values[INTENT_FIELD_ACTIVITY] == "com.netflix.ninja/.MainActivity"