Skip to content

Commit a6ede04

Browse files
committed
test(e2e): add behavioral cross-check + collection floor to dispatch smoke
The original smoke test (test_backend_dispatch_matches_workflow_env) only asserted the ``backend`` field that conftest sets. That guards against the dispatch picking the wrong code path, but it can't catch the case where conftest reports backend=X while a different HA instance is actually running. Adds two layered guards: - test_supervisor_addon_tool_behavior_matches_backend: calls ha_get_addon (which only works when a real Supervisor is running) and asserts success-vs-failure matches the claimed backend. - HAOS external + inaddon: tool returns success=True with non-empty addons list (the bake installs Node-RED, ESPHome, etc.) - testcontainer: tool returns success=False because no Supervisor proxy exists (RESOURCE_NOT_FOUND from supervisor/api WebSocket call) The asymmetry makes it impossible for one backend to impersonate the other while keeping the test green — exactly the "goes both ways" guarantee. - test_session_collected_test_count_above_floor: asserts request.session.items length >= 850 (current baseline is ~913 across all lanes). Catches collection-time regressions where a test file fails to import and pytest silently drops tens of tests, leaving the suite green with reduced coverage. All three tests stay under basic/ (no auto-applied haos_only marker) so they run unconditionally on every lane.
1 parent 67ba242 commit a6ede04

1 file changed

Lines changed: 97 additions & 28 deletions

File tree

Lines changed: 97 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Verifies the conftest backend dispatch picks the backend CI asked for.
1+
"""Multi-layer smoke tests for backend dispatch correctness.
22
33
The three e2e CI lanes set env vars that ``conftest.ha_container_with_fresh_config``
44
reads to choose a backend:
@@ -9,48 +9,60 @@
99
| haos-e2e-tests.yml (external) | set | unset | ``haos`` |
1010
| haos-e2e-inaddon-tests.yml | set | ``inaddon`` | ``haos_inaddon`` |
1111
12-
Without an explicit assertion, the dispatch has silent-failure modes that
13-
all leave CI green while running tests against the wrong backend:
14-
15-
1. ``HAOS_TEST_IMAGE_PATH`` set but ``is_haos_backend_selected()`` returns
16-
False (env-var name drift, import-time bug) — both HAOS lanes silently
17-
fall through to the testcontainer path.
18-
2. ``HAOS_TEST_MODE=inaddon`` set but ``is_haos_inaddon_mode()`` reads a
19-
different name — inaddon lane silently runs the external dispatch,
20-
so ``mcp_client`` talks to the in-process FastMCP server instead of
21-
the addon's HTTP endpoint. The whole inaddon integration is untested.
22-
3. Inaddon dispatch reached but ``addon_mcp_url`` never populated —
23-
downstream fixtures route to the wrong endpoint and surface as
24-
confusing errors later.
25-
26-
This file is placed under ``basic/`` (NOT ``haos_only/``) on purpose:
27-
the auto-applied ``haos_only`` marker in ``conftest.pytest_collection_modifyitems``
28-
would skip the test whenever ``is_haos_backend_selected()`` returns False,
29-
which is exactly the silent-failure case (1) above. We want the test to
30-
RUN on every lane and FAIL when the backend doesn't match the env.
12+
Three layers of guard, each catching a different silent-failure mode:
13+
14+
1. ``test_backend_dispatch_matches_workflow_env`` — asserts the ``backend``
15+
field that conftest sets matches what the workflow env vars imply.
16+
Catches dispatch falling through to the wrong code path.
17+
18+
2. ``test_supervisor_addon_tool_behavior_matches_backend`` — calls
19+
``ha_get_addon`` (which only works when a real Supervisor is present)
20+
and asserts success-vs-failure matches the claimed backend. Catches
21+
the case where conftest reports ``backend=X`` but actually a different
22+
HA instance is running (e.g. mock Supervisor on testcontainer making
23+
addon calls succeed when the real backend has no Supervisor at all,
24+
or HAOS reporting itself as container).
25+
26+
3. ``test_session_collected_test_count_above_floor`` — asserts the
27+
session collected at least the baseline number of tests. Catches
28+
collection-time regressions where a test file fails to import and
29+
pytest silently drops tens of tests.
30+
31+
This file is placed under ``basic/`` (NOT ``haos_only/``) on purpose: the
32+
auto-applied ``haos_only`` marker would skip these whenever
33+
``is_haos_backend_selected()`` returns False, which is exactly the
34+
silent-failure case we want to catch. ``basic/`` has no auto-applied
35+
markers so the tests run unconditionally on every lane.
3136
"""
3237

3338
from __future__ import annotations
3439

3540
import os
3641
from typing import Any
3742

43+
from ..utilities.assertions import safe_call_tool
44+
45+
# Floor for total collected tests across all lanes. As of 2026-05-22 each
46+
# lane collects ~913 tests (just differing skip mix per mode). Set well
47+
# below current value to allow normal test-add/remove churn while still
48+
# catching the case where ~50+ tests vanish from collection.
49+
_COLLECTION_FLOOR = 850
50+
3851

