Live log: a card was pulled while the server started and the TERMINAL
PROFILE send raised CardConnectionException 0x80100016
(SCARD_E_NOT_TRANSACTED). The init's except caught it (no crash), but
the code then continued with the card object it had already built:
card_present and _CARD_CONNECTED became True - a phantom equipped card -
and the auto-equip's _CARD_CONNECTED early return then skipped every
later insertion, leaving only a manual Equip or a restart.
A PC/SC error in the startup init now prints one line (a traceback is
noise for a normal removal), clears the locals that feed those flags and
calls _clear_app_card_state(app); the presence monitor equips the next
card. Non-PC/SC errors keep their traceback.
Pinned in tests/test_startup_cardless.py (the branch and the flag
sources). 1016 Python + 953 frontend green; simple-v463.
v3.32.2 fixed the endless re-arm, but on Windows the first attempt
still failed on the old link (seen in the confirmation log: the
"self._con.connect()" traceback) and only the retry recovered - a failed
init, a 2-4 s delay and a traceback in the log.
The monitor's insert path now marks the transport stale under
_CARD_LOCK, so the first equip attempt rebuilds the PC/SC link. It also
covers a link created at a cardless startup (never connected). The
rebuild still happens in the equip worker, never in the pyscard monitor
thread, and _ensure_transport releases the old link first.
Docs: the READMEs' troubleshooting bullet.
Tests: an insertion marks the transport for the rebuild.
1014 Python + 953 frontend green; simple-v462.
After a card swap (or the first insertion after startup) on Windows the
auto-equip looped forever: "card inserted, initializing" / "card gone
during initialization", with no "retry 2/3" lines.
An equip with the card still present produced no card (app.card None),
and that branch reported "card gone" and returned True - a no-op success
- so the three retries never ran and the watchdog re-armed every ~5 s.
- The card-gone no-op is now only for a real removal (card_present
False); a card-present init that produced no card raises a failure.
- Any failed attempt marks the transport stale, so the next attempt
rebuilds the PC/SC link (a Windows swap can leave the old link unable
to connect; _ensure_transport releases the old link before building
the new one, so the 2026-09-24 stale-handle bug cannot recur).
- The error pySim swallowed (cmd2) is printed as "AUTO-EQUIP: init
output:" with its traceback - the failure is diagnosable from the log.
- Docs: the READMEs' troubleshooting bullet.
- Version strings + the PWA cache bumped together (simple-v460).
Tests: a card-present init without a card is a failure, a mid-init
removal stays a silent no-op, any failure marks the transport, the
stale-handle traceback reaches the log, and an end-to-end retry: a
failed init, the old link released, a fresh one built, the retry
succeeds. 1012 Python + 953 frontend green.
The import created presets with the store's default counter (1). Cards
whose counters are already past it had to be edited preset by preset
afterwards.
- The outfile preview form gains a "Start counter value" field (default
1, the store's value). The value is written into every created keyset
of every imported card - parse_outfile's `start_counter`, 1-10 hex
digits, whitespace stripped and uppercased; a bad value raises and the
endpoint answers 400. Without it the store's default still applies
(the field is omitted), so the API keeps its old behaviour.
- The preview shows the counter; an import stores the protocol's
10-digit form (counter_hex).
- Docs: api.md (the field), help EN/RU (the form).
- Version strings + the PWA cache bumped together (simple-v459).
Tests: the parser (every keyset, normalisation, unset, the bad values),
the endpoint (the preview's keysets, the 400, the stored padded form),
the PWA (the field's default, the controls' normalisation, both bodies).
1008 Python + 953 frontend green.
Reinstalling the package was slow because setup rebuilds pysim's
prerequisites (pyscard compiles from source on Linux), while a clone
updates with a plain `git pull`.
- tools/update.py (+ update.sh / update.bat in the package, and the
release workflow shipping them) updates a packaged install in place:
it downloads the latest release package, extracts it (zip-slip guard,
permission bits restored - zipfile drops them), replaces the app files
over the app directory and re-installs the app into the existing
.venv. The .venv and simple_lastrun.log are not in the package, so
pysim and its prerequisites survive: seconds, not a rebuild.
--check / --version / --zip (offline) / --no-pip / --force; the same
version is a no-op.
- CHANGELOG.md (Keep a Changelog, from the first packaged release;
earlier history is the git log). The release workflow now uses the
version's CHANGELOG section as the release body
(tools/changelog_section.py, falling back to generated notes).
- The READMEs document the update path and the `git clone --depth 1`
option for the clone path; setup prints the update hint.
- Version strings + the PWA cache bumped together (simple-v458).
Verified: 1007 Python + 953 frontend; the release-package test covers
the new files; the updater was exercised end to end (a v3.31.2 install
with a real venv updated to the 3.32.0 package: files replaced, the
venv marker kept, pip metadata 3.32.0, exec bits kept, a second run a
no-op, --check correct).
ssl's TLS-PSK callbacks (PskTlsServer) exist only since Python 3.13 - the
first CI run on 3.12 errored in all 20 PSK tests, and the docs confirm
"Added in version 3.13".
- PskTlsServer._make_context raises a clear RuntimeError naming the
requirement instead of AttributeError; a source pin keeps the guard.
- tests/test_scp81.py skips PskTlsServerTest where the callbacks are
missing (it runs on 3.13+), so contributors on 3.9-3.12 no longer see
20 failures.
- The READMEs state the requirement, and the Windows notes now strongly
recommend Python 3.13: pyscard has wheels up to 3.13, and on 3.14
pyscard and some pysim dependency wheels are unavailable and build
from source (MSVC). setup.bat and setup.sh print the same guidance.
- Version strings + the PWA cache bumped together (simple-v457).
998 Python + 953 frontend green.
The READMEs now point users at a release package instead of a clone; this
adds the machinery behind it.
- tools/build_release.py builds dist/simple-<version>.zip: the runtime
tree (the server package incl. its bundled MCC/MNC list, the PWA
runtime files, docs/, tools/, the setup/start scripts, pyproject,
requirements-pysim.txt, the READMEs, LICENSE and THIRD-PARTY-NOTICES)
under a single top-level directory, with no tests, no frontend
sources/build config, no bytecode and no git metadata. The version
comes from server.py; --expect-version guards a mislabeled tag.
- .github/workflows/release.yml runs both test suites, builds the
package and publishes it on a v* tag (workflow_dispatch for an
existing tag); a re-run uploads with --clobber. pysim is checked out
next to the repo for the Python suite and is NOT bundled.
- tests/test_release_package.py pins the package's shape: the runtime
files, the absence of the development files, and that sw.js's
precache list is fully covered - so a new asset cannot silently go
missing.
- dist/ is gitignored; the READMEs name the artifact.
Verified: 997 Python + 953 frontend green; the built zip (53 files,
~0.95 MB) was unpacked in a clean directory and its setup.sh installed it
into a fresh venv - the console script runs and the installed server
resolves web_dir to the unpacked frontend/ (index.html, sw.js,
uicc_files.json present). The workflow YAML parses; GitHub-side
execution is unverified until the first tag is pushed.
EF_PBR ('4F30', TS 31.102 4.4.2.1) lists the phonebook's files as
BER-TLVs, and those files are not in pySim's model - so they were
invisible in the file tree.
- The PWA gains the decoder (efDecPbr, registered as EF.PBR/4f30): the
'A8'/'A9'/'AA' type-1/2/3 containers, the 'C0'-'CC' kinds, the 02/03
value lengths (FID / FID+SFI) and the master EF (the first FID after
the first 'A8'), tolerant of FF padding, truncation, unknown tags and
the spec's vague 'AA' path form (kept raw + noted, never a throw).
- The server reads the three PBR copies once per card session
(best-effort, exactly like EF.ARR) into server.pbr and serves the raw
records from the card-free GET /api/pbr.
- The file tree is populated SERVER-SIDE (no frontend involvement): a
small Python parser reads the cached records and /api/tree appends the
referenced files as children of the DF that holds the PBR
({'name','fid','isDir':false,'pbr':true,'sfi'}, deduplicated against
the model), and /api/tree|select|read|write auto-allow the probe for
exactly those FIDs, so the injected nodes are clickable without any
PWA flag. The node-to-PBR match is by the requested node's normalised
path: a leading MF before an ADF segment is dropped and an ADF
addressed by AID (the Profiler's walk) folds onto its name, so both
DF_PHONEBOOK copies resolve to their own PBR; a full path never falls
back to another DF's copy, and an ambiguous bare 5F3A injects nothing.
- Drift guard: the JS and Python parsers share
frontend/tests/pbr_vectors.json, which includes the TS 31.102 Annex G
record (its printed A8 length is internally inconsistent - 10
primitives x 5 bytes = 0x32, not the printed 0x2D - so the corrected
length is used, noted in the case name).
Docs: api.md (/api/pbr, the tree children, the auto-probe, the AA path
form), the repo AGENTS. 953 frontend + 989 Python; sw simple-v455.
Under start.bat the server's stdout/stderr are pipes into PowerShell's
Tee-Object, so Python encodes with the ANSI code page instead of the
wide console API. cp1251 (Russian Windows) cannot encode the banner's
box-drawing rule (U+2500), and that print killed the server with
UnicodeEncodeError before it could listen.
- _guard_console_streams() (the first statement of main()) sets
errors="replace" on both streams: an unencodable character prints as
'?' and can never raise.
- _console_rule() picks the widest banner rule its stream can encode -
U+2500 where the code page has it (utf-8, cp437), ASCII dashes
otherwise (cp1251, ascii).
- The seven text-layer trace strings are ASCII now: the APDU arrows
(U+2192) and the INIT/CAP-COMPAT em dashes (U+2014 - cp437 cannot
encode them either), so the traces stay readable on any code page.
The raw os.write paths (_log_stdout, the APDU tracer) bypass the text
layer, cannot crash, and were left as-is.
Tests: tests/test_console_encoding.py - the strict-cp1251 control, the
rule's fallback/pretty cases, the guarded banner+traces, a stream
without reconfigure, the main() source pin, and an AST pin that no
print/stdout/stderr literal in the package is non-ASCII.
tests/test_startup_cardless.py's CARDLESS_LINE follows the new ASCII
line.
Verified with PYTHONUNBUFFERED=1 PYTHONIOENCODING=cp1251: the pre-fix
code died at the banner, the fixed code reaches the listening banner
and serves a command (CMD: -> OK, no encode errors).
930 frontend + 952 Python; sw cache simple-v453.
The format is positional: an empty value drops its token, so the parser
now requires every data row to have exactly the header's column count.
That catches an empty value anywhere - including a middle one, which
would otherwise shift the following columns (a missing ADM made the KIC
columns read the KIK values: valid hex, wrong preset) - and a KICn/KIDn
pair declared in the header must carry a usable hex value in every row
(a placeholder fails).
A violation makes the whole file invalid: the report's error names the
line, no presets are returned, and an import (mode) answers 400 - it is
never partially imported. The PWA disables the Import button while the
report carries an error and guards the call.
Unchanged per-row reports: a non-hex PIN/PUK/ADM/PSK value (dropped, the
card kept), an unsupported key size (that keyset skipped), a card whose
remaining keysets are unusable (skipped). All three real samples satisfy
the rule exactly (11/11, 18/18, 18/18).
Docs: help EN/RU, api.md, the repo AGENTS. Tests: the short/long row and
the placeholder-keyset file errors, the valid-file round trip, the
endpoint's refusal (nothing stored), the PWA's disabled button.
926 frontend + 945 Python; sw cache simple-v449.
The import applied one global default keyset to every TAR entry of every
imported card. The preview's TAR table (TAR + MSL, the three role rows +
"+ TAR") now has a keyset select per row, mirroring the preset editor: an
empty "— none —" plus the keysets found in the file with their KIc/KID,
the first preselected, and the editor's amber warning when a non-zero MSL
has no keyset. Each TAR's packets then use exactly the chosen keyset.
- PWA: the global "Default keyset for the TARs" select (and the request's
tar_keyset) is gone; cardsOutfileTarsFromDom/FromForm carry each row's
keyset; cardsOutfileKeysets() became cardsOutfileKeysetOptions() (the
union with the KIc/KID bytes for the labels).
- Server: a `tars` entry's own `keyset` (1-15; an unusable value is a 400)
wins, then the optional global `tar_keyset` (the API shorthand), then the
card's first keyset - parse_outfile no longer clobbers per-entry keysets
with the card-level fallback. presets.normalise_keyset_ref() is
module-level (one rule for the store and the parser).
- Docs: help EN/RU, api.md (the tars entry keyset + the global fallback),
the repo AGENTS. Tests: the per-entry/global/card fallbacks, the
bad-keyset 400, the options helper, the row select and round-trip, the
import body. 925 frontend + 944 Python; sw cache simple-v448.
The batch plan is a list of named steps ({name, apdu, delete}) now and
_ram_install_run generalized into _ram_plan_run; POST /api/ram-batch/start
dispatches on kind:
- install-cap (default; delete_package prepends a cascade DELETE on the
package AID - a leftover package blocks INSTALL [for load]),
- reinstall-app (_ram_reinstall_plan: DELETE the applet instance + INSTALL
[for install] from the loaded package - no .cap, no LOAD),
- delete (_ram_delete_plan: one GP DELETE, aid + target app/package ->
P2=00/P2=80).
A delete step tolerates 'not found' (6A88/6A82 - the object is already
gone, the goal is met); any other delete failure stops the chain with the
delete's reason. The policy serves all three kinds and the single
/api/ram-install-app, which now deletes the instance first (make
selectable stays one step); /api/ram-install takes delete_package too.
The status/summary carry the run's kind (the panel labels follow:
installed/deleted/reinstalled) and each attempt carries the last three
steps (steps + steps_total) for the panel's per-step detail. The PWA's
three batch entries are grouped at the end of the operation selector (an
optgroup); ramIsBatchOp() is the one source for "per-card batch".
Docs: help EN/RU, api.md, the repo AGENTS. Tests: the plan builders per
kind, the validations, the delete-package prepend, the not-found
tolerance, the kind dispatch/status/notice, the steps tail, the endpoint
steps, the PWA wiring. 925 frontend + 943 Python; sw cache simple-v447.
A card may deliver the response packet (the PoR) in the GET RESPONSE of
an ENVELOPE that answered 61xx and signal a proactive command pending in
the same answer (91xx - the CAT success state, not an error).
_send_envelope ran the chain and then cleared the response data
(`data, sw = '', '9000'`), so the operation reported no PoR although the
card had returned one (live 2026-10-14: a GP DELETE answered 6113, the
GET RESPONSE delivered the PoR with 9130, and the delete verdict showed
"ok (no PoR)").
The delivered bytes are kept now; the chain still runs and the exchange
reports 9000. Test: the delivered PoR survives the chain (verified to
fail on the old code). 917 frontend + 928 Python; sw cache simple-v444.
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 review of the connection-check fix found the second-order race it
opened: _apply_equipped_card sets _CARD_CONNECTED = True and bumps
card_session *before* it clears and reads the ICCID (and long before the
network state / TERMINAL PROFILE / drain finish). A worker poll landing
in that window saw connected + a fresh session + iccid = None and
consumed the card with a bogus 'no ICCID' skip - the card was never
installed. The `equipping` flag could not guard it either: the manual
/api/command equip path never sets it.
The fix marks the finished init instead of guessing:
- _apply_equipped_card sets `server.card_ready_session =
server.card_session` as its last statement;
- the startup init (which inlines the post-equip work and never runs
_apply_equipped_card) sets the same marker;
- the worker requires the marker to match the card session, so it acts
only on a fully initialized card - a genuinely unreadable ICCID still
acts and reports its skip, as designed.
Tests: a worker test that keeps the marker stale for a couple of polls
(no attempt - verified to fail without the guard), the marker pins for
both equip paths, and the fake server carries the marker (a real
attribute) instead of inventing state. The unused `server` parameter of
_ram_batch_start is gone, and the two comments that called this work a
"review" now say audit.
915 frontend + 924 Python; sw cache simple-v438 (unchanged).
The v3.26.0-v3.28.0 audit found the worker reading
getattr(server, 'connected', False) - an attribute the server object has
never had. The connection state is the module global _CARD_CONNECTED
(set by __main__ / _apply_equipped_card / _handle_card_disconnect and
read by /api/status the same way), so the check was always False and a
batch never acted on an equipped card: it would wait forever. The unit
test hid it - the fake server invented a `connected` attribute the real
object does not have.
- The worker reads _CARD_CONNECTED. The fake server carries only
attributes the real object has, the worker test patches
_CARD_CONNECTED (verified to fail on the old line), and a source pin
keeps the worker off server.connected.
- The write-only `thread` field of the batch state is gone (nothing read
it).
- The outfile endpoint refuses a non-list `tars` with 400 instead of
silently falling back to the role defaults.
915 frontend + 922 Python; sw cache simple-v438 (unchanged).
- A non-ValueError exception from the install run (a card removed
mid-chain, a transport error) was recorded as a 'skipped' attempt with
the error text - but the chain had started and died, which is a
failure. Only a ValueError (a request/eligibility error: the preset's
ISD keyset, the plan) is a skip now; anything else is a 'failed' record
carrying its error. The PWA's failed row falls back to that error when
there is no failing step / status word to show.
- The panel's Clear button is disabled while a batch is active (the
server refuses the clear with 409 anyway) - no pointless error alert.
- api.md and the repo AGENTS document the two outcomes. Tests: a
transport error mid-chain is a failure (and the card stays seen), the
plan's input validation (the block size, a missing cap_hex), the failed
row without a step, the shared ram_format field, and the Clear state.
915 frontend + 921 Python; sw cache simple-v438 (unchanged).
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.
- The endpoint validates algorithm16 (3des2/aes, case-insensitive) and mode
(merge/replace or omitted): a misspelled value answers 400 instead of
silently picking 3DES2 or turning an import into a dry run. The choices
come from outfile.ALGORITHM16_CHOICES, derived from the algorithm map, so
the validation and the parser cannot drift apart.
- A present but unusable PIN/PUK value is reported with its raw text: a value
that strips to nothing (e.g. 'abcd') used to vanish without a report line,
contradicting the "nothing is silently dropped" contract. A column absent
from a short row stays silent - it is not present.
- The column-line detection drops the arbitrary 8-character column-name cap:
a candidate header must carry ICCID plus at least one column the import
consumes (a fixed name or a numbered KIC/KID), so a long unknown column
name next to a real one parses while 'ICCID <value>' and
'ICCID CUSTOMER_NAME' config lines are still rejected. Both real samples
are unaffected.
- PWA: a chosen default keyset survives an empty or failed preview (it used
to reset to null, and the import silently fell back to the card's first
keyset); the select renders the preview's keysets plus a remembered value.
- PWA: the destructive Replace mode asks for confirmation first, matching the
other store-wiping actions.
- api.md documents both validations. Tests: the endpoint's 400s and the
replace import, the raw-value report, the header guards, the keyset
preservation and the declined confirmation. 906 frontend + 904 Python;
sw cache simple-v437.
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.
An MSL 00 TAR without a default keyset is a legitimate keyless entry, but
_resolve_operation_keyset left the request's KIc/KID empty and the RAM /
APDU builds failed with "invalid literal for int() with base 16: ''".
The resolution now fills the spec's no-security 00/00 KIc/KID form for a
preset entry without a keyset, and refuses a TAR outside the preset with
a hint (the old silent '25' default was arbitrary). The PWA's TAR change
resets the form the same way - a stale previous TAR's KIc/KID would
otherwise travel explicitly and build a non-keyless packet.
Also: the dead _preset_tar_keyset helper removed; tests for both keyless
paths. 892 frontend + 886 Python; sw cache simple-v435.
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.
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 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.
The live card references 6F06 from MF-tree files (MF/7F20/6F07 ->
8B 03 6F06 14), but the resolver compared only the path's first
segment: the reference fell back to the ADF.USIM copy instead of the
file's own DF copy (ISO 7816-4 5.4.3.3 / TS 102 221 9.2.7 walk-up) -
masked on this card only because all four copies are byte-identical.
- The server reads the DF.TELECOM and DF.GSM copies too (MF/7F10/6F06,
MF/7F20/6F06 - TS 51.011), best effort.
- fcpArrRules resolves by the file's DF chain (own DF first, then
walking up to MF); a reference with no copy in the chain resolves
only when a single candidate exists, otherwise it stays silent.
- The implicit form prefers the file's DF copy (6F06), walking up to
the MF copy (2F06).
Tests: the DF copy wins over the ADF copy; two candidates outside the
chain stay unresolved; a single one resolves; the implicit walk-up;
the server reads the DF-tree paths. 879 frontend + 858 Python;
sw cache simple-v424.
A card present when the server starts is initialized by the startup
path in __main__.py, which reads EF.ICCID and the network-state files
directly and never runs _apply_equipped_card - the only place the ARR
cache was read. The cache therefore stayed empty for the whole
session (/api/arr {ok: false}) and the file manager showed no access
rules until an explicit Equip.
The startup init now reads the ARR copies in the same CAT-free window
(best effort, right after the netstate read, skipped without a readable
ICCID like the other reads) and stores them as server.arr, with an
INIT: EF.ARR log line. A later equip still refreshes the cache through
_apply_equipped_card.
Test: the startup wiring (the read, the assignment and the import).
869 frontend + 857 Python; sw cache simple-v423.
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.
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.
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.
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.
Second review round on the unpushed v3.20.0-v3.22.0 batch:
- install parameters: a zero memory quota (nv_quota: 0 - the vendor
reference form's C7 02 0000 C8 02 0000) is composed instead of being
dropped as "unset" (only None/blank leaves the TLV out); a truncated EF
length in stk_params is refused instead of read as an empty EF.
- test suites: a suite whose member script is gone is repairable - the
update endpoint accepts pruning a dangling reference (a removal of a
live script stays refused; the PWA's row Delete prunes it), and
reconcile_scripts_to_suites() lists an orphan even when its suite_id
write is refused by the validator, so a stored script that no longer
validates stays visible in the editor and fails at run start with the
message instead of disappearing.
- engine/PWA: the proactive action refuses a check.data (its result data
is the drained-command list - the check was silently ignored); the
vestigial _testScriptDraft is gone; berLenStr emits the 2/3-byte BER
long form (a >255-byte C9 composes correctly).
- docs: api.md (the stop cleanup answers ok, never an automatic cancel;
the server persists the counters - the PWA does not write them back; the
suite bundle import always merges the scripts; the quota-0 note) and
test-scripts.md (the event action covers all modelled events; the
direct-create path needs a suite_id; the format history); help EN/RU
counter wording.
- tests: the zero quota / truncated EF, the prune path, the invalid-script
listing, the proactive data-check refusal, berLenStr and the >255-byte
C9. Tests: 839 frontend + 804 Python; sw cache simple-v403.
The suite/script ownership invariant could break silently: a suites import
with mode "replace" wiped the suites while their scripts stayed behind
(suite_id pointing at gone suites - invisible in the UI), a scripts import
with mode "replace" left every suite's member list dangling, and a script
whose suite_id disagreed with its listing suite stayed inconsistent.
- server: reconcile_scripts_to_suites() repairs the invariant - a script a
suite lists adopts that suite (the member list is authoritative), a script
no suite lists joins "Imported scripts"; migrate_scripts_to_suites()
delegates to it (the startup migration is the same pass) and the suites
import runs it after loading, so a "replace" adopts the wiped suites'
scripts instead of losing them.
- the scripts import refuses mode "replace" while any suite still lists
scripts (400 naming the dangling references; merge is the normal path).
- nits from the review: a legacy numeric `event` is no longer parsed as hex
in the step summary/error check (0x0A displayed as 0x10), and an integral
float memory quota (a JSON client's 200.0) is accepted.
- tests: the reconciliation (missing suite, listed-but-mismatched,
idempotent), the scripts-replace guard (refused/allowed), the
suites-replace adoption, the float quota, the numeric-event summary.
Tests: 838 frontend + 800 Python; sw cache simple-v402.
The test-script event action edited its fields as a JSON textarea and only
accepted the four events events.py builds; every other event needed the raw
envelope action with hand-copied hex. It now renders the same EVENT_FORMS
form the Phone tab's send dialog uses, for every modelled event (29; no
release gating - a script may target another card).
- PWA: the field renderer/collector is shared (eventFormFieldsHtml /
eventFormValues / eventFormPatternError / eventFormSrc / eventFieldGroupsUpdate);
the Phone tab keeps its ids, its release gating and its onchange updaters,
the step editor uses test-f-ev- ids, the generic group updater and a live
Event data (hex) preview with the one-ENVELOPE check. The event select
lists all EVENT_FORMS events (the numeric keys are decimal strings -
parseInt(k, 10)); the action stores the collected fields, the built data
and the event's source (fixed cfg.src or the cfg.srcField input - the
action never carried src before, so MT call / IMS registration / slices
could not be network-sourced). The JSON textarea is gone.
- engine: the `event` action keeps `fields` validation for the four
server-built events (a stray data hex is ignored) and accepts the
form-built `data` for every other event - hex-validated and checked
against events.EVENT_DATA_MAX (244, the one-ENVELOPE budget; server.py's
_EVENT_DATA_MAX now reads it from events.py). The runner builds the four
at run time (fresh host clock) and sends the stored data for the rest.
- tests: the engine's data path (accepted / missing / malformed / oversized,
the four still fields-built), the runner sending the data (fake scc), the
editor's collect/prefill/patterns/source (Node), the shared helpers.
Docs: test-scripts.md, api.md, help EN/RU. Tests: 838 frontend +
797 Python; sw cache simple-v401.
The v3.22.0 review found that a suite's SCP80 counter state was rebuilt from
the preset snapshot for every member: a second SCP80 script sent the counter
the first had consumed (the store persists the advance, the in-memory preset
copy does not move) and the card answered cntr_low. Reproduced with a
two-member suite (both packets carried CNTR=0000000010).
- runner: `_test_run_script` takes a `shared` counter state (created once per
suite, passed to every member; a single script passes None) and copies the
no-keyset counter back into it, so a suite behaves like one script for
counter bookkeeping. Pinned by a two-member test asserting the second
packet's counter is the first's + 1 and the final counters.
- `_test_drain_pending`: the unknown-length FETCH uses 0xff - `'%02x' % 0x100`
emitted an odd-length APDU (a malformed fetch) when a 9100 SW (length 0)
reached the drain.
- PWA: the suite save adopts the stored suite (the server normalises the
setup-first/teardown-last order); the role rows' up/down buttons are
disabled (only members move); deleting a suite cancels its pending name
debounce (no late 404); `refreshDynamicI18n` redraws the test views, so a
language switch updates the suites list/report immediately.
- tests: the cross-member counter test (Python) and the save-adoption /
move-guard / i18n wiring tests (Node).
Tests: 835 frontend + 795 Python; sw cache simple-v398.