fix: test-script review findings (v3.5.13)

PWA:
- The SCP80 "Source" switch sticks: the choice is stored in
  `params.source`, switching keeps both values (the server honors the
  explicit source when both are present, and the form validates only the
  selected one).
- The SW check field is empty by default (placeholder "default: 9000
  (91?? when polling)"), so the server defaults apply - previously the
  pre-filled 9000 defeated the polling STATUS default and a card that
  announced a command made the step fail with "expected 9000".
- Item text checks offer contains/exact only (the mask mode was rejected
  by the server validation).
- The SCP80 counter is written back to the preset even when the run is
  observed after a page reload or was started elsewhere (resolved by
  ICCID or preset name from the run snapshot).

Server:
- Step-entry mutations happen under `_TEST_LOCK`
  (`_test_entry_update`/`_test_finish_entry`): the status endpoint
  serializes the state with json.dumps, so entries must not change while
  it iterates them.
- The pending-command drains (unexpected command, error, stop) hold
  `_CARD_LOCK` like every other card conversation.
- `_int` accepts plain decimals with leading zeros and 0x hex.
- The scripted TERMINAL RESPONSE text string uses the CR-set tag `8D`
  (consistent with the other TR TLVs; both are legal).
- A script-driven menu selection mirrors `server.menu_active`.

Tests: frontend +4 (source switch/round-trip, status SW default, render
checks, item modes), Python +3 (integer parsing, menu_active, source
selection).  Help/docs unaffected beyond api.md's `source` note.

583 frontend / 470 Python green.
This commit is contained in:
2026-09-27 01:48:12 +03:00
parent d1c6a9c27a
commit d1eb88d4d7
8 changed files with 229 additions and 64 deletions
+12 -6
View File
@@ -60,7 +60,8 @@ def _fail_level(value, default='error'):
def _int(value, what, lo, hi):
try:
v = int(str(value).strip(), 0)
text = str(value).strip()
v = int(text, 16 if text.lower().startswith('0x') else 10)
except (TypeError, ValueError):
raise ScriptError('%s must be an integer' % what)
if not lo <= v <= hi:
@@ -233,12 +234,17 @@ def _normalise_params(kind, p):
return {'apdu': _data_hex(p.get('apdu'), 'apdu')}
if kind == 'scp80':
out = {}
if p.get('sp'):
source = str(p.get('source') or '').lower()
if source not in ('', 'apdu', 'sp'):
raise ScriptError('scp80: source must be apdu or sp')
if source == 'sp' or (not source and p.get('sp')):
if not p.get('sp'):
raise ScriptError('scp80: secured packet is required')
out['sp'] = _data_hex(p.get('sp'), 'secured packet')
elif p.get('apdu'):
out['apdu'] = _data_hex(p.get('apdu'), 'scp80 apdu')
else:
raise ScriptError('scp80: apdu or sp is required')
if not p.get('apdu'):
raise ScriptError('scp80: apdu is required')
out['apdu'] = _data_hex(p.get('apdu'), 'scp80 apdu')
for key in ('tar', 'spi1', 'spi2'):
if p.get(key) not in (None, ''):
out[key] = _data_hex(p[key], 'scp80 %s' % key)
@@ -402,6 +408,6 @@ def build_tr(cmd_num, cmd_type, dev_dst, dev_src, respond):
if respond.get('text') is not None:
dcs = int(respond.get('dcs', 0x00))
body = _encode_text(respond['text'], dcs)
out += bytes([0x0D, len(body) + 1, dcs]) + body
out += bytes([0x8D, len(body) + 1, dcs]) + body
out += bytes([0x83, 0x02, result & 0xFF, 0x00])
return bytes(out)