@@ -180,8 +180,8 @@ def finish(self):
180180 self ._flush ()
181181
182182def _lookup (results , mod , meth ):
183- """Look up test result by module::method (precise), then bare method (fallback) ."""
184- return results .get (f'{ mod } ::{ meth } ' ) or results . get ( meth ) or ''
183+ """Look up only the exact catalog module::method identity ."""
184+ return results .get (f'{ mod } ::{ meth } ' ) or ''
185185
186186def ver_t (s ): return tuple (int (x ) for x in s .replace ('v' ,'' ).split ('.' )[:3 ])
187187def ver_ge (a , b ): return ver_t (a ) >= ver_t (b )
@@ -205,8 +205,7 @@ def detect_fw():
205205 except : return None
206206
207207def parse_junit (path ):
208- """Parse junit XML for pass/fail. Returns dict keyed by 'module::method' (precise)
209- and 'method' (fallback). Module is extracted from classname: tests.test_msg_foo.TestBar → test_msg_foo."""
208+ """Parse JUnit status under exact module::method and native-suite keys."""
210209 if not path or not os .path .exists (path ): return {}
211210 import xml .etree .ElementTree as ET
212211 results = {}
@@ -217,12 +216,15 @@ def parse_junit(path):
217216 elif tc .find ('error' ) is not None : status = 'error'
218217 elif tc .find ('skipped' ) is not None : status = 'skip'
219218 else : status = 'pass'
220- # Extract module from classname: tests.test_msg_foo.TestBar → test_msg_foo
219+ # Extract the test module from a pytest classname such as
220+ # tests.test_protection_levels.TestProtectionLevels. Exact lookup is
221+ # safe across every test_* module and avoids collision-prone method
222+ # fallback.
221223 mod = ''
222224 if cls :
223225 parts = cls .split ('.' )
224226 for p in parts :
225- if p .startswith ('test_msg_' ) or p . startswith ( 'test_sign_' ) or p . startswith ( 'test_verify_ ' ):
227+ if p .startswith ('test_ ' ):
226228 mod = p
227229 break
228230 results [f'{ cls } .{ name } ' ] = status
@@ -233,9 +235,6 @@ def parse_junit(path):
233235 # Key by module::method (disambiguates collisions like test_sign_btc_eth_swap)
234236 if mod :
235237 results [f'{ mod } ::{ name } ' ] = status
236- # Bare method fallback -- only set if no collision
237- if name not in results or status == 'pass' :
238- results [name ] = status
239238 return results
240239
241240
@@ -379,6 +378,10 @@ def junit_reconciliation(path):
379378 ('test_msg_resetdevice' , 'test_reset_device_pin' ),
380379 ('test_msg_resetdevice' , 'test_reset_device_18_words' ),
381380 ('test_msg_resetdevice' , 'test_reset_device_24_words' ),
381+ ('test_msg_recoverydevice_cipher' , 'test_nopin_nopassphrase' ),
382+ ('test_msg_recoverydevice_cipher' , 'test_pin_passphrase' ),
383+ ('test_msg_changepin' , 'test_set_pin' ),
384+ ('test_msg_ethereum_signtx' , 'test_ethereum_blind_sign_allowed' ),
382385 ('test_msg_ethereum_signtx' , 'test_ethereum_blind_sign_blocked' ),
383386 ('BodyFits' ,
384387 'ConstantPowerSeedRowsAreCompleteAndPagedAtRowBoundaries' ),
@@ -607,6 +610,8 @@ def junit_reconciliation(path):
607610 'Generate 12-word seed' ,
608611 'Device generates 128 bits of entropy from its RNG, combines it with host entropy, '
609612 'and displays the resulting 12-word BIP-39 mnemonic on every physical OLED page. '
613+ 'Each 256x64 image is one physical frame; its opposing-polarity halves are the '
614+ 'intentional constant-power layout, not a stitched composite. '
610615 'Words are NEVER sent over the normal host transport. '
611616 'User writes them down as their backup.' ,
612617 ['Seed word display' ]),
@@ -623,7 +628,8 @@ def junit_reconciliation(path):
623628 ('C3' , 'test_msg_resetdevice' , 'test_reset_device_pin' ,
624629 'Generate seed with PIN' ,
625630 'Same as C2 but also sets a PIN. PIN is entered twice for confirmation via the '
626- 'randomized 3x3 grid on OLED. Verifies PIN is stored and required for subsequent operations.' ,
631+ 'fully animated randomized 3x3 grid on OLED. Verifies PIN is stored and required for '
632+ 'subsequent operations.' ,
627633 ['PIN entry grid' ]),
628634 ('C4' , 'test_msg_resetdevice' , 'test_failed_pin' ,
629635 'PIN mismatch rejects setup' ,
@@ -655,14 +661,16 @@ def junit_reconciliation(path):
655661 []),
656662 ('C10' , 'test_msg_recoverydevice_cipher' , 'test_nopin_nopassphrase' ,
657663 'Cipher recovery (no PIN)' ,
658- 'Recovery via scrambled keyboard on OLED. The letter grid is randomized per-character, '
664+ 'Recovery via the fully animated scrambled keyboard on OLED. The letter grid is '
665+ 'randomized per-character, '
659666 'so even a compromised host cannot determine which letters the user selected. After all '
660- 'words are entered, device verifies BIP-39 checksum and reconstructs the seed.' ,
667+ 'words are entered, the terminal Success proves checksum validation and seed '
668+ 'reconstruction; cipher entry itself may end on an auto-completed word prefix.' ,
661669 ['Cipher grid on OLED' ]),
662670 ('C11' , 'test_msg_recoverydevice_cipher' , 'test_pin_passphrase' ,
663671 'Cipher recovery with PIN + passphrase' ,
664- 'Same recovery flow as C10 but also sets PIN and enables passphrase protection during '
665- 'the recovery process .' ,
672+ 'Same recovery flow as C10 but also captures the completed PIN grids and enables '
673+ 'passphrase protection as a stored setting; no passphrase value is entered during recovery .' ,
666674 ['Cipher + PIN entry' ]),
667675 ('C12' , 'test_msg_recoverydevice_cipher' , 'test_character_fail' ,
668676 'Invalid character rejection' ,
@@ -1117,7 +1125,7 @@ def junit_reconciliation(path):
11171125 'Blind sign permitted (AdvancedMode ON)' ,
11181126 'Contract data with AdvancedMode enabled. Device allows signing only after opaque-policy '
11191127 'and complete-calldata hash disclosure.' ,
1120- []),
1128+ ['Opaque policy warning' , 'Complete calldata hash approval' ]),
11211129 ('V9' , 'test_msg_ethereum_signtx' , 'test_ethereum_blind_sign_blocked' ,
11221130 'Opaque contract data blocked (AdvancedMode OFF)' ,
11231131 'On 7.14.2 the same arbitrary calldata is refused by policy before any signing approval. '
@@ -1513,6 +1521,7 @@ def render(output_path, fw_version, results, screenshot_dir=None,
15131521 pb .text (7 , f'Generator blob SHA: { provenance .get ("generator_blob_sha" , "MISSING" )} ' )
15141522 pb .text (7 , f'Generator SHA-256: { provenance .get ("generator_sha256" , "MISSING" )} ' )
15151523 pb .text (7 , f'ARM manifest SHA-256: { provenance .get ("arm_manifest_sha256" , "MISSING" )} ' )
1524+ pb .text (7 , f'Report controls SHA-256: { provenance .get ("report_controls_sha256" , "MISSING" )} ' )
15161525 pb .text (7 , f'Build: { provenance .get ("build_label" , "MISSING" )} ' )
15171526 for line in _w (f'CI run: { provenance .get ("run_url" , "MISSING" )} ' , 88 ):
15181527 pb .text (7 , line )
@@ -1950,6 +1959,8 @@ def main():
19501959 p .add_argument ('--run-url' , default = None , help = 'immutable CI run URL' )
19511960 p .add_argument ('--arm-manifest-sha256' , default = None ,
19521961 help = 'SHA-256 of the exact-head ARM binary manifest' )
1962+ p .add_argument ('--report-controls-sha256' , default = None ,
1963+ help = 'SHA-256 of the passing report mutation controls' )
19531964 p .add_argument ('--completeness-output' , default = None ,
19541965 help = 'write machine-readable catalog/frame completeness JSON' )
19551966 p .add_argument ('--required-tests-manifest' , default = None ,
@@ -2024,6 +2035,7 @@ def main():
20242035 'build_label' : args .build_label ,
20252036 'run_url' : args .run_url ,
20262037 'arm_manifest_sha256' : args .arm_manifest_sha256 ,
2038+ 'report_controls_sha256' : args .report_controls_sha256 ,
20272039 }
20282040 missing_provenance = [k for k , value in required_provenance .items () if not value ]
20292041 if missing_provenance :
@@ -2053,6 +2065,12 @@ def main():
20532065 print ('ERROR: ARM manifest SHA-256 is not exact: %s' %
20542066 args .arm_manifest_sha256 , file = sys .stderr )
20552067 sys .exit (2 )
2068+ if (len (args .report_controls_sha256 ) != 64 or
2069+ any (c not in '0123456789abcdef'
2070+ for c in args .report_controls_sha256 .lower ())):
2071+ print ('ERROR: report controls SHA-256 is not exact: %s' %
2072+ args .report_controls_sha256 , file = sys .stderr )
2073+ sys .exit (2 )
20562074 catalog_errors = catalog_integrity (fw , derived_sources )
20572075 if catalog_errors :
20582076 print ('ERROR: release evidence catalog is inconsistent:' , file = sys .stderr )
0 commit comments