1- """Verifies the conftest backend dispatch picks the backend CI asked for .
1+ """Multi-layer smoke tests for backend dispatch correctness .
22
33The three e2e CI lanes set env vars that ``conftest.ha_container_with_fresh_config``
44reads to choose a backend:
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
3338from __future__ import annotations
3439
3540import os
3641from 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
3952def 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