Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Stage 1 — Decouple:
- Move RunConfig + LLMResponse to markitect/llm/models.py (canonical)
- Move LLMAdapter + Mock/ErrorLLMAdapter to markitect/llm/adapter.py
- markitect/prompts/execution/models.py and llm_adapter.py become re-export shims
- All 4 adapters + factory.py updated to import from markitect.llm.*
- Parameterize app_name in toml_config.py (resolve_llm, get_default_layers,
get_preference_layers): paths and env var now derived from app_name arg
- Add tests/test_llm_isolation.py: 7 isolation + backward-compat tests
Stage 2 — Extract:
- Standalone llm-connect package created at ~/llm-connect/
- All 18 llm files copied; markitect.* imports replaced with llm_connect.*
- LLMError base inlined in llm_connect/exceptions.py (no markitect dep)
- llm-connect installed into markitect-venv; declared in pyproject.toml
Smoke test: markitect llm-check succeeds (live Gemini API call).
Backward compat: markitect.prompts.execution.{models,llm_adapter} still work.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
160 lines
5.9 KiB
Python
160 lines
5.9 KiB
Python
"""
|
|
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"
|