From 97a941c45d2183e8e3f5523e4cffbb4ac82eaaa8 Mon Sep 17 00:00:00 2001 From: Byron Gamatos Date: Fri, 3 Jul 2026 12:15:43 +0200 Subject: [PATCH] fix(tests): join background scan/enrich workers before closing the DB (#735) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the flaky pytest segfault (exit 139): the background scan and enrichment daemon threads (_scan_runner/_enrich_runner) use the shared MetadataDB connection, but test fixtures closed that connection in teardown without stopping them. A daemon thread mid-query on a freed SQLite conn is a native use-after-free → SIGSEGV. The app's startup kicks a scan, so almost any app-booting fixture was vulnerable. It only surfaced now because got-feedback/feedBack#728 added a push trigger, so ci/test runs on every push to main. Fix: server.py retains the scan/enrich thread handles and adds _join_background_db_threads(); every test fixture now joins the workers before conn.close(). Verified: the full suite runs to completion (no segfault) where it previously crashed at ~25%. Co-authored-by: Claude Opus 4.8 (1M context) --- server.py | 31 ++++++++++++++++++--- tests/test_albums_view.py | 1 + tests/test_art_candidates.py | 1 + tests/test_art_layer.py | 1 + tests/test_artist_alias.py | 1 + tests/test_artist_page.py | 1 + tests/test_artist_sort_title_order.py | 1 + tests/test_audio_effect_mappings.py | 1 + tests/test_audio_local_path.py | 2 ++ tests/test_batch_user_meta.py | 1 + tests/test_collections_api.py | 1 + tests/test_context_menu_api.py | 1 + tests/test_correlation_id.py | 2 ++ tests/test_curated_album.py | 1 + tests/test_demo_mode.py | 2 ++ tests/test_enrichment_plumbing.py | 1 + tests/test_feedpak_extension.py | 3 ++ tests/test_gap_fill.py | 2 ++ tests/test_group_filter_law.py | 1 + tests/test_highway_ws_authors.py | 2 ++ tests/test_highway_ws_instrument_routing.py | 1 + tests/test_highway_ws_notation.py | 1 + tests/test_library_filters.py | 1 + tests/test_library_keyset.py | 1 + tests/test_library_providers.py | 2 ++ tests/test_loose_traversal.py | 1 + tests/test_mb_enrichment.py | 1 + tests/test_minigames_routes.py | 1 + tests/test_playlists_api.py | 1 + tests/test_practice_suggestions.py | 1 + tests/test_profile_api.py | 1 + tests/test_progression_api.py | 1 + tests/test_scraper_options.py | 1 + tests/test_settings_api.py | 9 ++++++ tests/test_settings_export.py | 1 + tests/test_settings_export_library_db.py | 3 ++ tests/test_settings_instrument.py | 1 + tests/test_sloppak_cover_art.py | 1 + tests/test_sloppak_file_traversal.py | 1 + tests/test_song_stats_api.py | 1 + tests/test_song_user_meta.py | 1 + tests/test_starter_suggestions.py | 1 + tests/test_startup_status.py | 5 ++++ tests/test_version_endpoint.py | 1 + tests/test_wanted_api.py | 1 + tests/test_work_charts_api.py | 1 + tests/test_work_grouping.py | 1 + 47 files changed, 95 insertions(+), 4 deletions(-) diff --git a/server.py b/server.py index 3ed2237..596208e 100644 --- a/server.py +++ b/server.py @@ -5824,6 +5824,27 @@ def _background_scan(): _scan_kick_lock = threading.Lock() _scan_rescan_pending = False +# Handles to the running scan / enrichment worker threads. Both use the shared +# MetadataDB connection, so teardown/shutdown MUST join them before closing that +# connection — a daemon thread mid-query on a closed SQLite conn is a native +# use-after-free that segfaults the process (seen flaky in CI). Set by +# _kick_scan / _kick_enrich; joined by _join_background_db_threads(). +_scan_thread: threading.Thread | None = None +_enrich_thread: threading.Thread | None = None + + +def _join_background_db_threads(timeout: float = 30.0) -> None: + """Block until the background scan + enrichment workers finish (or timeout). + + A scan kicks enrichment on completion, so join the scan first — by the time + it returns, _kick_enrich() has set _enrich_thread — then join enrichment.""" + st = _scan_thread + if st is not None and st.is_alive(): + st.join(timeout) + et = _enrich_thread + if et is not None and et.is_alive(): + et.join(timeout) + def _kick_scan() -> bool: """Request a library rescan, single-flight + coalescing. @@ -5835,7 +5856,7 @@ def _kick_scan() -> bool: until the next periodic pass. Multiple late-arriving requests coalesce into a single follow-up. """ - global _scan_rescan_pending + global _scan_rescan_pending, _scan_thread with _scan_kick_lock: if _scan_status["running"]: _scan_rescan_pending = True @@ -5843,7 +5864,8 @@ def _kick_scan() -> bool: # Mark running synchronously so a parallel _kick_scan() observes it # before the worker thread has a chance to reassign _scan_status. _scan_status["running"] = True - threading.Thread(target=_scan_runner, daemon=True).start() + _scan_thread = threading.Thread(target=_scan_runner, daemon=True) + _scan_thread.start() return True @@ -6538,13 +6560,14 @@ def _kick_enrich() -> bool: """Request an enrichment pass, single-flight + coalescing (the _kick_scan contract): True = a worker thread was started, False = one is running and a follow-up pass was queued.""" - global _enrich_pending_pass + global _enrich_pending_pass, _enrich_thread with _enrich_kick_lock: if _enrich_status["running"]: _enrich_pending_pass = True return False _enrich_status["running"] = True - threading.Thread(target=_enrich_runner, daemon=True).start() + _enrich_thread = threading.Thread(target=_enrich_runner, daemon=True) + _enrich_thread.start() return True diff --git a/tests/test_albums_view.py b/tests/test_albums_view.py index 6fca243..cf73377 100644 --- a/tests/test_albums_view.py +++ b/tests/test_albums_view.py @@ -23,6 +23,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_art_candidates.py b/tests/test_art_candidates.py index d864050..5d6ba46 100644 --- a/tests/test_art_candidates.py +++ b/tests/test_art_candidates.py @@ -34,6 +34,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_art_layer.py b/tests/test_art_layer.py index 03d7744..fb641a2 100644 --- a/tests/test_art_layer.py +++ b/tests/test_art_layer.py @@ -29,6 +29,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_artist_alias.py b/tests/test_artist_alias.py index 62acba3..f07b1f2 100644 --- a/tests/test_artist_alias.py +++ b/tests/test_artist_alias.py @@ -22,6 +22,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_artist_page.py b/tests/test_artist_page.py index 654f74f..2b53237 100644 --- a/tests/test_artist_page.py +++ b/tests/test_artist_page.py @@ -41,6 +41,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_artist_sort_title_order.py b/tests/test_artist_sort_title_order.py index 7f16fd2..56ca98f 100644 --- a/tests/test_artist_sort_title_order.py +++ b/tests/test_artist_sort_title_order.py @@ -22,6 +22,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_audio_effect_mappings.py b/tests/test_audio_effect_mappings.py index 06f5d99..c99c14e 100644 --- a/tests/test_audio_effect_mappings.py +++ b/tests/test_audio_effect_mappings.py @@ -18,6 +18,7 @@ def client(tmp_path, monkeypatch): for attr in ("meta_db", "audio_effect_mappings"): conn = getattr(getattr(server, attr, None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_audio_local_path.py b/tests/test_audio_local_path.py index 4844adc..2b93e1d 100644 --- a/tests/test_audio_local_path.py +++ b/tests/test_audio_local_path.py @@ -34,6 +34,7 @@ def client_and_server(tmp_path, monkeypatch): meta_db = getattr(server, "meta_db", None) conn = getattr(meta_db, "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -60,6 +61,7 @@ def non_loopback_client(tmp_path, monkeypatch): meta_db = getattr(server, "meta_db", None) conn = getattr(meta_db, "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_batch_user_meta.py b/tests/test_batch_user_meta.py index f67f70b..1355a1b 100644 --- a/tests/test_batch_user_meta.py +++ b/tests/test_batch_user_meta.py @@ -22,6 +22,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_collections_api.py b/tests/test_collections_api.py index 3a494c6..74238dd 100644 --- a/tests/test_collections_api.py +++ b/tests/test_collections_api.py @@ -21,6 +21,7 @@ def server_mod(tmp_path, monkeypatch): yield mod conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_context_menu_api.py b/tests/test_context_menu_api.py index 3d76ec2..fdc1792 100644 --- a/tests/test_context_menu_api.py +++ b/tests/test_context_menu_api.py @@ -24,6 +24,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_correlation_id.py b/tests/test_correlation_id.py index 27cbf15..114132c 100644 --- a/tests/test_correlation_id.py +++ b/tests/test_correlation_id.py @@ -36,6 +36,7 @@ def client(tmp_path, monkeypatch): finally: conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -169,6 +170,7 @@ def test_server_app_request_id_propagated_to_logs(monkeypatch, tmp_path): ] conn = getattr(getattr(server_mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() lines = [ln for ln in buf.getvalue().splitlines() if "server_probe_event" in ln] diff --git a/tests/test_curated_album.py b/tests/test_curated_album.py index e6546ff..34ee8f4 100644 --- a/tests/test_curated_album.py +++ b/tests/test_curated_album.py @@ -20,6 +20,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_demo_mode.py b/tests/test_demo_mode.py index b58f2b8..0523f38 100644 --- a/tests/test_demo_mode.py +++ b/tests/test_demo_mode.py @@ -60,6 +60,7 @@ def _cleanup(server, client): server._DEMO_JANITOR_HOOKS.clear() conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -311,6 +312,7 @@ def test_register_demo_janitor_hook_in_plugin_context(tmp_path, monkeypatch): conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() # Clean up janitor state so it doesn't bleed into other tests. server._DEMO_JANITOR_STOP.set() diff --git a/tests/test_enrichment_plumbing.py b/tests/test_enrichment_plumbing.py index 63c304c..6c1a290 100644 --- a/tests/test_enrichment_plumbing.py +++ b/tests/test_enrichment_plumbing.py @@ -22,6 +22,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_feedpak_extension.py b/tests/test_feedpak_extension.py index 4c816b2..6c1d7c9 100644 --- a/tests/test_feedpak_extension.py +++ b/tests/test_feedpak_extension.py @@ -100,6 +100,7 @@ def scan_server(tmp_path, monkeypatch, isolate_logging): yield mod conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -161,6 +162,7 @@ def upload_client(tmp_path, monkeypatch): tc.close() conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -228,6 +230,7 @@ def settings_server(tmp_path, monkeypatch): finally: conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_gap_fill.py b/tests/test_gap_fill.py index 921587b..a870cf4 100644 --- a/tests/test_gap_fill.py +++ b/tests/test_gap_fill.py @@ -29,6 +29,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) @@ -255,5 +256,6 @@ def test_demo_mode_blocks_write(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_group_filter_law.py b/tests/test_group_filter_law.py index 22cf98b..46e4171 100644 --- a/tests/test_group_filter_law.py +++ b/tests/test_group_filter_law.py @@ -20,6 +20,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_highway_ws_authors.py b/tests/test_highway_ws_authors.py index 3de3f2c..b8330d9 100644 --- a/tests/test_highway_ws_authors.py +++ b/tests/test_highway_ws_authors.py @@ -30,6 +30,7 @@ def server_mod(monkeypatch, tmp_path): yield mod conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -124,6 +125,7 @@ def make_client(tmp_path, monkeypatch): server = sys.modules.get("server") conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_highway_ws_instrument_routing.py b/tests/test_highway_ws_instrument_routing.py index cf445f0..70b3ef5 100644 --- a/tests/test_highway_ws_instrument_routing.py +++ b/tests/test_highway_ws_instrument_routing.py @@ -103,6 +103,7 @@ def make_client(tmp_path, monkeypatch): server = sys.modules.get("server") conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_highway_ws_notation.py b/tests/test_highway_ws_notation.py index a9997f3..851e619 100644 --- a/tests/test_highway_ws_notation.py +++ b/tests/test_highway_ws_notation.py @@ -130,6 +130,7 @@ def make_client(tmp_path, monkeypatch): server = sys.modules.get("server") conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_library_filters.py b/tests/test_library_filters.py index 3205243..25dcb18 100644 --- a/tests/test_library_filters.py +++ b/tests/test_library_filters.py @@ -24,6 +24,7 @@ def server_mod(tmp_path, monkeypatch): yield mod conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_library_keyset.py b/tests/test_library_keyset.py index 3037e77..042afda 100644 --- a/tests/test_library_keyset.py +++ b/tests/test_library_keyset.py @@ -23,6 +23,7 @@ def server_mod(tmp_path, monkeypatch): yield mod conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_library_providers.py b/tests/test_library_providers.py index b6e7b6f..3df3270 100644 --- a/tests/test_library_providers.py +++ b/tests/test_library_providers.py @@ -15,6 +15,7 @@ def server_mod(tmp_path, monkeypatch): yield mod conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -298,4 +299,5 @@ def test_library_provider_registration_is_available_to_plugins(tmp_path, monkeyp assert captured["unregister_library_provider"] is server.unregister_library_provider finally: if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_loose_traversal.py b/tests/test_loose_traversal.py index 2bcc514..309fae2 100644 --- a/tests/test_loose_traversal.py +++ b/tests/test_loose_traversal.py @@ -37,6 +37,7 @@ def dlc_client(tmp_path, monkeypatch): meta_db = getattr(server, "meta_db", None) conn = getattr(meta_db, "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_mb_enrichment.py b/tests/test_mb_enrichment.py index a1b8881..dd988af 100644 --- a/tests/test_mb_enrichment.py +++ b/tests/test_mb_enrichment.py @@ -28,6 +28,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_minigames_routes.py b/tests/test_minigames_routes.py index 89cc73e..8e06530 100644 --- a/tests/test_minigames_routes.py +++ b/tests/test_minigames_routes.py @@ -409,6 +409,7 @@ def test_db_uses_wal_journal_mode(setup_routes): row = conn.execute("PRAGMA journal_mode").fetchone() assert row[0] == "wal" finally: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_playlists_api.py b/tests/test_playlists_api.py index ae1caf6..8548746 100644 --- a/tests/test_playlists_api.py +++ b/tests/test_playlists_api.py @@ -18,6 +18,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_practice_suggestions.py b/tests/test_practice_suggestions.py index a858725..8983e8b 100644 --- a/tests/test_practice_suggestions.py +++ b/tests/test_practice_suggestions.py @@ -24,6 +24,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_profile_api.py b/tests/test_profile_api.py index db44e9b..f6c2fb5 100644 --- a/tests/test_profile_api.py +++ b/tests/test_profile_api.py @@ -21,6 +21,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_progression_api.py b/tests/test_progression_api.py index c363aa2..e995e5f 100644 --- a/tests/test_progression_api.py +++ b/tests/test_progression_api.py @@ -73,6 +73,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_scraper_options.py b/tests/test_scraper_options.py index 755baa9..03859a7 100644 --- a/tests/test_scraper_options.py +++ b/tests/test_scraper_options.py @@ -30,6 +30,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_settings_api.py b/tests/test_settings_api.py index 328afd6..e79b230 100644 --- a/tests/test_settings_api.py +++ b/tests/test_settings_api.py @@ -75,6 +75,7 @@ def client(tmp_path, monkeypatch): meta_db = getattr(server, "meta_db", None) conn = getattr(meta_db, "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -296,6 +297,7 @@ def server_module(tmp_path, monkeypatch): meta_db = getattr(mod, "meta_db", None) conn = getattr(meta_db, "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -324,6 +326,7 @@ def test_get_dlc_dir_uses_config_when_env_empty(tmp_path, monkeypatch): finally: conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -345,6 +348,7 @@ def test_get_dlc_dir_env_takes_precedence(tmp_path, monkeypatch): finally: conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -362,6 +366,7 @@ def test_get_dlc_dir_env_dot_is_valid(tmp_path, monkeypatch): finally: conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -404,6 +409,7 @@ def scan_module(tmp_path, monkeypatch, isolate_logging): yield mod conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -525,6 +531,7 @@ def api_client(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() _restore_loaded_plugins(plugins_snapshot) @@ -653,6 +660,7 @@ def test_skip_startup_tasks_does_not_call_load_plugins_or_scan(tmp_path, monkeyp finally: conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() _restore_loaded_plugins(plugins_snapshot) @@ -698,6 +706,7 @@ def test_skip_startup_tasks_clears_stale_plugin_registry(tmp_path, monkeypatch, finally: conn = getattr(getattr(server, "meta_db", None), "conn", None) if server else None if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() _restore_loaded_plugins(plugins_snapshot) diff --git a/tests/test_settings_export.py b/tests/test_settings_export.py index 069c9ad..b84aaa7 100644 --- a/tests/test_settings_export.py +++ b/tests/test_settings_export.py @@ -28,6 +28,7 @@ def server_mod(tmp_path, monkeypatch): yield mod conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_settings_export_library_db.py b/tests/test_settings_export_library_db.py index d6ad46c..3b43666 100644 --- a/tests/test_settings_export_library_db.py +++ b/tests/test_settings_export_library_db.py @@ -31,6 +31,7 @@ def server_mod(tmp_path, monkeypatch): yield mod conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -87,6 +88,7 @@ def test_export_includes_consistent_library_db_snapshot(client, server_mod, tmp_ "SELECT title FROM songs WHERE filename = ?", ("snap.archive",) ).fetchall() finally: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() assert rows == [("SnapSong",)] @@ -271,6 +273,7 @@ def test_full_db_backup_restore_round_trip(client, server_mod, tmp_path): "SELECT title FROM songs WHERE filename = ?", ("keepme.archive",) ).fetchall() finally: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() assert rows == [("KeepMe",)] assert not (tmp_path / "web_library.db.restore").exists() diff --git a/tests/test_settings_instrument.py b/tests/test_settings_instrument.py index 9abbf5c..ecbf707 100644 --- a/tests/test_settings_instrument.py +++ b/tests/test_settings_instrument.py @@ -19,6 +19,7 @@ def env(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_sloppak_cover_art.py b/tests/test_sloppak_cover_art.py index be8e66c..e359be4 100644 --- a/tests/test_sloppak_cover_art.py +++ b/tests/test_sloppak_cover_art.py @@ -116,6 +116,7 @@ def dlc_client(tmp_path, monkeypatch): meta_db = getattr(server, "meta_db", None) conn = getattr(meta_db, "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_sloppak_file_traversal.py b/tests/test_sloppak_file_traversal.py index c117711..e3f7140 100644 --- a/tests/test_sloppak_file_traversal.py +++ b/tests/test_sloppak_file_traversal.py @@ -45,6 +45,7 @@ def dlc_client(tmp_path, monkeypatch): meta_db = getattr(server, "meta_db", None) conn = getattr(meta_db, "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_song_stats_api.py b/tests/test_song_stats_api.py index 8d592d1..9e138ba 100644 --- a/tests/test_song_stats_api.py +++ b/tests/test_song_stats_api.py @@ -18,6 +18,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_song_user_meta.py b/tests/test_song_user_meta.py index 187109d..b549a60 100644 --- a/tests/test_song_user_meta.py +++ b/tests/test_song_user_meta.py @@ -21,6 +21,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_starter_suggestions.py b/tests/test_starter_suggestions.py index a281603..c6cdb9e 100644 --- a/tests/test_starter_suggestions.py +++ b/tests/test_starter_suggestions.py @@ -25,6 +25,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_startup_status.py b/tests/test_startup_status.py index 88c7446..b48b9b9 100644 --- a/tests/test_startup_status.py +++ b/tests/test_startup_status.py @@ -85,6 +85,7 @@ def client(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -127,6 +128,7 @@ def startup_harness(tmp_path, monkeypatch, isolate_logging): server._DEMO_JANITOR_THREAD = None conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -692,6 +694,7 @@ def test_startup_status_e2e_real_plugin_loader(tmp_path, monkeypatch, isolate_lo server._DEMO_JANITOR_THREAD = None conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() with plugins_mod.PLUGINS_LOCK: plugins_mod.LOADED_PLUGINS.clear() @@ -782,6 +785,7 @@ def test_startup_status_endpoint_background_thread_path(tmp_path, monkeypatch, i server._DEMO_JANITOR_THREAD = None conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() @@ -830,6 +834,7 @@ def test_startup_status_endpoint_background_thread_failure(tmp_path, monkeypatch server._DEMO_JANITOR_THREAD = None conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_version_endpoint.py b/tests/test_version_endpoint.py index 5b8499f..dbd4269 100644 --- a/tests/test_version_endpoint.py +++ b/tests/test_version_endpoint.py @@ -44,6 +44,7 @@ def client(tmp_path, monkeypatch): finally: conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_wanted_api.py b/tests/test_wanted_api.py index 9009cb2..22335d0 100644 --- a/tests/test_wanted_api.py +++ b/tests/test_wanted_api.py @@ -22,6 +22,7 @@ def server_mod(tmp_path, monkeypatch): yield mod conn = getattr(getattr(mod, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() diff --git a/tests/test_work_charts_api.py b/tests/test_work_charts_api.py index 51e5102..e28c065 100644 --- a/tests/test_work_charts_api.py +++ b/tests/test_work_charts_api.py @@ -20,6 +20,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) diff --git a/tests/test_work_grouping.py b/tests/test_work_grouping.py index 0403536..f8fd740 100644 --- a/tests/test_work_grouping.py +++ b/tests/test_work_grouping.py @@ -22,6 +22,7 @@ def server(tmp_path, monkeypatch, isolate_logging): finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: + getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None)