v3.22.1: rename the proactive action kind to proactive-drain

The kind string `proactive` read like an expectation of a proactive command
(`expect` is the step type for that) although the action consumes and answers
them.  Renamed everywhere:

- ACTION_KINDS / TEST_ACTION_KINDS (drift-tested), the normaliser branch and
  its error strings, the dispatch and the helper names (_test_run_drain,
  _drain_require_match/_label);
- the report label is DRAIN and the PWA summary reads "DRAIN x5 require ...
  TR ok";
- the editor/collect/summary/problem branches, docs/test-scripts.md
  (Proactive drain), docs/api.md, help EN/RU ("Очистка очереди proactive");
- the tests (TestRunnerProactiveDrain, the normalisation/data-check tests).

No stored scripts used the old kind (unpublished series), so there is no
alias and no migration.  sw cache simple-v404.
Tests: 839 frontend + 804 Python green.
This commit is contained in:
2026-10-06 01:41:53 +03:00
parent e12dfc7871
commit f0a836363c
11 changed files with 78 additions and 78 deletions
+3 -3
View File
@@ -737,8 +737,8 @@ around the C-APDU) - KIc/KID and the counter always
come from the resolved preset, which must match the equipped card and be
complete), `status`
(`attempts`, `interval_ms` - when `attempts > 1` the default SW check is the
mask `91??`, i.e. poll until the card announces a command) or `proactive`
(the proactive cleanup: poll STATUS and consume every announced command -
mask `91??`, i.e. poll until the card announces a command) or `proactive-drain`
(the proactive drain: poll STATUS and consume every announced command -
`respond`/`first` TERMINAL RESPONSE results, `attempts`/`interval_ms` bound,
optional `require` `{command, qualifier?}` asserting at least one drained
command; the final SW is the step's `check.sw`, so a still-pending card fails
@@ -786,7 +786,7 @@ TERMINAL RESPONSE answers `91XX` (bounded); a cancel is never sent
automatically (it tells the card the user aborted the session and can push
assertive applications onto an error path). The cleanup is reported as a
`cleanup` step; a script that wants the refusal path answers explicitly (an
expectation's `respond`, or the `proactive` action's `first`).
expectation's `respond`, or the `proactive-drain` action's `first`).
### `GET /api/test/scripts` and the script store
+9 -9
View File
@@ -35,7 +35,7 @@ server-side since 3.16.0, `menu-select` by text and the spec-order TERMINAL
RESPONSE codings since 3.17.0, the semantic `event` action since 3.18.0, the
`envelope` action's `src` override since 3.18.1, the inline `por` object, the
`alpha`/`sms` checks, `scp80.params.format` and the case-insensitive
`menu-select` text match since 3.19.0, the `proactive` cleanup action and the
`menu-select` text match since 3.19.0, the `proactive-drain` action and the
ok-answering bounded cleanup since 3.21.0, the suites and the Simulator's
event forms for all modelled events since 3.22.0.
@@ -200,7 +200,7 @@ TEST-RUN step 4: MENU-SELECT ENVELOPE=80c2000009d30702020181900101 item=1 ('One'
- Other actions log their send/response pair too (`APDU TX=… -> SW=…`,
`ENVELOPE(Event Download) … -> SW=…`, `READ BINARY <path>`,
`READ RECORD <n> <path>`, `UPDATE BINARY <path>`, each with `-> SW=…`).
- A `proactive` action logs its rounds (`STATUS i/N -> …`, `FETCH=…`,
- A `proactive-drain` action logs its rounds (`STATUS i/N -> …`, `FETCH=…`,
`CMD 0x…`, `TR=… -> SW=…`); the run cleanup logs
`TEST-RUN drain: <CMD> (0xNN) TR=… -> SW=…`.
- A failing step adds `TEST-RUN step N failed: …` with the reason.
@@ -228,7 +228,7 @@ 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
`on_fail: "warning"`; to drain and confirm the card is idle, use the
`proactive` action.
`proactive-drain` action.
## Presets: where the keys, TARs and counters come from
@@ -309,7 +309,7 @@ from the form's source field; an explicit `src` overrides it. Both the
`src` overrides the device-identities source (`82` terminal, `83` network);
the default is the terminal, matching the Phone tab's forms.
### Proactive cleanup (`kind: "proactive"`)
### Proactive drain (`kind: "proactive-drain"`)
Consume whatever the card announces and leave it idle - at the start, in the
middle or at the end of a script (a leftover command from a previous run must
@@ -322,7 +322,7 @@ so a still-pending card fails the step and hands the pending command to the
next step.
```json
{"type": "action", "kind": "proactive",
{"type": "action", "kind": "proactive-drain",
"params": {"respond": {"result": "ok"},
"first": {"result": "cancel"},
"attempts": 5, "interval_ms": 200,
@@ -351,7 +351,7 @@ next step.
- `sw` - default `{"mode": "exact", "value": "9000"}` (a `status` action with
`attempts > 1` defaults to the `91??` mask).
- `data` - the response data (for SCP80: the inline PoR packet). The
`proactive` action does not take a `data` check - assert the drained
`proactive-drain` action does not take a `data` check - assert the drained
commands with `require` and the final status with `sw`.
- `por` (SCP80 actions only) - `"any"` (default, no check), `"ok"`
(`response_status == por_ok`), `"none"` (no PoR expected), or an **object**
@@ -447,7 +447,7 @@ UCS2 answer to a GET INPUT:
"respond": {"result": "ok", "text": "Пароль", "dcs": "08"}}
```
The `proactive` action's `respond`/`first` use the same shape (the command
The `proactive-drain` action's `respond`/`first` use the same shape (the command
type varies per drained command, so `text`/`item_id` apply where the command
carries them).
@@ -577,7 +577,7 @@ PoR-in-submit (with the transport asserted by the expectation):
the previous step; it fails at the step's `on_fail` level (the check row
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` action drains and confirms idle.
empty case, and the `proactive-drain` action drains and confirms idle.
- `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.
@@ -594,7 +594,7 @@ the user aborted the proactive session, which can push assertive applications
onto an error path and make them queue a further command. The cleanup is
reported like a normal step (`TR ok …`, `pending REFRESH (0x01) answered with
ok`); a script that wants to test the refusal path answers explicitly (an
expectation's `respond`, or the proactive action's `first`).
expectation's `respond`, or the `proactive-drain` action's `first`).
## Known limitations
+2 -2
View File
@@ -564,12 +564,12 @@
<p class="text-sm mb-2"><strong>Тест-скрипт</strong> &mdash; упорядоченный список <strong>действий</strong> и <strong>ожиданий</strong> проактивных команд, выполняемый на подключённой карте на стороне сервера. Пока запуск активен, карта принадлежит скрипту: остальные операции с картой отклоняются (HTTP&nbsp;409), фоновый опрос STATUS приостанавливается; отчёт о запуске хранится в памяти (в том числе после перезагрузки страницы) до нажатия <strong>Очистить отчёт</strong>. Скрипты хранятся в <strong>хранилище на сервере</strong> (<code class="font-mono text-sm">~/.pysim-simple-server/test_scripts.json</code>, <code class="font-mono text-sm">--test-scripts</code>), поэтому все браузеры работают с одним набором; изменения записываются в хранилище по мере правки (кнопки &laquo;Сохранить&raquo; нет &mdash; черновик без валидного шага остаётся локальным); экспорт/импорт JSON остаётся для резервных копий и переноса скриптов между стендами (старый экспорт из localStorage импортируется как есть).</p>
<p class="text-sm mb-2">У вкладки <strong>четыре представления</strong>: <strong>список наборов</strong> (по умолчанию; <em>Добавить набор</em> / <em>Редактировать</em> / <em>Запустить</em> / <em>Экспорт</em> / <em>Удалить</em>), <strong>редактор набора</strong> (скрипты набора в порядке выполнения &mdash; необязательный <em>setup</em> первым и <em>teardown</em> последним, с пометками ролей; у каждого скрипта <em>Запустить</em>/<em>Редактировать</em>/<em>Переместить</em>/<em>Скопировать</em>/<em>Удалить</em>, политика <em>при ошибке</em> <em>остановить</em> (по умолчанию) / <em>продолжить</em> и <em>Требуется ADM</em>), <strong>редактор скрипта</strong> (список шагов; кнопка <em>Старт</em> выполняет только этот скрипт и показывает результат под шагами &mdash; он не сохраняется) и <strong>отчёт набора</strong> (после запуска набора: статус каждого скрипта, таблица шагов с ожидаемым/фактическим, примечание об ошибке и выдержка из журнала запуска; сводка с успешно/предупреждения/ошибки/пропущено, время выполнения, счётчики SCP80 до&nbsp;&rarr;&nbsp;после по каждому keyset, пресет и предупреждение о смене сессии карты; экспорт в <em>JSON</em> или <em>Markdown</em>). <strong>Запуск набора</strong> выполняет его скрипты по порядку в <strong>одной сессии карты, без сбросов и без продолжения с места остановки</strong>; <em>при ошибке</em> решает, остановится ли набор на этом скрипте или запустится следующий, <strong>teardown выполняется всегда</strong> при остановке или ошибке (пока карта в ридере), чтобы следующий тест начался с чистого состояния, а сброс карты останавливает набор и отмечается в отчёте. <strong>Требуется ADM</strong> (у набора или скрипта) проверяет ключ ADM из подобранного пресета <strong>один раз</strong> перед запуском &mdash; ошибка отклоняет запуск (проверьте вручную через значок ADM; автоматических повторов нет). Скрипты хранятся в хранилище наборов (<code class="font-mono text-sm">~/.pysim-simple-server/test_suites.json</code>, <code class="font-mono text-sm">--test-suites</code>); каждый скрипт принадлежит ровно одному набору, и скрипт можно переместить или скопировать в другой набор (экспорт набора &mdash; это пакет с набором и его скриптами; старый экспорт только скриптов импортируется в автоматически созданный набор &laquo;Imported scripts&raquo;).</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Действия</strong> &mdash; <em>Envelope</em> (Event Download: сырой тип события + данные, с опциональным переопределением источника &mdash; 82 терминал / 83 сеть) или <em>Event</em> (семантические формы: <strong>та же форма, что и в диалоге отправки на вкладке «Телефон»</strong>, для всех смоделированных событий &mdash; четыре события, которые собирает сервер (Location status, Access technology change, Network rejection, Data connection status change), передаются полями, которые он проверяет и собирает во время запуска, остальные несут собранный формой hex данных события (показывается под формой, с проверкой одного ENVELOPE); источник device identities берётся из события (например, MT call &rarr; сеть) или из его поля источника), <em>Menu Selection</em> (item id 1&ndash;255 или текст пункта с точным/частичным совпадением, по умолчанию без учёта регистра &mdash; выбор <em>«Учёт регистра»</em> в редакторе шагов включает точное совпадение; текст подбирается по меню карты, которое следует за параметрами установки апплета), <em>UPDATE/READ файла</em> (путь + режим; чтение проверяет данные файла), <em>сырой APDU</em> (без авто-обработчика), <em>SCP80</em> (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2 и формат команды &mdash; компактный или обёрнутый в шаблон Command Scripting TS 102 226 &sect;5.2.1 <em>expanded</em> (AA) / <em>expanded AE&nbsp;80</em>; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты &mdash; запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик &mdash; а сервер сохраняет потреблённый счётчик в пресет по ходу запуска), <em>STATUS</em> (при <em>попытках</em> &gt; 1 опрашивает, пока карта не объявит команду) и <em>Очистка proactive</em> (опрос STATUS и поглощение всех объявленных команд &mdash; результат TERMINAL RESPONSE для каждой, опционально другой результат для первой; граница попыток/интервала; опциональная проверка <em>require</em> по поглощённому. Пустой опрос проходит, финальный STATUS должен быть idle, а оставшаяся команда передаётся следующему шагу &mdash; используйте в начале, середине или конце скрипта, чтобы убедиться, что карта перестала слать команды). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой <code class="font-mono text-sm">?</code> &mdash; <code class="font-mono text-sm">91??</code>) и проверка PoR для SCP80 (<em>любой / без PoR / por_ok</em> или своя проверка декодированного PoR &mdash; статус / SW / данные; транспорт через SEND SHORT MESSAGE работает так же).</li>
<li><strong>Действия</strong> &mdash; <em>Envelope</em> (Event Download: сырой тип события + данные, с опциональным переопределением источника &mdash; 82 терминал / 83 сеть) или <em>Event</em> (семантические формы: <strong>та же форма, что и в диалоге отправки на вкладке «Телефон»</strong>, для всех смоделированных событий &mdash; четыре события, которые собирает сервер (Location status, Access technology change, Network rejection, Data connection status change), передаются полями, которые он проверяет и собирает во время запуска, остальные несут собранный формой hex данных события (показывается под формой, с проверкой одного ENVELOPE); источник device identities берётся из события (например, MT call &rarr; сеть) или из его поля источника), <em>Menu Selection</em> (item id 1&ndash;255 или текст пункта с точным/частичным совпадением, по умолчанию без учёта регистра &mdash; выбор <em>«Учёт регистра»</em> в редакторе шагов включает точное совпадение; текст подбирается по меню карты, которое следует за параметрами установки апплета), <em>UPDATE/READ файла</em> (путь + режим; чтение проверяет данные файла), <em>сырой APDU</em> (без авто-обработчика), <em>SCP80</em> (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2 и формат команды &mdash; компактный или обёрнутый в шаблон Command Scripting TS 102 226 &sect;5.2.1 <em>expanded</em> (AA) / <em>expanded AE&nbsp;80</em>; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты &mdash; запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик &mdash; а сервер сохраняет потреблённый счётчик в пресет по ходу запуска), <em>STATUS</em> (при <em>попытках</em> &gt; 1 опрашивает, пока карта не объявит команду) и <em>Очистка очереди proactive</em> (опрос STATUS и поглощение всех объявленных команд &mdash; результат TERMINAL RESPONSE для каждой, опционально другой результат для первой; граница попыток/интервала; опциональная проверка <em>require</em> по поглощённому. Пустой опрос проходит, финальный STATUS должен быть idle, а оставшаяся команда передаётся следующему шагу &mdash; используйте в начале, середине или конце скрипта, чтобы убедиться, что карта перестала слать команды). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой <code class="font-mono text-sm">?</code> &mdash; <code class="font-mono text-sm">91??</code>) и проверка PoR для SCP80 (<em>любой / без PoR / por_ok</em> или своя проверка декодированного PoR &mdash; статус / SW / данные; транспорт через SEND SHORT MESSAGE работает так же).</li>
<li><strong>Ожидания</strong> &mdash; карта должна объявить проактивную команду на предыдущем шаге (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 &sect;7.4.2.1 / TS 102 223 &sect;6.3); <code class="font-mono text-sm">9000</code> означает &laquo;команды нет&raquo;: шаг падает на своём уровне ошибок &mdash; Ошибка останавливает скрипт, Предупреждение продолжает (терпимая форма &laquo;поглотить команду, если она есть&raquo;) &mdash; а действие STATUS с <em>попытками</em> &gt; 1 опрашивает, когда апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое: текст / пункт / raw, проверка <em>Alpha</em> (идентификатор Alpha команды, тег <code class="font-mono text-sm">05</code>/<code class="font-mono text-sm">85</code> &mdash; отличается от текстовой строки <code class="font-mono text-sm">8D</code>), проверка <em>SMS TPDU</em> (submit-TPDU команды SEND SHORT MESSAGE: получатель / PID / DCS / UDL / данные &mdash; для собственного SMS апплета), проверка <em>PoR</em> (SEND SHORT MESSAGE несёт PoR, когда пакет запросил PoR-in-submit, бит SPI2 <code class="font-mono text-sm">20</code>: статус ответа, SW R-APDU и данные ответа; перед этим должен выполниться шаг SCP80 &mdash; SW R-APDU находится <em>внутри</em> полезной нагрузки ответа: собственный TAR апплета возвращает данные приложения, поэтому проверяйте данные, а SW оставляйте пустым; отклонённый ENVELOPE (<code class="font-mono text-sm">62XX</code>/<code class="font-mono text-sm">63XX</code>) может вовсе не нести пакет ответа &mdash; для такого шага оставляйте проверку PoR «любой» или ожидайте «без PoR») и проверка <em>Files</em> (File List команды REFRESH, например квалификатор <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). <strong>TERMINAL RESPONSE</strong> (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.</li>
<li><strong>Уровни ошибок</strong> &mdash; несовпадение помечает шаг как <em>Ошибка</em> (скрипт останавливается) или <em>Предупреждение</em> (продолжает); уровень применяется к любой ошибке шага, включая ожидание без объявленной команды; каждая проверка показывается с ожидаемым и фактическим значением.</li>
<li><strong>Шаблоны</strong> &mdash; Empty, <em>Обзор меню STK</em>, <em>Апплет через SCP80</em>; редактор шагов — формный (действие Event показывает форму события из симулятора), а набор экспортируется/импортируется пакетом JSON (набор и его скрипты).</li>
</ul>
<p class="text-sm mb-3">Каждый шаг пишет семантическую строку в журнал сервера под своим номером (<code class="font-mono text-sm">TEST-RUN step N: …</code>), запрос перед ответом: отправленный APDU/данные и полученный SW, для SCP80 — открытый C-APDU, собранный защищённый пакет и R-APDU из PoR, для ожидания — тройка FETCH / команда / TERMINAL RESPONSE, очистка proactive — каждый раунд STATUS/FETCH/команда/TR, а очистка в конце запуска — поглощённые команды (отвечаются <code class="font-mono text-sm">ok</code> &mdash; cancel отправляется только по явному запросу скрипта), а для выбора пункта меню — полный ENVELOPE с определённым пунктом. Запустите сервер с <code class="font-mono text-sm">--apdu-trace</code>, чтобы добавить и журнал сырых транспортных APDU.</p>
<p class="text-sm mb-3">Каждый шаг пишет семантическую строку в журнал сервера под своим номером (<code class="font-mono text-sm">TEST-RUN step N: …</code>), запрос перед ответом: отправленный APDU/данные и полученный SW, для SCP80 — открытый C-APDU, собранный защищённый пакет и R-APDU из PoR, для ожидания — тройка FETCH / команда / TERMINAL RESPONSE, очистка очереди proactive — каждый раунд STATUS/FETCH/команда/TR, а очистка в конце запуска — поглощённые команды (отвечаются <code class="font-mono text-sm">ok</code> &mdash; cancel отправляется только по явному запросу скрипта), а для выбора пункта меню — полный ENVELOPE с определённым пунктом. Запустите сервер с <code class="font-mono text-sm">--apdu-trace</code>, чтобы добавить и журнал сырых транспортных APDU.</p>
<h3 id="bip" class="text-lg font-medium mb-2">8.10 BIP-терминал (обычный TCP)</h3>
<p class="text-sm mb-2">Пилюля <strong>BIP</strong> запускает терминальную сторону Bearer Independent Protocol для карт, использующих TCP без HTTP OTA (SCP81): апплет или пуш-триггер может открыть канал и обменяться сырыми данными. Одновременно работает только одна BIP-сессия &mdash; запуск здесь останавливает слушатель SCP81, а запуск слушателя SCP81 останавливает эту сессию (в обоих случаях сообщается, что было заменено).</p>
+2 -2
View File
@@ -563,12 +563,12 @@
<p class="text-sm mb-2">A <strong>test script</strong> is an ordered list of <strong>actions</strong> and proactive-command <strong>expectations</strong> executed server-side on the equipped card. While a run is active the card belongs to the script: other card actions are refused (HTTP&nbsp;409) and background STATUS polling is paused; the run report is kept in memory (also after reloading the page) until <strong>Clear report</strong>. Scripts live in the <strong>server-side store</strong> (<code class="font-mono text-sm">~/.pysim-simple-server/test_scripts.json</code>, <code class="font-mono text-sm">--test-scripts</code>), so every browser manages the same set; edits are written to the store as you change them (there is no Save button &mdash; a draft without a valid step stays local until it is valid); JSON export/import remains for backup and for moving scripts between benches (the old localStorage export imports as-is).</p>
<p class="text-sm mb-2">The pill has <strong>four views</strong>: the <strong>suite list</strong> (default; <em>Add suite</em> / <em>Edit</em> / <em>Run</em> / <em>Export</em> / <em>Delete</em>), the <strong>suite editor</strong> (the suite's scripts in run order &mdash; the optional <em>setup</em> first and the <em>teardown</em> last, marked with role badges; per script <em>Run</em>/<em>Edit</em>/<em>Move</em>/<em>Copy</em>/<em>Delete</em>, the per-member <em>on failure</em> policy <em>stop</em> (default) / <em>continue</em>, and <em>ADM required</em>), the <strong>script editor</strong> (the step list; its <em>Start</em> runs that single script and shows the result under the steps &mdash; not preserved) and the <strong>suite report</strong> (after a suite run: every script's status, its step table with expected/actual, the failing note and a run-log excerpt; the summary with passed/warnings/failed/skipped, wall time, the SCP80 counters before&nbsp;&rarr;&nbsp;after per keyset, the preset and a card-session-changed warning; export as <em>JSON</em> or <em>Markdown</em>). A <strong>suite run</strong> executes its scripts in order in <strong>one card session, without resets and without resume</strong>; <em>on failure</em> decides whether the suite stops at that script or runs the next one, the <strong>teardown always runs</strong> on a stop or a failure (while the card is in the reader) so the next test starts clean, and a card reset stops the suite and is flagged in the report. <strong>ADM required</strong> (on the suite or a script) verifies the matched preset's ADM key <strong>once</strong> before the start &mdash; a failure refuses the run (verify manually with the ADM badge; nothing is retried automatically). Scripts live in the suite's store (<code class="font-mono text-sm">~/.pysim-simple-server/test_suites.json</code>, <code class="font-mono text-sm">--test-suites</code>); every script belongs to exactly one suite, and a script can be moved or copied to another suite (a bundle export carries the suite plus its scripts; the old script-only export imports into an auto-created &ldquo;Imported scripts&rdquo; suite).</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Actions</strong> &mdash; <em>Envelope</em> (Event Download: raw event type + data, with an optional source override &mdash; 82 terminal / 83 network) or <em>Event</em> (the semantic forms: the <strong>same form the Phone tab's send dialog uses</strong>, for every modelled event &mdash; 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 &rarr; network) or its source field), <em>Menu Selection</em> (item id 1&ndash;255, or the item's text with an exact/contains match, case-insensitive by default &mdash; the step editor's <em>Match case</em> choice switches to an exact-case match; the text is resolved against the card's menu, which follows the applet's install parameters), <em>UPDATE/READ file</em> (path + mode; reads verify the file data), <em>raw APDU</em> (no auto-handler), <em>SCP80</em> (C-APDU or a pre-built secured packet, optional keyset number (kvn) / TAR / SPI1 / SPI2 overrides and a command format &mdash; compact, or the TS 102 226 &sect;5.2.1 Command Scripting template <em>expanded</em> (AA) / <em>expanded AE&nbsp;80</em> around the C-APDU; the keys and the counter come from the card preset matched by the equipped card's ICCID &mdash; the run names the stored preset, so it always uses its keysets, TARs and counter &mdash; the server persists the consumed counter into it as the run proceeds), <em>STATUS</em> (with <em>attempts</em> &gt; 1 it polls until the card announces a command) and <em>Proactive cleanup</em> (poll STATUS and consume every announced command &mdash; the TERMINAL RESPONSE result for each, optionally a different result for the first; the attempt/interval bound; an optional <em>require</em> 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 &mdash; use it at the start, in the middle or at the end of a script to make sure the card has stopped sending). Every action has a response check: expected SW and data (exact, or a mask with <code class="font-mono text-sm">?</code> nibble wildcards &mdash; <code class="font-mono text-sm">91??</code>) plus a PoR check for SCP80 (<em>any / no PoR / por_ok</em>, or a custom assertion of the decoded PoR &mdash; status / SW / data; the SEND SHORT MESSAGE transport works the same).</li>
<li><strong>Actions</strong> &mdash; <em>Envelope</em> (Event Download: raw event type + data, with an optional source override &mdash; 82 terminal / 83 network) or <em>Event</em> (the semantic forms: the <strong>same form the Phone tab's send dialog uses</strong>, for every modelled event &mdash; 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 &rarr; network) or its source field), <em>Menu Selection</em> (item id 1&ndash;255, or the item's text with an exact/contains match, case-insensitive by default &mdash; the step editor's <em>Match case</em> choice switches to an exact-case match; the text is resolved against the card's menu, which follows the applet's install parameters), <em>UPDATE/READ file</em> (path + mode; reads verify the file data), <em>raw APDU</em> (no auto-handler), <em>SCP80</em> (C-APDU or a pre-built secured packet, optional keyset number (kvn) / TAR / SPI1 / SPI2 overrides and a command format &mdash; compact, or the TS 102 226 &sect;5.2.1 Command Scripting template <em>expanded</em> (AA) / <em>expanded AE&nbsp;80</em> around the C-APDU; the keys and the counter come from the card preset matched by the equipped card's ICCID &mdash; the run names the stored preset, so it always uses its keysets, TARs and counter &mdash; the server persists the consumed counter into it as the run proceeds), <em>STATUS</em> (with <em>attempts</em> &gt; 1 it polls until the card announces a command) and <em>Proactive drain</em> (poll STATUS and consume every announced command &mdash; the TERMINAL RESPONSE result for each, optionally a different result for the first; the attempt/interval bound; an optional <em>require</em> 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 &mdash; use it at the start, in the middle or at the end of a script to make sure the card has stopped sending). Every action has a response check: expected SW and data (exact, or a mask with <code class="font-mono text-sm">?</code> nibble wildcards &mdash; <code class="font-mono text-sm">91??</code>) plus a PoR check for SCP80 (<em>any / no PoR / por_ok</em>, or a custom assertion of the decoded PoR &mdash; status / SW / data; the SEND SHORT MESSAGE transport works the same).</li>
<li><strong>Expectations</strong> &mdash; the card must have announced a proactive command in the previous step (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 &sect;7.4.2.1 / TS 102 223 &sect;6.3); a <code class="font-mono text-sm">9000</code> means &ldquo;no command&rdquo;: the step fails at its fail level &mdash; Error stops the script, Warning continues (the tolerant &ldquo;consume a command if there is one&rdquo; form) &mdash; and a STATUS action with <em>attempts</em> &gt; 1 polls when the applet delivers on poll. The command type, qualifier and contents are verified: text / item / raw checks, an <em>Alpha</em> check (the command's Alpha identifier, tag <code class="font-mono text-sm">05</code>/<code class="font-mono text-sm">85</code> &mdash; distinct from the <code class="font-mono text-sm">8D</code> text string), an <em>SMS TPDU</em> check (the submit TPDU of a SEND SHORT MESSAGE: destination / PID / DCS / UDL / user data &mdash; for an applet's own SMS), a <em>PoR</em> check (a SEND SHORT MESSAGE carries the PoR when the packet requested PoR-in-submit, SPI2 bit <code class="font-mono text-sm">20</code>: the response status, the R-APDU SW and the response data; make sure an SCP80 step ran first &mdash; the R-APDU SW lives <em>inside</em> the response payload: an applet's own TAR returns application data, so check its data and leave the SW empty; a refused ENVELOPE (<code class="font-mono text-sm">62XX</code>/<code class="font-mono text-sm">63XX</code>) may carry no response packet at all &mdash; leave the PoR check at <em>any</em> or expect <em>no PoR</em> for such a step) and a <em>Files</em> check (the File List of a REFRESH, e.g. qualifier <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). The scripted <strong>TERMINAL RESPONSE</strong> (result, item identifier, text, extra TLVs) is part of the instruction.</li>
<li><strong>Fail levels</strong> &mdash; a mismatch flags the step as <em>Error</em> (terminates the script) or <em>Warning</em> (continues); every step failure honours it, including an expectation that finds no pending command; each check is reported with expected vs actual values.</li>
<li><strong>Templates</strong> &mdash; Empty, <em>STK menu browsing</em>, <em>Applet via SCP80</em>; 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).</li>
</ul>
<p class="text-sm mb-3">Every step logs a semantic line to the server log under its step number (<code class="font-mono text-sm">TEST-RUN step N: …</code>), 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 cleanup every STATUS/FETCH/command/TR round and the run cleanup the commands it drained (answered <code class="font-mono text-sm">ok</code> &mdash; 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 <code class="font-mono text-sm">--apdu-trace</code> to add the raw transport APDU log as well.</p>
<p class="text-sm mb-3">Every step logs a semantic line to the server log under its step number (<code class="font-mono text-sm">TEST-RUN step N: …</code>), 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 <code class="font-mono text-sm">ok</code> &mdash; 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 <code class="font-mono text-sm">--apdu-trace</code> to add the raw transport APDU log as well.</p>
<h3 id="bip" class="text-lg font-medium mb-2">8.10 BIP terminal (generic TCP)</h3>
<p class="text-sm mb-2">The <strong>BIP</strong> pill runs the terminal side of the Bearer Independent Protocol for cards that use TCP without HTTP OTA (SCP81): an applet or a push trigger can open a channel and exchange raw data. Only one BIP session runs at a time &mdash; starting here stops a running SCP81 listener, and starting the SCP81 listener stops this session (both report what was replaced).</p>
+8 -8
View File
@@ -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.0';
const SIMPLE_VERSION = '3.22.1';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -6941,7 +6941,7 @@ function decodeResponse() {
// through /api/test/status. This UI edits the script and monitors the run;
// SCP80 steps use the card preset matched by the equipped card's ICCID, and
// the final SCP80 counter is written back to it.
const TEST_ACTION_KINDS = ['envelope', 'event', 'menu-select', 'file-write', 'file-read', 'apdu', 'scp80', 'status', 'proactive'];
const TEST_ACTION_KINDS = ['envelope', 'event', 'menu-select', 'file-write', 'file-read', 'apdu', 'scp80', 'status', 'proactive-drain'];
let _testScripts = null; // the script store cache (all suites)
let _testSuites = null; // the suite store cache
let _testView = 'suites'; // suites | suite | script | report
@@ -7128,7 +7128,7 @@ function testScriptProblem(script) {
if (data && !/^[0-9A-Fa-f]+$/.test(data)) return at + t('event data must be hex');
}
if ((s.kind === 'file-write' || s.kind === 'file-read') && !String(p.path || '').trim()) return at + t('file path is empty');
if (s.kind === 'proactive' && String(p.attempts === undefined ? '' : p.attempts).trim() !== ''
if (s.kind === 'proactive-drain' && String(p.attempts === undefined ? '' : p.attempts).trim() !== ''
&& !(parseInt(p.attempts, 10) >= 1)) return at + t('attempts must be at least 1');
} else if (s.type === 'expect') {
if (!String(s.command || '').trim()) return at + t('command is required');
@@ -7207,8 +7207,8 @@ function testStepSummary(step) {
else if (step.kind === 'file-write') s = 'UPDATE ' + (p.path || '?') + (p.mode && p.mode !== 'auto' ? ' (' + p.mode + (p.record ? ' ' + p.record : '') + ')' : '') + ' = ' + (p.data || '');
else if (step.kind === 'file-read') s = 'READ ' + (p.path || '?') + (p.mode && p.mode !== 'auto' ? ' (' + p.mode + (p.record ? ' ' + p.record : '') + ')' : '');
else if (step.kind === 'status') s = 'STATUS' + ((p.attempts || 1) > 1 ? ' x' + p.attempts + ' (poll)' : '');
else if (step.kind === 'proactive') {
const parts = ['PROACTIVE x' + (p.attempts === undefined ? 3 : p.attempts)];
else if (step.kind === 'proactive-drain') {
const parts = ['DRAIN x' + (p.attempts === undefined ? 3 : p.attempts)];
const rq = p.require || {};
if (rq.command) parts.push('require ' + rq.command + (rq.qualifier ? ' q=' + (rq.qualifier.value || rq.qualifier) : ''));
const rr = p.respond || {};
@@ -7882,7 +7882,7 @@ function testStepRender() {
} else if (step.kind === 'status') {
html += testFormRow('Attempts (1 = single STATUS)', testFormInput('test-f-attempts', p.attempts === undefined ? 1 : p.attempts, '1..1000'));
html += testFormRow('Interval (ms, when polling)', testFormInput('test-f-interval', p.interval_ms === undefined ? 200 : p.interval_ms, '0..10000'));
} else if (step.kind === 'proactive') {
} else if (step.kind === 'proactive-drain') {
const pr = p.respond || {};
html += testFormRow('TERMINAL RESPONSE result (each command)',
testFormSelect('test-f-presult', pr.result === undefined ? 'ok' : String(pr.result), [
@@ -8217,7 +8217,7 @@ function testStepCollect() {
if (fmt && fmt !== 'compact') p.format = fmt;
}
else if (kind === 'status') { p.attempts = num('test-f-attempts', 1); p.interval_ms = num('test-f-interval', 200); }
else if (kind === 'proactive') {
else if (kind === 'proactive-drain') {
p.respond = {result: val('test-f-presult') || 'ok'};
const first = val('test-f-pfirst');
if (first) p.first = {result: first};
@@ -8314,7 +8314,7 @@ function testStepFormError() {
}
if ((step.kind === 'file-write' || step.kind === 'file-read') && !p.path) return t('file path is empty');
if (step.kind === 'status' && !(p.attempts >= 1)) return t('attempts must be at least 1');
if (step.kind === 'proactive' && !(p.attempts >= 1)) return t('attempts must be at least 1');
if (step.kind === 'proactive-drain' && !(p.attempts >= 1)) return t('attempts must be at least 1');
} else if (!step.command) {
return t('command is required');
}
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v403';
const CACHE = 'simple-v404';
const URLS = [
'index.html',
'help.html',
+10 -10
View File
@@ -130,9 +130,9 @@ test('testScriptProblem accepts good scripts and names bad ones', () => {
assert.match(testScriptProblem({
steps: [{ type: 'action', kind: 'file-read', params: {} }] }), /file path/);
assert.match(testScriptProblem({ steps: [{ type: 'expect' }] }), /command is required/);
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'proactive',
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'proactive-drain',
params: { attempts: 0 } }] }), /attempts/);
assert.strictEqual(testScriptProblem({ steps: [{ type: 'action', kind: 'proactive',
assert.strictEqual(testScriptProblem({ steps: [{ type: 'action', kind: 'proactive-drain',
params: {} }] }), '');
// the selected SCP80 source decides which value is required
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'scp80',
@@ -143,11 +143,11 @@ test('testScriptProblem accepts good scripts and names bad ones', () => {
params: { source: 'sp', sp: 'AA', apdu: '80E2' } }] }), '');
});
test('the proactive cleanup step collects its fields', () => {
_testEditStep = { type: 'action', kind: 'proactive', params: {} };
test('the proactive-drain step collects its fields', () => {
_testEditStep = { type: 'action', kind: 'proactive-drain', params: {} };
_testEditChecks = [];
_testEditStepIndex = 0;
fakeForm({ 'test-step-kind': 'proactive', 'test-f-presult': 'ok', 'test-f-pfirst': 'cancel',
fakeForm({ 'test-step-kind': 'proactive-drain', 'test-f-presult': 'ok', 'test-f-pfirst': 'cancel',
'test-f-pattempts': '5', 'test-f-pinterval': '0', 'test-f-prequire': 'refresh',
'test-f-prequal': '00', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-fail': 'error' });
testStepCollect();
@@ -158,7 +158,7 @@ test('the proactive cleanup step collects its fields', () => {
assert.strictEqual(p.interval_ms, 0);
assert.deepStrictEqual(p.require, { command: 'REFRESH', qualifier: '00' });
// an empty require field drops the assertion
fakeForm({ 'test-step-kind': 'proactive', 'test-f-presult': 'ok', 'test-f-pfirst': '',
fakeForm({ 'test-step-kind': 'proactive-drain', 'test-f-presult': 'ok', 'test-f-pfirst': '',
'test-f-pattempts': '3', 'test-f-pinterval': '200', 'test-f-prequire': '',
'test-f-prequal': '', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-fail': 'error' });
testStepCollect();
@@ -186,14 +186,14 @@ test('testStepSummary renders actions', () => {
testStepSummary({ type: 'action', kind: 'status', params: { attempts: 5 } }),
'STATUS x5 (poll)');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'proactive',
testStepSummary({ type: 'action', kind: 'proactive-drain',
params: { attempts: 5, respond: { result: 'ok' },
require: { command: 'REFRESH', qualifier: '00' } } }),
'PROACTIVE x5 require REFRESH q=00 TR ok');
'DRAIN x5 require REFRESH q=00 TR ok');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'proactive',
testStepSummary({ type: 'action', kind: 'proactive-drain',
params: { attempts: 3, first: { result: 'cancel' } } }),
'PROACTIVE x3 TR ok first cancel');
'DRAIN x3 TR ok first cancel');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'file-read', params: { path: 'MF/2FE2' } }),
'READ MF/2FE2');
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.22.0"
version = "3.22.1"
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.
+13 -13
View File
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.22.0'
VERSION = '3.22.1'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -5853,7 +5853,7 @@ _TEST_KIND_LABELS = {
'menu-select': 'ENVELOPE(Menu Selection)',
'file-write': 'UPDATE FILE', 'file-read': 'READ FILE', 'apdu': 'APDU',
'scp80': 'SCP80', 'status': 'STATUS', 'cleanup': 'CLEANUP',
'proactive': 'PROACTIVE',
'proactive-drain': 'DRAIN',
}
# Card-touching endpoints refused while a script owns the card.
_TEST_BLOCKED_PATHS = frozenset([
@@ -6300,8 +6300,8 @@ def _test_run_status(scc, step):
return data or '', sw, ('STATUS x%d' % used if used > 1 else 'STATUS')
def _proactive_require_match(req, consumed):
"""Whether one consumed command satisfies a proactive action's `require`
def _drain_require_match(req, consumed):
"""Whether one consumed command satisfies a proactive-drain action's `require`
spec: the command type/name (ANY matches all) and an optional qualifier
(exact/mask)."""
if req.get('type') is not None and consumed.get('type_hex') != '%02X' % req['type']:
@@ -6312,7 +6312,7 @@ def _proactive_require_match(req, consumed):
return True
def _proactive_require_label(req):
def _drain_require_label(req):
"""The `require` spec as the report's expected string."""
if req.get('type') is not None:
name = req.get('name') or '0x%02X' % req['type']
@@ -6322,8 +6322,8 @@ def _proactive_require_label(req):
return name + ((' q=%s' % q['value']) if q else '')
def _test_run_proactive(server, step):
"""The proactive cleanup action: poll STATUS and consume every announced
def _test_run_drain(server, step):
"""The proactive drain action: poll STATUS and consume every announced
command - the TERMINAL RESPONSE is `first` for the first one, `respond`
for the rest - until the card answers 9000 (idle), a non-91XX SW appears
or the attempt budget is spent. The final SW is the step's SW check; an
@@ -6376,7 +6376,7 @@ def _test_run_proactive(server, step):
step['on_fail'])]
req = p.get('require')
if req:
matched = any(_proactive_require_match(req, c) for c in consumed)
matched = any(_drain_require_match(req, c) for c in consumed)
if consumed:
actual = ', '.join(
'%s%s' % (c['type_name'],
@@ -6385,7 +6385,7 @@ def _test_run_proactive(server, step):
else:
actual = '(nothing was pending)'
checks.append(_test_check_result('Require', matched,
_proactive_require_label(req), actual,
_drain_require_label(req), actual,
step['on_fail']))
status = testscript.combine_levels([c['level'] if not c['ok'] else 'ok'
for c in checks])
@@ -6646,8 +6646,8 @@ def _test_run_action(server, step, ctx):
(' RESP=%dB' % (len(data) // 2)) if data else ''))
elif kind == 'scp80':
data, sw, sent, por, counter = _test_run_scp80(server, step, ctx)
elif kind == 'proactive':
return _test_run_proactive(server, step)
elif kind == 'proactive-drain':
return _test_run_drain(server, step)
else:
raise testscript.ScriptError('unknown action kind %r' % kind)
checks = _test_action_checks(step, sw, data, por, kind)
@@ -6759,7 +6759,7 @@ def _test_run_expect(server, step, pending, ctx=None):
if not pending:
# Nothing to fetch: the step fails at its own on_fail level. The
# tolerant "consume a pending command if there is one" form is an
# expectation with on_fail "warning" (or the proactive cleanup action,
# expectation with on_fail "warning" (or the proactive-drain action,
# which tolerates an empty card by design). The UICC announces
# pending commands in the response to a command (TS 102 221 7.4.2.1).
want = step['command'].get('name')
@@ -6936,7 +6936,7 @@ def _test_drain_pending(scc, fetch_len, result=0x00, max_commands=3):
make the application emit another, so the drain keeps fetching while the
TERMINAL RESPONSE answers 91XX (bounded by `max_commands`). A script that
wants to test the refusal path answers explicitly (an expectation's
`respond`, or the proactive action's `first`)."""
`respond`, or the `proactive-drain` action's `first`)."""
answered = []
try:
while len(answered) < max_commands:
+12 -12
View File
@@ -33,7 +33,7 @@ __all__ = [
]
ACTION_KINDS = ('envelope', 'event', 'menu-select', 'file-write', 'file-read',
'apdu', 'scp80', 'status', 'proactive')
'apdu', 'scp80', 'status', 'proactive-drain')
FAIL_LEVELS = ('error', 'warning')
POR_CHECKS = ('none', 'ok', 'any')
@@ -357,28 +357,28 @@ def _normalise_params(kind, p, command_resolver=None):
interval = p.get('interval_ms')
interval = _int(200 if interval is None else interval, 'status interval_ms', 0, 10000)
return {'attempts': attempts, 'interval_ms': interval}
if kind == 'proactive':
# The proactive cleanup: consume whatever the card announces (the
if kind == 'proactive-drain':
# The proactive drain: consume whatever the card announces (the
# TERMINAL RESPONSE is `first` for the first command, `respond` for
# the rest) and confirm the card is idle; `require` asserts what was
# drained (at least one match - an empty drain fails it).
out = {'respond': normalise_respond(p.get('respond') or {}),
'attempts': _int(3 if p.get('attempts') is None else p.get('attempts'),
'proactive attempts', 1, 1000),
'proactive-drain attempts', 1, 1000),
'interval_ms': _int(200 if p.get('interval_ms') is None
else p.get('interval_ms'),
'proactive interval_ms', 0, 10000)}
'proactive-drain interval_ms', 0, 10000)}
if p.get('first') is not None:
out['first'] = normalise_respond(p['first'])
req = p.get('require')
if req is not None:
if not isinstance(req, dict):
raise ScriptError('proactive require must be an object')
ctype, cname = _resolve_command(req.get('command'), 'proactive require',
raise ScriptError('proactive-drain require must be an object')
ctype, cname = _resolve_command(req.get('command'), 'proactive-drain require',
command_resolver)
qualifier = _check_spec(req.get('qualifier'), 'proactive require.qualifier')
qualifier = _check_spec(req.get('qualifier'), 'proactive-drain require.qualifier')
if qualifier and '?' not in qualifier['value'] and len(qualifier['value']) != 2:
raise ScriptError('proactive require.qualifier must be one byte')
raise ScriptError('proactive-drain require.qualifier must be one byte')
out['require'] = {'type': ctype, 'name': cname, 'qualifier': qualifier}
return out
raise ScriptError('unknown action kind %r' % kind)
@@ -398,11 +398,11 @@ def _normalise_check(check, kind, params):
else:
sw = {'mode': 'exact', 'value': '9000'}
data = _check_spec(check.get('data'), 'check.data')
if kind == 'proactive' and data is not None:
# The proactive cleanup's result data is the drained command list, not
if kind == 'proactive-drain' and data is not None:
# The proactive drain's result data is the drained command list, not
# a single response: a `data` check would be silently ignored, so it
# is refused here (assert the drained commands with `require`).
raise ScriptError('check.data is not used by the proactive action - '
raise ScriptError('check.data is not used by the proactive drain action - '
'assert what was drained with require, or the final '
'status word with check.sw')
por = check.get('por')
+17 -17
View File
@@ -86,9 +86,9 @@ class TestValidation(unittest.TestCase):
self.assertEqual(check['sw'], {'mode': 'mask', 'value': '91??'})
self.assertEqual(check['data'], {'mode': 'mask', 'value': 'AA??'})
def test_proactive_action_normalises(self):
def test_proactive_drain_action_normalises(self):
script = T.normalise_script({'steps': [
{'type': 'action', 'kind': 'proactive',
{'type': 'action', 'kind': 'proactive-drain',
'params': {'respond': {'result': 'ok'}, 'first': {'result': 'cancel'},
'attempts': 5, 'interval_ms': 0,
'require': {'command': 'REFRESH', 'qualifier': '00'}}},
@@ -103,7 +103,7 @@ class TestValidation(unittest.TestCase):
self.assertEqual(p['require']['qualifier'], {'mode': 'exact', 'value': '00'})
# the defaults: ok / 3 attempts / 200 ms, no first, no require
script = T.normalise_script({'steps': [
{'type': 'action', 'kind': 'proactive'}]}, _resolver)
{'type': 'action', 'kind': 'proactive-drain'}]}, _resolver)
p = script['steps'][0]['params']
self.assertEqual(p['respond'], {'result': 0x00})
self.assertNotIn('first', p)
@@ -116,18 +116,18 @@ class TestValidation(unittest.TestCase):
{'require': {'command': 'NOT A COMMAND'}},
{'attempts': 0}):
with self.assertRaises(T.ScriptError, msg=repr(bad)):
T.normalise_script({'steps': [{'type': 'action', 'kind': 'proactive',
T.normalise_script({'steps': [{'type': 'action', 'kind': 'proactive-drain',
'params': bad}]}, _resolver)
def test_the_proactive_action_refuses_a_data_check(self):
# The proactive result's data is the drained-command list, not a
def test_the_proactive_drain_action_refuses_a_data_check(self):
# The proactive drain's data is the drained-command list, not a
# single response: a `data` check would be silently ignored, so the
# engine refuses it (assert what was drained with `require`).
with self.assertRaises(T.ScriptError) as cm:
T.normalise_script({'steps': [
{'type': 'action', 'kind': 'proactive',
{'type': 'action', 'kind': 'proactive-drain',
'check': {'data': 'AABB'}}]}, _resolver)
self.assertIn('check.data is not used by the proactive action',
self.assertIn('check.data is not used by the proactive drain action',
str(cm.exception))
def test_integer_fields_accept_decimals_with_leading_zeros_and_hex(self):
@@ -644,8 +644,8 @@ class TestRunnerDialogue(RunnerTestCase):
self.assertEqual(run['steps'], [])
class TestRunnerProactive(RunnerTestCase):
"""The proactive cleanup action (v3.21.0): consume whatever the card
class TestRunnerProactiveDrain(RunnerTestCase):
"""The proactive drain action (v3.21.0): consume whatever the card
announces, answer it and confirm the card is idle."""
def test_empty_drain_passes(self):
@@ -653,7 +653,7 @@ class TestRunnerProactive(RunnerTestCase):
scc = FakeScc()
scc.push('80F2', '', '9000')
run = self.run_script(FakeServer(scc), [
{'type': 'action', 'kind': 'proactive', 'params': {}},
{'type': 'action', 'kind': 'proactive-drain', 'params': {}},
])
self.assertEqual(run['status'], 'ok', run['steps'])
entry = run['steps'][0]
@@ -670,7 +670,7 @@ class TestRunnerProactive(RunnerTestCase):
scc.push('8014', '', '9000') # TR ok
scc.push('80F2', '', '9000') # confirming STATUS: idle
run = self.run_script(FakeServer(scc), [
{'type': 'action', 'kind': 'proactive', 'params': {'attempts': 3, 'interval_ms': 0}},
{'type': 'action', 'kind': 'proactive-drain', 'params': {'attempts': 3, 'interval_ms': 0}},
])
self.assertEqual(run['status'], 'ok', run['steps'])
entry = run['steps'][0]
@@ -687,7 +687,7 @@ class TestRunnerProactive(RunnerTestCase):
scc.push('8014', '', '9000')
scc.push('80F2', '', '9000')
run = self.run_script(FakeServer(scc), [
{'type': 'action', 'kind': 'proactive',
{'type': 'action', 'kind': 'proactive-drain',
'params': {'attempts': 3, 'interval_ms': 0, 'require': {'command': 'DISPLAY TEXT'}}},
])
self.assertEqual(run['status'], 'ok', run['steps'])
@@ -701,7 +701,7 @@ class TestRunnerProactive(RunnerTestCase):
scc.push('8014', '', '9000')
scc.push('80F2', '', '9000')
run = self.run_script(FakeServer(scc), [
{'type': 'action', 'kind': 'proactive',
{'type': 'action', 'kind': 'proactive-drain',
'params': {'attempts': 3, 'interval_ms': 0,
'require': {'command': 'REFRESH', 'qualifier': '00'}}},
])
@@ -714,7 +714,7 @@ class TestRunnerProactive(RunnerTestCase):
scc = FakeScc()
scc.push('80F2', '', '9000')
run = self.run_script(FakeServer(scc), [
{'type': 'action', 'kind': 'proactive',
{'type': 'action', 'kind': 'proactive-drain',
'params': {'require': {'command': 'REFRESH'}}},
])
self.assertEqual(run['status'], 'error')
@@ -731,7 +731,7 @@ class TestRunnerProactive(RunnerTestCase):
scc.push('8014', '', '9000') # TR ok
scc.push('80F2', '', '9000') # confirming STATUS
run = self.run_script(FakeServer(scc), [
{'type': 'action', 'kind': 'proactive',
{'type': 'action', 'kind': 'proactive-drain',
'params': {'attempts': 4, 'interval_ms': 0, 'respond': {'result': 'ok'},
'first': {'result': 'cancel'}}},
])
@@ -746,7 +746,7 @@ class TestRunnerProactive(RunnerTestCase):
scc.push('8012', DISPLAY_TEXT_CMD, '9000')
scc.push('8014', '', '9102') # another command pending
run = self.run_script(FakeServer(scc), [
{'type': 'action', 'kind': 'proactive', 'params': {'attempts': 1}},
{'type': 'action', 'kind': 'proactive-drain', 'params': {'attempts': 1}},
])
# the bound stopped with 91XX: the SW check fails and the pending is
# handed to the next step