aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-parse-args.js
blob: f9836ae3655210a1af2307441e5ad380688fcb15 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// 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));
// Pipes must forward each stage's own text too, otherwise splitPipe()'s raw stage is thrown
// away by a second _parseArgs() pass and `js "a b".length | wc -c` sees `a b.length`.
const chainSrc = termSrc.match(/async executePipeChain\(pipeParts\)\s*\{[\s\S]*?\n\s{12}\}/);
ok('executePipeChain keeps the raw stage', !!chainSrc && /dispatchCommand\(cmd, args, pipeParts\[i\]/.test(chainSrc[0]), chainSrc ? 'no pipeParts[i] passthrough' : 'executePipeChain not found');

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