Commit Graph

132 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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 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
catarrh ff3feb6079 docs: document includeCpi on /api/send-ota and the RAM endpoints
The OTA CPI information element is a request field (default true) on
/api/send-ota and the RAM family but was only mentioned in passing:

- /api/send-ota: an "SMS framing - includeCpi" paragraph (the CPI IE marker
  per TS 31.115 4.2/4.3; 137 vs 140 octets in a single SM, first concatenated
  segment 132 vs 134; the RAM/ISD operations send with it, a concatenated
  command carries it in its first SM; what false does) and
  "includeCpi": true in the request-body example;
- /api/ram-install and /api/ram-install-app: field-table rows;
- /api/cap-compat: the "as /api/ram-install" field list;
- /api/tar-probe: a sentence.

Docs only - no version/sw bump.
2026-10-05 22:44:17 +03:00
catarrh c1cd86e0c2 docs: qualify the RFM SELECT note (UICC shared FS vs ADF RFM)
The scripted-SELECT note cited TS 102 226 7.2 alone although the paragraph
right above lists both B00000 and B00001: 7.2 is the UICC shared FS RFM
application (implicitly selected in MF), while an ADF RFM application starts
in its ADF (7.3).  Also says the script has no *use* for an FCI rather than
nowhere to put it.
2026-10-05 22:37:32 +03:00
catarrh bf6730f028 v3.20.0 (review fixes): the PoR decode uses the effective counter; the PWA fallback matches the server's seed
Two boundary flaws found reviewing v3.20.0:

- /api/send-ota decoded the PoR with the request's `cntr`, not the counter
  the packet was actually built with.  With the new seeding an omitted
  counter is the normal case, so a warning ENVELOPE's verdict - notably the
  live card's cntr_low PoR - could fail its RC/CC check and be lost (the
  success and the failure path are both fixed).  Tests pin the decode counter
  on both paths; the failure path also echoes the effective `cntr`.
- the PWA's effective counter (preview / verify / the older-server fallback)
  fell back to the preset's first keyset and then 1, while the server's empty
  seed builds counter 0 (no preset / kvn 0 / an unknown keyset).  A manual
  form or a keyless packet then produced a spurious "Verify vs pySim"
  MISMATCH and a preview that differed from the delivered packet.
  spPresetCounterValue()/spCntrEffective() now mirror _preset_counter_seed()
  exactly: the packet's matched keyset, else 0.
- the `OTA SEND: counter not tracked` line reflects the effective counter (a
  seeded value is logged too); docs/api.md records that the seed uses the
  packet's keyset whatever SPI1.b5b4 says.

