1515
1616
1717ROOT = Path (__file__ ).resolve ().parents [1 ]
18+ V6_INPUT_DIGEST = "a" * 64
1819
1920
2021def build_v5 () -> dict :
@@ -35,6 +36,7 @@ def build_v6() -> dict:
3536 "reference_time" : canonical_reference_time (dt .date (2026 , 5 , 30 )).isoformat ().replace ("+00:00" , "Z" ),
3637 "generated_at" : "2026-05-31T12:00:00.123456Z" ,
3738 "expires_at" : "2026-06-07T12:00:00.123456Z" ,
39+ "input_digest" : V6_INPUT_DIGEST ,
3840 "freshness" : {
3941 "ai_signal" : {"present" : False , "valid" : False , "reason" : "not_provided" },
4042 "theme_momentum" : {"present" : False , "valid" : False , "reason" : "not_provided" },
@@ -72,6 +74,37 @@ def test_v5_legacy_missing_marker_remains_readable() -> None:
7274 validate_advisory_report (report )
7375
7476
77+ @pytest .mark .parametrize ("input_digest" , [None , "" , "a" * 63 , "A" * 64 , "sha256:" + "a" * 64 , True ])
78+ def test_v6_input_digest_is_required_lowercase_sha256 (input_digest : object ) -> None :
79+ report = build_v6 ()
80+ if input_digest is None :
81+ report .pop ("input_digest" )
82+ expected = "v6_fields_incomplete"
83+ else :
84+ report ["input_digest" ] = input_digest
85+ expected = "input_digest_invalid"
86+ with pytest .raises (AdvisoryValidationError , match = expected ):
87+ validate_advisory_report (report )
88+
89+
90+ def test_v5_does_not_silently_accept_v6_input_digest () -> None :
91+ report = build_v5 ()
92+ report ["input_digest" ] = V6_INPUT_DIGEST
93+ with pytest .raises (AdvisoryValidationError , match = "v6_fields_on_v5" ):
94+ validate_advisory_report (report )
95+
96+
97+ def test_checked_in_v6_contract_fixture_is_readable () -> None :
98+ fixture = json .loads ((ROOT / "tests/fixtures/advisory_report_v6.json" ).read_text (encoding = "utf-8" ))
99+ empty_input_set = {
100+ name : None
101+ for name in ("political_events" , "political_watchlist" , "ai_signal" , "theme_momentum" , "market_confirmation" )
102+ }
103+ canonical = json .dumps (empty_input_set , sort_keys = True , separators = ("," , ":" )).encode ("utf-8" )
104+ assert fixture ["input_digest" ] == hashlib .sha256 (canonical ).hexdigest ()
105+ validate_advisory_report (fixture )
106+
107+
75108@pytest .mark .parametrize (
76109 ("field" , "value" ),
77110 [
@@ -280,7 +313,12 @@ def test_manifest_version_is_report_derived_without_global_v6_contamination(tmp_
280313 markdown_path = markdown_path ,
281314 manifest_path = tmp_path / f"{ name } .manifest.json" ,
282315 )
283- assert json .loads (manifest .read_text (encoding = "utf-8" ))["contract_version" ] == expected
316+ payload = json .loads (manifest .read_text (encoding = "utf-8" ))
317+ assert payload ["contract_version" ] == expected
318+ if name == "v6" :
319+ assert payload ["input_digest" ] == V6_INPUT_DIGEST
320+ else :
321+ assert "input_digest" not in payload
284322
285323
286324def test_manifest_rejects_incomplete_v6_but_keeps_v5_legacy_omission (tmp_path : Path ) -> None :
0 commit comments