From 00675ca09c1e2059933a5a27f03961240b38f165 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?=
=?UTF-8?q?=D0=B8=D0=BD?=
Date: Wed, 7 Oct 2026 00:53:20 +0300
Subject: [PATCH] v3.22.7: a raised card error honours the step's on_fail
The runner's step loop caught an exception and hard-coded status "error" +
stop, ignoring on_fail - so a file-read of an absent optional file
(re-verified live on 3.22.6: MF/A153/49AC, note "6a82: Wrong parameters -
File not found", on_fail "warning" still stopped the run) could not express
the "read an optional file" pattern (49AC is documented optional: its
absence means no menu password).
The handler now treats a raised error as a failed "Error" check at the
step's level (the message as actual/detail and the note): on_fail "warning"
warns and the run continues, the default still stops; the drain-if-pending
behaviour is unchanged. Every raised exception is treated the same - the
two "file absent" raise paths (pySim's SwMatchError for model files and
probe_file's RuntimeError with the decoded SW for custom FIDs) cannot be
told apart by type, and load-time validation (normalise_script) stays
strict.
Docs: the guide's on_fail paragraph and run-errors list (the 6A82 bullet),
api.md and help EN/RU. Tests: a raising file-read with on_fail "warning"
warns and the next step runs; without on_fail the run still stops.
843 frontend + 821 Python; sw cache simple-v411.
---
docs/api.md | 4 +++-
docs/test-scripts.md | 11 +++++++--
frontend/help-ru.html | 2 +-
frontend/help.html | 2 +-
frontend/index.html | 2 +-
frontend/sw.js | 2 +-
pyproject.toml | 2 +-
pysim_simple_server/server.py | 21 ++++++++++++-----
tests/test_testscript.py | 44 +++++++++++++++++++++++++++++++++++
9 files changed, 76 insertions(+), 14 deletions(-)
diff --git a/docs/api.md b/docs/api.md
index b8ff33b..6298a5b 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -753,7 +753,9 @@ step left pending (`91XX`).
the decoded PoR of this exchange (inline or SEND SHORT MESSAGE; at least one
field). `on_fail` is `error` (terminates the script) or `warning` (continues)
and applies to every step failure - including an expectation that finds no
-pending command (v3.21.0; `warning` is the tolerant consume-if-pending form).
+pending command (v3.21.0; `warning` is the tolerant consume-if-pending form)
+and a card/transport error raised during the step (v3.22.7 - the error is
+reported as a failed `Error` check at the step's level).
**Expectation steps** (`type: "expect"`) require a command pending from the
previous step (`91XX`); they never poll - a `9000` response means no command
diff --git a/docs/test-scripts.md b/docs/test-scripts.md
index d5490c6..afae0b6 100644
--- a/docs/test-scripts.md
+++ b/docs/test-scripts.md
@@ -229,8 +229,11 @@ TEST-RUN step 4: MENU-SELECT ENVELOPE=80c2000009d30702020181900101 item=1 ('One'
Every step may carry `on_fail`: `"error"` (default - stops the run) or
`"warning"` (continues, the report shows ⚠). Each entry of `checks` may
override it with its own `on_fail`. The level applies to **every** step
-failure, including an expectation that finds no pending command (v3.21.0) -
-the tolerant "consume a command if there is one" form is an expectation with
+failure, including an expectation that finds no pending command (v3.21.0)
+and a **card error raised during the step** (v3.22.7 - an absent optional
+file's `6A82`, a refused SELECT: with `on_fail: "warning"` the step warns,
+the report shows the error as a failed check, and the run continues) - the
+tolerant "consume a command if there is one" form is an expectation with
`on_fail: "warning"`; to drain and confirm the card is idle, use the
`proactive-drain` action.
@@ -649,6 +652,10 @@ PoR-in-submit (with the transport asserted by the expectation):
names it). A `status` action with `attempts > 1` polls until a command
appears when the card delivers on poll; `on_fail: "warning"` tolerates the
empty case, and the `proactive-drain` action drains and confirms idle.
+- `6A82: … File not found` (or another raised card error) - the step's
+ `on_fail` decides: the default stops the run, `on_fail: "warning"` warns
+ and continues - the pattern for an **optional** file (`49AC` is documented
+ optional: its absence means no menu password).
- `menu-select: no menu item matches …` - the error lists the cached menu; a
miss on an explicit `case_sensitive: true` names the case-insensitive
candidates.
diff --git a/frontend/help-ru.html b/frontend/help-ru.html
index e57b781..a303002 100644
--- a/frontend/help-ru.html
+++ b/frontend/help-ru.html
@@ -566,7 +566,7 @@
- Действия — Envelope (Event Download: сырой тип события + данные, с опциональным переопределением источника — 82 терминал / 83 сеть) или Event (семантические формы: та же форма, что и в диалоге отправки на вкладке «Телефон», для всех смоделированных событий — четыре события, которые собирает сервер (Location status, Access technology change, Network rejection, Data connection status change), передаются полями, которые он проверяет и собирает во время запуска, остальные несут собранный формой hex данных события (показывается под формой, с проверкой одного ENVELOPE); источник device identities берётся из события (например, MT call → сеть) или из его поля источника), Menu Selection (item id 1–255 или текст пункта с точным/частичным совпадением, по умолчанию без учёта регистра — выбор «Учёт регистра» в редакторе шагов включает точное совпадение; текст подбирается по меню карты, которое следует за параметрами установки апплета), UPDATE/READ файла (путь + режим; чтение проверяет данные файла), сырой APDU (без авто-обработчика), SCP80 (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2 и формат команды — компактный или обёрнутый в шаблон Command Scripting TS 102 226 §5.2.1 expanded (AA) / expanded AE 80; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты — запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик — а сервер сохраняет потреблённый счётчик в пресет по ходу запуска), STATUS (при попытках > 1 опрашивает, пока карта не объявит команду) и Очистка очереди проактивных команд (опрос STATUS и поглощение всех объявленных команд — результат TERMINAL RESPONSE для каждой, опционально другой результат для первой; граница попыток/интервала; опциональная проверка require по поглощённому. Пустой опрос проходит, финальный STATUS должен быть idle, а оставшаяся команда передаётся следующему шагу — используйте в начале, середине или конце скрипта, чтобы убедиться, что карта перестала слать команды; она же поглощает команду, оставшуюся от предыдущего шага (ответившего
91XX)). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой ? — 91??) и проверка PoR для SCP80 (любой / без PoR / por_ok или своя проверка декодированного PoR — статус / SW / данные; транспорт через SEND SHORT MESSAGE работает так же).
- Ожидания — карта должна объявить проактивную команду на предыдущем шаге (SW
91XX, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); 9000 означает «команды нет»: шаг падает на своём уровне ошибок — Ошибка останавливает скрипт, Предупреждение продолжает (терпимая форма «поглотить команду, если она есть») — а действие STATUS с попытками > 1 опрашивает, когда апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое: текст / пункт / raw, проверка Alpha (идентификатор Alpha команды, тег 05/85 — отличается от текстовой строки 8D), проверка SMS TPDU (submit-TPDU команды SEND SHORT MESSAGE: получатель / PID / DCS / UDL / данные — для собственного SMS апплета), проверка PoR (SEND SHORT MESSAGE несёт PoR, когда пакет запросил PoR-in-submit, бит SPI2 20: статус ответа, SW R-APDU и данные ответа; перед этим должен выполниться шаг SCP80 — SW R-APDU находится внутри полезной нагрузки ответа: собственный TAR апплета возвращает данные приложения, поэтому проверяйте данные, а SW оставляйте пустым; отклонённый ENVELOPE (62XX/63XX) может вовсе не нести пакет ответа — для такого шага оставляйте проверку PoR «любой» или ожидайте «без PoR») и проверка Files (File List команды REFRESH, например квалификатор 00 = NAA initialization with full file change notification). TERMINAL RESPONSE (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.
- - Уровни ошибок — несовпадение помечает шаг как Ошибка (скрипт останавливается) или Предупреждение (продолжает); уровень применяется к любой ошибке шага, включая ожидание без объявленной команды; каждая проверка показывается с ожидаемым и фактическим значением.
+ - Уровни ошибок — несовпадение помечает шаг как Ошибка (скрипт останавливается) или Предупреждение (продолжает); уровень применяется к любой ошибке шага, включая ожидание без объявленной команды и ошибку карты, возникшую на шаге (например,
6A82 отсутствующего необязательного файла); каждая проверка показывается с ожидаемым и фактическим значением.
- Шаблоны — Empty, Обзор меню STK, Апплет через SCP80; редактор шагов — формный (действие Event показывает форму события из симулятора), а набор экспортируется/импортируется пакетом JSON (набор и его скрипты).
Каждый шаг пишет семантическую строку в журнал сервера под своим номером (TEST-RUN step N: …), запрос перед ответом: отправленный APDU/данные и полученный SW, для SCP80 — открытый C-APDU, собранный защищённый пакет и R-APDU из PoR, для ожидания — тройка FETCH / команда / TERMINAL RESPONSE, очистка очереди проактивных команд — каждый раунд STATUS/FETCH/команда/TR, а очистка в конце запуска — поглощённые команды (отвечаются ok — cancel отправляется только по явному запросу скрипта), а для выбора пункта меню — полный ENVELOPE с определённым пунктом. Запустите сервер с --apdu-trace, чтобы добавить и журнал сырых транспортных APDU.
diff --git a/frontend/help.html b/frontend/help.html
index 1b6ff07..50a92bb 100644
--- a/frontend/help.html
+++ b/frontend/help.html
@@ -565,7 +565,7 @@
- Actions — Envelope (Event Download: raw event type + data, with an optional source override — 82 terminal / 83 network) or Event (the semantic forms: the same form the Phone tab's send dialog uses, for every modelled event — the four events the server builds (Location status, Access technology change, Network rejection, Data connection status change) travel as fields it validates and builds at run time, every other event carries the form's built event-data hex (shown live under the form, with the one-ENVELOPE check); the device-identities source comes from the event (e.g. MT call → network) or its source field), Menu Selection (item id 1–255, or the item's text with an exact/contains match, case-insensitive by default — the step editor's Match case choice switches to an exact-case match; the text is resolved against the card's menu, which follows the applet's install parameters), UPDATE/READ file (path + mode; reads verify the file data), raw APDU (no auto-handler), SCP80 (C-APDU or a pre-built secured packet, optional keyset number (kvn) / TAR / SPI1 / SPI2 overrides and a command format — compact, or the TS 102 226 §5.2.1 Command Scripting template expanded (AA) / expanded AE 80 around the C-APDU; the keys and the counter come from the card preset matched by the equipped card's ICCID — the run names the stored preset, so it always uses its keysets, TARs and counter — the server persists the consumed counter into it as the run proceeds), STATUS (with attempts > 1 it polls until the card announces a command) and Proactive drain (poll STATUS and consume every announced command — the TERMINAL RESPONSE result for each, optionally a different result for the first; the attempt/interval bound; an optional require assertion over what was drained. An empty poll passes, the final STATUS must be idle, and a still-pending command is handed to the next step — use it at the start, in the middle or at the end of a script to make sure the card has stopped sending; it also consumes a command a previous step left pending (a step that answered
91XX). Every action has a response check: expected SW and data (exact, or a mask with ? nibble wildcards — 91??) plus a PoR check for SCP80 (any / no PoR / por_ok, or a custom assertion of the decoded PoR — status / SW / data; the SEND SHORT MESSAGE transport works the same).
- Expectations — the card must have announced a proactive command in the previous step (SW
91XX, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); a 9000 means “no command”: the step fails at its fail level — Error stops the script, Warning continues (the tolerant “consume a command if there is one” form) — and a STATUS action with attempts > 1 polls when the applet delivers on poll. The command type, qualifier and contents are verified: text / item / raw checks, an Alpha check (the command's Alpha identifier, tag 05/85 — distinct from the 8D text string), an SMS TPDU check (the submit TPDU of a SEND SHORT MESSAGE: destination / PID / DCS / UDL / user data — for an applet's own SMS), a PoR check (a SEND SHORT MESSAGE carries the PoR when the packet requested PoR-in-submit, SPI2 bit 20: the response status, the R-APDU SW and the response data; make sure an SCP80 step ran first — the R-APDU SW lives inside the response payload: an applet's own TAR returns application data, so check its data and leave the SW empty; a refused ENVELOPE (62XX/63XX) may carry no response packet at all — leave the PoR check at any or expect no PoR for such a step) and a Files check (the File List of a REFRESH, e.g. qualifier 00 = NAA initialization with full file change notification). The scripted TERMINAL RESPONSE (result, item identifier, text, extra TLVs) is part of the instruction.
- - Fail levels — a mismatch flags the step as Error (terminates the script) or Warning (continues); every step failure honours it, including an expectation that finds no pending command; each check is reported with expected vs actual values.
+ - Fail levels — a mismatch flags the step as Error (terminates the script) or Warning (continues); every step failure honours it, including an expectation that finds no pending command and a card error raised during the step (an absent optional file's
6A82); each check is reported with expected vs actual values.
- Templates — Empty, STK menu browsing, Applet via SCP80; the step editor is form-based (the event action renders the Simulator's event form), and a suite exports/imports as a JSON bundle (the suite plus its scripts).
Every step logs a semantic line to the server log under its step number (TEST-RUN step N: …), request before response: the APDU/data sent and the SW received, for SCP80 the plaintext C-APDU, the built secured packet and the PoR's R-APDU, for an expectation the FETCH / command / TERMINAL RESPONSE trio, a proactive drain every STATUS/FETCH/command/TR round and the run cleanup the commands it drained (answered ok — a cancel is only sent when a script asks for it), and for a menu selection the complete ENVELOPE with the resolved item. Start the server with --apdu-trace to add the raw transport APDU log as well.
diff --git a/frontend/index.html b/frontend/index.html
index 94df0b6..4503a7b 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1903,7 +1903,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
-const SIMPLE_VERSION = '3.22.6';
+const SIMPLE_VERSION = '3.22.7';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
diff --git a/frontend/sw.js b/frontend/sw.js
index 49d20f5..df2de50 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v410';
+const CACHE = 'simple-v411';
const URLS = [
'index.html',
'help.html',
diff --git a/pyproject.toml b/pyproject.toml
index a20d909..8ec9d8d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.22.6"
+version = "3.22.7"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py
index dd43f2d..3b0d54d 100644
--- a/pysim_simple_server/server.py
+++ b/pysim_simple_server/server.py
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
-VERSION = '3.22.6'
+VERSION = '3.22.7'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -7124,16 +7124,25 @@ def _test_run_script(server, script, preset, shared=None):
# Error terminates the script; Warning and OK continue.
break
except Exception as e:
- _test_entry_update(entry, {
- 'status': 'error', 'note': str(e),
- 'ms': int((time.time() - entry['started']) * 1000)})
- sys.stderr.write('TEST-RUN step %d failed: %s\n' % (index + 1, e))
+ # A raised card/transport error is a step failure like any other:
+ # it honours the step's on_fail (v3.22.7). An absent optional
+ # file (6A82), a refused SELECT or a preset/keyset error then
+ # warns and the run continues, with the error as a failed check;
+ # the default (error) still stops the run.
+ level = step.get('on_fail') or 'error'
+ check = _test_check_result('Error', False, 'the step to run',
+ str(e), level, detail=str(e))
+ _test_finish_entry(entry, {'status': level, 'checks': [check],
+ 'note': str(e)})
+ sys.stderr.write('TEST-RUN step %d failed (%s): %s\n'
+ % (index + 1, level, e))
if pending is not None:
with _CARD_LOCK:
drained = _test_drain_pending(scc, pending)
_test_entry_update(entry, {k: v for k, v in drained.items() if k != 'note'})
pending = None
- break
+ if level == 'error':
+ break
if pending is not None:
with _CARD_LOCK:
drained = _test_drain_pending(scc, pending)
diff --git a/tests/test_testscript.py b/tests/test_testscript.py
index 6123951..1774428 100644
--- a/tests/test_testscript.py
+++ b/tests/test_testscript.py
@@ -1690,6 +1690,50 @@ class TestRunnerActions(RunnerTestCase):
self.assertEqual(lchan.written, 'AA55')
self.assertEqual(run['steps'][1]['data'], 'AA55')
+ def test_a_raised_card_error_honours_on_fail(self):
+ # v3.22.7: a card error raised during a step (an absent optional
+ # file's 6A82, a refused SELECT) is a step failure like any other -
+ # on_fail "warning" warns and the run continues.
+ lchan = object() # _select_path (patched) raises before its use
+ server = FakeServer(FakeScc())
+ server.app = type('App', (), {'rs': type('Rs', (), {'lchan': [lchan]})()})()
+ server.scc.push('80F2', '', '9000')
+ with mock.patch.object(S, '_select_path', side_effect=RuntimeError(
+ '6a82: Wrong parameters - File not found')):
+ run = self.run_script(server, [
+ {'type': 'action', 'kind': 'file-read',
+ 'params': {'path': 'MF/A153/49AC'}, 'on_fail': 'warning'},
+ {'type': 'action', 'kind': 'status', 'params': {}},
+ ])
+ self.assertEqual(run['status'], 'warning', run['steps'])
+ self.assertEqual(len(run['steps']), 2)
+ entry = run['steps'][0]
+ self.assertEqual(entry['status'], 'warning')
+ self.assertIn('6a82', entry['note'])
+ check = entry['checks'][0]
+ self.assertEqual(check['label'], 'Error')
+ self.assertFalse(check['ok'])
+ self.assertEqual(check['level'], 'warning')
+ self.assertIn('6a82', check['actual'])
+ # the following step ran
+ self.assertEqual(run['steps'][1]['status'], 'ok')
+
+ def test_a_raised_card_error_still_stops_by_default(self):
+ lchan = object() # _select_path (patched) raises before its use
+ server = FakeServer(FakeScc())
+ server.app = type('App', (), {'rs': type('Rs', (), {'lchan': [lchan]})()})()
+ with mock.patch.object(S, '_select_path', side_effect=RuntimeError(
+ '6a82: Wrong parameters - File not found')):
+ run = self.run_script(server, [
+ {'type': 'action', 'kind': 'file-read',
+ 'params': {'path': 'MF/A153/49AC'}},
+ {'type': 'action', 'kind': 'status', 'params': {}},
+ ])
+ self.assertEqual(run['status'], 'error', run['steps'])
+ self.assertEqual(len(run['steps']), 1)
+ self.assertEqual(run['steps'][0]['status'], 'error')
+ self.assertIn('6a82', run['steps'][0]['note'])
+
def test_run_guard_blocks_card_endpoints_only(self):
S._TEST_RUNNING = True
try: