Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 13 additions & 37 deletions scripts/codelens.py
Original file line number Diff line number Diff line change
Expand Up @@ -1021,43 +1021,6 @@ def main():
if getattr(args, 'lite', False) and isinstance(result, dict):
result = _apply_lite(result, args.command)

# ─── Post-processing: --deep (hybrid LSP analysis) ──
if getattr(args, 'deep', False) and isinstance(result, dict):
try:
from hybrid_engine import HybridEngine
engine = HybridEngine(workspace, deep=True)
if engine.lsp_active:
if args.command == "dead-code":
items = result.get("results", {})
if isinstance(items, dict):
for cat, cat_items in items.items():
if isinstance(cat_items, list):
items[cat] = engine.verify_dead_code(cat_items)
result["deep_analysis"] = True
result["confidence_distribution"] = compute_confidence_distribution_flat(result)
elif args.command == "query":
result = engine.enhance_query(result, getattr(args, 'name', ''))
result["deep_analysis"] = True
elif args.command == "impact":
result = engine.enhance_impact(result, getattr(args, 'name', ''))
result["deep_analysis"] = True
elif args.command == "smell":
findings = result.get("findings", [])
if findings:
result["findings"] = engine.verify_dead_code(findings)
result["deep_analysis"] = True
result["confidence_distribution"] = compute_confidence_distribution_flat(result)
else:
result["deep_analysis"] = False
result["deep_analysis_hint"] = f"--deep not yet supported for {args.command}"
engine.cleanup()
else:
result["deep_analysis"] = False
result["deep_analysis_hint"] = "No LSP server available. Install one (run: codelens --lsp-status)"
except ImportError:
result["deep_analysis"] = False
result["deep_analysis_hint"] = "Hybrid engine not available (hybrid_engine.py not found)"

# ─── Post-processing: --max-tokens N ──
max_tokens = getattr(args, 'max_tokens', None)
if max_tokens and isinstance(result, dict):
Expand All @@ -1068,6 +1031,13 @@ def main():
result["_auto_setup"] = auto_setup_info

# ─── Post-processing: --deep (hybrid LSP analysis) ──
# Single consolidated block (issue #32: previously two duplicate blocks
# ran in sequence, double-instantiating HybridEngine and overwriting
# deep_analysis/lsp_active fields. Block 1 used HybridEngine() directly;
# Block 2 used create_hybrid_engine() + add_confidence_to_result().
# Block 2 is strictly more capable (handles complexity, adds confidence
# distribution), so Block 1 was deleted and the "unsupported command"
# hint was folded into the else branch below.)
deep = getattr(args, 'deep', False)
if deep and isinstance(result, dict) and args.command in (
"dead-code", "query", "impact", "smell", "complexity"
Expand Down Expand Up @@ -1119,6 +1089,12 @@ def main():
if isinstance(result, dict):
result["lsp_active"] = False
result["deep_error"] = str(e)
elif deep and isinstance(result, dict):
# --deep set but command not in the supported list — surface a hint
# so the user knows --deep was a no-op for this command. (Folded in
# from deleted Block 1 — see issue #32.)
result["deep_analysis"] = False
result["deep_analysis_hint"] = f"--deep not yet supported for {args.command}"
elif not deep and isinstance(result, dict) and args.command in (
"dead-code", "query", "impact", "smell", "complexity"
):
Expand Down
111 changes: 111 additions & 0 deletions tests/test_hybrid_engine.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,114 @@ def test_deep_graceful_degradation(self):
capture_output=True, text=True
)
assert result.returncode == 0


# ─── Issue #32: --deep must invoke create_hybrid_engine exactly once ────


class TestDeepSingleInvocation:
"""Regression guard for issue #32.

Before the fix, ``codelens.py`` had two duplicate ``--deep``
post-processing blocks that both ran when ``--deep`` was set for
``dead-code``, ``query``, ``impact``, ``smell``, ``complexity``.
Each block instantiated a fresh HybridEngine, so the engine was
created twice per ``--deep`` invocation — doubling LSP subprocess
calls and potentially double-counting findings in
``confidence_distribution``.

These tests assert ``create_hybrid_engine`` is invoked exactly once
per ``--deep`` CLI call, using ``unittest.mock.patch`` to count
``call_args``.
"""

def test_smell_deep_invokes_create_hybrid_engine_once(self):
"""``codelens smell --deep`` must call create_hybrid_engine exactly once.

Calls ``main()`` in-process (not via subprocess) so the mock is
visible to the code under test. Subprocess mocks don't work
across process boundaries.
"""
import tempfile
import shutil
from unittest.mock import patch, MagicMock

ws = tempfile.mkdtemp()
try:
# Minimal Python file so smell has something to analyze
with open(os.path.join(ws, "test.py"), "w") as f:
f.write("def foo():\n pass\n")

# Pre-build registry so auto-setup doesn't interfere
from commands.scan import cmd_scan
cmd_scan(ws)

# Patch create_hybrid_engine to count calls without actually
# starting LSP subprocesses (which would be slow + flaky in CI).
with patch("hybrid_engine.create_hybrid_engine") as mock_create:
mock_engine = MagicMock()
mock_engine.lsp_active = False
mock_create.return_value = mock_engine

# Call main() in-process with patched argv so the mock
# is visible. Redirect stdout to suppress JSON output.
old_argv = sys.argv
import io
old_stdout = sys.stdout
sys.argv = ["codelens.py", "smell", ws, "--deep", "--format", "json"]
sys.stdout = io.StringIO()
try:
from codelens import main
main()
except SystemExit:
# smell shouldn't sys.exit, but catch just in case
pass
finally:
sys.argv = old_argv
sys.stdout = old_stdout

assert mock_create.call_count == 1, (
f"Expected create_hybrid_engine to be called exactly once, "
f"got {mock_create.call_count} calls. This indicates the "
f"duplicate --deep block from issue #32 has regressed."
)
finally:
shutil.rmtree(ws, ignore_errors=True)

def test_deep_unsupported_command_sets_hint(self):
"""``--deep`` on an unsupported command must set deep_analysis_hint, not crash."""
import tempfile
import shutil

ws = tempfile.mkdtemp()
try:
with open(os.path.join(ws, "test.py"), "w") as f:
f.write("x = 1\n")

from commands.scan import cmd_scan
cmd_scan(ws)

# symbols is NOT in the --deep supported list
proc = subprocess.run(
[sys.executable, "scripts/codelens.py",
"symbols", "foo", ws, "--deep", "--format", "json"],
capture_output=True, text=True, timeout=60,
env={**os.environ, "PYTHONPATH": "scripts"},
)

# Should not crash, and should include the hint
assert proc.returncode == 0, f"Command failed: {proc.stderr}"
import json as _json
output = _json.loads(proc.stdout)
assert output.get("deep_analysis") is False, (
f"deep_analysis should be False for unsupported command, "
f"got: {output.get('deep_analysis')}"
)
assert "deep_analysis_hint" in output, (
"deep_analysis_hint must be set for unsupported --deep command"
)
assert "symbols" in output["deep_analysis_hint"], (
f"hint should mention the command name, got: {output['deep_analysis_hint']}"
)
finally:
shutil.rmtree(ws, ignore_errors=True)
Loading