diff options
Diffstat (limited to 'scripts/test-security.mjs')
| -rw-r--r-- | scripts/test-security.mjs | 194 |
1 files changed, 194 insertions, 0 deletions
diff --git a/scripts/test-security.mjs b/scripts/test-security.mjs new file mode 100644 index 0000000..f94a4fe --- /dev/null +++ b/scripts/test-security.mjs @@ -0,0 +1,194 @@ +#!/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);
\ No newline at end of file |