diff options
| author | Agando <roduccomm@gmail.com> | 2026-05-27 06:00:43 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-05-27 06:00:43 +0200 |
| commit | 212708b464c2bdbdd9670c00804623dca0729d2c (patch) | |
| tree | ee190cbb0d1524350baea641df65aa19817744b5 /modules/test | |
| parent | f4d934d341056142da1212c845eeeb170fd452c2 (diff) | |
auto: 2026-05-27_06:00 changes
Diffstat (limited to 'modules/test')
| -rw-r--r-- | modules/test/test-suite.js | 419 |
1 files changed, 419 insertions, 0 deletions
diff --git a/modules/test/test-suite.js b/modules/test/test-suite.js new file mode 100644 index 0000000..ddd0dbd --- /dev/null +++ b/modules/test/test-suite.js @@ -0,0 +1,419 @@ +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 + }, + + // ── 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 v4.2', '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'; + } +} |