Tests: 823 frontend + 737 Python; sw cache simple-v394.
2026-10-05 22:37:28 +03:00
catarrh 85cc4b9535 v3.20.0: the server-owned SCP80 counter (an untouched field = the preset's next value)
The counter an operation consumed was re-entered by the PWA on every send
(the form field was filled from the preset and posted back), so a stale tab
could send a value the card rejects (cntr_low), and typing in the field
silently wrote the preset.  Now the store is the single source of truth and
the form field is a per-send override (Option B).

- _preset_counter_seed: an explicit `cntr` in the request wins (a hand send,
  the counter probe or a pre-built packet may deliberately pick one);
  otherwise the preset store's value for the packet's keyset.  Wired into
  /api/send-ota (the APDU path; the pre-built `sp` path keeps the caller's
  bookkeeping value - the packet carries its own counter), /api/ram-install,
  /api/ram-install-app, /api/cap-compat, /api/tar-probe and /api/sp-verify
  (the reference must use the counter the JS packet was built with).
- the send-ota response echoes the effective `cntr` (a caller that omitted
  one learns what was used) and the OTA SEND log line prints it.
- persistence is unchanged and monotonic (presets.set_counter refuses
  backwards writes), pinned by tests: an explicit lower counter is used for
  the packet but never lowers the store, a higher accepted one is saved.
- PWA: the Secured packet counter field starts empty with the preset's next
  value as its placeholder (auto: <cntr>); typing affects that send only and
  is never written to the preset (spCntrSyncPreset is gone), spParams /
  getRamSpParams omit an empty counter, the RAM/send flows sync only the
  local preset copy (forward-only) instead of filling the field, the +/-
  buttons start from the effective value, and the preview/verify packet is
  built from the effective counter (genSp).  Placeholder translated EN/RU.
- the runner and /api/counter-probe keep their existing seeding (the probe
  already starts at the failed value and walks above it).
- docs: api.md's optional-`cntr` contract + seeding notes on every endpoint,
  help EN/RU 3.1.  Tests: 822 frontend + 734 Python; sw cache simple-v393.
2026-10-05 22:23:19 +03:00
catarrh 0e06b918a7 docs: a scripted RFM SELECT should use P2=0C (guide note)
The alfa-dsa RFM script work showed a card refusing the P2='00' (FCI) form
of a SELECT inside a secured command script with 6A86, while P2='0C' (no
response data) executes - and a script has nowhere to put an FCI anyway.
Worksheet 1 gains the note (TS 102 226 7.2: the RFM application starts in
MF); the repo AGENTS records the observation.  Docs only - no code, no
version or sw change.
2026-10-05 00:14:32 +03:00
catarrh 0d63e5592f docs: a refused envelope may carry no response packet (guide + help)
The alfa-dsa control-commands run showed the error case: an applet-level
refusal (envelope 6200 for an unknown command) carries no PoR at all, so
a por expectation on such a step can never match.  The authoring guide's
status-words section now states that a 62XX/63XX envelope does not
guarantee a response packet - other warning paths still deliver one (the
low-counter case arrives via SEND SHORT MESSAGE) - and that a step
expected to be refused should leave por at its "any" default or assert
"none"; help EN/RU notes the same.  docs/test-scripts.md only, no code
change; sw cache simple-v392 (help files are precached).
2026-10-04 22:30:30 +03:00
catarrh 78bc91d297 v3.19.2: the three-status-words disambiguation (envelope SW vs por.sw)
The script author read por.sw as the envelope SW, so a por.sw expectation
on an applet's own TAR failed with (none).

- docs/test-scripts.md: a 'The three status words (envelope SW / PoR
  status / R-APDU SW)' table in the authoring section - where each one
  comes from, the script field and the report row - plus a cross-reference
  from the por check paragraph; help EN/RU 8.9 points at it.
- a failed por.sw check on a response_type: 'raw' decode carries the check
  detail 'the response is application data - an applet's own TAR has no
  R-APDU SW; assert it with data' (the PWA renders check details).
- the runner's log prints APP DATA=<hex> (no R-APDU SW) for application
  responses instead of the misleading R-APDU SW=(none) data=...; the
  compact/scripting lines are unchanged.
- the step editor's custom-PoR block gets a one-line hint about the same.
Tests: 821 frontend + 730 Python; sw cache simple-v391.
2026-10-04 22:19:58 +03:00
catarrh 14c43f37b3 v3.19.1: the compact-PoR decode guard (applet TAR responses reported as data)
pySim's CompactRemoteResp was applied to every response packet, so a
third-party applet's own TAR response parsed as count=128/SW=0110/data=''
(live 2026-10-04: the alfa-dsa applet's `80 01 10`) - a fabricated status
word and a lost response.

_decode_por now takes the plaintext command script's length (cmd_len): the
compact structure's command count cannot exceed it (TS 102 226 5.1.2
Table 5.1 - each command needs at least one byte), so a larger count is
classified `response_type: 'raw'`, the secured data becomes
`decoded.last_response_data` (a por.data check matches it) and no SW is
decoded; `secured_data` is exposed in every case.

Callers pass cmd_len where they know the plaintext: the test runner's scp80
step (None for a pre-built packet; carried in ctx['last_scp80'] for the
submit-PoR path), /api/send-ota (both paths), the RAM step sender and the
counter probe; a caller without it keeps the previous parse.

