Private
Public Access
0
0
Files
manual_slop/tests/test_main_thread_purity.py
T
ed 61d21c70bb refactor(app_controller): remove requests + tomli_w top-level imports; add main thread purity test
Phase 8 of startup_speedup_20260606 track.

Part 1: app_controller.py cleanup
- Removed 'import requests' (was used in 2 places - lazy import added inside)
- Removed 'import tomli_w' (dead import; never referenced in app_controller)
- Migrated 2 threading.Thread spawns to use self.submit_io (the do_post
  closures in _handle_approve_ask and _handle_reject_ask)

Part 2: Main thread purity enforcement test
- tests/test_main_thread_purity.py: 7 tests verify that the 6 refactored
  files (ai_client, app_controller, commands, theme_2, markdown_helper,
  gui_2) have ZERO top-level imports from the 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}

This is the static enforcement (the runtime audit-hook test using
sys.addaudithook is a follow-up).

The test is RED before each refactor phase, GREEN after. If a future
commit re-introduces a heavy import in one of these files, the test
fails immediately in CI.

TESTS:
- 7/7 main thread purity tests PASS
- 15/15 log + app controller tests still PASS (no breakage from
  removing requests/tomli_w imports)
2026-06-06 18:01:39 -04:00

135 lines
4.6 KiB
Python

"""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}"