9fd8238051
The v3.25.5 report's follow-ups: a field the server could not use was silently dropped, and the PWA also filtered non-hex fields client-side (never sending them). The POST now collects them and names them in the response - `invalid` (a malformed key/value: not hex, an odd number of digits, a non-string value) and `unknown` (a well-formed code outside the qualifier list), both omitted when everything was applied - and the PWA's Save sends every field, composes "Not saved: 0A (invalid value), 18 (unknown qualifier)" (`pysimPliRejectMsg`) into a new #pli-save-msg line under the button and shows "Not saved" briefly. The guide's timer exception now says "card-initiated automatic action" (the timer retries every 5 s; polling/menu are suspended, not retrying). Docs: api.md's POST contract, help EN/RU - which also said "22 PLI qualifiers" while the list has 25. Tests: the rejection lists per case (odd length, non-hex value/key, non-string, unknown code) and the valid-save absence, the PWA message helper and the wiring pins. 887 frontend + 875 Python; sw cache simple-v430.
130 lines
6.0 KiB
JavaScript
130 lines
6.0 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
|
|
|
function extractBlock(startMarker, endMarker) {
|
|
const start = html.indexOf(startMarker);
|
|
const end = html.indexOf(endMarker, start);
|
|
if (start < 0 || end < 0) throw new Error('block not found: ' + startMarker);
|
|
return html.slice(start, end);
|
|
}
|
|
|
|
// The codec map's select fields reference ACCESSTECH; only the date/time
|
|
// codec is exercised here, but the map literal still evaluates that list.
|
|
eval(extractBlock('const ACCESSTECH = [', 'const EVENT_FORMS = {').replace(/^const /gm, 'var '));
|
|
eval(extractBlock('// ===== PLI data dictionary =====', 'async function pysimPliRender')
|
|
.replace(/^const /gm, 'var '));
|
|
|
|
test('the PLI date/time codec uses the 8.39 TP-SCTS coding', () => {
|
|
const c = PLI_CODECS['03'];
|
|
assert.strictEqual(c.tlv, '2607');
|
|
// +03:00: swapped semi-octet BCD digits, the time zone in quarters of an
|
|
// hour (0x21 = 12 quarters) - the eventDateTimeTlv / _encode_scts vectors
|
|
assert.strictEqual(c.encode({ dt: '2026-09-30', tm: '21:55:00', tz: '+03:00' }),
|
|
'260762900312550021');
|
|
assert.deepStrictEqual(c.decode('260762900312550021'),
|
|
{ dt: '2026-09-30', tm: '21:55:00', tz: '+03:00' });
|
|
// negative zones carry the sign bit 0x08: -2:30 = 10 quarters -> 0x09
|
|
assert.strictEqual(c.encode({ dt: '2026-01-05', tm: '08:07:09', tz: '-02:30' }),
|
|
'260762105080709009');
|
|
assert.deepStrictEqual(c.decode('260762105080709009'),
|
|
{ dt: '2026-01-05', tm: '08:07:09', tz: '-02:30' });
|
|
// 'FF' time zone = unknown (TS 123 040 9.2.3.11), never a bogus offset
|
|
assert.strictEqual(c.decode('2607629003125500FF').tz, 'unknown');
|
|
// a truncated value still returns null instead of throwing
|
|
assert.strictEqual(c.decode('2607123'), null);
|
|
});
|
|
|
|
test('the PLI codec tags follow TS 101 220 Table 7.23, base (CR=0) form', () => {
|
|
// the TR's response objects are Min = N, so TS 102 223 6.8.0 says they
|
|
// "should set the CR flag to comprehension not required" (v3.25.2):
|
|
// the base tag values (13/14/2D/2E/3F/62/65/63/26), not the CR-set forms
|
|
assert.strictEqual(PLI_CODECS['00'].tlv, '1305');
|
|
assert.strictEqual(PLI_CODECS['01'].tlv, '1408');
|
|
assert.strictEqual(PLI_CODECS['01'].encode({ imei: '490154203237518' }), '140894104502237315f8');
|
|
assert.strictEqual(PLI_CODECS['04'].tlv, '2D02');
|
|
assert.strictEqual(PLI_CODECS['06'].tlv, '3F01');
|
|
// 05 Timing Advance: 2E (C6 is a 3GPP2 tag)
|
|
assert.strictEqual(PLI_CODECS['05'].tlv, '2E02');
|
|
assert.strictEqual(PLI_CODECS['05'].encode({ status: '0', ta: '0' }), '2E020000');
|
|
// 08 IMEISV: 62 (94 is the IMEI tag)
|
|
assert.strictEqual(PLI_CODECS['08'].tlv, '6208');
|
|
assert.strictEqual(PLI_CODECS['08'].encode({ imeisv: '4901542032375108' }),
|
|
'62089410450223731580');
|
|
// 09 Network search mode: 65, '00' Manual / '01' Automatic (8.75)
|
|
assert.strictEqual(PLI_CODECS['09'].tlv, '6501');
|
|
assert.strictEqual(PLI_CODECS['09'].fields[0].opts[0].l, 'Manual');
|
|
assert.strictEqual(PLI_CODECS['09'].fields[0].opts[1].l, 'Automatic');
|
|
// 0A Battery state: 63/E3 is an enum, not a percentage (8.76)
|
|
assert.strictEqual(PLI_CODECS['0A'].tlv, '6301');
|
|
assert.strictEqual(PLI_CODECS['0A'].encode({ charge: '4' }), '630104');
|
|
assert.strictEqual(PLI_CODECS['0A'].encode({ charge: '0' }), '630100');
|
|
assert.strictEqual(PLI_CODECS['0E'].tlv, '3F');
|
|
});
|
|
|
|
test('the IMEI encoder pads the odd 15th digit with F (TS 124 008)', () => {
|
|
// the encoder's digits are lowercase (the server accepts either case)
|
|
assert.strictEqual(encImeiPli('490154203237518'), '94104502237315f8');
|
|
assert.strictEqual(decImei('94104502237315F8'), '490154203237518');
|
|
});
|
|
|
|
test('the server PLI defaults decode through their codecs', () => {
|
|
// mirrors PLI_DEFAULTS in server.py (v3.23.1; a drift guard for the codecs)
|
|
const defaults = {
|
|
'00': '130500F1100000', '01': '140894104502237315F8',
|
|
'04': '2D02656E', '05': '2E020000', '06': '3F0108',
|
|
'08': '62089410450223731580', '09': '650101',
|
|
'0A': '630104', '0E': '3F020308',
|
|
};
|
|
for (const [code, hex] of Object.entries(defaults)) {
|
|
assert.ok(PLI_CODECS[code].decode(hex), code + ' must decode');
|
|
}
|
|
assert.strictEqual(PLI_CODECS['01'].decode('140894104502237315F8').imei,
|
|
'490154203237518');
|
|
assert.deepStrictEqual(PLI_CODECS['09'].decode('650101'), { mode: '1' });
|
|
assert.deepStrictEqual(PLI_CODECS['0A'].decode('630104'), { charge: '4' });
|
|
});
|
|
|
|
// the same extraction the profiler tests use
|
|
function extractFunc(src, name) {
|
|
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
|
const m = re.exec(src);
|
|
if (!m) throw new Error('function ' + name + ' not found');
|
|
let i = m.index + m[0].length - 1;
|
|
let depth = 0;
|
|
for (; i < src.length; i++) {
|
|
if (src[i] === '{') depth++;
|
|
else if (src[i] === '}') {
|
|
depth--;
|
|
if (depth === 0) break;
|
|
}
|
|
}
|
|
return src.slice(m.index, i + 1);
|
|
}
|
|
|
|
test('a rejected PLI save is reported, not silently dropped', () => {
|
|
// the server names the fields it could not use (invalid/unknown) and the
|
|
// PWA shows them under the Save button (v3.25.6)
|
|
global.t = s => s;
|
|
eval(extractFunc(html, 'pysimPliRejectMsg'));
|
|
assert.strictEqual(pysimPliRejectMsg({}), '');
|
|
assert.strictEqual(pysimPliRejectMsg({ '0A': '630104' }), '');
|
|
assert.strictEqual(pysimPliRejectMsg({ invalid: ['0A'] }),
|
|
'Not saved: 0A (invalid value)');
|
|
assert.strictEqual(pysimPliRejectMsg({ unknown: ['18'] }),
|
|
'Not saved: 18 (unknown qualifier)');
|
|
assert.strictEqual(pysimPliRejectMsg({ invalid: ['0A'], unknown: ['18'] }),
|
|
'Not saved: 0A (invalid value), 18 (unknown qualifier)');
|
|
// the save posts every field (no silent client-side skip) and wires the
|
|
// message element + the reject branch
|
|
assert.ok(html.includes('if (el) data[q.code] = el.value.trim()'),
|
|
'every field is sent to the server for validation');
|
|
assert.ok(html.includes('id="pli-save-msg"'), 'the message element exists');
|
|
assert.ok(html.includes('const reject = pysimPliRejectMsg(resp);'),
|
|
'the save reports the rejection');
|
|
delete global.t;
|
|
});
|