Docs: test-scripts.md and api.md describe the raw fallback.
Tests: 821 frontend + 729 Python; sw cache simple-v390.
2026-10-04 22:04:57 +03:00
catarrh d5aed563a1 v3.19.0 (docs/engine): test-script guide audit; sms da accepts a leading +
An audit of docs/test-scripts.md against the engine (testscript.py,
events.py, the runner) found a handful of doc-level gaps, now fixed:

- the scp80 param defaults (tar = the preset's ISD role TAR, spi1 = that
  TAR's MSL, spi2 = 01, kvn = the first keyset);
- the counter rule as implemented: the run advances/persists the counter
  after every answered scp80 step when SPI1 requests a counter check
  (refused packets move the local value forward too - it only moves up);
- respond is optional (an omitted object answers ok/0x00);
- the run-log section: the exact READ BINARY/READ RECORD/UPDATE BINARY,
  SCP80 WRAPPED=/SP= and 'TEST-RUN step N failed:' lines;
- Known limitations: an expanded-format 61xx has no in-packet GET RESPONSE
  (TS 102 226 5.2.1.1) - send the GET RESPONSE as a follow-up scp80 step;
- sms check da: a leading + and whitespace are ignored.  The engine now
  drops the + (the TPDU carries the international flag in the dialling
  type, so '+79...' could otherwise never match the parsed digits);
  docs/api.md and the local AGENTS note it.

Tests: 821 frontend + 726 Python.
2026-10-04 21:26:44 +03:00
catarrh 4a353720d0 v3.19.0 (review fixes): expose the menu-select Match case choice; doc/comment fixes
Review of 7250dbd:
- the step editor's menu-select form gains a Match case select
  (Case-insensitive (default) / Case-sensitive); the step summary flags a
  case-sensitive step with (case), the saved params carry
  case_sensitive: true only when chosen, and the docs say the editor
  authors the flag.
- docs/api.md: fix the 'with an optionally' grammar.
- the Variant-2 menu test's comment claimed a contains match while it is an
  exact one; a real contains leg is added ('ыбор' -> item 128).
- timeless rationale for the case-insensitive default; the format note now
  dates the change.
sw cache simple-v389 (index.html/help changed).
Tests: 821 frontend + 726 Python.
2026-10-04 21:18:36 +03:00
catarrh 7250dbd63a v3.19.0 (cont.): live fixtures; menu text match is case-insensitive by default
No test scripts have been authored yet, so the format is still free to
change - the menu-select text match is now case-insensitive by default
(menu labels are UI text; case_sensitive: true still forces an exact-case
match, and a miss on it names the case-insensitive candidates).

Verification against the live card's own bytes (captured 2026-10-04):
- the Annex A Variant 2 Cyrillic menu (ids 128..130) resolves by text and
  the case behaviour above is pinned ('кофе' -> item 130);
- the applet's SEND SHORT MESSAGE (alpha 'Отправка...', SMS-SUBMIT to
  +79332505884) pins the new alpha and sms checks and the run-log line.

docs/help/api updated; sw cache simple-v388 (help pages changed).
Tests: 819 frontend + 726 Python.
2026-10-04 21:05:19 +03:00
catarrh 92506e72b9 v3.19.0: the applet-agent script batch - por objects, alpha/sms checks, scp80 format
The test scripts get the pieces an applet test needed:

- scp80 check.por accepts an object {status?, sw?, data?} asserting the
  decoded PoR of this very exchange (inline or via SEND SHORT MESSAGE);
  the evaluation is shared with the expect-step por check
  (_por_check_results).  The editor's PoR select gains 'custom' with the
  three fields (an empty object refuses on save).
- a new alpha content check reads the command's Alpha identifier (05/85,
  Annex A coded) - the text check stays bound to the 8D Text string, so a
  command carrying both keeps them distinct.
