From c9469b380157e052930564e4bb685a68862db140 Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 8 Sep 2026 07:21:18 +0000 Subject: [PATCH 1/2] fix(config): use [server] in isolated profile roots [server-testing] in the same file as [server] is the pre-profile model. Isolated roots (activitywatch-testing/, activitywatch-research/, ...) already separate instances, so they get a single [server] section with the profile's default port, and settings.json without a suffix. Legacy shared-root testing still reads [server-testing] and settings-testing.json so existing installs are not orphaned. Refs: ActivityWatch/activitywatch#1434 --- aw_server/config.py | 51 ++++++++++++++++++++++++++--- aw_server/main.py | 4 +-- aw_server/settings.py | 19 ++++++++--- tests/test_profile_config.py | 63 +++++++++++++++++++++++++----------- 4 files changed, 108 insertions(+), 29 deletions(-) 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..65ba4d6 100644 --- a/tests/test_profile_config.py +++ b/tests/test_profile_config.py @@ -21,9 +21,20 @@ def xdg_tmp(tmp_path, monkeypatch): 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): + data = xdg_tmp / "data" / "activitywatch" / "aw-server" + data.mkdir(parents=True) + (data / "peewee-sqlite-testing.v2.db").write_text("") + monkeypatch.setenv("AW_PROFILE", "testing") + assert config_section("testing") == "server-testing" def test_default_ports(self): assert default_port(DEFAULT_PROFILE) == 5600 @@ -32,29 +43,28 @@ 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): + data = xdg_tmp / "data" / "activitywatch" / "aw-server" + data.mkdir(parents=True) + (data / "peewee-sqlite-testing.v2.db").write_text("") + 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 +92,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 +102,23 @@ 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 + ): + from pathlib import Path + + 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"] From 897f9f85ed7b53e455e59ad26a8cf660ac73c0df Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 8 Sep 2026 07:37:55 +0000 Subject: [PATCH 2/2] test(config): isolate platformdirs so legacy-root tests pass off Linux XDG_*_HOME only redirects platformdirs on Linux. The legacy-testing marker was planted under the XDG data path, so Windows/macOS never saw it and kept using [server] / settings.json. --- tests/test_profile_config.py | 57 +++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/tests/test_profile_config.py b/tests/test_profile_config.py index 65ba4d6..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,13 +14,53 @@ @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_isolated_roots_use_server_section(self, xdg_tmp, monkeypatch): monkeypatch.delenv("AW_PROFILE", raising=False) @@ -30,9 +71,7 @@ def test_isolated_roots_use_server_section(self, xdg_tmp, monkeypatch): assert config_section("research") == "server" def test_legacy_testing_keeps_server_testing_section(self, xdg_tmp, monkeypatch): - data = xdg_tmp / "data" / "activitywatch" / "aw-server" - data.mkdir(parents=True) - (data / "peewee-sqlite-testing.v2.db").write_text("") + _plant_legacy_testing_db(xdg_tmp) monkeypatch.setenv("AW_PROFILE", "testing") assert config_section("testing") == "server-testing" @@ -44,9 +83,7 @@ def test_default_ports(self): class TestSettingsFilename: def test_legacy_testing_keeps_suffixed_name(self, xdg_tmp, monkeypatch): - data = xdg_tmp / "data" / "activitywatch" / "aw-server" - data.mkdir(parents=True) - (data / "peewee-sqlite-testing.v2.db").write_text("") + _plant_legacy_testing_db(xdg_tmp) monkeypatch.setenv("AW_PROFILE", "testing") settings = Settings(True) assert settings.config_file.name == "settings-testing.json" @@ -105,8 +142,6 @@ def test_named_profile_exports_env_and_uses_server_section( def test_isolated_named_profile_reads_server_section_port( self, xdg_tmp, monkeypatch ): - from pathlib import Path - import aw_core.dirs as aw_dirs export_profile("research")