mirror of
https://github.com/home-assistant/core.git
synced 2026-08-28 10:16:02 -05:00
sandbox: drop development-phase references from code
The final deliverable should not carry the scaffolding of the phases it was built in. Reword comments, docstrings, and generated-output strings that named build phases (Phase N / T1-T3 / Phase A1-A2) to describe what the code does, and rename the phase-numbered test files: test_phase4_subprocess -> test_subprocess test_phase9_shutdown -> test_shutdown test_phase13_proxies -> test_domain_proxies test_phase14 -> test_schema_and_unload test_phase19_devices -> test_device_registry Comments/docstrings/filenames only; no logic changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f5518f3705
commit
ee74f766bb
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
|
||||
@@ -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 /
|
||||
|
||||
@@ -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": <main-side 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 ``<owned_domain>_*`` 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 ``<config>/.storage/sandbox/<group>/<key>`` 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 == <anything>`` 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': '<group>'``
|
||||
# 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}",
|
||||
"",
|
||||
|
||||
+15
-14
@@ -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"
|
||||
" `<config>/.storage/sandbox/<group>/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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Sandbox-side event mirror (Phase 6).
|
||||
"""Sandbox-side event mirror.
|
||||
|
||||
Forwards every event whose ``event_type`` matches ``<approved_domain>_*``
|
||||
up to main via ``sandbox/fire_event``. Canonical examples: ``zha_event``,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ``<owned_domain>_*`` 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://<path>`` 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@ control channel: the three ``Store`` IO methods delegate to main via the
|
||||
namespaces every key as ``<config>/.storage/sandbox/<group>/<key>`` 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Phase 1 entity-bridge spike.
|
||||
"""Entity-bridge spike.
|
||||
|
||||
Two protocols are implemented in parallel:
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
+10
-10
@@ -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/<domain>/`` 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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Shared helpers for Phase 4 sandbox tests.
|
||||
"""Shared helpers for sandbox tests.
|
||||
|
||||
Provides:
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user