aboutsummaryrefslogtreecommitdiff
path: root/modules/test
diff options
context:
space:
mode:
Diffstat (limited to 'modules/test')
-rw-r--r--modules/test/test-suite.js419
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';
+ }
+}