diff --git a/homeassistant/components/sandbox/__init__.py b/homeassistant/components/sandbox/__init__.py index 54cb2d6eb344..9a9f68d40cd6 100644 --- a/homeassistant/components/sandbox/__init__.py +++ b/homeassistant/components/sandbox/__init__.py @@ -6,7 +6,7 @@ The integration owns three runtime objects, all hung off * :class:`SandboxManager` — supervises one subprocess per sandbox group ("main", "built-in", "custom"), lazily spawning them on first need. * :class:`SandboxFlowRouter` — installed as - ``hass.config_entries.router`` (Phase 4). Diverts new config flows to + ``hass.config_entries.router``. Diverts new config flows to sandbox runtimes and routes ``async_setup_entry`` for tagged entries. * :class:`SandboxBridge` (one per running sandbox) — owns the entity-side protocol: receives ``register_entity`` + ``state_changed`` pushes from @@ -57,7 +57,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: data.bridges[group] = async_create_bridge(hass, group=group, channel=channel) async def _on_shutdown_reply(group: str, reply: Any) -> None: - """Persist the sandbox's restore-state snapshot (Phase 9). + """Persist the sandbox's restore-state snapshot. The runtime ships its ``RestoreEntity`` state in the shutdown reply (a ``ShutdownResult``) rather than via the sandbox store @@ -100,7 +100,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _on_stop(_event: Event) -> None: """Stop every sandbox process on HA shutdown. - Phase 9: ask each sandbox to unload its entries and flush + Ask each sandbox to unload its entries and flush ``RestoreEntity`` state through the ``current_sandbox`` store bridge before pulling the plug. ``async_stop_all`` then handles SIGTERM / SIGKILL for any sandbox that didn't ack the graceful request diff --git a/homeassistant/components/sandbox/classifier.py b/homeassistant/components/sandbox/classifier.py index 35ca63b21317..64395effa79b 100644 --- a/homeassistant/components/sandbox/classifier.py +++ b/homeassistant/components/sandbox/classifier.py @@ -2,8 +2,8 @@ `classify(integration)` is a pure function from a loaded `Integration` (manifest + on-disk shape) to a `SandboxAssignment`. It is called by the -config-flow router (Phase 4) and by config-entry setup interception -(Phase 4) — every decision about "main vs sandbox" funnels through here. +config-flow router and by config-entry setup interception — every +decision about "main vs sandbox" funnels through here. Rule order (first match wins): diff --git a/homeassistant/components/sandbox/const.py b/homeassistant/components/sandbox/const.py index 1719b4511f12..2d135148a518 100644 --- a/homeassistant/components/sandbox/const.py +++ b/homeassistant/components/sandbox/const.py @@ -54,8 +54,8 @@ ALWAYS_MAIN: frozenset[str] = frozenset( # objects with Path values + temp files before the entity method # runs. Neither bridge option intercepts at service-call level yet, # and resolution depends on camera/image bytes (deny-listed). Folded - # in the Phase 1 decision doc — revisit when ai_task is made - # sandbox-aware or we add service-handler-level interception. + # into ALWAYS_MAIN — revisit when ai_task is made sandbox-aware or + # we add service-handler-level interception. "ai_task", # image owns the same bytes-returning entity surface camera does; # the deny-list above catches integrations *providing* an image diff --git a/homeassistant/components/sandbox/entity/__init__.py b/homeassistant/components/sandbox/entity/__init__.py index 9093ba976861..8292dd7a01db 100644 --- a/homeassistant/components/sandbox/entity/__init__.py +++ b/homeassistant/components/sandbox/entity/__init__.py @@ -7,9 +7,8 @@ service-handler kwarg filtering (``light.filter_turn_on_params``, ``climate`` schema validation, …) and frontend rendering see the same shape they would for a local entity. -Phase 5 ships proxies for the small "rich" set the spike and tests -exercise. The remaining domains from the v1 list use the same mechanical -pattern — see ``plan.md`` Phase 5's deferral note. +A small "rich" set of domains ships typed proxies; the remaining +domains use the same mechanical pattern. """ import contextlib diff --git a/homeassistant/components/sandbox/entity/calendar.py b/homeassistant/components/sandbox/entity/calendar.py index 536675134ea2..b6b3af8c66a5 100644 --- a/homeassistant/components/sandbox/entity/calendar.py +++ b/homeassistant/components/sandbox/entity/calendar.py @@ -13,7 +13,7 @@ class SandboxCalendarEntity(SandboxProxyEntity, CalendarEntity): Calendar service calls go through the standard ``calendar.*`` service handlers; the listing/iteration APIs are server-side queries we don't - proxy in Phase 13 (no test infra exercises them yet). + proxy (no test infra exercises them yet). """ @property diff --git a/homeassistant/components/sandbox/entity/scene.py b/homeassistant/components/sandbox/entity/scene.py index 56f83002e960..3877b7026966 100644 --- a/homeassistant/components/sandbox/entity/scene.py +++ b/homeassistant/components/sandbox/entity/scene.py @@ -1,8 +1,8 @@ """Sandbox proxy for ``scene`` entities. ``scene`` is in ``ALWAYS_MAIN`` so the classifier never routes it to a -sandbox in practice. The proxy ships anyway for symmetry — Phase 13 -covers the full set so a future classifier change doesn't surprise us. +sandbox in practice. The proxy ships anyway for symmetry — the full +set is covered so a future classifier change doesn't surprise us. """ from typing import Any diff --git a/homeassistant/components/sandbox/entity/todo.py b/homeassistant/components/sandbox/entity/todo.py index 575e08773a45..9d1392a74354 100644 --- a/homeassistant/components/sandbox/entity/todo.py +++ b/homeassistant/components/sandbox/entity/todo.py @@ -32,10 +32,10 @@ class SandboxTodoListEntity(SandboxProxyEntity, TodoListEntity): @property def todo_items(self) -> list[TodoItem] | None: """Item iteration happens on the sandbox side; do not proxy items.""" - # The Phase-13 proxy only mirrors state + service calls. Listing - # items is a server-side query that needs the same bridge plumbing - # ``calendar`` does and is deferred until those operations get a - # cross-process protocol (out of scope for this phase). + # The proxy only mirrors state + service calls. Listing items is a + # server-side query that needs the same bridge plumbing ``calendar`` + # does and is deferred until those operations get a cross-process + # protocol. return None async def async_create_todo_item(self, item: TodoItem) -> None: diff --git a/homeassistant/components/sandbox/entity/weather.py b/homeassistant/components/sandbox/entity/weather.py index d37d1bc93f14..11645544c2ef 100644 --- a/homeassistant/components/sandbox/entity/weather.py +++ b/homeassistant/components/sandbox/entity/weather.py @@ -25,7 +25,7 @@ class SandboxWeatherEntity(SandboxProxyEntity, WeatherEntity): Forecasts are computed by the sandbox-side ``WeatherEntity`` and pushed through the ``weather.get_forecasts`` service path, not over - the entity-method bridge — Phase 13 only proxies the condition + + the entity-method bridge — the proxy only mirrors the condition + instantaneous attributes. """ diff --git a/homeassistant/components/sandbox/manager.py b/homeassistant/components/sandbox/manager.py index c6a6bc611b63..3317c5c2d730 100644 --- a/homeassistant/components/sandbox/manager.py +++ b/homeassistant/components/sandbox/manager.py @@ -1,7 +1,7 @@ """Sandbox — subprocess lifecycle and supervision. -Phase 3 building block. The manager owns one supervised subprocess per -sandbox group (``main`` / ``built-in`` / ``custom``); higher phases call +The manager owns one supervised subprocess per sandbox group +(``main`` / ``built-in`` / ``custom``); callers invoke :meth:`SandboxManager.ensure_started` lazily as config entries are routed. The contract between manager and runtime is: @@ -118,7 +118,7 @@ class SandboxProcess: manager's loop. ``on_shutdown_reply`` is invoked with the runtime's reply to - :data:`MSG_SHUTDOWN` (Phase 9) so the caller can persist any + :data:`MSG_SHUTDOWN` so the caller can persist any ``restore_state`` payload before the subprocess exits. """ self.group = group @@ -225,7 +225,7 @@ class SandboxProcess: self._state = "stopped" async def async_graceful_shutdown(self, *, timeout: float) -> bool: - """Phase 9: ask the runtime to unload + flush, then wait for exit. + """Ask the runtime to unload + flush, then wait for exit. Sends ``sandbox/shutdown`` over the live channel and waits up to ``timeout`` for the runtime to reply and then exit on its @@ -551,7 +551,7 @@ class SandboxManager: back). Unix is opt-in so existing deployments keep using stdio. ``on_channel_ready`` is invoked once a sandbox's control channel is - live; Phase 4's router uses it to register inbound flow handlers + live; the router uses it to register inbound flow handlers (e.g., ``sandbox/notify_flow_changed``). """ self._hass = hass @@ -637,7 +637,7 @@ class SandboxManager: ) async def async_graceful_shutdown_all(self, *, timeout: float) -> None: - """Phase 9: ask every running sandbox to shut down gracefully. + """Ask every running sandbox to shut down gracefully. Best-effort fan-out. Sandboxes that did not ack inside ``timeout`` are left for :meth:`async_stop_all` to clean up with SIGTERM / diff --git a/homeassistant/components/sandbox/protocol.py b/homeassistant/components/sandbox/protocol.py index 222a43f28475..a136ac32dc77 100644 --- a/homeassistant/components/sandbox/protocol.py +++ b/homeassistant/components/sandbox/protocol.py @@ -29,7 +29,7 @@ Main → Sandbox calls: fetches the code before setup (see ``hass_client.sources``). * ``sandbox/entry_unload`` — ask the sandbox to unload an entry by id. * ``sandbox/call_service`` — generic service dispatch (shared with - Phase 6's main→sandbox service mirroring path). Payload mirrors a + the main→sandbox service mirroring path). Payload mirrors a ``ServiceCall``: ``(domain, service, target, service_data, context, return_response)``. Returns either ``None`` or a service-response dict. @@ -39,7 +39,7 @@ Sandbox → Main calls: entity, here's its description". Main builds the proxy and replies ``{"entity_id": }`` so the sandbox can route later ``call_service`` requests back to the right local entity. Optional - ``device_info`` field (Phase 19): a JSON-flattened ``DeviceInfo`` dict + ``device_info`` field: a JSON-flattened ``DeviceInfo`` dict — sets become lists of two-element lists (``identifiers`` / ``connections``), tuples become lists (``via_device``), and ``entry_type`` is the enum's string value. When present, main calls @@ -48,28 +48,28 @@ Sandbox → Main calls: * ``sandbox/unregister_entity`` — symmetric counterpart. * ``sandbox/state_changed`` — push (no response). Carries the marshalled state delta for one entity. -* ``sandbox/register_service`` (Phase 6) — sandbox tells main "I just +* ``sandbox/register_service`` — sandbox tells main "I just registered a service, please mirror it". Main installs a thin handler that forwards calls back over the shared ``sandbox/call_service`` channel. -* ``sandbox/unregister_service`` (Phase 6) — symmetric counterpart. -* ``sandbox/fire_event`` (Phase 6) — push (no response). The sandbox +* ``sandbox/unregister_service`` — symmetric counterpart. +* ``sandbox/fire_event`` — push (no response). The sandbox forwards each ``_*`` event so main listeners (notably ``automation``) can react as if the integration ran locally. -* ``sandbox/store_load`` (Phase 8) — sandbox-side ``Store.async_load`` +* ``sandbox/store_load`` — sandbox-side ``Store.async_load`` proxies to this RPC. Payload ``{"key": str}``; response is the wrapped ``{"version", "minor_version", "key", "data"}`` dict the sandbox last saved, or ``None`` if no data exists yet. The group is implicit from the channel — each :class:`SandboxBridge` only ever serves one group. -* ``sandbox/store_save`` (Phase 8) — sandbox-side ``Store`` flush. +* ``sandbox/store_save`` — sandbox-side ``Store`` flush. Payload ``{"key": str, "data": dict}``; main writes the wrapped dict to ``/.storage/sandbox//`` atomically. Response is ``{"ok": True}``. -* ``sandbox/store_remove`` (Phase 8) — sandbox-side +* ``sandbox/store_remove`` — sandbox-side ``Store.async_remove``. Payload ``{"key": str}``; main unlinks the file (if any). Response is ``{"ok": True}``. -Main → Sandbox shutdown (Phase 9): +Main → Sandbox shutdown: * ``sandbox/shutdown`` — ask the runtime to unload its entries, dump ``RestoreEntity`` state, fire ``EVENT_HOMEASSISTANT_FINAL_WRITE`` so any diff --git a/homeassistant/components/sandbox/proxy_flow.py b/homeassistant/components/sandbox/proxy_flow.py index 5bafd191ba52..6f875a643b87 100644 --- a/homeassistant/components/sandbox/proxy_flow.py +++ b/homeassistant/components/sandbox/proxy_flow.py @@ -234,12 +234,12 @@ class SandboxFlowProxy(ConfigFlow): ) # Any other type (MENU, EXTERNAL_STEP, SHOW_PROGRESS, …) is - # explicitly out of Phase 4 scope; surface a noisy abort so a - # follow-up doesn't silently drop the flow on the floor. + # not supported; surface a noisy abort so a follow-up doesn't + # silently drop the flow on the floor. self._terminated = True _LOGGER.warning( "Sandbox %r returned unsupported flow result type %s for %s;" - " aborting (Phase 4 supports FORM/CREATE_ENTRY/ABORT only)", + " aborting (only FORM/CREATE_ENTRY/ABORT are supported)", self._sandbox_group, result_type, self._handler_key, diff --git a/sandbox/categorize_failures.py b/sandbox/categorize_failures.py index af8876f9ead8..6d230ead8618 100644 --- a/sandbox/categorize_failures.py +++ b/sandbox/categorize_failures.py @@ -9,7 +9,7 @@ signature matchers, and writes: (``{bucket → {integration → [test_node, …]}}``) for downstream tooling. - A short stdout summary suitable for `tee`-ing onto an issue. -The bucket list mirrors the Phase 16 spec in ``plan.md``. Categories are +Categories are ordered most-specific → most-generic; the first match wins, so unknown genuinely means "no rule fired". A "≥95% of failures buckets out of ``unknown``" smoke gate lives at the bottom of the run; if that fails, @@ -65,7 +65,7 @@ def _compile(pattern: str) -> re.Pattern[str]: RULES: tuple[Rule, ...] = ( # ---- test-only ------------------------------------------------------- - # Phase 15's autotag patch mutates entry.data; tests that assert + # The autotag patch mutates entry.data; tests that assert # ``entry.data == `` or snapshot it see the new # ``__sandbox_group`` key. Same root cause whether it surfaces in an # `assert`, a `mappingproxy(...)` repr (pytest truncates the diff into @@ -88,7 +88,7 @@ RULES: tuple[Rule, ...] = ( "test-only", _compile(r"assert\s+mappingproxy\(.+?\)\s*==\s*[\{\[]"), ), - # Post-Phase-17: diagnostic snapshots that include the entry's + # Diagnostic snapshots that include the entry's # full ``as_dict()`` now see a top-level ``+ 'sandbox': ''`` # line in the diff. Same root cause as ``__sandbox_group`` — the # autotag synthesises the field for compat coverage, the snapshot @@ -130,7 +130,7 @@ RULES: tuple[Rule, ...] = ( ), # ---- protocol gaps --------------------------------------------------- - # data_schema serialisation drift. Phase 14 added voluptuous-serialize + # data_schema serialisation drift. The bridge does voluptuous-serialize # round-tripping; a regression would surface as None / wrong type. Rule( "data-schema-stripped", @@ -310,7 +310,7 @@ def render_summary( for node_list in integrations.values() ) lines = [ - "Phase 16 failure categorisation", + "Failure categorisation", "-" * 32, f"Total failures bucketed: {total_failures}", "", diff --git a/sandbox/generate_backlog.py b/sandbox/generate_backlog.py index dfaf10364346..06556d6ad7f1 100644 --- a/sandbox/generate_backlog.py +++ b/sandbox/generate_backlog.py @@ -2,7 +2,7 @@ Pairs with ``categorize_failures.py``. Reads ``BACKLOG_FAILURES.json`` (``{bucket → {integration → [{node_id, excerpt}]}}``) and writes the -section-per-bucket Markdown the Phase 16 spec asks for. +section-per-bucket Markdown backlog. The "Proposed fix" stub is intentionally left as a TODO marker per section — that field needs human judgement (file paths, rough size, @@ -38,55 +38,56 @@ BUCKET_BLURB: dict[str, str] = { " a snapshot that pre-dates the tag. Not a sandbox bridge bug." ), "proxy-missing": ( - "Phase 13 shipped proxies for all 32 entity domains. Any hit here" + "The bridge ships proxies for all 32 entity domains. Any hit here" " means the integration registers an entity in a domain the bridge" " doesn't recognise — either a new domain landed in HA Core or the" " dispatch map missed one." ), "data-schema-stripped": ( - "Phase 14 added a `voluptuous_serialize`-based round-trip so flow" + "The bridge does a `voluptuous_serialize`-based round-trip so flow" " schemas survive the bridge. Any hit here means a `vol.Schema`" " shape the serializer can't represent (custom validators, untagged" " `vol.Any`, etc.)." ), "service-schema-missing": ( "Mirror of `data-schema-stripped` for `hass.services.async_register`." - " Phase 14 ships the same bridge for service schemas; gaps here are" + " The same bridge serialises service schemas; gaps here are" " edge cases (custom validators, schema-on-the-fly registrations)." ), "unique-id-not-propagated": ( - "Phase 14 marshals `flow.context['unique_id']` into the proxy. Hits" + "The bridge marshals `flow.context['unique_id']` into the proxy. Hits" " here are flow shapes that set unique_id outside the standard" " `async_set_unique_id` path (e.g. discovery flows that abort" " inside `async_step_user`)." ), "restore-state-not-applied": ( - "Phase 9 warm-loads `RestoreStateData` from" + "The runtime warm-loads `RestoreStateData` from" " `/.storage/sandbox//core.restore_state`. Hits" " here mean an integration's restore-state assertion fires before" " the warm-load completes, or expects state the previous run never" " persisted." ), "context-not-propagated": ( - "Phase 6 forwards events with the sandbox-side `context_id` but does" - " NOT honour it for main's user/origin resolution. Integration tests" + "The event mirror forwards events with the sandbox-side `context_id`" + " but does NOT honour it for main's user/origin resolution. Integration" + " tests" " that assert on `context.user_id` or `context.parent_id` fail here." " Carrying a richer Context shape is post-v2 work." ), "re-entrant-channel": ( - "Phase 12 made the channel dispatcher concurrent so a handler can" + "The channel dispatcher is concurrent so a handler can" " issue `channel.call(...)`. Hits here mean a re-entrant shape we" " haven't seen — the semaphore cap might be too aggressive, or a" " handler chain exceeds the in-flight ceiling." ), "store-key-rejected": ( - "Phase 8's `RemoteStore` validator rejects keys containing `/`, `\\`," + "The `RemoteStore` validator rejects keys containing `/`, `\\`," " NUL, `.`, or `..`. An integration using one of those characters" " trips this. Probably wants a translation step on the sandbox side" " rather than relaxing the validator." ), "flow-step-not-async": ( - "Phase 4's flow proxy expects every step on the integration's" + "The flow proxy expects every step on the integration's" " `ConfigFlow` to be an async coroutine. A sync step would emit a" " `coroutine 'async_step_*' was never awaited` warning that this" " bucket catches." @@ -97,7 +98,7 @@ BUCKET_BLURB: dict[str, str] = { " has a bug or the platform-set should grow." ), "non-idempotent-service-handler": ( - "The `ai_task`/`image` shape Phase 1 surfaced — the service handler" + "The `ai_task`/`image` shape — the service handler" " does material work before calling the entity method. Today's" " resolution is `ALWAYS_MAIN`; integrations that hit this should" " join that set or sandbox-handler interception needs designing" @@ -144,12 +145,12 @@ def _bucket_priority(name: str) -> int: def render_backlog(payload: dict[str, dict[str, list[dict[str, str]]]]) -> str: """Render the BACKLOG.md draft.""" lines: list[str] = [ - "# Sandbox — Phase 16 backlog", + "# Sandbox — compat sweep backlog", "", "**Auto-generated draft** from `BACKLOG_FAILURES.json`." " `generate_backlog.py` writes the skeleton; the *Proposed fix* and" " *Estimated size* lines need human curation before this lands as the" - " final Phase 16 deliverable.", + " final deliverable.", "", "Sections are ordered by integration count (largest first). Within" " each section the affected-integration roll-up is capped at 10 — the" diff --git a/sandbox/hass_client/hass_client/approved_domains.py b/sandbox/hass_client/hass_client/approved_domains.py index 8f6ea81e9d2b..6d480dc42131 100644 --- a/sandbox/hass_client/hass_client/approved_domains.py +++ b/sandbox/hass_client/hass_client/approved_domains.py @@ -1,4 +1,4 @@ -"""Sandbox-side approved-domains gate (Phase 6). +"""Sandbox-side approved-domains gate. A single shared :class:`ApprovedDomains` instance tracks which domains the sandbox is allowed to own. It is the firewall the user asked for: diff --git a/sandbox/hass_client/hass_client/event_mirror.py b/sandbox/hass_client/hass_client/event_mirror.py index 8663ce983fdd..b108a349eaf1 100644 --- a/sandbox/hass_client/hass_client/event_mirror.py +++ b/sandbox/hass_client/hass_client/event_mirror.py @@ -1,4 +1,4 @@ -"""Sandbox-side event mirror (Phase 6). +"""Sandbox-side event mirror. Forwards every event whose ``event_type`` matches ``_*`` up to main via ``sandbox/fire_event``. Canonical examples: ``zha_event``, diff --git a/sandbox/hass_client/hass_client/flow_runner.py b/sandbox/hass_client/hass_client/flow_runner.py index ba5eafc4417d..78f687460b59 100644 --- a/sandbox/hass_client/hass_client/flow_runner.py +++ b/sandbox/hass_client/hass_client/flow_runner.py @@ -12,9 +12,8 @@ manager-side proxy :class:`ConfigFlow` calls these handlers across the Flow results cross the wire as plain dicts. ``data_schema`` and the ``progress_task`` field are intentionally stripped — the schema lives on the sandbox where validation happens, and the task is a runtime object -that can't be serialised. Phase 5 lifts the bridge to a richer -representation; the docstring in ``_marshal_result`` is the load-bearing -note for that follow-up. +that can't be serialised. The docstring in ``_marshal_result`` is the +load-bearing note for how the schema is later marshalled. """ from collections.abc import Mapping @@ -69,7 +68,7 @@ class _SandboxFlowManager(ConfigEntriesFlowManager): Main owns the canonical entry store; the sandbox just runs the flow and returns the result. The default ``async_finish_flow`` would create an entry inside the sandbox-private store and try to set the - integration up locally — that's Phase 5 / 6 work, not Phase 4's. + integration up locally — that's later work, not this layer's. """ async def async_finish_flow( @@ -149,7 +148,7 @@ def _marshal_result( ) -> pb.FlowResult: """Marshal a FlowResult into the typed ``FlowResult`` message. - ``data_schema`` is rendered via :func:`serialize_schema` (Phase 14) — + ``data_schema`` is rendered via :func:`serialize_schema` — the wire payload carries the same list-of-fields shape :func:`voluptuous_serialize.convert` produces, so the proxy on main can rebuild a usable :class:`vol.Schema`. ``flow.context`` (which diff --git a/sandbox/hass_client/hass_client/sandbox/__init__.py b/sandbox/hass_client/hass_client/sandbox/__init__.py index 219bdc539289..d318bd52e07f 100644 --- a/sandbox/hass_client/hass_client/sandbox/__init__.py +++ b/sandbox/hass_client/hass_client/sandbox/__init__.py @@ -3,21 +3,21 @@ Composes the sandbox's per-process services: * :class:`FlowRunner` — drives integration ``ConfigFlow`` instances - out-of-process (Phase 4). + out-of-process. * :class:`EntryRunner` — accepts ``sandbox/entry_setup`` pushes and - runs ``async_setup_entry`` against the sandbox-private HA (Phase 5). + runs ``async_setup_entry`` against the sandbox-private HA. * :class:`EntityBridge` — pushes entity registrations + state changes - back to main (Phase 5). + back to main. * :class:`ServiceMirror` / :class:`EventMirror` — mirror service registrations and ``_*`` events up to main, gated by - :class:`ApprovedDomains` (Phase 6). + :class:`ApprovedDomains`. The handshake: open the control channel (transport selected by the ``--url`` scheme — ``stdio://`` by default, ``unix://`` to dial back to the manager's unix socket), send a :data:`MSG_READY` frame as the first message, warm-load restore state, register handlers, then idle until SIGTERM (or until main asks for a graceful shutdown over the channel — see -Phase 9's :meth:`SandboxRuntime._handle_shutdown`). +:meth:`SandboxRuntime._handle_shutdown`). """ import asyncio @@ -173,7 +173,7 @@ class SandboxRuntime: "one event loop? (see plan Risk #3)" ) sandbox_token = current_sandbox.set(ChannelSandboxBridge(self._channel)) - # Phase 9: start the channel reader first so the warm-load + # Start the channel reader first so the warm-load # round-trip can resolve, then pre-load this sandbox group's # restore-state cache. The contextvar (set above) routes the # load to main. The data lives on main under @@ -241,7 +241,7 @@ class SandboxRuntime: return await _open_stdio_channel(name=self.group) async def _handle_shutdown(self, _payload: object) -> pb.ShutdownResult: - """Phase 9: unload entries, flush restore state, then exit cleanly. + """Unload entries, flush restore state, then exit cleanly. Runs inside the channel dispatcher so the reply is written before the runtime starts its teardown. The actual shutdown event is set @@ -257,7 +257,7 @@ class SandboxRuntime: async def _run_graceful_shutdown(self) -> pb.ShutdownResult: """Unload every loaded entry and snapshot RestoreEntity state. - Phase 12 fires ``EVENT_HOMEASSISTANT_FINAL_WRITE`` and waits for + Fires ``EVENT_HOMEASSISTANT_FINAL_WRITE`` and waits for the bus to drain so ``Store``s with pending ``async_delay_save`` writes flush to main via the ``current_sandbox`` bridge — the now-concurrent channel dispatcher means the re-entrant @@ -269,7 +269,7 @@ class SandboxRuntime: is owned by the runtime's explicit warm-load / shutdown-dump path, not by an integration's ``Store``, so it doesn't ride the FINAL_WRITE flush. Shipping it back in the reply keeps the data - path symmetric with Phase 9 — main writes it via + path symmetric with the warm-load — main writes it via :meth:`SandboxBridge._handle_store_save`-style atomic write. """ flow_runner = self._flow_runner @@ -291,7 +291,7 @@ class SandboxRuntime: if ok: unloaded += 1 - # Phase 12: fire FINAL_WRITE so ``async_delay_save``-using + # Fire FINAL_WRITE so ``async_delay_save``-using # ``Store``s flush their pending data. Concurrent channel # dispatcher means each bridge write can re-enter the channel # without deadlocking against this handler. @@ -333,7 +333,7 @@ async def _load_restore_state(hass: Any) -> None: periodic ``async_setup_dump`` listener via ``start.async_at_start``, which only fires on a fully-started HA. The sandbox's HA never goes through ``async_start``, so we skip that listener and rely on - Phase 9's shutdown handler to force the final dump. + the shutdown handler to force the final dump. No store swap is needed: ``RestoreStateData`` builds a vanilla ``Store``, and ``Store.async_load`` reads ``current_sandbox`` at call diff --git a/sandbox/hass_client/hass_client/sandbox/__main__.py b/sandbox/hass_client/hass_client/sandbox/__main__.py index 1e8c09449d0d..f4c0de1f0855 100644 --- a/sandbox/hass_client/hass_client/sandbox/__main__.py +++ b/sandbox/hass_client/hass_client/sandbox/__main__.py @@ -1,8 +1,8 @@ """Entry point for ``python -m hass_client.sandbox``. The Sandbox manager spawns this module as a subprocess. CLI arguments -mirror what the websocket client will need in Phase 4 so the manager-side -command line is stable across phases. +mirror what the websocket client needs so the manager-side command line +is stable. """ import argparse diff --git a/sandbox/hass_client/hass_client/sandbox_bridge.py b/sandbox/hass_client/hass_client/sandbox_bridge.py index 3d2c70b734d2..d3ceef41c732 100644 --- a/sandbox/hass_client/hass_client/sandbox_bridge.py +++ b/sandbox/hass_client/hass_client/sandbox_bridge.py @@ -6,7 +6,7 @@ control channel: the three ``Store`` IO methods delegate to main via the namespaces every key as ``/.storage/sandbox//`` so two sandbox processes — or main itself — can't read each other's data. -The bodies are lifted from the pre-contextvar Phase 8 store subclass that +The bodies are lifted from the pre-contextvar store subclass that this primitive replaced: same load semantics, same orjson preserialise on save, same channel error handling. The difference is *how* it's wired — ``Store`` reads ``current_sandbox`` at call time instead of being rebound diff --git a/sandbox/hass_client/hass_client/service_mirror.py b/sandbox/hass_client/hass_client/service_mirror.py index 912574fd3a20..52fe169cb981 100644 --- a/sandbox/hass_client/hass_client/service_mirror.py +++ b/sandbox/hass_client/hass_client/service_mirror.py @@ -1,4 +1,4 @@ -"""Sandbox-side service-registration mirror (Phase 6). +"""Sandbox-side service-registration mirror. Watches ``EVENT_SERVICE_REGISTERED`` / ``EVENT_SERVICE_REMOVED`` on the sandbox bus. For each registration whose domain is in @@ -6,7 +6,7 @@ sandbox bus. For each registration whose domain is in main with the metadata main needs to install a forwarding handler. Same shape for removals via ``sandbox/unregister_service``. -Schemas are intentionally not serialised in Phase 6 — the sandbox is the +Schemas are intentionally not serialised — the sandbox is the authoritative validator (the call comes back over ``sandbox/call_service`` and is run through ``services.async_call`` on the sandbox side, where the real schema lives). Main only needs diff --git a/sandbox/hass_client/hass_client/spike/__init__.py b/sandbox/hass_client/hass_client/spike/__init__.py index 82b9f4e73446..7b2b430e03e3 100644 --- a/sandbox/hass_client/hass_client/spike/__init__.py +++ b/sandbox/hass_client/hass_client/spike/__init__.py @@ -1,4 +1,4 @@ -"""Phase 1 entity-bridge spike. +"""Entity-bridge spike. Two protocols are implemented in parallel: diff --git a/sandbox/hass_client/hass_client/testing/_autotag.py b/sandbox/hass_client/hass_client/testing/_autotag.py index 6b21136f5607..82701640acad 100644 --- a/sandbox/hass_client/hass_client/testing/_autotag.py +++ b/sandbox/hass_client/hass_client/testing/_autotag.py @@ -11,8 +11,7 @@ classifies the entry's domain and, if it routes to a sandbox group, sets :attr:`ConfigEntry.sandbox` to the matching group name before the original ``add_to_hass`` adds it to the manager. Setting the field rather than mutating ``entry.data`` keeps the autotag invisible -to integration tests that assert on data contents (the Phase 17 fix -the BACKLOG had as the single highest-leverage gap). +to integration tests that assert on data contents. The classifier here is a synchronous filesystem-only re-implementation of :func:`homeassistant.components.sandbox.classifier.classify`. diff --git a/sandbox/hass_client/tests/test_approved_domains.py b/sandbox/hass_client/tests/test_approved_domains.py index a7d688cb8c31..b2ac8833eb1c 100644 --- a/sandbox/hass_client/tests/test_approved_domains.py +++ b/sandbox/hass_client/tests/test_approved_domains.py @@ -1,4 +1,4 @@ -"""Tests for the Phase 6 :class:`ApprovedDomains` gate.""" +"""Tests for the :class:`ApprovedDomains` gate.""" from hass_client.approved_domains import ApprovedDomains diff --git a/sandbox/hass_client/tests/test_entity_bridge.py b/sandbox/hass_client/tests/test_entity_bridge.py index 552fc458c1fe..be5a4a24ac09 100644 --- a/sandbox/hass_client/tests/test_entity_bridge.py +++ b/sandbox/hass_client/tests/test_entity_bridge.py @@ -1,4 +1,4 @@ -"""Phase 5 tests for :class:`hass_client.entity_bridge.EntityBridge`. +"""Tests for :class:`hass_client.entity_bridge.EntityBridge`. The bridge listens for ``EVENT_STATE_CHANGED`` on the sandbox-private :class:`HomeAssistant`. We drive it by registering a fake entity into a diff --git a/sandbox/hass_client/tests/test_entry_runner.py b/sandbox/hass_client/tests/test_entry_runner.py index 50a0216ba62a..6eaf25a7aa66 100644 --- a/sandbox/hass_client/tests/test_entry_runner.py +++ b/sandbox/hass_client/tests/test_entry_runner.py @@ -1,4 +1,4 @@ -"""Phase 5 tests for :class:`hass_client.entry_runner.EntryRunner`. +"""Tests for :class:`hass_client.entry_runner.EntryRunner`. Exercises the sandbox-side ``sandbox/entry_setup`` round-trip plus the ``sandbox/call_service`` channel. @@ -86,35 +86,35 @@ async def test_entry_setup_calls_integration_setup_entry( async def _async_unload_entry(_hass: Any, _entry: ConfigEntry) -> bool: return True - class _DemoFlow(ConfigFlow, domain="phase5_demo"): + class _DemoFlow(ConfigFlow, domain="demo_setup"): VERSION = 1 # `ConfigFlow.__init_subclass__` adds _DemoFlow to HANDLERS — clean it # back up at teardown so other tests don't see a stale handler. - assert "phase5_demo" in ha_config_entries.HANDLERS + assert "demo_setup" in ha_config_entries.HANDLERS # Stand up a fake integration in the loader caches. Both the main # module and the config_flow module must be present in DATA_COMPONENTS # — entry.async_setup imports the latter before calling # async_setup_entry. - module = ModuleType("homeassistant.components.phase5_demo") - module.DOMAIN = "phase5_demo" + module = ModuleType("homeassistant.components.demo_setup") + module.DOMAIN = "demo_setup" module.async_setup_entry = _async_setup_entry # type: ignore[attr-defined] module.async_unload_entry = _async_unload_entry # type: ignore[attr-defined] - config_flow_module = ModuleType("homeassistant.components.phase5_demo.config_flow") - runner.hass.data[ha_loader.DATA_COMPONENTS]["phase5_demo"] = module - runner.hass.data[ha_loader.DATA_COMPONENTS]["phase5_demo.config_flow"] = ( + config_flow_module = ModuleType("homeassistant.components.demo_setup.config_flow") + runner.hass.data[ha_loader.DATA_COMPONENTS]["demo_setup"] = module + runner.hass.data[ha_loader.DATA_COMPONENTS]["demo_setup.config_flow"] = ( config_flow_module ) - runner.hass.config.components.add("phase5_demo") + runner.hass.config.components.add("demo_setup") integration = ha_loader.Integration( runner.hass, - "homeassistant.components.phase5_demo", + "homeassistant.components.demo_setup", None, { - "domain": "phase5_demo", - "name": "Phase 5 Demo", + "domain": "demo_setup", + "name": "Demo Setup", "config_flow": True, "documentation": "https://example.com", "iot_class": "local_polling", @@ -127,11 +127,11 @@ async def test_entry_setup_calls_integration_setup_entry( runner.hass.data[ha_loader.DATA_INTEGRATIONS] = runner.hass.data.get( ha_loader.DATA_INTEGRATIONS, {} ) - runner.hass.data[ha_loader.DATA_INTEGRATIONS]["phase5_demo"] = integration + runner.hass.data[ha_loader.DATA_INTEGRATIONS]["demo_setup"] = integration payload = pb.EntrySetup( entry_id="test_entry_id_5", - domain="phase5_demo", + domain="demo_setup", title="Demo", source="user", version=1, @@ -158,7 +158,7 @@ async def test_entry_setup_reports_failure_reason( payload = pb.EntrySetup( entry_id="missing_entry_id", - domain="phase5_missing", + domain="demo_missing", title="Missing", source="user", version=1, diff --git a/sandbox/hass_client/tests/test_event_mirror.py b/sandbox/hass_client/tests/test_event_mirror.py index 8f7a20c468ed..0e4eb2cf4f62 100644 --- a/sandbox/hass_client/tests/test_event_mirror.py +++ b/sandbox/hass_client/tests/test_event_mirror.py @@ -1,4 +1,4 @@ -"""Phase 6 tests for :class:`hass_client.event_mirror.EventMirror`.""" +"""Tests for :class:`hass_client.event_mirror.EventMirror`.""" import asyncio import tempfile diff --git a/sandbox/hass_client/tests/test_flow_runner.py b/sandbox/hass_client/tests/test_flow_runner.py index cedf954d79b9..5109733a4558 100644 --- a/sandbox/hass_client/tests/test_flow_runner.py +++ b/sandbox/hass_client/tests/test_flow_runner.py @@ -1,4 +1,4 @@ -"""Phase 4 tests for :class:`hass_client.flow_runner.FlowRunner`. +"""Tests for :class:`hass_client.flow_runner.FlowRunner`. Exercises the sandbox-side flow loop against a mock integration whose ``async_setup_entry`` is intercepted before the FlowRunner is asked to @@ -131,7 +131,7 @@ async def test_flow_init_returns_form( assert result.type == "form" assert result.step_id == "user" - # Phase 14: data_schema rides as the same list-of-fields shape + # data_schema rides as the same list-of-fields shape # voluptuous_serialize.convert produces, so the proxy on main can # rebuild a usable vol.Schema (or hand the list straight to the # frontend). @@ -185,7 +185,7 @@ async def test_flow_step_validation_error_returns_form( async def test_flow_init_marshals_unique_id( channels: tuple[Channel, Channel], runner: FlowRunner ) -> None: - """flow_init pulls ``unique_id`` out of the live flow's context (Phase 14).""" + """flow_init pulls ``unique_id`` out of the live flow's context.""" main, sandbox = channels runner.register(sandbox) main.start() diff --git a/sandbox/hass_client/tests/test_sandbox_bridge.py b/sandbox/hass_client/tests/test_sandbox_bridge.py index 11fd5601ff7d..d6ea52e28222 100644 --- a/sandbox/hass_client/tests/test_sandbox_bridge.py +++ b/sandbox/hass_client/tests/test_sandbox_bridge.py @@ -244,7 +244,7 @@ async def test_delayed_save_flushes_through_bridge( they funnel through ``_async_handle_write_data`` -> ``_async_write_data``. The contextvar branch must live at ``_async_write_data`` (not only ``async_save``) or these writes would silently land on the sandbox's - local disk instead of reaching main. The Phase 8 store subclass + local disk instead of reaching main. The earlier store subclass overrode ``_async_write_data`` and masked this; deleting it surfaced the gap. """ diff --git a/sandbox/hass_client/tests/test_sandbox_runtime.py b/sandbox/hass_client/tests/test_sandbox_runtime.py index d1b12703153a..20e061d6e333 100644 --- a/sandbox/hass_client/tests/test_sandbox_runtime.py +++ b/sandbox/hass_client/tests/test_sandbox_runtime.py @@ -1,4 +1,4 @@ -"""Phase 3 client-side tests for ``hass_client.sandbox``. +"""Client-side tests for ``hass_client.sandbox``. The HA Core test suite owns the integration-level coverage (subprocess spawn, restart budget, multi-group). These tests pin the runtime's @@ -50,9 +50,9 @@ async def test_runtime_starts_in_locked_down_sharing_posture( ) -> None: """The sandbox HA sees only its own entities — no subscription to main. - Phase 20 dropped the unwired ``share_*`` config surface; the - locked-down posture is now a property of the runtime itself rather - than a config flag. See ``sandbox/docs/design-share-states.md`` + There is no ``share_*`` config surface; the locked-down posture is + a property of the runtime itself rather than a config flag. See + ``sandbox/docs/design-share-states.md`` for the future opt-in design. """ runtime = SandboxRuntime( @@ -87,7 +87,7 @@ async def test_runtime_shuts_down_on_request( url="ws://x", group="built-in", # Pytest captures stdin/stdout; skip channel setup for this - # in-process shutdown test (Phase 4 covers the real stdio path + # in-process shutdown test (the real stdio path is covered # via the manager-driven subprocess tests). channel_factory=_noop_channel_factory, ) diff --git a/sandbox/hass_client/tests/test_service_mirror.py b/sandbox/hass_client/tests/test_service_mirror.py index 552a86f2724a..1b56050ba8da 100644 --- a/sandbox/hass_client/tests/test_service_mirror.py +++ b/sandbox/hass_client/tests/test_service_mirror.py @@ -1,4 +1,4 @@ -"""Phase 6 tests for :class:`hass_client.service_mirror.ServiceMirror`. +"""Tests for :class:`hass_client.service_mirror.ServiceMirror`. Drives the mirror against a real sandbox-private :class:`HomeAssistant` (via :class:`hass_client.flow_runner.FlowRunner`) and an in-memory diff --git a/sandbox/hass_client/tests/test_shutdown.py b/sandbox/hass_client/tests/test_shutdown.py index 4b1af09876ad..8576cbffe981 100644 --- a/sandbox/hass_client/tests/test_shutdown.py +++ b/sandbox/hass_client/tests/test_shutdown.py @@ -1,4 +1,4 @@ -"""Phase 9 tests for :class:`hass_client.sandbox.SandboxRuntime` shutdown. +"""Tests for :class:`hass_client.sandbox.SandboxRuntime` shutdown. The runtime registers ``sandbox/shutdown`` once its channel is up. These tests exercise: @@ -165,7 +165,7 @@ async def test_shutdown_fires_final_write_event( runtime_pair: tuple[SandboxRuntime, Channel, asyncio.Task[int]], capsys: pytest.CaptureFixture[str], ) -> None: - """Phase 12: the shutdown handler fires EVENT_HOMEASSISTANT_FINAL_WRITE. + """The shutdown handler fires EVENT_HOMEASSISTANT_FINAL_WRITE. Concurrent channel dispatcher means the FINAL_WRITE fire-and-drain inside the shutdown handler no longer deadlocks against re-entrant @@ -191,13 +191,13 @@ async def test_shutdown_flushes_pending_delay_save( runtime_pair: tuple[SandboxRuntime, Channel, asyncio.Task[int]], capsys: pytest.CaptureFixture[str], ) -> None: - """Phase 12: ``async_delay_save`` writes flush through the store bridge. + """``async_delay_save`` writes flush through the store bridge. Without the concurrent channel dispatcher this would deadlock: the Store's FINAL_WRITE listener would re-enter the same channel reader - that is dispatching the shutdown handler. With Phase 12 the inner - ``store_save`` lands on the main-side handler while the shutdown - handler is still running. + that is dispatching the shutdown handler. The concurrent dispatcher + lets the inner ``store_save`` land on the main-side handler while the + shutdown handler is still running. """ runtime, main_channel, _task = runtime_pair hass = runtime._flow_runner.hass # noqa: SLF001 diff --git a/sandbox/hass_client/tests/test_transport_scheme.py b/sandbox/hass_client/tests/test_transport_scheme.py index bfb20a6c3ef4..9ee0eedf3e71 100644 --- a/sandbox/hass_client/tests/test_transport_scheme.py +++ b/sandbox/hass_client/tests/test_transport_scheme.py @@ -1,4 +1,4 @@ -"""Runtime-side transport selection + unix channel (transport T3). +"""Runtime-side transport selection + unix channel. The HA Core suite owns the manager-driven subprocess coverage. These tests pin the runtime side: ``--url`` scheme → transport kind, the diff --git a/sandbox/run_compat.py b/sandbox/run_compat.py index 50f9f05b1907..16bbe3f942bf 100644 --- a/sandbox/run_compat.py +++ b/sandbox/run_compat.py @@ -39,7 +39,7 @@ CORE_ROOT = _HERE.parent CORE_TESTS_DIR = CORE_ROOT / "tests" / "components" HASS_CLIENT_DIR = _HERE / "hass_client" DEFAULT_RESULTS_CSV = _HERE / "COMPAT.csv" -# COMPAT.md is the curated Phase 15 baseline report and is NOT overwritten +# COMPAT.md is the curated baseline report and is NOT overwritten # on every run. Auto-generated runs land in COMPAT_LATEST.md so reviewers # can diff against the curated baseline. DEFAULT_REPORT_MD = _HERE / "COMPAT_LATEST.md" diff --git a/sandbox/run_compat_full.py b/sandbox/run_compat_full.py index 761b0b186fa6..01d648130634 100644 --- a/sandbox/run_compat_full.py +++ b/sandbox/run_compat_full.py @@ -1,6 +1,6 @@ """Run the full sandbox compat sweep across every routable integration. -Phase 16 broadens the Phase 15 baseline (v1's 37-integration list) to cover +This sweep broadens the 37-integration baseline to cover **every config-entry-based integration** that the sandbox classifier would route to a sandbox. The goal of this run is not to fix anything — it is to *measure* and *categorize* so the resulting backlog is grounded in real @@ -13,7 +13,7 @@ Discovery rules (see ``discover_integrations``): sandbox). - Skip the ``ALWAYS_MAIN`` set (decided by the classifier already). - Skip integrations whose ``manifest.json`` has ``config_flow: false`` - (Phase 1 scoped YAML-only integrations out). + (YAML-only integrations are out of scope). - Skip integrations whose ``tests/components//`` directory has no ``test_*.py`` files (no signal either way). - Skip integrations whose source ships a platform file in @@ -22,7 +22,7 @@ Discovery rules (see ``discover_integrations``): For every surviving integration the runner spawns a pytest subprocess with ``-p hass_client.testing.pytest_plugin`` (the in-process plugin used by -the Phase 15 baseline), captures the JUnit XML output, and dumps the full +the baseline), captures the JUnit XML output, and dumps the full text output + per-test failure tracebacks into the per-integration error directory so the categorizer (``categorize_failures.py``) can bucket every failure. @@ -42,7 +42,7 @@ Usage:: # Restrict to a list (handy for iterating on the categorizer) uv run python run_compat_full.py input_boolean light switch - # Validation: re-run Phase 15's 37-integration baseline + # Validation: re-run the 37-integration baseline uv run python run_compat_full.py --baseline-37 The two committed deliverables are written at the end: @@ -80,7 +80,7 @@ ERRORS_DIR = Path(os.environ.get("SANDBOX_ERRORS_DIR", "/tmp/sandbox_errors")) # Mirrors homeassistant/components/sandbox/const.py. Duplicated here # because importing the live module would require booting the core test -# env from this stand-alone driver. The Phase 2 unit tests guard against +# env from this stand-alone driver. The unit tests guard against # behavioural drift; the per-integration sweep is allowed to lag by an # entry — if either set changes, update both copies and re-run. ALWAYS_MAIN: frozenset[str] = frozenset( @@ -90,7 +90,7 @@ SANDBOX_INCOMPATIBLE_PLATFORMS: frozenset[str] = frozenset( {"stt", "tts", "conversation", "assist_satellite", "wake_word", "camera"} ) -# Phase 15's 37-integration list, for the ``--baseline-37`` shortcut. The +# The 37-integration baseline list, for the ``--baseline-37`` shortcut. The # list lives both here and in ``COMPAT.md``; keep them in sync. BASELINE_37: tuple[str, ...] = ( "input_boolean", "input_button", "input_datetime", "input_number", @@ -209,7 +209,7 @@ def _parse_junit(xml_path: Path, *, errors_dir: Path, integration: str) -> Resul result.skipped = int(suite.attrib.get("skipped", 0)) result.duration = float(suite.attrib.get("time", 0.0)) # JUnit's ``tests`` counts every test including failures/errors/skipped. - # Re-derive ``passed`` so the dataclass meaning matches Phase 15's CSV. + # Re-derive ``passed`` so the dataclass meaning matches the baseline CSV. result.passed = max(0, result.passed - result.failed - result.errors - result.skipped) integration_errors_dir = errors_dir / integration @@ -368,7 +368,7 @@ def write_report( ) lines: list[str] = [ - "# Sandbox — full compat sweep (Phase 16)", + "# Sandbox — full compat sweep", "", "**This file is auto-generated by `run_compat_full.py`** — re-run the", "script to refresh it. Companion machine-readable CSV is `COMPAT_FULL.csv`,", @@ -385,7 +385,7 @@ def write_report( "", "## Discovery", "", - "Walked `homeassistant/components/`, applied the Phase 16 filters:", + "Walked `homeassistant/components/`, applied the discovery filters:", "", "| Filter | Skipped |", "| --- | ---: |", @@ -486,7 +486,7 @@ def main(argv: list[str] | None = None) -> int: ) parser.add_argument( "--baseline-37", action="store_true", - help="Restrict to Phase 15's 37-integration baseline list.", + help="Restrict to the 37-integration baseline list.", ) parser.add_argument( "--concurrency", type=int, default=4, diff --git a/tests/components/sandbox/_helpers.py b/tests/components/sandbox/_helpers.py index 9408d23411f7..a64304c41236 100644 --- a/tests/components/sandbox/_helpers.py +++ b/tests/components/sandbox/_helpers.py @@ -1,4 +1,4 @@ -"""Shared helpers for Phase 4 sandbox tests. +"""Shared helpers for sandbox tests. Provides: diff --git a/tests/components/sandbox/test_bridge.py b/tests/components/sandbox/test_bridge.py index 742f818f7dac..2a8ef655f642 100644 --- a/tests/components/sandbox/test_bridge.py +++ b/tests/components/sandbox/test_bridge.py @@ -1,4 +1,4 @@ -"""Tests for the Phase 5 :class:`SandboxBridge` — main-side entity bridge.""" +"""Tests for the :class:`SandboxBridge` — main-side entity bridge.""" import asyncio from typing import Any @@ -41,8 +41,8 @@ async def _wire( @pytest.fixture def ignore_translations_for_mock_domains() -> list[str]: - """Suppress strings.json checks for the Phase 6 mock domains.""" - return ["phase6_demo", "phase6_local"] + """Suppress strings.json checks for the service-mirror mock domains.""" + return ["mirror_demo", "mirror_local"] @pytest.fixture(name="entry") @@ -553,23 +553,23 @@ async def test_register_service_installs_forwarder(hass: HomeAssistant) -> None: result = await sandbox_channel.call( "sandbox/register_service", pb.RegisterService( - domain="phase6_demo", + domain="mirror_demo", service="do_thing", supports_response="none", ), ) assert result.installed is True - assert hass.services.has_service("phase6_demo", "do_thing") + assert hass.services.has_service("mirror_demo", "do_thing") await hass.services.async_call( - "phase6_demo", "do_thing", {"foo": "bar"}, blocking=True + "mirror_demo", "do_thing", {"foo": "bar"}, blocking=True ) finally: await main_channel.close() await sandbox_channel.close() assert len(seen_calls) == 1 - assert seen_calls[0].domain == "phase6_demo" + assert seen_calls[0].domain == "mirror_demo" assert seen_calls[0].service == "do_thing" assert struct_to_dict(seen_calls[0].service_data) == {"foo": "bar"} @@ -614,14 +614,14 @@ async def test_forwarded_context_restores_on_echoed_state( await sandbox_channel.call( "sandbox/register_service", pb.RegisterService( - domain="phase6_demo", service="do_thing", supports_response="none" + domain="mirror_demo", service="do_thing", supports_response="none" ), ) # The user who pressed the button that triggered the sandboxed action. user_context = Context(user_id="user-1", parent_id="parent-1") await hass.services.async_call( - "phase6_demo", "do_thing", {}, blocking=True, context=user_context + "mirror_demo", "do_thing", {}, blocking=True, context=user_context ) assert forwarded_ids == [user_context.id] @@ -659,13 +659,13 @@ async def test_register_service_skips_existing_handler( async def _local(_call: Any) -> None: return None - hass.services.async_register("phase6_local", "noop", _local) + hass.services.async_register("mirror_local", "noop", _local) try: result = await sandbox_channel.call( "sandbox/register_service", pb.RegisterService( - domain="phase6_local", + domain="mirror_local", service="noop", supports_response="none", ), @@ -676,7 +676,7 @@ async def test_register_service_skips_existing_handler( assert result.installed is False # The existing handler is still in place — the bridge didn't replace it. - assert hass.services.has_service("phase6_local", "noop") + assert hass.services.has_service("mirror_local", "noop") async def test_unregister_service_removes_forwarder( @@ -689,23 +689,23 @@ async def test_unregister_service_removes_forwarder( await sandbox_channel.call( "sandbox/register_service", pb.RegisterService( - domain="phase6_demo", + domain="mirror_demo", service="stop", supports_response="none", ), ) - assert hass.services.has_service("phase6_demo", "stop") + assert hass.services.has_service("mirror_demo", "stop") result = await sandbox_channel.call( "sandbox/unregister_service", - pb.UnregisterService(domain="phase6_demo", service="stop"), + pb.UnregisterService(domain="mirror_demo", service="stop"), ) finally: await main_channel.close() await sandbox_channel.close() assert result.removed is True - assert not hass.services.has_service("phase6_demo", "stop") + assert not hass.services.has_service("mirror_demo", "stop") async def test_fire_event_lands_on_main_bus(hass: HomeAssistant) -> None: diff --git a/tests/components/sandbox/test_channel.py b/tests/components/sandbox/test_channel.py index 99be67dbcd59..a6c7dcd97426 100644 --- a/tests/components/sandbox/test_channel.py +++ b/tests/components/sandbox/test_channel.py @@ -193,7 +193,7 @@ async def test_push_message_is_one_way(channels: tuple) -> None: async def test_handler_can_call_back_without_deadlock(channels: tuple) -> None: """A handler that issues channel.call mid-execution doesn't deadlock. - Phase 12: dispatch runs in a task so the reader keeps draining the + Dispatch runs in a task so the reader keeps draining the wire — the nested call's reply can be picked up while the outer handler is still suspended. """ diff --git a/tests/components/sandbox/test_classifier.py b/tests/components/sandbox/test_classifier.py index 7cdef75f40ba..f6e86ea67f96 100644 --- a/tests/components/sandbox/test_classifier.py +++ b/tests/components/sandbox/test_classifier.py @@ -96,7 +96,7 @@ async def test_always_main_domains_pin_to_main( async def test_phase1_spike_late_additions_pin_to_main( hass: HomeAssistant, domain: str ) -> None: - """ai_task and image were folded into ALWAYS_MAIN by the Phase 1 spike. + """ai_task and image are folded into ALWAYS_MAIN. Pinned as their own test so the regression message is unambiguous if someone removes them from the deny-list without reading the decision doc. @@ -152,7 +152,7 @@ async def test_each_incompatible_platform_forces_main( async def test_image_is_domain_not_platform_level(hass: HomeAssistant) -> None: - """Phase 1 decision: `image` lives in ALWAYS_MAIN, not the platform list. + """`image` lives in ALWAYS_MAIN, not the platform list. Camera covers the bytes-platform case; image entities returning bytes drive the domain-level rule. Lock the shape so a future cleanup doesn't diff --git a/tests/components/sandbox/test_device_registry.py b/tests/components/sandbox/test_device_registry.py index 775540b67fb3..5b1dfe4d64ca 100644 --- a/tests/components/sandbox/test_device_registry.py +++ b/tests/components/sandbox/test_device_registry.py @@ -1,4 +1,4 @@ -"""Phase 19 tests — device_info bridging from the sandbox to main's registries.""" +"""Tests — device_info bridging from the sandbox to main's registries.""" from typing import Any @@ -180,7 +180,7 @@ async def test_area_assignment_propagates_to_proxy( assert device is not None dr.async_get(hass).async_update_device(device.id, area_id=area.id) # The proxy's entity_registry entry inherits area through HA's standard - # device → entity area-resolution path (no Phase 19 code involvement). + # device → entity area-resolution path (no sandbox code involvement). refreshed_device = dr.async_get(hass).async_get(device.id) assert refreshed_device is not None assert refreshed_device.area_id == area.id diff --git a/tests/components/sandbox/test_domain_proxies.py b/tests/components/sandbox/test_domain_proxies.py index 972754c1eaa0..f4ca4a1f7ef0 100644 --- a/tests/components/sandbox/test_domain_proxies.py +++ b/tests/components/sandbox/test_domain_proxies.py @@ -1,4 +1,4 @@ -"""Per-domain smoke tests for the 28 Phase-13 proxy entities. +"""Per-domain smoke tests for the 28 proxy entities. Each parametrised case: @@ -11,7 +11,7 @@ Each parametrised case: ``sandbox/call_service`` RPC carries the expected ``(domain, service)`` plus an entity-targeted target list. -The 4 proxies that already shipped in Phase 5 (light / switch / sensor / +The 4 "rich" proxies (light / switch / sensor / binary_sensor) have dedicated coverage in ``test_bridge.py``; this file holds the 28 additions plus ``scene`` (which is in ``ALWAYS_MAIN`` but still ships a proxy for symmetry). @@ -381,7 +381,7 @@ async def test_phase13_proxy_smoke( method_kwargs: dict[str, Any], expected_service: str, ) -> None: - """Each Phase-13 proxy registers, accepts state, and translates a method.""" + """Each proxy registers, accepts state, and translates a method.""" bridge, main_channel, sandbox_channel = await _wire(hass) calls: list[pb.CallService] = [] diff --git a/tests/components/sandbox/test_manager.py b/tests/components/sandbox/test_manager.py index eefd6a9b5ccd..c702513a8317 100644 --- a/tests/components/sandbox/test_manager.py +++ b/tests/components/sandbox/test_manager.py @@ -1,4 +1,4 @@ -"""Phase 3 tests for the sandbox lifecycle manager. +"""Tests for the sandbox lifecycle manager. These exercise the real subprocess machinery — the runtime entry point at ``python -m hass_client.sandbox`` is spawned for the happy path; the diff --git a/tests/components/sandbox/test_perf.py b/tests/components/sandbox/test_perf.py index 68e544b235f7..8a30b14dcf6f 100644 --- a/tests/components/sandbox/test_perf.py +++ b/tests/components/sandbox/test_perf.py @@ -1,6 +1,6 @@ -"""Phase 14 perf benchmark — 200-light area call through the bridge batcher. +"""Perf benchmark — 200-light area call through the bridge batcher. -Validates the Phase 5 :class:`_CallServiceBatcher` coalesces a 200-entity +Validates the :class:`_CallServiceBatcher` coalesces a 200-entity area-targeted ``light.turn_on`` into a single ``sandbox/call_service`` round-trip with sub-100 ms latency. @@ -42,8 +42,8 @@ from tests.common import MockConfigEntry # Total number of sandbox-resident lights pushed into the bridge. _LIGHT_COUNT = 200 -# Wall-clock bar for the area call. The Phase 1 spike measured Option B -# at ~64 ms / 100 entities in-process; the batcher should compress the +# Wall-clock bar for the area call. The entity-bridge spike measured +# Option B at ~64 ms / 100 entities in-process; the batcher should compress the # 200-entity area call into one RPC, so we budget 500 ms on the # generous end to absorb slow CI shared runners. If we ever exceed this # bar, either the batcher regressed or the channel grew per-call diff --git a/tests/components/sandbox/test_proto_transport.py b/tests/components/sandbox/test_proto_transport.py index 666b8d901b8d..facb679b4173 100644 --- a/tests/components/sandbox/test_proto_transport.py +++ b/tests/components/sandbox/test_proto_transport.py @@ -1,6 +1,6 @@ -"""T2 transport tests: ProtobufCodec round-trips + the Context security model. +"""Transport tests: ProtobufCodec round-trips + the Context security model. -Covers the guarantees the protobuf wire adds on top of T1: +Covers the guarantees the protobuf wire adds: * a frame survives an encode → decode → re-encode cycle byte-identically (no field drops), including fidelity #7's structured voluptuous error data; diff --git a/tests/components/sandbox/test_proxy_flow.py b/tests/components/sandbox/test_proxy_flow.py index d6ac389c9e5a..6ae00e8a36d5 100644 --- a/tests/components/sandbox/test_proxy_flow.py +++ b/tests/components/sandbox/test_proxy_flow.py @@ -156,8 +156,8 @@ async def test_full_flow_user_to_create_entry( assert struct_to_dict(stub.step_calls[0].user_input) == {"host": "1.2.3.4"} # The new ConfigEntry is tagged with the sandbox group via the - # ConfigEntry.sandbox first-class field (Phase 17 — keeps the tag - # off entry.data where integration tests assert on it). + # ConfigEntry.sandbox first-class field (keeps the tag off entry.data + # where integration tests assert on it). entries = hass.config_entries.async_entries("test_proxy_full") assert len(entries) == 1 assert entries[0].sandbox == "built-in" diff --git a/tests/components/sandbox/test_schema_and_unload.py b/tests/components/sandbox/test_schema_and_unload.py index 2df0d0f9878f..d9aa0e5774c0 100644 --- a/tests/components/sandbox/test_schema_and_unload.py +++ b/tests/components/sandbox/test_schema_and_unload.py @@ -1,6 +1,6 @@ -"""Phase 14 follow-ups for sandbox. +"""Schema bridging, unique_id propagation, and the unload hook. -Covers four pieces that Phase 5 / 6 deferred and Phase 14 fills in: +Covers four pieces: * The serialised :class:`vol.Schema` bridge for flow forms and mirrored services (the proxy reconstructs a usable schema from the wire shape). @@ -99,14 +99,14 @@ def _wired_sandbox( @pytest.fixture def ignore_translations_for_mock_domains() -> list[str]: - """Suppress strings.json checks for the Phase 14 mock domains.""" + """Suppress strings.json checks for the mock domains.""" return [ - "phase14_schema", - "phase14_unique", - "phase14_duplicate", - "phase14_unload", - "phase14_local", - "phase14_svc", + "mock_schema", + "mock_unique", + "mock_duplicate", + "mock_unload", + "mock_local", + "mock_svc", ] @@ -214,14 +214,14 @@ async def test_flow_form_renders_reconstructed_schema( hass: HomeAssistant, manager: FakeSandboxManager ) -> None: """A FORM with a serialised data_schema arrives on main with the schema.""" - mock_integration(hass, MockModule("phase14_schema")) + mock_integration(hass, MockModule("mock_schema")) serialized_schema = [ {"name": "host", "type": "string", "required": True}, ] form = pb.FlowResult( type=FlowResultType.FORM.value, flow_id="sandbox-flow-schema", - handler="phase14_schema", + handler="mock_schema", step_id="user", ) form.data_schema.extend(serialized_schema) @@ -236,7 +236,7 @@ async def test_flow_form_renders_reconstructed_schema( ): await _install_router(hass, manager) result = await hass.config_entries.flow.async_init( - "phase14_schema", context={"source": SOURCE_USER} + "mock_schema", context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -258,7 +258,7 @@ async def test_register_service_with_schema_validates_on_main( ) -> None: """Sandbox-mirrored service uses its reconstructed schema on main calls.""" main_channel, sandbox_channel = make_channel_pair( - name_a="main-phase14", name_b="sandbox-phase14" + name_a="main-mock", name_b="sandbox-mock" ) bridge = SandboxBridge(hass, group="built-in", channel=main_channel) main_channel.start() @@ -277,7 +277,7 @@ async def test_register_service_with_schema_validates_on_main( ] register_service = pb.RegisterService( - domain="phase14_svc", + domain="mock_svc", service="do_thing", supports_response="none", ) @@ -291,12 +291,12 @@ async def test_register_service_with_schema_validates_on_main( with pytest.raises(vol.Invalid): await hass.services.async_call( - "phase14_svc", "do_thing", {"wrong": "field"}, blocking=True + "mock_svc", "do_thing", {"wrong": "field"}, blocking=True ) assert seen == [] await hass.services.async_call( - "phase14_svc", "do_thing", {"host": "1.2.3.4"}, blocking=True + "mock_svc", "do_thing", {"host": "1.2.3.4"}, blocking=True ) assert len(seen) == 1 assert struct_to_dict(seen[0].service_data) == {"host": "1.2.3.4"} @@ -315,11 +315,11 @@ async def test_unique_id_propagates_to_proxy_context( hass: HomeAssistant, manager: FakeSandboxManager ) -> None: """A sandbox-side ``unique_id`` is mirrored onto the proxy's context.""" - mock_integration(hass, MockModule("phase14_unique")) + mock_integration(hass, MockModule("mock_unique")) form = pb.FlowResult( type=FlowResultType.FORM.value, flow_id="sandbox-flow-uid", - handler="phase14_unique", + handler="mock_unique", step_id="user", ) form.context.update({"source": SOURCE_USER, "unique_id": "abc-123"}) @@ -334,11 +334,11 @@ async def test_unique_id_propagates_to_proxy_context( ): await _install_router(hass, manager) result = await hass.config_entries.flow.async_init( - "phase14_unique", context={"source": SOURCE_USER} + "mock_unique", context={"source": SOURCE_USER} ) # The framework now reads unique_id off the proxy's context; # ``async_progress_by_handler`` surfaces it for duplicate checks. - progress = hass.config_entries.flow.async_progress_by_handler("phase14_unique") + progress = hass.config_entries.flow.async_progress_by_handler("mock_unique") assert result["type"] is FlowResultType.FORM assert len(progress) == 1 @@ -349,11 +349,11 @@ async def test_duplicate_unique_id_aborts_second_flow( hass: HomeAssistant, manager: FakeSandboxManager ) -> None: """A second flow with the same propagated unique_id aborts on main.""" - mock_integration(hass, MockModule("phase14_duplicate")) + mock_integration(hass, MockModule("mock_duplicate")) form_a = pb.FlowResult( type=FlowResultType.FORM.value, flow_id="sandbox-flow-dup-a", - handler="phase14_duplicate", + handler="mock_duplicate", step_id="user", ) form_a.context.update({"source": SOURCE_USER, "unique_id": "dup-1"}) @@ -361,7 +361,7 @@ async def test_duplicate_unique_id_aborts_second_flow( form_b = pb.FlowResult( type=FlowResultType.FORM.value, flow_id="sandbox-flow-dup-b", - handler="phase14_duplicate", + handler="mock_duplicate", step_id="user", ) form_b.context.update({"source": SOURCE_USER, "unique_id": "dup-1"}) @@ -376,13 +376,13 @@ async def test_duplicate_unique_id_aborts_second_flow( ): await _install_router(hass, manager) first = await hass.config_entries.flow.async_init( - "phase14_duplicate", context={"source": SOURCE_USER} + "mock_duplicate", context={"source": SOURCE_USER} ) # The framework's duplicate-detection guard fires inside the # second `async_init`. See `_check_in_progress_by_unique_id`. try: second = await hass.config_entries.flow.async_init( - "phase14_duplicate", context={"source": SOURCE_USER} + "mock_duplicate", context={"source": SOURCE_USER} ) except AbortFlow as err: second = { @@ -406,7 +406,7 @@ async def test_async_unload_consults_router_for_sandboxed_entry( hass: HomeAssistant, manager: FakeSandboxManager ) -> None: """ConfigEntries.async_unload calls the router for sandbox-tagged entries.""" - mock_integration(hass, MockModule("phase14_unload")) + mock_integration(hass, MockModule("mock_unload")) with ( _wired_sandbox(manager, group="built-in", responses=[]) as stub, patch( @@ -416,7 +416,7 @@ async def test_async_unload_consults_router_for_sandboxed_entry( ): await _install_router(hass, manager) entry = MockConfigEntry( - domain="phase14_unload", + domain="mock_unload", data={"host": "1.2.3.4"}, sandbox="built-in", ) @@ -444,11 +444,11 @@ async def test_async_unload_falls_through_for_non_sandboxed_entry( mock_integration( hass, - MockModule("phase14_local", async_unload_entry=_async_unload_entry), + MockModule("mock_local", async_unload_entry=_async_unload_entry), ) await _install_router(hass, manager) - entry = MockConfigEntry(domain="phase14_local", data={"host": "1.2.3.4"}) + entry = MockConfigEntry(domain="mock_local", data={"host": "1.2.3.4"}) entry.add_to_hass(hass) # Mark loaded directly; we're not exercising async_setup here. entry.mock_state(hass, ConfigEntryState.LOADED) diff --git a/tests/components/sandbox/test_shutdown.py b/tests/components/sandbox/test_shutdown.py index 9e29e4d6ef39..f85426681ddb 100644 --- a/tests/components/sandbox/test_shutdown.py +++ b/tests/components/sandbox/test_shutdown.py @@ -1,4 +1,4 @@ -"""Phase 9 tests for graceful shutdown orchestration. +"""Tests for graceful shutdown orchestration. The main side spawns the real ``python -m hass_client.sandbox`` runtime, calls :meth:`SandboxManager.async_graceful_shutdown_all`, and diff --git a/tests/components/sandbox/test_spike.py b/tests/components/sandbox/test_spike.py index f25cde6818c6..33e23e917189 100644 --- a/tests/components/sandbox/test_spike.py +++ b/tests/components/sandbox/test_spike.py @@ -1,4 +1,4 @@ -"""Phase 1 spike — compare Options A and B on a 100-light area call. +"""Entity-bridge spike — compare Options A and B on a 100-light area call. Each test: @@ -107,7 +107,7 @@ async def test_option_a_correctness_and_latency( """Option A: method-forward RPC must work and stay under the budget.""" result = await _measure("A", main_hass, sandbox_hass) pytest.option_a_result = result # type: ignore[attr-defined] - # Generous bound — Phase 1 plan target is ~50ms for 100 entities. + # Generous bound — the target is ~50ms for 100 entities. assert result["median"] < 1.0, f"Option A median too slow: {result}" @@ -139,7 +139,7 @@ async def test_report_comparison() -> None: await sandbox_hass.async_stop(force=True) await main_hass.async_stop(force=True) - print("\n=== Phase 1 spike — light.turn_on area call ===") + print("\n=== Entity-bridge spike — light.turn_on area call ===") print(f"Entities: {LIGHT_COUNT}, iterations: {ITERATIONS}\n") print( f"{'option':<8}{'median (ms)':>14}{'min (ms)':>12}" diff --git a/tests/components/sandbox/test_store.py b/tests/components/sandbox/test_store.py index fdfc0baae704..d9fe40968ed6 100644 --- a/tests/components/sandbox/test_store.py +++ b/tests/components/sandbox/test_store.py @@ -1,4 +1,4 @@ -"""Phase 8 tests for the main-side Store handlers on :class:`SandboxBridge`. +"""Tests for the main-side Store handlers on :class:`SandboxBridge`. We exercise the three ``sandbox/store_*`` handlers via the in-memory channel pair, with the bridge wired against the real ``hass`` config @@ -53,10 +53,10 @@ async def test_store_save_writes_to_namespaced_path(hass: HomeAssistant) -> None wrapped = { "version": 1, "minor_version": 1, - "key": "phase8_demo", + "key": "demo_key", "data": {"hello": "world"}, } - save = pb.StoreSave(key="phase8_demo") + save = pb.StoreSave(key="demo_key") save.data.update(wrapped) try: result = await sandbox_channel.call("sandbox/store_save", save) @@ -65,7 +65,7 @@ async def test_store_save_writes_to_namespaced_path(hass: HomeAssistant) -> None await sandbox_channel.close() assert result.ok - path = _store_path(hass, "built-in", "phase8_demo") + path = _store_path(hass, "built-in", "demo_key") assert path.is_file() # The file holds the wrapped Store payload verbatim. assert json.loads(path.read_text(encoding="utf-8")) == wrapped @@ -77,15 +77,15 @@ async def test_store_load_returns_saved_payload(hass: HomeAssistant) -> None: wrapped = { "version": 2, "minor_version": 3, - "key": "phase8_demo", + "key": "demo_key", "data": {"counter": 42}, } - save = pb.StoreSave(key="phase8_demo") + save = pb.StoreSave(key="demo_key") save.data.update(wrapped) try: await sandbox_channel.call("sandbox/store_save", save) loaded = await sandbox_channel.call( - "sandbox/store_load", pb.StoreLoad(key="phase8_demo") + "sandbox/store_load", pb.StoreLoad(key="demo_key") ) finally: await main_channel.close() diff --git a/tests/components/sandbox/test_subprocess.py b/tests/components/sandbox/test_subprocess.py index 9295e6fc00ba..1618e4dba4c1 100644 --- a/tests/components/sandbox/test_subprocess.py +++ b/tests/components/sandbox/test_subprocess.py @@ -1,4 +1,4 @@ -"""End-to-end subprocess tests for Phase 4. +"""End-to-end subprocess tests. Spawns the real ``python -m hass_client.sandbox`` runtime and exercises the JSON-line control channel: handshake → ping round-trip → graceful diff --git a/tests/components/sandbox/test_testing_plugins.py b/tests/components/sandbox/test_testing_plugins.py index 60763cb3879c..3e08b4fc6717 100644 --- a/tests/components/sandbox/test_testing_plugins.py +++ b/tests/components/sandbox/test_testing_plugins.py @@ -1,4 +1,4 @@ -"""Phase 10 tests: the ``hass_client.testing`` pytest plugins. +"""Tests for the ``hass_client.testing`` pytest plugins. Two plugins are exercised here: @@ -11,7 +11,7 @@ Two plugins are exercised here: with the real-subprocess sandbox. The real-subprocess fixture itself is covered by -:mod:`test_phase4_subprocess` which already drives ``SandboxManager`` +:mod:`test_subprocess` which already drives ``SandboxManager`` through a real subprocess; the tests here unit-check the hook shape without spawning a nested pytest run. """ @@ -159,7 +159,7 @@ def test_autotag_sets_mock_config_entry_sandbox() -> None: assert entry.sandbox is None entry.add_to_hass(fake_hass) assert entry.sandbox == "built-in" - # entry.data is untouched — this is the whole point of Phase 17. + # entry.data is untouched — the tag rides on entry.sandbox instead. assert dict(entry.data) == {"foo": "bar"} assert fake_hass.config_entries._entries == {entry.entry_id: entry} diff --git a/tests/components/sandbox/test_transport_unix.py b/tests/components/sandbox/test_transport_unix.py index b671cdcaf65e..d4fad7e5b94f 100644 --- a/tests/components/sandbox/test_transport_unix.py +++ b/tests/components/sandbox/test_transport_unix.py @@ -1,4 +1,4 @@ -"""Unix-socket control-channel transport (transport T3). +"""Unix-socket control-channel transport. Spawns the real ``python -m hass_client.sandbox`` runtime with the manager configured for the unix-socket transport: the manager opens a