""" S1.3 — LLM isolation gate. Confirms that markitect.llm.* has zero imports from markitect.prompts.* or markitect.infospace.*, making the module safe to extract into a standalone llm-connect library. These tests must pass before extraction (S2). """ import importlib import pkgutil import sys from pathlib import Path def _collect_llm_modules() -> list[str]: """Return fully-qualified names of all modules under markitect.llm.""" import markitect.llm as pkg pkg_path = Path(pkg.__file__).parent names = [] for info in pkgutil.walk_packages([str(pkg_path)], prefix="markitect.llm."): names.append(info.name) # Include the package itself names.insert(0, "markitect.llm") return names def _direct_imports(module_name: str) -> set[str]: """Return set of top-level module names imported by *module_name*.""" mod = importlib.import_module(module_name) src_file = getattr(mod, "__file__", None) if not src_file or not src_file.endswith(".py"): return set() imports: set[str] = set() with open(src_file) as f: for line in f: stripped = line.strip() if stripped.startswith("from ") or stripped.startswith("import "): # Extract the root package of the imported name parts = stripped.split() if parts[0] == "from" and len(parts) >= 2: imports.add(parts[1].split(".")[0] + "." + parts[1].split(".")[1] if "." in parts[1] else parts[1]) # Also capture full dotted path for cross-module check imports.add(parts[1]) return imports def _import_lines(src_file: str) -> list[str]: """Return only import-statement lines from a Python source file.""" lines = [] with open(src_file) as f: for line in f: stripped = line.strip() if stripped.startswith("from ") or stripped.startswith("import "): lines.append(stripped) return lines def test_no_prompts_import_in_llm_tree(): """markitect.llm must not import anything from markitect.prompts.*""" violations = [] for mod_name in _collect_llm_modules(): try: mod = importlib.import_module(mod_name) except ImportError: continue src_file = getattr(mod, "__file__", None) if not src_file or not src_file.endswith(".py"): continue for line in _import_lines(src_file): if "markitect.prompts" in line: violations.append(mod_name) break assert violations == [], ( f"These llm modules still import from markitect.prompts: {violations}" ) def test_no_infospace_import_in_llm_tree(): """markitect.llm must not import anything from markitect.infospace.*""" violations = [] for mod_name in _collect_llm_modules(): try: mod = importlib.import_module(mod_name) except ImportError: continue src_file = getattr(mod, "__file__", None) if not src_file or not src_file.endswith(".py"): continue for line in _import_lines(src_file): if "markitect.infospace" in line: violations.append(mod_name) break assert violations == [], ( f"These llm modules still import from markitect.infospace: {violations}" ) def test_runconfig_and_llmresponse_canonical_in_llm(): """RunConfig and LLMResponse must be defined in markitect.llm.models.""" from markitect.llm.models import RunConfig, LLMResponse assert RunConfig.__module__ == "markitect.llm.models", ( f"RunConfig.module = {RunConfig.__module__!r}, expected 'markitect.llm.models'" ) assert LLMResponse.__module__ == "markitect.llm.models", ( f"LLMResponse.module = {LLMResponse.__module__!r}, expected 'markitect.llm.models'" ) def test_llmadapter_canonical_in_llm(): """LLMAdapter must be defined in markitect.llm.adapter.""" from markitect.llm.adapter import LLMAdapter assert LLMAdapter.__module__ == "markitect.llm.adapter", ( f"LLMAdapter.module = {LLMAdapter.__module__!r}, expected 'markitect.llm.adapter'" ) def test_backward_compat_prompts_reexport(): """markitect.prompts.execution.models must still export RunConfig/LLMResponse.""" from markitect.prompts.execution.models import RunConfig, LLMResponse from markitect.llm.models import RunConfig as RC, LLMResponse as LR assert RunConfig is RC, "prompts re-export RunConfig must be the same object as llm.models.RunConfig" assert LLMResponse is LR, "prompts re-export LLMResponse must be the same object as llm.models.LLMResponse" def test_backward_compat_llmadapter_reexport(): """markitect.prompts.execution.llm_adapter must still export LLMAdapter.""" from markitect.prompts.execution.llm_adapter import LLMAdapter from markitect.llm.adapter import LLMAdapter as LA assert LLMAdapter is LA, "prompts re-export LLMAdapter must be the same object as llm.adapter.LLMAdapter" def test_app_name_parameterization(): """resolve_llm(app_name=X) uses ~/.config/X/config.toml and X_HELPER_MODEL.""" from markitect.llm.toml_config import ( _model_env_var, _user_config_path, _dir_config_name, resolve_llm, ) assert _model_env_var("railiance") == "RAILIANCE_HELPER_MODEL" assert _model_env_var("markitect") == "MARKITECT_HELPER_MODEL" assert str(_user_config_path("railiance")).endswith(".config/railiance/config.toml") assert _dir_config_name("railiance") == ".railiance.toml" # Smoke: resolve falls back to hardcoded for unknown app r = resolve_llm(app_name="nonexistent_app_xyz") assert r.provider_source == "hardcoded" assert r.model_source == "hardcoded"