diff --git a/src/serena/cli.py b/src/serena/cli.py index 98b8ed66..b13d3860 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -429,8 +429,7 @@ class TopLevelCommands(AutoRegisteringGroup): modes_selection_def: ModeSelectionDefinition | None = None if modes: modes_selection_def = ModeSelectionDefinition(default_modes=modes) - serena_config = SerenaConfig.from_config_file() - serena_config.web_dashboard = False + serena_config = SerenaConfig.from_config_file().with_headless_mode_overrides() print(serena_config.default_modes) print(serena_config.base_modes) @@ -928,10 +927,8 @@ class ProjectCommands(AutoRegisteringGroup): logging.configure(level=logging.INFO) project_path = os.path.abspath(project) - serena_config = SerenaConfig.from_config_file() + serena_config = SerenaConfig.from_config_file().with_headless_mode_overrides() serena_config.language_backend = LanguageBackend.LSP - serena_config.gui_log_window = False - serena_config.web_dashboard = False proj = Project.load(project_path, serena_config=serena_config) # Create log file with timestamp @@ -1113,7 +1110,7 @@ class ToolCommands(AutoRegisteringGroup): agent = SerenaAgent( project=None, - serena_config=SerenaConfig(web_dashboard=False, log_level=logging.INFO), + serena_config=SerenaConfig(log_level=logging.INFO).with_headless_mode_overrides(), context=serena_context, ) tool = agent.get_tool_by_name(tool_name) diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index aaf17fd4..4aaabdec 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -1027,6 +1027,19 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): config._save() return config + def with_headless_mode_overrides(self) -> "SerenaConfig": + """ + Modifies this instance to apply overrides for headless mode, where any GUI/user interaction-based features are disabled. + This is intended to be applied for cases where a `SerenaConfig` instance is needed to instantiate a `SerenaAgent` instance + while the user is not expected to interact with the system (e.g. a CLI command or a test). + + :return: the instance with overrides applied for headless mode + """ + self.gui_log_window = False + self.web_dashboard = False + self.jetbrains_launch_command = None + return self + @cached_property def project_paths(self) -> list[str]: return sorted(str(project.project_root) for project in self.projects) diff --git a/test/serena/config/test_global_ignored_paths.py b/test/serena/config/test_global_ignored_paths.py index da9fadb9..862ac2f2 100644 --- a/test/serena/config/test_global_ignored_paths.py +++ b/test/serena/config/test_global_ignored_paths.py @@ -20,7 +20,7 @@ def _create_test_project( ignored_paths=project_ignored_paths or [], ignore_all_files_in_gitignore=False, ) - serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=global_ignored_paths) + serena_config = SerenaConfig(ignored_paths=global_ignored_paths).with_headless_mode_overrides() return Project( project_root=str(project_root), project_config=config, @@ -149,7 +149,7 @@ class TestRegisteredProjectGlobalIgnoredPaths: ignored_paths=[], ignore_all_files_in_gitignore=False, ) - serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=["node_modules"]) + serena_config = SerenaConfig(ignored_paths=["node_modules"]).with_headless_mode_overrides() registered = RegisteredProject( project_root=str(self.project_path), project_config=config, @@ -169,7 +169,7 @@ class TestRegisteredProjectGlobalIgnoredPaths: project_root=str(self.project_path), project_config=config, ) - serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=[]) + serena_config = SerenaConfig(ignored_paths=[]).with_headless_mode_overrides() project = registered.get_project_instance(serena_config=serena_config) assert not project.is_ignored_path(str(self.project_path / "node_modules" / "pkg.js")) @@ -181,7 +181,7 @@ class TestRegisteredProjectGlobalIgnoredPaths: (serena_dir / "project.yml").write_text( 'project_name: "test_project"\nlanguages: ["python"]\nignored_paths: []\nignore_all_files_in_gitignore: false\n' ) - serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=["node_modules"]) + serena_config = SerenaConfig(ignored_paths=["node_modules"]).with_headless_mode_overrides() registered = RegisteredProject.from_project_root( str(self.project_path), serena_config=serena_config, @@ -197,7 +197,7 @@ class TestRegisteredProjectGlobalIgnoredPaths: ignored_paths=[], ignore_all_files_in_gitignore=False, ) - serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=["node_modules"]) + serena_config = SerenaConfig(ignored_paths=["node_modules"]).with_headless_mode_overrides() project = Project( project_root=str(self.project_path), project_config=config, @@ -237,7 +237,7 @@ class TestGlobalIgnoredPathsWithGitignore: ignored_paths=["build"], ignore_all_files_in_gitignore=True, ) - serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=["node_modules"]) + serena_config = SerenaConfig(ignored_paths=["node_modules"]).with_headless_mode_overrides() project = Project( project_root=str(self.project_path), project_config=config, @@ -258,14 +258,12 @@ class TestSerenaConfigIgnoredPaths: def test_serena_config_default_ignored_paths(self) -> None: """SerenaConfig defaults to empty ignored_paths.""" - config = SerenaConfig(gui_log_window=False, web_dashboard=False) + config = SerenaConfig().with_headless_mode_overrides() assert config.ignored_paths == [] def test_serena_config_with_ignored_paths(self) -> None: """SerenaConfig can be created with explicit ignored_paths.""" config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, ignored_paths=["node_modules", "*.log", "build"], - ) + ).with_headless_mode_overrides() assert config.ignored_paths == ["node_modules", "*.log", "build"] diff --git a/test/serena/config/test_serena_config.py b/test/serena/config/test_serena_config.py index 4848e8a1..0a3cb43c 100644 --- a/test/serena/config/test_serena_config.py +++ b/test/serena/config/test_serena_config.py @@ -208,11 +208,9 @@ def _make_config_with_project( ) -> tuple[SerenaConfig, str]: """Create a SerenaConfig with a single registered project and return (config, project_name).""" config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, log_level=logging.ERROR, language_backend=global_backend, - ) + ).with_headless_mode_overrides() project = Project( project_root=str(Path(__file__).parent.parent / "resources" / "repos" / "python" / "test_repo"), project_config=ProjectConfig( @@ -252,11 +250,9 @@ class TestEffectiveLanguageBackend: def test_no_project_uses_global_backend(self): """When no startup project is provided, effective backend is the global one.""" config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, log_level=logging.ERROR, language_backend=LanguageBackend.LSP, - ) + ).with_headless_mode_overrides() agent = SerenaAgent(project=None, serena_config=config) try: assert agent.get_language_backend() == LanguageBackend.LSP @@ -338,90 +334,68 @@ class TestGetConfiguredProjectSerenaFolder: """Tests for SerenaConfig.get_configured_project_serena_folder (pure template resolution).""" def test_default_location(self): - config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, - ) + config = SerenaConfig().with_headless_mode_overrides() result = config.get_configured_project_serena_folder("/home/user/myproject") assert result == os.path.abspath("/home/user/myproject/.serena") def test_custom_location_with_project_folder_name(self): config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, project_serena_folder_location="/projects-metadata/$projectFolderName/.serena", - ) + ).with_headless_mode_overrides() result = config.get_configured_project_serena_folder("/home/user/myproject") assert result == os.path.abspath("/projects-metadata/myproject/.serena") def test_custom_location_with_project_dir(self): config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, project_serena_folder_location="$projectDir/.custom-serena", - ) + ).with_headless_mode_overrides() result = config.get_configured_project_serena_folder("/home/user/myproject") assert result == os.path.abspath("/home/user/myproject/.custom-serena") def test_custom_location_with_both_placeholders(self): config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, project_serena_folder_location="/data/$projectFolderName/$projectDir/.serena", - ) + ).with_headless_mode_overrides() result = config.get_configured_project_serena_folder("/home/user/proj") assert result == os.path.abspath("/data/proj/home/user/proj/.serena") def test_default_field_value(self): - config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, - ) + config = SerenaConfig().with_headless_mode_overrides() assert config.project_serena_folder_location == DEFAULT_PROJECT_SERENA_FOLDER_LOCATION def test_rejects_unknown_placeholder(self): config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, project_serena_folder_location="$projectDir/$unknownVar/.serena", - ) + ).with_headless_mode_overrides() with pytest.raises(SerenaConfigError, match=r"Unknown placeholder '\$unknownVar'"): config.get_configured_project_serena_folder("/home/user/myproject") def test_rejects_typo_projectDirs(self): """$projectDirs should not be silently treated as $projectDir + 's'.""" config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, project_serena_folder_location="$projectDirs/.serena", - ) + ).with_headless_mode_overrides() with pytest.raises(SerenaConfigError, match=r"Unknown placeholder '\$projectDirs'"): config.get_configured_project_serena_folder("/home/user/myproject") def test_rejects_typo_projectfoldername_lowercase(self): config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, project_serena_folder_location="/data/$projectfoldername/.serena", - ) + ).with_headless_mode_overrides() with pytest.raises(SerenaConfigError, match=r"Unknown placeholder '\$projectfoldername'"): config.get_configured_project_serena_folder("/home/user/myproject") def test_no_placeholders_is_valid(self): config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, project_serena_folder_location="/fixed/path/.serena", - ) + ).with_headless_mode_overrides() result = config.get_configured_project_serena_folder("/home/user/myproject") assert result == os.path.abspath("/fixed/path/.serena") def test_error_message_lists_supported_placeholders(self): config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, project_serena_folder_location="$bogus/.serena", - ) + ).with_headless_mode_overrides() with pytest.raises(SerenaConfigError, match=r"\$projectDir.*\$projectFolderName|\$projectFolderName.*\$projectDir"): config.get_configured_project_serena_folder("/home/user/myproject") @@ -452,7 +426,7 @@ class TestProjectSerenaDataFolder: return project def test_default_config_creates_in_project_dir(self): - config = SerenaConfig(gui_log_window=False, web_dashboard=False) + config = SerenaConfig().with_headless_mode_overrides() project = self._make_project(config) expected = os.path.abspath(str(self.project_path / SERENA_MANAGED_DIR_NAME)) assert project.path_to_serena_data_folder() == expected @@ -461,10 +435,8 @@ class TestProjectSerenaDataFolder: custom_base = Path(self.test_dir) / "metadata" custom_base.mkdir() config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, project_serena_folder_location=str(custom_base) + "/$projectFolderName/.serena", - ) + ).with_headless_mode_overrides() project = self._make_project(config) expected = os.path.abspath(str(custom_base / "myproject" / ".serena")) assert project.path_to_serena_data_folder() == expected @@ -474,10 +446,8 @@ class TestProjectSerenaDataFolder: existing_serena = self.project_path / SERENA_MANAGED_DIR_NAME existing_serena.mkdir() config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, project_serena_folder_location="/nonexistent/path/$projectFolderName/.serena", - ) + ).with_headless_mode_overrides() project = self._make_project(config) assert project.path_to_serena_data_folder() == str(existing_serena) @@ -491,10 +461,8 @@ class TestProjectSerenaDataFolder: custom_serena.mkdir(parents=True) config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, project_serena_folder_location=str(custom_base) + "/$projectFolderName/.serena", - ) + ).with_headless_mode_overrides() project = self._make_project(config) assert project.path_to_serena_data_folder() == str(custom_serena) diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index c1e2578f..5bbd0a6d 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -758,7 +758,7 @@ SAFE_DELETE_SUCCEEDS_CASES = [ @pytest.fixture def serena_config(): - config = SerenaConfig(gui_log_window=False, web_dashboard=False, log_level=logging.ERROR) + config = SerenaConfig(log_level=logging.ERROR).with_headless_mode_overrides() # Create test projects for all supported languages test_projects = [] @@ -867,7 +867,7 @@ class TestSerenaAgent: * an invalid project path is specified at startup All cases must not raise an exception. """ - serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False) + serena_config = SerenaConfig().with_headless_mode_overrides() SerenaAgent(project=project, serena_config=serena_config) def _symbol_matches_expected_name(self, symbol: dict, expected_name: str) -> bool: diff --git a/test/serena/test_tool_parameter_types.py b/test/serena/test_tool_parameter_types.py index 1299cb3a..42b8948a 100644 --- a/test/serena/test_tool_parameter_types.py +++ b/test/serena/test_tool_parameter_types.py @@ -13,7 +13,7 @@ def test_all_tool_parameters_have_type(context): For every tool exposed by Serena, ensure that the generated Open‑AI schema contains a ``type`` entry for each parameter. """ - cfg = SerenaConfig(gui_log_window=False, web_dashboard=False, log_level=logging.ERROR) + cfg = SerenaConfig(log_level=logging.ERROR).with_headless_mode_overrides() registry = ToolRegistry() cfg.included_optional_tools = tuple(registry.get_tool_names_optional()) factory = SerenaMCPFactory(transport="stdio", context=context)