142 lines
5.5 KiB
Python
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)
|