aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-doctor-thresholds.js
diff options
context:
space:
mode:
authorHermes SysOp <sysop@webx11.duckdns.org>2026-11-12 16:39:12 +0100
committerHermes SysOp <sysop@webx11.duckdns.org>2026-11-12 16:39:12 +0100
commit228ad621e2631487e74acdf9bf58e797f60ab745 (patch)
tree6372c40b0ec6ec1470999b0e8e39cd267f24e979 /modules/test/test-doctor-thresholds.js
parentcbaa678ae624860ce4f918428a7b09d5f0e0ae2f (diff)
fix(newos4): honest size/network measurements + checkpoint and phantom-delete fixes (v4.6.160)
Applies the three 2026-11-09 patches after review, and fixes two more navigator.onLine lies found while verifying them. Each patch was checked against the live code first; one of its claims did not survive. 1. virtual-fs — phantom deletes and checkpoint bloat - deleteCheckpoint('nope') returned true and printed "Deleted checkpoint" for a name that did not exist: _resolvePath() validates intermediate segments only, so the leaf was never checked. New _requireLeaf() refuses a resolved path whose leaf is absent, and deleteCheckpoint uses it. - CHECKPOINT SIZE: checkpoint() cloned the whole root minus _snapshots/_sessions/.trash, so _mods (module originals) and .semantic_index were duplicated into EVERY checkpoint — measured 2,101,686 bytes for a 2 MiB _mods. Both are now excluded on write and preserved across rollback (without the keep-list change a rollback would delete the live module originals). Measured after: 185 bytes. - Claim corrected: the patch also claimed deleteFile() reported success on a phantom path. It did not — live code already required `node && node.type === 'file'` and returned false. The guard is kept as defence-in-depth, and the test asserts the real behaviour rather than the claimed one. 2. module-fs — vfs-size reported ~2x the real size - It measured JSON.stringify(root).length: UTF-16 code units PLUS every structural character of the serialisation. A 1046-byte tree printed as 1961; anything under ~5 KB printed as "0.00 MB". - Every number is now stored bytes (UTF-8 of file content): one measure() walk for the total, the per-file table, the top-directory table and the system dirs, so the rows add up to the total. The patch's arithmetic was still off in seven places, all fixed here: a UTF-16 fallback that reintroduced the bug when Blob was missing, a top-5 table that could never sum to the total (now top 10 + an aggregated remainder row), the top-dirs table dropping files stored at root (now a "/(root files)" row), a 2-decimal MB total that printed 0.00 for small VFS, three duplicate hand-rolled walks, readDir() used for dir sizes, and "1 files". 3. module-system — status/doctor no longer trust navigator.onLine - status printed "Network: Offline" from navigator.onLine, which an Android WebView reports false while fully connected. - doctor now awaits a real HEAD probe (AbortController, 4 s): only 'reachable' is healthy, a throw is HIGH "Network: unreachable", a non-ok answer is MEDIUM and names the status (405 to HEAD is not "offline"). - THE PATCH'S THRESHOLDS COULD NEVER FIRE: it compared localStorage bytes against navigator.storage.estimate().quota, which is the whole-origin quota (GBs, mostly Cache Storage), while localStorage has its own ~5 MB cap — a 4.5 MB localStorage reported "All systems healthy" against a 2 GB quota. Now: localStorage measured as (key+value)*2 UTF-16 bytes with HIGH > 4 MiB / MEDIUM > 3 MiB, VFS measured in UTF-8 bytes with HIGH > 3 MiB and > 80% of the measured localStorage bytes. The origin quota appears only in one informational line and decides no severity. - Also removed navigator.onLine from version, bug-report and bug (a grep for it is now 0 in the module), and added a "Measured:" line so a clean report still shows what was measured. 4. terminal.js / module-servermon.js — the same lie, where the user looks - The status bar printed "❌ Offline" in red from navigator.onLine; on a WebView that is false while online. It now shows "📶 Network?" with a title pointing at `doctor` unless the browser claims online. - `net` test line now reads "Browser reports: offline (unreliable)" as a warning instead of a red failure — the real verdict comes from the probes that follow. Verified: 40/40 node tests (3 new files: 36 + 53 + 34 assertions, each proven red against the pre-patch code; the doctor test runs against the PATCH's threshold logic and shows the 4.5 MB case staying silent). An independent orchestrator probe re-derived all of it: phantom delete false, 2 MB _mods absent from the checkpoint and preserved through rollback, vfs-size exact bytes for ASCII/é/☃ nested trees, 4.5 MB localStorage HIGH despite a 2 GB quota, failing probe unreachable, status never claiming Online/Offline. Served bytes identical for core-bundle.js / main.html / module-system.js / virtual-fs.js. Live Chromium on v4.6.160: vfs-size "85.8 KB (87880 bytes) across 40 files", doctor "Measured: VFS 0.08 MB UTF-8 · localStorage 0.07 MB UTF-16 in 14 keys", a stubbed failing probe reports HIGH unreachable, the status bar reads "📶 Network?" when the browser lies, deleteCheckpoint on a missing name returns false, and the console has 0 warnings and 0 JS errors.
Diffstat (limited to 'modules/test/test-doctor-thresholds.js')
-rw-r--r--modules/test/test-doctor-thresholds.js341
1 files changed, 341 insertions, 0 deletions
diff --git a/modules/test/test-doctor-thresholds.js b/modules/test/test-doctor-thresholds.js
new file mode 100644
index 0000000..120b941
--- /dev/null
+++ b/modules/test/test-doctor-thresholds.js
@@ -0,0 +1,341 @@
+// 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); }
+})();