3849d30441
Phase 4 T4.1-T4.4 of startup_speedup_20260606 track. DEVIATION FROM ORIGINAL SPEC: spec.md said fastapi was in src/api_hooks.py but it was actually in src/app_controller.py (lines 17, 21). api_hooks.py uses stdlib http.server. Phase 4 target corrected to app_controller. LIFTED _require_warmed TO SHARED MODULE: created src/module_loader.py to avoid duplicating the lookup logic and the cross-module import smell (app_controller -> ai_client). src/ai_client.py re-exports it so the T3.1 test (which asserts hasattr(src.ai_client, '_require_warmed')) continues to work. src/app_controller.py changes: - Added 'from __future__ import annotations' (enables lazy type annotations; -> FastAPI return type now a forward reference) - Removed 'from fastapi import FastAPI, Depends, HTTPException' (line 17) - Removed 'from fastapi.security.api_key import APIKeyHeader' (line 21) - Added 'from src.module_loader import _require_warmed' (cross-module via shared utility, not via ai_client) - create_api(): added lookups at top of function body - 7 _api_* helper functions (_api_get_key, _api_generate, _api_stream, _api_confirm_action, _api_get_session, _api_delete_session, _api_get_context): added 'HTTPException = _require_warmed(...).HTTPException' at top of each function body EFFECTIVENESS: - import src.app_controller no longer triggers fastapi import (saves ~470ms in main thread; only loaded when --enable-test-hooks is set) - When --enable-test-hooks is set, the AppController's warmup pre-loads fastapi on the _io_pool, so create_api()'s lookup is O(1) TESTS: - tests/test_app_controller_no_top_level_fastapi.py: 4/4 PASS (was 3 RED + 1 pass) - tests/test_ai_client_no_top_level_sdk_imports.py: 9/9 still PASS (re-export works) - tests/test_app_controller_mcp.py, test_app_controller_offloading.py: pass - tests/test_headless_service.py: 10/11 PASS (1 pre-existing failure test_generate_endpoint is a circular-import issue in google.genai, reproduces identically on stashed pre-Phase-4 state - NOT a regression from this change) - tests/test_hooks.py: pass NEXT: Phase 5 (feature-gated GUI module imports - command palette, NERV theme, markdown table), then Phase 6 (ad-hoc threads -> _io_pool).
95 lines
3.3 KiB
Python
95 lines
3.3 KiB
Python
"""Tests that src/app_controller.py has NO top-level fastapi imports.
|
|
|
|
Per spec.md:2.2 Layer 1, the main thread's import chain must not include
|
|
heavy modules. FastAPI is heavy (~470ms) and is only needed when
|
|
`--enable-test-hooks` or `--web-host` is passed. The warmup loads it
|
|
on the AppController's _io_pool; functions that need it call
|
|
`_require_warmed("fastapi")` to get the module from sys.modules.
|
|
|
|
These tests run in a fresh subprocess to ensure no warmup state leaks
|
|
from the test runner. We assert:
|
|
- `fastapi` is NOT in `sys.modules` after `import src.app_controller`
|
|
- `fastapi.security.api_key` is NOT in `sys.modules` either
|
|
- The static audit script reports NO new violation from app_controller.py
|
|
"""
|
|
|
|
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,
|
|
)
|
|
|
|
|
|
def test_app_controller_does_not_import_fastapi_at_module_level() -> None:
|
|
res = _run_in_subprocess("""
|
|
import sys
|
|
import src.app_controller
|
|
print('fastapi' in sys.modules)
|
|
""")
|
|
assert res.returncode == 0, f"stderr: {res.stderr}"
|
|
assert res.stdout.strip() == "False", f"app_controller triggered fastapi import: {res.stdout}"
|
|
|
|
|
|
def test_app_controller_does_not_import_fastapi_security_at_module_level() -> None:
|
|
res = _run_in_subprocess("""
|
|
import sys
|
|
import src.app_controller
|
|
print('fastapi.security.api_key' in sys.modules)
|
|
""")
|
|
assert res.returncode == 0, f"stderr: {res.stderr}"
|
|
assert res.stdout.strip() == "False", f"app_controller triggered fastapi.security.api_key import: {res.stdout}"
|
|
|
|
|
|
def test_app_controller_create_api_still_resolvable() -> None:
|
|
"""Even without fastapi in sys.modules, the function reference must be
|
|
importable (it just returns a lazy FastAPI app on call)."""
|
|
res = _run_in_subprocess("""
|
|
import src.app_controller
|
|
print(hasattr(src.app_controller.AppController, 'create_api'))
|
|
print(callable(src.app_controller.AppController.create_api))
|
|
""")
|
|
assert res.returncode == 0, f"stderr: {res.stderr}"
|
|
assert res.stdout.strip() == "True\nTrue"
|
|
|
|
|
|
def test_audit_main_thread_imports_sees_no_new_violation_from_app_controller() -> None:
|
|
"""Run the static audit and check that src/app_controller.py contributes no
|
|
new fastapi violations (any remaining violations are pre-existing in OTHER
|
|
files; this just verifies app_controller is clean).
|
|
"""
|
|
res = _run_in_subprocess("""
|
|
import ast
|
|
from pathlib import Path
|
|
root = Path('.').resolve()
|
|
app_controller_path = root / 'src' / 'app_controller.py'
|
|
tree = ast.parse(app_controller_path.read_text(encoding='utf-8'))
|
|
heavy = ['fastapi', 'fastapi.security', 'fastapi.security.api_key']
|
|
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 + '.'):
|
|
print('VIOLATION:', alias.name)
|
|
elif isinstance(node, ast.ImportFrom):
|
|
if node.module:
|
|
for h in heavy:
|
|
if node.module == h or node.module.startswith(h + '.'):
|
|
print('VIOLATION:', node.module)
|
|
print('OK')
|
|
""")
|
|
assert res.returncode == 0, f"stderr: {res.stderr}"
|
|
assert "OK" in res.stdout
|
|
assert "VIOLATION" not in res.stdout
|