- a new sms content check asserts a SEND SHORT MESSAGE's submit TPDU:
  DA / PID / DCS / UDL / UD (the parsed fields are logged and reported;
  an applet's own SMS, not the OTA PoR the por check decodes).
- scp80.params.format wraps the C-APDU in the TS 102 226 5.2.1 Command
  Scripting template (expanded = AA..., expanded-ae = AE 80...00 00); the
  log shows WRAPPED=, the report C-APDU=<effective> format=<fmt>.
- docs: Worksheet 1 addresses an RFM TAR (B00000/B00001/B00010 compact,
  B00120/B00140/B00130 expanded) with the own-TAR and MSL/spi1 notes, and
  file-write is documented as a direct APDU that does not fire
  EVENT_REMOTE_FILE_UPDATE.

Tests: 819 frontend + 724 Python; sw cache simple-v387.
2026-10-04 20:57:38 +03:00
catarrh c444468d2a v3.18.3: the test-run card-exchange log
Verifying a script from the log was impossible: the runner wrote no card
lines, SCP80's plaintext C-APDU / secured packet / PoR landed only in the
report, and a menu selection logged nothing (the interactive path showed just
"STATUS poll (menu-select 9000)" / "STATUS -> 9000").

Every step now writes a `TEST-RUN step N:` line family to stderr, request
before response:

- generic actions: the APDU/raw event data sent and the SW/RESP received
  (`APDU TX=...`, `ENVELOPE(Event Download) ... data=...`, `READ/UPDATE ... -> SW=...`);
- SCP80: `C-APDU=` (or `SP=`), `SECURED=<hex> (N B) TAR=... SPI1=... SPI2=... CNTR=...`,
  the response SW, and the PoR - `PoR[inline] status=... TAR=... CNTR=... raw=...`
  plus `R-APDU SW=... data=...` (with the bad-format note);
- expectations: `FETCH=<apdu> -> SW=... <raw>`, `CMD 0x.. <NAME> qual=..`,
  `TR=<apdu> -> SW=...`, and `PoR[sms-submit] ...` + `R-APDU` for the SEND
  SHORT MESSAGE transport;
- status: `STATUS 1/N -> <sw>` per attempt;
- menu selection: `MENU-SELECT ENVELOPE=<apdu> item=<id> ('<text>') -> SW=...`
  (both the runner action and /api/menu-select; the old STATUS lines stay as
  context).

The shared secured-packet sender logs each segment's answer
(`OTA SEND: ENVELOPE i/N -> <SW>`), the scp80 step's report `sent` now carries
the C-APDU/SP hex, and `--apdu-trace` remains the raw transport view.

help EN/RU and docs/test-scripts.md gained the "reading the run log" note.
Tests: 816 frontend + 718 Python; sw cache simple-v385.
2026-10-04 19:50:55 +03:00
catarrh 1aa1b44063 v3.18.1: the raw envelope action takes the source override too
The events without a semantic builder (MT call, IMS registration, slices, ...)
are network-sourced, but the raw `envelope` action had no `src` override - the
only way to express them was the semantic set.  `envelope` now accepts
`src` (82 terminal / 83 network) exactly like the `event` action; the step
editor exposes it and the authoring guide gets the row.

Also: the guide marks `por` as SCP80-only (the normaliser refuses it
elsewhere) and the semantic-events section names the shared src override.

Tests: 813 frontend + 717 Python; sw cache simple-v382.
2026-10-04 18:06:35 +03:00
catarrh fb4e4bd7b2 v3.18.0: the semantic event action for test scripts
The other-applet batch: a test script can send the common event downloads
semantically instead of raw hex.

- events.py: server-side mirrors of the PWA's event forms for Location status
  (0x03), Access technology change (0x0B), Network rejection (0x12 - the
  LA/RA/TA object selected by the registration group, the extended EMM cause
  and the extended information object) and Data connection status change
  (0x1D - status/type/TI plus the optional cause, host-clock 8.39 date-time,
  location information, access technology, APN and PDP/PDN/PDU type).  Fields
  are validated before the run (a bad value is a script error, not a step
  failure); `src` overrides the device-identities source (default terminal,
  matching the Phone forms).
- The engine gains the `event` action kind; the runner builds and sends it.
  The PWA's action-kind list stays in sync (drift-guarded - the guard itself
  caught its own regex bug on the first run).
