Merge pull request #1590 from oraios/serena-config-headless-mode

Add SerenaConfig.with_headless_mode_overrides and apply it
This commit is contained in:
Dr. Dominik Jain authored and GitHub committed 2026-06-24 12:14:01 +02:00
commit a0435467b6
6 files changed
+43 -67

No files matched your search

+3 -6
View File
@@ -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)
+13
View File
@@ -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)
@@ -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"]
+16 -48
View File
@@ -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)
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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)