from datetime import UTC, datetime import pytest from pydantic import ValidationError from tht.ports.evidence import AcquiredDocument, EvidenceSource, SourceObject class StubSource: def discover(self): return iter( [ SourceObject( source_id="handbook", uri="https://host/handbook.md", fingerprint="sha256:abc", ) ] ) def acquire(self, item: SourceObject) -> AcquiredDocument: return AcquiredDocument( source=item, content=b"# Handbook", acquired_at=datetime(2026, 7, 12, tzinfo=UTC), media_type="text/markdown", ) def test_runtime_checkable_source_protocol(): source = StubSource() assert isinstance(source, EvidenceSource) assert source.acquire(next(source.discover())).content == b"# Handbook" def test_source_objects_are_frozen_and_metadata_defaults_are_independent(): first = SourceObject(source_id="a", uri="file:///a", fingerprint="sha256:a") second = SourceObject(source_id="b", uri="file:///b", fingerprint="sha256:b") with pytest.raises(ValidationError): first.uri = "file:///changed" # type: ignore[misc] first.metadata["owner"] = "team-a" assert second.metadata == {} @pytest.mark.parametrize("metadata", [{"api_key": "secret"}, {"auth": {"token": "secret"}}]) def test_source_metadata_rejects_credentials(metadata): with pytest.raises(ValidationError, match="credential-like"): SourceObject( source_id="a", uri="https://host/a", fingerprint="etag:abc", metadata=metadata ) def test_source_metadata_must_be_json_safe(): with pytest.raises(ValidationError): SourceObject( source_id="a", uri="file:///a", fingerprint="sha256:a", metadata={"path": object()}, ) def test_acquired_document_does_not_accept_credentials_as_extra_fields(): item = SourceObject(source_id="a", uri="https://host/a", fingerprint="etag:abc") with pytest.raises(ValidationError): AcquiredDocument(source=item, content=b"a", api_key="secret")