feat: CAP compatibility test in the popup + per-AID failure analysis (v3.6.52)

The compatibility test now runs in the same operation modal as the installs
(live progress from the server's ram_progress, Close disabled until done) and
its result gains a per-AID detail block instead of the comma list:

- the probe summary (probe: 2 imports -> 0.0 . +1 appended);
- the import list as tested (ramCompatImportRows): the CAP's imports with the
  version the card was asked for and the CAP's own version when the probe
  lowered it, appended synthetic entries and unmatched lines - each with the
  resolved library name (jcAidName), so a raw AID list becomes readable rows;
- on a rejection, the failure analysis (ramCompatFailureHtml): which CAP
  section the failing block covers (load-file byte range and component names,
  'completes Import'), what the card checks at that point (JC VM spec 4.5.2)
  or that the import list already passed, the remote status word with its
  name, and what the probe state implies (every import at 0.0 -> a missing
  package or another load-file check is the cause; otherwise lower the
  versions and bisect).

ramCompatVerdict names the remote SW; the inline result keeps the verdict +
short probe note and the modal carries the escaped step lines + the detail
HTML (ramOpModalFinish gained an asHtml mode); the server reports
probe_unmatched as [{'aid', 'version'}] (_probe_unmatched), documented in
docs/api.md.  The same failure block for the real install is the next step.

sw cache simple-v325; tests: 550 Python / 703 Node.
This commit is contained in:
2026-09-30 09:22:22 +03:00
parent 599d84fd66
commit ee747003f2
10 changed files with 434 additions and 42 deletions
+18 -1
View File
@@ -10,7 +10,8 @@ import zipfile
from pysim_simple_server import capmem
from pysim_simple_server.server import (
_ber_len, _cap_apdu_sequence, _cap_compat_blocks, _cap_import_aids,
_cap_import_end, _cap_info_body, _cap_parse, _probe_import_versions)
_cap_import_end, _cap_info_body, _cap_parse, _probe_import_versions,
_probe_unmatched)
# ─── synthetic CAP builder ───────────────────────────────────────────────
@@ -276,6 +277,22 @@ class ImportProbeTests(unittest.TestCase):
self.assertEqual(_cap_import_aids(self._load_file()),
[JAVACARD_FRAMEWORK.hex().upper(), self.OTHER_AID.hex().upper()])
def test_probe_unmatched_reports_the_requested_version(self):
known = set(_cap_import_aids(self._load_file()))
# an override for an AID the CAP does not import: kept, with the
# version the probe asked for (the modal lists it per AID)
self.assertEqual(
_probe_unmatched({JAVACARD_FRAMEWORK.hex().upper(): '0.0',
'DEADBEEF': '1.2', 'all': '0.0'}, known),
[{'aid': 'DEADBEEF', 'version': '1.2'}])
# matched AIDs and the 'all' override are not unmatched
self.assertEqual(_probe_unmatched({'all': '0.0'}, known), [])
self.assertEqual(_probe_unmatched({JAVACARD_FRAMEWORK.hex().lower(): '0.0'}, known), [])
# a lowercase / spaced unmatched AID is normalised for display
self.assertEqual(_probe_unmatched({'dead beef': '1.2'}, known),
[{'aid': 'DEADBEEF', 'version': '1.2'}])
self.assertEqual(_probe_unmatched(None, known), [])
def test_probe_rejects_bad_additions(self):
load = self._load_file()
with self.assertRaises(ValueError):
+25
View File
@@ -222,6 +222,31 @@ class CapCompatHttpTests(unittest.TestCase):
self.assertEqual(status, 400, resp)
self.assertIn('import probe', resp['error'])
def test_cap_compat_reports_unmatched_probe_lines_with_versions(self):
# v3.6.52: probe overrides that match no Import entry come back as a
# per-AID list ({aid, version}) for the modal's detail block.
from pysim_simple_server import server as srv
self.server.scc = object()
patches = [
mock.patch.object(srv, '_ram_detect_format', lambda *a, **k: 'compact'),
mock.patch.object(srv, '_ram_send_gp_apdu', lambda *a, **k: True),
]
for p in patches:
p.start()
try:
status, resp = self._post('/api/cap-compat', {
'cap_hex': _mini_cap_hex(),
'probe_imports': {'A0000000620101': '0.0', 'dead beef': '1.2'},
})
finally:
for p in reversed(patches):
p.stop()
self.assertEqual(status, 200, resp)
self.assertTrue(resp['success'], resp)
self.assertEqual(resp['probe_unmatched'], [{'aid': 'DEADBEEF', 'version': '1.2'}])
self.assertEqual(resp['probe_imports'],
[{'aid': 'A0000000620101', 'from': '1.0', 'to': '0.0'}])
class RamInstallNvFootprintHttpTests(unittest.TestCase):
"""The RAM install response carries the measured NV footprint (v3.6.51):