#!/usr/bin/env node // Test harness for newOS4 WebX11SecurityManager (checkAndRun, suggest, levels, AI loop guard). // Loads the security + terminal class source in a vm sandbox with a stub terminal. // Run: node scripts/test-security.mjs (exit 1 on failure) import { readFileSync } from 'node:fs'; import vm from 'node:vm'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const SRC = readFileSync(path.join(ROOT, 'modules/core/terminal.js'), 'utf8'); function makeCtx() { const ctx = { setInterval, clearInterval, setTimeout, clearTimeout, console, localStorage: { getItem: () => null, setItem: () => {}, removeItem: () => {} }, document: { createElement: () => ({ style: {}, appendChild: () => {}, classList: { add: () => {} }, setAttribute: () => {}, addEventListener: () => {} }), getElementById: () => null, addEventListener: () => {}, }, window: {}, }; ctx.window = ctx; vm.createContext(ctx); // terminal.js defines classes inside an IIFE/module scope — expose them for the test vm.runInContext(SRC + '\n;globalThis.__WebX11SecurityManager = (typeof WebX11SecurityManager !== "undefined") ? WebX11SecurityManager : null;', ctx, { filename: 'terminal.js' }); return ctx; } let pass = 0, fail = 0; const check = (name, cond, got) => { if (cond) { pass++; console.log(' ok ' + name); } else { fail++; console.log(' FAIL ' + name + (got !== undefined ? ' — got: ' + JSON.stringify(got) : '')); } }; const ctx = makeCtx(); if (!ctx.__WebX11SecurityManager) { console.error('WebX11SecurityManager not reachable at module scope — class is file-scoped.'); console.error('Check how terminal.js wraps the class and expose it for tests.'); process.exit(2); } // Stub terminal: capture print calls, provide the module registry shape security expects function makeStubTerminal() { const out = []; return { out, print: (t, ty) => out.push({ text: String(t), type: ty || '' }), moduleManager: { modules: { // A module with commands so _collectAllCommands finds them (tail needed for typo tests) 'FsModule': { commands: { 'ls': () => {}, 'cat': () => {}, 'grep': () => {}, 'tail': () => {}, 'head': () => {}, 'sed': () => {}, 'sort': () => {} } }, } }, security: null, // set by caller showConfirmationBar: null, showSuggestionBar: null, _tryAIRoute: null, _dispatch: null, }; } function makeSec(level, stubTerminal) { const sec = new ctx.__WebX11SecurityManager(stubTerminal, level); stubTerminal.security = sec; return sec; } const text = (out) => out.map(o => o.text).join('\n'); // ─── construction & base state ─── { const t = makeStubTerminal(); const sec = makeSec(1, t); check('constructs at level 1', sec.level === 1); check('depth counter starts at 0', sec._aiToolDepth === 0); check('suggest finds tail for taill', JSON.stringify(sec.suggest('taill')) === '{"cmd":"tail","dist":1}', sec.suggest('taill')); check('suggest null for gibberish', sec.suggest('zzzzzzzzz') === null); } // ─── level 2: unknown → blocked, no AI ─── { const t = makeStubTerminal(); const sec = makeSec(2, t); let aiRouted = false; sec._tryAIRoute = () => { aiRouted = true; }; sec._dispatch = () => {}; await sec.checkAndRun('foobarxyz'); check('L2 unknown blocked message', text(t.out).includes('blocked (level 2)'), text(t.out)); check('L2 unknown does not route to AI', aiRouted === false); } // ─── level 1 + AI depth: unknown → structured error, no bar, no AI route ─── { const t = makeStubTerminal(); const sec = makeSec(1, t); sec._aiToolDepth = 1; // simulate AI tool call let aiRouted = false; sec._tryAIRoute = () => { aiRouted = true; }; sec._dispatch = () => {}; await sec.checkAndRun('foobarxyz'); check('AI-depth unknown returns "Command not found"', text(t.out).includes('Command not found: foobarxyz'), text(t.out)); check('AI-depth unknown does not route to AI', aiRouted === false); const t2 = makeStubTerminal(); const sec2 = makeSec(1, t2); sec2._aiToolDepth = 2; // nested depth let aiRouted2 = false; sec2._tryAIRoute = () => { aiRouted2 = true; }; sec2._dispatch = () => {}; await sec2.checkAndRun('taill x.txt'); check('nested AI-depth typo returns error + suggestion', text(t2.out).includes('Command not found: taill') && text(t2.out).includes('tail'), text(t2.out)); check('nested AI-depth does not show suggestion bar', !t2.out.some(o => o.text.includes('did you mean:')), t2.out.map(o => o.text)); check('nested AI-depth does not route to AI', aiRouted2 === false); } // ─── level 1 user path: typo → suggestion bar (stub has no showSuggestionBar → fallback message) ─── { const t = makeStubTerminal(); const sec = makeSec(1, t); let aiRouted = false; sec._tryAIRoute = () => { aiRouted = true; }; sec._dispatch = () => {}; // stub terminal lacks showSuggestionBar → code takes AI-route fallback... verify current guard await sec.checkAndRun('taill x.txt'); // With showSuggestionBar missing, we expect routing to AI (bar unavailable) check('user typo without bar API routes to AI', aiRouted === true); } // ─── level 1 user path with bar API: typo → bar, no AI ─── { const t = makeStubTerminal(); t.showSuggestionBar = (cmd, args) => { t.barShown = { cmd, args }; }; const sec = makeSec(1, t); let aiRouted = false; sec._tryAIRoute = () => { aiRouted = true; }; sec._dispatch = () => {}; await sec.checkAndRun('taill x.txt'); check('user typo with bar API shows bar', t.barShown && t.barShown.cmd === 'tail', t.barShown); check('user typo with bar API does not route to AI', aiRouted === false); } // ─── level 1 user path: real unknown → AI route ─── { const t = makeStubTerminal(); const sec = makeSec(1, t); let aiRouted = false; sec._tryAIRoute = () => { aiRouted = true; }; sec._dispatch = () => {}; await sec.checkAndRun('foobarxyz'); check('user real-unknown routes to AI', aiRouted === true); check('user real-unknown message says routing', text(t.out).includes('routing to AI'), text(t.out)); } // ─── known whitelisted command dispatches normally ─── { const t = makeStubTerminal(); const sec = makeSec(1, t); let dispatched = null; sec._dispatch = (cmd, args) => { dispatched = { cmd, args }; }; await sec.checkAndRun('sed s/a/b/ file.txt'); check('whitelisted sed dispatches', dispatched && dispatched.cmd === 'sed', dispatched); check('no security messages for known cmd', t.out.length === 0, t.out); } // ─── AI depth does not leak across sequential calls ─── { const t = makeStubTerminal(); const sec = makeSec(1, t); sec._tryAIRoute = () => {}; sec._dispatch = () => {}; sec._aiToolDepth = 1; await sec.checkAndRun('nope1'); check('depth still 1 after first call', sec._aiToolDepth === 1); sec._aiToolDepth = 0; let aiRouted = false; sec._tryAIRoute = () => { aiRouted = true; }; await sec.checkAndRun('nope2'); check('depth 0 → routes to AI again', aiRouted === true); } // ─── L2 blocked message with suggestion ─── { const t = makeStubTerminal(); const sec = makeSec(2, t); sec._tryAIRoute = () => {}; sec._dispatch = () => {}; await sec.checkAndRun('taill x.txt'); check('L2 block includes did-you-mean', text(t.out).includes('Did you mean: "tail"?'), text(t.out)); } console.log(''); console.log(`${pass} passed, ${fail} failed`); process.exit(fail ? 1 : 0);