diff --git a/src/omnifocus_operator/repository/hybrid/hybrid.py b/src/omnifocus_operator/repository/hybrid/hybrid.py index 10b1a2e6..4f241bc0 100644 --- a/src/omnifocus_operator/repository/hybrid/hybrid.py +++ b/src/omnifocus_operator/repository/hybrid/hybrid.py @@ -104,9 +104,14 @@ def _get_local_tz() -> ZoneInfo: - """Get system timezone as ZoneInfo from /etc/localtime symlink (macOS).""" + """Get system timezone as ZoneInfo from /etc/localtime symlink (macOS). + + /etc/localtime resolves through /var/db/timezone/zoneinfo, but fully + resolving symlinks can land on /usr/share/zoneinfo.default instead of + /usr/share/zoneinfo depending on macOS build, so match either variant. + """ tz_path = pathlib.Path("/etc/localtime").resolve() - tz_name = str(tz_path).split("zoneinfo/")[-1] + tz_name = re.sub(r".*/zoneinfo(?:\.default)?/", "", str(tz_path)) return ZoneInfo(tz_name) diff --git a/tests/test_hybrid_repository.py b/tests/test_hybrid_repository.py index 8bd66c7d..49ca102c 100644 --- a/tests/test_hybrid_repository.py +++ b/tests/test_hybrid_repository.py @@ -8,6 +8,7 @@ from __future__ import annotations import asyncio +import pathlib import plistlib import sqlite3 import time @@ -32,7 +33,11 @@ from omnifocus_operator.contracts.use_cases.list.tasks import ListTasksRepoQuery from omnifocus_operator.models.enums import Availability, FolderAvailability, TagAvailability from omnifocus_operator.models.snapshot import AllEntities -from omnifocus_operator.repository.hybrid.hybrid import _FRESHNESS_TIMEOUT, HybridRepository +from omnifocus_operator.repository.hybrid.hybrid import ( + _FRESHNESS_TIMEOUT, + HybridRepository, + _get_local_tz, +) from tests.doubles import InMemoryBridge, StubBridge # Core Foundation epoch: Jan 1, 2001 00:00:00 UTC @@ -1924,6 +1929,30 @@ async def test_project_reads_plain_text_note(self, hybrid_repo: HybridRepository # ============================================================================ +class TestGetLocalTz: + """_get_local_tz() derives an IANA ZoneInfo name from the /etc/localtime symlink.""" + + @pytest.mark.parametrize( + "resolved_path", + [ + "/usr/share/zoneinfo/Australia/Sydney", + "/usr/share/zoneinfo.default/Australia/Sydney", + "/var/db/timezone/zoneinfo/Australia/Sydney", + ], + ) + def test_resolves_tz_name_across_symlink_variants(self, resolved_path: str) -> None: + """Regression: some macOS builds resolve /etc/localtime through + /usr/share/zoneinfo.default rather than /usr/share/zoneinfo, which used to + break naive `.split("zoneinfo/")` parsing and raise + `ZoneInfo keys may not be absolute paths`. + """ + with patch( + "pathlib.Path.resolve", + return_value=pathlib.PosixPath(resolved_path), + ): + assert _get_local_tz() == ZoneInfo("Australia/Sydney") + + class TestLocalDatetimeParsing: """dateDue, dateToStart, datePlanned should be parsed as local time with DST."""