Skip to content

Commit bc035d6

Browse files
authored
Merge branch 'master' into feat/search-aggregate-membership
2 parents b55647d + 5282a48 commit bc035d6

12 files changed

Lines changed: 1657 additions & 86 deletions

File tree

homeassistant-addon-dev/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: "Home Assistant MCP Server (Dev)"
22
description: "Development channel - AI assistant integration via MCP (unstable)"
3-
version: "8.3.0.dev2385"
3+
version: "8.3.0.dev2387"
44
slug: "ha_mcp_dev"
55
url: "https://github.com/homeassistant-ai/ha-mcp"
66
stage: experimental

site/src/data/tools.json

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

src/ha_mcp/tools/tools_addons.py

Lines changed: 285 additions & 66 deletions
Large diffs are not rendered by default.

tests/pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ markers =
6464
external_only: in-process-server tiers only — skip for inaddon, stdio, container-embedded, and HAOS-embedded servers; #1349, #1527, #2233
6565
inaddon_only: HAOS inaddon mode only — exercises is_running_in_addon()=True paths; #1349
6666
haos_stdio_only: HAOS stdio mode only — exercises the installed ha-mcp command over a real stdio subprocess transport; #2233
67+
haos_tls: final HAOS embedded scenario that restarts Core with HTTPS in the existing worker VM, then restores HTTP; #2241
6768
not_on_embedded: skip on the embedded backend (E2E_BACKEND=embedded) — provably redundant with the lane's own in-process ha_mcp_server session backend; #1527
6869
not_on_haos_embedded: skip on the haos_embedded backend (HAOS_TEST_MODE=embedded) — provably redundant with the lane's own session backend, which enables the entry once and drives the in-process server for the whole suite; #1527
6970

tests/src/e2e/basic/test_backend_dispatch_smoke.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,19 @@
8787
# intentional marker-gated additions rather than runtime skips. A new lane
8888
# starts at its observed count plus the five-item buffer described above;
8989
# subsequent additions consume that buffer explicitly.
90-
"container": 74, # was 73; +1 HAOS stdio visibility e2e (haos_stdio_only)
91-
"haos": 49, # was 48; +1 HAOS stdio visibility e2e (haos_stdio_only)
90+
# #2241's final same-VM Core-TLS scenario runs only on haos_embedded and
91+
# deliberately adds one collection-time skip on every other lane — via
92+
# haos_tls on the HAOS lanes, and via haos_only on container/embedded,
93+
# which already skip its directory.
94+
"container": 75,
95+
"haos": 50,
9296
# HAOS stdio is the external HAOS set plus ``external_only`` tests, whose
9397
# test-process monkeypatches cannot reach the subprocess server. The first
9498
# full lane run on 2026-08-18 observed 103 collection-time marker skips
95-
# (plus 9 runtime skips); keep the same five-item buffer as established lanes.
96-
"haos_stdio": 108,
97-
"haos_inaddon": 77, # was 76; +1 visibility e2e (haos_stdio_only)
99+
# (plus 9 runtime skips); keep the same five-item buffer as established
100+
# lanes (108), +1 #2241 haos_tls scenario.
101+
"haos_stdio": 109,
102+
"haos_inaddon": 78,
98103
# Embedded backend (#1527, E2E_BACKEND=embedded). Skips exactly the container
99104
# lane's marker-skips PLUS two embedded-specific additions:
100105
# - haos_only + inaddon_only tests skip on embedded just like on container
@@ -103,12 +108,14 @@
103108
# by test-process env/monkeypatch — same reason as inaddon), plus the
104109
# workflows/embedded smoke test (not_on_embedded).
105110
# Static def-level derivation (Docker-less, so parametrize item-inflation isn't
106-
# visible locally): haos_only 52 + inaddon_only-outside-haos 11 + external_only
111+
# visible locally): haos_only 53 + inaddon_only-outside-haos 11 + external_only
107112
# 36 (auto_backup 19, supervisor_mock 15, self_update_notice 1, file_operations
108-
# 1) + not_on_embedded 2 = 101. Parametrize inflates that to the CI-observed
109-
# count of 133 in the 2026-08-18 CI run (132 before the self-restart e2e).
113+
# 1) + not_on_embedded 2 = 102. Parametrize inflates that to the CI-observed
114+
# count of 133 in the 2026-08-18 CI run (132 before the self-restart e2e);
115+
# +1 visibility e2e (haos_stdio_only) and +1 #2241 haos_tls scenario
116+
# (haos_only) bridge 133 -> 135.
110117
# Read future changes from CI instead of deriving them.
111-
"embedded": 134, # was 133; +1 visibility e2e (haos_stdio_only)
118+
"embedded": 135,
112119
# HAOS embedded backend (#1527, HAOS_TEST_MODE=embedded). A HAOS lane, so it
113120
# skips the SAME set as the external HAOS lane (container_only + inaddon_only)
114121
# PLUS two haos_embedded-specific additions:

