class TestSuiteModule { constructor() { this.commands = {}; this.terminal = null; this._results = []; this._startTime = 0; } requirements() { return { met: true, missing: [] }; } getHelpInfo() { return { 'test': 'test [name|auto|list] — Run test suite (no args = all, auto = non-destructive only)', }; } registerCommands() { this.commands['test'] = (args) => this._run(args); } // ── Test definitions ── _tests() { return { // ── Filesystem ── 'ls-root': { desc: 'ls lists files in root', cmd: 'ls', expect: (out) => out.split('\n').length >= 1, destructive: false }, 'cd-pwd': { desc: 'cd and pwd work together', cmd_parts: [ { cmd: 'cd /' }, { cmd: 'pwd', expect: (out) => out.includes('/') }, ], destructive: false }, 'cat-put': { desc: 'put creates and cat reads a file', cmd_parts: [ { cmd: 'put /tmp/__test_hello.txt "Hello Test Suite"' }, { cmd: 'cat /tmp/__test_hello.txt', expect: (out) => out.includes('Hello Test Suite') }, { cmd: 'rm /tmp/__test_hello.txt' }, ], destructive: true }, 'grep-file': { desc: 'grep finds content in a file', cmd_parts: [ { cmd: 'put /tmp/__test_grep_data.txt "apple\\nbanana\\ncherry\\napple"' }, { cmd: 'grep apple /tmp/__test_grep_data.txt', expect: (out) => out.includes('apple') && out.split('\n').filter(l => l).length >= 2 }, { cmd: 'rm /tmp/__test_grep_data.txt' }, ], destructive: true }, // ── BusyBox ── 'which-ls': { desc: 'which finds ls command', cmd: 'which ls', expect: (out) => out.includes('ls'), destructive: false }, 'env-vars': { desc: 'env shows environment variables', cmd: 'env', expect: (out) => out.includes('SHELL=') && out.includes('USER=') && out.includes('PWD='), destructive: false }, 'date-formats': { desc: 'date shows multiple time formats', cmd: 'date', expect: (out) => out.includes('ISO:') && out.includes('UTC:') && out.includes('Unix:'), destructive: false }, 'cal-output': { desc: 'cal generates ASCII calendar', cmd: 'cal 5 2026', expect: (out) => out.includes('May') && out.includes('2026') && out.includes('Su'), destructive: false }, 'seq-range': { desc: 'seq generates number sequence', cmd: 'seq 3 7', expect: (out) => out.includes('3') && out.includes('5') && out.includes('7'), destructive: false }, 'base64-encode': { desc: 'base64 encodes and decodes', cmd_parts: [ { cmd: 'base64 hello', expect: (out) => out.trim() === 'aGVsbG8=' }, { cmd: 'base64 -d aGVsbG8=', expect: (out) => out.trim() === 'hello' }, ], destructive: false }, 'printf-format': { desc: 'printf formats strings', cmd: 'printf "Hello %s, you are %d" World 42', expect: (out) => out.includes('Hello World, you are 42'), destructive: false }, 'whoami-user': { desc: 'whoami returns a username', cmd: 'whoami', expect: (out) => out.trim().length > 0, destructive: false }, 'basename-dirname': { desc: 'basename and dirname parse paths', cmd_parts: [ { cmd: 'basename /a/b/c.txt', expect: (out) => out.trim() === 'c.txt' }, { cmd: 'dirname /a/b/c.txt', expect: (out) => out.trim() === '/a/b' }, ], destructive: false }, // ── Shell ── 'pipe-ls-grep': { desc: 'ls | grep filters terminal output', cmd: 'ls | grep skills', expect: (out) => out.includes('skills'), destructive: false }, 'redirect-write': { desc: '> redirect writes output to VFS file', cmd_parts: [ { cmd: 'echo "pipe test line 1" > /tmp/__test_redirect.txt' }, { cmd: 'cat /tmp/__test_redirect.txt', expect: (out) => out.includes('pipe test line 1') }, { cmd: 'rm /tmp/__test_redirect.txt' }, ], destructive: true }, 'redirect-append': { desc: '>> append adds to existing file', cmd_parts: [ { cmd: 'echo "line A" > /tmp/__test_append.txt' }, { cmd: 'echo "line B" >> /tmp/__test_append.txt' }, { cmd: 'cat /tmp/__test_append.txt', expect: (out) => out.includes('line A') && out.includes('line B') }, { cmd: 'rm /tmp/__test_append.txt' }, ], destructive: true }, 'chain-and': { desc: '&& runs second command on success', cmd: 'touch /tmp/__test_chain && rm /tmp/__test_chain && echo "chain ok"', expect: (out) => out.includes('chain ok'), destructive: false }, 'chain-or': { desc: '|| runs second command on failure', cmd: 'rm /tmp/__test_nonexistent_xyz || echo "or fallback"', expect: (out) => out.includes('or fallback'), destructive: false }, 'sequence-semi': { desc: '; runs commands in sequence', cmd: 'echo "first" ; echo "second"', expect: (out) => out.includes('first') && out.includes('second'), destructive: false }, 'export-var': { desc: 'export and $VAR substitution works', cmd_parts: [ { cmd: 'export TESTVAR=hello42' }, { cmd: 'echo $TESTVAR', expect: (out) => out.includes('hello42') }, { cmd: 'unset TESTVAR' }, ], destructive: false }, 'redirect-input': { desc: '< reads file as stdin', cmd_parts: [ { cmd: 'echo "hello from stdin" > /tmp/__test_stdin.txt' }, { cmd: 'cat /tmp/__test_stdin.txt', expect: (out) => out.includes('hello from stdin') }, { cmd: 'rm /tmp/__test_stdin.txt' }, ], destructive: true }, // ── CryptoGzip ── 'gzip-file': { desc: 'gzip compresses a VFS file', cmd_parts: [ { cmd: 'echo "repetitive data aaaaa bbbbb ccccc" > /tmp/__test_gzip_in.txt' }, { cmd: 'gzip /tmp/__test_gzip_in.txt' }, { cmd: 'cat /tmp/__test_gzip_in.txt.gz', expect: (out) => out.length > 0 }, { cmd: 'rm /tmp/__test_gzip_in.txt.gz' }, ], destructive: true }, 'gunzip-file': { desc: 'gunzip decompresses', cmd_parts: [ { cmd: 'echo "gunzip me please" > /tmp/__test_gunzip.txt' }, { cmd: 'gzip /tmp/__test_gunzip.txt' }, { cmd: 'gunzip /tmp/__test_gunzip.txt.gz' }, { cmd: 'cat /tmp/__test_gunzip.txt', expect: (out) => out.includes('gunzip me please') }, { cmd: 'rm /tmp/__test_gunzip.txt' }, ], destructive: true }, 'encrypt-decrypt': { desc: 'encrypt and decrypt roundtrip', cmd_parts: [ { cmd: 'echo "secret data xyz" > /tmp/__test_crypt.txt' }, { cmd: 'encrypt /tmp/__test_crypt.txt testkey123' }, { cmd: 'rm /tmp/__test_crypt.txt' }, { cmd: 'decrypt /tmp/__test_crypt.txt.enc testkey123' }, { cmd: 'cat /tmp/__test_crypt.txt', expect: (out) => out.includes('secret data xyz') }, { cmd: 'rm /tmp/__test_crypt.txt' }, { cmd: 'rm /tmp/__test_crypt.txt.enc' }, ], destructive: true }, // ── Skills ── 'skill-list': { desc: 'skill-list returns skills', cmd: 'skill-list', expect: (out) => out.includes('Skills') || out.includes('No skills'), destructive: false }, 'skill-create-verify': { desc: 'skill create and view roundtrip', cmd_parts: [ { cmd: 'skill-create __test_dummy "Dummy test skill" testing test' }, { cmd: 'skill-view __test_dummy', expect: (out) => out.includes('__test_dummy') }, { cmd: 'skill-delete __test_dummy' }, ], destructive: true }, // ── Security ── 'security-level-get': { desc: 'security-level returns current level', cmd: 'security-level', expect: (out) => out.length > 0, destructive: false }, // ── Aliases ── 'alias-list': { desc: 'alias shows built-in defaults', cmd: 'alias', expect: (out) => out.includes('ll=ls') && out.includes('h=help') && out.includes('cls=clear') && out.includes('sh=script'), destructive: false }, 'alias-show-specific': { desc: 'alias shows a specific alias value', cmd: 'alias ll', expect: (out) => out.includes('ll=ls') && out.includes('built-in'), destructive: false }, 'alias-missing-unknown': { desc: 'alias shows not found', cmd: 'alias non_existent_alias_xyz', expect: (out) => out.includes('not found'), destructive: false }, 'unalias-builtin-refused': { desc: 'unalias refuses to remove built-in alias', cmd: 'unalias ll', expect: (out) => out.includes('Cannot unalias built-in'), destructive: false }, 'alias-create-and-run': { desc: 'create custom alias and execute it', cmd_parts: [ { cmd: 'alias greet=echo hello from alias' }, { cmd: 'greet', expect: (out) => out.includes('hello from alias') }, { cmd: 'unalias greet' }, ], destructive: true }, 'alias-overwrite': { desc: 'create, then overwrite, execute updated alias', cmd_parts: [ { cmd: 'alias greet2=echo first' }, { cmd: 'alias greet2=echo second' }, { cmd: 'greet2', expect: (out) => out.includes('second') }, { cmd: 'unalias greet2' }, ], destructive: true }, 'unalias-custom': { desc: 'unalias removes a custom alias', cmd_parts: [ { cmd: 'alias tempcmd=echo temp' }, { cmd: 'unalias tempcmd' }, { cmd: 'alias tempcmd', expect: (out) => out.includes('not found') }, ], destructive: true }, // ── Modules ── 'commands-help': { desc: 'help lists multiple command categories', cmd: 'help', expect: (out) => out.includes('commands') || out.includes('System') || out.includes('Filesystem'), destructive: false }, 'listmods': { desc: 'listmods shows loaded modules', cmd: 'listmods', expect: (out) => out.includes('Module') || out.includes('modules') || out.includes('loaded'), destructive: false }, }; } // ── Runner ── async _run(args) { const tests = this._tests(); const names = Object.keys(tests); if (args.length > 0 && args[0] === 'list') { this.terminal.print('=== Available Tests ===', 'info'); for (const name of names) { const t = tests[name]; const tag = t.destructive ? '⚠️ destructive' : '✅ safe'; this.terminal.print(` ${name.padEnd(24)} ${t.desc} [${tag}]`); } this.terminal.print(`\n${names.length} tests total`, 'info'); return; } const filter = args.length > 0 ? args[0].toLowerCase() : null; const autoOnly = filter === 'auto'; let selected = names; if (filter && filter !== 'auto' && filter !== 'all') { selected = names.filter(n => n.includes(filter)); if (selected.length === 0) { this.terminal.print(`No tests matching "${filter}"`, 'error'); return; } } const toRun = autoOnly ? selected.filter(n => !tests[n].destructive) : selected; const destructiveCount = toRun.filter(n => tests[n].destructive).length; // Save original security level const origLevel = localStorage.getItem('webx11_security_level') || '1'; // Set to permissive for destructive tests this.terminal.security.setLevel(0); // Print header const divider = '─'.repeat(50); this.terminal.print(divider, 'info'); this.terminal.print(' Test Suite — newOS4 ' + (window.NEWOS4_VERSION || 'v4.0'), 'info'); this.terminal.print(divider, 'info'); this.terminal.print(` Running ${toRun.length} tests (${destructiveCount} destructive, ${toRun.length - destructiveCount} safe)`, 'info'); this.terminal.print('', 'info'); this._results = []; this._startTime = Date.now(); let passed = 0, failed = 0, skipped = 0; for (const name of toRun) { const t = tests[name]; const result = await this._runSingle(name, t); this._results.push(result); if (result.passed) { passed++; this.terminal.print(` ✅ ${name} (${result.duration}s) — ${t.desc}`, 'success'); } else if (result.skipped) { skipped++; this.terminal.print(` ⏭ ${name} — ${result.reason}`, 'info'); } else { failed++; this.terminal.print(` ❌ ${name} (${result.duration}s) — ${t.desc}`, 'error'); this.terminal.print(` Expected: ${result.expected}`, 'warning'); this.terminal.print(` Got: ${result.got}`, 'error'); } } // Restore security level this.terminal.security.setLevel(parseInt(origLevel)); // Summary const elapsed = ((Date.now() - this._startTime) / 1000).toFixed(1); this.terminal.print('', 'info'); this.terminal.print(divider, 'info'); const status = failed === 0 ? 'ALL PASSED ✅' : `${failed} FAILED ❌`; this.terminal.print(` Result: ${passed}/${toRun.length} passed ${failed} failed ${skipped} skipped (${elapsed}s)`, failed === 0 ? 'success' : 'error'); if (failed > 0) { this.terminal.print(' Failures:', 'warning'); for (const r of this._results) { if (!r.passed && !r.skipped) { this.terminal.print(` ${r.name} — ${r.desc}`, 'error'); } } } } async _runSingle(name, t) { const start = Date.now(); try { if (t.skip) { return { name, passed: false, skipped: true, reason: t.skip, duration: 0 }; } // Multi-step test if (t.cmd_parts) { for (const part of t.cmd_parts) { const captured = await this._capture(part.cmd); if (part.expect) { if (!part.expect(captured)) { return { name, passed: false, desc: t.desc, expected: this._formatExpect(t), got: captured.substring(0, 200), duration: ((Date.now() - start) / 1000).toFixed(1) }; } } } return { name, passed: true, desc: t.desc, duration: ((Date.now() - start) / 1000).toFixed(1) }; } // Single command test const output = await this._capture(t.cmd); if (t.expect && !t.expect(output)) { return { name, passed: false, desc: t.desc, expected: this._formatExpect(t), got: output.substring(0, 200), duration: ((Date.now() - start) / 1000).toFixed(1) }; } return { name, passed: true, desc: t.desc, duration: ((Date.now() - start) / 1000).toFixed(1) }; } catch (e) { return { name, passed: false, desc: t.desc, expected: this._formatExpect(t), got: `EXCEPTION: ${e.message}`, duration: ((Date.now() - start) / 1000).toFixed(1) }; } } async _capture(cmdText) { const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; this.terminal.print = (text, type) => { output += text + '\n'; }; await this.terminal.executeCommand(cmdText); this.terminal.print = originalPrint; return output; } _formatExpect(t) { if (t.expect && t.expect.toString) { const s = t.expect.toString(); // Try to extract meaningful description from the function const match = s.match(/out\.includes\(['"]([^'"]+)['"]\)/); if (match) return `output includes "${match[1]}"`; if (s.includes('&&')) return 'all conditions met'; if (s.includes('.length')) return 'non-empty output'; return t.desc; } return 'no expectation'; } }