From 801177972b2be338823e79e8ce3321251ccebad8 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Thu, 27 Aug 2026 23:44:24 +0200 Subject: [PATCH] Split _sync.py along the transport/resolution seam item 8 needs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 371 -> 168. Token discovery and both backends move to _sync_transport.py; _sync.py keeps resolution (cache, staleness, fallback), the Alarm dataclass and the CLI. The seam is chosen for item 8, not just for line count: that phase adds a dismissal-record reader, which belongs beside the alarm reader in the transport rather than inside the resolution chain. ALARM_CACHE_FILE deliberately stays in _sync. The suite patches `wake_alarm._sync.ALARM_CACHE_FILE` to keep tests off ~/.config/wake_alarm/alarm_cache.json — the file shutdown-wrapper.sh reads at poweroff — and moving that binding would silently reconnect the tests to the real one. That is the failure mode that once wrote 27 fake entries into a live log while the suite still looked green. The split broke 31 tests loudly, all of them patch targets that had to follow the code: the three SYNC_* constants, the two fetchers, the patch.multiple on the firebase names, and the reload test for the crdt_sync import guard. Every one is repointed at _sync_transport; none was left silently patching nothing. Verified after: 465 passed at 100% coverage, `python -m wake_alarm._sync --show` prints, `--no-cache` still resolves the phone's real alarm through Firebase, `_trigger --dry-run` prints, and wake_state.json is byte-identical. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EckWmyfetYq3Mi8MAnUudh --- wake_alarm/_sync.py | 218 +++---------------------- wake_alarm/_sync_transport.py | 218 +++++++++++++++++++++++++ wake_alarm/tests/test_alarm_payload.py | 4 +- wake_alarm/tests/test_sync.py | 6 +- wake_alarm/tests/test_sync_backends.py | 33 ++-- wake_alarm/tests/test_sync_cli.py | 4 +- wake_alarm/tests/test_sync_token.py | 4 +- 7 files changed, 273 insertions(+), 214 deletions(-) create mode 100644 wake_alarm/_sync_transport.py diff --git a/wake_alarm/_sync.py b/wake_alarm/_sync.py index 8d20efa..9f2b796 100644 --- a/wake_alarm/_sync.py +++ b/wake_alarm/_sync.py @@ -28,43 +28,40 @@ import json import logging import sys import time -import urllib.error -import urllib.request -from wake_alarm._alarm_payload import in_range, newest_alarm, parse_log +from wake_alarm._alarm_payload import in_range from wake_alarm._constants import ( ALARM_CACHE_FILE, ALARM_CACHE_MAX_AGE_SECONDS, FALLBACK_ALARM_HOUR, - SYNC_ALARM_FILENAME, - SYNC_ALARM_PATH, - SYNC_API_BASE, - SYNC_APP_NAME, - SYNC_CONFIG_FILE, - SYNC_DEVICES_PREFIX, - SYNC_HTTP_TIMEOUT_SECONDS, - SYNC_REPO_NAME, - SYNC_REPO_OWNER, - SYNC_TOKEN_FALLBACK_FILES, ) from wake_alarm._logging_setup import configure_logging +from wake_alarm._sync_transport import ( + _FIREBASE_AVAILABLE, + _fetch_firebase_alarm, + _fetch_github_alarm, + _read_token, + _read_token_fallback, + fetch_remote_alarm, +) + +# Re-imported under their original names: the suite patches +# `wake_alarm._sync._fetch_firebase_alarm` and friends, and a moved symbol +# makes patch() stop intercepting *silently*. __all__ stops ruff deleting +# them as unused. +__all__ = [ + "_FIREBASE_AVAILABLE", + "Alarm", + "_fetch_firebase_alarm", + "_fetch_github_alarm", + "_read_token", + "_read_token_fallback", + "fetch_remote_alarm", + "get_alarm", +] _logger = logging.getLogger(__name__) -# Optional at runtime, exactly like python-kasa in _smart_plug.py: the trigger -# runs every minute under /usr/bin/python, so a dependency that failed to -# install must degrade this read to the GitHub path, not raise. -try: - from crdt_sync import ConfigError, firebase_client_for - from crdt_sync._config import CONFIG_FILE as FIREBASE_CONFIG_FILE - from crdt_sync._firebase_auth import FirebaseAuthError - from crdt_sync._remote import RemoteSyncError - - _FIREBASE_AVAILABLE = True -except ImportError: - _FIREBASE_AVAILABLE = False - _logger.info("crdt_sync is not installed; reading the alarm from GitHub") - @dataclass(frozen=True) class Alarm: @@ -87,173 +84,6 @@ class Alarm: return f"{self.hour:02d}:{self.minute:02d} ({self.source})" -def _read_token_config() -> str | None: - """Read the token from this package's own JSON config. - - Returns: - The token, or None if the file is missing, unreadable, malformed, or - holds no usable token. - """ - try: - with SYNC_CONFIG_FILE.open() as handle: - config = json.load(handle) - except (OSError, json.JSONDecodeError): - return None - if not isinstance(config, dict): - return None - token = config.get("token") - if not isinstance(token, str) or not token: - return None - return token - - -def _read_token_fallback() -> str | None: - """Read the token from a sibling app's raw single-line token file. - - Returns: - The first usable token found, or None if none of the fallback files - exists or holds one. - """ - for path in SYNC_TOKEN_FALLBACK_FILES: - try: - token = path.read_text().strip() - except OSError: - continue - if token: - _logger.debug("Using sync token from %s", path) - return token - return None - - -def _read_token() -> str | None: - """Read the GitHub token, preferring this package's own config. - - Falls back to the sibling companion apps' token files so one repo-scoped - PAT covers every app syncing through ``kuhyx/syncs``. - - Returns: - The token, or None if no configured source holds one. - """ - # No warning on a miss here: the caller reports "no token" once. Logging - # twice for one unconfigured file makes every tick noisy in the journal. - return _read_token_config() or _read_token_fallback() - - -def _fetch_firebase_alarm() -> tuple[int, int] | None: - """Fetch the newest alarm any device has pushed to Firebase. - - Discovers device directories rather than reading one fixed path: the - phone's device id is migrating from the constant "phone" to a per-install - uuid, and a reader pinned to the old path would stop seeing the alarm the - moment the phone migrated — falling back to FALLBACK_ALARM_HOUR, i.e. - waking the user at the wrong time. Listing covers both ids, and - SYNC_ALARM_PATH remains as a last resort for a backend that cannot list. - - Returns None — never raises — for every failure mode, including - ``crdt_sync`` not being importable on this interpreter, so the caller can - fall back to GitHub and then to the cache. - - Returns: - The (hour, minute) pair, or None if unconfigured or unreadable. - """ - if not _FIREBASE_AVAILABLE: - return None - - # Checked before constructing anything, so an unconfigured machine never - # touches the network (and a socket-blocking test suite stays quiet). - if not FIREBASE_CONFIG_FILE.is_file(): - _logger.info("Firebase not configured at %s", FIREBASE_CONFIG_FILE) - return None - - try: - client = firebase_client_for(SYNC_APP_NAME) - published: list[tuple[str, str]] = [] - for device_id in client.list_directory(SYNC_DEVICES_PREFIX): - raw = client.get_file_text( - f"{SYNC_DEVICES_PREFIX}/{device_id}/{SYNC_ALARM_FILENAME}" - ) - if raw is not None: - published.append((device_id, raw)) - if not published: - # No directories listed (or none held a file): still try the - # legacy fixed path, so a phone that has not migrated is read. - raw = client.get_file_text(SYNC_ALARM_PATH) - if raw is None: - _logger.info("No alarm published to Firebase yet") - return None - published.append(("phone", raw)) - except (ConfigError, FirebaseAuthError, RemoteSyncError, OSError) as exc: - _logger.info("Cannot fetch the alarm from Firebase: %s", exc) - return None - - return newest_alarm(published) - - -def _fetch_github_alarm() -> tuple[int, int] | None: - """Fetch the alarm the phone last pushed to GitHub. - - Uses the raw media type so the Contents API returns the file body directly - rather than base64-wrapped JSON. - - Returns: - The (hour, minute) pair, or None if unconfigured, unreachable, or - malformed. Never raises — a failed read must degrade to the cache, - not crash the trigger. - """ - token = _read_token() - if token is None: - # INFO, not WARNING: get_alarm() runs on every one-minute trigger tick, - # so a persistent condition logged loudly here is 1440 identical - # journal lines a day. The trigger warns once, at the moment it - # actually fires on a degraded source — that is the actionable event. - _logger.info("No sync token configured at %s", SYNC_CONFIG_FILE) - return None - - url = ( - f"{SYNC_API_BASE}/repos/{SYNC_REPO_OWNER}/{SYNC_REPO_NAME}" - f"/contents/{SYNC_ALARM_PATH}" - ) - request = urllib.request.Request( - url, - headers={ - "Authorization": f"token {token}", - "Accept": "application/vnd.github.raw", - }, - ) - # The URL is built from module constants, never from remote input, but - # assert the scheme anyway: this call carries a credential, and an http:// - # or file:// target would leak it. Belt and braces for a bad edit above. - if request.type != "https": - _logger.error("Refusing to send the sync token over %s", request.type) - return None - try: - with urllib.request.urlopen( - request, timeout=SYNC_HTTP_TIMEOUT_SECONDS - ) as response: - raw = response.read().decode("utf-8") - except (urllib.error.URLError, OSError, UnicodeDecodeError) as exc: - # INFO for the same reason as the no-token path: a persistent failure - # (404 because the phone never pushed, an expired token, no network) - # would otherwise be one WARNING per cache expiry, forever. - _logger.info("Cannot fetch synced alarm: %s", exc) - return None - - return parse_log(raw) - - -def fetch_remote_alarm() -> tuple[int, int] | None: - """Fetch the alarm the phone last pushed, Firebase first. - - GitHub is tried only when Firebase yields nothing, which covers both the - not-yet-migrated machine and a Firebase outage. Once every device has - moved, deleting the GitHub branch is the whole retirement. - - Returns: - The (hour, minute) pair, or None if no backend has a usable answer. - """ - return _fetch_firebase_alarm() or _fetch_github_alarm() - - def _read_cache() -> tuple[int, int, float] | None: """Read the cached alarm. diff --git a/wake_alarm/_sync_transport.py b/wake_alarm/_sync_transport.py new file mode 100644 index 0000000..840f133 --- /dev/null +++ b/wake_alarm/_sync_transport.py @@ -0,0 +1,218 @@ +"""Fetching the alarm the phone pushed — Firebase first, GitHub as mirror. + +Split out of ``_sync.py`` for the 250-line cap, along the seam item 8 needs: +this module is the **transport** (token discovery and the two backends), while +``_sync.py`` keeps **resolution** (cache, staleness, fallback) and the CLI. +Item 8 adds a dismissal-record reader, which belongs here beside the alarm +reader rather than inside the resolution chain. + +``ALARM_CACHE_FILE`` deliberately does NOT live here: the suite patches +``wake_alarm._sync.ALARM_CACHE_FILE`` to keep tests off +``~/.config/wake_alarm/alarm_cache.json``, which ``shutdown-wrapper.sh`` reads +at poweroff, and moving that binding would silently reconnect the tests to the +real file. +""" + +from __future__ import annotations + +import json +import logging +import urllib.error +import urllib.request + +from wake_alarm._alarm_payload import newest_alarm, parse_log +from wake_alarm._constants import ( + SYNC_ALARM_FILENAME, + SYNC_ALARM_PATH, + SYNC_API_BASE, + SYNC_APP_NAME, + SYNC_CONFIG_FILE, + SYNC_DEVICES_PREFIX, + SYNC_HTTP_TIMEOUT_SECONDS, + SYNC_REPO_NAME, + SYNC_REPO_OWNER, + SYNC_TOKEN_FALLBACK_FILES, +) + +_logger = logging.getLogger(__name__) + +# Optional at runtime, exactly like python-kasa in _smart_plug.py: the trigger +# runs every minute under /usr/bin/python, so a dependency that failed to +# install must degrade this read to the GitHub path, not raise. +try: + from crdt_sync import ConfigError, firebase_client_for + from crdt_sync._config import CONFIG_FILE as FIREBASE_CONFIG_FILE + from crdt_sync._firebase_auth import FirebaseAuthError + from crdt_sync._remote import RemoteSyncError + + _FIREBASE_AVAILABLE = True +except ImportError: + _FIREBASE_AVAILABLE = False + _logger.info("crdt_sync is not installed; reading the alarm from GitHub") + + +def _read_token_config() -> str | None: + """Read the token from this package's own JSON config. + + Returns: + The token, or None if the file is missing, unreadable, malformed, or + holds no usable token. + """ + try: + with SYNC_CONFIG_FILE.open() as handle: + config = json.load(handle) + except (OSError, json.JSONDecodeError): + return None + if not isinstance(config, dict): + return None + token = config.get("token") + if not isinstance(token, str) or not token: + return None + return token + + +def _read_token_fallback() -> str | None: + """Read the token from a sibling app's raw single-line token file. + + Returns: + The first usable token found, or None if none of the fallback files + exists or holds one. + """ + for path in SYNC_TOKEN_FALLBACK_FILES: + try: + token = path.read_text().strip() + except OSError: + continue + if token: + _logger.debug("Using sync token from %s", path) + return token + return None + + +def _read_token() -> str | None: + """Read the GitHub token, preferring this package's own config. + + Falls back to the sibling companion apps' token files so one repo-scoped + PAT covers every app syncing through ``kuhyx/syncs``. + + Returns: + The token, or None if no configured source holds one. + """ + # No warning on a miss here: the caller reports "no token" once. Logging + # twice for one unconfigured file makes every tick noisy in the journal. + return _read_token_config() or _read_token_fallback() + + +def _fetch_firebase_alarm() -> tuple[int, int] | None: + """Fetch the newest alarm any device has pushed to Firebase. + + Discovers device directories rather than reading one fixed path: the + phone's device id is migrating from the constant "phone" to a per-install + uuid, and a reader pinned to the old path would stop seeing the alarm the + moment the phone migrated — falling back to FALLBACK_ALARM_HOUR, i.e. + waking the user at the wrong time. Listing covers both ids, and + SYNC_ALARM_PATH remains as a last resort for a backend that cannot list. + + Returns None — never raises — for every failure mode, including + ``crdt_sync`` not being importable on this interpreter, so the caller can + fall back to GitHub and then to the cache. + + Returns: + The (hour, minute) pair, or None if unconfigured or unreadable. + """ + if not _FIREBASE_AVAILABLE: + return None + + # Checked before constructing anything, so an unconfigured machine never + # touches the network (and a socket-blocking test suite stays quiet). + if not FIREBASE_CONFIG_FILE.is_file(): + _logger.info("Firebase not configured at %s", FIREBASE_CONFIG_FILE) + return None + + try: + client = firebase_client_for(SYNC_APP_NAME) + published: list[tuple[str, str]] = [] + for device_id in client.list_directory(SYNC_DEVICES_PREFIX): + raw = client.get_file_text( + f"{SYNC_DEVICES_PREFIX}/{device_id}/{SYNC_ALARM_FILENAME}" + ) + if raw is not None: + published.append((device_id, raw)) + if not published: + # No directories listed (or none held a file): still try the + # legacy fixed path, so a phone that has not migrated is read. + raw = client.get_file_text(SYNC_ALARM_PATH) + if raw is None: + _logger.info("No alarm published to Firebase yet") + return None + published.append(("phone", raw)) + except (ConfigError, FirebaseAuthError, RemoteSyncError, OSError) as exc: + _logger.info("Cannot fetch the alarm from Firebase: %s", exc) + return None + + return newest_alarm(published) + + +def _fetch_github_alarm() -> tuple[int, int] | None: + """Fetch the alarm the phone last pushed to GitHub. + + Uses the raw media type so the Contents API returns the file body directly + rather than base64-wrapped JSON. + + Returns: + The (hour, minute) pair, or None if unconfigured, unreachable, or + malformed. Never raises — a failed read must degrade to the cache, + not crash the trigger. + """ + token = _read_token() + if token is None: + # INFO, not WARNING: get_alarm() runs on every one-minute trigger tick, + # so a persistent condition logged loudly here is 1440 identical + # journal lines a day. The trigger warns once, at the moment it + # actually fires on a degraded source — that is the actionable event. + _logger.info("No sync token configured at %s", SYNC_CONFIG_FILE) + return None + + url = ( + f"{SYNC_API_BASE}/repos/{SYNC_REPO_OWNER}/{SYNC_REPO_NAME}" + f"/contents/{SYNC_ALARM_PATH}" + ) + request = urllib.request.Request( + url, + headers={ + "Authorization": f"token {token}", + "Accept": "application/vnd.github.raw", + }, + ) + # The URL is built from module constants, never from remote input, but + # assert the scheme anyway: this call carries a credential, and an http:// + # or file:// target would leak it. Belt and braces for a bad edit above. + if request.type != "https": + _logger.error("Refusing to send the sync token over %s", request.type) + return None + try: + with urllib.request.urlopen( + request, timeout=SYNC_HTTP_TIMEOUT_SECONDS + ) as response: + raw = response.read().decode("utf-8") + except (urllib.error.URLError, OSError, UnicodeDecodeError) as exc: + # INFO for the same reason as the no-token path: a persistent failure + # (404 because the phone never pushed, an expired token, no network) + # would otherwise be one WARNING per cache expiry, forever. + _logger.info("Cannot fetch synced alarm: %s", exc) + return None + + return parse_log(raw) + + +def fetch_remote_alarm() -> tuple[int, int] | None: + """Fetch the alarm the phone last pushed, Firebase first. + + GitHub is tried only when Firebase yields nothing, which covers both the + not-yet-migrated machine and a Firebase outage. Once every device has + moved, deleting the GitHub branch is the whole retirement. + + Returns: + The (hour, minute) pair, or None if no backend has a usable answer. + """ + return _fetch_firebase_alarm() or _fetch_github_alarm() diff --git a/wake_alarm/tests/test_alarm_payload.py b/wake_alarm/tests/test_alarm_payload.py index e3e467f..8a95356 100644 --- a/wake_alarm/tests/test_alarm_payload.py +++ b/wake_alarm/tests/test_alarm_payload.py @@ -61,9 +61,9 @@ def _patch_paths( ) -> None: """Redirect every on-disk path into tmp_path for every test.""" with ( - patch("wake_alarm._sync.SYNC_CONFIG_FILE", config_file), + patch("wake_alarm._sync_transport.SYNC_CONFIG_FILE", config_file), patch("wake_alarm._sync.ALARM_CACHE_FILE", cache_file), - patch("wake_alarm._sync.SYNC_TOKEN_FALLBACK_FILES", fallback_files), + patch("wake_alarm._sync_transport.SYNC_TOKEN_FALLBACK_FILES", fallback_files), ): yield diff --git a/wake_alarm/tests/test_sync.py b/wake_alarm/tests/test_sync.py index d0ce51a..8aeb4bb 100644 --- a/wake_alarm/tests/test_sync.py +++ b/wake_alarm/tests/test_sync.py @@ -60,9 +60,9 @@ def _patch_paths( ) -> None: """Redirect every on-disk path into tmp_path for every test.""" with ( - patch("wake_alarm._sync.SYNC_CONFIG_FILE", config_file), + patch("wake_alarm._sync_transport.SYNC_CONFIG_FILE", config_file), patch("wake_alarm._sync.ALARM_CACHE_FILE", cache_file), - patch("wake_alarm._sync.SYNC_TOKEN_FALLBACK_FILES", fallback_files), + patch("wake_alarm._sync_transport.SYNC_TOKEN_FALLBACK_FILES", fallback_files), ): yield @@ -111,7 +111,7 @@ class TestFetchRemoteAlarm: """The token is never sent over a plaintext or file:// URL.""" self._with_token(config_file) with ( - patch("wake_alarm._sync.SYNC_API_BASE", "http://api.github.com"), + patch("wake_alarm._sync_transport.SYNC_API_BASE", "http://api.github.com"), patch("urllib.request.urlopen") as urlopen, ): assert _fetch_github_alarm() is None diff --git a/wake_alarm/tests/test_sync_backends.py b/wake_alarm/tests/test_sync_backends.py index 1d6c8e6..9eb3b41 100644 --- a/wake_alarm/tests/test_sync_backends.py +++ b/wake_alarm/tests/test_sync_backends.py @@ -64,7 +64,7 @@ class TestFetchFirebaseAlarm: config = MagicMock() config.is_file.return_value = configured return patch.multiple( - "wake_alarm._sync", + "wake_alarm._sync_transport", _FIREBASE_AVAILABLE=True, FIREBASE_CONFIG_FILE=config, firebase_client_for=MagicMock( @@ -77,7 +77,7 @@ class TestFetchFirebaseAlarm: def test_missing_dependency_yields_none(self) -> None: """A machine without crdt_sync falls back instead of raising.""" - with patch("wake_alarm._sync._FIREBASE_AVAILABLE", new=False): + with patch("wake_alarm._sync_transport._FIREBASE_AVAILABLE", new=False): assert _fetch_firebase_alarm() is None def test_unconfigured_never_touches_the_network(self) -> None: @@ -183,7 +183,7 @@ class TestOptionalCrdtSyncImport: import importlib import sys - from wake_alarm import _sync + from wake_alarm import _sync_transport for name in ( "crdt_sync", @@ -193,11 +193,14 @@ class TestOptionalCrdtSyncImport: ): monkeypatch.setitem(sys.modules, name, None) try: - reloaded = importlib.reload(_sync) + reloaded = importlib.reload(_sync_transport) assert reloaded._FIREBASE_AVAILABLE is False finally: monkeypatch.undo() - importlib.reload(_sync) + # Restore the real module for every later test: a reload left in + # the import-failed state would silently disable Firebase for the + # rest of the session. + importlib.reload(_sync_transport) class TestFetchRemoteAlarmDispatch: @@ -206,8 +209,10 @@ class TestFetchRemoteAlarmDispatch: def test_prefers_firebase(self) -> None: """When Firebase answers, GitHub is never asked.""" with ( - patch("wake_alarm._sync._fetch_firebase_alarm", return_value=(6, 15)), - patch("wake_alarm._sync._fetch_github_alarm") as github, + patch( + "wake_alarm._sync_transport._fetch_firebase_alarm", return_value=(6, 15) + ), + patch("wake_alarm._sync_transport._fetch_github_alarm") as github, ): assert fetch_remote_alarm() == (6, 15) github.assert_not_called() @@ -215,15 +220,21 @@ class TestFetchRemoteAlarmDispatch: def test_falls_back_to_github(self) -> None: """A Firebase miss still resolves through the mirror.""" with ( - patch("wake_alarm._sync._fetch_firebase_alarm", return_value=None), - patch("wake_alarm._sync._fetch_github_alarm", return_value=(7, 30)), + patch( + "wake_alarm._sync_transport._fetch_firebase_alarm", return_value=None + ), + patch( + "wake_alarm._sync_transport._fetch_github_alarm", return_value=(7, 30) + ), ): assert fetch_remote_alarm() == (7, 30) def test_both_missing_yields_none(self) -> None: """With neither backend usable the caller degrades to the cache.""" with ( - patch("wake_alarm._sync._fetch_firebase_alarm", return_value=None), - patch("wake_alarm._sync._fetch_github_alarm", return_value=None), + patch( + "wake_alarm._sync_transport._fetch_firebase_alarm", return_value=None + ), + patch("wake_alarm._sync_transport._fetch_github_alarm", return_value=None), ): assert fetch_remote_alarm() is None diff --git a/wake_alarm/tests/test_sync_cli.py b/wake_alarm/tests/test_sync_cli.py index d0ba695..614d2c4 100644 --- a/wake_alarm/tests/test_sync_cli.py +++ b/wake_alarm/tests/test_sync_cli.py @@ -61,9 +61,9 @@ def _patch_paths( ) -> None: """Redirect every on-disk path into tmp_path for every test.""" with ( - patch("wake_alarm._sync.SYNC_CONFIG_FILE", config_file), + patch("wake_alarm._sync_transport.SYNC_CONFIG_FILE", config_file), patch("wake_alarm._sync.ALARM_CACHE_FILE", cache_file), - patch("wake_alarm._sync.SYNC_TOKEN_FALLBACK_FILES", fallback_files), + patch("wake_alarm._sync_transport.SYNC_TOKEN_FALLBACK_FILES", fallback_files), ): yield diff --git a/wake_alarm/tests/test_sync_token.py b/wake_alarm/tests/test_sync_token.py index a06c136..fc1b9b2 100644 --- a/wake_alarm/tests/test_sync_token.py +++ b/wake_alarm/tests/test_sync_token.py @@ -58,9 +58,9 @@ def _patch_paths( ) -> None: """Redirect every on-disk path into tmp_path for every test.""" with ( - patch("wake_alarm._sync.SYNC_CONFIG_FILE", config_file), + patch("wake_alarm._sync_transport.SYNC_CONFIG_FILE", config_file), patch("wake_alarm._sync.ALARM_CACHE_FILE", cache_file), - patch("wake_alarm._sync.SYNC_TOKEN_FALLBACK_FILES", fallback_files), + patch("wake_alarm._sync_transport.SYNC_TOKEN_FALLBACK_FILES", fallback_files), ): yield