tests/src/e2e/conftest.py

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import time
3232
import urllib.error
3333
import urllib.request
34+
import warnings
3435
from collections.abc import AsyncGenerator
3536
from functools import partial
3637
from pathlib import Path
@@ -39,6 +40,7 @@
3940
import pytest
4041
import requests
4142
from testcontainers.core.container import DockerContainer
43+
from urllib3.exceptions import InsecureRequestWarning
4244

4345
# Add src to path for imports
4446
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
@@ -67,6 +69,7 @@
6769
set_default_backup_password,
6870
stage_embedded_server_feature_flags_in_qcow2,
6971
stage_embedded_server_wheel_in_qcow2,
72+
stage_home_assistant_tls_in_qcow2,
7073
trigger_dev_addon_update,
7174
wait_for_addon_ha_link_ready,
7275
wait_for_addon_mcp_ready,
@@ -228,6 +231,34 @@ def _log_readiness_timing(gate: str, elapsed_s: float, **extras: Any) -> None:
228231
_READINESS_TIMINGS.append({"gate": gate, "elapsed_s": elapsed_s, **extras})
229232

230233

234+
def _move_haos_tls_items_last(items: list[Any]) -> None:
235+
"""Make the one destructive Core-restart scope the final xdist work unit.
236+
237+
``loadscope`` groups tests into scopes by module (or module::class). With
238+
xdist's default ``--loadscope-reorder`` the scopes are sorted by
239+
descending size with a stable sort, so the one-test TLS module moved to
240+
the end stays last among the one-test scopes; with ``--no-loadscope-reorder``
241+
plain collection order keeps it last outright. The FIFO workqueue then
242+
dispatches it as the final unit. The worker that receives it may still
243+
hold up to two queued ordinary tests (xdist tops nodes up at <=2 pending)
244+
but executes units in dispatch order, so Core is restarted in that
245+
worker's already-running VM only after its ordinary work has run.
246+
"""
247+
ordinary = [item for item in items if "haos_tls" not in item.keywords]
248+
tls = [item for item in items if "haos_tls" in item.keywords]
249+
items[:] = [*ordinary, *tls]
250+
251+
252+
def _apply_haos_tls_skip(item: Any, enabled: bool, skip_marker: Any) -> None:
253+
"""Skip a ``haos_tls`` item on every lane where the scenario is disabled.
254+
255+
Kept out-of-line so ``pytest_collection_modifyitems`` stays under the
256+
repo-wide C901 ceiling — inlining this branch trips it.
257+
"""
258+
if "haos_tls" in item.keywords and not enabled:
259+
item.add_marker(skip_marker)
260+
261+
231262
def pytest_collection_modifyitems(config, items):
232263
"""Enforce backend markers and auto-apply ``haos_only`` to its dir.
233264
@@ -249,6 +280,8 @@ def pytest_collection_modifyitems(config, items):
249280
exercised). Skipped on external mode and on testcontainer.
250281
- ``haos_stdio_only``: HAOS stdio mode only (``mcp_client`` launches the
251282
installed ``ha-mcp`` command and uses real stdio JSON-RPC framing).
283+
- ``haos_tls``: final HAOS-embedded scenario. It restarts Core with HTTPS,
284+
exercises the same VM, and restores HTTP before session teardown.
252285
"""
253286
del config
254287
haos = is_haos_backend_selected()
@@ -281,6 +314,9 @@ def pytest_collection_modifyitems(config, items):
281314
skip_haos_stdio_only = pytest.mark.skip(
282315
reason="HAOS stdio mode required (set HAOS_TEST_MODE=stdio)"
283316
)
317+
skip_haos_tls = pytest.mark.skip(
318+
reason="final Core TLS scenario runs only in the existing HAOS embedded worker"
319+
)
284320
skip_external_only = pytest.mark.skip(
285321
reason="out-of-process server (stdio/inaddon/embedded); test needs an "
286322
"in-process server it can reconfigure via env/monkeypatch or reach an "
@@ -306,6 +342,12 @@ def pytest_collection_modifyitems(config, items):
306342
item.add_marker(skip_inaddon_only)
307343
if "haos_stdio_only" in keywords and not haos_stdio:
308344
item.add_marker(skip_haos_stdio_only)
345+
_apply_haos_tls_skip(item, haos_embedded, skip_haos_tls)
346+
# Deliberate: inserting the haos_tls dispatch above broke the old
347+
# ``elif`` chain into this ``if``. An item carrying several gate
348+
# markers now collects one skip marker per gate instead of the first
349+
# only — still one skipped item, same counts; only the reported
350+
# reason (first marker added) could differ.
309351
# ``external_only`` skips on any tier where the server is NOT in the
310352
# pytest process: the inaddon HAOS addon AND the embedded backend's
311353
# in-process MCP server (both #1527). The name is historical (from
@@ -327,7 +369,7 @@ def pytest_collection_modifyitems(config, items):
327369
# cannot observe pytest-process env changes, monkeypatches, or in-process
328370
# mocks. The same tests retain coverage on container/external HAOS lanes.
329371
#
330-
elif "external_only" in keywords and (
372+
if "external_only" in keywords and (
331373
inaddon or embedded or haos_embedded or haos_stdio
332374
):
333375
item.add_marker(skip_external_only)
@@ -347,6 +389,7 @@ def pytest_collection_modifyitems(config, items):
347389
# the sole thing exercising the in-process server).
348390
if "not_on_haos_embedded" in keywords and haos_embedded:
349391
item.add_marker(skip_not_on_haos_embedded)
392+
_move_haos_tls_items_last(items)
350393

351394

352395
# Fail fast on a doomed run, on EVERY e2e lane (this conftest is shared by the
@@ -973,12 +1016,16 @@ def _embedded_mcp_result(resp: requests.Response) -> dict[str, Any] | None:
9731016
return parse_mcp_response(resp.headers.get("Content-Type", ""), resp.content)
9741017

9751018

976-
def _wait_for_embedded_webhook_ready(webhook_url: str, timeout: int) -> bool:
1019+
def _wait_for_embedded_webhook_ready(
1020+
webhook_url: str, timeout: int, *, verify: bool = True
1021+
) -> bool:
9771022
"""Poll the embedded server's ingress webhook until MCP ``initialize`` works.
9781023
9791024
A valid JSON-RPC ``result`` means the in-process MCP server has installed
9801025
itself, started its worker thread, and registered the webhook. Returns False
9811026
on timeout so the caller can dump diagnostics and fail with context.
1027+
``verify=False`` lets the TLS scenario poll the ``https://`` webhook while
1028+
Core runs its trial certificate.
9821029
"""
9831030
payload = {
9841031
"jsonrpc": "2.0",
@@ -997,9 +1044,19 @@ def _wait_for_embedded_webhook_ready(webhook_url: str, timeout: int) -> bool:
9971044
deadline = time.monotonic() + timeout
9981045
while time.monotonic() < deadline:
9991046
try:
1000-
resp = requests.post(
1001-
webhook_url, headers=headers, data=json.dumps(payload), timeout=30
1002-
)
1047+
with warnings.catch_warnings():
1048+
if not verify:
1049+
# This test deliberately uses Core's hostname-mismatched
1050+
# loopback certificate. Keep the suite's warnings-as-errors
1051+
# policy for every warning except this expected one.
1052+
warnings.simplefilter("ignore", InsecureRequestWarning)
1053+
resp = requests.post(
1054+
webhook_url,
1055+
headers=headers,
1056+
data=json.dumps(payload),
1057+
timeout=30,
1058+
verify=verify,
1059+
)
10031060
if resp.status_code == 200:
10041061
parsed = _embedded_mcp_result(resp)
10051062
if parsed is not None and "result" in parsed:
@@ -1842,6 +1899,22 @@ def _prepare_haos_image(
18421899
# below applies it via Docker layer cache (#1349 item 7).
18431900
if inaddon:
18441901
refresh_dev_addon_source_in_qcow2(image_path)
1902+
if haos_embedded:
1903+
# Best-effort like the wheel staging above: only the final TLS scenario
1904+
# consumes this certificate, and it asserts on HAOS_TEST_TLS_CA_PATH —
1905+
# a staging failure should fail that one test, not abort the lane.
1906+
try:
1907+
certificate = stage_home_assistant_tls_in_qcow2(image_path)
1908+
except RuntimeError:
1909+
logger.warning(
1910+
"HAOS Core TLS staging failed; the TLS scenario will report it",
1911+
exc_info=True,
1912+
)
1913+
else:
1914+
# Do not alter process-wide trust during ordinary tests. The final
1915+
# TLS scenario trusts this cert only while reproducing the legacy
1916+
# mismatch.
1917+
os.environ["HAOS_TEST_TLS_CA_PATH"] = str(certificate)
18451918
return image_path
18461919

18471920

0 commit comments

Comments
 (0)