aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-shell-test-cmd.js
blob: 6abdd11b0a4d931a62fe9ad9332f7913ed447a59 (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
// node modules/test/test-shell-test-cmd.js
//
// `test` / `[` were not registered, so `[ -f /x ] && echo yes` fell through to the AI fallback and
// asked a model whether a file exists (measured 2026-10-29 on v4.6.132). _evalTest() already had the
// operators; the command was missing. Contract pinned here:
//   success -> resolves silently
//   false   -> rejects with a `silent` error (so `&&`/`||` skip without printing "Error: ")
//   `[`     -> requires the closing bracket, never delegates
//   `test`  -> a non-POSIX argv still reaches the regression-suite runner registered before it
const fs = require('fs'), path = require('path');
// shell.js ends with `if (typeof window.webx11 !== 'undefined' && window.webx11) { … }` — and
// `typeof window.webx11` throws when `window` itself is undeclared, so give it an empty object.
global.window = {};
const src = fs.readFileSync(path.join(__dirname, '../ext/shell.js'), 'utf8');
const ShellModule = new Function(src + '\n;return typeof ShellModule !== "undefined" ? ShellModule : null;')();

let pass = 0, fail = 0;
const ok = (name, cond, detail) => { cond ? pass++ : (fail++, console.log('FAIL ' + name + (detail ? ' — ' + detail : ''))); };
const eq = (name, got, want) => ok(name, JSON.stringify(got) === JSON.stringify(want), 'got ' + JSON.stringify(got) + ' want ' + JSON.stringify(want));

function mk(filesArg) {
    const printed = [];
    const files = filesArg || { '/a.txt': 'x', '/dir': null };   // null => directory
    const term = {
        print: (m, lvl) => printed.push({ m: String(m), lvl: lvl || 'plain' }),
        markDirty: () => {},
        fs: {
            currentPath: [],
            readFile: p => (typeof files[p] === 'string' ? files[p] : null),
            readDir: p => (p in files && files[p] === null ? [] : null)
        },
        // A real (simple) parser: the assertions prove _tokenize delegates by counting the calls.
        security: { calls: 0, _parseArgs: function (s) { this.calls++; return s.trim() ? s.trim().split(/\s+/) : []; } }
    };
    const m = new ShellModule();
    m.terminal = term;
    m.registerCommands();
    return { m, term, printed };
}

const throws = async (fn) => { try { await fn(); return null; } catch (e) { return e; } };

(async () => {
    const { m, term, printed } = mk();

    ok('test command registered', typeof m.commands['test'] === 'function');
    ok('[ command registered', typeof m.commands['['] === 'function');

    ok('test -f existing file -> resolves', await throws(() => m.commands['test'](['-f', '/a.txt'])) === null);

    const missing = await throws(() => m.commands['test'](['-f', '/nope']));
    ok('test -f missing file -> rejects', !!missing);
    ok('rejection is silent', !!missing && missing.silent === true, 'silent=' + (missing && missing.silent));
    ok('rejection carries no message', !!missing && !missing.message, 'msg=' + JSON.stringify(missing && missing.message));

    ok('[ -f /a.txt ] -> resolves', await throws(() => m.commands['['](['-f', '/a.txt', ']'])) === null);

    const noBracket = await throws(() => m.commands['['](['-f', '/a.txt']));
    ok('[ without ] -> rejects', !!noBracket && /missing '\]'/.test(noBracket.message), noBracket && noBracket.message);
    ok('[ without ] is NOT silent (a syntax error must be visible)', !!noBracket && !noBracket.silent);

    ok('test -d dir -> resolves', await throws(() => m.commands['test'](['-d', '/dir'])) === null);
    ok('test -d file -> rejects', !!(await throws(() => m.commands['test'](['-d', '/a.txt']))));

    const noExpr = await throws(() => m.commands['test']([]));
    ok('test with no fallback and no args -> silent reject (POSIX false)', !!noExpr && noExpr.silent === true);

    // shared printer: silent errors print nothing, real ones print "Error: …"
    printed.length = 0;
    m._printErr(term, Object.assign(new Error(''), { silent: true }));
    ok('_printErr swallows silent errors', printed.length === 0, JSON.stringify(printed));
    m._printErr(term, new Error('boom'));
    eq('_printErr prints real errors', printed, [{ m: 'Error: boom', lvl: 'error' }]);

    // _tokenize must delegate to the security parser, never to split(/\s+/) of its own
    const before = term.security.calls;
    eq('_tokenize delegates to security._parseArgs', m._tokenize('anything at all'), ['anything', 'at', 'all']);
    ok('_tokenize actually called the parser', term.security.calls > before, 'calls=' + term.security.calls);
    eq('_tokenize empty -> []', m._tokenize('   '), []);
    eq('_tokenize undefined -> []', m._tokenize(undefined), []);

    // `test` also names the on-demand regression-suite runner ("test auto" / "test list").
    const { m: d } = mk();
    const seen = [];
    const prev = d.commands['test'];
    d.commands['test'] = (args) => d._cmdTest(args, 'test', (a) => { seen.push(a); return 'suite'; });
    ok('previous handler is what delegation falls back to', typeof prev === 'function');

    ok('test auto delegates to the suite runner', (await d.commands['test'](['auto'])) === 'suite' && JSON.stringify(seen) === '[["auto"]]', JSON.stringify(seen));
    ok('bare test delegates to the suite runner', (await d.commands['test']([])) === 'suite' && seen.length === 2, JSON.stringify(seen));
    ok('test -f still runs POSIX (no delegation)', (await throws(() => d.commands['test'](['-f', '/a.txt']))) === null && seen.length === 2, 'seen=' + JSON.stringify(seen));
    ok('string equality form runs POSIX', (await throws(() => d.commands['test'](['/a.txt', '=', '/a.txt']))) === null && seen.length === 2, 'seen=' + JSON.stringify(seen));
    const t2 = await throws(() => d.commands['['](['-f', '/nope', ']']));
    ok('[ never delegates', !!t2 && t2.silent === true, JSON.stringify(seen));

    console.log(pass + '/' + (pass + fail) + ' pass');
    process.exit(fail ? 1 : 0);
})();