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
|
import fs from 'fs';
import vm from 'vm';
const SRC = '/var/www/webx11.duckdns.org/spaces/newos4/modules/ext/course.js';
const MANIFEST = { version: 'test-1', updated: '2026-10-18', chapters: [
{ id: '01-orientation', title: 'Orientation', lessons: [{ id: '1.1', title: 'T', objective: 'O', minutes: 5 }] }
], projects: ['p1'] };
const CHAPTER = { id: '01-orientation', title: 'Orientation', lessons: [{ id: '1.1', title: 'T', objective: 'O', minutes: 5,
blocks: [{ type: 'text', text: 'hello' }, { type: 'cmd', text: 'status' }, { type: 'tip', text: 'tip' }],
exercise: { instruction: 'touch ok.txt', accept: ['touch ok.txt'], check: { type: 'file', path: '/ok.txt' }, hint: 'touch ok.txt' } }] };
let printed = [];
const terminal = {
print(t) { printed.push(String(t)); },
fs: { _f: {}, readFile(p) { return Object.prototype.hasOwnProperty.call(this._f, p) ? this._f[p] : null; },
writeFile(p, c) { this._f[p] = c; }, mkdirRecursive() {}, fsReadJSON(p) { try { return JSON.parse(this._f[p] || 'null'); } catch (e) { return null; } },
fsWriteJSON(p, o) { this._f[p] = JSON.stringify(o); } },
_pushPrintCapture(cb) { this._cap = cb; }, _popPrintCapture() { this._cap && this._cap('output'); },
ollamaManager: { currentModel: 'test-model' }
};
const store = {};
const sandbox = {
window: { webx11: { moduleManager: { registerModule() {} } } },
localStorage: { getItem: k => store[k] ?? null, setItem: (k, v) => { store[k] = String(v); }, removeItem: k => { delete store[k]; } },
fetch: async (url) => {
if (url.includes('course.json')) return { ok: true, json: async () => MANIFEST };
if (url.includes('atlas.json')) return { ok: true, json: async () => ({ commands: { status: { desc: 'Status', module: 'system', tier: 'core-bundle', category: 'system' }, touch: { desc: 'Touch', module: 'fs', tier: 'core-bundle', category: 'filesystem' }, ls: { desc: 'List', module: 'fs', tier: 'core-bundle', category: 'filesystem' } } }) };
if (url.includes('chapters/')) return { ok: true, json: async () => CHAPTER };
if (url.includes('event.php')) return { ok: true, json: async () => ({}) };
if (url.includes('/ai/')) return { ok: true, json: async () => ({ choices: [{ message: { content: 'answer' } }] }) };
return { ok: false, status: 404, json: async () => ({}) };
},
console, document: { body: {} }, setTimeout, clearTimeout, Date, Math, JSON, RegExp
};
sandbox.globalThis = sandbox;
sandbox.webx11 = sandbox.window.webx11;
vm.createContext(sandbox);
vm.runInContext(fs.readFileSync(SRC, 'utf8') + '\n;globalThis.__CourseModule = CourseModule;', sandbox);
const mod = new sandbox.__CourseModule();
mod.terminal = terminal;
let pass = 0, fail = 0;
const check = (n, c, got) => { if (c) { pass++; console.log('PASS', n); } else { fail++; console.log('FAIL', n, got || ''); } };
const run = async () => {
mod.registerCommands();
await mod._warm();
await mod.handle(['list']);
check('list mentions Orientation', printed.join('\n').includes('Orientation'));
await mod.handle(['open', '1.1']);
check('lesson printed', printed.join('\n').includes('Exercise'));
mod.onCommandStart('touch', ['ok.txt']); terminal.fs.writeFile('/ok.txt', 'x'); mod.onCommandExecuted('touch', ['ok.txt']);
check('file exercise completes', !!mod._progress().completed['1.1'], JSON.stringify(mod._progress()));
check('xp awarded', mod._progress().xp >= 10);
check('match exact', mod._match('ls', 'ls') && !mod._match('ls', 'ls -la'));
check('match wildcard', mod._match('touch *', 'touch a.txt'));
await mod.handle(['ask', 'what is status?']);
check('tutor answered', printed.join('\n').includes('answer'));
check('skill installed', !!terminal.fs._f['/skills/course-01-orientation.md']);
console.log(`RESULT ${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);
};
run().catch(e => { console.error(e); process.exit(1); });
|