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); });