aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-parse-args.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-29 12:29:14 +0100
committerAgando <roduccomm@gmail.com>2026-10-29 12:29:14 +0100
commitdb65ce7628b0073c41ff1c13334723a043b24479 (patch)
tree1fd5bc4711d7f385937a22b7ae5e58513e06c3f0 /modules/test/test-parse-args.js
parent43d067396b5a620441b9a299c45e7b0b129ccd15 (diff)
fix(newos4): js keeps its string literals — dispatcher forwards the raw command line (v4.6.130)
_parseArgs() treats quote characters as delimiters and drops them, so every string literal in a js expression died before eval: `js "abc".toUpperCase()` reached the module as `abc.toUpperCase()` → ReferenceError: abc is not defined. Same for `js import('fs')` → `import(fs)`. Found while verifying the v4.6.128 console fix (plan 2026-10-29_112000-newos4-parseargs-strips-js-string-literals.md). - terminal.js: checkAndRun() records the untokenised line (`_currentRaw`), _dispatch() forwards it, dispatchCommand(cmd, args, rawText) passes it to the module. Security classification still runs on the tokenised form, and pipes/AI/script-runner calls pass nothing, so they behave as before. - console.js: `js` evaluates _expr(args, rawText) — the raw remainder. `node -e` deliberately does NOT: the tokenizer already keeps `'…'` inside `"…"`, and the raw form would hand eval the string literal `"import('fs')"` instead of the import (caught by the live probe, not by review). js-run keeps tokenised paths. - modules/test/test-parse-args.js: tokenizer table (records the deletion as the current contract), dispatcher plumbing asserts, _expr cases, and the user-visible paths. 27 assertions. - module-loader.js + main.html + bundle: v4.6.130 (129 was minted, then console.js changed again — re-minting so no cached client keeps the intermediate). Verification node modules/test/test-parse-args.js -> 27/27 pass same file against HEAD sources (git show HEAD:...) -> TypeError: _expr is not a function (RED) scripts/lint-no-undef.py terminal.js console.js -> clean, 122 cross-file globals core-bundle.js rebuilt from HEAD sources + this patch in /tmp/iso (the working tree carries another session's uncommitted ollama-manager.js `_httpError`); diff vs HEAD bundle = 14 lines, all mine. critical-ext-bundle.js byte-identical. Live page v4.6.130 (real executeCommand, output captured): js "abc".toUpperCase() -> ABC js 'a b'.split(' ').length -> 2 js JSON.stringify({a:1}) -> {"a":1} js 6*7 -> 42 js import('fs') -> red "Failed to resolve module specifier 'fs'" + Node-builtin hint node -e "import('fs')" -> same error + hint (was printing the expression as a string) echo "two words" -> two words js-run /nope.js -> js-run: /nope.js: No such file unhandledrejection count -> 0 The sibling's parked terminal.js hunk (BillingModule registry entry) was stashed for this commit and is restored in the working tree afterwards; it is not part of this change.
Diffstat (limited to 'modules/test/test-parse-args.js')
-rw-r--r--modules/test/test-parse-args.js116
1 files changed, 116 insertions, 0 deletions
diff --git a/modules/test/test-parse-args.js b/modules/test/test-parse-args.js
new file mode 100644
index 0000000..75fd7ab
--- /dev/null
+++ b/modules/test/test-parse-args.js
@@ -0,0 +1,116 @@
+// node modules/test/test-parse-args.js
+//
+// Freezes the tokenizer contract AND proves the raw-passthrough that lets `js` see string
+// literals again (v4.6.130). Two layers:
+// 1. _parseArgs — table of input -> tokens. It deletes quote characters by design; the
+// table records that as the CURRENT contract, not as desired behaviour.
+// 2. evalJS/_expr — the console command gets the untokenised remainder from the dispatcher
+// (dispatchCommand(cmd, args, rawText)), so `js "abc".toUpperCase()` works
+// while `echo "hi"` still prints `hi`.
+const fs = require('fs');
+const path = require('path');
+
+const coreDir = path.join(__dirname, '../core');
+const extDir = path.join(__dirname, '../ext');
+const termSrc = fs.readFileSync(path.join(coreDir, 'terminal.js'), 'utf8');
+
+let fails = 0, passes = 0;
+const eq = (name, got, want) => {
+ const g = JSON.stringify(got), w = JSON.stringify(want);
+ if (g === w) { passes++; return; }
+ fails++;
+ console.log(`FAIL ${name}\n got ${g}\n want ${w}`);
+};
+const ok = (name, cond, detail) => {
+ if (cond) { passes++; return; }
+ fails++;
+ console.log(`FAIL ${name}${detail ? '\n ' + detail : ''}`);
+};
+
+// ---- 1. tokenizer table ---------------------------------------------------------------
+const m = termSrc.match(/_parseArgs\((\w+)\)\s*\{[\s\S]*?\n\s{12}\}/);
+if (!m) { console.log('FAIL _parseArgs not found — signature or indent changed'); process.exit(1); }
+const parseArgs = new Function('return ({' + m[0] + '})._parseArgs;')();
+
+const table = [
+ ['', []],
+ [' ', []],
+ ['js', ['js']],
+ ['js 1+1', ['js', '1+1']],
+ ['js "a b"', ['js', 'a b']], // spaces survive, quotes do not
+ ['js "abc".toUpperCase()', ['js', 'abc.toUpperCase()']], // the defect, recorded
+ ["js import('fs')", ['js', 'import(fs)']], // ditto
+ ['node -e "import(\'fs\')"', ['node', '-e', "import('fs')"]], // inner quotes survive inDouble
+ ["'unbalanced", ['unbalanced']],
+ ['echo "x" > f', ['echo', 'x', '>', 'f']],
+];
+for (const [input, want] of table) eq(`_parseArgs(${JSON.stringify(input)})`, parseArgs(input), want);
+
+// ---- 2. dispatcher plumbing ------------------------------------------------------------
+ok('dispatchCommand takes rawText', /async dispatchCommand\(cmd, args, rawText\)/.test(termSrc));
+ok('module receives rawText', /mod\.commands\[cmd\]\(args, rawText\)/.test(termSrc));
+ok('checkAndRun records raw input', /this\._currentRaw\s*=\s*rawInput/.test(termSrc));
+ok('_dispatch forwards raw input', /dispatchCommand\(cmd, args, this\._currentRaw/.test(termSrc));
+
+// ---- 3. console command uses the raw expression ---------------------------------------
+const consoleSrc = fs.readFileSync(path.join(extDir, 'console.js'), 'utf8');
+const JSConsoleModule = new Function(consoleSrc + '\n;return JSConsoleModule;')();
+
+function mk() {
+ const out = [];
+ const inst = new JSConsoleModule();
+ inst.terminal = { print: (msg, lvl) => out.push({ msg: String(msg), lvl: lvl || 'plain' }) };
+ inst.registerCommands();
+ return { inst, out };
+}
+
+const { inst: probe } = mk();
+eq('_expr from raw (js)', probe._expr(['abc.toUpperCase()'], 'js "abc".toUpperCase()'), '"abc".toUpperCase()');
+eq('_expr falls back to args', probe._expr(['1+1'], undefined), '1+1');
+eq('_expr empty stays empty', probe._expr([], 'js'), '');
+
+// node -e deliberately keeps the tokenised args: the raw form would turn `import('fs')`
+// into the string literal `"import('fs')"`. The tokenizer already keeps `'…'` inside `"…"`.
+
+// the behaviour the user reported: a string literal must survive the dispatcher
+(async () => {
+ const tick = () => new Promise(r => setTimeout(r, 60));
+
+ const n1 = mk();
+ n1.inst.commands['node'](['-e', "import('fs')"]);
+ await tick();
+ ok('node -e passes the import, not a string literal',
+ n1.out.length >= 1 && !/^import\('fs'\)$/.test(n1.out[0].msg), JSON.stringify(n1.out));
+
+ const t1 = mk();
+ t1.inst.evalJS(['abc.toUpperCase()'], 'js "abc".toUpperCase()');
+ eq('js "abc".toUpperCase() -> ABC', t1.out, [{ msg: 'ABC', lvl: 'success' }]);
+
+ // A failed dynamic import: Node rejects, the browser rejects with exactly this shape.
+ // Node cannot resolve a bare `fs` from CJS, so use a module specifier that really fails.
+ const t2 = mk();
+ t2.inst.evalJS(['x'], "js import('node:nonexistent-v4-6-129')");
+ await tick();
+ eq('js import(<missing>) prints two lines', t2.out.length, 2);
+ eq(' first line is an error', t2.out[0] && t2.out[0].lvl, 'error');
+ ok(' second line is the Node-builtin hint', t2.out[1] && /is a Node built-in/.test(t2.out[1].msg),
+ JSON.stringify(t2.out));
+
+ const t3 = mk();
+ t3.inst.evalJS(['1+1'], 'js 1+1');
+ eq('js 1+1 -> 2 (unchanged)', t3.out, [{ msg: '2', lvl: 'success' }]);
+
+ const t4 = mk(); // programmatic call (AI tool, script runner) has no rawText
+ t4.inst.evalJS(['6*7']);
+ eq('evalJS without rawText -> 42', t4.out, [{ msg: '42', lvl: 'success' }]);
+
+ // Only js/node consult rawText — js-run keeps tokenised paths, every other command
+ // (echo, grep, cat) never reads it, so their quoted arguments behave exactly as before.
+ ok('js-run does not take rawText', /commands\['js-run'\] = \(args\) => this\.evalJSFile\(args\)/.test(consoleSrc));
+ ok('_expr is used only by evalJS', (consoleSrc.match(/_expr\(/g) || []).length === 2,
+ String((consoleSrc.match(/_expr\(/g) || []).length));
+ eq('_parseArgs keeps a quoted space as one token', parseArgs('echo "two words"'), ['echo', 'two words']);
+
+ console.log(`${passes}/${passes + fails} pass`);
+ process.exit(fails ? 1 : 0);
+})();