// node modules/test/test-network-payload-honesty.js // Payload honesty: no payload may turn `navigator.onLine` into a network verdict, and the // world-model VFS size must be STORED UTF-8 BYTES. // // Why: an Android WebView reports navigator.onLine === false while the page is fully connected. // v4.6.159/160 removed that boolean from `status`, `doctor`, the status bar and the net test; // these are the two payloads that were left, plus every reader that printed "Offline" from them. // // Asserts (all on OUTPUT, never on a regex over source): // · navigator.onLine === false must not produce `online: false` in the aiassist health payload, // nor any online/offline claim in the world-model snapshot or its LLM digest; // · world-model `vfsSize` == UTF-8 byte sum ('é' 2 + '☃' 3 + 1000 ASCII + 'abc' 3 = 1008), // and != JSON.stringify(root).length (UTF-16 units + serialisation structure); // · the consumer renders 'unknown': the real dashboard script is executed against a stub DOM // and the actual health panel HTML is inspected; world-model's showModel output is captured; // · a missing `navigator` does not throw. // // N4_CORE / N4_DASH point the same assertions at another build (mutation check). const fsMod = require('fs'), path = require('path'); const CORE = process.env.N4_CORE || path.join(__dirname, '..', 'core'); const DASH = process.env.N4_DASH || path.join(__dirname, '..', '..', 'downloads', 'dashboard.html'); // ── browser globals the modules reach for ────────────────────────────────── const store = {}; global.window = global; global.localStorage = { getItem: k => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: k => { delete store[k]; }, }; global.indexedDB = { open() { throw new Error('no idb'); }, deleteDatabase() { throw new Error('no idb'); } }; global.NEWOS4_VERSION = 'v4.6.160-test'; function setNavigator(onLine) { if (onLine === 'missing') { try { delete globalThis.navigator; } catch (e) { /* older node: shadow below */ } try { Object.defineProperty(globalThis, 'navigator', { configurable: true, value: undefined }); } catch (e) {} return; } Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { onLine: onLine, userAgent: 'test-agent', storage: undefined }, }); } // ── classes, loaded from source exactly as the bundle does ───────────────── const WorldModelModule = new Function( fsMod.readFileSync(path.join(CORE, 'module-world-model.js'), 'utf8') + '\nreturn WorldModelModule;')(); const AI = new Function( fsMod.readFileSync(path.join(CORE, 'module-aiassist.js'), 'utf8') + '\nreturn WebX11AIAssistantModule;')(); let fails = 0, passes = 0; function ok(name, cond, detail) { if (cond) { passes++; return; } fails++; console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); } // ── the honest tree: 'é' = 2 B, '☃' = 3 B, 1000 ASCII, nested 'abc' = 3 B ── const EXPECTED_BYTES = 2 + 3 + 1000 + 3; // 1008 stored bytes function makeTree() { return { type: 'dir', content: { 'e.txt': { type: 'file', content: 'é' }, 's.txt': { type: 'file', content: '☃' }, 'ascii.txt': { type: 'file', content: 'x'.repeat(1000) }, 'sub': { type: 'dir', content: { 'n.txt': { type: 'file', content: 'abc' } } }, }, }; } // ── an in-memory VFS with only the methods these modules call ────────────── function mkFs(root) { const writes = new Map(), json = new Map(); const resolve = (p) => { if (!p || p === '/') return root; const parts = String(p).replace(/^\/+|\/+$/g, '').split('/').filter(Boolean); let node = root; for (const part of parts) { node = node && node.content && node.content[part]; if (!node) return null; } return node || null; }; const fs = { root, currentPath: [], writes, ls(p) { const node = resolve(p || '/'); if (!node || !node.content) return []; return Object.keys(node.content).map(name => ({ name, type: node.content[name].type, _virtual: false, })); }, readDir(p) { return fs.ls(p); }, readFile(p) { if (writes.has(p)) return writes.get(p); const n = resolve(p); return n && n.type === 'file' ? (n.content || '') : null; }, writeFile(p, c) { writes.set(p, String(c)); return true; }, deleteFile(p) { writes.delete(p); return true; }, mkdir() { return true; }, fsWriteJSON(p, obj) { json.set(p, obj); writes.set(p, JSON.stringify(obj)); return true; }, fsReadJSON(p) { if (json.has(p)) return json.get(p); if (writes.has(p)) { try { return JSON.parse(writes.get(p)); } catch (e) { return null; } } const n = resolve(p); if (n && n.type === 'file') { try { return JSON.parse(n.content); } catch (e) { return null; } } return null; }, }; return fs; } function mkTerminal(fs, wm) { const out = []; return { out, text: () => out.map(l => l.msg).join('\n'), fs, ollamaManager: { connected: false, currentModel: null, currentProvider: null }, moduleManager: { modules: wm ? { WorldModelModule: wm } : {}, moduleRegistry: [{ name: 'A' }, { name: 'B' }], getAllCommands: () => ({}), }, countErrors: () => ({ total: 0, debug: 0, latency: 0, log: 0 }), print: (msg, cls) => out.push({ msg: String(msg), cls: cls || '' }), }; } function newWorldModel() { const wm = new WorldModelModule(); wm.terminal = mkTerminal(mkFs(makeTree())); wm._synthesize(); return wm; } async function aiassistPayload(wm) { const ai = Object.create(AI.prototype); ai.terminal = mkTerminal(mkFs(makeTree()), wm); return await ai._collectDashboardData(); } // ══════════════════════════════════════════════════════════════════════════ // A/B. navigator.onLine === false must not become a payload fact // ══════════════════════════════════════════════════════════════════════════ (async function main() { setNavigator(false); const wm = newWorldModel(); const data = await aiassistPayload(wm); // ── A. aiassist health payload (debug panel / dashboard source) ── const health = (data && data.health) || {}; ok('A1 health payload has no `online` key', !Object.prototype.hasOwnProperty.call(health, 'online'), JSON.stringify(health)); ok('A2 serialised health carries no "online"', JSON.stringify(health).indexOf('"online"') === -1, JSON.stringify(health)); ok('A3 health.network is "unknown"', health.network === 'unknown', String(health.network)); ok('A4 health.networkKnown is false', health.networkKnown === false, String(health.networkKnown)); ok('A5 health.vfsSizeBytes is the UTF-8 sum', health.vfsSizeBytes === EXPECTED_BYTES, String(health.vfsSizeBytes)); ok('A6 health.vfsSize is a byte figure, not JSON length', typeof health.vfsSize === 'string' && /B$/.test(health.vfsSize.trim()), String(health.vfsSize)); ok('A7 whole payload contains no "online":false', JSON.stringify(data).indexOf('"online":false') === -1); ok('A8 aiassist vfs bytes differ from JSON.stringify(root).length', health.vfsSizeBytes !== JSON.stringify(makeTree()).length, health.vfsSizeBytes + ' vs ' + JSON.stringify(makeTree()).length); // ── B. world-model snapshot + LLM digest ── const sys = (wm._model && wm._model.system) || {}; ok('B1 world-model system has no `online` key', !Object.prototype.hasOwnProperty.call(sys, 'online'), JSON.stringify(sys)); ok('B2 world-model network is "unknown"', sys.network === 'unknown', String(sys.network)); ok('B3 world-model networkKnown is false', sys.networkKnown === false, String(sys.networkKnown)); const snap = JSON.stringify(wm._modelSnapshot()); ok('B4 snapshot has no "online" field', snap.indexOf('"online"') === -1); ok('B5 snapshot states network unknown', /"network":"unknown"/.test(snap)); const digest = wm._modelDigest(); ok('B6 digest makes no Online/Offline claim', !/\b(Online|Offline)\b/.test(digest), digest.slice(0, 200)); ok('B7 world-model vfsSize is the UTF-8 sum', sys.vfsSize === EXPECTED_BYTES, String(sys.vfsSize)); ok('B8 vfsSize differs from JSON.stringify(root).length', sys.vfsSize !== JSON.stringify(makeTree()).length, sys.vfsSize + ' vs ' + JSON.stringify(makeTree()).length); ok('B9 files.sizeBytes reconciles with system.vfsSize', wm._model.files.sizeBytes === sys.vfsSize, wm._model.files.sizeBytes + ' vs ' + sys.vfsSize); // ── C. the UTF-8 unit itself, one file at a time (no averaging) ── // Guarded so a missing helper FAILS an assertion instead of aborting the run. const u8len = (s) => (typeof wm._utf8Len === 'function' ? wm._utf8Len(s) : NaN); const measure = (n) => (typeof wm._measureVfs === 'function' ? wm._measureVfs(n) : { bytes: NaN, files: NaN }); ok('C1 é counts as 2 bytes', u8len('é') === 2, String(u8len('é'))); ok('C2 ☃ counts as 3 bytes', u8len('☃') === 3, String(u8len('☃'))); ok('C3 a nested dir adds only its own file content', measure(makeTree().content['sub']).bytes === 3, String(measure(makeTree().content['sub']).bytes)); ok('C4 measure counts files, not directories', measure(makeTree()).files === 4, String(measure(makeTree()).files)); // ══════════════════════════════════════════════════════════════════════ // D. the consumer renders 'unknown' — real dashboard script, stub DOM // ══════════════════════════════════════════════════════════════════════ const dashHtml = fsMod.readFileSync(DASH, 'utf8'); let dashScript = null; for (const m of dashHtml.matchAll(/