// node modules/test/test-doctor-thresholds.js // Proves `doctor` measures real storage cost and can actually fire: // · network comes from a live HEAD probe, never from the legacy onLine boolean; // · localStorage is charged as UTF-16 (Σ (key+value) × 2), against its own ~5 MB cap; // · VFS is summed as UTF-8 bytes of file content (Blob), the same unit vfs-size uses; // · navigator.storage.estimate() is CONTEXT ONLY — a 2 GB origin quota must NOT suppress a // 4.5 MB localStorage finding. That is the exact defect in the proposed patch, whose // `lsBytes > quota * 0.8` comparison could never be true on a real browser. // Run through the real class loaded from modules/core/module-system.js. const fsMod = require('fs'), path = require('path'); const ROOT = path.join(__dirname, '..', 'core'); // N4_SRC lets the same assertions run against another build of the file (mutation check). const SRC_PATH = process.env.N4_SRC || path.join(ROOT, 'module-system.js'); global.window = global; const src = fsMod.readFileSync(SRC_PATH, 'utf8'); const SystemModule = new Function(src + '\nreturn SystemModule;')(); // ── a localStorage that charges what a browser charges ────────────────────── // Values are held as given; the module must do the ×2 itself, so this fake reports // .length (UTF-16 code units) exactly like the real Storage object does. function makeLS(pairs) { const map = new Map(Object.entries(pairs || {})); return { get length() { return map.size; }, key(i) { return Array.from(map.keys())[i] ?? null; }, getItem(k) { return map.has(k) ? map.get(k) : null; }, setItem(k, v) { map.set(k, String(v)); }, removeItem(k) { map.delete(k); }, clear() { map.clear(); }, _map: map, }; } // ── terminal stub ────────────────────────────────────────────────────────── function mkTerminal(opts) { const out = []; const files = (opts && opts.files) || {}; const dirs = (opts && opts.dirs) || { '/': [] }; return { out, text: () => out.map(l => l.msg).join('\n'), fs: { root: (opts && opts.root) || { type: 'dir', content: {} }, ls: () => (dirs['/'] || []), readDir: (p) => dirs[p] || null, readFile: (p) => (p in files ? files[p] : null), }, ollamaManager: { connected: (opts && opts.ollama !== undefined) ? opts.ollama : true }, moduleManager: { modules: { SystemModule: {}, VirtualFSModule: {} } }, print: (msg, cls) => out.push({ msg: String(msg), cls: cls || '' }), }; } function mkModule(opts) { const t = mkTerminal(opts); const m = new SystemModule(); m.terminal = t; m.registerCommands(); return { m, t }; } let fails = 0, passes = 0; function ok(name, cond, detail) { if (cond) { passes++; return; } fails++; console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); } // ── helpers to build payloads of an exact byte size ──────────────────────── const kb = 1024; function asciiFile(mb) { return 'a'.repeat(Math.round(mb * 1024 * 1024)); } // A VFS file node tree holding one big ASCII file. function rootWithFile(content) { return { type: 'dir', content: { tmp: { type: 'dir', content: { 'big.txt': { type: 'file', content } } } } }; } // Drive `doctor` with a given fake browser surface. async function doctor(opts) { const { m, t } = mkModule(opts); const o = opts || {}; const savedNavigator = global.navigator; const savedFetch = global.fetch; const savedLS = global.localStorage; global.localStorage = o.ls || makeLS({}); if ('fetch' in o) { if (o.fetch === undefined) delete global.fetch; else global.fetch = o.fetch; } if ('storage' in o) { const nav = Object.assign({}, savedNavigator); if (o.storage === undefined) delete nav.storage; else nav.storage = o.storage; Object.defineProperty(global, 'navigator', { value: nav, configurable: true, writable: true }); } try { const r = m.commands['doctor'](); if (r && typeof r.then === 'function') await r; } finally { global.fetch = savedFetch; global.localStorage = savedLS; Object.defineProperty(global, 'navigator', { value: savedNavigator, configurable: true, writable: true }); } return t; } // Extract the issue lines the report printed (top 3) plus the full issue count line. function issueLines(t) { return t.out.map(l => l.msg).filter(s => /^\s{4}\S+ \[(HIGH|MEDIUM|LOW)\]/.test(s)); } function report(t) { return t.text(); } function okFetch(status) { return () => Promise.resolve({ ok: status >= 200 && status < 300, status }); } // ══════════════════════════════════════════════════════════════════════════ // A. Network — live probe, not the legacy boolean // ══════════════════════════════════════════════════════════════════════════ (async () => { // A1: a healthy fetch must produce NO Network issue at all. { const t = await doctor({ fetch: okFetch(200), files: {} }); ok('A1 fetch ok → no Network issue', !/Network: (unreachable|HTTP probe)/.test(report(t)), report(t)); ok('A1b report says reachable', /Network:\s+reachable/.test(report(t)), report(t)); } // A2: a rejected fetch must be a HIGH unreachable finding. { const t = await doctor({ fetch: () => Promise.reject(new Error('net::ERR_NAME_NOT_RESOLVED')) }); const txt = report(t); ok('A2 fetch reject → high Network: unreachable', /\[HIGH\] Network: unreachable/.test(txt), txt); } // A3: a 503 must be named, not flattened to "offline". { const t = await doctor({ fetch: okFetch(503) }); const txt = report(t); ok('A3 fetch 503 → Network issue naming http-503', /Network:.*http-503/.test(txt), txt); ok('A3b 503 is not reported as unreachable', !/Network: unreachable/.test(txt), txt); } // A4: legacy boolean must not exist in the source at all. { const hits = src.split('\n').filter(l => /navigator\s*\.\s*onLine/.test(l)).length; ok('A4 no navigator.onLine reference in module-system.js', hits === 0, hits + ' hit(s)'); } // ══════════════════════════════════════════════════════════════════════ // B. localStorage — UTF-16 cost against its own ~5 MB cap // ══════════════════════════════════════════════════════════════════════ // 4.5 MB of UTF-16 = 2.25 M code units of value + key. { const t = await doctor({ fetch: okFetch(200), ls: makeLS({ webx11_big: 'a'.repeat(Math.round(4.5 * 1024 * 1024 / 2)) }), }); const txt = report(t); ok('B1 localStorage 4.5 MB → HIGH localStorage issue', /\[HIGH\] localStorage: 4\.5 MB/.test(txt), txt); ok('B1b message names the ~5 MB per-origin cap', /~5 MB per-origin cap/.test(txt), txt); } { // 3.2 MB of UTF-16 = 1.6 M code units. const t = await doctor({ fetch: okFetch(200), ls: makeLS({ webx11_mid: 'b'.repeat(Math.round(3.2 * 1024 * 1024 / 2)) }), }); const txt = report(t); ok('B2 localStorage 3.2 MB → MEDIUM', /\[MEDIUM\] localStorage: 3\.2 MB/.test(txt), txt); ok('B2b no HIGH localStorage at 3.2 MB', !/\[HIGH\] localStorage/.test(txt), txt); } { const t = await doctor({ fetch: okFetch(200), ls: makeLS({ webx11_small: 'c'.repeat(Math.round(1 * 1024 * 1024 / 2)) }), }); ok('B3 localStorage 1 MB → no localStorage issue', !/localStorage:/.test(report(t)), report(t)); } // ══════════════════════════════════════════════════════════════════════ // C. VFS — UTF-8 bytes of content, share of the localStorage budget // ══════════════════════════════════════════════════════════════════════ { // 3.5 MB of ASCII VFS + 4 MB localStorage budget → 87% share → HIGH. const t = await doctor({ fetch: okFetch(200), root: rootWithFile(asciiFile(3.5)), ls: makeLS({ webx11_vfs: 'd'.repeat(Math.round(4 * 1024 * 1024 / 2)) }), }); const txt = report(t); ok('C1 VFS 3.5 MB of 4 MB budget → HIGH VFS issue', /\[HIGH\] VFS: 3\.5 MB/.test(txt), txt); ok('C1b message states the measured share', /87% of the 4\.0 MB localStorage budget/.test(txt), txt); ok('C1c VFS does not claim a JSON limit', !/512KB|256KB/.test(txt), txt); } { // 1 MB VFS → under both thresholds, nothing. const t = await doctor({ fetch: okFetch(200), root: rootWithFile(asciiFile(1)), ls: makeLS({ webx11_vfs: 'e'.repeat(Math.round(4 * 1024 * 1024 / 2)) }), }); ok('C2 VFS 1 MB → no VFS issue', !/VFS: /.test(report(t)), report(t)); } { // 2.5 MB VFS → MEDIUM (large alone, but not a big share of an 8 MB budget). const t = await doctor({ fetch: okFetch(200), root: rootWithFile(asciiFile(2.5)), ls: makeLS({ webx11_vfs: 'f'.repeat(Math.round(8 * 1024 * 1024 / 2)) }), }); const txt = report(t); ok('C3 VFS 2.5 MB → MEDIUM', /\[MEDIUM\] VFS: 2\.5 MB/.test(txt), txt); } { // UTF-8, not chars: 1 MiB of 'é' is 2.00 MB of UTF-8 (chars would read 1.00 MB). const t = await doctor({ fetch: okFetch(200), root: rootWithFile('é'.repeat(1024 * 1024)), ls: makeLS({ webx11_vfs: 'g'.repeat(Math.round(8 * 1024 * 1024 / 2)) }), }); ok('C4 multibyte counted as UTF-8 bytes (1M é = 2.00 MB)', /Measured: VFS 2\.00 MB UTF-8/.test(report(t)), report(t)); } // ══════════════════════════════════════════════════════════════════════ // D. The patch's bug: origin quota must not suppress a real finding // ══════════════════════════════════════════════════════════════════════ { const est = () => Promise.resolve({ usage: 40 * 1024 * 1024, quota: 2 * 1024 * 1024 * 1024 }); const t = await doctor({ fetch: okFetch(200), storage: { estimate: est }, ls: makeLS({ webx11_big: 'h'.repeat(Math.round(4.5 * 1024 * 1024 / 2)) }), }); const txt = report(t); ok('D1 2 GB origin quota does NOT suppress a 4.5 MB localStorage finding', /\[HIGH\] localStorage: 4\.5 MB/.test(txt), txt); ok('D2 quota is printed as context only', /Origin usage 40\.0 MB of ~2048 MB quota/.test(txt), txt); ok('D3 context line states localStorage has its own cap', /localStorage has its own ~5 MB cap/.test(txt), txt); ok('D4 quota does not turn a big VFS healthy either', !/All systems healthy/.test(txt), txt); } { // And a big quota must not invent an issue where none exists. const t = await doctor({ fetch: okFetch(200), storage: { estimate: () => Promise.resolve({ usage: 10 * 1024 * 1024, quota: 2 * 1024 * 1024 * 1024 }) }, ls: makeLS({ webx11_a: 'tiny' }), }); ok('D5 healthy storage + huge quota → all healthy', /All systems healthy/.test(report(t)), report(t)); } // ══════════════════════════════════════════════════════════════════════ // E. Degraded browsers — every probe is individually guarded // ══════════════════════════════════════════════════════════════════════ { const t = await doctor({ fetch: undefined, files: {} }); const txt = report(t); ok('E1 fetch undefined → completes and prints a report', /Doctor Report/.test(txt) && /Measured: VFS/.test(txt), txt); ok('E1b no Network issue invented without a probe', !/Network: unreachable/.test(txt), txt); } { // Blob removed: the UTF-8 fallback must still measure 2 bytes per 'é'. const savedBlob = global.Blob; delete global.Blob; try { const t = await doctor({ fetch: okFetch(200), root: rootWithFile('é'.repeat(1024 * 1024)), ls: makeLS({ webx11_vfs: 'i'.repeat(Math.round(8 * 1024 * 1024 / 2)) }), }); ok('E2 Blob undefined → completes and still counts UTF-8 (2.00 MB)', /Doctor Report/.test(report(t)) && /Measured: VFS 2\.00 MB UTF-8/.test(report(t)), report(t)); } finally { global.Blob = savedBlob; } } { const t = await doctor({ fetch: okFetch(200), storage: undefined, files: {} }); const txt = report(t); ok('E3 storage.estimate undefined → completes, no quota line', /Doctor Report/.test(txt) && !/Origin usage/.test(txt), txt); } { // estimate() that throws must not kill the report. const t = await doctor({ fetch: okFetch(200), storage: { estimate: () => Promise.reject(new Error('SecurityError')) }, }); ok('E4 estimate() rejecting → completes', /Doctor Report/.test(report(t)), report(t)); } // ══════════════════════════════════════════════════════════════════════ // F. status must not claim Online/Offline // ══════════════════════════════════════════════════════════════════════ { const { m, t } = mkModule({ fetch: okFetch(200) }); m.commands['status'](); const txt = report(t); const netLine = t.out.map(l => l.msg).find(s => /^Network:/.test(s)) || ''; ok('F1 status prints a Network line', netLine.length > 0, txt); ok('F2 status line says neither Online nor Offline', !/\b(Online|Offline)\b/.test(netLine), netLine); ok('F3 status points at doctor', /doctor/.test(netLine), netLine); ok('F4 status still reports files/modules/ollama', /^Files:/m.test(txt) && /^Modules:/m.test(txt) && /^Ollama:/m.test(txt), txt); } // ══════════════════════════════════════════════════════════════════════ // G. Output shape kept: top 3 issues + Recommended actions // ══════════════════════════════════════════════════════════════════════ { const t = await doctor({ fetch: () => Promise.reject(new Error('down')), ollama: false, root: rootWithFile(asciiFile(3.5)), ls: makeLS({ webx11_big: 'j'.repeat(Math.round(4.5 * 1024 * 1024 / 2)) }), files: { '/logs/current.log': Array.from({ length: 900 }, () => 'x').join('\n') }, }); const txt = report(t); ok('G1 at most 3 issue lines printed', issueLines(t).length <= 3, issueLines(t).length + ' lines'); ok('G2 remainder is summarised', /\.\.\. and \d+ more/.test(txt), txt); ok('G3 Recommended actions section present', /Recommended actions:/.test(txt), txt); } // ── summary ─────────────────────────────────────────────────────────── console.log('\n' + passes + '/' + (passes + fails) + ' assertions passed'); if (fails > 0) { console.log(fails + ' FAILED'); process.exit(1); } })();