Commit Graph

582 Commits

Author SHA1 Message Date
catarrh f2046d7f0a v3.25.5: the PLI dictionary override guard fix
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.
2026-10-11 16:34:08 +03:00
catarrh 2a58cfb235 v3.25.4: the per-operation Contains match for the profile access rules
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.
2026-10-11 16:18:15 +03:00
catarrh 1aa24507fc docs: the timer-expiration exception during test runs
The BIP events flow during a run (v3.25.3), but a TIMER MANAGEMENT timer
the card armed still does not fire then - the v3.25.1 guard retries it
every 5 s until the run ends.  Documented as a deliberate exception to
the "the card's own conversation continues" rule, in the authoring guide
and both AGENTS, to review later.

883 frontend + 873 Python (unchanged).
2026-10-11 15:55:08 +03:00
catarrh 8d39904d2c v3.25.3: the card's BIP commands during test runs
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.
2026-10-11 14:02:29 +03:00
catarrh 4a8f855cfc v3.25.2 review: the guide's stale TR example and the AGENTS wording
The authoring guide's run-log sample still showed the CR-set device
identities (TR=81030125008202...); the builders now emit 02 02, so the
example reads TR=81030125000202....  The AGENTS tag-variant bullet now
says "identical parsed values" - the header parser's returned
command-details tag follows the style by design.

883 frontend + 866 Python; sw cache simple-v426.
2026-10-11 13:39:07 +03:00
catarrh 547ea61708 v3.25.2: the TR tag-form alignment with TS 102 223 6.8.0/6.8.1
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.
2026-10-11 13:31:49 +03:00
catarrh beb001846f Remove the accidentally committed runtime log
simple_lastrun_fetch_loop.log (142 KB of live-loop debug output)
slipped into af00932 - the repo only ignored the exact name
simple_lastrun.log.  The file is removed from the index and .gitignore
now covers simple_lastrun*.log, so the variants stay local.

865 Python + 883 frontend (unchanged).
2026-10-11 12:31:02 +03:00
catarrh af00932aca v3.25.1: the run-ownership fix for the proactive auto-handling
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.
2026-10-11 12:23:12 +03:00
catarrh 87931afc41 docs review: the missing RU RAM operation rows
The README_RUS RAM table kept the English operation names, so the
insertion anchored on a Russian name matched nothing (and unlike the
other edits it had no assert) - the lead-in said "пять операций" while
the table still listed two.  Added the CAP compatibility test,
INSTALL [for install] (loaded package) and INSTALL [for make
selectable] rows with RU descriptions, matching the table's style.

883 frontend + 858 Python; sw cache simple-v424.
2026-10-11 11:13:24 +03:00
catarrh ad2ac1cfd7 docs: sync the help and READMEs with the recent UI
Audit of the UI additions against the documentation; fixed gaps:

- help §1.1: the browser Back/Forward view history (v3.22.5) was
  undocumented.
- help §8.9: the TERMINAL RESPONSE result select's spec-table grouping
  (v3.23.8) - the guide had it, the help did not.
- help §7: the snapshot comparison is byte-exact for the FCI and the
  EF.ARR contents and does not run the semantic access-rules aspect
  (that belongs to profiles) - now stated.
- help §3.2 + both READMEs: the two loaded-package RAM operations
  (INSTALL [for install] (loaded package), INSTALL [for make
  selectable]) were missing; the READMEs also lacked the CAP
  compatibility test and still said "two operations" (now five).
- READMEs: the file manager's referenced EF.ARR rules (v3.24.0) and the
  profile access-rules check/editor (v3.25.0) added.
- READMEs: the Cards table gained the PIN/PUK fields (v3.11.0) and the
  CAT release (v3.12.0).

883 frontend + 858 Python; sw cache simple-v424.
2026-10-11 00:27:26 +03:00
catarrh db78696865 v3.25.0 review: the access-rules open-state lifecycle
- profilerEdit resets the expanded-block map, so opening another profile
  (or a freshly scanned one) starts with every access-rules block
  collapsed instead of inheriting the previous profile's state.
