diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 81a6ad4..09bdb98 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,6 +7,7 @@ stages: variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + CODELLENS_STRICT_COMMANDS: "1" cache: paths: diff --git a/scripts/commands/__init__.py b/scripts/commands/__init__.py index eaba8ac..5667d23 100644 --- a/scripts/commands/__init__.py +++ b/scripts/commands/__init__.py @@ -27,12 +27,20 @@ def get_all_commands(): import importlib import logging +_STRICT_COMMAND_IMPORTS = os.environ.get("CODELLENS_STRICT_COMMANDS", "").lower() in { + "1", + "true", + "yes", +} + _commands_dir = os.path.dirname(__file__) for fname in sorted(os.listdir(_commands_dir)): if fname.endswith('.py') and fname != '__init__.py': try: importlib.import_module(f'.{fname[:-3]}', package='commands') except Exception as e: + if _STRICT_COMMAND_IMPORTS: + raise logging.getLogger('codelens').error( f"Failed to import command module '{fname}': {e}" ) diff --git a/scripts/commands/self_analyze.py b/scripts/commands/self_analyze.py index 629ea48..ed620b2 100644 --- a/scripts/commands/self_analyze.py +++ b/scripts/commands/self_analyze.py @@ -192,10 +192,11 @@ def _compute_overall_health(analyses: Dict[str, Any]) -> Dict[str, Any]: } -# ─── Command Registration ───────────────────────────────────── - -COMMAND_INFO = { - "help": "Run CodeLens on its own codebase (dogfooding / meta-analysis)", - "add_args": add_args, - "execute": execute, -} +from commands import register_command + +register_command( + "self-analyze", + "Run CodeLens on its own codebase (dogfooding / meta-analysis)", + add_args, + execute, +) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..9541c5b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,6 @@ +"""Shared pytest configuration for CodeLens.""" + +import os + +# Fail fast when command modules fail to import during test runs. +os.environ.setdefault("CODELLENS_STRICT_COMMANDS", "1") diff --git a/tests/test_command_registry.py b/tests/test_command_registry.py new file mode 100644 index 0000000..f106860 --- /dev/null +++ b/tests/test_command_registry.py @@ -0,0 +1,65 @@ +"""Tests for command module registration and strict import behavior.""" + +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +SCRIPT_DIR = Path(__file__).resolve().parents[1] / "scripts" +COMMANDS_DIR = SCRIPT_DIR / "commands" + +if str(SCRIPT_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPT_DIR)) + +from commands import COMMAND_REGISTRY + + +def test_every_command_module_registers(): + """Each commands/*.py module must register at least one CLI command.""" + missing = [] + for module_path in sorted(COMMANDS_DIR.glob("*.py")): + if module_path.name == "__init__.py": + continue + + module_name = f"commands.{module_path.stem}" + registered = [ + name + for name, info in COMMAND_REGISTRY.items() + if getattr(info["execute"], "__module__", None) == module_name + ] + if not registered: + missing.append(module_path.name) + + assert not missing, ( + "Command modules without register_command(): " + + ", ".join(missing) + ) + + +def test_strict_command_imports_fail_fast_on_broken_module(): + """CODELLENS_STRICT_COMMANDS=1 should surface broken command imports.""" + broken_module = COMMANDS_DIR / "_test_broken_import.py" + broken_module.write_text("def broken(\n", encoding="utf-8") + env = os.environ.copy() + env["CODELLENS_STRICT_COMMANDS"] = "1" + env["PYTHONPATH"] = str(SCRIPT_DIR) + + try: + result = subprocess.run( + [ + sys.executable, + "-c", + "import importlib; importlib.import_module('commands')", + ], + cwd=SCRIPT_DIR, + env=env, + capture_output=True, + text=True, + ) + finally: + broken_module.unlink(missing_ok=True) + + assert result.returncode != 0 + assert "SyntaxError" in result.stderr or "_test_broken_import" in result.stderr