// 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() 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); })();