- Deleting a rule re-keys the map (profilerArrShiftOpen): the entries
  below stay, the ones above shift down, so the expanded blocks stay
  with their rules.
- The declaration sits before the render function that reads it.
- Test fixture: a real command-description tag (0x85) instead of the
  RESIZE branch's 0x84.

Tests: the shift helper, the profilerEdit reset pin, the existing
editor tests.  883 frontend + 858 Python; sw cache simple-v424.
2026-10-11 00:17:36 +03:00
catarrh b00d533f03 v3.25.0 review: the collapsible access-rules block
The access-rules form is large for every file rule, so the block now
collapses to its header (a toggle carrying the rule count) and hides
entirely when None is selected - the header keeps the title and the
None/Check radios, so re-enabling is one click (Check also reopens the
block).  The expanded state is transient editor UI state (a module map
keyed by the rule index), kept across the editor's re-renders; a rule
without accessRules starts hidden and Check creates the empty block to
build rules in.

Tests: collapsed by default (header + count, no rows), expanded rows,
None and no-accessRules hidden, the enabled-but-empty hint, and the
radio driving the visibility.  882 frontend + 858 Python; sw cache
simple-v424.
2026-10-11 00:02:20 +03:00
catarrh c69a779ca4 v3.25.0 review: the structured access-rule editor
The access-rules block was read-only and its Check radio was disabled
without captured lines, so a hand-made rule could not define rules and
captured ones could not be adjusted.  It now edits the decoded rule's
structure:

- fcpArrRuleParse/fcpArrRuleEncode parse a canonical decoded line into
  the AM (bit mask, RESIZE FILE, INCREASE) + SC condition model and
  re-encode it byte-identically (round-trip tested); the template forms
  (OR/AND/NOT, SM CRTs, command descriptions) stay raw and removable.
- The editor renders each rule as operation checkboxes (or the command
  form) and condition selects (always / never / a key reference with
  its usage), with a preview line of the canonical string the check
  compares; + rule adds READ always; + condition / x adjust the rows.
- The None/Check radio no longer disables; a new rule can be built by
  hand and captured rules adjusted.  The stored format is unchanged
  (the canonical strings), so capture, the check, the set comparison
  and validation are untouched.

Tests: parse/encode round-trips (incl. raw forms), the mutators, the
editor render, plus the existing check tests.  882 frontend + 858
Python; sw cache simple-v424.
2026-10-10 23:55:07 +03:00
catarrh a37266f865 v3.25.0 review: the DF-tree ARR resolution walk-up
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.
2026-10-10 23:26:16 +03:00
catarrh 3bf005619e v3.25.0: the access-rules check in profiles
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.
2026-10-10 23:09:47 +03:00
catarrh 4d87094a98 v3.24.0 review: the ARR cache on the startup init path
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.
2026-10-10 22:36:02 +03:00
catarrh 3862ae6f4e v3.24.0 review: the ARR fetch retry and the DF-scope note
- pysimArrEnsure no longer caches a transient /api/arr fetch failure:
  the next file click retries instead of silently showing no ARR rules
  for the rest of the card session (a success is still cached per
  session, and a session change refetches).
- The help (EN/RU) and both AGENTS state the cache scope: only the MF
  (2F06) and ADF.USIM (6F06) ARR copies are read - a reference to
  another DF's copy is left unresolved.
- Test: the transient failure retries, a success is cached, a session
  change refetches.  869 frontend + 856 Python; sw cache simple-v423.
2026-10-10 22:26:22 +03:00
catarrh b6537bd54b v3.24.0: the EF.ARR access rules in the FCP view
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.
2026-10-10 00:30:45 +03:00
catarrh 9b4114012b v3.23.9 review: the counter-low exemption and the hint cleanup
Review fixes for the CAP compatibility test suggestion:

- A cntr_low rejection is exempt from both the failure hint and the
  one-click button: the card never executed the command (the packet's
  counter was rejected), so the counter box is the actionable path -
  the v3.6.58 rule that a low counter is never retried.
