From 228ad621e2631487e74acdf9bf58e797f60ab745 Mon Sep 17 00:00:00 2001 From: Hermes SysOp Date: Thu, 12 Nov 2026 16:39:12 +0100 Subject: fix(newos4): honest size/network measurements + checkpoint and phantom-delete fixes (v4.6.160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- modules/test/test-virtual-fs-require-leaf.js | 160 +++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 modules/test/test-virtual-fs-require-leaf.js (limited to 'modules/test/test-virtual-fs-require-leaf.js') diff --git a/modules/test/test-virtual-fs-require-leaf.js b/modules/test/test-virtual-fs-require-leaf.js new file mode 100644 index 0000000..7b62d0e --- /dev/null +++ b/modules/test/test-virtual-fs-require-leaf.js @@ -0,0 +1,160 @@ +// node modules/test/test-virtual-fs-require-leaf.js +// Proves the 2026-11-09 virtual-fs patch: phantom deletes are refused (_requireLeaf) and +// checkpoints stop cloning _mods / .semantic_index (while rollback keeps the live copies). +// Offline: no browser, no real localStorage, no IndexedDB, no timers. +const fsMod = require('fs'), path = require('path'); +const ROOT = path.join(__dirname, '..', 'core'); + +// ── persistence stub: in-memory localStorage, IndexedDB that refuses to exist ── +const ls = {}; +global.localStorage = { getItem: k => (k in ls ? ls[k] : null), setItem: (k, v) => { ls[k] = String(v); }, removeItem: k => { delete ls[k]; } }; +global.indexedDB = { open() { throw new Error('no idb in tests'); }, deleteDatabase() { throw new Error('no idb in tests'); } }; +global.window = global; + +// The file ends with `window.VirtualFileSystem = VirtualFileSystem;` — so the class is a +// window global *and* is in scope at the bottom of the script. Return the real class object. +function load(file, cls) { + const src = fsMod.readFileSync(path.join(ROOT, file), 'utf8'); + return new Function(src + '\nreturn ' + cls + ';')(); +} +const VFS = load('virtual-fs.js', 'VirtualFileSystem'); + +let fails = 0, passes = 0; +function ok(name, cond, detail) { + if (cond) { passes++; return; } + fails++; + console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); +} +function mk() { + const vfs = new VFS(); + vfs.saveToStorage = () => true; // keep the 2s setTimeout saver out of the test + vfs.root = { type: 'dir', content: {} }; + vfs.currentPath = []; + if (vfs._ready && typeof vfs._ready.catch === 'function') vfs._ready.catch(() => {}); + return vfs; +} +const keysOf = node => Object.keys(node.content).sort(); + +// ── 0. export shape ──────────────────────────────────────────────────────────── +ok('0 export shape: window.VirtualFileSystem is the class', typeof VFS === 'function' && global.window.VirtualFileSystem === VFS); +ok('0 constructor name', VFS.name === 'VirtualFileSystem', VFS.name); +ok('0 has _requireLeaf guard', typeof VFS.prototype._requireLeaf === 'function'); + +// ── A. deleteFile of a missing leaf must be refused, sibling untouched ───────── +{ + const vfs = mk(); + vfs.mkdirRecursive('/docs'); + vfs.writeFile('/docs/real.txt', 'keep me'); + ok('A1 deleteFile("/docs/ghost.txt") returns false', vfs.deleteFile('/docs/ghost.txt') === false); + ok('A2 sibling survived', vfs.readFile('/docs/real.txt') === 'keep me'); + ok('A3 nothing was trashed', !vfs.root.content['.trash'] || keysOf(vfs.root.content['.trash']).length === 0); + ok('A4 deleteFile("/no/such/dir/x.txt") returns false', vfs.deleteFile('/no/such/dir/x.txt') === false); + + // ── B. deleteFile of a real file removes exactly that file ──────────────── + vfs.writeFile('/docs/other.txt', 'other'); + ok('B1 deleteFile of a real file returns true', vfs.deleteFile('/docs/real.txt') === true); + ok('B2 the file is gone', vfs.readFile('/docs/real.txt') === null); + ok('B3 sibling intact', vfs.readFile('/docs/other.txt') === 'other'); + ok('B4 only that entry was removed', JSON.stringify(keysOf(vfs.root.content.docs)) === '["other.txt"]', JSON.stringify(keysOf(vfs.root.content.docs))); +} + +// ── C. deleteCheckpoint of a nonexistent checkpoint is refused ──────────────── +{ + const vfs = mk(); + vfs.mkdirRecursive('/_snapshots'); + vfs.writeFile('/_snapshots/checkpoint-keep.json', JSON.stringify({ createdAt: '2026-01-01T00:00:00.000Z' })); + ok('C1 deleteCheckpoint("nope") is falsy', !vfs.deleteCheckpoint('nope')); + ok('C2 _snapshots unchanged', JSON.stringify(keysOf(vfs.root.content['_snapshots'])) === '["checkpoint-keep.json"]', JSON.stringify(keysOf(vfs.root.content['_snapshots']))); + ok('C3 existing checkpoint still readable', !!vfs.fsReadJSON('/_snapshots/checkpoint-keep.json')); + ok('C4 real checkpoint delete still returns true', vfs.deleteCheckpoint('keep') === true); + ok('C5 it was actually removed', keysOf(vfs.root.content['_snapshots']).length === 0); +} + +// ── D. a checkpoint must not carry _mods or .semantic_index ─────────────────── +{ + const vfs = mk(); + vfs.mkdirRecursive('/_mods'); vfs.writeFile('/_mods/mod-a.js', 'MOD-ORIGINAL'); + vfs.mkdirRecursive('/.semantic_index'); vfs.writeFile('/.semantic_index/idx.json', '{"nodes":1}'); + vfs.writeFile('/notes.txt', 'plain'); + const name = vfs.checkpoint('excl'); + const snap = vfs.fsReadJSON('/_snapshots/checkpoint-' + name + '.json'); + ok('D1 checkpoint written', !!snap && !!snap.root); + ok('D2 _mods absent from the snapshot root', !('_mods' in snap.root.content), JSON.stringify(Object.keys(snap.root.content))); + ok('D3 .semantic_index absent from the snapshot root', !('.semantic_index' in snap.root.content)); + ok('D4 ordinary files are still snapshotted', snap.root.content['notes.txt'] && snap.root.content['notes.txt'].content === 'plain'); + ok('D5 scaffolding still excluded (_snapshots/_sessions/.trash)', + !('_snapshots' in snap.root.content) && !('_sessions' in snap.root.content) && !('.trash' in snap.root.content)); + + // ── E. rollback keeps the LIVE _mods / .semantic_index ──────────────────── + vfs.writeFile('/notes.txt', 'edited after checkpoint'); + ok('E0 pre-rollback edit in place', vfs.readFile('/notes.txt') === 'edited after checkpoint'); + ok('E1 rollback returns true', vfs.rollback(name) === true); + ok('E2 live _mods survives rollback', vfs.readFile('/_mods/mod-a.js') === 'MOD-ORIGINAL', String(vfs.readFile('/_mods/mod-a.js'))); + ok('E3 live .semantic_index survives rollback', vfs.readFile('/.semantic_index/idx.json') === '{"nodes":1}'); + ok('E4 ordinary file rolled back to the snapshot content', vfs.readFile('/notes.txt') === 'plain', String(vfs.readFile('/notes.txt'))); + ok('E5 snapshot bookkeeping survived rollback', keysOf(vfs.root.content['_snapshots']).indexOf('checkpoint-' + name + '.json') !== -1); +} + +// ── F. a legacy checkpoint that *does* embed _mods must not clobber the live one ── +{ + const vfs = mk(); + vfs.mkdirRecursive('/_mods'); vfs.writeFile('/_mods/live.js', 'LIVE'); + vfs.mkdirRecursive('/.semantic_index'); vfs.writeFile('/.semantic_index/live.json', '{"live":true}'); + vfs.writeFile('/f.txt', 'v1'); + const name = vfs.checkpoint('legacy'); + const p = '/_snapshots/checkpoint-' + name + '.json'; + const snap = vfs.fsReadJSON(p); // simulate a pre-patch 4MB snapshot + snap.root.content['_mods'] = { type: 'dir', content: { 'stale.js': { type: 'file', content: 'STALE' } } }; + snap.root.content['.semantic_index'] = { type: 'dir', content: { 'stale.json': { type: 'file', content: 'STALE' } } }; + vfs.writeFile(p, JSON.stringify(snap)); + ok('F1 rollback of a legacy snapshot succeeds', vfs.rollback(name) === true); + ok('F2 live _mods wins over the stale copy', vfs.readFile('/_mods/live.js') === 'LIVE' && vfs.readFile('/_mods/stale.js') === null); + ok('F3 live .semantic_index wins over the stale copy', + vfs.readFile('/.semantic_index/live.json') === '{"live":true}' && vfs.readFile('/.semantic_index/stale.json') === null); +} + +// ── G. plain checkpoint/restore round trip is unbroken ─────────────────────── +{ + const vfs = mk(); + vfs.mkdirRecursive('/work'); + vfs.writeFile('/work/a.txt', 'version-1'); + vfs.writeFile('/work/b.txt', 'b-1'); + const name = vfs.checkpoint('roundtrip'); + vfs.writeFile('/work/a.txt', 'version-2'); + vfs.deleteFile('/work/b.txt'); + vfs.writeFile('/work/new.txt', 'added-later'); + ok('G1 rollback returns true', vfs.rollback(name) === true); + ok('G2 modified file restored', vfs.readFile('/work/a.txt') === 'version-1', String(vfs.readFile('/work/a.txt'))); + ok('G3 deleted file restored', vfs.readFile('/work/b.txt') === 'b-1'); + ok('G4 post-checkpoint file removed by the rollback', vfs.readFile('/work/new.txt') === null); +} + +// ── H. measured saving on a realistic 2 MB _mods ──────────────────────────── +{ + const vfs = mk(); + vfs.mkdirRecursive('/_mods'); + vfs.writeFile('/_mods/bundle.js', 'x'.repeat(2 * 1024 * 1024)); // 2 MiB module originals + vfs.mkdirRecursive('/.semantic_index'); + vfs.writeFile('/.semantic_index/idx.json', JSON.stringify({ nodes: 'y'.repeat(4096) })); + vfs.writeFile('/doc.txt', 'hello'); + const name = vfs.checkpoint('big'); + const raw = vfs.readFile('/_snapshots/checkpoint-' + name + '.json'); + const after = raw.length; + // what the old code produced: the same snapshot with the two dirs cloned back in + const snap = JSON.parse(raw); + snap.root.content['_mods'] = JSON.parse(JSON.stringify(vfs.root.content['_mods'])); + snap.root.content['.semantic_index'] = JSON.parse(JSON.stringify(vfs.root.content['.semantic_index'])); + const before = JSON.stringify(snap).length; + console.log('MEASURE checkpoint bytes before exclusion: ' + before.toLocaleString('en-US')); + console.log('MEASURE checkpoint bytes after exclusion: ' + after.toLocaleString('en-US')); + console.log('MEASURE saved: ' + (before - after).toLocaleString('en-US') + ' bytes (' + (100 * (before - after) / before).toFixed(1) + '%)'); + ok('H1 excluded snapshot stays small (< 200 KB)', after < 200 * 1024, after + ' bytes'); + ok('H2 the old shape would have carried the 2 MiB _mods', before > 2 * 1024 * 1024, before + ' bytes'); +} + +if (fails === 0) { + console.log('test-virtual-fs-require-leaf: ' + passes + ' passed, 0 failed'); +} else { + console.log('test-virtual-fs-require-leaf: ' + passes + ' passed, ' + fails + ' FAILED'); + process.exit(1); +} -- cgit v1.2.3