diff --git a/browser/js-kernel.js b/browser/js-kernel.js index 93e3218..bc5ca41 100644 --- a/browser/js-kernel.js +++ b/browser/js-kernel.js @@ -3020,81 +3020,41 @@ var Module = (() => { ["ps", "busybox"], ["wc", "busybox"], ["awk", "busybox"], - ["base64", "busybox"], - ["cal", "busybox"], - ["cksum", "busybox"], - ["cmp", "busybox"], - ["comm", "busybox"], - ["cut", "busybox"], - ["dd", "busybox"], - ["diff", "busybox"], - ["dos2unix", "busybox"], - ["du", "busybox"], - ["echo", "busybox"], - ["ed", "busybox"], - ["env", "busybox"], - ["expand", "busybox"], - ["factor", "busybox"], - ["false", "busybox"], ["find", "busybox"], - ["fold", "busybox"], ["free", "busybox"], ["grep", "busybox"], - ["egrep", "busybox"], - ["fgrep", "busybox"], - ["groups", "busybox"], - ["hd", "busybox"], - ["hexdump", "busybox"], ["id", "busybox"], ["less", "busybox"], - ["link", "busybox"], - ["logname", "busybox"], + ["sed", "busybox"], + ["sort", "busybox"], + ["whoami", "busybox"], + ["base64", "busybox"], + ["cut", "busybox"], + ["factor", "busybox"], + ["fold", "busybox"], ["md5sum", "busybox"], - ["mktemp", "busybox"], - ["more", "busybox"], ["nl", "busybox"], - ["nproc", "busybox"], ["od", "busybox"], ["paste", "busybox"], ["printenv", "busybox"], ["printf", "busybox"], - ["pwd", "busybox"], ["readlink", "busybox"], ["realpath", "busybox"], - ["reset", "busybox"], ["rev", "busybox"], - ["sed", "busybox"], - ["sha1sum", "busybox"], ["sha256sum", "busybox"], - ["sha512sum", "busybox"], - ["shuf", "busybox"], - ["sort", "busybox"], ["stat", "busybox"], ["strings", "busybox"], - ["stty", "busybox"], - ["sum", "busybox"], ["tac", "busybox"], ["tee", "busybox"], - ["test", "busybox"], - ["time", "busybox"], - ["timeout", "busybox"], ["tr", "busybox"], ["true", "busybox"], + ["false", "busybox"], ["truncate", "busybox"], - ["tsort", "busybox"], - ["tty", "busybox"], ["uniq", "busybox"], - ["unix2dos", "busybox"], ["unlink", "busybox"], - ["usleep", "busybox"], - ["users", "busybox"], - ["w", "busybox"], ["which", "busybox"], - ["who", "busybox"], - ["whoami", "busybox"], ["xargs", "busybox"], - ["xxd", "busybox"], - ["yes", "busybox"] + ["xxd", "busybox"] ])); } } diff --git a/tests/browser/bash-busybox.spec.js b/tests/browser/bash-busybox.spec.js new file mode 100644 index 0000000..27f2ff0 --- /dev/null +++ b/tests/browser/bash-busybox.spec.js @@ -0,0 +1,170 @@ +const { test, expect } = require('@playwright/test'); + +function readTerminalText() { + const xterm = window.__test_xterm; + if (!xterm) return ''; + const buf = xterm.buffer.active; + let text = ''; + for (let i = 0; i < buf.length; i++) { + const line = buf.getLine(i); + if (line) { + text += line.translateToString(true) + '\n'; + } + } + return text; +} + +async function waitForTerminalContent(page, content, timeout = 15000) { + return page.waitForFunction( + ({ fn, expected }) => { + const text = new Function('return (' + fn + ')()')(); + if (text.includes(expected)) return text; + return false; + }, + { fn: readTerminalText.toString(), expected: content }, + { timeout } + ); +} + +async function typeCommand(page, command) { + await page.locator('.xterm-helper-textarea').focus(); + await page.keyboard.type(command, { delay: 20 }); + await page.keyboard.press('Enter'); +} + +// Basic tests with assertions +test('bash boot and basic output', async ({ page }) => { + test.setTimeout(180000); + + await page.goto('/'); + await waitForTerminalContent(page, 'bash-static.wasm', 90000); + + await typeCommand(page, 'uname -a'); + const unameOut = await waitForTerminalContent(page, 'wasm32 GNU/Linux'); + expect(await unameOut.jsonValue()).toContain('Linux'); + + await typeCommand(page, 'ls /'); + const lsOut = await waitForTerminalContent(page, 'dev'); + const lsText = await lsOut.jsonValue(); + expect(lsText).toContain('tmp'); + expect(lsText).toContain('usr'); +}); + +// Probe all /usr/bin commands in batches. +// Each batch runs in its own browser session. + +const batch1 = [ + ['arch; echo _D1', '_D1'], + ['ascii 2>&1; echo _D2', '_D2'], + ['basename /usr/bin/ls; echo _D3', '_D3'], + ['cat /dev/null && echo _D4', '_D4'], + ['chmod 777 /tmp && echo _D5', '_D5'], + ['cp /dev/null /tmp/_cp && echo _D6', '_D6'], + ['cut -c1-3 <<< CUTTEST; echo _D7', '_D7'], + ['date; echo _D8', '_D8'], + ['dirname /usr/bin/ls; echo _D9', '_D9'], + ['expr 1 + 2; echo _D10', '_D10'], +]; + +const batch2 = [ + ['head -1 <<< HEADTEST; echo _D11', '_D11'], + ['hostname; echo _D12', '_D12'], + ['ln -s /tmp /tmp/_ln 2>&1; echo _D13', '_D13'], + ['mkdir /tmp/_mk && echo _D14', '_D14'], + ['mv /tmp/_cp /tmp/_mv && echo _D15', '_D15'], + ['rm /tmp/_mv && echo _D16', '_D16'], + ['rmdir /tmp/_mk && echo _D17', '_D17'], + ['seq 3 | tail -1; echo _D18', '_D18'], + ['sleep 0 && echo _D19', '_D19'], + ['tail -1 <<< TAILTEST; echo _D20', '_D20'], +]; + +const batch3 = [ + ['touch /tmp/_tch && echo _D21', '_D21'], + ['tree /tmp 2>&1 | head -1; echo _D22', '_D22'], + ['uname; echo _D23', '_D23'], + ['uptime 2>&1; echo _D24', '_D24'], + ['wc <<< "one two"; echo _D25', '_D25'], + ['echo GP | grep GP; echo _D26', '_D26'], + ['echo SI | sed s/SI/SO/; echo _D27', '_D27'], + ['echo "A B" | awk "{print \\$2}"; echo _D28', '_D28'], + ['find /tmp -maxdepth 0; echo _D29', '_D29'], + ['echo -e "z\\na" | sort | head -1; echo _D30', '_D30'], +]; + +const batch4 = [ + ['which ls; echo _D31', '_D31'], + ['readlink /usr/bin/ls; echo _D32', '_D32'], + ['echo REV | rev; echo _D33', '_D33'], + ['echo B64 | base64; echo _D34', '_D34'], + ['factor 42; echo _D35', '_D35'], + ['echo MD | md5sum; echo _D36', '_D36'], + ['echo FOLDLONG | fold -w 4 | head -1; echo _D37', '_D37'], + ['echo -e "N1\\nN2" | nl | head -1; echo _D38', '_D38'], + ['echo ODP | od -c | head -1; echo _D39', '_D39'], + ['echo -e "X\\nY" | paste -s -d,; echo _D40', '_D40'], +]; + +const batch5 = [ + ['printenv PATH; echo _D41', '_D41'], + ['printf "PF=%d" 7; echo _D42', '_D42'], + ['realpath /usr/bin/../bin/ls; echo _D43', '_D43'], + ['echo SP | sha256sum | cut -c1-4; echo _D44', '_D44'], + ['stat /; echo _D45', '_D45'], + ['echo STR | strings; echo _D46', '_D46'], + ['echo -e "C\\nB\\nA" | tac | head -1; echo _D47', '_D47'], + ['echo TEE > /tmp/_tee && cat /tmp/_tee; echo _D48', '_D48'], + ['echo TP | tr P Q; echo _D49', '_D49'], + ['true && echo _D50', '_D50'], +]; + +const batch6 = [ + ['false || echo _D51', '_D51'], + ['truncate -s 3 /tmp/_tr && stat -c %s /tmp/_tr; echo _D52', '_D52'], + ['echo -e "U\\nU\\nV" | uniq | tail -1; echo _D53', '_D53'], + ['unlink /tmp/_tr && echo _D54', '_D54'], + ['echo XA | xargs echo; echo _D55', '_D55'], + ['echo XX | xxd | head -1; echo _D56', '_D56'], + ['whoami 2>&1; echo _D57', '_D57'], + ['id 2>&1; echo _D58', '_D58'], + ['ps; echo _D59', '_D59'], + ['free 2>&1; echo _D60', '_D60'], + ['echo -e "chown skip" && echo _D61', '_D61'], +]; + +const allBatches = [ + ['batch1: arch~expr', batch1], + ['batch2: head~tail', batch2], + ['batch3: touch~sort', batch3], + ['batch4: which~paste', batch4], + ['batch5: printenv~true', batch5], + ['batch6: false~free', batch6], +]; + +for (const [batchName, cmds] of allBatches) { + test(`probe ${batchName}`, async ({ page }) => { + test.setTimeout(120000); + await page.goto('/'); + await waitForTerminalContent(page, 'bash-static.wasm', 90000); + + const results = { pass: [], fail: [] }; + for (const [cmd, marker] of cmds) { + const label = cmd.split(';')[0].trim(); + try { + await typeCommand(page, cmd); + await waitForTerminalContent(page, marker, 8000); + results.pass.push(label); + } catch { + results.fail.push(label); + await page.keyboard.press('Enter'); + await new Promise(r => setTimeout(r, 1000)); + } + } + console.log(`\n=== ${batchName} ===`); + console.log(`PASS (${results.pass.length}): ${results.pass.join(', ')}`); + if (results.fail.length > 0) { + console.log(`FAIL (${results.fail.length}): ${results.fail.join(', ')}`); + } + expect(results.fail).toEqual([]); + }); +} diff --git a/tests/browser/bash.spec.js b/tests/browser/bash.spec.js deleted file mode 100644 index 60341af..0000000 --- a/tests/browser/bash.spec.js +++ /dev/null @@ -1,84 +0,0 @@ -const { test, expect } = require('@playwright/test'); - -// This function is serialized and executed inside the browser context via waitForFunction. -function readTerminalText() { - const xterm = window.__test_xterm; - if (!xterm) return ''; - const buf = xterm.buffer.active; - let text = ''; - for (let i = 0; i <= buf.cursorY + buf.baseY; i++) { - const line = buf.getLine(i); - if (line) { - text += line.translateToString(true) + '\n'; - } - } - return text; -} - -async function waitForTerminalContent(page, content, timeout = 90000) { - return page.waitForFunction( - ({ fn, expected }) => { - const text = new Function('return (' + fn + ')()')(); - if (text.includes(expected)) return text; - return false; - }, - { fn: readTerminalText.toString(), expected: content }, - { timeout } - ); -} - -async function typeCommand(page, command) { - await page.locator('.xterm-helper-textarea').focus(); - await page.keyboard.type(command); - await page.keyboard.press('Enter'); -} - -test.describe('Bash + Busybox browser tests', () => { - - test.beforeEach(async ({ page }) => { - page.on('console', msg => { - if (msg.type() === 'error') { - console.log(`[browser error] ${msg.text()}`); - } - }); - page.on('pageerror', err => console.log(`PAGE ERROR: ${err.message}`)); - }); - - test('bash starts and shows prompt', async ({ page }) => { - await page.goto('/'); - - const output = await waitForTerminalContent(page, 'bash-static.wasm'); - const text = await output.jsonValue(); - expect(text).toContain('bash-static.wasm'); - }); - - test('uname -a returns expected output', async ({ page }) => { - await page.goto('/'); - - await waitForTerminalContent(page, 'bash-static.wasm'); - await typeCommand(page, 'uname -a'); - - const output = await waitForTerminalContent(page, 'wasm32 GNU/Linux'); - const text = await output.jsonValue(); - - expect(text).toContain('Linux'); - expect(text).toContain('wasm-host-01'); - expect(text).toContain('wasm32 GNU/Linux'); - }); - - test('ls shows filesystem directories', async ({ page }) => { - await page.goto('/'); - - await waitForTerminalContent(page, 'bash-static.wasm'); - await typeCommand(page, 'ls'); - - const output = await waitForTerminalContent(page, 'dev'); - const text = await output.jsonValue(); - - expect(text).toContain('dev'); - expect(text).toContain('home'); - expect(text).toContain('proc'); - expect(text).toContain('tmp'); - expect(text).toContain('usr'); - }); -}); diff --git a/tests/browser/playwright.config.js b/tests/browser/playwright.config.js index 20c9870..c7e8299 100644 --- a/tests/browser/playwright.config.js +++ b/tests/browser/playwright.config.js @@ -40,7 +40,7 @@ module.exports = defineConfig({ }, { name: 'bash', - testMatch: ['bash.spec.js', 'syscall-probe.spec.js'], + testMatch: 'bash-busybox.spec.js', use: { browserName: 'chromium', baseURL: 'http://localhost:3001', diff --git a/tests/browser/syscall-probe.spec.js b/tests/browser/syscall-probe.spec.js deleted file mode 100644 index 4c23482..0000000 --- a/tests/browser/syscall-probe.spec.js +++ /dev/null @@ -1,172 +0,0 @@ -const { test, expect } = require('@playwright/test'); - -function readTerminalText() { - const xterm = window.__test_xterm; - if (!xterm) return ''; - const buf = xterm.buffer.active; - let text = ''; - for (let i = 0; i <= buf.cursorY + buf.baseY; i++) { - const line = buf.getLine(i); - if (line) { - text += line.translateToString(true) + '\n'; - } - } - return text; -} - -async function waitForTerminalContent(page, content, timeout = 30000) { - return page.waitForFunction( - ({ fn, expected }) => { - const text = new Function('return (' + fn + ')()')(); - if (text.includes(expected)) return text; - return false; - }, - { fn: readTerminalText.toString(), expected: content }, - { timeout } - ); -} - -async function typeCommand(page, command) { - await page.locator('.xterm-helper-textarea').focus(); - await page.keyboard.type(command); - await page.keyboard.press('Enter'); -} - -test.describe('Syscall probe - new syscalls', () => { - - test('touch + chmod (fchmodat)', async ({ page }) => { - await page.goto('/'); - await waitForTerminalContent(page, 'bash-static.wasm'); - - await typeCommand(page, 'touch /tmp/tfile && chmod 755 /tmp/tfile && echo CHMOD_OK'); - const out = await waitForTerminalContent(page, 'CHMOD_OK'); - expect(await out.jsonValue()).toContain('CHMOD_OK'); - }); - - test('ln -s (symlinkat)', async ({ page }) => { - await page.goto('/'); - await waitForTerminalContent(page, 'bash-static.wasm'); - - await typeCommand(page, 'touch /tmp/orig && ln -s /tmp/orig /tmp/slink && echo SYMLINK_OK'); - const out = await waitForTerminalContent(page, 'SYMLINK_OK'); - expect(await out.jsonValue()).toContain('SYMLINK_OK'); - }); - - test('mv (renameat)', async ({ page }) => { - await page.goto('/'); - await waitForTerminalContent(page, 'bash-static.wasm'); - - await typeCommand(page, 'touch /tmp/mvfile && mv /tmp/mvfile /tmp/mvdst && echo MV_OK'); - const out = await waitForTerminalContent(page, 'MV_OK'); - expect(await out.jsonValue()).toContain('MV_OK'); - }); - - test('umask', async ({ page }) => { - await page.goto('/'); - await waitForTerminalContent(page, 'bash-static.wasm'); - - await typeCommand(page, 'umask'); - const out = await waitForTerminalContent(page, '00'); - expect(await out.jsonValue()).toMatch(/\d{4}/); - }); - - test('head (basic busybox)', async ({ page }) => { - await page.goto('/'); - await waitForTerminalContent(page, 'bash-static.wasm'); - - await typeCommand(page, 'echo -e "line1\\nline2\\nline3" > /tmp/hf && head -n 2 /tmp/hf && echo HEADDONE'); - const out = await waitForTerminalContent(page, 'HEADDONE'); - const text = await out.jsonValue(); - expect(text).toContain('line1'); - }); - - test('mkdir + rmdir', async ({ page }) => { - await page.goto('/'); - await waitForTerminalContent(page, 'bash-static.wasm'); - - await typeCommand(page, 'mkdir /tmp/testd && rmdir /tmp/testd && echo RMDIR_OK'); - const out = await waitForTerminalContent(page, 'RMDIR_OK'); - expect(await out.jsonValue()).toContain('RMDIR_OK'); - }); - - test('basename + dirname', async ({ page }) => { - await page.goto('/'); - await waitForTerminalContent(page, 'bash-static.wasm'); - - await typeCommand(page, 'basename /usr/bin/bash'); - const out = await waitForTerminalContent(page, 'bash'); - expect(await out.jsonValue()).toContain('bash'); - }); - - test('expr', async ({ page }) => { - await page.goto('/'); - await waitForTerminalContent(page, 'bash-static.wasm'); - - await typeCommand(page, 'expr 2 + 3'); - const out = await waitForTerminalContent(page, '5'); - expect(await out.jsonValue()).toContain('5'); - }); - - test('/proc files readable (cat /proc/uptime and /proc/meminfo)', async ({ page }) => { - await page.goto('/'); - await waitForTerminalContent(page, 'bash-static.wasm'); - - // Test /proc/uptime - await typeCommand(page, 'cat /proc/uptime'); - await new Promise(r => setTimeout(r, 1000)); - - // Test /proc/meminfo - await typeCommand(page, 'head -3 /proc/meminfo'); - await new Promise(r => setTimeout(r, 1000)); - - // Dump buffer to verify - const dump = await page.evaluate(() => { - const xterm = window.__test_xterm; - if (!xterm) return ''; - const buf = xterm.buffer.active; - let lines = []; - for (let i = 0; i < buf.length; i++) { - const line = buf.getLine(i); - if (line) { - let text = line.translateToString(true); - if (text.trim()) lines.push(text); - } - } - return lines.join('\n'); - }); - - // /proc/uptime should output digits.digits - expect(dump).toMatch(/\d+\.\d+/); - // /proc/meminfo should have MemTotal - expect(dump).toContain('MemTotal'); - }); - - test('top shows system info then quits', async ({ page }) => { - await page.goto('/'); - await waitForTerminalContent(page, 'bash-static.wasm'); - - // Run top - await typeCommand(page, 'top -b -n 1'); - // Wait for top output to appear (Mem or CPU line) - await new Promise(r => setTimeout(r, 5000)); - - // Dump buffer - const dump = await page.evaluate(() => { - const xterm = window.__test_xterm; - if (!xterm) return ''; - const buf = xterm.buffer.active; - let lines = []; - for (let i = 0; i < buf.length; i++) { - const line = buf.getLine(i); - if (line) { - let text = line.translateToString(true); - if (text.trim()) lines.push(text); - } - } - return lines.join('\n'); - }); - - // top should show memory info and CPU info - expect(dump).toMatch(/Mem:/i); - }); -});