- The INSTALL [for load] advice names the cascade action: the package
  row's "Delete All" (a plain delete of a package with related objects
  can be refused), in EN and RU.
- The RU LOAD hint quotes the RU button label ("Все 0.0" rather than
  the English "Set all 0.0").
- Two orphaned LANG_RU entries from the replaced hint strings removed
  (the i18n test only checks the missing direction, so they survived).

Tests: the counter-low exemption, the cascade wording and the button
guard pin.  862 frontend + 842 Python; sw cache simple-v422.
2026-10-09 22:58:43 +03:00
catarrh 1473396e5b v3.23.9: the CAP compatibility test suggestion on a failed install
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.
2026-10-09 21:59:51 +03:00
catarrh dc8d71c6a7 v3.23.8 review: the mixed-case command lookup and the flat-list docs
Two findings from the review of 2b994a7 (and c9ab15c, reviewed clean):

- trCommandCode() uppercased the query but compared it with the raw
  CMD_NAMES values, so the one mixed-case entry ('End of the proactive UICC
  session', 0x81) never matched.  No visible effect today (0x81 has no table
  entry - it falls back to the flat list), but a future map entry for a
  mixed-case name would silently not group.  The comparison is now
  case-insensitive, pinned by a test.
- The docs now say that the commands the spec's tables omit (SEND SS / SEND
  USSD / SEND SHORT MESSAGE / GEOGRAPHICAL LOCATION REQUEST / End of the
  proactive UICC session) keep the flat list, like ANY and the drain's rows.

Also added independent semantic pins for the parsed table (REFRESH 03/08,
PLAY TONE 09, TIMER MANAGEMENT 24, LAUNCH BROWSER 26, OPEN CHANNEL 28/3A/3B,
the MMS commands 27/3D, GET READER STATUS 38 - a column-shift regression in
the parse would break them).

861 frontend + 842 Python.
2026-10-08 22:03:39 +03:00
catarrh 2b994a7a89 v3.23.8: the command-grouped TERMINAL RESPONSE result list
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.
2026-10-08 21:55:57 +03:00
catarrh c9ab15c392 v3.23.7: the proactive log survives a TERMINAL PROFILE re-send
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.
2026-10-08 00:20:06 +03:00
catarrh 9fb46b786f v3.23.6: the event name keywords in the test-script editor
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.
2026-10-07 21:56:20 +03:00
catarrh 02a78fcca6 v3.23.5 review: the numeric result display and the single datalist
Two findings from the review of e0dfc10:

- trResultFormValue stringified the stored value, so a numeric result
  (an API-authored or hand-imported script - the engine reads ints as the
  value) was misread as hex: 10 showed as '10 - cancel' (0x10) instead of
  the value 0x0A, 16 matched no option and fell back to '00 - ok', and
  saving the step wrote the shown option back, silently changing the value.
  Numbers are converted to their hex form first.
- trResultRowHtml rendered the 'tr-addinfo-list' datalist on every call, so
  the drain's form (respond + first) carried two elements with the same id.
  It is rendered once per form (an opts.list flag on the first result row;
  the second row and the source pin the single occurrence).

Tests: the numeric form values, the datalist only with {list: true} and the
two-row composition rendering exactly one.  855 frontend + 838 Python.
2026-10-07 21:37:58 +03:00
catarrh e0dfc1032e v3.23.5: the TERMINAL RESPONSE result list and its spec corrections
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.
2026-10-07 16:43:22 +03:00
catarrh cc335def00 v3.23.4: the event form's numeric option values (v3.22.0 regression)
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.
2026-10-07 13:44:31 +03:00
catarrh e8c991648d v3.23.3 review: pin the notice box targeting with a DOM test
The review of 1786300 found the box targeting pinned only by source-pattern
assertions.  The new test drives ramShowCntrLow through the fake document
and asserts the notice lands in the originating view's box (and hides the
other one), so a regression to the static '#ram-cntr-low' lookup fails
behaviourally, not just textually.

