"""Unit tests for the sidecar resolver and the resolver chain (Sections 9-11).""" from __future__ import annotations import datetime as dt from pathlib import Path import pytest from git_auto_tracker.authors.base import AuthorResult from git_auto_tracker.authors.resolver import ResolverChain from git_auto_tracker.authors.sidecar import SidecarMetadataResolver from git_auto_tracker.changes.models import Confidence from git_auto_tracker.config import AuthorResolutionConfig from git_auto_tracker.errors import ConfigurationError @pytest.fixture def resolver() -> SidecarMetadataResolver: return SidecarMetadataResolver() class TestSidecarResolver: def test_json_sidecar(self, tmp_path: Path, resolver: SidecarMetadataResolver) -> None: (tmp_path / "pump.xml").write_text("", encoding="utf-8") (tmp_path / "pump.xml.metadata").write_text( '{"last_modified_by": "DOMAIN\\\\jsmith",' ' "name": "John Smith",' ' "last_modified_at": "2026-09-11T14:31:22+02:00"}', encoding="utf-8", ) result = resolver.resolve(tmp_path, "pump.xml") assert result.identifier == "DOMAIN\\jsmith" assert result.name == "John Smith" assert result.confidence is Confidence.PROBABLE expected = dt.datetime( 2026, 9, 11, 14, 31, 22, tzinfo=dt.timezone(dt.timedelta(hours=2)) ) assert result.timestamp == expected def test_keyvalue_sidecar(self, tmp_path: Path, resolver: SidecarMetadataResolver) -> None: (tmp_path / "pump.xml.metadata").write_text( "last_modified_by=DOMAIN\\adoe\n", encoding="utf-8" ) result = resolver.resolve(tmp_path, "pump.xml") assert result.identifier == "DOMAIN\\adoe" def test_missing_sidecar_is_unknown( self, tmp_path: Path, resolver: SidecarMetadataResolver ) -> None: result = resolver.resolve(tmp_path, "absent.xml") assert result.identifier == "UNKNOWN" assert result.confidence is Confidence.UNKNOWN def test_malformed_sidecar_is_unknown( self, tmp_path: Path, resolver: SidecarMetadataResolver ) -> None: (tmp_path / "bad.xml.metadata").write_text("{not json", encoding="utf-8") result = resolver.resolve(tmp_path, "bad.xml") assert result.identifier == "UNKNOWN" def test_sidecar_without_author_is_unknown( self, tmp_path: Path, resolver: SidecarMetadataResolver ) -> None: (tmp_path / "partial.xml.metadata").write_text('{"revision": 7}', encoding="utf-8") result = resolver.resolve(tmp_path, "partial.xml") assert result.identifier == "UNKNOWN" class _FakeResolver: """Test resolver with configurable results, registered under a name.""" def __init__(self, name: str, result: AuthorResult) -> None: self.name = name self._result = result self.calls = 0 def resolve(self, repository_root: Path, file_path: str) -> AuthorResult: self.calls += 1 return self._result class TestResolverChain: def _chain(self, resolvers: list, minimum: str = "probable") -> ResolverChain: chain = ResolverChain( [r for r in resolvers], Confidence(minimum), AuthorResult.unknown("fallback"), ) return chain def test_first_usable_wins(self, tmp_path: Path) -> None: weak = _FakeResolver( "a", AuthorResult("DOMAIN\\weak", confidence=Confidence.WEAK, source="a") ) strong = _FakeResolver( "b", AuthorResult("DOMAIN\\strong", confidence=Confidence.PROBABLE, source="b") ) chain = self._chain([weak, strong]) result = chain.resolve(tmp_path, "f.xml") assert result.identifier == "DOMAIN\\strong" assert weak.calls == 1 assert strong.calls == 1 # weak skipped, strong consulted def test_below_threshold_results_are_not_used(self, tmp_path: Path) -> None: weak = _FakeResolver( "a", AuthorResult("DOMAIN\\weak", confidence=Confidence.WEAK, source="a") ) chain = self._chain([weak], minimum="probable") result = chain.resolve(tmp_path, "f.xml") assert result.identifier == "UNKNOWN" # fallback def test_exception_in_resolver_continues_chain(self, tmp_path: Path) -> None: class _Boom: name = "boom" def resolve(self, repository_root: Path, file_path: str) -> AuthorResult: raise RuntimeError("audit log unavailable") good = _FakeResolver( "b", AuthorResult("DOMAIN\\ok", confidence=Confidence.AUTHORITATIVE, source="b") ) chain = self._chain([_Boom(), good]) result = chain.resolve(tmp_path, "f.xml") assert result.identifier == "DOMAIN\\ok" def test_fallback_when_all_unknown(self, tmp_path: Path) -> None: unknown = _FakeResolver("a", AuthorResult.unknown("a")) chain = self._chain([unknown]) result = chain.resolve(tmp_path, "f.xml") assert result.identifier == "UNKNOWN" def test_unknown_resolver_name_rejected(self) -> None: config = AuthorResolutionConfig(resolvers=["nope"], minimum_confidence="probable") with pytest.raises(ConfigurationError): ResolverChain.from_config(config) def test_sidecar_config_name(self) -> None: config = AuthorResolutionConfig(resolvers=["sidecar_metadata"]) chain = ResolverChain.from_config(config) assert isinstance(chain._resolvers[0], SidecarMetadataResolver)