Skip to content

Commit 13312ff

Browse files
fix: isolate API unit app imports from worker contracts
Harden worker contract eviction to drop deepest API-shadowed modules first, and make job-poll unit tests reassert the API import root then clear cached API app modules so shared pytest runs stay green. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 01469e6 commit 13312ff

2 files changed

Lines changed: 69 additions & 7 deletions

File tree

apps/api/tests/unit/test_job_poll_session_hygiene.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import sys
6+
from pathlib import Path
57
from types import ModuleType, SimpleNamespace
68
from typing import Any
79
from unittest.mock import AsyncMock, MagicMock, patch
@@ -18,9 +20,49 @@
1820
configure_import_environment()
1921
ensure_import_paths()
2022

23+
_API_ROOT = str(Path(__file__).resolve().parents[2])
2124

22-
def _load_api_modules() -> tuple[ModuleType, ModuleType, ModuleType, ModuleType]:
25+
26+
def _prioritize_api_import_root() -> None:
27+
"""Keep apps/api ahead of apps/worker for the shared `app` package name."""
2328
ensure_import_paths()
29+
if _API_ROOT in sys.path:
30+
sys.path.remove(_API_ROOT)
31+
sys.path.insert(0, _API_ROOT)
32+
33+
34+
def _is_api_app_module(module: ModuleType | None) -> bool:
35+
if module is None:
36+
return False
37+
module_file = getattr(module, "__file__", None)
38+
if isinstance(module_file, str) and module_file.startswith(_API_ROOT):
39+
return True
40+
module_paths = getattr(module, "__path__", ())
41+
try:
42+
return any(str(path).startswith(_API_ROOT) for path in module_paths)
43+
except KeyError:
44+
return False
45+
46+
47+
def _drop_non_api_app_modules() -> None:
48+
for module_name in sorted(sys.modules, key=len, reverse=True):
49+
if module_name != "app" and not module_name.startswith("app."):
50+
continue
51+
if not _is_api_app_module(sys.modules.get(module_name)):
52+
sys.modules.pop(module_name, None)
53+
54+
55+
def _drop_api_app_modules() -> None:
56+
for module_name in sorted(sys.modules, key=len, reverse=True):
57+
if module_name != "app" and not module_name.startswith("app."):
58+
continue
59+
if _is_api_app_module(sys.modules.get(module_name)):
60+
sys.modules.pop(module_name, None)
61+
62+
63+
def _load_api_modules() -> tuple[ModuleType, ModuleType, ModuleType, ModuleType]:
64+
_prioritize_api_import_root()
65+
_drop_non_api_app_modules()
2466
from app.services.auth import api_key_authentication_service
2567
from app.services.rate_limit import (
2668
data_structures,
@@ -36,6 +78,14 @@ def _load_api_modules() -> tuple[ModuleType, ModuleType, ModuleType, ModuleType]
3678
)
3779

3880

81+
@pytest.fixture(autouse=True)
82+
def _clear_api_app_modules_after_unit_test():
83+
"""Avoid leaving API's `app` package cached for later worker contract tests."""
84+
yield
85+
_drop_api_app_modules()
86+
87+
88+
3989
@pytest.mark.asyncio
4090
async def test_get_tier_reuses_provided_session_without_get_db_context() -> None:
4191
_, _, _, tier_service = _load_api_modules()

apps/worker/tests/contract/conftest.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,14 @@ def _module_loaded_from(module_name: str, root: Path) -> bool:
3737
return True
3838

3939
module_paths = getattr(module, "__path__", ())
40-
return any(str(module_path).startswith(root_value) for module_path in module_paths)
40+
try:
41+
return any(
42+
str(module_path).startswith(root_value) for module_path in module_paths
43+
)
44+
except KeyError:
45+
# Namespace path iteration can raise if a parent package was already
46+
# removed from sys.modules mid-eviction.
47+
return False
4148

4249

4350
def _ensure_worker_import_context() -> None:
@@ -46,11 +53,16 @@ def _ensure_worker_import_context() -> None:
4653
sys.path.remove(worker_root_value)
4754
sys.path.insert(0, worker_root_value)
4855

49-
cached_module_names = list(sys.modules)
50-
for module_name in cached_module_names:
51-
if module_name == "app" or module_name.startswith("app."):
52-
if _module_loaded_from(module_name, _API_ROOT):
53-
sys.modules.pop(module_name, None)
56+
cached_module_names = [
57+
module_name
58+
for module_name in sys.modules
59+
if module_name == "app" or module_name.startswith("app.")
60+
]
61+
# Evict deepest modules first so namespace __path__ checks never observe a
62+
# missing parent `app` entry while walking API-shadowed packages.
63+
for module_name in sorted(cached_module_names, key=len, reverse=True):
64+
if _module_loaded_from(module_name, _API_ROOT):
65+
sys.modules.pop(module_name, None)
5466

5567

5668
@pytest.fixture(autouse=True)

0 commit comments

Comments
 (0)