- Byte parity: the vectors are generated from the PWA's own builders and
  pinned in tests/test_events.py; the frontend suites assert the same values.
  PLMN and SCTS helpers are mirrored too.
- The Phone tab's event form shows the built "Event data (hex)" (updated as
  the fields change), so every event without a builder can be used through
  the raw `envelope` action.
- The test-step editor offers the event select + a JSON fields textarea;
  malformed JSON refuses the save with a clear message.
- docs/test-scripts.md (the semantic-event table, an event example, the
  format stamp), docs/api.md and help EN/RU updated.

Tests: 812 frontend + 717 Python; sw cache simple-v381.
2026-10-04 18:00:53 +03:00
catarrh 0b9e9e4c5e v3.17.0: test scripts for the applet handoff - menu by text, spec-order TRs, codings
The applet-testing batch agreed after v3.15/3.16:

- menu-select by text: the step names the item by `text` (+ `mode`
  exact/contains, `case_sensitive`) instead of the install-dependent id
  (both or neither is refused); the runner resolves it against the card's
  cached menu and raises a step error naming the available items when nothing
  (or several items) matches.  The cache is refreshed on every SET UP MENU
  fetch - the proactive chain and the expect step - so an applet install (new
  items/ids after its REFRESH) no longer leaves a stale menu behind.  The
  step editor offers the id or the text + mode and the client-side pre-check
  bounds them.
- TERMINAL RESPONSE object order: both builders emitted the Item identifier
  before the Result (and the scripted builder its Text string before it too);
  TS 102 223 6.8.0 mandates Command details, Device identities, Result,
  Duration, Text string, Item identifier.  `testscript.build_tr` and
  `_menu_send_response` now follow it; `respond.raw` is documented as
  appended after the standard objects, and the exact-byte tests pin the order.
- Text codings: the scripted response codes the GSM default alphabet via the
  GSM 03.38 codec ('00' packed, '04' 8-bit) instead of latin-1, keeps '08'
  UCS2 and sends the spec's null text string for an empty answer; the packing
  helper is shared with the interactive path (`testscript.pack_gsm7`).  A
  UCS2 GET INPUT answer is documented and tested.
- docs/test-scripts.md gained the handoff brief for the applet agent: the
  value-collection table, fill-in worksheets for the two applet scenarios, a
  menu-by-text example, the validation checklist and a format stamp; help
  EN/RU and docs/api.md updated.

Tests: 809 frontend + 705 Python; sw cache simple-v380.
2026-10-04 17:48:29 +03:00
catarrh 8430dd1694 v3.16.0: test scripts move to a server-side store
The scripts lived in the browser's localStorage although only the local
server can run them.  The new test_scripts.py store
(~/.pysim-simple-server/test_scripts.json, --test-scripts PATH) sits next to
the card presets: stable uuids and timestamps, atomic writes, and every
mutation is validated with the runner's own engine (the submitted shape is
kept - the normaliser's output is not re-normalisable: it fills defaults
like an 'any' PoR check that the check normaliser then rejects), so a stored
script cannot fail at run start for a validation reason.  Import accepts the
old localStorage export as-is (export/import remains the migration path; the
PWA import also takes an exported array).

- /api/test/scripts (GET card-free: path/count + scripts), POST create,
  /update, /delete and /import serve the store; the endpoints answer 503
  without a store and 500 on a write failure, like the preset endpoints.
- /api/test/run takes a stored script_id (resolved when no inline script is
  sent); the PWA sends the edited copy inline plus the id for the report,
  and the run snapshot carries script_id.
- The PWA's Test script pill loads from the store, writes through on edits
  (debounced name, immediate step/new/copy/delete), reports load/save/delete
  failures inline, and exports the portable {name, steps} shape.

Tests: 808 frontend + 701 Python; sw cache simple-v379.
2026-10-04 17:24:21 +03:00
catarrh abc9ea2751 v3.15.0: test-script fixes for applet testing - preset id, kvn, PoR and file-list checks
Three gaps blocked SCP80 applet testing from the PWA:

