From 6e1c8a422500f58c5bf6b10b804e6d166ad7a357 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sat, 9 Aug 2025 16:41:24 +0200 Subject: [PATCH] CLI: added health-check command [ci skip] --- src/serena/cli.py | 148 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/src/serena/cli.py b/src/serena/cli.py index 5a724dbc..88aecd6a 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -1,4 +1,5 @@ import glob +import json import os import shutil import subprocess @@ -9,6 +10,7 @@ from typing import Any, Literal import click from sensai.util import logging +from sensai.util.logging import FileLoggerContext, datetime_tag from tqdm import tqdm from serena.agent import SerenaAgent @@ -28,7 +30,7 @@ from serena.constants import ( ) from serena.mcp import SerenaMCPFactory, SerenaMCPFactorySingleProcess from serena.project import Project -from serena.tools import ToolRegistry +from serena.tools import FindReferencingSymbolsTool, FindSymbolTool, GetSymbolsOverviewTool, SearchForPatternTool, ToolRegistry from serena.util.logging import MemoryLogHandler from solidlsp.ls_config import Language from solidlsp.util.subprocess_util import subprocess_kwargs @@ -506,6 +508,150 @@ class ProjectCommands(AutoRegisteringGroup): ls.save_cache() click.echo(f"Successfully indexed file '{file}', {len(symbols)} symbols saved to {ls.cache_path}.") + @staticmethod + @click.command("health-check", help="Perform a comprehensive health check of the project's tools and language server.") + @click.argument("project", type=click.Path(exists=True, file_okay=False, dir_okay=True), default=os.getcwd()) + def health_check(project: str) -> None: + """ + Perform a comprehensive health check of the project's tools and language server. + + :param project: path to the project directory, defaults to the current working directory. + """ + # NOTE: completely written by Claude Code, only functionality was reviewed, not implementation + logging.configure(level=logging.INFO) + project_path = os.path.abspath(project) + proj = Project.load(project_path) + + # Create log file with timestamp + timestamp = datetime_tag() + log_dir = os.path.join(project_path, ".serena", "logs", "health-checks") + os.makedirs(log_dir, exist_ok=True) + log_file = os.path.join(log_dir, f"health_check_{timestamp}.log") + + with FileLoggerContext(log_file, append=False, enabled=True): + log.info("Starting health check for project: %s", project_path) + + try: + # Create SerenaAgent with dashboard disabled + log.info("Creating SerenaAgent with disabled dashboard...") + config = SerenaConfig(gui_log_window_enabled=False, web_dashboard=False) + agent = SerenaAgent(project=project_path, serena_config=config) + log.info("SerenaAgent created successfully") + + # Find first non-empty file that can be analyzed + log.info("Searching for analyzable files...") + files = proj.gather_source_files() + target_file = None + + for file_path in files: + try: + full_path = os.path.join(project_path, file_path) + if os.path.getsize(full_path) > 0: + target_file = file_path + log.info("Found analyzable file: %s", target_file) + break + except (OSError, FileNotFoundError): + continue + + if not target_file: + log.error("No analyzable files found in project") + click.echo("❌ Health check failed: No analyzable files found") + click.echo(f"Log saved to: {log_file}") + return + + # Get tools from agent + overview_tool = agent.get_tool(GetSymbolsOverviewTool) + find_symbol_tool = agent.get_tool(FindSymbolTool) + find_refs_tool = agent.get_tool(FindReferencingSymbolsTool) + search_pattern_tool = agent.get_tool(SearchForPatternTool) + + # Test 1: Get symbols overview + log.info("Testing GetSymbolsOverviewTool on file: %s", target_file) + overview_result = agent.execute_task(lambda: overview_tool.apply(target_file)) + overview_data = json.loads(overview_result) + log.info("GetSymbolsOverviewTool returned %d symbols", len(overview_data)) + + if not overview_data: + log.error("No symbols found in file %s", target_file) + click.echo("❌ Health check failed: No symbols found in target file") + click.echo(f"Log saved to: {log_file}") + return + + # Extract suitable symbol (prefer class or function over variables) + # LSP symbol kinds: 5=class, 12=function, 6=method, 9=constructor + preferred_kinds = [5, 12, 6, 9] # class, function, method, constructor + + selected_symbol = None + for symbol in overview_data: + if symbol.get("kind") in preferred_kinds: + selected_symbol = symbol + break + + # If no preferred symbol found, use first available + if not selected_symbol: + selected_symbol = overview_data[0] + log.info("No class or function found, using first available symbol") + + symbol_name = selected_symbol.get("name_path", "unknown") + symbol_kind = selected_symbol.get("kind", "unknown") + log.info("Using symbol for testing: %s (kind: %d)", symbol_name, symbol_kind) + + # Test 2: FindSymbolTool + log.info("Testing FindSymbolTool for symbol: %s", symbol_name) + find_symbol_result = agent.execute_task( + lambda: find_symbol_tool.apply(symbol_name, relative_path=target_file, include_body=True) + ) + find_symbol_data = json.loads(find_symbol_result) + log.info("FindSymbolTool found %d matches for symbol %s", len(find_symbol_data), symbol_name) + + # Test 3: FindReferencingSymbolsTool + log.info("Testing FindReferencingSymbolsTool for symbol: %s", symbol_name) + try: + find_refs_result = agent.execute_task(lambda: find_refs_tool.apply(symbol_name, relative_path=target_file)) + find_refs_data = json.loads(find_refs_result) + log.info("FindReferencingSymbolsTool found %d references for symbol %s", len(find_refs_data), symbol_name) + except Exception as e: + log.warning("FindReferencingSymbolsTool failed for symbol %s: %s", symbol_name, str(e)) + find_refs_data = [] + + # Test 4: SearchForPatternTool to verify references + log.info("Testing SearchForPatternTool for pattern: %s", symbol_name) + try: + search_result = agent.execute_task( + lambda: search_pattern_tool.apply(substring_pattern=symbol_name, restrict_search_to_code_files=True) + ) + search_data = json.loads(search_result) + pattern_matches = sum(len(matches) for matches in search_data.values()) + log.info("SearchForPatternTool found %d pattern matches for %s", pattern_matches, symbol_name) + except Exception as e: + log.warning("SearchForPatternTool failed for pattern %s: %s", symbol_name, str(e)) + pattern_matches = 0 + + # Verify tools worked as expected + tools_working = True + if not find_symbol_data: + log.error("FindSymbolTool returned no results") + tools_working = False + + if len(find_refs_data) == 0 and pattern_matches == 0: + log.warning( + "Both FindReferencingSymbolsTool and SearchForPatternTool found no matches - this might indicate an issue" + ) + + log.info("Health check completed successfully") + + if tools_working: + click.echo("✅ Health check passed - All tools working correctly") + else: + click.echo("⚠️ Health check completed with warnings - Check log for details") + + except Exception as e: + log.exception("Health check failed with exception: %s", str(e)) + click.echo(f"❌ Health check failed: {e!s}") + + finally: + click.echo(f"Log saved to: {log_file}") + class ToolCommands(AutoRegisteringGroup): """Group for 'tool' subcommands."""