"""Tests that the Main Thread Purity Invariant holds for refactored modules. Per spec.md:2.2 Layer 1, the main thread (the one that enters immapp.run()) must NEVER import a module heavier than imgui_bundle and the lean gui_2 skeleton. Heavy imports are loaded by the AppController's _io_pool warmup and accessed via _require_warmed() at use sites. This is the static enforcement test. The runtime audit hook test (sys.addaudithook) is the empirical check; see tests/test_main_thread_purity_runtime.py for the runtime version (Phase 8 T8.2). This test uses scripts/audit_main_thread_imports.py (built in T1.4) to verify that the modules we refactored in Phases 3-7 contribute ZERO new violations to the main-thread import graph. The test is RED before each phase's refactor, GREEN after. For each refactored file, the test checks the top-level imports against the heavy denylist. If any of these files still have top-level heavy imports, the test FAILS - that means the refactor is incomplete or a future commit re-introduced a heavy import. """ import subprocess import sys import textwrap from pathlib import Path ROOT = Path(__file__).resolve().parent.parent def _run_in_subprocess(snippet: str) -> subprocess.CompletedProcess: script = textwrap.dedent(snippet) return subprocess.run( [sys.executable, "-c", script], capture_output=True, text=True, cwd=str(ROOT), timeout=30, ) # Heavy modules that must NOT be top-level imports in any main-thread- # reachable file. Mirrors the spec's heavy denylist. HEAVY_DENYLIST = { "google.genai", "anthropic", "openai", "requests", "google.genai.types", "fastapi", "fastapi.security.api_key", "src.command_palette", "src.theme_nerv", "src.theme_nerv_fx", "src.markdown_table", "numpy", "tkinter", "tomli_w", } # Files that the refactor targets. Each must have ZERO top-level imports # of any module in HEAVY_DENYLIST. REFACTOR_TARGETS = [ "src/ai_client.py", # Phase 3 "src/app_controller.py", # Phase 4 "src/commands.py", # Phase 5A "src/theme_2.py", # Phase 5B "src/markdown_helper.py", # Phase 5C "src/gui_2.py", # Phase 5D ] def _check_file_violations(file_path: str) -> str: """Run a subprocess that AST-checks the file for heavy top-level imports. Returns the stdout of the subprocess (which lists any violations found).""" res = _run_in_subprocess(f""" import ast from pathlib import Path root = Path('.').resolve() path = root / {file_path!r} if not path.exists(): print('FILE_NOT_FOUND') else: tree = ast.parse(path.read_text(encoding='utf-8')) heavy = {sorted(HEAVY_DENYLIST)!r} violations = [] for node in tree.body: if isinstance(node, ast.Import): for alias in node.names: for h in heavy: if alias.name == h or alias.name.startswith(h + '.'): violations.append(f'{{alias.name}} @ line {{node.lineno}}') elif isinstance(node, ast.ImportFrom): if node.module: for h in heavy: if node.module == h or node.module.startswith(h + '.'): violations.append(f'{{node.module}} @ line {{node.lineno}}') for v in violations: print('VIOLATION:', v) if not violations: print('CLEAN') """) return res.stdout def test_ai_client_has_no_heavy_top_level_imports() -> None: out = _check_file_violations("src/ai_client.py") assert "VIOLATION" not in out, f"ai_client.py has heavy top-level imports: {out}" def test_app_controller_has_no_heavy_top_level_imports() -> None: out = _check_file_violations("src/app_controller.py") assert "VIOLATION" not in out, f"app_controller.py has heavy top-level imports: {out}" def test_commands_has_no_heavy_top_level_imports() -> None: out = _check_file_violations("src/commands.py") assert "VIOLATION" not in out, f"commands.py has heavy top-level imports: {out}" def test_theme_2_has_no_heavy_top_level_imports() -> None: out = _check_file_violations("src/theme_2.py") assert "VIOLATION" not in out, f"theme_2.py has heavy top-level imports: {out}" def test_markdown_helper_has_no_heavy_top_level_imports() -> None: out = _check_file_violations("src/markdown_helper.py") assert "VIOLATION" not in out, f"markdown_helper.py has heavy top-level imports: {out}" def test_gui_2_has_no_heavy_top_level_imports() -> None: out = _check_file_violations("src/gui_2.py") assert "VIOLATION" not in out, f"gui_2.py has heavy top-level imports: {out}" def test_refactor_targets_summary() -> None: """Aggregate check: all 6 refactored files are clean.""" summary = [] for f in REFACTOR_TARGETS: out = _check_file_violations(f) clean = "CLEAN" in out summary.append((f, clean)) all_clean = all(c for _, c in summary) assert all_clean, f"Not all refactored files are clean: {summary}"