v3.21.0: the test-runner tolerance batch + the proactive cleanup action

Five reported issues, plus the action that makes an explicit drain possible:

- on_fail is honoured by every step failure: an expectation with no pending
  command (or a FETCH that returns nothing) fails as a Command/Fetch check at
  the step's level instead of raising past it; the default still stops the
  run, on_fail "warning" continues (the tolerant consume-if-pending form).
- the run cleanup answers ok (0x00) and keeps draining while the TR answers
  91XX (bounded); a cancel is only sent when a script asks for it - it tells
  the card the user aborted the session and pushes assertive applets onto an
  error path.
- new `proactive` action: STATUS rounds consume every announced command
  (respond/first TERMINAL RESPONSE results, attempts/interval_ms bound) until
  the card answers 9000; an optional require asserts at least one drained
  command (an empty drain fails it), the final SW is the step's SW check and
  a still-pending command is handed to the next step.  PWA editor + summary +
  validation, EN/RU help, docs, action-kind drift test.
- the PoR response form follows the command's TAR: _tar_is_rm (preset role
  entries + the TS 101 220 Annex D ISD/RFM allocations) decides; a non-RM
  TAR's reply is always raw (TS 102 226 4) - no fabricated status word even
  when the bytes happen to parse (01 00 0B 91 ... used to read as SW 000B).
- a secured data too short for the compact structure (a 2-byte applet reply)
  is recovered by _por_header_parse (the decode minus the compact parse; a
  malformed packet or a failed RC/CC/DS check still yields no por) and
  reported raw, so short applet replies are assertable via por.data.

Docs: test-scripts.md, api.md, help EN/RU, repo AGENTS.  sw cache simple-v396.
Tests: 825 frontend + 755 Python green.
This commit is contained in:
2026-10-05 23:36:47 +03:00
parent 82c5733906
commit e6ced47323
12 changed files with 788 additions and 100 deletions
+36
View File
@@ -107,6 +107,10 @@ 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',
params: { attempts: 0 } }] }), /attempts/);
assert.strictEqual(testScriptProblem({ steps: [{ type: 'action', kind: 'proactive',
params: {} }] }), '');
// the selected SCP80 source decides which value is required
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'scp80',
params: { source: 'apdu', sp: 'AA' } }] }), /SCP80/);
@@ -116,6 +120,29 @@ 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: {} };
_testEditChecks = [];
_testEditStepIndex = 0;
fakeForm({ 'test-step-kind': 'proactive', '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();
const p = _testEditStep.params;
assert.strictEqual(p.respond.result, 'ok');
assert.strictEqual(p.first.result, 'cancel');
assert.strictEqual(p.attempts, 5);
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': '',
'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();
assert.ok(!('first' in _testEditStep.params));
assert.ok(!('require' in _testEditStep.params));
});
test('testStepSummary renders actions', () => {
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'menu-select', params: { item_id: 3 },
@@ -135,6 +162,15 @@ test('testStepSummary renders actions', () => {
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'status', params: { attempts: 5 } }),
'STATUS x5 (poll)');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'proactive',
params: { attempts: 5, respond: { result: 'ok' },
require: { command: 'REFRESH', qualifier: '00' } } }),
'PROACTIVE x5 require REFRESH q=00 TR ok');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'proactive',
params: { attempts: 3, first: { result: 'cancel' } } }),
'PROACTIVE x3 TR ok first cancel');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'file-read', params: { path: 'MF/2FE2' } }),
'READ MF/2FE2');