- testRunPreset() still marshalled the pre-v3.8 flat preset fields
  (kic/kid/keys/counter/tar/spi1/spi2, all empty on a stored preset), so
  every SCP80 run failed with "SCP80 preset is incomplete: no keyset is
  defined".  The PWA sends the matched preset's id now and the server
  resolves it from the store (_test_run_preset_from_body; the inline
  `preset` body stays for API callers), so a run always uses the stored
  keysets, TARs and counters.  Regression tests on both sides.
- testscript._normalise_params dropped the scp80 step's `kvn` (the editor
  exposed it and the runner and validation consumed it, so scripts
  silently used the preset's first keyset).  kvn is validated (1-15) and
  passed through; the client-side problem check bounds it too.
- The PoR delivered as a SEND SHORT MESSAGE (PoR-in-submit, SPI2 bit 0x20)
  is decoded for the expect step that fetches it: the new `por` content
  check asserts the response status / R-APDU SW / response data, decoded
  with the preceding scp80 step's context (concatenated SMS accumulate
  across expectations).  The inline transport stays on the action
  (check.por / check.data).
- The new `files` content check asserts the File List of a REFRESH
  (TS 102 223 8.18, tag 12/92) - e.g. qualifier 00 = NAA initialization
  with full file change notification - as an order-insensitive set of FID
  paths.

The step editor exposes both checks; help EN/RU, docs/api.md and the new
authoring reference docs/test-scripts.md document the format.  Tests: 803
frontend + 680 Python; sw cache simple-v378.
2026-10-04 17:09:06 +03:00
catarrh 3c5882d16d v3.14.2: the counter probe's verdict rule
The ladder only advances on cntr_low; any other verdict stops the search.
A stop on a non-por_ok verdict after at least one cntr_low keeps the value
anyway - the earlier rejection proves it is above the card's counter - but
the response reports success: false with counter_saved, the verdict and the
saved counter ("something is likely wrong with the security settings");
the PWA shows it amber.  Without a prior cntr_low nothing is saved, and a
missing PoR stays an error.  actual_response_sms_submit/ussd count as
accepted.

Docs: docs/api.md, repo AGENTS.  sw cache simple-v377.
Tests: 672 Python + 798 frontend green.
2026-10-02 14:58:18 +03:00
catarrh 475c13aeb8 v3.14.1: the counter probe demands its PoR
_counter_probe_params validates SPI2, forces b1 (PoR required; a caller's
00 becomes 01) and defaults to the RAM listing transport 21; the no-PoR
error names the effective SPI2, and ramSyncCounter posts
ramListingSpi2('40') for the RAM view (the failed operation's transport).

Live 2026-10-02 (NP BAP): the probe had sent the plain SCP80 form's 00
(no PoR) - the card answered the ENVELOPE 9000 and sent nothing, so the
probe stopped after one packet with "card verdict: no PoR".

Docs: docs/api.md, repo AGENTS.  sw cache simple-v376.
Tests: 670 Python + 798 frontend green.
2026-10-02 14:40:07 +03:00
catarrh 3660a7e173 v3.12.0: the card preset carries the card's CAT (USAT) release
The release selector lives in the preset rather than a global chip: presets
already travel with the card (ICCID auto-selection, export/import), and the
operations that will consume the release are preset-aware.

- presets.py: the optional `release` field (4-18; empty = unspecified) joins
  PRESET_FIELDS; a value outside 4-18 is refused on save/import; the store
  schema version is 5.
- catrelease.py: DEFAULT_RELEASE = 12 and preset_release() - the effective
  release is the preset's own value, else Rel-12 (the safe level for cards
  whose release is not known; an unset release never filters anything away).
  The PWA mirrors both (CAT_RELEASE_DEFAULT + presetRelease()), drift-guarded.
- Cards tab: the editor's header row gains a compact CAT release select
  (unspecified / pre-Rel-6 stored as 5 / Rel-6 ... Rel-18); the empty option
  names the Rel-12 default and the tooltip explains the guard; the field is
  collected, cleared and restored with the other form fields.
- Nothing filters yet: the Simulator's event parameters (stage 3) will use
  the effective release.
