aboutsummaryrefslogtreecommitdiff
path: root/scripts/test-course.mjs
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-18 14:05:18 +0200
committerAgando <roduccomm@gmail.com>2026-10-18 14:05:18 +0200
commit88ad8ed2fbbda61920b11a79df29acb58928b5af (patch)
tree7fd3df420ed78e59ccdc7861c7408347b207cfb5 /scripts/test-course.mjs
parent005a1ad651383cca9793f24213de3a22c2474967 (diff)
newOS4: interactive AI course (terminal module, portal, atlas, telemetry, daily maintenance+cron) + free store card
Diffstat (limited to 'scripts/test-course.mjs')
-rw-r--r--scripts/test-course.mjs59
1 files changed, 59 insertions, 0 deletions
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); });