5050PICO_GATES = ("GP0" , "GP1" , "GP2" )
5151VERSION = "0.6.0"
5252RELEASE_DIGEST = "ab" * 32
53+ OI1_RAW_SHA256 = "cd" * 32
5354
5455
5556def load_module (name : str , path : Path ):
@@ -87,6 +88,33 @@ def sha256_bytes(value: bytes) -> str:
8788 return hashlib .sha256 (value ).hexdigest ()
8889
8990
91+ def post_oi_nvs_binding (
92+ artifact : bytes ,
93+ * ,
94+ release_digest : str = RELEASE_DIGEST ,
95+ oi1_raw_sha256 : str = OI1_RAW_SHA256 ,
96+ ) -> dict [str , object ]:
97+ summary = {
98+ "schema_version" : 1 ,
99+ "profile_id" : C3_PROFILE ,
100+ "candidate_release_json_sha256" : release_digest ,
101+ "candidate_firmware_sha256" : sha256_bytes (artifact ),
102+ "oi1_raw_sha256" : oi1_raw_sha256 ,
103+ "reset_samples" : 10 ,
104+ "partition_offset" : 0x9000 ,
105+ "partition_size" : 0x6000 ,
106+ "integrity" : "passed" ,
107+ "written_namespaces" : [],
108+ }
109+ receipt = {"summary" : summary , "inventory" : []}
110+ raw = canonical_json_bytes (receipt )
111+ return {
112+ "receipt_sha256" : sha256_bytes (raw ),
113+ "receipt_size_bytes" : len (raw ),
114+ "summary" : summary ,
115+ }
116+
117+
90118def private_result (
91119 profile_id : str ,
92120 artifact : bytes ,
@@ -101,7 +129,7 @@ def private_result(
101129 gate_names = PICO_GATES
102130 else : # pragma: no cover - fixture misuse.
103131 raise AssertionError ("unsupported private-gate fixture profile" )
104- return {
132+ result = {
105133 "schema_version" : 1 ,
106134 "status" : "passed" ,
107135 "profile_id" : profile_id ,
@@ -110,6 +138,12 @@ def private_result(
110138 digest_key : sha256_bytes (artifact ),
111139 "gates" : {name : "passed" for name in gate_names },
112140 }
141+ if profile_id == C3_PROFILE :
142+ result ["post_oi_nvs" ] = post_oi_nvs_binding (
143+ artifact ,
144+ release_digest = release_digest ,
145+ )
146+ return result
113147
114148
115149def write_private_result (path : Path , value : object ) -> None :
@@ -123,6 +157,7 @@ def expected_summary(
123157 artifact : bytes ,
124158) -> dict [str , object ]:
125159 value = private_result (profile_id , artifact )
160+ value .pop ("post_oi_nvs" , None )
126161 value ["qualification_result_sha256" ] = sha256_bytes (result_path .read_bytes ())
127162 return value
128163
@@ -134,6 +169,7 @@ def test_pure_private_gate_module_and_api_exist(self) -> None:
134169 return
135170 for name in (
136171 "validate_result_file" ,
172+ "validate_c3_result_for_observation" ,
137173 "validate_public_summary" ,
138174 "private_result_snapshot" ,
139175 ):
@@ -295,6 +331,80 @@ def test_artifact_kind_digest_and_bytes_cannot_be_substituted(self) -> None:
295331 with self .assertRaises (GATE .QualificationError ):
296332 self .validate (profile_id )
297333
334+ def test_c3_post_oi_nvs_binding_is_exact_and_observation_bound (self ) -> None :
335+ original = private_result (C3_PROFILE , self .artifacts [C3_PROFILE ])
336+ observation = {
337+ "raw_log_sha256" : OI1_RAW_SHA256 ,
338+ "reset_to_service_advertisement_ms" : [1000 ] * 10 ,
339+ }
340+ summary = GATE .validate_c3_result_for_observation (
341+ self .result_paths [C3_PROFILE ],
342+ artifact_path = self .artifact_paths [C3_PROFILE ],
343+ expected_version = VERSION ,
344+ candidate_release_json_sha256 = RELEASE_DIGEST ,
345+ observation = observation ,
346+ )
347+ self .assertNotIn ("post_oi_nvs" , summary )
348+
349+ mutations = {
350+ "missing" : lambda value : value .pop ("post_oi_nvs" ),
351+ "receipt-digest" : lambda value : value ["post_oi_nvs" ].__setitem__ (
352+ "receipt_sha256" , "0" * 64
353+ ),
354+ "receipt-size" : lambda value : value ["post_oi_nvs" ].__setitem__ (
355+ "receipt_size_bytes" , 0
356+ ),
357+ "release" : lambda value : value ["post_oi_nvs" ]["summary" ].__setitem__ (
358+ "candidate_release_json_sha256" , "0" * 64
359+ ),
360+ "firmware" : lambda value : value ["post_oi_nvs" ]["summary" ].__setitem__ (
361+ "candidate_firmware_sha256" , "0" * 64
362+ ),
363+ "raw" : lambda value : value ["post_oi_nvs" ]["summary" ].__setitem__ (
364+ "oi1_raw_sha256" , "0" * 64
365+ ),
366+ "resets" : lambda value : value ["post_oi_nvs" ]["summary" ].__setitem__ (
367+ "reset_samples" , 9
368+ ),
369+ "offset" : lambda value : value ["post_oi_nvs" ]["summary" ].__setitem__ (
370+ "partition_offset" , 0
371+ ),
372+ "size" : lambda value : value ["post_oi_nvs" ]["summary" ].__setitem__ (
373+ "partition_size" , 0x5000
374+ ),
375+ "integrity" : lambda value : value ["post_oi_nvs" ]["summary" ].__setitem__ (
376+ "integrity" , "failed"
377+ ),
378+ "phy" : lambda value : value ["post_oi_nvs" ]["summary" ].__setitem__ (
379+ "written_namespaces" , ["phy" ]
380+ ),
381+ }
382+ for name , mutate in mutations .items ():
383+ with self .subTest (mutation = name ):
384+ changed = copy .deepcopy (original )
385+ mutate (changed )
386+ self .rewrite (C3_PROFILE , changed )
387+ with self .assertRaises (GATE .QualificationError ):
388+ self .validate (C3_PROFILE )
389+
390+ self .rewrite (C3_PROFILE , original )
391+ for name , changed_observation in {
392+ "wrong-raw" : {** observation , "raw_log_sha256" : "0" * 64 },
393+ "nine-resets" : {
394+ ** observation ,
395+ "reset_to_service_advertisement_ms" : [1000 ] * 9 ,
396+ },
397+ }.items ():
398+ with self .subTest (observation = name ):
399+ with self .assertRaises (GATE .QualificationError ):
400+ GATE .validate_c3_result_for_observation (
401+ self .result_paths [C3_PROFILE ],
402+ artifact_path = self .artifact_paths [C3_PROFILE ],
403+ expected_version = VERSION ,
404+ candidate_release_json_sha256 = RELEASE_DIGEST ,
405+ observation = changed_observation ,
406+ )
407+
298408 def test_gate_sets_require_every_exact_gate_passed (self ) -> None :
299409 for profile_id , gate_names in (
300410 (C3_PROFILE , C3_GATES ),
@@ -446,6 +556,7 @@ def payload(self) -> dict[str, object]:
446556 def summaries (self ) -> tuple [dict [str , object ], dict [str , object ], dict [str , object ]]:
447557 waveshare = {"schema_version" : 1 , "status" : "passed" }
448558 c3 = private_result (C3_PROFILE , b"c3" )
559+ c3 .pop ("post_oi_nvs" )
449560 c3 ["qualification_result_sha256" ] = "2" * 64
450561 pico = private_result (PICO_PROFILE , b"pico" )
451562 pico ["qualification_result_sha256" ] = "3" * 64
@@ -566,6 +677,14 @@ def test_finalizer_source_snapshots_validates_binds_and_rechecks_both_inputs(sel
566677 "each C3/Pico input must be snapshotted before and after promotion" ,
567678 )
568679
680+ def test_completion_and_finalizer_bind_c3_receipt_to_the_oi_observation (self ) -> None :
681+ seam = "_V060_PROFILE_GATE.validate_c3_result_for_observation"
682+ self .assertIn (
683+ seam ,
684+ inspect .getsource (RELEASE .create_hil_completion_fragment ),
685+ )
686+ self .assertIn (seam , inspect .getsource (RELEASE .finalize_public_bundle ))
687+
569688 def test_public_tree_validator_revalidates_both_derived_summaries (self ) -> None :
570689 source = inspect .getsource (RELEASE ._validate_hil )
571690 self .assertIn ("_V060_PROFILE_GATE.validate_public_summary" , source )
0 commit comments