- docs/api.md + help EN/RU updated; sw cache simple-v368.
2026-10-02 11:28:33 +03:00
catarrh 5a3c4c3529 v3.11.0 review fixes: ADM length, TAR re-render hygiene
- the ADM input capped the key at 8 chars, but the server's _verify_adm
  takes 4-16 hex digits (padded to 8 CHV bytes) and the old input had no
  limit; maxlength=16 now, with a static guard in html.test.js.
- cardsTarAdd/cardsTarRemove re-rendered the table from the save-path
  reader, so an emptied role field was silently refilled with its default.
  The renderer no longer invents defaults; a raw reader (cardsTarsFromTable)
  feeds Add/Remove and the language-switch refresh, while the save path
  (cardsTarsFromForm) keeps the default-filling for the mandatory roles.
- the editor's dynamic TAR/keyset rows now rebuild on a language switch
  (refreshDynamicI18n) from what is typed - their placeholders and titles
  carry t() strings.  (The title/Save label were already refreshed there.)
- dropped the now-unused cardsRoleMsl helper and its test references.
- docs: the ADM is documented as 4-16 hex digits (help EN/RU, api.md,
  AGENTS).  sw cache simple-v362.

Tests: 665 Python + 784 frontend green; the whole inline PWA script
syntax-checked.
2026-10-02 10:27:42 +03:00
catarrh 65c565e058 v3.11.0: Cards tab redesign (list + editor), preset PIN/PUK codes
The Cards tab now shows the preset list by default; "Add preset" / "Edit"
open a separate editor view (Save stores and returns to the list, Cancel
discards; ramGoToPreset opens it too).  The editor keeps the name/ICCID
header (with From card) and groups its fields into bordered blocks:
- PIN/ADM: the new optional card codes PIN1/PUK1/PIN2/PUK2 (plain text,
  4-8 decimal digits) plus ADM, moved out of the top row; nothing consumes
  the PIN/PUK codes yet.
- TAR: one uniform table (TAR, MSL, description); the first three rows are
  the fixed role entries with the immutable English descriptions "ISD
  compact format" / "UICC shared FS RFM compact format" / "ADF FS RFM
  compact format"; further rows are deletable with an optional description.
- SCP80 (keyset editor) and SCP81 (PSK pair) as before.

Store: PRESET_FIELDS gains pin1/puk1/pin2/puk2 (optional; whitespace
stripped; 4-8 decimal digits validated on save and import), SCHEMA_VERSION
4, docs/help/AGENTS updated.  TAR rows use unified ids (cards-tr-N-*), the
editor switches via cardsShowList/cardsShowEditor.

Tests: 665 Python + 783 frontend green; the whole inline PWA script
syntax-checked.  sw cache simple-v361.
2026-10-02 09:32:45 +03:00
catarrh c14450c3ae docs: move the release reference next to the pinned specs (workspace docs/uicc/) 2026-10-02 08:48:27 +03:00
catarrh 967d9e64c7 docs: ETSI/3GPP CAT release reference (docs/ETSI-3GPP-Releases.md)
Stage 1 of the release guard: which proactive command, event, data object and
PLI qualifier appeared in which release.

Sources: the change histories of the three pinned specs - 3GPP TS 31.111
Annex W, ETSI TS 102 223 Annex V and ETSI TS 101 220 Annex Q.  The version
major equals the release (V7.x = Rel-7 ... V18.x = Rel-18); ETSI aligned its
numbering with 3GPP, but the documents differ, so every row carries its source
document.

Content: curated tables for the 32 events, the proactive commands, the data
objects/TLVs the tool builds, the PLI qualifiers and selected TERMINAL PROFILE
features (release + source/CR per row; 'baseline' = the original Rel-4 spec,
'?' = not pinned down by the histories), the per-release feature entries of the
three histories, and a machine-readable JSON summary as the seed for the
code-side table (catrelease.py + the PWA mirror).

