fix: report the real PoR/remote-SW result of every RAM install step (v3.6.2)

The RAM installer read `por['decoded']['response_status']`, but the compact
response decoder's `decoded` only carries {number_of_commands,
last_status_word, last_response_data} - so the suffix was always empty and
every step showed the useless `por_error_`, even when the PoR was por_ok.
The PoR verdict is the top-level `response_status`.

With that fixed, the decoded details also show that the remote command's own
status word was the real verdict all along: a captured live install returned
por_ok with `last_status_word` 6700/6F00 (the card rejected every RAM APDU)
while the installer reported success.

- new `_ram_step_result()`/`_por_remote_sw()`/`_ram_remote_sw_ok()`: a step
  fails on a non-por_ok PoR, on a remote SW outside the success set (9000,
  61xx more data, 62xx/63xx warnings, CAFE GP "more data"), or on an
  undecodable PoR (a 9000 transport SW with no PoR at all is `no_por`, not a
  failure).  A decoded 61xx is explicitly success, per GP/ISO.
- step records gain por_sw/por_type/por_cntr/por_data/por_raw and
  `por_error`; the response carries `failed_step` and a detailed error such
  as `LOAD (1/9): remote SW 6700`; the log line now prints
  `status=por_ok remote_sw=6700`.
- PWA: new pure `ramStepLine()` renders e.g.
  `❌ Шаг 2: LOAD (1/9) — PoR ok · remote SW 6700 (Wrong length in Lc) · 274 B / 3 SMS`
  (SW meaning via the existing lookupSw decoder) and the transport SW only
  when it is not 9000.
- tests: tests/test_ota_helpers.py RamPorStepTest (success set incl. 61xx,
  the captured 6700 failure, no-PoR/undecodable, expanded responses);
  ram.test.js ramStepLine cases.
- docs/api.md RAM install step fields + failure semantics; AGENTS updated.

603 frontend / 487 Python green; version 3.6.2; sw simple-v275.
This commit is contained in:
2026-09-27 23:21:56 +03:00
parent b92a47cd44
commit 36d2f71bc7
7 changed files with 215 additions and 27 deletions
+62
View File
@@ -30,6 +30,9 @@ from pysim_simple_server.server import (
_ota_reference,
_parse_select_item,
_parse_setup_menu_items,
_por_remote_sw,
_ram_remote_sw_ok,
_ram_step_result,
_record_tr,
_send_secured_packet,
_spi_from_bytes,
@@ -1243,3 +1246,62 @@ class TerminalProfileTest(unittest.TestCase):
self.assertEqual(server_obj.event_list, [0x09])
self.assertEqual(server_obj.sim_menu, 'menu')
self.assertTrue(resp['menu'])
class RamPorStepTest(unittest.TestCase):
"""The RAM install reports the PoR verdict and the remote command's own
status word. Vectors: a captured live response where every remote
command returned SW 6700/6F00 while the PoR itself was por_ok, and
synthetic success responses (9000, 6113)."""
# 02 71 00 | 000e 0a | TAR b00011/000000 | CNTR | PCNT | STS=00 | compact
POR_INSTALL = '027100000e0a000000000000011b0000026700'
POR_INSTALL_OK = '027100000e0ab0001100000000000000019000'
POR_INSTALL_61XX = '027100000e0ab0001100000000000000016113'
@staticmethod
def _por(hexstr):
return _decode_por('00', '00', '01', '01', '0000000000',
'00' * 16, '00' * 16, hexstr)
def test_remote_sw_success_set(self):
for sw in ('9000', '6113', '62F1', '6310', 'CAFE'):
self.assertTrue(_ram_remote_sw_ok(sw), sw)
for sw in ('6700', '6F00', '6A82', '6400', '', None):
self.assertFalse(_ram_remote_sw_ok(sw), sw)
def test_step_reports_the_remote_sw_failure(self):
por = self._por(self.POR_INSTALL)
self.assertEqual(por['response_status'], 'por_ok')
step, err = _ram_step_result('LOAD (1/9)', '9000', por,
self.POR_INSTALL, 274, 3)
self.assertEqual(step['por_status'], 'por_ok')
self.assertEqual(step['por_sw'], '6700')
self.assertEqual(step['por_type'], 'compact')
self.assertEqual(err, 'remote SW 6700')
self.assertEqual(step['por_error'], 'remote SW 6700')
def test_step_accepts_9000_and_61xx_remote_sw(self):
for hexstr, sw in ((self.POR_INSTALL_OK, '9000'),
(self.POR_INSTALL_61XX, '6113')):
por = self._por(hexstr)
step, err = _ram_step_result('LOAD (1/9)', '9000', por, hexstr, 10, 1)
self.assertIsNone(err, sw)
self.assertNotIn('por_error', step)
self.assertEqual(step['por_sw'], sw)
def test_step_no_por_and_undecodable(self):
# transport SW 9000 with no response data at all: no PoR expected
step, err = _ram_step_result('LOAD', '9000', None, '', 10, 1)
self.assertIsNone(err)
self.assertEqual(step['por_status'], 'no_por')
# response data present but undecodable: a failure with the raw kept
step, err = _ram_step_result('LOAD', '9000', None, 'DEADBEEF', 10, 1)
self.assertEqual(err, 'PoR undecodable')
self.assertEqual(step['por_raw'], 'DEADBEEF')
def test_remote_sw_from_expanded_response(self):
por = {'response_status': 'por_ok', 'decoded': {},
'responses': [{'status_word': '9000'}, {'status_word': '6A82'}]}
self.assertEqual(_por_remote_sw(por), '6A82')