diff options
| author | Hermes SysOp <sysop@webx11.duckdns.org> | 2026-11-12 16:39:12 +0100 |
|---|---|---|
| committer | Hermes SysOp <sysop@webx11.duckdns.org> | 2026-11-12 16:39:12 +0100 |
| commit | 228ad621e2631487e74acdf9bf58e797f60ab745 (patch) | |
| tree | 6372c40b0ec6ec1470999b0e8e39cd267f24e979 /modules/test/test-doctor-thresholds.js | |
| parent | cbaa678ae624860ce4f918428a7b09d5f0e0ae2f (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.js | 341 |
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); } +})(); |