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.
This commit is contained in:
@@ -321,6 +321,10 @@ from the form's source field; an explicit `src` overrides it. Both the
|
||||
"lac": "00FF", "cell": "0001"}}}
|
||||
```
|
||||
|
||||
The `event` value may be the code (a hex byte or a number) or one of the name
|
||||
keywords above; the editor resolves either form to the event - showing the
|
||||
right form and fields - and saves the hex byte form.
|
||||
|
||||
| `event` | fields |
|
||||
|---|---|
|
||||
| `location_status` / `0x03` | `status` 0 normal / 1 limited / 2 no service; for normal service `mcc`, `mnc`, `lac`, `cell` |
|
||||
|
||||
+30
-8
@@ -1905,7 +1905,7 @@
|
||||
// ===== Version =====
|
||||
// Single source of truth for the PWA version: shown in the header and used
|
||||
// by the server version check in pysimConnect().
|
||||
const SIMPLE_VERSION = '3.23.5';
|
||||
const SIMPLE_VERSION = '3.23.6';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== view history (the browser Back/Forward buttons) =====
|
||||
@@ -7372,9 +7372,11 @@ function testStepSummary(step) {
|
||||
: 'item=' + p.item_id);
|
||||
}
|
||||
else if (step.kind === 'event') {
|
||||
// a legacy numeric event is the code itself; the editor stores hex
|
||||
const code = (typeof p.event === 'number') ? p.event : parseInt(p.event, 16);
|
||||
s = 'EVENT ' + (EVENT_NAMES[code] || ('0x' + String(p.event || '?')));
|
||||
// the value may be a number, the editor's hex byte or a name keyword
|
||||
const code = eventCode(p.event);
|
||||
s = 'EVENT ' + (EVENT_NAMES[code] || (Number.isInteger(code)
|
||||
? '0x' + code.toString(16).toUpperCase().padStart(2, '0')
|
||||
: String(p.event || '?')));
|
||||
if (p.src) s += ' src=' + p.src;
|
||||
}
|
||||
else if (step.kind === 'apdu') s = 'APDU ' + (p.apdu || '?');
|
||||
@@ -8156,7 +8158,7 @@ function testStepRender() {
|
||||
const codes = Object.keys(EVENT_FORMS).map(k => parseInt(k, 10))
|
||||
.sort((a, b) => a - b);
|
||||
const rawEvent = p.event === undefined ? '03' : p.event;
|
||||
let cur = (typeof rawEvent === 'number') ? rawEvent : parseInt(String(rawEvent), 16);
|
||||
let cur = eventCode(rawEvent);
|
||||
if (!(Number.isInteger(cur) && cur >= 0 && cur <= 0xFF)) cur = 3;
|
||||
const opts = codes.map(c => ({
|
||||
v: c.toString(16).toUpperCase().padStart(2, '0'),
|
||||
@@ -8640,9 +8642,7 @@ function testStepFormError() {
|
||||
if (problem) return problem;
|
||||
}
|
||||
if (step.kind === 'event') {
|
||||
const rawEvent = (step.params || {}).event;
|
||||
const code = (typeof rawEvent === 'number') ? rawEvent
|
||||
: parseInt(String(rawEvent || ''), 16);
|
||||
const code = eventCode((step.params || {}).event);
|
||||
const cfg = EVENT_FORMS[code];
|
||||
if (!cfg) return t('No form for this event - use the raw Envelope action.');
|
||||
const bad = eventFormPatternError(cfg, (step.params || {}).fields || {});
|
||||
@@ -15222,6 +15222,28 @@ function pysimFsResetEdit() {
|
||||
// points at 3GPP for carry their TS 31.111 7.5 event name; "Reserved for
|
||||
// 3GPP" is a spec cross-reference, not a usage restriction, so it never
|
||||
// appears in the user-facing names.
|
||||
|
||||
// The test-script `event` value may be the editor's hex byte, a number or one
|
||||
// of the server's name keywords (pysim_simple_server/events.py's
|
||||
// EVENT_NAMES); this map mirrors it, drift-guarded through
|
||||
// frontend/tests/event_vectors.json ("aliases").
|
||||
const EVENT_ALIASES = {
|
||||
'location status': 0x03, location_status: 0x03, 'location-status': 0x03,
|
||||
'access technology change': 0x0B, access_tech: 0x0B, 'access-technology-change': 0x0B,
|
||||
'network rejection': 0x12, network_rejection: 0x12, 'network-rejection': 0x12,
|
||||
'data connection status change': 0x1D, data_connection: 0x1D, 'data-connection-status-change': 0x1D,
|
||||
};
|
||||
|
||||
// The event code of a script's `event` value (a number, a hex byte or a name
|
||||
// keyword), or NaN when it is none of them.
|
||||
function eventCode(value) {
|
||||
if (typeof value === 'number') return value;
|
||||
const s = String(value === undefined || value === null ? '' : value).trim();
|
||||
if (/^(0x)?[0-9a-fA-F]{1,2}$/.test(s)) return parseInt(s.replace(/^0x/i, ''), 16);
|
||||
const alias = EVENT_ALIASES[s.toLowerCase()];
|
||||
return alias === undefined ? NaN : alias;
|
||||
}
|
||||
|
||||
const EVENT_NAMES = {
|
||||
0x00: 'MT call', 0x01: 'Call connected', 0x02: 'Call disconnected',
|
||||
0x03: 'Location status', 0x04: 'User activity', 0x05: 'Idle screen available',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v418';
|
||||
const CACHE = 'simple-v419';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -31,6 +31,8 @@ function extractFunc(src, name) {
|
||||
// Rewrite top-level const -> var so the maps leak out of sloppy-mode eval.
|
||||
eval(extractBlock('const CMD_NAMES = {', 'function cmdQualifierShort').replace(/^const /gm, 'var '));
|
||||
eval(extractFunc(html, 'cmdQualifierShort'));
|
||||
eval(extractBlock('const EVENT_ALIASES = {', 'function eventCode').replace(/^const /gm, 'var '));
|
||||
eval(extractFunc(html, 'eventCode'));
|
||||
eval(extractBlock('const EVENT_NAMES = {', 'const REJECTION_CAUSES = [').replace(/^const /gm, 'var '));
|
||||
eval(extractBlock('const REJECTION_CAUSES = [', 'const EVENT_FORMS = {').replace(/^const /gm, 'var '));
|
||||
eval(extractBlock('const EVENT_FORMS = {', 'const PLI_QUALIFIERS = [').replace(/^const /gm, 'var '));
|
||||
@@ -324,12 +326,38 @@ test('the event form previews the bytes the send would deliver', () => {
|
||||
assert.strictEqual(els['event-send-preview'].value, '');
|
||||
});
|
||||
|
||||
test('a name-keyword event points at the right form', () => {
|
||||
// the server's name keywords (used by the applet test scripts) resolve to
|
||||
// their code, so the editor shows the right event and prefills its fields
|
||||
// instead of falling back to event 03
|
||||
const cfg = EVENT_FORMS[eventCode('access_tech')];
|
||||
assert.ok(cfg, 'the access-tech form');
|
||||
assert.deepStrictEqual(cfg.fields.map(f => f.id), ['tech']);
|
||||
assert.strictEqual(cfg.build({ tech: 8 }), 'BF0108');
|
||||
const dc = EVENT_FORMS[eventCode('data_connection')];
|
||||
assert.ok(dc, 'the data-connection form');
|
||||
assert.ok(dc.fields.some(f => f.id === 'status') && dc.fields.some(f => f.id === 'type'));
|
||||
});
|
||||
|
||||
test('the shared event vectors match the PWA builders', () => {
|
||||
// the same fixture is asserted against the Python builders by
|
||||
// tests/test_events.py - a change on either side fails the other suite
|
||||
// until the fixture and both implementations agree
|
||||
const vectors = JSON.parse(fs.readFileSync(path.join(__dirname, 'event_vectors.json'), 'utf8'));
|
||||
assert.ok(vectors.events.length && vectors.scts.length, 'the fixture must not be empty');
|
||||
// the event name aliases (the server's keywords) resolve to the codes
|
||||
assert.ok(vectors.aliases, 'the fixture must carry the aliases');
|
||||
const wantAliases = {};
|
||||
for (const [name, hex] of Object.entries(vectors.aliases)) {
|
||||
wantAliases[name] = parseInt(hex, 16);
|
||||
assert.strictEqual(eventCode(name), parseInt(hex, 16), name);
|
||||
}
|
||||
assert.deepStrictEqual(EVENT_ALIASES, wantAliases);
|
||||
assert.deepStrictEqual(eventCode(0x0B), 0x0B);
|
||||
assert.deepStrictEqual(eventCode('0B'), 0x0B);
|
||||
assert.deepStrictEqual(eventCode('0x1d'), 0x1D);
|
||||
assert.ok(Number.isNaN(eventCode('nonsense')));
|
||||
assert.ok(Number.isNaN(eventCode('')));
|
||||
for (const v of vectors.events) {
|
||||
const build = EVENT_FORMS[parseInt(v.event, 16)].build;
|
||||
assert.strictEqual(build(v.fields).toUpperCase(), v.hex,
|
||||
|
||||
@@ -18,6 +18,20 @@
|
||||
{"event": "1D", "fields": {"status": "2", "type": "1", "ti": "", "mcc": "250", "loc_status": "2"}, "hex": "9D0102AA01011C0100130752F010000000019B0102"},
|
||||
{"event": "1D", "fields": {"status": "0", "type": "2", "ti": "AA", "mcc": "262", "mnc": "01", "lac": "0001", "cell": "0002", "tech": "7", "loc_status": "1", "apn": "web.operator.example", "pdp_type": "4"}, "hex": "9D0100AA01021C01AA130762F21000010002BF01079B0101C7147765622E6F70657261746F722E6578616D706C650B0104"}
|
||||
],
|
||||
"aliases": {
|
||||
"location status": "03",
|
||||
"location_status": "03",
|
||||
"location-status": "03",
|
||||
"access technology change": "0B",
|
||||
"access_tech": "0B",
|
||||
"access-technology-change": "0B",
|
||||
"network rejection": "12",
|
||||
"network_rejection": "12",
|
||||
"network-rejection": "12",
|
||||
"data connection status change": "1D",
|
||||
"data_connection": "1D",
|
||||
"data-connection-status-change": "1D"
|
||||
},
|
||||
"scts": [
|
||||
{"year": 2026, "month": 9, "day": 30, "hour": 21, "min": 55, "sec": 0, "tz_east_min": 180, "hex": "62900312550021"},
|
||||
{"year": 2026, "month": 1, "day": 5, "hour": 8, "min": 7, "sec": 9, "tz_east_min": -150, "hex": "62105080709009"}
|
||||
|
||||
@@ -31,6 +31,9 @@ function extractFunc(src, name, asyncFn) {
|
||||
eval(extractBlock('const CMD_NAMES = {', 'function cmdQualifierShort').replace(/^const /gm, 'var '));
|
||||
eval(extractBlock('const EVENT_NAMES = {', 'const EVENT_FORMS = {').replace(/^const /gm, 'var '));
|
||||
eval(extractBlock('const EVENT_FORMS = {', 'const PLI_QUALIFIERS = [').replace(/^const /gm, 'var '));
|
||||
// the event name aliases (the server's EVENT_NAMES keywords) + the resolver
|
||||
eval(extractBlock('const EVENT_ALIASES = {', 'function eventCode').replace(/^const /gm, 'var '));
|
||||
eval(extractFunc(html, 'eventCode'));
|
||||
// the builders' pure helpers (eventFormFieldsHtml/Values are the shared form)
|
||||
eval(extractFunc(html, 'bytesToHex'));
|
||||
eval(extractFunc(html, 'eventAddressTlv'));
|
||||
@@ -195,6 +198,24 @@ test('testStepSummary renders actions', () => {
|
||||
assert.strictEqual(
|
||||
testStepSummary({ type: 'action', kind: 'status', params: { attempts: 5 } }),
|
||||
'STATUS x5 (poll)');
|
||||
// the event action's value may be a name keyword (the server's alias
|
||||
// form), the editor's hex byte or a number - all show the event name
|
||||
for (const ev of ['access_tech', '0B', '0b', 11, 0x0B]) {
|
||||
assert.strictEqual(
|
||||
testStepSummary({ type: 'action', kind: 'event', params: { event: ev } }),
|
||||
'EVENT Access technology change (single access technology)', String(ev));
|
||||
}
|
||||
assert.strictEqual(
|
||||
testStepSummary({ type: 'action', kind: 'event', params: { event: 'data_connection' },
|
||||
check: { sw: { mode: 'mask', value: '91??' } } }),
|
||||
'EVENT Data connection status change · SW ~91??');
|
||||
// a value that is neither a code nor a known name is shown as-is
|
||||
assert.strictEqual(
|
||||
testStepSummary({ type: 'action', kind: 'event', params: { event: 'nonsense' } }),
|
||||
'EVENT nonsense');
|
||||
assert.strictEqual(
|
||||
testStepSummary({ type: 'action', kind: 'event', params: { event: '1F' } }),
|
||||
'EVENT Slices status change');
|
||||
assert.strictEqual(
|
||||
testStepSummary({ type: 'action', kind: 'proactive-drain',
|
||||
params: { attempts: 5, respond: { result: 'ok' },
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.23.5"
|
||||
version = "3.23.6"
|
||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.23.5'
|
||||
VERSION = '3.23.6'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
|
||||
@@ -41,6 +41,16 @@ class EventVectorTests(unittest.TestCase):
|
||||
self.assertEqual(events.scts_bytes(stamp, v['tz_east_min']).hex().upper(),
|
||||
v['hex'])
|
||||
|
||||
def test_the_shared_event_name_aliases(self):
|
||||
# the script's `event` value may be one of these name keywords; the
|
||||
# PWA mirrors the map (frontend/tests/event_forms.test.js)
|
||||
want = {name: int(hexv, 16) for name, hexv in VECTORS['aliases'].items()}
|
||||
self.assertEqual(events.EVENT_NAMES, want)
|
||||
for name, code in want.items():
|
||||
self.assertEqual(events.resolve_event(name), code, name)
|
||||
self.assertEqual(events.resolve_event(hex(code)[2:].upper().zfill(2)), code)
|
||||
self.assertIsNone(events.resolve_event('nonsense'))
|
||||
|
||||
def test_the_host_clock_date_time_object(self):
|
||||
# the data-connection builder embeds the 8.39 object 26 07 ...
|
||||
out = events.build(0x1D, {'status': '0', 'type': '0', 'ti': '00',
|
||||
|
||||
Reference in New Issue
Block a user