1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
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);
|