From c87c563925f90f631e41f0bfe62b74e5189b1517 Mon Sep 17 00:00:00 2001 From: Wolfvin Date: Sat, 4 Jul 2026 07:53:48 +0000 Subject: [PATCH] chore(commands): remove 32 deprecated aliases (#199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #199: remove the 32 deprecated CLI aliases retained for one version after PR #195's 78→12 consolidation. The aliases (affected, arch-metrics, architecture, binary-scan, circular, complexity, dashboard, dataflow, dead-code, dependents, diff, env-check, git-status, graph-schema, import-snapshot, init, lsp-status, orient, outline, ownership, perf-hint, query-graph, regex-audit, secrets, semantic-query, side-effect, smell, staleness, symbols, taint, trace, vuln-scan) were hidden wrappers that printed a deprecation warning then redirected to the new umbrella command. Changes: - Removed register_command() calls from 30 alias modules in scripts/commands/. The modules themselves are kept because the 12 umbrella commands import them via importlib for their --check sub-analyses (e.g. audit.py imports commands.dead_code, security.py imports commands.secrets). - Deleted 2 alias modules that no umbrella imports: symbols.py, semantic_query.py (semantic search is reached via 'search --mode semantic'). - Removed deprecated_alias_for parameter from register_command() and the deprecation-warning dispatch block in codelens.py. - Updated README deprecated aliases section. - Updated tests that exclusively tested deprecated-alias behavior. MCP server fix (scripts/mcp_server.py): - Added importlib fallback in _execute_command() so MCP tools that dispatch to the now-unregistered alias modules (codelens_architecture, codelens_secrets, codelens_symbols, etc.) keep working. The CLI alias is gone (argparse rejects with 'invalid choice'), but the MCP surface is preserved. --diff-base post-filter fix (scripts/codelens.py): - Extended the post-filter to handle umbrella commands. The old logic checked args.command against alias names; now it also recognizes the umbrella names (security, audit, impact, context, deps) and filters each sub-result in result['r'] individually, attaching the diff_scope summary at the top level. --deep hybrid analysis fix (scripts/codelens.py): - Extended --deep post-processing to handle 'audit --check ' by mapping the check name back to the alias name (smell, dead-code, complexity) and targeting the enhancement at the matching sub-result in result['r']. DoD verification: - codelens --command-count → 12 ✓ - codelens dead-code / secrets / symbols → 'invalid choice' ✓ - codelens --help shows only the 12 umbrella commands ✓ - README deprecated aliases section updated ✓ Test results (vs baseline at 7fa4096): - 0 new regressions introduced - 4 pre-existing failures fixed (test_cli argparse -f conflict on 'affected'; test_compact_format MCP graph_schema tool visibility) - 18 pre-existing failures unchanged (test_doctor, test_search_pagination, test_confidence, etc. — unrelated to #199) Findings (out of scope, flagged for BOS): - DoD #1 says 'delete all 32 .py files'. This is impossible without breaking the umbrella commands, which import these modules via importlib for their --check sub-analyses. The approach taken here (remove the registration, keep the module) achieves the user-facing DoD (#2, #3, #4, #5) while preserving the implementation. --- README.md | 6 +- scripts/codelens.py | 185 ++++++++++++++++----------- scripts/commands/__init__.py | 33 ++--- scripts/commands/affected.py | 10 +- scripts/commands/arch_metrics.py | 10 +- scripts/commands/architecture.py | 11 +- scripts/commands/binary_scan.py | 10 +- scripts/commands/circular.py | 9 +- scripts/commands/complexity.py | 9 +- scripts/commands/dashboard.py | 9 +- scripts/commands/dataflow.py | 7 +- scripts/commands/dead_code.py | 9 +- scripts/commands/dependents.py | 9 +- scripts/commands/diff.py | 11 +- scripts/commands/env_check.py | 9 +- scripts/commands/git_status.py | 10 +- scripts/commands/graph_schema.py | 10 +- scripts/commands/import_snapshot.py | 11 +- scripts/commands/init.py | 9 +- scripts/commands/lsp_status.py | 10 +- scripts/commands/orient.py | 11 +- scripts/commands/outline.py | 9 +- scripts/commands/ownership.py | 9 +- scripts/commands/perf_hint.py | 9 +- scripts/commands/query_graph.py | 10 +- scripts/commands/regex_audit.py | 9 +- scripts/commands/secrets.py | 9 +- scripts/commands/semantic_query.py | 68 ---------- scripts/commands/side_effect.py | 9 +- scripts/commands/smell.py | 9 +- scripts/commands/staleness.py | 10 +- scripts/commands/symbols.py | 51 -------- scripts/commands/taint.py | 9 +- scripts/commands/trace.py | 9 +- scripts/commands/vuln_scan.py | 9 +- scripts/mcp_server.py | 35 ++++- tests/test_affected_command.py | 21 ++- tests/test_cli.py | 74 +++-------- tests/test_command_registry.py | 20 +++ tests/test_compact_format.py | 20 ++- tests/test_diff_scope.py | 15 ++- tests/test_hybrid_engine.py | 18 ++- tests/test_issue195_consolidation.py | 108 +++++++--------- tests/test_query_graph.py | 41 +++--- tests/test_secrets_gitleaks.py | 26 ++-- tests/test_semantic_search_engine.py | 28 ++-- tests/test_staleness.py | 87 ++++++++++--- 47 files changed, 445 insertions(+), 675 deletions(-) delete mode 100644 scripts/commands/semantic_query.py delete mode 100644 scripts/commands/symbols.py diff --git a/README.md b/README.md index 554918a5..ceffb179 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ python3 scripts/codelens.py query "myFunction" --lite ## Command Reference -CodeLens consolidates 78 legacy commands into **12 focused umbrella commands** (issue #195). Each umbrella command accepts a `--check ` flag to select a specific sub-analysis, or runs all sub-analyses by default. Legacy command names still work as deprecated aliases (backward compat for one version) but print a redirect warning to stderr. +CodeLens consolidates 78 legacy commands into **12 focused umbrella commands** (issue #195). Each umbrella command accepts a `--check ` flag to select a specific sub-analysis, or runs all sub-analyses by default. The 32 deprecated aliases retained for one version after #195 have now been removed (issue #199) — see [Deprecated Aliases](#deprecated-aliases) below. ### The 12 Umbrella Commands @@ -95,9 +95,9 @@ CodeLens consolidates 78 legacy commands into **12 focused umbrella commands** ( | `history [workspace] [--check history\|ownership\|git-status]` | history, ownership, git-status | Historical trends, code ownership, git scan state. Default `--check history`. | | `graph [workspace] "Cypher query"` | query-graph (raw Cypher) | Raw Cypher-subset graph query for power users. Casual callers should prefer `search --mode graph`. | -### Deprecated Aliases (backward compat, 1 version) +### Deprecated Aliases -All 40+ legacy command names (e.g. `codelens dead-code`, `codelens symbols`, `codelens trace`, `codelens secrets`, `codelens diff`, `codelens dashboard`, `codelens ownership`, `codelens git-status`, `codelens env-check`, `codelens lsp-status`, `codelens arch-metrics`, `codelens architecture`, `codelens impact`, `codelens dataflow`, `codelens circular`, `codelens affected`, `codelens dependents`, `codelens import-snapshot`, `codelens staleness`, `codelens perf-hint`, `codelens side-effect`, `codelens vuln-scan`, `codelens taint`, `codelens binary-scan`, `codelens regex-audit`, `codelens outline`, `codelens orient`, `codelens semantic-query`, `codelens query-graph`, `codelens graph-schema`, `codelens init`) are still callable but hidden from `--help`. Invoking any of them prints a deprecation warning to stderr that redirects to the new umbrella command. +All deprecated aliases have been removed in this version (issue #199, post-#195 cleanup). The 32 legacy command names that were retained as hidden aliases for one version after the #195 consolidation — `affected`, `arch-metrics`, `architecture`, `binary-scan`, `circular`, `complexity`, `dashboard`, `dataflow`, `dead-code`, `dependents`, `diff`, `env-check`, `git-status`, `graph-schema`, `import-snapshot`, `init`, `lsp-status`, `orient`, `outline`, `ownership`, `perf-hint`, `query-graph`, `regex-audit`, `secrets`, `semantic-query`, `side-effect`, `smell`, `staleness`, `symbols`, `taint`, `trace`, `vuln-scan` — are no longer registered. Invoking any of them now produces an `invalid choice` error from argparse instead of a deprecation warning. Use the 12 umbrella commands above (e.g. `codelens audit --check dead-code` instead of `codelens dead-code`). ### Dropped Commands (removed in issue #195) diff --git a/scripts/codelens.py b/scripts/codelens.py index f7b694b4..4d343d90 100755 --- a/scripts/codelens.py +++ b/scripts/codelens.py @@ -902,8 +902,11 @@ def compute_confidence_distribution_flat(result: Dict[str, Any]) -> Dict[str, in def main(): # Command count is derived from the visible (non-hidden) command set at # runtime so it can never drift from the actual number of umbrella - # commands (issue #38). Hidden deprecated aliases are excluded from the - # headline count per issue #195 consolidation (78 → 12 focused commands). + # commands (issue #38). Hidden commands are excluded from the headline + # count per issue #195 consolidation (78 → 12 focused commands). The 32 + # deprecated aliases introduced by #195 were removed in issue #199; the + # remaining hidden commands are the 13 pending-decision commands tracked + # by issue #200. # The `--command-count` flag below prints it for scripts / CI; the # description also includes it so `--help` is self-documenting. from commands import get_visible_commands as _get_visible_commands @@ -925,16 +928,15 @@ def main(): default=False, help="Print the runtime command count (len of visible COMMAND_REGISTRY) " "and exit. Single source of truth for issue #38 reconciliation. " - "Hidden deprecated aliases are excluded (issue #195).", + "Hidden commands are excluded (issues #195/#199/#200).", ) subparsers = parser.add_subparsers( dest="command", help="Available commands", # Issue #195: the default metavar lists every choice including - # hidden deprecated aliases. Override with only the 12 visible - # umbrella command names so --help is clean. Hidden commands are - # still dispatchable (registered below) but don't clutter the - # usage line. + # hidden commands. Override with only the 12 visible umbrella + # command names so --help is clean. Hidden commands are still + # dispatchable (intercepted below) but don't clutter the usage line. metavar="{" + ",".join(sorted(_visible_registry.keys())) + "}", ) @@ -942,11 +944,13 @@ def main(): registry = get_all_commands() # Build subparsers from the command registry. - # Issue #195: hidden commands (deprecated aliases) are NOT registered as - # subparsers at all — that way they don't appear in --help choices, body, - # or usage line. They are still dispatchable via the manual intercept in - # the dispatch block below (we pre-parse sys.argv[1] and if it matches a - # hidden command, we build a synthetic namespace and execute directly). + # Issue #195: hidden commands are NOT registered as subparsers at all — + # that way they don't appear in --help choices, body, or usage line. They + # are still dispatchable via the manual intercept in the dispatch block + # below (we pre-parse sys.argv[1] and if it matches a hidden command, we + # build a synthetic namespace and execute directly). The 32 deprecated + # aliases from #195 were removed in #199; the only hidden commands left + # are the 13 pending-decision commands tracked by #200. _existing_subparser_args = {} _hidden_commands = {} for cmd_name, cmd_info in sorted(registry.items()): @@ -1119,12 +1123,14 @@ def main(): global_diff_base = arg.split('=', 1)[1] i += 1 - # Issue #195: intercept hidden deprecated aliases BEFORE argparse rejects + # Issue #195/#200: intercept hidden commands BEFORE argparse rejects # them as "invalid choice". Hidden commands are not registered as # subparsers (so they don't appear in --help), but they remain callable - # for backward compat. We detect them by scanning sys.argv for the first - # non-flag token that matches a hidden command name, then build a - # synthetic namespace and dispatch directly. + # (the 13 pending-decision commands from issue #200). We detect them by + # scanning sys.argv for the first non-flag token that matches a hidden + # command name, then build a synthetic namespace and dispatch directly. + # Issue #199 removed the 32 deprecated aliases, so they are no longer + # here and now fall through to argparse's "invalid choice" error. _hidden_cmd_name = None _hidden_cmd_args = None if _hidden_commands: @@ -1309,18 +1315,6 @@ def main(): try: cmd_info = registry[args.command] - # Issue #195: deprecated alias warning. Old commands still execute - # (backward compat for one version) but print a redirect hint to - # stderr so users migrate to the new umbrella command. - _alias_for = cmd_info.get("deprecated_alias_for") - if _alias_for: - print( - f"[CodeLens] DEPRECATED: '{args.command}' is a deprecated alias. " - f"Use 'codelens {_alias_for}' instead. " - f"This alias will be removed in the next version (issue #195).", - file=sys.stderr, - ) - result = cmd_info["execute"](args, workspace) # ─── Dispatch enrichment (scan-specific) ────── @@ -1396,46 +1390,70 @@ def main(): # 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" - ): + # Issue #199: the --deep supported list used the old alias command + # names (smell, dead-code, complexity). After #199 these are reached + # via the ``audit`` umbrella with ``--check ``. Map the + # umbrella+check combination back to the alias name so the existing + # enhancement logic keeps working. + _DEEP_SUPPORTED_ALIASES = ("dead-code", "query", "impact", "smell", "complexity") + _AUDIT_DEEP_CHECK_MAP = { + "dead-code": "dead-code", + "smell": "smell", + "complexity": "complexity", + } + _deep_effective_command = args.command + if args.command == "audit" and getattr(args, "check", None): + _check_first = str(args.check).split(",")[0].strip() + _deep_effective_command = _AUDIT_DEEP_CHECK_MAP.get(_check_first, args.command) + if deep and isinstance(result, dict) and _deep_effective_command in _DEEP_SUPPORTED_ALIASES: try: from hybrid_engine import create_hybrid_engine, add_confidence_to_result hybrid = create_hybrid_engine(workspace, deep=True) - if args.command == "dead-code": + # Issue #199: umbrella commands wrap sub-results under + # result["r"][i]["_check"] == . Find the sub-result + # matching _deep_effective_command so the enhancement targets + # the right dict. Non-umbrella commands use the top-level result. + _deep_target = result + if isinstance(result.get("r"), list): + for _sub in result["r"]: + if isinstance(_sub, dict) and _sub.get("_check") == _deep_effective_command: + _deep_target = _sub + break + + if _deep_effective_command == "dead-code": # Enhance dead-code findings with LSP verification all_findings = [] - for cat_items in result.get("results", {}).values(): + for cat_items in _deep_target.get("results", {}).values(): if isinstance(cat_items, list): all_findings.extend(cat_items) if all_findings: verified = hybrid.verify_dead_code(all_findings) - result["lsp_verified"] = True - result["lsp_active"] = hybrid.lsp_active + _deep_target["lsp_verified"] = True + _deep_target["lsp_active"] = hybrid.lsp_active - elif args.command == "query" and result.get("found"): - result = hybrid.enhance_query(result, result.get("query", "")) - result["lsp_active"] = hybrid.lsp_active + elif _deep_effective_command == "query" and _deep_target.get("found"): + _deep_target = hybrid.enhance_query(_deep_target, _deep_target.get("query", "")) + _deep_target["lsp_active"] = hybrid.lsp_active - elif args.command == "impact": - result = hybrid.enhance_impact(result, result.get("symbol", "")) - result["lsp_active"] = hybrid.lsp_active + elif _deep_effective_command == "impact": + _deep_target = hybrid.enhance_impact(_deep_target, _deep_target.get("symbol", "")) + _deep_target["lsp_active"] = hybrid.lsp_active - elif args.command == "smell": + elif _deep_effective_command == "smell": all_findings = [] - for cat_items in result.get("by_category", {}).values(): + for cat_items in _deep_target.get("by_category", {}).values(): if isinstance(cat_items, list): all_findings.extend(cat_items) if all_findings: hybrid.enhance_smell(all_findings) - result["lsp_active"] = hybrid.lsp_active + _deep_target["lsp_active"] = hybrid.lsp_active - elif args.command == "complexity": - funcs = result.get("functions", []) + elif _deep_effective_command == "complexity": + funcs = _deep_target.get("functions", []) if funcs: hybrid.enhance_complexity(funcs) - result["lsp_active"] = hybrid.lsp_active + _deep_target["lsp_active"] = hybrid.lsp_active # Add confidence distribution to stats result = add_confidence_to_result(result) @@ -1452,9 +1470,7 @@ def main(): # 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" - ): + elif not deep and isinstance(result, dict) and _deep_effective_command in _DEEP_SUPPORTED_ALIASES: # Auto-detect: if LSP available and --deep not specified, show hint try: from hybrid_engine import get_lsp_status @@ -1537,40 +1553,63 @@ def main(): # are NOT filtered because their results are structural (node/edge # graphs) rather than file-keyed findings — filtering them would # silently corrupt the graph. - if diff_scope is not None and isinstance(result, dict) and args.command in ( + # + # Issue #199: the old alias command names (secrets, smell, dead-code, + # etc.) were removed from the CLI; the same analyses are now reached + # via the 12 umbrella commands (security, audit, etc.) with a + # ``--check `` flag. The post-filter now also runs for the + # umbrella commands — when the result has the umbrella shape + # (``{"s":..., "st":..., "r":[...]}``), each sub-result in ``r`` is + # filtered individually and the ``diff_scope`` summary is attached + # at the top level so consumers see what was filtered. + _DIFF_BASE_FILTERABLE_UMBRELLAS = { + "security", "audit", "impact", "context", "deps", + } + _is_filterable_command = args.command in ( "secrets", "smell", "complexity", "dead-code", "debug-leak", "circular", "taint", "vuln-scan", "check", "analyze", "missing-refs", "side-effect", "perf-hint", "regex-audit", "a11y", "css-deep", "dataflow", "stack-trace", "config-drift", "ownership", "test-map", - ): + ) or args.command in _DIFF_BASE_FILTERABLE_UMBRELLAS + if diff_scope is not None and isinstance(result, dict) and _is_filterable_command: _FILTER_KEYS = ( "findings", "leaks", "hints", "issues", "violations", "matches", "chains", "results", ) + + def _filter_one(sub_result): + """Filter findings in a single sub-result dict; return (before, after).""" + nonlocal total_before, total_after + if not isinstance(sub_result, dict): + return + for key in _FILTER_KEYS: + val = sub_result.get(key) + if isinstance(val, list): + before = len(val) + sub_result[key] = diff_scope.filter_findings(val) + total_before += before + total_after += len(sub_result[key]) + elif isinstance(val, dict): + # Category-keyed (dead-code by_category, smell by_category) + for sub_key, sub_val in val.items(): + if isinstance(sub_val, list): + before = len(sub_val) + val[sub_key] = diff_scope.filter_findings(sub_val) + total_before += before + total_after += len(val[sub_key]) + total_before = 0 total_after = 0 - for key in _FILTER_KEYS: - val = result.get(key) - if isinstance(val, list): - before = len(val) - result[key] = diff_scope.filter_findings(val) - total_before += before - total_after += len(result[key]) - elif isinstance(val, dict): - # Category-keyed (dead-code by_category, smell by_category) - for sub_key, sub_val in val.items(): - if isinstance(sub_val, list): - before = len(sub_val) - val[sub_key] = diff_scope.filter_findings(sub_val) - total_before += before - total_after += len(val[sub_key]) - # Also filter the flat ``findings`` list that some commands - # (e.g., ``check``) produce at the top level. - if "findings" in result and isinstance(result["findings"], list): - # Already filtered above if ``findings`` is in _FILTER_KEYS, - # but ``check`` stores them under ``findings`` — covered. - pass + # Issue #199: umbrella commands return {"s":..., "st":..., "r":[...]}. + # Filter each sub-result in ``r`` so the post-filter respects the + # umbrella structure. Non-umbrella commands are filtered at the + # top level (the original pre-#199 behavior). + if "r" in result and isinstance(result["r"], list): + for sub_result in result["r"]: + _filter_one(sub_result) + else: + _filter_one(result) # Attach diff_scope summary so consumers can see what was filtered result["diff_scope"] = diff_scope.summary() result["diff_scope"]["findings_before_filter"] = total_before diff --git a/scripts/commands/__init__.py b/scripts/commands/__init__.py index f37b041f..a929e3f6 100644 --- a/scripts/commands/__init__.py +++ b/scripts/commands/__init__.py @@ -1,23 +1,28 @@ """Command registry for CodeLens CLI. -Issue #195 consolidation: commands carry two optional metadata fields: +Issue #195 consolidated 78 legacy commands into 12 focused umbrella commands. +Each umbrella command accepts a ``--check `` flag to select a +specific sub-analysis. The per-sub-analysis implementations live in sibling +modules under ``scripts/commands/`` and are imported by their umbrella via +:func:`importlib.import_module`. + +Commands carry one optional metadata field: - ``hidden`` (bool, default False) — hidden commands are still callable but do not appear in ``--help`` output and are excluded from ``--command-count`` - and the MCP tool count. Used for deprecated aliases that point at the new - umbrella commands. - -- ``deprecated_alias_for`` (str|None, default None) — when set, invoking this - command prints a deprecation warning to stderr that redirects the user to - the named umbrella command. The old command still executes normally - (backward compat for one version per issue #195 DoD point 2). + and the MCP tool count. As of issue #199 the 32 deprecated aliases + introduced by #195 have been removed entirely (their registrations were + deleted and the two orphaned modules — ``symbols.py`` and + ``semantic_query.py`` — were deleted because no umbrella imports them). + The remaining hidden commands are the 13 pending-decision commands tracked + by issue #200 (analyze, check, config-drift, deps-audit, entrypoints, lsp, + list, missing-refs, plugin, query, state-map, test-map, type-infer). """ COMMAND_REGISTRY = {} -def register_command(name, help_text, add_args_fn, execute_fn, - hidden=False, deprecated_alias_for=None): +def register_command(name, help_text, add_args_fn, execute_fn, hidden=False): """Register a command with the CLI. Parameters @@ -33,17 +38,13 @@ def register_command(name, help_text, add_args_fn, execute_fn, Function ``(args, workspace) -> result_dict``. hidden : bool, optional If True, the command is callable but hidden from ``--help`` and - excluded from the runtime command count (issue #195). - deprecated_alias_for : str, optional - If set, the command is a deprecated alias for the named umbrella - command. A deprecation warning is printed to stderr before execute. + excluded from the runtime command count (issues #195/#200). """ COMMAND_REGISTRY[name] = { "help": help_text, "add_args": add_args_fn, "execute": execute_fn, "hidden": hidden, - "deprecated_alias_for": deprecated_alias_for, } @@ -62,7 +63,7 @@ def get_visible_commands(): Used by ``--command-count``, ``--help`` subparser construction, and ``sync_command_count.py`` so the headline count reflects the 12 - umbrella commands rather than the full deprecated-alias set. + umbrella commands rather than the full hidden set. """ return {name: info for name, info in COMMAND_REGISTRY.items() if not info.get("hidden", False)} diff --git a/scripts/commands/affected.py b/scripts/commands/affected.py index 55126637..35cfa9a6 100644 --- a/scripts/commands/affected.py +++ b/scripts/commands/affected.py @@ -163,12 +163,4 @@ def execute(args, workspace): result["note"] = "No test files found in dependents. Use --include-source to list all affected source files." return result - -register_command( - "affected", - "Identify test files affected by source changes (issue #62 Phase 1)", - add_args, - execute, -hidden=True, -deprecated_alias_for='deps', -) +# Issue #199: deprecated "affected" alias registration removed; this module is now an implementation module imported by the "deps" umbrella command. diff --git a/scripts/commands/arch_metrics.py b/scripts/commands/arch_metrics.py index 1a02ac6a..56242a1b 100644 --- a/scripts/commands/arch_metrics.py +++ b/scripts/commands/arch_metrics.py @@ -227,12 +227,4 @@ def execute(args: argparse.Namespace, workspace: str) -> Dict[str, Any]: "sort_by": args.sort_by, } - -register_command( - "arch-metrics", - "Compute architecture metrics (fan-in/out, instability, god-module detection) from graph", - add_args, - execute, -hidden=True, -deprecated_alias_for='summary', -) +# Issue #199: deprecated "arch-metrics" alias registration removed; this module is now an implementation module imported by the "summary" umbrella command. diff --git a/scripts/commands/architecture.py b/scripts/commands/architecture.py index e868ea34..0dc51991 100644 --- a/scripts/commands/architecture.py +++ b/scripts/commands/architecture.py @@ -65,13 +65,4 @@ def execute(args, workspace): return get_architecture(workspace, lite=lite) - -register_command( - "architecture", - "Single-call codebase overview for AI agents (languages, frameworks, " - "entry points, packages, routes, hotspots, total symbols)", - add_args, - execute, -hidden=True, -deprecated_alias_for='summary', -) +# Issue #199: deprecated "architecture" alias registration removed; this module is now an implementation module imported by the "summary" umbrella command. diff --git a/scripts/commands/binary_scan.py b/scripts/commands/binary_scan.py index cbf92d7a..6143b18d 100644 --- a/scripts/commands/binary_scan.py +++ b/scripts/commands/binary_scan.py @@ -848,12 +848,4 @@ def _generate_recommendations( return recs - -register_command( - "binary-scan", - "Scan for binary/compiled artifacts with reverse-engineering analysis (superset of artifact-scan)", - add_args, - execute, - hidden=True, - deprecated_alias_for='security', -) +# Issue #199: deprecated "binary-scan" alias registration removed; this module is now an implementation module imported by the "security" umbrella command. diff --git a/scripts/commands/circular.py b/scripts/commands/circular.py index 46d50dbc..4bd00933 100644 --- a/scripts/commands/circular.py +++ b/scripts/commands/circular.py @@ -16,11 +16,4 @@ def add_args(parser): def execute(args, workspace): return detect_circular(workspace, domain=args.domain, max_cycles=args.max_cycles) - -register_command("circular", "Detect circular dependencies", add_args, execute, - -hidden=True, - -deprecated_alias_for='deps', - -) +# Issue #199: deprecated "circular" alias registration removed; this module is now an implementation module imported by the "deps" umbrella command. diff --git a/scripts/commands/complexity.py b/scripts/commands/complexity.py index 5660a551..d6755d6f 100644 --- a/scripts/commands/complexity.py +++ b/scripts/commands/complexity.py @@ -26,11 +26,4 @@ def execute(args, workspace): sort_by=args.sort_by, limit=args.limit, max_files=args.max_files) - -register_command("complexity", "Compute cyclomatic/cognitive complexity", add_args, execute, - -hidden=True, - -deprecated_alias_for='audit', - -) +# Issue #199: deprecated "complexity" alias registration removed; this module is now an implementation module imported by the "audit" umbrella command. diff --git a/scripts/commands/dashboard.py b/scripts/commands/dashboard.py index b9eca789..ad69bc1c 100644 --- a/scripts/commands/dashboard.py +++ b/scripts/commands/dashboard.py @@ -85,11 +85,4 @@ def on_modified(self, event): return result - -register_command("dashboard", "Generate HTML visualization dashboard", add_args, execute, - -hidden=True, - -deprecated_alias_for='summary', - -) +# Issue #199: deprecated "dashboard" alias registration removed; this module is now an implementation module imported by the "summary" umbrella command. diff --git a/scripts/commands/dataflow.py b/scripts/commands/dataflow.py index 7f2881c0..60ed832a 100644 --- a/scripts/commands/dataflow.py +++ b/scripts/commands/dataflow.py @@ -221,9 +221,4 @@ def _generate_actionable_items(result): return items - -register_command("dataflow", "Trace data flow source→sink with cross-file call graph analysis", - add_args, execute, - hidden=True, - deprecated_alias_for='impact', - ) +# Issue #199: deprecated "dataflow" alias registration removed; this module is now an implementation module imported by the "impact" umbrella command. diff --git a/scripts/commands/dead_code.py b/scripts/commands/dead_code.py index b4876bb8..156bb632 100644 --- a/scripts/commands/dead_code.py +++ b/scripts/commands/dead_code.py @@ -84,11 +84,4 @@ def execute(args, workspace): result["stats"]["confidence_distribution"] = dist return result - -register_command("dead-code", "Enhanced dead code detection", add_args, execute, - -hidden=True, - -deprecated_alias_for='audit', - -) +# Issue #199: deprecated "dead-code" alias registration removed; this module is now an implementation module imported by the "audit" umbrella command. diff --git a/scripts/commands/dependents.py b/scripts/commands/dependents.py index e877538c..3e789569 100644 --- a/scripts/commands/dependents.py +++ b/scripts/commands/dependents.py @@ -42,11 +42,4 @@ def execute(args, workspace): return {"status": "error", "error": "No file specified. Usage: codelens dependents [workspace]"} return get_dependents(file_path, workspace, depth=args.depth) - -register_command("dependents", "Module-level import tracking", add_args, execute, - -hidden=True, - -deprecated_alias_for='deps', - -) +# Issue #199: deprecated "dependents" alias registration removed; this module is now an implementation module imported by the "deps" umbrella command. diff --git a/scripts/commands/diff.py b/scripts/commands/diff.py index 6e7b25dd..ad91753c 100644 --- a/scripts/commands/diff.py +++ b/scripts/commands/diff.py @@ -198,13 +198,4 @@ def cmd_diff_git_aware(workspace: str) -> Dict[str, Any]: "impact": impact, } - -register_command( - "diff", - "Compare registry snapshots (--git-aware for git-diff delta + impact)", - add_args, - execute, -hidden=True, -deprecated_alias_for='impact', -) - +# Issue #199: deprecated "diff" alias registration removed; this module is now an implementation module imported by the "impact" umbrella command. diff --git a/scripts/commands/env_check.py b/scripts/commands/env_check.py index a7c21b42..83422439 100644 --- a/scripts/commands/env_check.py +++ b/scripts/commands/env_check.py @@ -14,11 +14,4 @@ def add_args(parser): def execute(args, workspace): return check_env_vars(workspace, var_name=args.var_name) - -register_command("env-check", "Audit environment variables", add_args, execute, - -hidden=True, - -deprecated_alias_for='doctor', - -) +# Issue #199: deprecated "env-check" alias registration removed; this module is now an implementation module imported by the "doctor" umbrella command. diff --git a/scripts/commands/git_status.py b/scripts/commands/git_status.py index 600cf3f5..63b40f92 100644 --- a/scripts/commands/git_status.py +++ b/scripts/commands/git_status.py @@ -128,12 +128,4 @@ def cmd_git_status(workspace: str) -> Dict[str, Any]: "rescan_recommended": rescan, } - -register_command( - "git-status", - "Show git-aware scan state (SHA, branch, changed files, rescan recommendation)", - add_args, - execute, -hidden=True, -deprecated_alias_for='history', -) +# Issue #199: deprecated "git-status" alias registration removed; this module is now an implementation module imported by the "history" umbrella command. diff --git a/scripts/commands/graph_schema.py b/scripts/commands/graph_schema.py index a2b8a93e..ff0f6e00 100644 --- a/scripts/commands/graph_schema.py +++ b/scripts/commands/graph_schema.py @@ -118,12 +118,4 @@ def execute(args, workspace): db_path = getattr(args, "db_path", None) return get_graph_schema(workspace, db_path=db_path) - -register_command( - "graph-schema", - "Return the shape of the code graph (node/edge counts, type distribution, indexes)", - add_args, - execute, -hidden=True, -deprecated_alias_for='api-map', -) +# Issue #199: deprecated "graph-schema" alias registration removed; this module is now an implementation module imported by the "api-map" umbrella command. diff --git a/scripts/commands/import_snapshot.py b/scripts/commands/import_snapshot.py index ddfae6e1..baf6b7c6 100644 --- a/scripts/commands/import_snapshot.py +++ b/scripts/commands/import_snapshot.py @@ -168,13 +168,4 @@ def cmd_import_snapshot( "tables": list(SNAPSHOT_TABLES), } - -register_command( - "import-snapshot", - "Import a CodeLens graph snapshot (.codelens.gz) into the database; " - "use --merge to deduplicate with the existing graph (issue #12)", - add_args, - execute, -hidden=True, -deprecated_alias_for='deps', -) +# Issue #199: deprecated "import-snapshot" alias registration removed; this module is now an implementation module imported by the "deps" umbrella command. diff --git a/scripts/commands/init.py b/scripts/commands/init.py index 6bd78f7b..8ea1eee8 100644 --- a/scripts/commands/init.py +++ b/scripts/commands/init.py @@ -70,11 +70,4 @@ def cmd_init(workspace: str) -> Dict[str, Any]: "hooks_json_created": os.path.exists(hooks_path), } - -register_command("init", "Initialize .codelens with auto-detected config", add_args, execute, - -hidden=True, - -deprecated_alias_for='scan', - -) +# Issue #199: deprecated "init" alias registration removed; this module is now an implementation module imported by the "scan" umbrella command. diff --git a/scripts/commands/lsp_status.py b/scripts/commands/lsp_status.py index e2a0bed2..7dd4c1f4 100644 --- a/scripts/commands/lsp_status.py +++ b/scripts/commands/lsp_status.py @@ -39,12 +39,4 @@ def execute(args, workspace): return get_lsp_status() - -register_command( - "lsp-status", - "Check which LSP servers are available for deep analysis", - add_args, - execute, -hidden=True, -deprecated_alias_for='doctor', -) +# Issue #199: deprecated "lsp-status" alias registration removed; this module is now an implementation module imported by the "doctor" umbrella command. diff --git a/scripts/commands/orient.py b/scripts/commands/orient.py index d357a7a7..5e03c9b8 100644 --- a/scripts/commands/orient.py +++ b/scripts/commands/orient.py @@ -390,13 +390,4 @@ def _render_text(brief: Dict[str, Any]) -> None: print(f" Test framework: {infra.get('test_framework', 'none')}") print(f" Linter: {infra.get('linter', 'none')}") - -register_command( - "orient", - "10-second codebase orientation brief (framework, commands, entry points, " - "start-here files, CI/Docker)", - add_args, - execute, -hidden=True, -deprecated_alias_for='context', -) +# Issue #199: deprecated "orient" alias registration removed; this module is now an implementation module imported by the "context" umbrella command. diff --git a/scripts/commands/outline.py b/scripts/commands/outline.py index c1fcd9a4..9e0f49b6 100644 --- a/scripts/commands/outline.py +++ b/scripts/commands/outline.py @@ -41,11 +41,4 @@ def execute(args, workspace): result["has_more"] = (offset + limit) < total return result - -register_command("outline", "Get file structure outline", add_args, execute, - -hidden=True, - -deprecated_alias_for='context', - -) +# Issue #199: deprecated "outline" alias registration removed; this module is now an implementation module imported by the "context" umbrella command. diff --git a/scripts/commands/ownership.py b/scripts/commands/ownership.py index cef3ae6a..90c84d14 100644 --- a/scripts/commands/ownership.py +++ b/scripts/commands/ownership.py @@ -19,11 +19,4 @@ def execute(args, workspace): function_name=args.function_name ) - -register_command("ownership", "Git blame-based code ownership", add_args, execute, - -hidden=True, - -deprecated_alias_for='history', - -) +# Issue #199: deprecated "ownership" alias registration removed; this module is now an implementation module imported by the "history" umbrella command. diff --git a/scripts/commands/perf_hint.py b/scripts/commands/perf_hint.py index 08f475d9..c99ad5eb 100644 --- a/scripts/commands/perf_hint.py +++ b/scripts/commands/perf_hint.py @@ -19,11 +19,4 @@ def execute(args, workspace): return detect_perf_hints(workspace, severity=args.severity, category=args.category, max_files=args.max_files) - -register_command("perf-hint", "Detect performance anti-patterns", add_args, execute, - -hidden=True, - -deprecated_alias_for='audit', - -) +# Issue #199: deprecated "perf-hint" alias registration removed; this module is now an implementation module imported by the "audit" umbrella command. diff --git a/scripts/commands/query_graph.py b/scripts/commands/query_graph.py index 09f76c2d..8b4c2b93 100644 --- a/scripts/commands/query_graph.py +++ b/scripts/commands/query_graph.py @@ -76,12 +76,4 @@ def execute(args, workspace): from query_graph_engine import execute_query return execute_query(query, workspace, db_path=db_path) - -register_command( - "query-graph", - "Query the code graph with a Cypher-subset query (MATCH/WHERE/RETURN/LIMIT)", - add_args, - execute, -hidden=True, -deprecated_alias_for='graph', -) +# Issue #199: deprecated "query-graph" alias registration removed; this module is now an implementation module imported by the "graph" umbrella command. diff --git a/scripts/commands/regex_audit.py b/scripts/commands/regex_audit.py index c8478472..0e34d5e5 100644 --- a/scripts/commands/regex_audit.py +++ b/scripts/commands/regex_audit.py @@ -16,11 +16,4 @@ def add_args(parser): def execute(args, workspace): return audit_regex_patterns(workspace, severity=args.severity, max_files=args.max_files) - -register_command("regex-audit", "Audit regex for ReDoS and issues", add_args, execute, - -hidden=True, - -deprecated_alias_for='security', - -) +# Issue #199: deprecated "regex-audit" alias registration removed; this module is now an implementation module imported by the "security" umbrella command. diff --git a/scripts/commands/secrets.py b/scripts/commands/secrets.py index 72cac4fd..d58377e6 100644 --- a/scripts/commands/secrets.py +++ b/scripts/commands/secrets.py @@ -85,11 +85,4 @@ def execute(args, workspace): stats["backend"] = "regex" return result - -register_command("secrets", "Detect hardcoded secrets and API keys", add_args, execute, - -hidden=True, - -deprecated_alias_for='security', - -) +# Issue #199: deprecated "secrets" alias registration removed; this module is now an implementation module imported by the "security" umbrella command. diff --git a/scripts/commands/semantic_query.py b/scripts/commands/semantic_query.py deleted file mode 100644 index 9d8a4a3b..00000000 --- a/scripts/commands/semantic_query.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Semantic query command — TF-IDF symbol search (issue #11, Option A).""" - -import os -from typing import Any, Dict - -from semantic_search_engine import semantic_query -from commands import register_command - - -def add_args(parser): - parser.add_argument( - "query", - help=( - "Natural-language or code-fragment query " - "(e.g. 'user authentication flow', 'parse jwt', 'error handler'). " - "Symbol names, signatures, kinds, and file paths are all searched." - ), - ) - parser.add_argument( - "workspace", - nargs="?", - default=None, - help="Path to workspace root (auto-detected if omitted).", - ) - parser.add_argument( - "--top", - type=int, - default=10, - metavar="N", - help="Maximum number of results to return (default: 10; use 0 for all).", - ) - parser.add_argument( - "--db-path", - default=None, - metavar="PATH", - help="Custom path for the SQLite registry database " - "(default: /.codelens/codelens.db).", - ) - - -def execute(args, workspace) -> Dict[str, Any]: - """Dispatch to :func:`semantic_engine.semantic_query`. - - The CLI ``--top`` flag is the user-facing name; the engine takes the - same value as ``top_k``. We pass ``workspace`` and ``db_path`` straight - through so users can override the database location for ad-hoc queries - against a snapshot. - """ - top_k = getattr(args, "top", 10) - if top_k is None: - top_k = 10 - db_path = getattr(args, "db_path", None) - return semantic_query( - workspace=workspace, - query=args.query, - top_k=top_k, - db_path=db_path, - ) - - -register_command( - "semantic-query", - "Semantic symbol search via TF-IDF (find symbols by meaning, not just name)", - add_args, - execute, -hidden=True, -deprecated_alias_for='search', -) diff --git a/scripts/commands/side_effect.py b/scripts/commands/side_effect.py index ee50284f..8254e8e9 100644 --- a/scripts/commands/side_effect.py +++ b/scripts/commands/side_effect.py @@ -21,11 +21,4 @@ def execute(args, workspace): max_files=args.max_files ) - -register_command("side-effect", "Analyze function side effects (pure vs impure)", add_args, execute, - -hidden=True, - -deprecated_alias_for='audit', - -) +# Issue #199: deprecated "side-effect" alias registration removed; this module is now an implementation module imported by the "audit" umbrella command. diff --git a/scripts/commands/smell.py b/scripts/commands/smell.py index 16e42fcc..a1771ef9 100644 --- a/scripts/commands/smell.py +++ b/scripts/commands/smell.py @@ -41,11 +41,4 @@ def execute(args, workspace): result["actionable_items"] = actionable return result - -register_command("smell", "Detect code smells across workspace", add_args, execute, - -hidden=True, - -deprecated_alias_for='audit', - -) +# Issue #199: deprecated "smell" alias registration removed; this module is now an implementation module imported by the "audit" umbrella command. diff --git a/scripts/commands/staleness.py b/scripts/commands/staleness.py index 8a7033fa..aa120ea8 100644 --- a/scripts/commands/staleness.py +++ b/scripts/commands/staleness.py @@ -138,12 +138,4 @@ def execute(args: argparse.Namespace, workspace: str) -> Dict[str, Any]: return result - -register_command( - "staleness", - "List files whose index entry is stale (issue #66 Phase 1)", - add_args, - execute, -hidden=True, -deprecated_alias_for='audit', -) +# Issue #199: deprecated "staleness" alias registration removed; this module is now an implementation module imported by the "audit" umbrella command. diff --git a/scripts/commands/symbols.py b/scripts/commands/symbols.py deleted file mode 100644 index 2260cdf1..00000000 --- a/scripts/commands/symbols.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Symbols command — Search registry symbols by name.""" - -from search_engine import search_symbols -from commands import register_command - - -def add_args(parser): - parser.add_argument("name", help="Symbol name to search") - parser.add_argument("workspace", nargs="?", default=None, - help="Path to workspace root (auto-detected if omitted)") - parser.add_argument("--domain", choices=["frontend", "backend", "all"], default="all", - help="Domain to search") - parser.add_argument("--fuzzy", action="store_true", help="Allow partial/fuzzy matching") - parser.add_argument("--limit", type=int, default=20, - help="Max results to return (default: 20). Use --limit 0 for unlimited.") - parser.add_argument("--offset", type=int, default=0, - help="Offset for pagination (default: 0)") - - -def execute(args, workspace): - # search_symbols caps at max_results internally; we add an outer - # pagination layer so callers can paginate beyond the engine's own cap. - max_results = 500 # engine-level cap; --limit paginates within this - result = search_symbols( - workspace, args.name, - domain=args.domain, fuzzy=args.fuzzy, - max_results=max_results, - ) - # Apply pagination (issue #17). - if isinstance(result, dict) and "results" in result: - results = result["results"] - total = len(results) - limit = args.limit if args.limit and args.limit > 0 else total - offset = max(args.offset, 0) - paginated = results[offset:offset + limit] - result["results"] = paginated - result["total_count"] = total - result["count"] = len(paginated) - result["offset"] = offset - result["limit"] = limit - result["has_more"] = (offset + limit) < total - return result - - -register_command("symbols", "Search symbols in registry by name", add_args, execute, - -hidden=True, - -deprecated_alias_for='search', - -) diff --git a/scripts/commands/taint.py b/scripts/commands/taint.py index d3ed6766..ee0d3e4a 100644 --- a/scripts/commands/taint.py +++ b/scripts/commands/taint.py @@ -130,11 +130,4 @@ def execute(args, workspace): return result - -register_command("taint", "Run AST-based taint analysis for vulnerability detection", add_args, execute, - -hidden=True, - -deprecated_alias_for='security', - -) +# Issue #199: deprecated "taint" alias registration removed; this module is now an implementation module imported by the "security" umbrella command. diff --git a/scripts/commands/trace.py b/scripts/commands/trace.py index c5b0e078..762eace3 100644 --- a/scripts/commands/trace.py +++ b/scripts/commands/trace.py @@ -81,11 +81,4 @@ def execute(args, workspace): result["limit"] = limit return result - -register_command("trace", "Trace deep call chain from a symbol", add_args, execute, - -hidden=True, - -deprecated_alias_for='context', - -) +# Issue #199: deprecated "trace" alias registration removed; this module is now an implementation module imported by the "context" umbrella command. diff --git a/scripts/commands/vuln_scan.py b/scripts/commands/vuln_scan.py index 5e295202..df95206e 100644 --- a/scripts/commands/vuln_scan.py +++ b/scripts/commands/vuln_scan.py @@ -89,11 +89,4 @@ def execute(args, workspace): max_age=max_age_seconds, ) - -register_command("vuln-scan", "Scan dependencies for known CVEs (OSV.dev + native audit)", add_args, execute, - -hidden=True, - -deprecated_alias_for='security', - -) +# Issue #199: deprecated "vuln-scan" alias registration removed; this module is now an implementation module imported by the "security" umbrella command. diff --git a/scripts/mcp_server.py b/scripts/mcp_server.py index 7b4d0cd3..9dfdba8e 100644 --- a/scripts/mcp_server.py +++ b/scripts/mcp_server.py @@ -2225,10 +2225,37 @@ def _execute_command(self, cmd_name: str, arguments: Dict[str, Any], workspace: cmd_info = self._command_registry.get(cmd_name) if cmd_info is None: - return { - "status": "error", - "error": f"Unknown command: {cmd_name}", - "available_commands": sorted(self._command_registry.keys()) + # Issue #199: the 32 deprecated CLI aliases were removed from + # COMMAND_REGISTRY, but their implementation modules remain + # (umbrella commands import them via importlib for their --check + # sub-analyses). Several MCP tools (codelens_architecture, + # codelens_secrets, codelens_symbols, codelens_trace, etc.) still + # dispatch to these modules. Fall back to importing the module + # directly and use its execute()/add_args() callables so the MCP + # surface keeps working after the CLI aliases are gone. + import importlib + module_name = "commands." + cmd_name.replace("-", "_") + try: + mod = importlib.import_module(module_name) + except ImportError as exc: + return { + "status": "error", + "error": f"Unknown command: {cmd_name} ({exc})", + "available_commands": sorted(self._command_registry.keys()) + } + execute_fn = getattr(mod, "execute", None) + add_args_fn = getattr(mod, "add_args", None) + if execute_fn is None or add_args_fn is None: + return { + "status": "error", + "error": f"Module {module_name} does not expose execute()/add_args()", + "available_commands": sorted(self._command_registry.keys()) + } + cmd_info = { + "help": getattr(mod, "__doc__", "") or "", + "add_args": add_args_fn, + "execute": execute_fn, + "hidden": True, } # Issue #58, Phase 1: validate any agent-supplied ``file`` / diff --git a/tests/test_affected_command.py b/tests/test_affected_command.py index e011b51c..fbab61a5 100644 --- a/tests/test_affected_command.py +++ b/tests/test_affected_command.py @@ -450,14 +450,23 @@ def test_command_execute_stdin_with_comments_and_blanks(small_workspace, monkeyp def test_command_registered_in_registry(): - """The command should be auto-registered via commands/__init__.py.""" + """The ``affected`` CLI alias was removed in issue #199. + + The implementation module ``commands.affected`` survives because the + ``deps`` umbrella command imports it for its ``--check affected`` sub- + analysis. The CLI alias ``codelens affected`` is no longer registered + and must yield an ``invalid choice`` argparse error. + """ from commands import COMMAND_REGISTRY - assert "affected" in COMMAND_REGISTRY - entry = COMMAND_REGISTRY["affected"] - assert entry["help"] - assert callable(entry["add_args"]) - assert callable(entry["execute"]) + assert "affected" not in COMMAND_REGISTRY, ( + "affected alias should have been removed in #199" + ) + # The implementation module must still be importable (umbrella dep). + import importlib + mod = importlib.import_module("commands.affected") + assert callable(mod.execute) + assert callable(mod.add_args) # ─── Issue #176: TypeScript affected + workspace-as-first-arg ───────────── diff --git a/tests/test_cli.py b/tests/test_cli.py index f5849ee5..83409f36 100755 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -788,12 +788,12 @@ class TestLspStatusEntryPointParity: hint/recommendation field names — so CLI users and MCP agents got different answers to the same question. - After the fix, both entry points delegate to ``hybrid_engine.get_lsp_status`` - (single source of truth). These tests assert structural parity: the set of - top-level keys must be identical, and the set of per-server keys must be - identical. The byte-identical check is covered by the repro diff in the - PR description; these tests guard against future regressions in the - test suite itself. + After the fix, both entry points delegated to ``hybrid_engine.get_lsp_status`` + (single source of truth). Issue #199 then removed the ``lsp-status`` CLI + alias entirely (the ``--lsp-status`` flag remains as the sole CLI entry + point). These tests now exercise only the ``--lsp-status`` flag form and + assert it returns a well-formed payload so any future regression in the + flag-intercept path is caught. """ @staticmethod @@ -816,60 +816,18 @@ def _run_codelens(extra_args): assert idx >= 0, f"No JSON in stdout: {proc.stdout!r}" return json.loads(proc.stdout[idx:]) - def test_top_level_keys_match(self): - """``--lsp-status`` and ``lsp-status`` must have identical top-level keys.""" - flag_payload = self._run_codelens(["--lsp-status"]) - sub_payload = self._run_codelens(["lsp-status"]) - - flag_keys = set(flag_payload.keys()) - sub_keys = set(sub_payload.keys()) - - assert flag_keys == sub_keys, ( - f"Top-level key sets differ between --lsp-status and lsp-status.\n" - f" --lsp-status only: {flag_keys - sub_keys}\n" - f" lsp-status only: {sub_keys - flag_keys}\n" - f" common : {flag_keys & sub_keys}" - ) - - def test_per_server_keys_match(self): - """Per-server field sets must be identical across both entry points.""" - flag_payload = self._run_codelens(["--lsp-status"]) - sub_payload = self._run_codelens(["lsp-status"]) - - flag_servers = flag_payload.get("servers", {}) - sub_servers = sub_payload.get("servers", {}) - - # Same set of server names - assert set(flag_servers.keys()) == set(sub_servers.keys()), ( - f"Server name sets differ:\n" - f" --lsp-status only: {set(flag_servers) - set(sub_servers)}\n" - f" lsp-status only: {set(sub_servers) - set(flag_servers)}" + def test_flag_payload_has_servers_key(self): + """``--lsp-status`` must return a payload with a ``servers`` dict.""" + payload = self._run_codelens(["--lsp-status"]) + assert "servers" in payload, ( + f"--lsp-status payload missing 'servers' key; got: {list(payload.keys())}" ) - # For each server, same set of field names - for name in flag_servers: - flag_fields = set(flag_servers[name].keys()) - sub_fields = set(sub_servers[name].keys()) - assert flag_fields == sub_fields, ( - f"Per-server field sets differ for server {name!r}:\n" - f" --lsp-status only: {flag_fields - sub_fields}\n" - f" lsp-status only: {sub_fields - flag_fields}" - ) - - def test_payloads_byte_identical(self): - """Full payload equality — the strongest possible parity guarantee. - - Both entry points must produce byte-identical JSON (after canonical - formatting), not just structural parity. This catches any future - regression that introduces a divergent field value. - """ - flag_payload = self._run_codelens(["--lsp-status"]) - sub_payload = self._run_codelens(["lsp-status"]) - - assert flag_payload == sub_payload, ( - "Payloads differ between --lsp-status and lsp-status:\n" - f" --lsp-status: {json.dumps(flag_payload, sort_keys=True, indent=2)}\n" - f" lsp-status : {json.dumps(sub_payload, sort_keys=True, indent=2)}" + def test_flag_payload_has_status_ok(self): + """``--lsp-status`` must report status=ok (or at least not error).""" + payload = self._run_codelens(["--lsp-status"]) + assert payload.get("status") in ("ok", "partial", "degraded"), ( + f"--lsp-status status unexpected: {payload.get('status')!r}" ) diff --git a/tests/test_command_registry.py b/tests/test_command_registry.py index 533b3fab..22979dae 100644 --- a/tests/test_command_registry.py +++ b/tests/test_command_registry.py @@ -21,12 +21,32 @@ def test_every_command_module_registers(): Issue #195: a small allowlist of utility modules (kept for backward compat with tests/scripts but not registered as commands) is excluded. + + Issue #199: the 30 deprecated alias modules retained after #195 had + their register_command() calls removed. They survive as implementation + modules imported by the 12 umbrella commands via importlib (e.g. + audit.py imports commands.dead_code, security.py imports + commands.secrets). Their .py files are kept; only the CLI alias + registration is gone. The 2 alias modules that no umbrella imports + (symbols.py, semantic_query.py) were deleted entirely. """ # Issue #195: migrate.py is a utility wrapper around # PersistentRegistry.migrate_from_json, kept so existing tests that # import cmd_migrate continue to work. It does NOT register a command # (migrate was dropped per the consolidation). _UTILITY_MODULES = {"migrate"} + # Issue #199: these modules are implementation-only — their + # register_command() calls were removed because the CLI alias is gone, + # but the umbrella commands import them for --check sub-analyses. + _DEPRECATED_ALIAS_MODULES = { + "affected", "arch_metrics", "architecture", "binary_scan", + "circular", "complexity", "dashboard", "dataflow", "dead_code", + "dependents", "diff", "env_check", "git_status", "graph_schema", + "import_snapshot", "init", "lsp_status", "orient", "outline", + "ownership", "perf_hint", "query_graph", "regex_audit", "secrets", + "side_effect", "smell", "staleness", "taint", "trace", "vuln_scan", + } + _UTILITY_MODULES |= _DEPRECATED_ALIAS_MODULES missing = [] for module_path in sorted(COMMANDS_DIR.glob("*.py")): if module_path.name == "__init__.py": diff --git a/tests/test_compact_format.py b/tests/test_compact_format.py index 2d4b75ad..747e8feb 100644 --- a/tests/test_compact_format.py +++ b/tests/test_compact_format.py @@ -364,13 +364,23 @@ def test_returns_zeros_without_db(self, tmp_workspace): assert schema["indexes"] == 0 def test_command_registered(self): - """graph-schema must be auto-registered in the command registry.""" + """graph-schema CLI alias was removed in issue #199. + + The implementation module ``commands.graph_schema`` survives + because the ``api-map`` umbrella command imports it for its + ``--check graph-schema`` sub-analysis. The CLI alias + ``codelens graph-schema`` is no longer registered. + """ from commands import get_all_commands cmds = get_all_commands() - assert "graph-schema" in cmds - info = cmds["graph-schema"] - assert "add_args" in info - assert "execute" in info + assert "graph-schema" not in cmds, ( + "graph-schema alias should have been removed in #199" + ) + # The implementation module must still be importable (api-map dep). + import importlib + mod = importlib.import_module("commands.graph_schema") + assert callable(mod.execute) + assert callable(mod.add_args) # ─── 7. MCP codelens_graph_schema tool exists ──────────────── diff --git a/tests/test_diff_scope.py b/tests/test_diff_scope.py index 0e59e01c..17be6588 100644 --- a/tests/test_diff_scope.py +++ b/tests/test_diff_scope.py @@ -381,13 +381,15 @@ def test_diff_base_flag_in_help(self): self.assertIn("--diff-base", proc.stdout) def test_invalid_ref_exits_nonzero(self): - proc = self._run_cli("secrets", "tests/fixtures", "--diff-base", "nonexistent-ref-xyz") + # Issue #199: `secrets` alias removed — use `security --check secrets` + proc = self._run_cli("security", "tests/fixtures", "--check", "secrets", "--diff-base", "nonexistent-ref-xyz") self.assertNotEqual(proc.returncode, 0) self.assertIn("diff_scope_error", proc.stderr + proc.stdout) def test_valid_ref_produces_diff_scope_in_output(self): """--diff-base HEAD~1 should add a diff_scope key to the JSON output.""" - proc = self._run_cli("secrets", "tests/fixtures", "--diff-base", "HEAD~1") + # Issue #199: `secrets` alias removed — use `security --check secrets` + proc = self._run_cli("security", "tests/fixtures", "--check", "secrets", "--diff-base", "HEAD~1") # Find JSON in output (skip stderr hint lines) import json out = proc.stdout @@ -414,7 +416,8 @@ def test_empty_diff_early_exit(self): env["PYTHONUTF8"] = "1" env.pop("CODELENS_AI_MODE", None) proc = subprocess.run( - [sys.executable, self.cli, "secrets", tmpdir, "--diff-base", "HEAD"], + # Issue #199: `secrets` alias removed — use `security --check secrets` + [sys.executable, self.cli, "security", tmpdir, "--check", "secrets", "--diff-base", "HEAD"], capture_output=True, text=True, env=env, timeout=60, cwd=self.codelens_repo, ) @@ -424,13 +427,17 @@ def test_empty_diff_early_exit(self): json_start = out.find("{") self.assertGreater(json_start, -1) data = json.loads(out[json_start:]) + # Issue #199: empty-diff early-exit returns a flat payload with + # status/message/diff_scope at the top level (the umbrella never + # runs because codelens.py short-circuits before dispatch). self.assertEqual(data["status"], "ok") self.assertIn("No changed files", data.get("message", "")) self.assertEqual(data["diff_scope"]["changed_count"], 0) def test_diff_base_before_subcommand(self): """--diff-base works both before and after the subcommand.""" - proc = self._run_cli("--diff-base", "HEAD~1", "secrets", "tests/fixtures") + # Issue #199: `secrets` alias removed — use `security --check secrets` + proc = self._run_cli("--diff-base", "HEAD~1", "security", "tests/fixtures", "--check", "secrets") import json out = proc.stdout json_start = out.find("{") diff --git a/tests/test_hybrid_engine.py b/tests/test_hybrid_engine.py index 0f859772..113160aa 100755 --- a/tests/test_hybrid_engine.py +++ b/tests/test_hybrid_engine.py @@ -167,9 +167,10 @@ def test_deep_graceful_degradation(self): test_file = os.path.join(tmpdir, "test.xyz") with open(test_file, "w") as f: f.write("hello") + # Issue #199: `dead-code` alias removed — use `audit --check dead-code` result = subprocess.run( [sys.executable, os.path.join(SCRIPT_DIR, "codelens.py"), - "dead-code", tmpdir, "--deep"], + "audit", tmpdir, "--check", "dead-code", "--deep"], capture_output=True, text=True ) assert result.returncode == 0 @@ -224,10 +225,11 @@ def test_smell_deep_invokes_create_hybrid_engine_once(self): # Call main() in-process with patched argv so the mock # is visible. Redirect stdout to suppress JSON output. + # Issue #199: `smell` alias removed — use `audit --check smell` old_argv = sys.argv import io old_stdout = sys.stdout - sys.argv = ["codelens.py", "smell", ws, "--deep", "--format", "json"] + sys.argv = ["codelens.py", "audit", ws, "--check", "smell", "--deep", "--format", "json"] sys.stdout = io.StringIO() try: from codelens import main @@ -260,10 +262,12 @@ def test_deep_unsupported_command_sets_hint(self): from commands.scan import cmd_scan cmd_scan(ws) - # symbols is NOT in the --deep supported list + # Issue #199: `symbols` alias removed — `search --mode symbol` + # is the post-#199 entry point. search is NOT in the --deep + # supported list. proc = subprocess.run( [sys.executable, "scripts/codelens.py", - "symbols", "foo", ws, "--deep", "--format", "json"], + "search", "--mode", "symbol", "foo", ws, "--deep", "--format", "json"], capture_output=True, text=True, timeout=60, env={**os.environ, "PYTHONPATH": "scripts"}, ) @@ -279,8 +283,10 @@ def test_deep_unsupported_command_sets_hint(self): 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']}" + # The hint should mention the command name (search or symbol) + hint = output["deep_analysis_hint"] + assert "search" in hint or "symbol" in hint, ( + f"hint should mention the command name, got: {hint}" ) finally: shutil.rmtree(ws, ignore_errors=True) diff --git a/tests/test_issue195_consolidation.py b/tests/test_issue195_consolidation.py index d80c0bb0..7c5ab678 100644 --- a/tests/test_issue195_consolidation.py +++ b/tests/test_issue195_consolidation.py @@ -2,10 +2,12 @@ Verifies: - All 12 umbrella commands are registered and visible in COMMAND_REGISTRY. -- --help only shows the 12 umbrella commands (hidden aliases suppressed). +- --help only shows the 12 umbrella commands (hidden commands suppressed). - --command-count reports 12. - Each umbrella command's execute() returns the {s, st, r} shape. -- Deprecated aliases print a redirect warning to stderr when invoked. +- The 32 deprecated aliases from #195 have been removed (issue #199): they + are no longer in COMMAND_REGISTRY and invoking them yields an + ``invalid choice`` argparse error instead of a deprecation warning. """ from __future__ import annotations @@ -47,47 +49,32 @@ def test_only_12_visible_commands(): ) -def test_absorbed_commands_marked_hidden_and_deprecated(): - """A sample of absorbed commands must be hidden + deprecated_alias_for set.""" - samples = { - "init": "scan", - "symbols": "search", - "semantic-query": "search", - "dead-code": "audit", - "complexity": "audit", - "secrets": "security", - "taint": "security", - "diff": "impact", - "dataflow": "impact", - "dashboard": "summary", - "arch-metrics": "summary", - "graph-schema": "api-map", - "env-check": "doctor", - "ownership": "history", - "git-status": "history", - "outline": "context", - "trace": "context", - "orient": "context", - "affected": "deps", - "dependents": "deps", - "circular": "deps", - "import-snapshot": "deps", - "staleness": "audit", - "perf-hint": "audit", - "side-effect": "audit", - "vuln-scan": "security", - "binary-scan": "security", - "regex-audit": "security", - "query-graph": "graph", - "architecture": "summary", - } - for old_name, umbrella in samples.items(): - assert old_name in COMMAND_REGISTRY, f"{old_name!r} not in registry" - info = COMMAND_REGISTRY[old_name] - assert info.get("hidden") is True, f"{old_name!r} not hidden" - assert info.get("deprecated_alias_for") == umbrella, ( - f"{old_name!r} deprecated_alias_for = {info.get('deprecated_alias_for')!r}, " - f"expected {umbrella!r}" +# The 32 deprecated aliases retained after issue #195 were removed in #199. +DEPRECATED_ALIASES_REMOVED = { + "affected", "arch-metrics", "architecture", "binary-scan", "circular", + "complexity", "dashboard", "dataflow", "dead-code", "dependents", + "diff", "env-check", "git-status", "graph-schema", "import-snapshot", + "init", "lsp-status", "orient", "outline", "ownership", "perf-hint", + "query-graph", "regex-audit", "secrets", "semantic-query", "side-effect", + "smell", "staleness", "symbols", "taint", "trace", "vuln-scan", +} + + +def test_deprecated_aliases_not_registered(): + """All 32 deprecated aliases must be absent from COMMAND_REGISTRY (#199).""" + still_registered = [a for a in DEPRECATED_ALIASES_REMOVED if a in COMMAND_REGISTRY] + assert not still_registered, ( + f"these aliases should have been removed in #199 but are still " + f"registered: {still_registered}" + ) + + +def test_deprecated_alias_modules_not_registered_as_commands(): + """No remaining command module may carry a deprecated_alias_for marker.""" + for name, info in COMMAND_REGISTRY.items(): + assert "deprecated_alias_for" not in info, ( + f"{name!r} still carries a deprecated_alias_for marker; the " + f"field was removed in #199" ) @@ -104,14 +91,6 @@ def test_dropped_commands_not_registered(): assert name not in COMMAND_REGISTRY, f"dropped command {name!r} still registered" -def test_lsp_status_hidden_redirects_to_doctor(): - """lsp-status is kept as a utility but hidden + deprecated for doctor.""" - assert "lsp-status" in COMMAND_REGISTRY - info = COMMAND_REGISTRY["lsp-status"] - assert info.get("hidden") is True - assert info.get("deprecated_alias_for") == "doctor" - - # ─── 2. CLI smoke tests ───────────────────────────────────────────── def _run_cli(*args, expect_success=True): @@ -157,17 +136,26 @@ def test_command_count_reports_12(): ) -def test_deprecated_alias_prints_warning(): - """Invoking a deprecated alias must print a redirect warning to stderr.""" - # Use a simple workspace with no .codelens so the command fails fast - # but the deprecation warning is still emitted before execution. +@pytest.mark.parametrize("alias", [ + "dead-code", "secrets", "diff", "staleness", "orient", + "query-graph", "init", "vuln-scan", "symbols", "taint", +]) +def test_deprecated_alias_invalid_choice(alias): + """Invoking a removed alias must fail with 'invalid choice' (issue #199). + + Before #199 these printed a deprecation warning and still ran; after #199 + they are no longer registered and argparse rejects them with exit code 2. + """ with tempfile.TemporaryDirectory() as ws: - result = _run_cli("dead-code", ws, expect_success=False) - assert "DEPRECATED" in result.stderr, ( - f"deprecation warning not in stderr: {result.stderr!r}" + result = _run_cli(alias, ws, expect_success=False) + assert result.returncode != 0, ( + f"codelens {alias} should fail, got exit {result.returncode}" + ) + assert "invalid choice" in result.stderr, ( + f"expected 'invalid choice' for {alias!r}, got stderr: {result.stderr!r}" ) - assert "audit" in result.stderr, ( - f"redirect target 'audit' not in stderr: {result.stderr!r}" + assert "DEPRECATED" not in result.stderr, ( + f"no deprecation warning expected after #199, got: {result.stderr!r}" ) diff --git a/tests/test_query_graph.py b/tests/test_query_graph.py index 464bbb9e..a24cd010 100644 --- a/tests/test_query_graph.py +++ b/tests/test_query_graph.py @@ -577,31 +577,29 @@ def test_truncated_flag(self, graph_db): # ─── CLI command registration ────────────────────────────────────────────── -class TestCliCommandRegistration: - """The ``query-graph`` command must auto-register from commands/query_graph.py.""" +class TestQueryGraphModule: + """``commands.query_graph`` is the implementation module invoked by the + ``graph`` umbrella command. Issue #199 removed the ``query-graph`` alias + registration; the module itself is retained because the ``graph`` (and + ``search --mode graph``) umbrella commands import it. + """ - def test_command_registered(self): - from commands import COMMAND_REGISTRY - assert "query-graph" in COMMAND_REGISTRY - info = COMMAND_REGISTRY["query-graph"] - assert "help" in info - assert "add_args" in info - assert "execute" in info - assert callable(info["add_args"]) - assert callable(info["execute"]) - - def test_command_help_mentions_match(self): + def test_module_exposes_execute_and_add_args(self): + from commands.query_graph import execute, add_args + assert callable(execute) + assert callable(add_args) + + def test_alias_not_registered(self): from commands import COMMAND_REGISTRY - help_text = COMMAND_REGISTRY["query-graph"]["help"] - assert "MATCH" in help_text - assert "Cypher" in help_text + assert "query-graph" not in COMMAND_REGISTRY, ( + "query-graph alias should have been removed in #199" + ) def test_execute_with_validate_flag(self, graph_db): """--validate flag checks syntax without touching the DB.""" - from commands import COMMAND_REGISTRY + from commands.query_graph import execute from argparse import Namespace ws, _ = graph_db - info = COMMAND_REGISTRY["query-graph"] args = Namespace( query="MATCH (f:Function) RETURN f.name", workspace=ws, @@ -609,15 +607,14 @@ def test_execute_with_validate_flag(self, graph_db): limit=None, validate=True, ) - r = info["execute"](args, ws) + r = execute(args, ws) assert r["valid"] is True def test_execute_with_cli_limit(self, graph_db): """--limit flag on CLI appends LIMIT to query.""" - from commands import COMMAND_REGISTRY + from commands.query_graph import execute from argparse import Namespace ws, db_path = graph_db - info = COMMAND_REGISTRY["query-graph"] args = Namespace( query="MATCH (f:Function) RETURN f.name", workspace=ws, @@ -625,7 +622,7 @@ def test_execute_with_cli_limit(self, graph_db): limit=2, validate=False, ) - r = info["execute"](args, ws) + r = execute(args, ws) assert r["status"] == "ok" assert r["count"] == 2 assert r["truncated"] is True diff --git a/tests/test_secrets_gitleaks.py b/tests/test_secrets_gitleaks.py index e9ca3896..c4eed99a 100644 --- a/tests/test_secrets_gitleaks.py +++ b/tests/test_secrets_gitleaks.py @@ -503,40 +503,46 @@ def _run_cli(self, *args): return proc def test_no_gitleaks_flag_in_help(self): - proc = self._run_cli("secrets", "--help") + # Issue #199: `secrets` alias removed — use `security --help` + proc = self._run_cli("security", "--help") self.assertIn("--no-gitleaks", proc.stdout) def test_secrets_runs_with_regex_backend_when_gitleaks_absent(self): """When gitleaks is not installed, backend should be 'regex'.""" - proc = self._run_cli("secrets", "tests/fixtures") + # Issue #199: `secrets` alias removed — use `security --check secrets` + proc = self._run_cli("security", "tests/fixtures", "--check", "secrets") import json as _json out = proc.stdout json_start = out.find("{") self.assertGreater(json_start, -1, "No JSON in output") data = _json.loads(out[json_start:]) - self.assertEqual(data["backend"], "regex") + # security umbrella wraps secrets under data["r"][0] + self.assertEqual(data["r"][0]["backend"], "regex") # gitleaks_hint should be present (telling user how to install) - self.assertTrue(data.get("gitleaks_hint")) - self.assertIn("gitleaks", data["gitleaks_hint"]) + self.assertTrue(data["r"][0].get("gitleaks_hint")) + hint = data["r"][0].get("gitleaks_hint", "") + self.assertIn("gitleaks", hint) def test_no_gitleaks_flag_suppresses_hint(self): """--no-gitleaks should suppress the gitleaks_hint.""" - proc = self._run_cli("secrets", "tests/fixtures", "--no-gitleaks") + # Issue #199: `secrets` alias removed — use `security --check secrets` + proc = self._run_cli("security", "tests/fixtures", "--check", "secrets", "--no-gitleaks") import json as _json out = proc.stdout json_start = out.find("{") data = _json.loads(out[json_start:]) - self.assertEqual(data["backend"], "regex") - self.assertFalse(data.get("gitleaks_hint")) + self.assertEqual(data["r"][0]["backend"], "regex") + self.assertFalse(data["r"][0].get("gitleaks_hint")) def test_stats_backend_field_set(self): """stats.backend should be set so compact/ai formatters pick it up.""" - proc = self._run_cli("secrets", "tests/fixtures") + # Issue #199: `secrets` alias removed — use `security --check secrets` + proc = self._run_cli("security", "tests/fixtures", "--check", "secrets") import json as _json out = proc.stdout json_start = out.find("{") data = _json.loads(out[json_start:]) - self.assertEqual(data["stats"]["backend"], "regex") + self.assertEqual(data["r"][0]["stats"]["backend"], "regex") if __name__ == "__main__": diff --git a/tests/test_semantic_search_engine.py b/tests/test_semantic_search_engine.py index b709bc7a..5e4598e0 100644 --- a/tests/test_semantic_search_engine.py +++ b/tests/test_semantic_search_engine.py @@ -522,26 +522,24 @@ def test_multiple_query_terms_prefer_documents_with_all(self, workspace): # ─── CLI registration smoke test ────────────────────────────── class TestCommandRegistration: - """Verify the semantic-query command is registered and importable.""" + """Issue #199 removed the ``semantic-query`` CLI alias entirely. - def test_command_registered(self): - # Importing commands.semantic_query should register it - from commands import COMMAND_REGISTRY - assert "semantic-query" in COMMAND_REGISTRY, ( - "semantic-query must be in COMMAND_REGISTRY after import" - ) + The ``commands.semantic_query`` module was deleted because no umbrella + command imports it (semantic search is reached via ``search --mode + semantic``). These tests verify the removal is complete: the module + must not be importable and the alias must not be in COMMAND_REGISTRY. + """ - def test_command_has_help_text(self): + def test_command_not_registered(self): from commands import COMMAND_REGISTRY - info = COMMAND_REGISTRY["semantic-query"] - assert info["help"] - assert "semantic" in info["help"].lower() or "tf-idf" in info["help"].lower() + assert "semantic-query" not in COMMAND_REGISTRY, ( + "semantic-query alias should have been removed in #199" + ) - def test_command_module_imports_cleanly(self): - # Re-import to make sure no import-time errors + def test_module_deleted(self): import importlib - import commands.semantic_query as mod - importlib.reload(mod) + with pytest.raises(ModuleNotFoundError): + importlib.import_module("commands.semantic_query") if __name__ == "__main__": diff --git a/tests/test_staleness.py b/tests/test_staleness.py index 4478b91a..95f046df 100644 --- a/tests/test_staleness.py +++ b/tests/test_staleness.py @@ -472,13 +472,23 @@ def test_module_function_caches(self, tmp_path, reset_default_detector): class TestStalenessCommand: - """The ``codelens staleness`` CLI command.""" + """The ``staleness`` CLI alias was removed in issue #199. + + The implementation module ``commands.staleness`` survives because the + ``audit`` umbrella command imports it for its ``--check staleness`` + sub-analysis. The CLI alias ``codelens staleness`` is no longer + registered and must yield an ``invalid choice`` argparse error. + """ def test_command_is_registered(self): - assert "staleness" in COMMAND_REGISTRY - info = COMMAND_REGISTRY["staleness"] - assert callable(info["execute"]) - assert callable(info["add_args"]) + assert "staleness" not in COMMAND_REGISTRY, ( + "staleness alias should have been removed in #199" + ) + # The implementation module must still be importable (audit dep). + import importlib + mod = importlib.import_module("commands.staleness") + assert callable(mod.execute) + assert callable(mod.add_args) def test_no_workspace_returns_error(self): from commands import staleness as cmd @@ -536,7 +546,11 @@ def test_stale_file_appears_in_output(self, tmp_path, reset_default_detector): class TestCLISmoke: - """End-to-end: invoke ``codelens staleness`` as a real subprocess.""" + """End-to-end: invoke ``codelens audit --check staleness`` as a real subprocess. + + Issue #199 removed the ``codelens staleness`` CLI alias; the staleness + sub-analysis is now reached via the ``audit`` umbrella command. + """ def _run_cli(self, workspace, *extra_args): env = os.environ.copy() @@ -546,8 +560,10 @@ def _run_cli(self, workspace, *extra_args): [ sys.executable, os.path.join(_SCRIPTS_DIR, "codelens.py"), - "staleness", + "audit", workspace, + "--check", + "staleness", "--format", "json", *extra_args, @@ -569,8 +585,10 @@ def test_staleness_runs_cleanly_on_empty_workspace(self, tmp_path): start = out.find("{") assert start >= 0 payload = json.loads(out[start:]) - assert payload["status"] == "ok" - assert payload["stale_count"] == 0 + # audit umbrella wraps staleness under payload["r"][0] + assert payload["s"] == "ok" + assert payload["r"][0]["_check"] == "staleness" + assert payload["r"][0]["stale_count"] == 0 # ─── Regression: positional workspace arg (issue #178) ──────────────────── @@ -586,15 +604,23 @@ class TestStalenessWorkspaceArgRegression: been fixed, but these tests pin the expected behavior so any future regression is caught immediately. + Issue #199 removed the ``staleness`` CLI alias; these regression tests + now exercise the positional ``workspace`` arg via the ``audit`` + umbrella (``codelens audit --check staleness ``), which is + the post-#199 entry point for the staleness sub-analysis. The + underlying argparse behavior (optional ``workspace`` positional with + ``nargs="?"``) is identical because ``audit.add_args`` registers + ``workspace`` the same way the old ``staleness.add_args`` did. + Definition of Done (from issue #178): - - ``codelens staleness /path/to/workspace`` works without error - - ``codelens staleness`` (no args) still auto-detects as before + - ``codelens audit --check staleness /path/to/workspace`` works without error + - ``codelens audit --check staleness`` (no args) still auto-detects as before - Consistent with how other commands handle the optional ``workspace`` positional """ def _run_cli(self, *args): - """Invoke ``codelens staleness`` with arbitrary args, capture result.""" + """Invoke ``codelens audit --check staleness`` with arbitrary args.""" env = os.environ.copy() env["PYTHONPATH"] = _SCRIPTS_DIR env["PYTHONUTF8"] = "1" @@ -602,6 +628,8 @@ def _run_cli(self, *args): [ sys.executable, os.path.join(_SCRIPTS_DIR, "codelens.py"), + "audit", + "--check", "staleness", *args, ], @@ -660,7 +688,7 @@ def test_no_args_auto_detects_and_exits_zero(self, tmp_path, monkeypatch): ) def test_positional_workspace_with_json_format(self, tmp_path): - """``codelens staleness --format json`` produces valid JSON. + """``codelens audit --check staleness --format json`` produces valid JSON. Combines the positional arg with the format flag to verify they don't conflict. This is the exact pattern used in CI pipelines. @@ -674,8 +702,10 @@ def test_positional_workspace_with_json_format(self, tmp_path): start = out.find("{") assert start >= 0, f"no JSON in stdout:\n{out}" payload = json.loads(out[start:]) - assert payload["status"] == "ok" - assert payload["workspace"] == os.path.abspath(str(tmp_path)) + # audit umbrella wraps staleness under payload["r"][0] + assert payload["s"] == "ok" + assert payload["r"][0]["_check"] == "staleness" + assert payload["r"][0]["workspace"] == os.path.abspath(str(tmp_path)) def test_workspace_positional_is_optional_nargs_question(self): """The ``workspace`` arg is registered with ``nargs="?"`` (optional). @@ -741,12 +771,29 @@ def _get_workspace_action(add_args_fn): ) def test_help_shows_workspace_positional(self): - """``staleness --help`` lists ``[workspace]`` as a positional arg. + """``audit --check staleness --help`` lists ``[workspace]`` as a positional arg. - A regression that removes the positional would also remove it - from ``--help``. This test catches that. + Issue #199: the ``staleness`` alias is gone; the help entry point + is now ``audit --help``. The ``workspace`` positional must still + appear (it is registered by ``audit.add_args``). """ - result = self._run_cli("--help") + # Bypass _run_cli because it injects --check staleness before --help, + # which argparse would reject. Invoke audit --help directly. + env = os.environ.copy() + env["PYTHONPATH"] = _SCRIPTS_DIR + env["PYTHONUTF8"] = "1" + result = subprocess.run( + [ + sys.executable, + os.path.join(_SCRIPTS_DIR, "codelens.py"), + "audit", + "--help", + ], + capture_output=True, + text=True, + env=env, + timeout=30, + ) # argparse exits 0 on --help. assert result.returncode == 0, ( f"exit={result.returncode}\nstderr={result.stderr}" @@ -754,7 +801,7 @@ def test_help_shows_workspace_positional(self): # The usage line should contain "workspace" as a positional. help_text = result.stdout + result.stderr assert "workspace" in help_text, ( - f"staleness --help does not mention 'workspace' positional.\n" + f"audit --help does not mention 'workspace' positional.\n" f"stdout={result.stdout}\nstderr={result.stderr}" )