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:
+52
-2
@@ -1877,7 +1877,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.20.0';
|
||||
const SIMPLE_VERSION = '3.21.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -6913,7 +6913,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'];
|
||||
const TEST_ACTION_KINDS = ['envelope', 'event', 'menu-select', 'file-write', 'file-read', 'apdu', 'scp80', 'status', 'proactive'];
|
||||
let _testScripts = null;
|
||||
let _testCurrentIdx = -1;
|
||||
let _testRunState = null;
|
||||
@@ -7072,6 +7072,8 @@ function testScriptProblem(script) {
|
||||
}
|
||||
}
|
||||
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() !== ''
|
||||
&& !(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');
|
||||
} else {
|
||||
@@ -7147,6 +7149,15 @@ 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)];
|
||||
const rq = p.require || {};
|
||||
if (rq.command) parts.push('require ' + rq.command + (rq.qualifier ? ' q=' + (rq.qualifier.value || rq.qualifier) : ''));
|
||||
const rr = p.respond || {};
|
||||
parts.push('TR ' + (rr.result === undefined ? 'ok' : rr.result));
|
||||
if (p.first && p.first.result) parts.push('first ' + p.first.result);
|
||||
s = parts.join(' ');
|
||||
}
|
||||
else s = String(step.kind || '?');
|
||||
const c = step.check || {};
|
||||
const ck = [];
|
||||
@@ -7464,6 +7475,24 @@ 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') {
|
||||
const pr = p.respond || {};
|
||||
html += testFormRow('TERMINAL RESPONSE result (each command)',
|
||||
testFormSelect('test-f-presult', pr.result === undefined ? 'ok' : String(pr.result), [
|
||||
{v: 'ok', l: t('ok')}, {v: 'cancel', l: t('cancel')}, {v: 'back', l: t('back')},
|
||||
{v: 'timeout', l: t('timeout')}, {v: 'no_response', l: t('no response')}]));
|
||||
const pf = p.first || {};
|
||||
html += testFormRow('First command only (optional)',
|
||||
testFormSelect('test-f-pfirst', pf.result === undefined ? '' : String(pf.result), [
|
||||
{v: '', l: t('same as above')}, {v: 'ok', l: t('ok')}, {v: 'cancel', l: t('cancel')},
|
||||
{v: 'back', l: t('back')}, {v: 'timeout', l: t('timeout')},
|
||||
{v: 'no_response', l: t('no response')}]));
|
||||
html += testFormRow('Attempts (STATUS rounds)', testFormInput('test-f-pattempts', p.attempts === undefined ? 3 : p.attempts, '1..1000'));
|
||||
html += testFormRow('Interval (ms, between rounds)', testFormInput('test-f-pinterval', p.interval_ms === undefined ? 200 : p.interval_ms, '0..10000'));
|
||||
const rq = p.require || {};
|
||||
html += testFormRow('Require a command (optional)', '<div class="flex gap-2">'
|
||||
+ testFormInput('test-f-prequire', rq.command, 'REFRESH')
|
||||
+ testFormInput('test-f-prequal', rq.qualifier && rq.qualifier.value, '00') + '</div>');
|
||||
}
|
||||
const c = step.check || {};
|
||||
const sw = c.sw || {};
|
||||
@@ -7720,6 +7749,20 @@ 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') {
|
||||
p.respond = {result: val('test-f-presult') || 'ok'};
|
||||
const first = val('test-f-pfirst');
|
||||
if (first) p.first = {result: first};
|
||||
p.attempts = num('test-f-pattempts', 3);
|
||||
p.interval_ms = num('test-f-pinterval', 200);
|
||||
const requireCmd = val('test-f-prequire');
|
||||
if (requireCmd) {
|
||||
const rq = {command: requireCmd.toUpperCase()};
|
||||
const rqHex = hex('test-f-prequal');
|
||||
if (rqHex) rq.qualifier = rqHex;
|
||||
p.require = rq;
|
||||
}
|
||||
}
|
||||
step.params = p;
|
||||
const check = {};
|
||||
const swVal = hex('test-f-sw');
|
||||
@@ -7793,6 +7836,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');
|
||||
} else if (!step.command) {
|
||||
return t('command is required');
|
||||
}
|
||||
@@ -21276,6 +21320,12 @@ const LANG_RU = {
|
||||
'SPI2 override (hex, optional)': 'Переопределение SPI2 (hex, опционально)',
|
||||
'Attempts (1 = single STATUS)': 'Попытки (1 = один STATUS)',
|
||||
'Interval (ms, when polling)': 'Интервал (мс, при опросе)',
|
||||
'Attempts (STATUS rounds)': 'Попытки (раунды STATUS)',
|
||||
'Interval (ms, between rounds)': 'Интервал (мс, между раундами)',
|
||||
'TERMINAL RESPONSE result (each command)': 'Результат TERMINAL RESPONSE (для каждой команды)',
|
||||
'First command only (optional)': 'Только для первой команды (необязательно)',
|
||||
'same as above': 'как выше',
|
||||
'Require a command (optional)': 'Требовать команду (необязательно)',
|
||||
'attempts must be at least 1': 'число попыток должно быть ≥ 1',
|
||||
'Expected SW': 'Ожидаемое SW',
|
||||
'default: 9000 (91?? when polling)': 'по умолчанию: 9000 (91?? при опросе)',
|
||||
|
||||
Reference in New Issue
Block a user