diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-29 12:53:41 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-29 12:53:41 +0100 |
| commit | 6daf236b585dc47ad8ffe293cdefd5de8231c5d8 (patch) | |
| tree | df2f7c72def23f4664090689c3a9f54f28d5cb69 | |
| parent | 1a27ba0e2ec736aa9b30c69a3aa2ee2ab86d0709 (diff) | |
feat(newos4): register `test` / `[` so chains decide locally instead of asking a model (v4.6.135)
`[ -f "/qt.txt" ] && echo exists` — the probe that closed the v4.6.132 batch — was not a shell test
at all: both commands were unregistered, so the line fell through to the AI fallback and a model was
asked whether a file exists. `_evalTest()` has implemented `-f -d -e -z -n ! = !=` for the if/while
parser since the script runner was written; only the command was missing.
* `test` and `[` registered in ShellModule. Success resolves silently; false rejects with a
`silent` error so `&&`/`||` skip it — POSIX prints nothing on exit 1.
* `[` requires the closing bracket (`[ : missing ']'`), and that message is NOT silent.
* `test` already names the on-demand regression-suite runner ("test auto" / "test list" in
help.html). A argv that does not look like a test expression (`-f`/`-d`/`-e`/`-z`/`-n`/`!`,
or `a = b` / `a != b`) is delegated to the handler registered before us, so `test auto`
still runs the suite and the two meanings cannot collide.
* terminal.dispatchCommand() now honours `err.silent` — without it a top-level `test -f /nope`
printed `Error in ShellModule.test:` (measured on v4.6.134, fixed in v4.6.135).
* help.html documents the bracket form next to the existing `test` row.
Three tags were minted minutes apart — v4.6.133 (commands), v4.6.134 (suite delegation),
v4.6.135 (silent top-level) — each a real cache-bust for a fix that superseded the previous build;
the net change is this commit.
Verification
node modules/test/test-shell-test-cmd.js -> 24/24 pass (RED against HEAD: "test command
registered" fails, "m.commands.test is not a function")
node modules/test/test-pipe-guard.js -> all seven flags true
node modules/test/test-parse-args.js -> 28/28 · daily job -> node tests 22/22 pass
newos4-maintain.py -> atlas 355 commands (was 354), modules-index 349, help 364/364
live v4.6.135, read from the terminal DOM:
[ -f /qt.txt ] && echo F135 -> F135
test -f /nope || echo M135 -> M135
[ -d /nope ] && echo BAD || echo N134-> N134 (false branch skipped, nothing printed)
test -> no output (suite not loaded; POSIX false)
test -f /nope -> no output
js "a b".length | wc -c -> 1 (v4.6.132 pipe fix still intact)
The AI fallback is no longer reachable for a bracketed file test.
| -rw-r--r-- | course/atlas.json | 158 | ||||
| -rw-r--r-- | help.html | 1 | ||||
| -rw-r--r-- | main.html | 6 | ||||
| -rw-r--r-- | modules/core/core-bundle.js | 3 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/terminal.js | 3 | ||||
| -rw-r--r-- | modules/ext/critical-ext-bundle.js | 44 | ||||
| -rw-r--r-- | modules/ext/shell.js | 44 | ||||
| -rw-r--r-- | modules/test/test-shell-test-cmd.js | 98 |
9 files changed, 321 insertions, 38 deletions
diff --git a/course/atlas.json b/course/atlas.json index 4eeeff4..341ddb3 100644 --- a/course/atlas.json +++ b/course/atlas.json @@ -1,6 +1,6 @@ { - "version": "2026-10-21T23:46:07Z", - "source_hash": "fdf2b4d2aaf3dbc0", + "version": "2026-10-29T11:53:19Z", + "source_hash": "9f5f08a8dd1b7f05", "commands": { "a2a-ack": { "module": "module-agent-registry", @@ -587,6 +587,15 @@ "builtin": false, "example": "contract-register" }, + "cost": { + "module": "module-billing", + "file": "modules/core/module-billing.js", + "tier": "core-bundle", + "category": "general", + "desc": "cost [on|off] — AI prices, today's free-message allowance and this session's spend", + "builtin": false, + "example": "cost" + }, "course": { "module": "course", "file": "modules/ext/course.js", @@ -938,6 +947,33 @@ "builtin": false, "example": "factory-reset-old" }, + "feedback": { + "module": "module-debug-supervisor", + "file": "modules/core/module-debug-supervisor.js", + "tier": "core-bundle", + "category": "general", + "desc": "feedback <message> — Send feedback to the WebX11 maintenance agent", + "builtin": false, + "example": "feedback" + }, + "feedback-admin": { + "module": "module-debug-supervisor", + "file": "modules/core/module-debug-supervisor.js", + "tier": "core-bundle", + "category": "general", + "desc": "feedback-admin — command from module-debug-supervisor", + "builtin": false, + "example": "feedback-admin" + }, + "feedback-inbox": { + "module": "module-debug-supervisor", + "file": "modules/core/module-debug-supervisor.js", + "tier": "core-bundle", + "category": "general", + "desc": "feedback-inbox — Read replies from the maintenance agent (admin session)", + "builtin": false, + "example": "feedback-inbox" + }, "fetch": { "module": "module-fetcher", "file": "modules/core/module-fetcher.js", @@ -1118,6 +1154,15 @@ "builtin": false, "example": "gui-text" }, + "guides": { + "module": "module-fs", + "file": "modules/core/module-fs.js", + "tier": "core-bundle", + "category": "filesystem", + "desc": "Use trash for recursive dir removal too", + "builtin": false, + "example": "guides" + }, "gunzip": { "module": "cryptogzip", "file": "modules/ext/cryptogzip.js", @@ -1163,6 +1208,33 @@ "builtin": false, "example": "help" }, + "hermes": { + "module": "module-debug-supervisor", + "file": "modules/core/module-debug-supervisor.js", + "tier": "core-bundle", + "category": "general", + "desc": "hermes — command from module-debug-supervisor", + "builtin": false, + "example": "hermes" + }, + "hermes-poll": { + "module": "module-debug-supervisor", + "file": "modules/core/module-debug-supervisor.js", + "tier": "core-bundle", + "category": "general", + "desc": "hermes-poll — command from module-debug-supervisor", + "builtin": false, + "example": "hermes-poll" + }, + "hermes-reset": { + "module": "module-debug-supervisor", + "file": "modules/core/module-debug-supervisor.js", + "tier": "core-bundle", + "category": "general", + "desc": "hermes-reset — command from module-debug-supervisor", + "builtin": false, + "example": "hermes-reset" + }, "history": { "module": "terminal", "file": "modules/core/terminal.js", @@ -1942,7 +2014,7 @@ "file": "modules/core/module-predictor.js", "tier": "core-bundle", "category": "general", - "desc": "predictor-config <key> <value> — Tune: confidence-threshold, min-data-points", + "desc": "predictor-config <key> <value> — Tune: confidence-threshold, min-data-points, pair-ttl", "builtin": false, "example": "predictor-config" }, @@ -1951,7 +2023,7 @@ "file": "modules/core/module-predictor.js", "tier": "core-bundle", "category": "general", - "desc": "predictor-status — Model accuracy, data points, prediction count", + "desc": "predictor-status — Paired accuracy, pending predictions, data points", "builtin": false, "example": "predictor-status" }, @@ -2419,7 +2491,7 @@ "file": "modules/ext/skill-pack-core.js", "tier": "ext-auto", "category": "ai", - "desc": "skill-create <name> <desc> \"<steps>\" [tags] — Create a new skill", + "desc": "skill-create <name> <desc> <path-or-text> [--tags=a,b] [--category=x] — Create a new skill", "builtin": false, "example": "skill-create" }, @@ -2621,6 +2693,15 @@ "builtin": false, "example": "tar" }, + "test": { + "module": "shell", + "file": "modules/ext/shell.js", + "tier": "critical-ext-bundle", + "category": "shell", + "desc": "32 automated tests: filesystem, busybox, shell, cryptogzip, skills, security, modules. Load manually with load-local then test", + "builtin": false, + "example": "test" + }, "test-net": { "module": "module-servermon", "file": "modules/core/module-servermon.js", @@ -2748,12 +2829,12 @@ "example": "uptime" }, "usage": { - "module": "terminal", - "file": "modules/core/terminal.js", + "module": "module-billing", + "file": "modules/core/module-billing.js", "tier": "core-bundle", - "category": "system", - "desc": "usage — command from terminal", - "builtin": true, + "category": "general", + "desc": "usage — same as cost", + "builtin": false, "example": "usage" }, "vaultmind-connect": { @@ -3166,7 +3247,7 @@ "js-run", "node" ], - "lines": 96 + "lines": 139 }, "course": { "file": "modules/ext/course.js", @@ -3232,7 +3313,7 @@ "files", "preview" ], - "lines": 1151 + "lines": 1278 }, "finder": { "file": "modules/ext/finder.js", @@ -3296,7 +3377,7 @@ "contract-list", "contract-register" ], - "lines": 474 + "lines": 506 }, "module-aiasm": { "file": "modules/core/module-aiasm.js", @@ -3350,7 +3431,16 @@ "vaultmind-status", "wallet-lookup" ], - "lines": 4678 + "lines": 4934 + }, + "module-billing": { + "file": "modules/core/module-billing.js", + "tier": "core-bundle", + "commands": [ + "cost", + "usage" + ], + "lines": 185 }, "module-chatstudio": { "file": "modules/core/module-chatstudio.js", @@ -3384,9 +3474,15 @@ "debug-stats", "debug-sync", "debug-sync-url", - "debug-trends" + "debug-trends", + "feedback", + "feedback-admin", + "feedback-inbox", + "hermes", + "hermes-poll", + "hermes-reset" ], - "lines": 890 + "lines": 1228 }, "module-fetcher": { "file": "modules/core/module-fetcher.js", @@ -3410,6 +3506,7 @@ "empty-trash", "find", "grep", + "guides", "head", "ls", "ls-trash", @@ -3439,7 +3536,7 @@ "vfs-size", "wc" ], - "lines": 823 + "lines": 982 }, "module-leases": { "file": "modules/core/module-leases.js", @@ -3466,7 +3563,7 @@ "memgraph-stats", "memgraph-status" ], - "lines": 663 + "lines": 703 }, "module-modmgr": { "file": "modules/core/module-modmgr.js", @@ -3553,7 +3650,7 @@ "orchestrator-wake", "pipeline" ], - "lines": 1442 + "lines": 1485 }, "module-predictor": { "file": "modules/core/module-predictor.js", @@ -3563,7 +3660,7 @@ "predictor-config", "predictor-status" ], - "lines": 282 + "lines": 509 }, "module-sandbox": { "file": "modules/core/module-sandbox.js", @@ -3656,7 +3753,7 @@ "uptime", "version" ], - "lines": 815 + "lines": 834 }, "module-world-model": { "file": "modules/core/module-world-model.js", @@ -3668,7 +3765,7 @@ "world-model-refresh", "world-model-summary" ], - "lines": 600 + "lines": 647 }, "secret-vault": { "file": "modules/core/secret-vault.js", @@ -3693,9 +3790,10 @@ "script", "set", "sh", + "test", "unset" ], - "lines": 627 + "lines": 956 }, "skill-pack-core": { "file": "modules/ext/skill-pack-core.js", @@ -3716,7 +3814,7 @@ "skill-view", "skill-webdav-sync" ], - "lines": 1150 + "lines": 1334 }, "theme": { "file": "modules/ext/theme.js", @@ -3770,7 +3868,7 @@ "webdav-rm", "webdav-status" ], - "lines": 2368 + "lines": 2450 }, "wpm": { "file": "modules/ext/wpm.js", @@ -3782,12 +3880,12 @@ } }, "coverage": { - "commands": 346, - "described": 346, - "modules": 47, + "commands": 355, + "described": 355, + "modules": 48, "tiers": { - "core-bundle": 258, - "critical-ext-bundle": 28, + "core-bundle": 266, + "critical-ext-bundle": 29, "ext-auto": 60, "ext-manual": 0 } @@ -525,6 +525,7 @@ diff a.txt b.txt</pre> <tr><td><code>checkpoint-del</code></td><td>Delete a checkpoint</td><td><code>checkpoint-del</code></td></tr> <tr><td><code>smart-prompt</code></td><td>Toggle context-aware prompt</td><td><code>smart-prompt</code></td></tr> <tr><td><code>test</code></td><td>Run test suite</td><td><code>test auto</code></td></tr> + <tr><td><code>[ … ]</code> / <code>test …</code></td><td>POSIX file and string test for chains — silent, combines with <code>&&</code> / <code>||</code>. Operators: <code>-f</code> <code>-d</code> <code>-e</code> <code>-z</code> <code>-n</code> <code>!</code> <code>=</code> <code>!=</code>. The expression form of <code>test</code> is routed to this; <code>test</code> with no expression still runs the suite.</td><td><code>[ -f /notes.txt ] && echo yes</code></td></tr> </table></div> <h3>AI Assistant</h3> @@ -1349,13 +1349,13 @@ </script> <!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix --> -<script src="modules/core/core-bundle.js?v=v4.6.132"></script> +<script src="modules/core/core-bundle.js?v=v4.6.135"></script> <!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules --> -<script src="modules/core/module-loader.js?v=v4.6.132"></script> +<script src="modules/core/module-loader.js?v=v4.6.135"></script> <!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work --> -<script src="modules/ext/critical-ext-bundle.js?v=v4.6.132"></script> +<script src="modules/ext/critical-ext-bundle.js?v=v4.6.135"></script> <script> // ═══ ONE TRUE VERSION SOURCE ═══ diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index b070e65..07609e3 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -4553,6 +4553,9 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm } return; } catch (err) { + // v4.6.134: `silent` errors are control flow, not failures — `test`/`[` + // reject to tell `&&`/`||` to skip, and must print nothing (POSIX). + if (err && err.silent) return; this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); console.error(`[Terminal] Error in ${modName}.${cmd}:`, err); return; diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 0d39123..52cee36 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -4,7 +4,7 @@ // Core module bundle loaded synchronously in main.html. // This loader only handles version cache-busting, splash, terminal init, and optional ext modules. - const LATEST_VER = 'v4.6.132'; + const LATEST_VER = 'v4.6.135'; const CACHE = '?v=' + LATEST_VER; // Exposed so ext modules can register their own URL with the same cache-busting version // instead of an unversioned path the browser may already have cached (shell.js did). diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 4122b3c..ca6bb61 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -1961,6 +1961,9 @@ } return; } catch (err) { + // v4.6.134: `silent` errors are control flow, not failures — `test`/`[` + // reject to tell `&&`/`||` to skip, and must print nothing (POSIX). + if (err && err.silent) return; this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); console.error(`[Terminal] Error in ${modName}.${cmd}:`, err); return; diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js index 0f2e1b9..53f1205 100644 --- a/modules/ext/critical-ext-bundle.js +++ b/modules/ext/critical-ext-bundle.js @@ -170,6 +170,46 @@ class ShellModule { this.commands['script'] = (args) => this._cmdScript(args); this.commands['sh'] = (args) => this._cmdScript(args); this.commands['set'] = () => this._cmdSet(); + // v4.6.133: `test` / `[` were not registered, so `[ -f /x ] && echo yes` fell through to the + // AI fallback and asked a model whether a file exists. _evalTest() already implemented the + // operators (-f -d -e -z -n ! = !=) for the if/while parser; the command was simply missing. + // `test` already means "run the regression suite" in the on-demand test-suite module, so a + // non-expression argument list is delegated to whatever handler was registered before us. + const prevTest = this.commands['test']; + this.commands['test'] = (args) => this._cmdTest(args, 'test', prevTest); + this.commands['['] = (args) => this._cmdTest(args, '['); + } + + /** True when the argv looks like a POSIX test expression rather than a suite-runner subcommand. */ + _looksLikeTest(expr) { + if (!expr) return false; + if (/^(?:!(?:\s|$)|-[fderwxszn]\b)/.test(expr)) return true; // `-f x`, `! -d y` + if (/\s(?:=|==|!=)\s/.test(expr)) return true; // `a = b` + return false; + } + + /** POSIX-ish `test`/`[`: silent on success, throws a silent error on false so `&&`/`||` skip. */ + _cmdTest(args, name, fallback) { + let expr = (args || []).join(' ').trim(); + if (name === 'test' && !this._looksLikeTest(expr) && typeof fallback === 'function') { + return fallback(args); // `test`, `test auto`, `test list` -> the suite runner + } + if (name === '[') { + if (!expr.endsWith(']')) throw new Error("[ : missing ']'"); + expr = expr.slice(0, -1).trim(); + } + if (!expr) { // `test` with no expression: false, and no output + const e = new Error(''); e.silent = true; throw e; + } + if (!this._evalTest(expr)) { + const e = new Error(''); e.silent = true; throw e; // POSIX: exit 1 prints nothing + } + } + + /** Error printer shared by the shell paths: `silent` errors are control flow (a false test), not failures. */ + _printErr(term, e) { + if (!e || e.silent) return; + (term || this.terminal).print('Error: ' + (e.message || e), 'error'); } // ── Hook into terminal.executeCommand ── @@ -259,7 +299,7 @@ class ShellModule { await self.commands[cmd](args); lastOk = true; } catch(e) { - this.print('Error: ' + e.message, 'error'); + self._printErr(this, e); lastOk = false; } } else { @@ -267,7 +307,7 @@ class ShellModule { await this.security.checkAndRun(expandedCmd); lastOk = true; } catch(e) { - this.print('Error: ' + e.message, 'error'); + self._printErr(this, e); lastOk = false; } } diff --git a/modules/ext/shell.js b/modules/ext/shell.js index cb26484..ff07d92 100644 --- a/modules/ext/shell.js +++ b/modules/ext/shell.js @@ -34,6 +34,46 @@ class ShellModule { this.commands['script'] = (args) => this._cmdScript(args); this.commands['sh'] = (args) => this._cmdScript(args); this.commands['set'] = () => this._cmdSet(); + // v4.6.133: `test` / `[` were not registered, so `[ -f /x ] && echo yes` fell through to the + // AI fallback and asked a model whether a file exists. _evalTest() already implemented the + // operators (-f -d -e -z -n ! = !=) for the if/while parser; the command was simply missing. + // `test` already means "run the regression suite" in the on-demand test-suite module, so a + // non-expression argument list is delegated to whatever handler was registered before us. + const prevTest = this.commands['test']; + this.commands['test'] = (args) => this._cmdTest(args, 'test', prevTest); + this.commands['['] = (args) => this._cmdTest(args, '['); + } + + /** True when the argv looks like a POSIX test expression rather than a suite-runner subcommand. */ + _looksLikeTest(expr) { + if (!expr) return false; + if (/^(?:!(?:\s|$)|-[fderwxszn]\b)/.test(expr)) return true; // `-f x`, `! -d y` + if (/\s(?:=|==|!=)\s/.test(expr)) return true; // `a = b` + return false; + } + + /** POSIX-ish `test`/`[`: silent on success, throws a silent error on false so `&&`/`||` skip. */ + _cmdTest(args, name, fallback) { + let expr = (args || []).join(' ').trim(); + if (name === 'test' && !this._looksLikeTest(expr) && typeof fallback === 'function') { + return fallback(args); // `test`, `test auto`, `test list` -> the suite runner + } + if (name === '[') { + if (!expr.endsWith(']')) throw new Error("[ : missing ']'"); + expr = expr.slice(0, -1).trim(); + } + if (!expr) { // `test` with no expression: false, and no output + const e = new Error(''); e.silent = true; throw e; + } + if (!this._evalTest(expr)) { + const e = new Error(''); e.silent = true; throw e; // POSIX: exit 1 prints nothing + } + } + + /** Error printer shared by the shell paths: `silent` errors are control flow (a false test), not failures. */ + _printErr(term, e) { + if (!e || e.silent) return; + (term || this.terminal).print('Error: ' + (e.message || e), 'error'); } // ── Hook into terminal.executeCommand ── @@ -123,7 +163,7 @@ class ShellModule { await self.commands[cmd](args); lastOk = true; } catch(e) { - this.print('Error: ' + e.message, 'error'); + self._printErr(this, e); lastOk = false; } } else { @@ -131,7 +171,7 @@ class ShellModule { await this.security.checkAndRun(expandedCmd); lastOk = true; } catch(e) { - this.print('Error: ' + e.message, 'error'); + self._printErr(this, e); lastOk = false; } } diff --git a/modules/test/test-shell-test-cmd.js b/modules/test/test-shell-test-cmd.js new file mode 100644 index 0000000..6abdd11 --- /dev/null +++ b/modules/test/test-shell-test-cmd.js @@ -0,0 +1,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); +})(); |