847 frontend + 830 Python.
2026-10-07 10:34:37 +03:00
catarrh 17863001f3 v3.23.3: the low-counter notice polish
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.
2026-10-07 10:28:42 +03:00
catarrh 9ad22f2e26 v3.23.2: the start-reset ordering fix (review follow-up)
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.
2026-10-07 10:09:14 +03:00
catarrh 9ef1a59f2c v3.23.1: the PLI dictionary defaults and the PLI codec tag fixes
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.
2026-10-07 09:46:39 +03:00
catarrh 6bea57599c v3.23.0: the start-with-reset suite option
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.
2026-10-07 09:22:24 +03:00
catarrh eee2888a2b v3.22.8: the text/dcs load-time validation (review follow-ups)
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.
2026-10-07 01:03:44 +03:00
catarrh 00675ca09c v3.22.7: a raised card error honours the step's on_fail
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.
2026-10-07 00:53:20 +03:00
catarrh 0fa1dee4b5 v3.22.6: the view-history review fixes
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.
2026-10-06 21:58:34 +03:00
catarrh 9a4dc80f11 v3.22.5: the PWA view history and the i18n completeness
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.
2026-10-06 21:37:09 +03:00
catarrh 320854eebc v3.22.4: the proactive-drain pending exemption
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.
2026-10-06 21:22:22 +03:00
catarrh 7d997272ef v3.22.3: the mismatch on_fail, the self-healing move, the pruning reconciliation
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.
2026-10-06 21:14:07 +03:00
catarrh a76e0154b6 v3.22.2: the runner's TERMINAL RESPONSE inherits the terminal's own objects
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.
2026-10-06 20:55:07 +03:00
catarrh 28a940c9f5 v3.22.1 (review follow-ups): the counter comment, the raw fallback, the RU label
The three nits from the review of the rename/docs commits:

- the PWA comment above TEST_ACTION_KINDS still claimed the final SCP80
  counter is written back - the server persists every consumed counter into
  the preset and the snapshot carries the latest values (display only);
- test-scripts.md's `por` paragraph regained the RM raw fallback the rewrite
  dropped: a remote-management reply that cannot be the compact structure
  (its command count exceeds the command script that was sent, or the data
  is too short for TS 102 226 5.1.2 Table 5.1) is raw too;
