The profile rule editor rendered the same "FCI hex (raw SELECT response)"
field for Exact and Custom, although the custom check never reads
`fciHex` (it compares the data objects against the card's live FCI) - so
the field looked like an expectation it was not.
- The raw-FCI field and its decoded preview are exact-mode-only now; the
Custom mode shows just its data-object editor.
- Switching a rule to Custom seeds the data objects from its captured FCI
when the list is empty (the same all-'value' seeding a captured profile
gets); the "load from the captured FCI" link refills an emptied list,
and a rule without a captured FCI still starts empty for manual rows.
From the v3.28.3 review: the ADM's byte-pair rule now lives in one
place, presets.ADM_PATTERN, used by /api/verify-adm and the outfile
import - two literals could drift and an imported ADM could silently
become unverifiable. The RU help's ADM wording is "4-16 hex-цифр
(чётное число)".
Docs: help EN/RU, the repo AGENTS. Tests: the mode switch (seed / keep /
no FCI / switch-away), the field-gating source pin, the pattern's shape
and the both-users pin. 917 frontend + 927 Python; sw cache simple-v442.
The review of v3.28.2 found the parser accepting an odd-length ADM (e.g.
12345), while /api/verify-adm requires byte pairs
((?:[0-9A-F]{2}){2,8}) - the stored value could never verify and was
refused with a misleading "adm must be 4-16 hex digits".
The parser now applies the verifier's exact rule: an odd length (a half
byte) or a value over 16 digits is dropped at import time with "ADM is
not 4-16 hex digits in byte pairs".
Docs: help EN/RU, api.md and the repo AGENTS say "4-16 hex digits in
byte pairs". Tests: the odd-length and the over-16 drop cases (both
real samples are even-length and unaffected: BAP 3632333239463337,
AM0027 CFA3DD9C6B7F0F8C). 915 frontend + 925 Python; sw cache
simple-v441.
The ADM column carries the key's hex value (the length varies, 4-16 hex
digits) - the form the preset store and /api/verify-adm use (pySim pads
it with 'f' to the 8 CHV bytes). The parser treated it as an ASCII
string hex-encoded and either mangled it (3632333239463337 -> 62329F37,
a wrong ADM that would fail VERIFY and consume an attempt) or dropped it
(CFA3DD9C6B7F0F8C - the live report with AM0027_alfa_dsa.out).
- The ADM is stored as-is: non-hex stripped, uppercased, [0-9A-F]{4,16};
a non-hex or too-short value is dropped with "ADM is not 4-16 hex
digits".
- _ascii_from_hex() is gone (its only user was the ADM column).
- Docs: help EN/RU, api.md and the repo AGENTS now say "the key's hex
value, 4-16 hex digits (the length varies)".
Note: presets imported from a BAP-style file before this fix hold the
mangled ADM - re-import or correct them.
Tests: the BAP fixture (the hex as-is), the AM0027 shape, a
short/lowercase value and the drop cases. Both real samples verified
(BAP 3632333239463337, AM0027 CFA3DD9C6B7F0F8C; the ALFA sample has no
ADM column and is unchanged). 915 frontend + 925 Python; sw cache
simple-v440.
- A throbber (#ram-batch-spin) next to the phase line shows while a card
is actually being installed (active && current - the stopping-mid-
install phase included) and hides again while waiting.
- The waiting prompt is explicit now: "Insert next card. Waiting for a
card..." (RU "Вставьте следующую карту. Ожидание карты…"); the old
"Waiting for a card..." key is gone.
- The notice reports only the last *finished* card: starting the next
card's install clears it (state['notice'] = '' alongside
current/installing in _ram_batch_handle_card), so the previous card's
verdict never lingers over the next install. It stays visible while
waiting - the "report the status + ask for the next card" behavior.
- The batch operation's RU label is "Пакетная установка (.cap на
множество карт)"; the help row's title aligned.
Tests: the render test (the new prompt, the throbber visible while
installing / stopping-mid-install and hidden while waiting, an empty
notice hiding the box) and the worker test (the notice is empty
mid-install and set again for the new outcome). 915 frontend + 925
Python; sw cache simple-v439.
The bulk workflow: one .cap and one set of install parameters applied to
card after card - swap cards and each one installs as soon as it is
equipped. The server owns the loop, so closing the tab or reloading the
page does not interrupt it.
The install chain is now shared: _ram_install_plan(body) builds the
card-independent part (the parsed .cap, the INSTALL/LOAD sequence, the
block size, the transport knobs) and _ram_install_run(server, body, plan,
scc) runs one chain (the per-card preset/keyset/counter resolution
included); the /api/ram-install handler is the plan + run + response, and
the batch worker calls the same pair. The plan is built once at Start,
so a bad .cap or parameter combination is a 400 there (a malformed
archive included - a zip parse error is the caller's, never a dropped
connection), not a per-card failure. _op_keyset_resolve() is the module
form of the handler's _op_keyset() (the dead _keyset_guard method is
gone).
The worker (_ram_batch_worker, state _RAM_BATCH) polls the card state
every 0.5 s and acts on a new card session while connected; a card
already equipped at Start installs immediately. Per card the preset is
matched by ICCID and its ISD role TAR supplies the keyset (v3.26.0) and
the counter seed (v3.20.0); a card with no readable ICCID, no matching
preset or no usable ISD keyset is recorded as skipped with its reason.
A card is attempted once per run (any outcome): re-equipping it records
"already processed in this batch (<outcome>)" and never installs again -
restarting the batch is the retry. Failures carry the failing step and
the remote SW, are never retried and never stop the run; an unexpected
card-handling error is recorded and the loop keeps waiting.
Endpoints: POST /api/ram-batch/start (no card needed; 400/409), GET
/api/ram-batch/status (card-free - the panel polls it during an install),
POST /api/ram-batch/stop (the first _CARD_FREE_POST: it only sets a flag,
so the button works while the worker holds the card lock; a running
install always finishes first) and POST /api/ram-batch/clear (409 while
active). /api/status carries the ram_batch summary (the detailed state
minus the attempts list), so the panel's headline - the live phase, the
current card, the counts - rides the 2 s poll; the attempts report (the
last 500 kept, the total counted) is fetched only when it changed. The
batch and a test run exclude each other (409 both ways); the state is in
memory - a server restart ends a run.
PWA: the Batch install (.cap file, per card) RAM operation shares the
install form (ramInstallCapFields(); the single install uses it too and
now sends ram_format, and the block-size validation moved before the
modal), hides the preset selector + Execute and the preset-mismatch
banner, and renders Start/Stop/Clear, the phase headline (equip the next
card / installing card + live step / stopping after the current card),
the counts and the attempts table (newest first). The panel only works
inside the visible SCP80 RAM pill.
Docs: help EN/RU, api.md (a section + the four table rows), the repo
AGENTS. Tests: the state/report helpers, the per-card decisions, the
worker loop (a real thread + a fake install), the lifecycle and guards,
the HTTP endpoints (the malformed .cap, the stop under a held card lock,
the test-run refusal) and the PWA panel helpers/wiring. 914 frontend +
919 Python; sw cache simple-v438.
Import many card presets at once from a card vendor's plain-text dump.
The new pure pysim_simple_server/outfile.py (parse_outfile) plus
POST /api/presets/import-outfile: without `mode` the call is a dry-run
preview returning the parsed presets and a report (nothing is stored),
with `merge`/`replace` the same parse runs and the presets go through
the preset store.
The parser: the data starts at the first line whose whitespace tokens
all look like column names and include ICCID (a leading `word:` prefix
is stripped, so a `var_out:IMSI ICCID ...` header line matches while a
config line naming a value - `ICCID:897...` or `ICCID 897...` - cannot);
one row per card follows. Recognised columns (case-insensitive): ICCID,
PIN1/PIN2, PUK1/PUK2, ADM (the ASCII string, hex-encoded), PSK (32 hex;
an optional PSKID/PSK_ID names the identity, defaulting to the card's
ICCID) and KICn/KIDn - one keyset per numbered pair, up to 15;
IMSI*/EKI/KIKn are ignored. The KIc/KID bytes are inferred: the keyset
number is the column number, the algorithm nibble follows the key size
(8 B DES, 16 B 3DES2 or AES - the preview's choice, 3DES2 default,
24 B 3DES3; anything else skips the keyset with a note), both bytes
carrying the same number (TS 102 225 A.2). The TARs never come from the
file: the caller's table (default the three role entries) is applied
with one global default keyset written into every entry (the card's
first keyset when unset). An invalid optional code is dropped with a
report line instead of failing the card; the report names the columns,
the rows read, the parsed keysets and every skip.
The ICCID normaliser fix (both sides, drift-guarded): the trailing F
(or Fs) is the odd-nibble padding - when the rest is digits only it is
the plain digit form (the vendors print the ICCID as digits+F),
otherwise the value is the raw nibble-swapped EF hex; a raw value never
ends in F, so the rule is unambiguous.
PWA: the Import outfile button in the Cards list, the hidden file input
and the preview panel (the common name, the 16-byte algorithm select,
the TAR table with the role defaults editable and + TAR / remove for
extras, the global default keyset select over the parsed keysets, the
merge/replace mode, the parsed cards table, the report, Refresh
preview, Import). The panel joins refreshDynamicI18n for the language
switch.
Docs: help EN/RU (the term outfile stays untranslated in RU), api.md's
new section, the repo AGENTS. Tests: the parser (both fixture shapes,
the column detection, the key-size inference, the options/TAR table,
the PSK identity, the skips, the store round-trip), the endpoint (the
preview, the import, the duplicate skip, the 400s), the normaliser on
both sides, the PWA helpers/wiring and the algorithm16 drift guard
against outfile.py. 904 frontend + 903 Python; sw cache simple-v436.
Every preset TAR entry carries `keyset` (int 1-15) - the keyset every
operation addressed to that TAR uses: mandatory for any non-zero MSL
(the counters are keyset-linked), optional for MSL 00 (allowed: some
cards reject the no-security 00/00 KIc/KID form). The store fills it
automatically when the preset has a single keyset, the validation
refuses an entry naming an undefined keyset, and the schema is 6 (a v5
file loads leniently; editing it forces the fix).
The keyset selectors are gone: the SCP80 TAR field became a select of
the preset's entries (applying the TAR's keyset to the form and seeding
the SPI1 from its MSL); the RAM operations use the preset's ISD role
entry (the PWA sends only the preset id, the ISD TAR, SPI and the
counter override - the server resolves the keyset and the keys:
presets.tar_keyset, _preset_tar_keyset, _resolve_operation_keyset,
_op_keyset, used by send-ota's APDU path, ram-install, ram-install-app
and cap-compat); the counter probe and the test runner's scp80 steps
default to their TAR's keyset (an explicit kvn stays an override). A
security TAR without a keyset refuses the operation ("set it in the
Cards tab") - never a silent keyless send - and the start buttons wait
for a usable operation-TAR keyset.
The TAR probe checks the preset's TAR list only (the standard
TAR_PROBE_TARS list and the custom-TAR localStorage are gone; a `tars`
list selects a subset, a TAR outside the preset is refused), each row
using its own MSL and default keyset. The Cards editor's TAR table
gained a Keyset column (the only keyset preselected, an amber marker
when a security MSL lacks one).
Docs: help EN/RU, api.md's tar-probe contract, both AGENTS. Tests: the
store (the field, the auto-fill, the schema, the string forms), the
server resolution (send-ota + the refusal, the probe's per-row keysets
and refusals), the PWA (the TAR select, the RAM ISD resolution, the
keyset-free requests) - several fixtures updated for the new schema.
891 frontend + 885 Python; sw cache simple-v434.
fciMode 'custom' + fciDos + fciExtra ('ignore' default / 'fail'): each
row is a data-object key (the decoder's keys - 2/4-hex tags and the
nested A5/xx/C6/xx children), a mode (value / present / absent) and, for
value, hex with the contents vocabulary (no '?' = prefix - 8B + 2F06 =
any EF.ARR record of that FID, the record-number-agnostic form; ? =
fixed-length nibble mask). Only the listed objects are checked; 'fail'
also rejects any unlisted object (order-insensitive strictness).
The scan dialog's FCP/FCI selector gained the mode ("Custom (per data
object)" - the acronym spelled out) and seeds every rule created from a
card or a snapshot with that file's captured FCI objects; a manually
added rule starts empty, and the editor offers "load from the captured
FCI" when the list is empty and the rule carries an FCI. The report
reuses the access-rules diff marks; the aspect is 'FCI (custom)',
separate from 'Exact FCI' (the filetype/size/record checks still run).
Docs: help EN/RU (the check bullet + the scan dialog paragraph).
Tests: the check (exact/prefix/mask, present/absent, duplicates, nested
keys, extras ignore/fail, the empty list, the parse error, the
size/record checks still running), the seeding, the editor
rows/datalist/load link, the report, the aspects, the validation.
893 frontend + 880 Python; sw cache simple-v432.
The live report: an SPI2 01 secured packet reported no PoR. For such a
packet TS 31.115 4.2/4.4 (b6=0) has the Response Packet retrieved by the
ME from the ENVELOPE exchange (SEND SHORT MESSAGE is the b6=1 case), but
_send_envelope only asked for response bytes after a 61xx answer - the
card signals them with 62xx/63xx instead (the "more data" quirk the
remote-command SWs already acknowledge, _ram_remote_sw_ok), so the whole
PoR stayed on the card and the late-PoR STATUS poll found nothing.
_envelope_get_response (shared with _send_event_download, which had the
same 61xx-only code): a 61xx names the GET RESPONSE length; a 62xx/63xx
without data gets Le 256 (00c0000000 - a bare 61 no longer emits the
odd-length '%02x' % 0x100 APDU). The result is adopted when bytes were
delivered (or for a 61xx); otherwise the warning stays, so the v3.6.58
SEND-SM PoR path (the STATUS poll) keeps working.
Tests: the 61xx length, the 63xx+PoR fetch (Le 256), the bare-61
encoding, the warning-kept path (the poll still runs) and the
event-download sharing. 887 frontend + 880 Python; sw cache simple-v431.
The v3.25.5 report's follow-ups: a field the server could not use was
silently dropped, and the PWA also filtered non-hex fields client-side
(never sending them). The POST now collects them and names them in the
response - `invalid` (a malformed key/value: not hex, an odd number of
digits, a non-string value) and `unknown` (a well-formed code outside the
qualifier list), both omitted when everything was applied - and the PWA's
Save sends every field, composes "Not saved: 0A (invalid value), 18
(unknown qualifier)" (`pysimPliRejectMsg`) into a new #pli-save-msg line
under the button and shows "Not saved" briefly.
The guide's timer exception now says "card-initiated automatic action"
(the timer retries every 5 s; polling/menu are suspended, not retrying).
Docs: api.md's POST contract, help EN/RU - which also said "22 PLI
qualifiers" while the list has 25. Tests: the rejection lists per case
(odd length, non-hex value/key, non-string, unknown code) and the
valid-save absence, the PWA message helper and the wiring pins.
887 frontend + 875 Python; sw cache simple-v430.
The live report: POST /api/pli-dict did not change the dictionary.
_PLI_DATA held a '' placeholder for every qualifier, and the clear
branch's pop removed the placeholder even when there was no override.
The PWA's Save posts every field, so one Save wiped the placeholders;
the guard `if code in _PLI_DATA` then silently skipped every later
override for those qualifiers (HTTP 200 + the unchanged/default dict)
until a restart - and the next Save wiped them again.
_PLI_DATA is now overrides-only ({}; the GET and _pli_data_hex already
fall back to PLI_DEFAULTS) and the guard tests PLI_QUALIFIER_NAMES, so a
new override always sticks and a clear never poisons a later set.
Tests: the endpoint test now saves/restores the dict (its pop(0x01)
cleanup was the same trap) plus a regression - the PWA-style full-dict
POST, then new overrides with and without defaults must stick, a clear
restores the default and a later set still works, an unknown qualifier
is ignored. 886 frontend + 874 Python; sw cache simple-v429.
The access-rules check was exact-only: every listed decoded line had to be
on the card and every card rule listed (missing + extra both fail), so
"READ/WRITE must be always, the RESIZE condition is irrelevant" could not
be expressed.
A rule may now carry accessRules.mode: 'contains' (the editor's Match
selector: Exact default / Contains; the collapsed header badges
"· per operation"): every listed rule is matched per operation - each AM
bit / command entry (RESIZE/INCREASE) must be covered by a card rule with
the same SC list, whatever AM grouping the card uses
(fcpArrRuleOps/fcpArrRuleOpCovered/fcpArrRuleSatisfied) - and the rules not
listed are not checked (no extra side). A form outside the supported
subset (a raw template form, a labelled pairs-form line) compares by exact
string; the proprietary (b8) form stays whole. The check carries mode and
the report notes "compared per operation; other card rules are ignored".
Validation accepts mode in {exact, contains}; the capture keeps writing no
mode (exact).
Docs: help EN/RU. Tests: the per-op matcher, the contains/exact verdicts
(the combined-AM grouping case, a condition mismatch, the RESIZE condition,
not-listed ignored), the editor selector/badge, the validation, the report
note. 886 frontend + 873 Python; sw cache simple-v428.
The runner's three FETCH/TR sites (the proactive-drain action, the
expectation, the cleanup drain) built every TERMINAL RESPONSE from the
script, so a card-announced OPEN CHANNEL during a run was answered
"performed successfully" without opening a channel - SEND DATA was
dropped and RECEIVE DATA returned nothing (silent data loss, broken
applet state).
_test_step_tr now routes 0x40-0x44 through _handle_bip_command: the
terminal answers for real, so the card's channels work inside a run.
The script keeps an escape hatch - a respond with `raw` TLVs or a
non-ok result wins (the applet error-path tests).
The BIP events flow during a run again for the channels the card
itself opened: _bip_data_available and _bip_flush_channel_events no
longer stop at the run guard (a channel only exists because the card
opened it) and the runner flushes the link events after each TR, so an
applet's own HTTP/BIP conversation completes inside a scripted run.
BIP control stays blocked (409).
Tests: the drain/expect/cleanup routing, the raw and non-ok-result
overrides, the run-time data-available and link events. 883 frontend
+ 873 Python; sw cache simple-v427.
The TR's response objects are all Min = N in the spec's own table, and
6.8.0 says such objects "should set the CR flag to comprehension not
required" - so the builders now emit the base tag forms:
- device identities 82 -> 02 (every builder)
- PLI data 93/94/AD/AE/BF/E2/E5 -> 13/14/2D/2E/3F/62/65 (26/63 already
base); the PWA inline codecs follow
- POLL INTERVAL Duration 84 -> 04
- GET INKEY/INPUT Text string 8D -> 0D
- SELECT ITEM Item identifier 90 -> 10
- Timer identifier/value A4/A5 -> 24/25
(_bip_tr already used the reference traces' 01/02/03.)
The command details now echo the command's tag form (01/81) besides
its qualifier - 6.8.1 requires the object to be identical to the
command's "including the comprehension required flag": the parser
returns cd_tag and _build_tr, _handle_timer_command,
_handle_bip_command, the menu pendings and testscript.build_tr carry
it. The Result keeps 03/83 (Min = Y). _tr_data_only strips both tag
styles and the POLL Duration decode accepts 04.
Tests: the base forms per builder + the echo + the PWA drift guards;
883 frontend + 866 Python; sw cache simple-v426.
A card that re-queues a command immediately after each accepted TR
(live 2026-10-11: the EP BAP card re-issued the same PROVIDE LOCAL
INFORMATION 1100 times) turned the background poll's FETCH/TR chain
into an unbounded loop that held _CARD_LOCK forever - every card
operation, a test run's start included, blocked behind it.
The chain now yields:
- _handle_proactive_chain aborts before the next FETCH while a test run
owns the card (_TEST_RUNNING) - the pending command stays for the
script - and takes max_rounds; the background poll passes
_POLL_CHAIN_MAX = 10, so a chatty card cannot hold the lock.
- _do_status_poll re-arms on its run skip (a heartbeat that never
fetches); _timer_expired and the BIP Data-available event wait during
a run; _bip_flush_channel_events stays quiet too.
- _sim_resume_poll() resumes polling at every run end - the two worker
bodies, their exception paths and the spawn failure.
Also the TR command-details qualifier echo (TS 102 223 6.8.1: the
object shall be identical to the command's): _build_tr,
testscript.build_tr and the interactive _menu_send_response echoed a
hardcoded 00 - harmless for the loop's qual-00 PLI, wrong for any
other qualifier. The menu pendings now carry cmd_qual (parsed in the
on_fetch handler).
Tests: the chain bound/abort (a fake card re-queueing a command), the
poll/timer guards, the resume wiring, the qualifier echo in both
builders. 883 frontend + 865 Python; sw cache simple-v425.
A profile file rule may carry accessRules {expected, enabled?, ignoreFciRef?}:
the check resolves the file FCI's '8B' security-attribute reference against
the card's (or the snapshot's) EF.ARR copies and compares the decoded rules as
a set - record-number independent, so cards with a different EF.ARR layout
still verify. Missing/extra lines are listed in the report; a file whose FCI
lost the reference fails; a reference that cannot be resolved (no cached
EF.ARR / record out of range) is an unverifiable error.
Capture fills the lines when a profile is created from a card
(pysimArrEnsure) or a snapshot (profilerSnapshotArr), before the contents
ignore list - the rules are the meaningful per-file check, and EF.ARR's own
contents stay ignored by default as before. The editor shows the captured
lines read-only with a None/Check toggle; with exact FCI an optional "Ignore
the EF.ARR reference in the exact FCI" checkbox compares the FCI without the
'8B' value (every other DO stays byte-exact; fcpWithoutArrRef), so a different
record number no longer fails while the rules check owns its meaning.
The snapshot comparison is unchanged (byte-exact EF.ARR + FCI catch record
renumbering). New 'Access rules' aspect in the report/summary with a
matching/missing/extra line diff.
Tests: capture, the four check outcomes, the ignore-FCI path, the
aspect/report, validation. 877 frontend + 857 Python; sw cache simple-v424.
The file manager's decoded FCI shows the '8B' security attribute
reference (EF_ARR 2F06, record 23). The referenced record's access
rules are now rendered as indented muted lines under that row, decoded
with the existing EF.ARR record decoder (AM_DO/SC_DO conditions).
The server reads the card's ARR copies (MF/2F06 and ADF.USIM/6F06,
best effort) once per card session in _apply_equipped_card and keeps
them as server.arr; GET /api/arr serves the cache without the card
lock, a disconnect clears it, and a write to an ARR copy through
/api/write refreshes it. The PWA fetches it lazily per card session
(pysimArrEnsure, keyed by card_session) and resolves the reference with
the selected file's DF context: an ADF.USIM file uses the ADF's 6F06
copy, the implicit reference form too (ISO 7816-4 5.4.3.3 lookup
order). Unresolvable references stay silent.
The '8B' decode now covers all three ISO 7816-4 Table 25 forms:
implicit ARR byte / FID + record / FID + (SEID, ARR) pairs - the pairs
form was mis-decoded as a record before. A snapshot resolves the same
references offline from its own captured EF.ARR copy.
Tests: the three forms, the resolution (DF preference, out-of-range
silence), the snapshot path, the server read/cache/route/write-refresh.
868 frontend + 856 Python; sw cache simple-v423.
A RAM install that fails at a LOAD step is typically the library-version
gate: the card verifies the CAP's import list when the Import component
completes (JC VM 4.5.2). The failure hint now says so and points at the
CAP compatibility test (Set all 0.0, then restore the lines one at a time
to find the missing package and version), replacing the old "retry with a
smaller LOAD block" advice, which is a red herring for that gate.
The failure popup gains a one-click "Run the CAP compatibility test"
button: it closes the popup, switches the RAM operation selector to compat
and runs it with the same .cap (the file input still holds it and the probe
list is prefilled with its imports), so the diagnostic is one click away.
An INSTALL [for load] refusal now gets its own advice instead of the LOAD
hint: the card refused to open the load file - usually a duplicate load
file from an earlier run (delete the package first, Explore -> Delete), or
rejected load parameters.
Tests: the hint's wording and branches (LOAD vs INSTALL [for load]), the
button handler's op switch. 862 frontend + 842 Python; sw cache
simple-v422.
The step editor's result select offers the full TS 102 223 8.12.0 list; the
spec's applicability table (Tables 6.1/6.2/6.3, "Proactive commands versus
possible terminal response") now groups it by the selected command: the
command-relevant values first (an <optgroup> "relevant for <command>"), the
rest after ("all other results").
The grouping is ordering-only - nothing is hidden, the manual hex entry
covers everything, and ANY (the drain's respond/first too) stays a flat list,
so a stored result always matches an option and real-card deviations remain
reachable.
TR_RESULT_BY_COMMAND is parsed from the pinned PDF (41 commands) and pinned
by frontend/tests/tr_result_by_command.json; trCommandCode() resolves the
editor's command names to their type codes (the first code per name,
matching testCommandOptions) and testFormSelect gained the optgroup support.
Tests: the map vs the parsed fixture, the grouping and the union (nothing
lost), the optgroup rendering and the call-site pin. 861 frontend + 842
Python; sw cache simple-v421.
Pressing the Phone tab's Send (or Rescue) re-sends the profile and starts a
new CAT session. The log was cleared with the rest of the session state,
losing the only record of the card's proactive commands - a common
tweak-the-profile-and-resend loop wiped the history every time.
_resend_terminal_profile now calls _mark_proactive_session(
'terminal-profile') instead: the entries stay, _PROACTIVE_SESSION_START
restarts (so the new session's elapsed values begin at 0) and a boundary
marker entry is appended. The PWA renders the marker as a
'- TERMINAL PROFILE re-sent - new CAT session -' divider (EN/RU).
Equip / disconnect keep the full clear (a card change); the pending-command
cancel, menu timeout and event list reset are unchanged.
Tests: the marker keeps the entries and restarts the timer, the reset still
clears, the re-send path pinned by source (Python) and the renderer's marker
branch (Node). 857 frontend + 842 Python; sw cache simple-v420.
A script's `event` value may be a code (a hex byte or a number) or one of the
server's name keywords (location_status, access_tech, network_rejection,
data_connection, ... - events.py's EVENT_NAMES); the API accepts both. The
PWA parsed every value as a hex string: the step list showed
'EVENT 0xaccess_tech' (the unparsed fallback), the editor fell back to event
03 (Location status) with the location field set, and saving then rewrote
the step to that event - silently corrupting name-authored scripts (the two
Alfa2G trigger scripts use access_tech/data_connection).
A mirrored EVENT_ALIASES map plus an eventCode() resolver (number / hex byte
/ name -> code) now feed the step summary, the editor's event block and the
event check; the summary's fallback no longer prefixes a non-numeric value
with 0x, and the editor keeps its canonical hex form on save (the same code).
Drift guard: the aliases live in frontend/tests/event_vectors.json, asserted
by the Node test (the PWA's map + resolver) and the Python test
(events.EVENT_NAMES + resolve_event). Tests: 856 frontend + 839 Python;
sw cache simple-v419.
The step editor's result select (the expect's respond, the proactive-drain's
respond/first) offered five symbolic values with no hex. It now offers the
full TS 102 223 v18.3.0 8.12.0 list (00-3F), labelled '<hex> - <alias>' (or
'<hex> - <spec name>'), plus a manual hex entry, and an optional
additional-information byte for the general result (83 02 <result> <info>,
default 00; the 8.12.2 terminal-problem values are suggested through a
datalist).
The script aliases were corrected to the spec: refused 03->22,
not_understood 04->32, modified 06->07, no_response 22->12 - a stored script
using one of them now answers with the corrected value; no_response stays
accepted for compatibility but is no longer offered by the editor.
The server's TR decode tables (_RESULT_NAMES_BASIC/_GENERAL, a GSM 11.14
mix displayed in the proactive log) are replaced by the spec table plus the
8.12.2 additional-information names; the dcs field parses as hex (0A used to
be refused and 10 read as decimal 10); the drain summary's 22 label is fixed.
The result labels are not translated (spec text); the unused RU keys were
removed. Shared fixture frontend/tests/tr_results.json, drift-guarded by
both suites. 855 frontend + 838 Python; sw cache simple-v418.
esc() is string-only, and most EVENT_FORMS option values are numbers
({v:0,l:'Normal service'}, the ACCESSTECH list, ...): the shared event-form
renderer escaped them with esc(o.v), so opening any such event form threw
'str.replace is not a function' - in the Phone tab (pysimEventSendForm) and
the test-script step editor alike (both call the same builder). Introduced
with v3.22.0, when the event action started using the Simulator's forms.
Option values (and the data-gr group) are stringified before escaping, the
pattern the sibling option builder already used; the emitted DOM values are
unchanged ("0", "10").
Test: event_forms.test.js renders every event form (Phone and step-editor
variants) and asserts a numeric option prefills selected - verified to fail
on the pre-fix code. 848 frontend + 830 Python; sw cache simple-v417.
The two buttons under a cntr_low verdict shared the red warning style, and
the notice only ever rendered in the RAM pill: an SCP80 send wrote it into
the hidden RAM container, so the plain-send view showed the verdict text
but offered no way to act on it.
- The counter probe button is amber and the preset jump green (the warning
text stays red).
- The probe renders the inline throbber the RAM operations use
(animate-spin, no modal) while it runs; its result replaces it in the
same box.
- Each pill carries its own notice box (#ram-cntr-low / #sp-cntr-low):
ramShowCntrLow/ramSyncCounter target the originating view's box and
clear the other, the SCP80 send passes 'sp' explicitly (it used to rely
on the keyset-view heuristic), and a new send attempt clears the stale
verdict (ramClearResults clears both boxes).
- The prebuilt Tailwind output was regenerated (npm run build):
border-amber-300/-700 and border-emerald-300/-700 were absent from
style.css (the stale-style trap).
Tests: the button classes, the spinner and the box targeting pinned in
ram.test.js. 846 frontend + 830 Python; sw cache simple-v416.
A suite with start_reset and an incomplete SCP80 preset reset the card and
only then refused the start: the reset block ran before _test_preset_error.
The reset now runs after the preset checks - a run that cannot start must
not touch the card - and stays before the ADM gate, which must see the
post-reset card session (the reset bumps card_session, invalidating the ADM
latch). The ICCID re-match stays with the reset block.
Test: an incomplete preset refuses with 400 and _card_reset_reinit is not
called. 846 frontend + 830 Python; sw cache simple-v415.
An unconfigured PLI dictionary answered result-ok with no data object,
which confuses applications that expect data. The dictionary now has a
base layer of sensible defaults for the qualifiers the pinned ETSI specs
define (TS 102 223 8.x, tags per TS 101 220 V18.3.0 Table 7.23):
- location as the test network 001/01 + LAC 0000 (8.19),
- the classic test IMEI 490154203237518 as F-padded BCD (8.20) and its
IMEISV (8.74),
- language 'en' (8.45), Timing Advance (2E/AE), E-UTRAN (8.61; UTRAN +
E-UTRAN for the multiple-technology qualifier 0E),
- automatic search mode (8.75), a full battery (8.76).
An override set through /api/pli-dict wins, and clearing a field restores
its default (the PWA's Save re-renders the effective values; the GET returns
the override or the default for the full qualifier set). The date/time
qualifier stays empty so the host clock keeps answering it; the
3GPP/3GPP2-specific qualifiers (NMR, ESN, MEID, CSG/HNB, WLAN, slices,
CAG, ...) stay empty - the ETSI specs define no contents for them, and a
fabricated object would mislead the application more than no answer.
Verifying the values against the pinned tag table exposed pre-existing bugs
in the PWA's PLI inline codecs, fixed here:
- Timing Advance used C6 (a 3GPP2 tag) -> 2E/AE;
- IMEISV used 94 (the IMEI tag) -> 62/E2;
- Network Search Mode used AD (the Language tag) -> 65/E5, and its labels
were swapped ('00' Manual / '01' Automatic per 8.75);
- the battery field was a percentage where 8.76 is an enum ('00' very low
... '04' full, 'FE'/'FF');
- encImeiPli padded the odd 15th digit with 0 instead of F (TS 124 008).
Docs: help EN/RU, api.md, the guide. Tests: the defaults + the resolution
order (Python), the HTTP endpoint's defaults/override/clearing, the codec
tags/labels and a server-defaults drift guard (Node). 846 frontend +
829 Python; sw cache simple-v414.
A suite may carry `start_reset` (store schema v2, default false; the editor's
"Start with card reset" checkbox): before the run, the server physically
resets the card and runs the full re-initialization - `_card_reset_reinit`
(extracted from the eSIM profile-switch reinit) does reset_card() + equip +
_apply_equipped_card + the shell registration probe.
The reset runs in _test_suite_run_request before the preset/ADM checks:
- the run's preset must still match the post-reset ICCID (a mismatch refuses
the start with a re-select hint),
- a require_adm suite re-verifies the ADM for the new card session (the
latch is per session).
The suite report carries start_reset + reset {ok, iccid, ms}; the PWA shows
"started with a card reset" in the report header and a RESET badge in the
suite list. Mid-run resets stay fatal.
Documented caveat: the init drain consumes the card's initial proactive
commands, so a script cannot catch the first command after the reset (they
stay in the proactive log).
Docs: test-scripts.md, api.md, help EN/RU, repo AGENTS.
Tests: 843 frontend + 827 Python; sw cache simple-v413.
The review of the v3.22.7 fix asked when an expect can raise after its
FETCH: the realistic case is the scripted TERMINAL RESPONSE encoding -
_encode_text raises UnicodeEncodeError when respond.text has a non-GSM
character while dcs is 00 (the default) or 04, i.e. after the command was
fetched and with the card's proactive session left unanswered. The other
candidates are guarded (the TPDU/PoR/file-list parsers, the PLI dictionary)
or need the card to be gone (a transport failure between the FETCH and the
TR).
normalise_respond now runs the same coding the TR builder uses and refuses
the script at load with an actionable message ("use dcs 08 (UCS2) for
non-GSM characters"), so the failure happens at save/run start, before any
FETCH; UCS2, ASCII and the empty answer are unaffected.
Also from the review: the guide's run-log line now documents the v3.22.7
format (TEST-RUN step N failed (<level>): ...), and the "unguarded item
text decode" observation proved a non-issue - _decode_stk_text is internally
tolerant (odd UCS2 / unknown prefixes fall back without raising), so the
item parsers are left alone.
Tests: a non-GSM text with the default dcs and with 04 is refused (the
message names dcs 08), UCS2/ASCII/empty pass, and a whole script is refused
with the same message. 843 frontend + 822 Python; sw cache simple-v412.
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.
Findings from the review of the v3.22.5 router:
- the Card reader's pills (file manager / pySim command line / raw APDU)
join the router - the one view group the sweep missed: _pysimSub feeds the
state, pysimSwitchSubtab records, navCurrent/navApply know the tab;
- the nested parser switch (cApduSwitchSubtab('parse'|'response'), used by
the send-to-parser jump) sets _parserSub before recursing, so the
recursion's own parserSwitchSubtab renders the target and the two records
collapse into one history entry instead of two;
- navApply catches a failing replay and falls back to the default tab (the
console carries the failure) instead of letting the exception escape the
popstate handler with a stale _navState;
- the cards editor's recorded preset id comes from _cardsEditId (set by
cardsEdit, cleared by cardsNew/cardsCancelEdit/cardsRemove) instead of
cards[_cardsEditIdx], so a store refetch that shifts the array can no
longer record the wrong preset;
- nav.test.js: pysimSwitchSubtab added to the hook list, a navCurrent
state-building test (all tab branches), the apply-side assertions and the
single-entry parser check.
Tests: 843 frontend + 819 Python; sw cache simple-v410.
The browser Back button now walks the app's own view stack:
- a small state object {tab, sub, parser, view, listtab, preset, suite,
script, item} is recorded on every view switch (navRecord/navCurrent/
navStateKey) via history.pushState - session-only, the URL never changes
(pushState throwing on file:// degrades to no history);
- popstate replays through the same switch functions under a _navApplying
guard (they re-render and re-translate): the seven tabs, every pill set
(SCP80 / Remote APDU + parser / Simulator / SCP81), the Cards list/editor
(preset id), the test-suite views (suite/script ids) and the Profiler
views/list tabs (profile/snapshot ids); unresolvable ids fall back to the
area's default view;
- the in-app Cancel/Back buttons (cardsCancelEdit, testScriptBack,
testBackToSuites, profilerBackToList, profilerSaveProfile) call
navReplace() so their entry is replaced instead of stacked, and the
initial entry is seeded with replaceState so the first Back returns to
the initial view.
The reported untranslated suite Run button was the tip of a larger gap: the
sweep found 21 t() keys without a Russian entry (Run/Suite/Move/Copy/Warning/
Text, the respond-result names, the event-data errors, the CAP tooltip, ...)
- all added, and html.test.js now asserts every t('...') literal and every
data-l10n/data-l10n-title attribute has a LANG_RU entry.
Tests: nav.test.js (the recording/replace hooks, the state-key
canonicalisation, the router core), the i18n completeness test, the
extracted-function stubs in the cards/phone/profiler tests.
842 frontend + 819 Python; sw cache simple-v409.
The runner's unexpected-pending-command guard fired for any non-expect step,
so a proactive-drain after a step that answered 91XX failed with
"unexpected proactive command pending (SW 91XX) - add an expect step or a
status action" - contradicting the drain's own doc ("consume whatever the
card announces") and the v3.22.2 RAM recipe ("an expect or a proactive-drain").
The guard now exempts a proactive-drain step: its STATUS poll re-announces
the unfetched command, fetches and answers it, and the drain's final-SW and
pending handling is unchanged (a still-pending card is handed to the next
step, an idle card passes). No pending-length plumbing: STATUS-first keeps
the drain's attempts/log semantics and tolerates a card that dropped the
command. The guard note now names the drain instead of the misleading
"status action" (a status action in that position also trips the guard).
Docs: the drain section in test-scripts.md, api.md's action list and the
help EN/RU drain bullet. Tests: 839 frontend + 819 Python; sw cache
simple-v408.
Three test-management issues:
- an expectation whose command type did not match the pending command
hard-coded level 'error', so on_fail "warning" tolerated "no command
pending" but not "a different command is pending". The check now honours
the step's on_fail - the tolerant form for "the applet either asks X or
does Y" (the mismatched command is still fetched and answered).
- moving a script between suites could answer 500 and leave two owners:
_test_script_move detached only from the suite named by the script's
suite_id and appended to the target unconditionally, so a duplicate
listing or a stale suite_id (a hand edit, an interrupted move) collided
on the attach. It now detaches from every suite that lists the script,
takes the on_fail from the explicit owner (suite_id if that suite lists
it, else the first lister), updates the script first (a refused
validation changes nothing) and re-attaches with a prune; the same-suite
no-op only applies when the target really is the only lister.
- reconcile_scripts_to_suites() now prunes duplicate listings to the
explicit owner (it only adopted before), so those states self-heal at
startup and after a bundle import; the "Imported scripts" suite is only
created when there are orphans.
Docs: the bundle-import note (importing into the source store duplicates
the suite and its scripts - the round-trip is not idempotent) and the
expectation on_fail clarification.
Tests: 839 frontend + 818 Python; sw cache simple-v407.
Two reported gaps:
- a scripted PROVIDE LOCAL INFORMATION expectation answered with an empty
TR - the PLI dictionary (and the host clock for the date/time qualifier)
was silently inert during runs while the poll path used it all along, so
an applet waiting for its data stalled behind a passing expectation. A
POLL INTERVAL expectation likewise never echoed the Duration (6.8.4).
testscript.build_tr() gained `answer_tlvs` (inserted after the Result per
the 6.8.0 order, before text/item/raw) and the shared
_test_terminal_response() wires the expect step, the proactive-drain
action and the run cleanup to it; the script's `raw` overrides, and a
non-success result gets no data object.
- a GP INSTALL/LOAD/DELETE step always failed the default exact-9000 check
although the envelope SW 91XX is the successful answer (the re-announced
SET UP MENU; XX is the pending command's length, never pinnable).
Documented rather than loosening the default: a new "Driving a RAM install
from a script" subsection in docs/test-scripts.md (the 9??? mask + por
any, the per-step pending rule - a proactive-drain/expect after every
91XX step), a sentence at the sw-default bullet, and an scp80
default-pinning test.
Docs: api.md's respond clause, help EN/RU's TR Config note.
Tests: 839 frontend + 812 Python; sw cache simple-v406.
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.
A suite is the root object: every script belongs to exactly one suite, the
suite holds the ordered member list by reference - the optional setup first,
the members, the optional teardown last - and runs them in one card session,
without resets and without resume.
- test_suites.py: the suite store (test_suites.json, --test-suites), validated
and atomic like the script store: roles normalised (setup first, teardown
last, at most one of each, a script appears once per suite), per-member
on_fail stop/continue and the suite-level require_adm.
- test_scripts.py schema v2: suite_id/require_adm; list_by_suite()/orphans();
the import reports the id_map a bundle import remaps with. v3.21.0 scripts
without a suite are attached once to an auto-created "Imported scripts"
suite (migrate_scripts_to_suites, called from __main__).
- API: /api/test/suites (+ update|delete|import - delete refused while
non-empty; the bundle import imports the scripts first, remaps the suite
references and skips unresolvable ones); the scripts endpoints gain
suite_id/require_adm plus move|copy (delete detaches from the suite first;
a suite update refuses a membership change).
- runner: /api/test/run {suite_id} resolves the members and the preset, runs
the ADM gate, then setup -> members (per-member on_fail) -> teardown. The
teardown always runs on a stop or a failure (skipped only when the card is
gone; it clears the stop flag for its duration); a card reset stops the
suite, skips the remaining members and is flagged. Members' live steps land
in the shared snapshot, their records (status/steps/note/log excerpt/
counters) in _TEST_RUN['suite']; the summary carries the counts, wall time,
preset, the SCP80 counters before/after per keyset and the session flags.
_test_log buffers the semantic lines (bounded) for the report's per-member
excerpts.
- ADM gate (_ensure_adm): a suite/script require_adm verifies the matched
preset's key once per card session (the /api/verify-adm badge sets the same
latch); a failure refuses the start, stamps the session (no automatic
retry) and needs a manual verify.
- PWA: the Test script pill becomes four views - the suite list (default),
the suite editor (the script list: role badges, on_fail, up/down, per-script
Run/Edit/Move/Copy/Delete, Add script with the templates), the script editor
(the step list; a single run shows its result under the steps and is not
preserved) and the suite report (per-member blocks with the step table,
notes and run log; summary; export JSON/Markdown). Suite bundles export/
import; the legacy script export imports into "Imported scripts". EN/RU.
- tests: the suite store/migration, the runner orchestration (order, on_fail,
setup failure, stop, card reset, teardown, per-member logs) and the ADM gate
(Python); the suite HTTP paths; the PWA report/Markdown/wiring (Node).
Tests: 833 frontend + 794 Python; sw cache simple-v397.
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.
The counter an operation consumed was re-entered by the PWA on every send
(the form field was filled from the preset and posted back), so a stale tab
could send a value the card rejects (cntr_low), and typing in the field
silently wrote the preset. Now the store is the single source of truth and
the form field is a per-send override (Option B).
- _preset_counter_seed: an explicit `cntr` in the request wins (a hand send,
the counter probe or a pre-built packet may deliberately pick one);
otherwise the preset store's value for the packet's keyset. Wired into
/api/send-ota (the APDU path; the pre-built `sp` path keeps the caller's
bookkeeping value - the packet carries its own counter), /api/ram-install,
/api/ram-install-app, /api/cap-compat, /api/tar-probe and /api/sp-verify
(the reference must use the counter the JS packet was built with).
- the send-ota response echoes the effective `cntr` (a caller that omitted
one learns what was used) and the OTA SEND log line prints it.
- persistence is unchanged and monotonic (presets.set_counter refuses
backwards writes), pinned by tests: an explicit lower counter is used for
the packet but never lowers the store, a higher accepted one is saved.
- PWA: the Secured packet counter field starts empty with the preset's next
value as its placeholder (auto: <cntr>); typing affects that send only and
is never written to the preset (spCntrSyncPreset is gone), spParams /
getRamSpParams omit an empty counter, the RAM/send flows sync only the
local preset copy (forward-only) instead of filling the field, the +/-
buttons start from the effective value, and the preview/verify packet is
built from the effective counter (genSp). Placeholder translated EN/RU.
- the runner and /api/counter-probe keep their existing seeding (the probe
already starts at the failed value and walks above it).
- docs: api.md's optional-`cntr` contract + seeding notes on every endpoint,
help EN/RU 3.1. Tests: 822 frontend + 734 Python; sw cache simple-v393.
The script author read por.sw as the envelope SW, so a por.sw expectation
on an applet's own TAR failed with (none).
- docs/test-scripts.md: a 'The three status words (envelope SW / PoR
status / R-APDU SW)' table in the authoring section - where each one
comes from, the script field and the report row - plus a cross-reference
from the por check paragraph; help EN/RU 8.9 points at it.
- a failed por.sw check on a response_type: 'raw' decode carries the check
detail 'the response is application data - an applet's own TAR has no
R-APDU SW; assert it with data' (the PWA renders check details).
- the runner's log prints APP DATA=<hex> (no R-APDU SW) for application
responses instead of the misleading R-APDU SW=(none) data=...; the
compact/scripting lines are unchanged.
- the step editor's custom-PoR block gets a one-line hint about the same.
Tests: 821 frontend + 730 Python; sw cache simple-v391.
pySim's CompactRemoteResp was applied to every response packet, so a
third-party applet's own TAR response parsed as count=128/SW=0110/data=''
(live 2026-10-04: the alfa-dsa applet's `80 01 10`) - a fabricated status
word and a lost response.
_decode_por now takes the plaintext command script's length (cmd_len): the
compact structure's command count cannot exceed it (TS 102 226 5.1.2
Table 5.1 - each command needs at least one byte), so a larger count is
classified `response_type: 'raw'`, the secured data becomes
`decoded.last_response_data` (a por.data check matches it) and no SW is
decoded; `secured_data` is exposed in every case.
Callers pass cmd_len where they know the plaintext: the test runner's scp80
step (None for a pre-built packet; carried in ctx['last_scp80'] for the
submit-PoR path), /api/send-ota (both paths), the RAM step sender and the
counter probe; a caller without it keeps the previous parse.
Docs: test-scripts.md and api.md describe the raw fallback.
Tests: 821 frontend + 729 Python; sw cache simple-v390.
The test scripts get the pieces an applet test needed:
- scp80 check.por accepts an object {status?, sw?, data?} asserting the
decoded PoR of this very exchange (inline or via SEND SHORT MESSAGE);
the evaluation is shared with the expect-step por check
(_por_check_results). The editor's PoR select gains 'custom' with the
three fields (an empty object refuses on save).
- a new alpha content check reads the command's Alpha identifier (05/85,
Annex A coded) - the text check stays bound to the 8D Text string, so a
command carrying both keeps them distinct.
- a new sms content check asserts a SEND SHORT MESSAGE's submit TPDU:
DA / PID / DCS / UDL / UD (the parsed fields are logged and reported;
an applet's own SMS, not the OTA PoR the por check decodes).
- scp80.params.format wraps the C-APDU in the TS 102 226 5.2.1 Command
Scripting template (expanded = AA..., expanded-ae = AE 80...00 00); the
log shows WRAPPED=, the report C-APDU=<effective> format=<fmt>.
- docs: Worksheet 1 addresses an RFM TAR (B00000/B00001/B00010 compact,
B00120/B00140/B00130 expanded) with the own-TAR and MSL/spi1 notes, and
file-write is documented as a direct APDU that does not fire
EVENT_REMOTE_FILE_UPDATE.
Tests: 819 frontend + 724 Python; sw cache simple-v387.
The review of v3.18.3 found the event steps logging twice: the shared sender
writes its generic `ENVELOPE(Event Download): type=... data=...` /
`ENVELOPE SW: ...` pair, and the runner's step-annotated line repeated the
same data. `_send_event_download` gained `log=True`; the runner's envelope
and event branches pass `log=False`, so an event step logs exactly one line -
the annotated one, which already carries the step number, the event name,
the effective source and the response SW. All other callers keep their
output unchanged (netsim, the interactive event form, the BIP/timer paths).
The review's test-pinning gaps are closed:
- the shared sender's per-segment answer line
(`OTA SEND: ENVELOPE i/N -> <SW>`) is asserted in the multi-segment test;
- the scp80 step's report `sent` C-APDU/SP is asserted in the run-log test;
- the single-ENVELOPE-line behaviour (and the source override) is asserted
in the event-action test.
Tests: 816 frontend + 718 Python; sw cache simple-v386.
Verifying a script from the log was impossible: the runner wrote no card
lines, SCP80's plaintext C-APDU / secured packet / PoR landed only in the
report, and a menu selection logged nothing (the interactive path showed just
"STATUS poll (menu-select 9000)" / "STATUS -> 9000").
Every step now writes a `TEST-RUN step N:` line family to stderr, request
before response:
- generic actions: the APDU/raw event data sent and the SW/RESP received
(`APDU TX=...`, `ENVELOPE(Event Download) ... data=...`, `READ/UPDATE ... -> SW=...`);
- SCP80: `C-APDU=` (or `SP=`), `SECURED=<hex> (N B) TAR=... SPI1=... SPI2=... CNTR=...`,
the response SW, and the PoR - `PoR[inline] status=... TAR=... CNTR=... raw=...`
plus `R-APDU SW=... data=...` (with the bad-format note);
- expectations: `FETCH=<apdu> -> SW=... <raw>`, `CMD 0x.. <NAME> qual=..`,
`TR=<apdu> -> SW=...`, and `PoR[sms-submit] ...` + `R-APDU` for the SEND
SHORT MESSAGE transport;
- status: `STATUS 1/N -> <sw>` per attempt;
- menu selection: `MENU-SELECT ENVELOPE=<apdu> item=<id> ('<text>') -> SW=...`
(both the runner action and /api/menu-select; the old STATUS lines stay as
context).
The shared secured-packet sender logs each segment's answer
(`OTA SEND: ENVELOPE i/N -> <SW>`), the scp80 step's report `sent` now carries
the C-APDU/SP hex, and `--apdu-trace` remains the raw transport view.
help EN/RU and docs/test-scripts.md gained the "reading the run log" note.
Tests: 816 frontend + 718 Python; sw cache simple-v385.
The four findings from the review of the 3.15-3.18 batch:
- The event step's `_eventFieldsError` flag is cleared at the start of each
collect: fixing malformed JSON used to leave the save refused until the
editor was closed and reopened. Regression test covers invalid -> fixed.
- The script store keeps an entry that no longer validates on load (a
tightened engine rule or a hand edit): it is served, fails at run start with
the validator's message and can be fixed in the editor, instead of being
dropped from memory and erased by the next save. Mutation paths still
refuse it. (The presets store already had this behaviour.)
- Writebacks are serialized per script (`_testWriteChains`): two quick saves
of an unsaved draft no longer create two server-side scripts - the window
could be minutes while a RAM operation holds the card lock. Regression
test asserts one create + one update.
- Deleting a script cancels its pending debounced save and marks it deleted,
so a queued writeback never resurrects it; a create already in flight is
undone with a delete. Regression test covers the pending-save case.
- The event vectors moved to a shared fixture
(frontend/tests/event_vectors.json) asserted by BOTH suites - a one-sided
builder change now fails the other suite instead of drifting silently.
Tests: 816 frontend + 716 Python; sw cache simple-v383.
The events without a semantic builder (MT call, IMS registration, slices, ...)
are network-sourced, but the raw `envelope` action had no `src` override - the
only way to express them was the semantic set. `envelope` now accepts
`src` (82 terminal / 83 network) exactly like the `event` action; the step
editor exposes it and the authoring guide gets the row.
Also: the guide marks `por` as SCP80-only (the normaliser refuses it
elsewhere) and the semantic-events section names the shared src override.
Tests: 813 frontend + 717 Python; sw cache simple-v382.
The other-applet batch: a test script can send the common event downloads
semantically instead of raw hex.
- events.py: server-side mirrors of the PWA's event forms for Location status
(0x03), Access technology change (0x0B), Network rejection (0x12 - the
LA/RA/TA object selected by the registration group, the extended EMM cause
and the extended information object) and Data connection status change
(0x1D - status/type/TI plus the optional cause, host-clock 8.39 date-time,
location information, access technology, APN and PDP/PDN/PDU type). Fields
are validated before the run (a bad value is a script error, not a step
failure); `src` overrides the device-identities source (default terminal,
matching the Phone forms).
- The engine gains the `event` action kind; the runner builds and sends it.
The PWA's action-kind list stays in sync (drift-guarded - the guard itself
caught its own regex bug on the first run).
- Byte parity: the vectors are generated from the PWA's own builders and
pinned in tests/test_events.py; the frontend suites assert the same values.
PLMN and SCTS helpers are mirrored too.
- The Phone tab's event form shows the built "Event data (hex)" (updated as
the fields change), so every event without a builder can be used through
the raw `envelope` action.
- The test-step editor offers the event select + a JSON fields textarea;
malformed JSON refuses the save with a clear message.
- docs/test-scripts.md (the semantic-event table, an event example, the
format stamp), docs/api.md and help EN/RU updated.
Tests: 812 frontend + 717 Python; sw cache simple-v381.
The applet-testing batch agreed after v3.15/3.16:
- menu-select by text: the step names the item by `text` (+ `mode`
exact/contains, `case_sensitive`) instead of the install-dependent id
(both or neither is refused); the runner resolves it against the card's
cached menu and raises a step error naming the available items when nothing
(or several items) matches. The cache is refreshed on every SET UP MENU
fetch - the proactive chain and the expect step - so an applet install (new
items/ids after its REFRESH) no longer leaves a stale menu behind. The
step editor offers the id or the text + mode and the client-side pre-check
bounds them.
- TERMINAL RESPONSE object order: both builders emitted the Item identifier
before the Result (and the scripted builder its Text string before it too);
TS 102 223 6.8.0 mandates Command details, Device identities, Result,
Duration, Text string, Item identifier. `testscript.build_tr` and
`_menu_send_response` now follow it; `respond.raw` is documented as
appended after the standard objects, and the exact-byte tests pin the order.
- Text codings: the scripted response codes the GSM default alphabet via the
GSM 03.38 codec ('00' packed, '04' 8-bit) instead of latin-1, keeps '08'
UCS2 and sends the spec's null text string for an empty answer; the packing
helper is shared with the interactive path (`testscript.pack_gsm7`). A
UCS2 GET INPUT answer is documented and tested.
- docs/test-scripts.md gained the handoff brief for the applet agent: the
value-collection table, fill-in worksheets for the two applet scenarios, a
menu-by-text example, the validation checklist and a format stamp; help
EN/RU and docs/api.md updated.
Tests: 809 frontend + 705 Python; sw cache simple-v380.
The scripts lived in the browser's localStorage although only the local
server can run them. The new test_scripts.py store
(~/.pysim-simple-server/test_scripts.json, --test-scripts PATH) sits next to
the card presets: stable uuids and timestamps, atomic writes, and every
mutation is validated with the runner's own engine (the submitted shape is
kept - the normaliser's output is not re-normalisable: it fills defaults
like an 'any' PoR check that the check normaliser then rejects), so a stored
script cannot fail at run start for a validation reason. Import accepts the
old localStorage export as-is (export/import remains the migration path; the
PWA import also takes an exported array).
- /api/test/scripts (GET card-free: path/count + scripts), POST create,
/update, /delete and /import serve the store; the endpoints answer 503
without a store and 500 on a write failure, like the preset endpoints.
- /api/test/run takes a stored script_id (resolved when no inline script is
sent); the PWA sends the edited copy inline plus the id for the report,
and the run snapshot carries script_id.
- The PWA's Test script pill loads from the store, writes through on edits
(debounced name, immediate step/new/copy/delete), reports load/save/delete
failures inline, and exports the portable {name, steps} shape.
Tests: 808 frontend + 701 Python; sw cache simple-v379.
Three gaps blocked SCP80 applet testing from the PWA:
- testRunPreset() still marshalled the pre-v3.8 flat preset fields
(kic/kid/keys/counter/tar/spi1/spi2, all empty on a stored preset), so
every SCP80 run failed with "SCP80 preset is incomplete: no keyset is
defined". The PWA sends the matched preset's id now and the server
resolves it from the store (_test_run_preset_from_body; the inline
`preset` body stays for API callers), so a run always uses the stored
keysets, TARs and counters. Regression tests on both sides.
- testscript._normalise_params dropped the scp80 step's `kvn` (the editor
exposed it and the runner and validation consumed it, so scripts
silently used the preset's first keyset). kvn is validated (1-15) and
passed through; the client-side problem check bounds it too.
- The PoR delivered as a SEND SHORT MESSAGE (PoR-in-submit, SPI2 bit 0x20)
is decoded for the expect step that fetches it: the new `por` content
check asserts the response status / R-APDU SW / response data, decoded
with the preceding scp80 step's context (concatenated SMS accumulate
across expectations). The inline transport stays on the action
(check.por / check.data).
- The new `files` content check asserts the File List of a REFRESH
(TS 102 223 8.18, tag 12/92) - e.g. qualifier 00 = NAA initialization
with full file change notification - as an order-insensitive set of FID
paths.
The step editor exposes both checks; help EN/RU, docs/api.md and the new
authoring reference docs/test-scripts.md document the format. Tests: 803
frontend + 680 Python; sw cache simple-v378.
The ladder only advances on cntr_low; any other verdict stops the search.
A stop on a non-por_ok verdict after at least one cntr_low keeps the value
anyway - the earlier rejection proves it is above the card's counter - but
the response reports success: false with counter_saved, the verdict and the
saved counter ("something is likely wrong with the security settings");
the PWA shows it amber. Without a prior cntr_low nothing is saved, and a
missing PoR stays an error. actual_response_sms_submit/ussd count as
accepted.
Docs: docs/api.md, repo AGENTS. sw cache simple-v377.
Tests: 672 Python + 798 frontend green.