From 88ad8ed2fbbda61920b11a79df29acb58928b5af Mon Sep 17 00:00:00 2001 From: Agando Date: Sun, 18 Oct 2026 14:05:18 +0200 Subject: newOS4: interactive AI course (terminal module, portal, atlas, telemetry, daily maintenance+cron) + free store card --- scripts/test-course.mjs | 59 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 scripts/test-course.mjs (limited to 'scripts/test-course.mjs') diff --git a/scripts/test-course.mjs b/scripts/test-course.mjs new file mode 100644 index 0000000..7a64dc8 --- /dev/null +++ b/scripts/test-course.mjs @@ -0,0 +1,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); }); -- cgit v1.2.3