From 4e3d8f1d53d557c3e0b8e2cb47193b55035ab8cd Mon Sep 17 00:00:00 2001 From: Matysh Date: Fri, 31 Jul 2026 13:12:14 +0300 Subject: [PATCH] Test harness: run async recorder regressions on a private event loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit asyncio.run() clears the thread's current-loop slot when it finishes; the CI HA harness keeps a session event loop, so every test that followed the new HP-1540-05 regression failed at SETUP with 'There is no current event loop'. The pure-only local run never sees the harness and stayed green — which is exactly how it slipped through. The regressions now spin up an isolated loop and leave the ambient one untouched. --- tests_backend/test_trail_recorder.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests_backend/test_trail_recorder.py b/tests_backend/test_trail_recorder.py index 2e9446c..069bb76 100644 --- a/tests_backend/test_trail_recorder.py +++ b/tests_backend/test_trail_recorder.py @@ -156,6 +156,23 @@ def test_unavailable_vacuum_is_no_verdict(): # ---------------- v1.54.0 audit regressions ---------------- +def _run_isolated(coro): + """Run a coroutine on a private loop WITHOUT touching the ambient one. + + asyncio.run() clears the thread's current-loop slot when it finishes; in + the CI HA harness (pytest-asyncio keeps a session event loop) that + poisoned the setup of every test that followed — 'There is no current + event loop in thread MainThread' across whole files. + """ + import asyncio + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(coro) + finally: + loop.close() + + + def test_map_index_zero_matches_frontend_contract(): # HP-1540-02: `map_index: 0` is a VALID first map. The old or-chain @@ -235,7 +252,7 @@ def test_refresh_builds_pair_lists_and_dedups_subscription(): "camera.map": S("idle", {}), }) rec = trails.TrailRecorder(hass, RT()) - asyncio.run(rec.async_refresh()) + _run_isolated(rec.async_refresh()) assert rec.pairs == {"camera.map": [("m_f1", "vacuum.x50"), ("m_f2", "vacuum.x50")]} assert tracked == [["camera.map", "vacuum.x50"]] finally: @@ -297,6 +314,6 @@ def test_overlapping_refreshes_leave_one_subscription_teardown_zero(): await t3 assert active == [], f"teardown must leave zero subscriptions, got {active}" - asyncio.run(scenario()) + _run_isolated(scenario()) finally: trails.async_track_state_change_event = old_track