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:
2026-10-07 21:56:20 +03:00
parent 02a78fcca6
commit 9fb46b786f
9 changed files with 110 additions and 11 deletions
+28
View File
@@ -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,
+14
View File
@@ -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"}
+21
View File
@@ -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' },