diff --git a/aw_server/config.py b/aw_server/config.py index 16fb942..f5836dc 100644 --- a/aw_server/config.py +++ b/aw_server/config.py @@ -1,6 +1,8 @@ +import os + from aw_core.config import load_config_toml -from .profile import DEFAULT_PROFILE, is_testing +from .profile import DEFAULT_PROFILE, ENV_VAR, TESTING_PROFILE, is_testing default_config = """ [server] @@ -21,18 +23,57 @@ """.strip() -def load_config(): +def _using_legacy_testing_root() -> bool: + """True when testing data still lives on the shared ``activitywatch/`` root.""" + try: + from aw_core.dirs import using_legacy_testing_root + + return using_legacy_testing_root() + except ImportError: + return os.environ.get(ENV_VAR) == TESTING_PROFILE + + +def default_config_for(profile: str) -> str: + """Default TOML for this profile's config file. + + Isolated roots (including a fresh ``activitywatch-testing/``) get a + single ``[server]`` section — the directory already isolates, and + ``[server-testing]`` in the same file is the pre-profile model. + The two-section default stays on the shared root so legacy testing + still finds ``[server-testing]`` next to prod. + """ + if profile != DEFAULT_PROFILE and not _using_legacy_testing_root(): + port = default_port(profile) + return f""" +[server] +host = "localhost" +port = "{port}" +storage = "peewee" +cors_origins = "" + +[server.custom_static] +""".strip() + return default_config + + +def load_config(profile: str = DEFAULT_PROFILE): """Load aw-server.toml from the current profile's config dir. Must be called *after* ``export_profile`` so aw-core dirs see ``AW_PROFILE`` and isolate the file from other instances. """ - return load_config_toml("aw-server", default_config) + return load_config_toml("aw-server", default_config_for(profile)) def config_section(profile: str) -> str: - """TOML section for this profile: ``server`` or ``server-``.""" - return "server" if profile == DEFAULT_PROFILE else f"server-{profile}" + """TOML section for this profile. + + Isolated roots use ``[server]``. ``[server-testing]`` remains only + for the legacy shared-root testing layout (same 3-rule as aw-core#152). + """ + if profile != DEFAULT_PROFILE and _using_legacy_testing_root(): + return f"server-{profile}" + return "server" def default_port(profile: str) -> int: diff --git a/aw_server/main.py b/aw_server/main.py index c4dd518..2785344 100644 --- a/aw_server/main.py +++ b/aw_server/main.py @@ -118,10 +118,10 @@ def parse_settings(): testing = is_testing(profile) """ Parse config file """ - config = load_config() + config = load_config(profile) section = config_section(profile) if section not in config: - if profile not in (DEFAULT_PROFILE,): + if profile not in (DEFAULT_PROFILE,) and section != "server": logger.warning( "Profile %s has no [%s] section, falling back to [server] " "(port %s may collide with the default instance)", diff --git a/aw_server/settings.py b/aw_server/settings.py index 65c2971..e285f0a 100644 --- a/aw_server/settings.py +++ b/aw_server/settings.py @@ -6,12 +6,23 @@ from .profile import profile_from_env, profile_suffix +def _settings_suffix(testing: bool) -> str: + """Bare ``settings.json`` in isolated roots; ``-testing`` only in legacy.""" + try: + from aw_core.dirs import legacy_testing_suffix + + return legacy_testing_suffix(testing) + except ImportError: + return profile_suffix(profile_from_env(testing=testing)) + + class Settings: def __init__(self, testing: bool): - # Dir isolation (AW_PROFILE) already separates profiles; the filename - # suffix is the pre-profile workaround and stays so --testing still - # finds settings-testing.json. Named profiles get the same shape. - filename = f"settings{profile_suffix(profile_from_env(testing=testing))}.json" + # Isolated roots (including new-style activitywatch-testing/) use + # bare settings.json — the directory already isolates. The + # settings-testing.json suffix stays so legacy shared-root testing + # still finds its file next to prod. + filename = f"settings{_settings_suffix(testing)}.json" self.config_file = Path(get_config_dir("aw-server")) / filename self.load() diff --git a/tests/test_profile_config.py b/tests/test_profile_config.py index 3be4ade..4a6b92f 100644 --- a/tests/test_profile_config.py +++ b/tests/test_profile_config.py @@ -2,6 +2,7 @@ import os import sys +from pathlib import Path import pytest @@ -13,17 +14,66 @@ @pytest.fixture def xdg_tmp(tmp_path, monkeypatch): - monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config")) - monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "data")) - monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / "cache")) + """Isolate dirs on every OS. + + ``XDG_*_HOME`` only redirects platformdirs on Linux. Windows/macOS keep + using APPDATA / ~/Library, so a marker planted under the XDG data path + is invisible there (CI: Test on windows-latest / macOS-latest). Patch + the same platformdirs getters aw-core tests patch. + """ + data = tmp_path / "data" + config = tmp_path / "config" + cache = tmp_path / "cache" + + def _join(root: Path, appname: str) -> str: + return str(root / appname) + + monkeypatch.setenv("XDG_CONFIG_HOME", str(config)) + monkeypatch.setenv("XDG_DATA_HOME", str(data)) + monkeypatch.setenv("XDG_CACHE_HOME", str(cache)) + monkeypatch.setattr( + "aw_core.dirs.platformdirs.user_data_dir", + lambda appname, *a, **k: _join(data, appname), + ) + monkeypatch.setattr( + "aw_core.dirs.platformdirs.user_config_dir", + lambda appname, *a, **k: _join(config, appname), + ) + monkeypatch.setattr( + "aw_core.dirs.platformdirs.user_cache_dir", + lambda appname, *a, **k: _join(cache, appname), + ) + monkeypatch.setattr( + "aw_core.dirs.platformdirs.user_cache_path", + lambda appname, *a, **k: cache / appname, + ) + monkeypatch.setattr( + "aw_core.dirs.platformdirs.user_log_dir", + lambda appname, *a, **k: str(cache / appname / "log"), + ) monkeypatch.delenv("AW_PROFILE", raising=False) return tmp_path +def _plant_legacy_testing_db(xdg_tmp: Path) -> None: + data = xdg_tmp / "data" / "activitywatch" / "aw-server" + data.mkdir(parents=True) + (data / "peewee-sqlite-testing.v2.db").write_text("") + + class TestConfigHelpers: - def test_sections_are_disjoint(self): - sections = {config_section(p) for p in ("default", "testing", "research")} - assert sections == {"server", "server-testing", "server-research"} + def test_isolated_roots_use_server_section(self, xdg_tmp, monkeypatch): + monkeypatch.delenv("AW_PROFILE", raising=False) + assert config_section("default") == "server" + monkeypatch.setenv("AW_PROFILE", "testing") + assert config_section("testing") == "server" + monkeypatch.setenv("AW_PROFILE", "research") + assert config_section("research") == "server" + + def test_legacy_testing_keeps_server_testing_section(self, xdg_tmp, monkeypatch): + _plant_legacy_testing_db(xdg_tmp) + monkeypatch.setenv("AW_PROFILE", "testing") + assert config_section("testing") == "server-testing" def test_default_ports(self): assert default_port(DEFAULT_PROFILE) == 5600 @@ -32,29 +82,26 @@ def test_default_ports(self): class TestSettingsFilename: - def test_testing_keeps_legacy_name(self, tmp_path, monkeypatch): - monkeypatch.setattr( - "aw_server.settings.get_config_dir", lambda module: str(tmp_path) - ) - monkeypatch.delenv("AW_PROFILE", raising=False) + def test_legacy_testing_keeps_suffixed_name(self, xdg_tmp, monkeypatch): + _plant_legacy_testing_db(xdg_tmp) + monkeypatch.setenv("AW_PROFILE", "testing") settings = Settings(True) assert settings.config_file.name == "settings-testing.json" - def test_default_unsuffixed(self, tmp_path, monkeypatch): - monkeypatch.setattr( - "aw_server.settings.get_config_dir", lambda module: str(tmp_path) - ) + def test_isolated_testing_uses_bare_name(self, xdg_tmp, monkeypatch): + monkeypatch.setenv("AW_PROFILE", "testing") + settings = Settings(True) + assert settings.config_file.name == "settings.json" + + def test_default_unsuffixed(self, xdg_tmp, monkeypatch): monkeypatch.delenv("AW_PROFILE", raising=False) settings = Settings(False) assert settings.config_file.name == "settings.json" - def test_named_profile_suffix(self, tmp_path, monkeypatch): - monkeypatch.setattr( - "aw_server.settings.get_config_dir", lambda module: str(tmp_path) - ) + def test_named_profile_uses_bare_name(self, xdg_tmp, monkeypatch): monkeypatch.setenv("AW_PROFILE", "research") settings = Settings(False) - assert settings.config_file.name == "settings-research.json" + assert settings.config_file.name == "settings.json" class TestParseSettings: @@ -82,7 +129,7 @@ def test_default_keeps_port_5600_and_unsets_env(self, xdg_tmp, monkeypatch): assert settings.port == 5600 assert "AW_PROFILE" not in os.environ - def test_named_profile_exports_env_and_falls_back_to_server_section( + def test_named_profile_exports_env_and_uses_server_section( self, xdg_tmp, monkeypatch ): monkeypatch.setattr(sys, "argv", ["aw-server", "--profile", "research"]) @@ -92,6 +139,21 @@ def test_named_profile_exports_env_and_falls_back_to_server_section( assert settings.port == 5600 assert os.environ["AW_PROFILE"] == "research" + def test_isolated_named_profile_reads_server_section_port( + self, xdg_tmp, monkeypatch + ): + import aw_core.dirs as aw_dirs + + export_profile("research") + cfg = Path(aw_dirs.get_config_dir("aw-server")) / "aw-server.toml" + cfg.write_text( + '[server]\nhost = "localhost"\nport = "5667"\n' + 'storage = "peewee"\ncors_origins = ""\n[server.custom_static]\n' + ) + monkeypatch.setattr(sys, "argv", ["aw-server", "--profile", "research"]) + settings, _storage = parse_settings() + assert settings.port == 5667 + def test_cli_port_override(self, xdg_tmp, monkeypatch): monkeypatch.setattr( sys, "argv", ["aw-server", "--profile", "research", "--port", "5667"]