|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import hashlib |
| 4 | +import json |
| 5 | +from dataclasses import replace |
4 | 6 | from pathlib import Path |
5 | 7 |
|
6 | 8 | import pytest |
@@ -192,6 +194,68 @@ def test_v2_duplicate_identity_or_digest_conflicts_fail_closed() -> None: |
192 | 194 | parse_reports_index({"schema_version": 2, "reports": [first, same_digest]}) |
193 | 195 |
|
194 | 196 |
|
| 197 | +def test_same_digest_is_allowed_across_different_periods() -> None: |
| 198 | + first_report = build_report(as_of="2026-06-20") |
| 199 | + second_report = build_report(as_of="2026-06-27") |
| 200 | + digest = digest_for(first_report) |
| 201 | + |
| 202 | + index = parse_reports_index( |
| 203 | + { |
| 204 | + "schema_version": 2, |
| 205 | + "reports": [v2_entry(first_report, digest=digest), v2_entry(second_report, digest=digest)], |
| 206 | + } |
| 207 | + ) |
| 208 | + |
| 209 | + assert {binding.period_key for binding in index.bindings} == { |
| 210 | + "weekly:2026-06-15:2026-06-21", |
| 211 | + "weekly:2026-06-22:2026-06-28", |
| 212 | + } |
| 213 | + |
| 214 | + |
| 215 | +def test_v1_verified_binding_cannot_be_serialized_as_incomplete_v2() -> None: |
| 216 | + report = build_report() |
| 217 | + binding = parse_reports_index({"schema_version": 1, "reports": [v1_entry()]}).bindings[0] |
| 218 | + verified_v1 = verify_identity_binding(binding, report) |
| 219 | + |
| 220 | + with pytest.raises(IdentityMetadataError, match="v2_serialization_invalid_binding"): |
| 221 | + serialize_reports_index_v2(ReportsIndex(2, (verified_v1,))) |
| 222 | + |
| 223 | + |
| 224 | +def test_complete_v2_binding_serializes_and_parses_round_trip() -> None: |
| 225 | + report = build_report() |
| 226 | + parsed = parse_reports_index({"schema_version": 2, "reports": [v2_entry(report)]}) |
| 227 | + verified = verify_identity_binding(parsed.bindings[0], report) |
| 228 | + |
| 229 | + round_tripped = parse_reports_index( |
| 230 | + json.loads(serialize_reports_index_v2(ReportsIndex(2, (verified,)))) |
| 231 | + ).bindings[0] |
| 232 | + |
| 233 | + assert round_tripped.period_key == verified.period_key |
| 234 | + assert round_tripped.fingerprint_digest == verified.fingerprint_digest |
| 235 | + assert round_tripped.json_name == verified.json_name |
| 236 | + assert round_tripped.verification_status == "PENDING_REPORT_VALIDATION" |
| 237 | + |
| 238 | + |
| 239 | +@pytest.mark.parametrize( |
| 240 | + "mutation", |
| 241 | + [ |
| 242 | + lambda binding: replace(binding, display_order=None), |
| 243 | + lambda binding: replace(binding, display_order=-1), |
| 244 | + lambda binding: replace(binding, display_order=True), |
| 245 | + lambda binding: replace(binding, fingerprint_version=None), |
| 246 | + lambda binding: replace(binding, fingerprint_digest=None), |
| 247 | + lambda binding: replace(binding, schema_version=None), |
| 248 | + ], |
| 249 | +) |
| 250 | +def test_serializer_revalidates_complete_v2_invariants(mutation) -> None: |
| 251 | + report = build_report() |
| 252 | + parsed = parse_reports_index({"schema_version": 2, "reports": [v2_entry(report)]}) |
| 253 | + verified = verify_identity_binding(parsed.bindings[0], report) |
| 254 | + |
| 255 | + with pytest.raises(IdentityMetadataError, match="v2_serialization"): |
| 256 | + serialize_reports_index_v2(ReportsIndex(2, (mutation(verified),))) |
| 257 | + |
| 258 | + |
195 | 259 | @pytest.mark.parametrize("key", ["md", "manifest"]) |
196 | 260 | def test_optional_identity_names_are_validated_when_declared(key: str) -> None: |
197 | 261 | report = build_report() |
|
0 commit comments