fix: follow-up test-script review findings (v3.5.14)

- The SCP80 counter write-back resolves the preset by NAME first (the run
  snapshot prefers the name) and only falls back to an ICCID-looking
  value: presets with digits in their names are found after a page reload
  (the previous fallback ran cardsNormIccid on the name and gave up).
- testScriptProblem validates the *selected* SCP80 source (like the form
  and the server), so source=apdu with only sp filled is caught locally
  instead of failing with a server 400.
- _test_run_start cleans up (_TEST_RUNNING=False, run state error) when
  the worker thread cannot be created/started, and the endpoint answers
  500 with a clear error instead of leaving the card blocked behind a run
  that never started.
- The 5 s poll writes the counter back whenever a run is finished
  (idempotent), so a reloaded page saves it without visiting the pill.
- Mask wildcards ('?') are stripped from the hex fields that cannot carry
  a mask (APDU, secured packet, event/file data, TAR/SPI overrides, DCS,
  extra TLVs); check values keep them.

Tests: frontend +3 (write-back by name / by ICCID, mask-free fields,
source-aware script check), Python +1 (failed thread start unblocks).
586 frontend / 471 Python green; version 3.5.14; sw simple-v267.
This commit is contained in:
2026-09-27 01:52:03 +03:00
parent d1eb88d4d7
commit 2d2a40a73c
6 changed files with 115 additions and 24 deletions
+25 -15
View File
@@ -1616,7 +1616,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.5.13';
const SIMPLE_VERSION = '3.5.14';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -6479,7 +6479,11 @@ function testScriptProblem(script) {
if (TEST_ACTION_KINDS.indexOf(s.kind) < 0) return at + t('unknown action kind');
const p = s.params || {};
if (s.kind === 'apdu' && !String(p.apdu || '').trim()) return at + t('APDU is empty');
if (s.kind === 'scp80' && !String(p.apdu || p.sp || '').trim()) return at + t('SCP80 needs an APDU or a secured packet');
if (s.kind === 'scp80') {
const src = p.source || (p.sp ? 'sp' : 'apdu');
const value = src === 'sp' ? p.sp : p.apdu;
if (!String(value || '').trim()) return at + t('SCP80 needs an APDU or a secured packet');
}
if (s.kind === 'menu-select' && !(p.item_id >= 1 && p.item_id <= 255)) return at + t('item id must be 1..255');
if ((s.kind === 'file-write' || s.kind === 'file-read') && !String(p.path || '').trim()) return at + t('file path is empty');
} else if (s.type === 'expect') {
@@ -6928,33 +6932,34 @@ function testStepCollect() {
if (!step) return;
const val = id => { const el = document.getElementById(id); return el ? el.value.trim() : ''; };
const hex = id => val(id).replace(/[^0-9a-fA-F?]/g, '').toUpperCase();
const hexNoMask = id => val(id).replace(/[^0-9a-fA-F]/g, '').toUpperCase();
const num = (id, dflt) => { const v = parseInt(val(id), 10); return isNaN(v) ? dflt : v; };
if (step.type === 'action') {
const kind = val('test-step-kind') || step.kind;
step.kind = kind;
const p = {};
if (kind === 'envelope') { p.event = num('test-f-event', 3); p.data = hex('test-f-data'); }
if (kind === 'envelope') { p.event = num('test-f-event', 3); p.data = hexNoMask('test-f-data'); }
else if (kind === 'menu-select') { p.item_id = num('test-f-item', 1); }
else if (kind === 'file-write' || kind === 'file-read') {
p.path = val('test-f-path');
p.mode = val('test-f-mode') || 'auto';
const rec = num('test-f-record', 0);
if (rec) p.record = rec;
if (kind === 'file-write') p.data = hex('test-f-fdata');
if (kind === 'file-write') p.data = hexNoMask('test-f-fdata');
}
else if (kind === 'apdu') { p.apdu = hex('test-f-apdu'); }
else if (kind === 'apdu') { p.apdu = hexNoMask('test-f-apdu'); }
else if (kind === 'scp80') {
// the source is stored explicitly: switching it must not drop the
// value of the other source (and must stick when the field is empty)
const prev = step.params || {};
const src = val('test-f-src') || prev.source || (prev.sp ? 'sp' : 'apdu');
// keep both values: the field of the inactive source is not rendered
if (document.getElementById('test-f-sp')) p.sp = hex('test-f-sp');
if (document.getElementById('test-f-sp')) p.sp = hexNoMask('test-f-sp');
else p.sp = prev.sp || '';
if (document.getElementById('test-f-apdu')) p.apdu = hex('test-f-apdu');
if (document.getElementById('test-f-apdu')) p.apdu = hexNoMask('test-f-apdu');
else p.apdu = prev.apdu || '';
p.source = src;
['tar', 'spi1', 'spi2'].forEach(k => { const v = hex('test-f-' + k); if (v) p[k] = v; });
['tar', 'spi1', 'spi2'].forEach(k => { const v = hexNoMask('test-f-' + k); if (v) p[k] = v; });
}
else if (kind === 'status') { p.attempts = num('test-f-attempts', 1); p.interval_ms = num('test-f-interval', 200); }
step.params = p;
@@ -6984,8 +6989,8 @@ function testStepCollect() {
const ri = num('test-f-ritem', 0);
if (ri) r.item_id = ri;
const text = val('test-f-rtext');
if (text) { r.text = text; r.dcs = hex('test-f-rdcs') || '00'; }
const raw = hex('test-f-rraw');
if (text) { r.text = text; r.dcs = hexNoMask('test-f-rdcs') || '00'; }
const raw = hexNoMask('test-f-rraw');
if (raw) r.raw = raw;
step.respond = r;
step.on_fail = val('test-f-fail') || 'error';
@@ -7176,10 +7181,14 @@ function testWriteBackCounter() {
const next = String(st.scp80_counter).toUpperCase();
let idx = _testLastPresetIdx;
if (idx < 0 && st.preset) {
// a run started elsewhere (or observed after a page reload): find the
// preset the snapshot names, by ICCID or by name
const norm = cardsNormIccid(st.preset);
idx = norm ? cardsFindByIccid(norm) : cards.findIndex(c => (c.name || '') === st.preset);
// A run started elsewhere (or observed after a page reload): the run
// snapshot prefers the preset name, so look that up first and only
// fall back to an ICCID-looking value.
idx = cards.findIndex(c => (c.name || '') === st.preset);
if (idx < 0) {
const norm = cardsNormIccid(st.preset);
if (norm && norm.length >= 5) idx = cardsFindByIccid(norm);
}
}
if (idx < 0) return;
const preset = cards[idx];
@@ -10962,7 +10971,8 @@ function pysimStartBackendPoll() {
_testRunState = ts;
if (wasRunning !== !!ts.running) pysimApplyAvailability();
if (isViewVisible('phone-sub-test')) testRenderRun(ts);
if (wasRunning && !ts.running) testWriteBackCounter();
// idempotent: saves the counter of a run finished elsewhere
if (!ts.running) testWriteBackCounter();
}
} catch (e) { /* ignore */ }
}, 5000);