3952
def test_backend_dispatch_matches_workflow_env(
4053
ha_container_with_fresh_config: dict[str, Any],
4154
) -> None:
4255
"""Conftest dispatch must pick the backend the workflow env implies.
4356
44-
Runs unconditionally on every lane — assertion branches off the env
45-
vars to mirror conftest's own dispatch logic. Mismatch means the
46-
dispatch silently picked a different backend than CI asked for.
57+
Runs unconditionally on every lane — branches off env vars to mirror
58+
conftest's own dispatch logic. Mismatch means the dispatch silently
59+
picked a different backend than CI asked for.
4760
"""
4861
image_path = os.environ.get("HAOS_TEST_IMAGE_PATH")
4962
mode = os.environ.get("HAOS_TEST_MODE", "")
5063
backend = ha_container_with_fresh_config["backend"]
5164

5265
if image_path and mode == "inaddon":
53-
# haos-e2e-inaddon-tests.yml lane
5466
assert backend == "haos_inaddon", (
5567
f"Workflow set HAOS_TEST_IMAGE_PATH + HAOS_TEST_MODE=inaddon "
5668
f"but dispatch picked backend={backend!r}. The inaddon "
@@ -64,24 +76,81 @@ def test_backend_dispatch_matches_workflow_env(
6476
)
6577
assert ha_container_with_fresh_config["container"] is None
6678
elif image_path:
67-
# haos-e2e-tests.yml (external) lane
6879
assert backend == "haos", (
6980
f"Workflow set HAOS_TEST_IMAGE_PATH but dispatch picked "
7081
f"backend={backend!r}. The lane silently fell through to "
7182
f"the testcontainer path; tests are running against the "
7283
f"wrong HA instance."
7384
)
74-
# External HAOS sets the testcontainer keys to None.
7585
assert ha_container_with_fresh_config["container"] is None
7686
assert ha_container_with_fresh_config["port"] is None
7787
assert ha_container_with_fresh_config["config_path"] is None
78-
# addon_mcp_url is the inaddon-only routing key.
7988
assert ha_container_with_fresh_config["addon_mcp_url"] is None
8089
else:
81-
# e2e-tests.yml (testcontainer) lane
8290
assert backend == "container", (
8391
f"No HAOS env vars set, expected testcontainer backend, "
8492
f"got backend={backend!r}."
8593
)
8694
assert ha_container_with_fresh_config["container"] is not None
8795
assert ha_container_with_fresh_config["port"] is not None
96+
97+
98+
async def test_supervisor_addon_tool_behavior_matches_backend(
99+
mcp_client: Any,
100+
ha_container_with_fresh_config: dict[str, Any],
101+
) -> None:
102+
"""Behavioral cross-check: ``ha_get_addon`` must succeed on HAOS, fail on container.
103+
104+
Stronger guarantee than the dispatch-field check: conftest could
105+
self-report ``backend=container`` but actually have HAOS running
106+
(or vice versa). A real Supervisor only exists on HAOS — the HA
107+
Core testcontainer has no Supervisor service running. So:
108+
109+
- HAOS external + inaddon: ``ha_get_addon`` returns a populated
110+
addons list (the bake installs several addons).
111+
- testcontainer: ``ha_get_addon`` returns ``success=False`` because
112+
the Supervisor proxy endpoint is unreachable.
113+
114+
The asymmetry of this check makes it impossible for one backend to
115+
impersonate the other while keeping this test green.
116+
"""
117+
backend = ha_container_with_fresh_config["backend"]
118+
result = await safe_call_tool(mcp_client, "ha_get_addon", {})
119+
120+
if backend in ("haos", "haos_inaddon"):
121+
assert result.get("success") is True, (
122+
f"ha_get_addon failed on {backend} backend; Supervisor must "
123+
f"be running. Result: {result!r}"
124+
)
125+
# list_addons returns ``{"success": True, "addons": [...], "summary": {...}}``
126+
# — ``addons`` is a top-level key, not nested under ``data``.
127+
addons = result.get("addons") or []
128+
assert isinstance(addons, list) and len(addons) > 0, (
129+
f"Expected installed addons on {backend} (the bake installs "
130+
f"Node-RED, ESPHome, AppDaemon, dev addon, etc.); got "
131+
f"{addons!r}"
132+
)
133+
else:
134+
# testcontainer has no Supervisor → ha_get_addon must fail
135+
assert result.get("success") is False, (
136+
f"ha_get_addon unexpectedly succeeded on {backend} backend. "
137+
f"Testcontainer has no Supervisor service; success here "
138+
f"means we're actually running on HAOS but conftest reported "
139+
f"backend={backend!r}. Result: {result!r}"
140+
)
141+
142+
143+
def test_session_collected_test_count_above_floor(request: Any) -> None:
144+
"""Session collected at least ``_COLLECTION_FLOOR`` tests.
145+
146+
Catches collection-time regressions: a test file fails to import,
147+
pytest collects tens of fewer tests, the suite stays green with
148+
reduced coverage. Collection count is mode-independent (all lanes
149+
collect the same items, mode only changes pass/skip mix).
150+
"""
151+
total = len(request.session.items)
152+
assert total >= _COLLECTION_FLOOR, (
153+
f"Only {total} tests collected, expected >= {_COLLECTION_FLOOR}. "
154+
f"A test file likely failed to import, dropping coverage. Check "
155+
f"for collection errors in the pytest output."
156+
)

0 commit comments

Comments
 (0)