From e60b5f6575277faaedfedb2817ee9d594dee13dd Mon Sep 17 00:00:00 2001 From: syf2211 Date: Sun, 28 Jun 2026 11:04:58 +0000 Subject: [PATCH 1/2] fix(commands): fail fast on broken imports and enforce registry coverage Add CODELLENS_STRICT_COMMANDS for CI/dev fail-fast when a command module fails to import, plus a meta-test ensuring every commands/*.py registers at least one CLI command. Also register the missing self-analyze command. Fixes #39 --- .gitlab-ci.yml | 1 + scripts/commands/__init__.py | 8 ++++ scripts/commands/self_analyze.py | 15 ++++---- tests/conftest.py | 6 +++ tests/test_command_registry.py | 65 ++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_command_registry.py 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 From d3693cbcdae053050c8597c0a205ae78d5c0a72e Mon Sep 17 00:00:00 2001 From: Wolfvin Date: Sun, 28 Jun 2026 16:50:22 +0000 Subject: [PATCH 2/2] fix(commands): correct CODELLENS -> CODELENS typo in strict import env var syf2211's PR #69 had the env var spelled CODELLENS_STRICT_COMMANDS (double L) in all 4 files. Issue #39 specifies CODELENS_STRICT_COMMANDS (single L). Anyone reading the issue and setting the env var with the correct spelling would get no effect - strict mode silently wouldn't activate. This fixup branch is based on syf2211's work (credited) with only the typo corrected. All other changes (strict mode fail-fast, self_analyze registration fix, meta-test, CI integration) are unchanged. Closes #39 Supersedes #69 --- .gitignore | 1 + .gitlab-ci.yml | 2 +- scripts/commands/__init__.py | 2 +- tests/conftest.py | 2 +- tests/test_command_registry.py | 4 ++-- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index fe62674..406964b 100755 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ Thumbs.db # Debug *.log +uv.lock diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 09bdb98..913a30f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,7 +7,7 @@ stages: variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" - CODELLENS_STRICT_COMMANDS: "1" + CODELENS_STRICT_COMMANDS: "1" cache: paths: diff --git a/scripts/commands/__init__.py b/scripts/commands/__init__.py index 5667d23..8681737 100644 --- a/scripts/commands/__init__.py +++ b/scripts/commands/__init__.py @@ -27,7 +27,7 @@ def get_all_commands(): import importlib import logging -_STRICT_COMMAND_IMPORTS = os.environ.get("CODELLENS_STRICT_COMMANDS", "").lower() in { +_STRICT_COMMAND_IMPORTS = os.environ.get("CODELENS_STRICT_COMMANDS", "").lower() in { "1", "true", "yes", diff --git a/tests/conftest.py b/tests/conftest.py index 9541c5b..8d0ae19 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,4 +3,4 @@ import os # Fail fast when command modules fail to import during test runs. -os.environ.setdefault("CODELLENS_STRICT_COMMANDS", "1") +os.environ.setdefault("CODELENS_STRICT_COMMANDS", "1") diff --git a/tests/test_command_registry.py b/tests/test_command_registry.py index f106860..82ba576 100644 --- a/tests/test_command_registry.py +++ b/tests/test_command_registry.py @@ -39,11 +39,11 @@ def test_every_command_module_registers(): def test_strict_command_imports_fail_fast_on_broken_module(): - """CODELLENS_STRICT_COMMANDS=1 should surface broken command imports.""" + """CODELENS_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["CODELENS_STRICT_COMMANDS"] = "1" env["PYTHONPATH"] = str(SCRIPT_DIR) try: