diff --git a/muxplex/settings.py b/muxplex/settings.py index dc0b234..ee02a9b 100644 --- a/muxplex/settings.py +++ b/muxplex/settings.py @@ -8,6 +8,7 @@ import copy import json import os import socket +import time from pathlib import Path SETTINGS_PATH = Path.home() / ".config" / "muxplex" / "settings.json" @@ -151,10 +152,36 @@ def patch_settings(patch: dict) -> dict: # restore by index so editing a URL doesn't erase the key. remote["key"] = existing_remote_keys_by_index[i] + if any(key in SYNCABLE_KEYS for key in patch if key in DEFAULT_SETTINGS): + current["settings_updated_at"] = time.time() + save_settings(current) return current +def apply_synced_settings(incoming_settings: dict, incoming_timestamp: float) -> dict: + """Apply synced settings from a remote server. + + Only applies keys that are in SYNCABLE_KEYS. Sets settings_updated_at + to the incoming timestamp (NOT time.time()) to prevent sync loops. + """ + current = load_settings() + for key in SYNCABLE_KEYS: + if key in incoming_settings: + current[key] = incoming_settings[key] + current["settings_updated_at"] = incoming_timestamp + save_settings(current) + return current + + +def get_syncable_settings() -> dict: + """Return only syncable settings + the settings_updated_at timestamp.""" + settings = load_settings() + result = {key: settings[key] for key in SYNCABLE_KEYS if key in settings} + result["settings_updated_at"] = settings.get("settings_updated_at", 0.0) + return result + + def load_federation_key() -> str: """Load the federation key from disk or env-overridden path. diff --git a/muxplex/tests/test_settings.py b/muxplex/tests/test_settings.py index 089291c..856b001 100644 --- a/muxplex/tests/test_settings.py +++ b/muxplex/tests/test_settings.py @@ -12,6 +12,8 @@ import muxplex.settings as settings_mod from muxplex.settings import ( DEFAULT_SETTINGS, SYNCABLE_KEYS, + apply_synced_settings, + get_syncable_settings, load_federation_key, load_settings, patch_settings, @@ -918,3 +920,55 @@ def test_syncable_keys_subset_of_default_settings(): def test_defaults_include_settings_updated_at(): """DEFAULT_SETTINGS must include settings_updated_at with default 0.0.""" assert DEFAULT_SETTINGS["settings_updated_at"] == 0.0 + + +# ============================================================ +# Sync-aware patch_settings + apply_synced_settings (task-6) +# ============================================================ + + +def test_patch_settings_bumps_timestamp_for_syncable_key(): + """patch_settings bumps settings_updated_at when a syncable key is patched.""" + patch_settings({"fontSize": 20}) + settings = load_settings() + assert settings["settings_updated_at"] > 0.0 + + +def test_patch_settings_does_not_bump_timestamp_for_nonsyncable_key(): + """patch_settings does NOT bump settings_updated_at for non-syncable keys.""" + patch_settings({"host": "0.0.0.0"}) + settings = load_settings() + assert settings["settings_updated_at"] == 0.0 + + +def test_apply_synced_settings_uses_incoming_timestamp(): + """apply_synced_settings sets settings_updated_at to the incoming timestamp.""" + apply_synced_settings({"fontSize": 18}, 1712600000.0) + settings = load_settings() + assert settings["fontSize"] == 18 + assert settings["settings_updated_at"] == 1712600000.0 + + +def test_apply_synced_settings_ignores_nonsyncable_keys(): + """apply_synced_settings ignores keys not in SYNCABLE_KEYS.""" + apply_synced_settings({"fontSize": 18, "host": "evil.com"}, 1712600000.0) + settings = load_settings() + assert settings["fontSize"] == 18 + assert settings["host"] == "127.0.0.1" # unchanged from default + + +def test_get_syncable_settings_returns_only_syncable_keys(): + """get_syncable_settings returns only SYNCABLE_KEYS + settings_updated_at.""" + result = get_syncable_settings() + for key in result: + assert key in SYNCABLE_KEYS or key == "settings_updated_at" + assert "host" not in result + assert "settings_updated_at" in result + + +def test_apply_synced_settings_does_not_use_time_now(): + """apply_synced_settings must use the incoming timestamp, not time.time().""" + old_ts = 1000.0 + apply_synced_settings({"fontSize": 16}, old_ts) + settings = load_settings() + assert settings["settings_updated_at"] == old_ts # exact match, not "close to now"