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.
This commit is contained in:
2026-10-07 16:41:53 +03:00
parent cc335def00
commit e0dfc1032e
14 changed files with 627 additions and 86 deletions
+10
View File
@@ -57,6 +57,16 @@ globalThis.testSuites = () => _testSuites || [];
eval(extractFunc(html, 'testFormRow'));
eval(extractFunc(html, 'testFormInput'));
eval(extractFunc(html, 'testFormSelect'));
// the TERMINAL RESPONSE result tables + the shared result-row helpers
eval(extractBlock('const TR_RESULTS = [', 'const TR_RESULT_ADD_INFO = [').replace(/^const /gm, 'var '));
eval(extractBlock('const TR_RESULT_ADD_INFO = [', 'const TR_RESULT_ALIASES = {').replace(/^const /gm, 'var '));
eval(extractBlock('const TR_RESULT_ALIASES = {', 'function trResultOptions').replace(/^const /gm, 'var '));
eval(extractFunc(html, 'trResultOptions'));
eval(extractFunc(html, 'trResultFormValue'));
eval(extractFunc(html, 'trResultValid'));
eval(extractFunc(html, 'trResultCollect'));
eval(extractFunc(html, 'testTrResultChanged'));
eval(extractFunc(html, 'trResultRowHtml'));
eval(extractFunc(html, 'testStepRender'));
eval(extractFunc(html, 'testRenderChecks'));
eval(extractFunc(html, 'testStepCollect'));
+73
View File
@@ -0,0 +1,73 @@
{
"comment": "TERMINAL RESPONSE general results (TS 102 223 v18.3.0 8.12.0) and the terminal-problem additional information (8.12.2). Mirrored by pysim_simple_server/testscript.py (TR_RESULTS/TR_RESULT_ADD_INFO/RESULT_NAMES) and frontend/index.html (the same names); both suites drift-guard against this fixture.",
"results": [
{"v": "00", "name": "Command performed successfully", "alias": "ok"},
{"v": "01", "name": "Command performed with partial comprehension", "alias": "partial"},
{"v": "02", "name": "Command performed, with missing information", "alias": "missing"},
{"v": "03", "name": "REFRESH performed with additional EFs read"},
{"v": "04", "name": "Command performed successfully, but requested icon could not be displayed"},
{"v": "05", "name": "Command performed, but modified by call control by NAA"},
{"v": "06", "name": "Command performed successfully, limited service"},
{"v": "07", "name": "Command performed with modification", "alias": "modified"},
{"v": "08", "name": "REFRESH performed but indicated NAA was not active"},
{"v": "09", "name": "Command performed successfully, tone not played"},
{"v": "10", "name": "Proactive UICC session terminated by the user", "alias": "cancel"},
{"v": "11", "name": "Backward move in the proactive UICC session requested by the user", "alias": "back"},
{"v": "12", "name": "No response from user", "alias": "timeout"},
{"v": "13", "name": "Help information required by the user"},
{"v": "14", "name": "Reserved for GSM/3G"},
{"v": "15", "name": "Reserved for 3GPP (for future usage)"},
{"v": "16", "name": "Reserved for 3GPP (for future usage)"},
{"v": "20", "name": "Terminal currently unable to process command"},
{"v": "21", "name": "Network currently unable to process command"},
{"v": "22", "name": "User did not accept the proactive command", "alias": "refused"},
{"v": "23", "name": "User cleared down call before connection or network release"},
{"v": "24", "name": "Action in contradiction with the current timer state"},
{"v": "25", "name": "Interaction with call control by NAA, temporary problem"},
{"v": "26", "name": "Launch browser generic error"},
{"v": "27", "name": "MMS temporary problem"},
{"v": "28", "name": "Reserved for 3GPP (BIP temporary error, T timer in the additional information)"},
{"v": "29", "name": "Reserved for 3GPP (for future usage)"},
{"v": "30", "name": "Command beyond terminal's capabilities"},
{"v": "31", "name": "Command type not understood by terminal"},
{"v": "32", "name": "Command data not understood by terminal", "alias": "not_understood"},
{"v": "33", "name": "Command number not known by terminal"},
{"v": "34", "name": "Reserved for GSM/3G"},
{"v": "35", "name": "Reserved for GSM/3G"},
{"v": "36", "name": "Error, required values are missing"},
{"v": "37", "name": "Reserved for GSM/3G"},
{"v": "38", "name": "MultipleCard commands error"},
{"v": "39", "name": "Interaction with call control by NAA, permanent problem"},
{"v": "3A", "name": "Bearer Independent Protocol permanent error"},
{"v": "3B", "name": "Access Technology unable to process command"},
{"v": "3C", "name": "Frames error"},
{"v": "3D", "name": "MMS Error"},
{"v": "3E", "name": "Reserved for 3GPP (for future usage)"},
{"v": "3F", "name": "Reserved for 3GPP (for future usage)"}
],
"aliases": {
"ok": "00",
"partial": "01",
"missing": "02",
"refused": "22",
"not_understood": "32",
"modified": "07",
"cancel": "10",
"back": "11",
"timeout": "12",
"no_response": "12"
},
"additional_info": [
{"v": "00", "name": "No specific cause can be given"},
{"v": "01", "name": "Screen is busy"},
{"v": "02", "name": "Terminal currently busy on call"},
{"v": "03", "name": "Reserved for GSM/3G"},
{"v": "04", "name": "No service"},
{"v": "05", "name": "Access control class bar"},
{"v": "06", "name": "Radio resource not granted"},
{"v": "07", "name": "Not in speech call"},
{"v": "08", "name": "Reserved for GSM/3G"},
{"v": "09", "name": "Terminal currently busy on SEND DTMF command"},
{"v": "0A", "name": "No NAA active"}
]
}
+122
View File
@@ -0,0 +1,122 @@
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');
const fixture = JSON.parse(fs.readFileSync(path.join(__dirname, 'tr_results.json'), '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);
}
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);
}
// Rewrite top-level const -> var so the tables leak out of sloppy-mode eval.
eval(extractBlock('const TR_RESULTS = [', 'const TR_RESULT_ADD_INFO = [').replace(/^const /gm, 'var '));
eval(extractBlock('const TR_RESULT_ADD_INFO = [', 'const TR_RESULT_ALIASES = {').replace(/^const /gm, 'var '));
eval(extractBlock('const TR_RESULT_ALIASES = {', 'function trResultOptions').replace(/^const /gm, 'var '));
eval(extractFunc(html, 'esc'));
eval(extractFunc(html, 'trResultOptions'));
eval(extractFunc(html, 'trResultFormValue'));
eval(extractFunc(html, 'trResultValid'));
eval(extractFunc(html, 'trResultCollect'));
eval(extractFunc(html, 'testTrResultChanged'));
eval(extractFunc(html, 'testFormInput'));
eval(extractFunc(html, 'testFormSelect'));
eval(extractFunc(html, 'trResultRowHtml'));
globalThis.t = s => s;
const fixtureResults = fixture.results.map(r => {
const o = { v: r.v, name: r.name };
if (r.alias) o.alias = r.alias;
return o;
});
test('the PWA result tables match the shared fixture', () => {
assert.deepStrictEqual(TR_RESULTS, fixtureResults);
assert.deepStrictEqual(TR_RESULT_ADD_INFO, fixture.additional_info);
assert.deepStrictEqual(TR_RESULT_ALIASES, fixture.aliases);
});
test('the corrected aliases carry the TS 102 223 8.12.0 values', () => {
assert.strictEqual(TR_RESULT_ALIASES.refused, '22');
assert.strictEqual(TR_RESULT_ALIASES.not_understood, '32');
assert.strictEqual(TR_RESULT_ALIASES.modified, '07');
assert.strictEqual(TR_RESULT_ALIASES.no_response, '12');
assert.strictEqual(TR_RESULT_ALIASES.ok, '00');
});
test('trResultOptions labels every entry with its hex value', () => {
const opts = trResultOptions();
// the full list plus the manual entry
assert.strictEqual(opts.length, TR_RESULTS.length + 1);
assert.deepStrictEqual(opts[0], { v: 'ok', l: '00 - ok' });
const byValue = {};
opts.forEach(o => { byValue[o.v] = o.l; });
assert.strictEqual(byValue.modified, '07 - modified');
assert.strictEqual(byValue['09'], '09 - Command performed successfully, tone not played');
assert.strictEqual(byValue['20'], '20 - Terminal currently unable to process command');
assert.strictEqual(byValue['3F'], '3F - Reserved for 3GPP (for future usage)');
assert.deepStrictEqual(opts[opts.length - 1], { v: 'manual', l: 'manual input' });
// the compat-only alias is not offered (12's canonical alias is timeout)
assert.ok(!('no_response' in byValue), 'no_response must not be an option');
});
test('trResultFormValue maps the stored alias/hex/manual values', () => {
assert.deepStrictEqual(trResultFormValue('ok'), { value: 'ok', manual: '' });
assert.deepStrictEqual(trResultFormValue('OK'), { value: 'ok', manual: '' });
assert.deepStrictEqual(trResultFormValue('07'), { value: 'modified', manual: '' });
assert.deepStrictEqual(trResultFormValue('0x09'), { value: '09', manual: '' });
assert.deepStrictEqual(trResultFormValue('1a'), { value: 'manual', manual: '1A' });
assert.deepStrictEqual(trResultFormValue(''), { value: '', manual: '' });
assert.deepStrictEqual(trResultFormValue(undefined), { value: '', manual: '' });
assert.deepStrictEqual(trResultFormValue('bogus'), { value: 'ok', manual: '' });
});
test('trResultValid accepts the aliases and hex bytes only', () => {
['ok', 'refused', '07', '0x0a', 'FF'].forEach(v => assert.ok(trResultValid(v), v));
['manual', '', 'f', 'zzz'].forEach(v => assert.ok(!trResultValid(v), v));
});
test('the step editor offers the full list for all three result fields', () => {
const form = extractFunc(html, 'testStepRender');
assert.ok(form.includes("trResultRowHtml('test-f-result'"), 'the expectation result');
assert.ok(form.includes("trResultRowHtml('test-f-presult'"), 'the drain result');
assert.ok(form.includes("trResultRowHtml('test-f-pfirst'"), 'the drain first');
assert.ok(form.includes("{empty: 'same as above'}"), 'the first keeps its empty option');
});
test('trResultRowHtml renders the manual entry and the additional info', () => {
const out = trResultRowHtml('test-f-result', '07', '04');
assert.ok(out.includes('07 - modified'), out);
assert.ok(out.includes('value="04"'), out);
assert.ok(out.includes('id="tr-addinfo-list"'), out);
assert.ok(out.includes('No specific cause can be given'), out);
assert.ok(out.includes('id="test-f-result-hexwrap" class="w-24 hidden"'), out);
const manual = trResultRowHtml('test-f-result', '1A', '');
assert.ok(manual.includes('id="test-f-result-hexwrap" class="w-24"'), manual);
assert.ok(manual.includes('value="1A"'), manual);
assert.ok(manual.includes('<option value="manual" selected>'), manual);
// a server-normalised (integer) additional info shows as hex
const intAdd = trResultRowHtml('test-f-result', '07', 10);
assert.ok(intAdd.includes('value="0A"'), intAdd);
});