- the RU help label reads "Очистка очереди проактивных команд" (the mixed
  Latin "proactive" is gone; the rest of the RU help says "проактивных
  команд").  sw cache simple-v405.

Docs/comment only - tests unchanged and green: 839 frontend + 804 Python.
2026-10-06 01:52:21 +03:00
catarrh e018762589 docs: test-scripts.md up to speed
- the actions table still named the drain kind `proactive` (the rename swept
  the prose but missed the table row);
- the `por` check paragraph carried the pre-3.21.0 rule (the command-count
  bound): the response form follows the command's TAR and a short reply is
  raw too - now cross-referenced to "The three status words";
- the event action's `src`: the editor writes the event's own source
  (network for MT call / IMS registration), a hand-written step without
  `src` is sent as the terminal;
- the format line notes the kind was `proactive` until 3.22.1;
- the cleanup section notes that each suite member ends the same way, the
  "no actions" note names the ok drain, and the polling limitation mentions
  proactive-drain.

Docs only.
2026-10-06 01:44:57 +03:00
catarrh f0a836363c v3.22.1: rename the proactive action kind to proactive-drain
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.
2026-10-06 01:41:53 +03:00
catarrh e12dfc7871 v3.22.0 (review fixes): the quota zero, the suite repair paths, the docs
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.
2026-10-06 01:37:58 +03:00
catarrh b2cb42caf5 v3.22.0 (review fixes): the ownership reconciliation and the import guards
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.
2026-10-06 01:15:20 +03:00
catarrh 40c1e0f148 v3.22.0: the event action uses the Simulator's forms (all modelled events)
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.
2026-10-06 01:07:12 +03:00
catarrh bcd9fb9a4c v3.22.0 (fix): an expanded run log stays open across the live report polls
The report view re-renders the member blocks on every poll while a suite runs
(innerHTML replacement), so an expanded Run log collapsed within 500 ms.  The
<details> now carries the stable member index (data-member) and
testRenderReport carries the open state across the re-render; the log content
keeps updating in place.

- frontend/tests/testsuites.test.js: the wiring pin (data-member emitted, the
  open state read and restored).
- sw cache simple-v400 (index.html changed); style.css rebuilt.

Tests: 837 frontend + 795 Python.
2026-10-06 00:50:24 +03:00
catarrh 22a0a58f27 v3.22.0 (review fixes): the suite members share the SCP80 counter; drain/PWA nits
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.
2026-10-06 00:41:11 +03:00
catarrh 5976ef89b4 v3.22.0: the test suites (scripts grouped, ordered, with setup/teardown)
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.
2026-10-06 00:26:50 +03:00
catarrh e6ced47323 v3.21.0: the test-runner tolerance batch + the proactive cleanup action
Five reported issues, plus the action that makes an explicit drain possible:

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

Docs: test-scripts.md, api.md, help EN/RU, repo AGENTS.  sw cache simple-v396.
Tests: 825 frontend + 755 Python green.
2026-10-05 23:36:47 +03:00
catarrh 82c5733906 v3.20.0 (fix): the install-parameter composition - one System Specific Parameters EF
Two reported SCP80/RAM issues, both confirmed against the specs:

- docs/api.md called `stk_params` a "Hex CA TLV", but TS 102 226 8.2.1.3.2.1
  puts the CA *inside* the System Specific Parameters EF ('EF'), and the PWA
  builds `EF <len> CA <len> ...` (the SIM access domain leads the CA value as
  `01 <ad>` / `00`).  A client following the doc sent a CA at the wrong level.
- the RAM composition appended the STK part after a caller-composed
  C9+EF(quotas) prefix, producing TWO EF objects.  The card read the first
  (the quotas) and ignored the CA: the install was accepted (por_ok) but the
  toolkit parameters were never registered, so no STK menu appeared (live
  2026-10-05).  Per GPD_SPE_013 v1.1 Table 6-5 (pySim's
  gen_install_parameters) the quotas (C7/C8) and the CA share ONE EF; an
  EA-form STK part is a sibling of EF (the reference TCA loader's
  `C9 00 EF 00 EA ...`).

- server: `_compose_install_params()` (used by `_cap_install_apdu` ->
  `_cap_apdu_sequence`, hence /api/ram-install, /api/ram-install-app,
  /api/scp81/gen-install and the PWA's install script template) enforces the
  rule: a caller-composed EF plus an EF-form `stk_params`, a bare CA, a
  malformed EF and quotas next to a non-empty `install_params` are refused
  (400 / `{"ok": false}` with the fix named); with an empty `install_params`
  the quotas and an EF-form CA are composed into one EF
  (`C900 EF{C7?,C8?,CA}`), an EA part stays a sibling.
- the documented `nv_quota`/`volatile_quota` fields are now real (they were
  named in the docs but implemented nowhere): integers composing C8/C7.
- PWA: `composeRamInstallParams()` composes the same single EF itself (a
  CA-form STK field takes the quotas into its EF, the merged EF travels in
  `stk_params`), the RAM install bodies send it, the placeholder shows it
  with a note when the quotas moved into the STK EF, and a refused request
  (no steps) reports the error instead of "FAILED at step: undefined".
- docs/api.md: the corrected `stk_params` description with both accepted
  shapes, the composition rule paragraph with the refusal cases, the fixed
  example (`C90000` -> `C900`), and the /api/ram-install-app and
  /api/scp81/gen-install cross-references.
- tests: composition unit tests (merge/refusals/EA-sibling) and the
  ram-install-app 400 paths (Python); `ramQuotaTlvs`/`composeRamInstallParams`/
  `ramInstallParamsCompose` pins incl. the CA merge (Node).
  Tests: 824 frontend + 740 Python; sw cache simple-v395.
2026-10-05 23:01:42 +03:00