The data keeps the real release (Rel-4/5/6 ...); the UI will collapse
everything before Rel-6 to "pre-Rel-6".
2026-10-02 08:41:37 +03:00
catarrh 355bcadb59 v3.10.0 review fixes: repair /api/ram-install-app, escape quotes in esc
- /api/ram-install-app referenced undefined spi1/tar (the resolution block
  was only added to /api/ram-install and /api/cap-compat), so every request
  answered 500.  The same preset/MSL resolution is now in place, with
  regression tests for both modes (install / make selectable) and for the
  no-MSL refusal before any card I/O.
- esc() now escapes " and ' too: the TAR editor renders free-text
  descriptions (and other values) into attribute contexts, so a crafted
  preset import could inject attributes.  Three tests asserting the old
  unescaped output updated; a dedicated esc test added.
- _spi1_for_tar names the no-TAR case in its hint (a pre-built packet
  without tar/spi1 used to report "TAR ?").
- preset free TAR rows no longer store an empty "role" key (the documented
  shape); a non-list `tars` in an update is refused (400) instead of being
  silently ignored.

Live: the no-MSL request answers 400 with the hint; a make_selectable with
a bogus AID runs the format probe and the INSTALL (derived SPI1 16 from the
preset MSL), the card answers remote SW 6A88 and the counter advances - no
500.  Suites: 658 Python + 775 frontend.

No version bump: completes the unreleased v3.10.0 (6360dc5 was never
pushed).  sw cache simple-v359.
2026-10-02 08:40:42 +03:00
catarrh 6360dc5bae v3.10.0: preset TAR table with per-TAR MSL; derive the SPI1 from the TAR
Presets drop the card-wide SPI1/SPI2 and the fixed tar/uiccTar/usimTar
fields: a preset now carries `tars` - the three mandatory role entries
(isd/uiccRfm/usimRfm, spec defaults pre-settled) plus free TARs with an
optional description, each with its MSL (Minimum SPI1, TS 102 226
8.2.1.3.2.4).  v3.9.1 presets convert on load/import (the spi1 seeds all
three MSLs); duplicate TARs and empty TAR/MSL values are refused; the
store schema version is 3.

Operations derive a packet's SPI1 from its TAR's MSL (_spi1_for_tar): the
RAM ops, the test runner (per-step overrides still win), the counter probe
and each TAR-probe row; the plain send keeps an explicit SPI1 with a
`warning` when it is below the MSL, and a TAR outside the preset without an
explicit SPI1 is refused with a hint.  The TAR probe resolves per checked
TAR before sending and reports the effective spi1 per row.  The secured
packet form's SPI2 default is 00 (the RAM/probe operations pin their own).

PWA: the Cards form edits the TAR table (role rows with MSL inputs plus
Add/Remove free rows with a description), the list shows a TARs (MSL)
column, the SCP80 TAR field suggests the preset's entries (datalist) and a
preset apply seeds the SPI1 from the operation TAR's MSL with an amber
warning below it; the preset-wide SPI1/SPI2 fields and the header marker's
SPI requirement are gone (completeness = a keyset + the three role entries
with their MSLs).  Help EN/RU, docs/api.md and AGENTS updated; sw cache
simple-v358.

Live: TAR probe 000000 at its MSL 16 (registered, por_ok, counter
advanced); a send without spi1 derived 16 (por_ok, counter advanced); a
send with spi1 0A returned the warning and the card refused it (6200, no
counter consumed); an unknown TAR without spi1 was refused with the hint.
2026-10-02 08:28:46 +03:00
catarrh f059d05040 release: v3.9.1 - the counter probe sends a fixed read-only APDU
- The probe no longer takes a caller-supplied `apdu`: it always sends the
  read-only SELECT MF (the counter advances on the packet's security
  acceptance whatever the applet answers - 6A86/6D00 are normal there), so
  no caller can turn the sync probe into a write.
- Version 3.9.1: server VERSION, pyproject.toml and the PWA header; sw cache
  simple-v357; docs/api.md and the repo AGENTS.md updated, and the workspace
  AGENTS.md gains the v3.9.1 history entry (its "Currently" line was stale at
  v3.7.1).
2026-10-02 07:29:12 +03:00