Files
git-automatic-tracker-py/tests/unit/test_authors.py
T
2026-09-12 09:01:14 +02:00

142 lines
5.5 KiB
Python

"""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("<x/>", 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)