summaryrefslogtreecommitdiff
path: root/modules/test/test-virtual-fs-require-leaf.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-virtual-fs-require-leaf.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-virtual-fs-require-leaf.js')
-rw-r--r--modules/test/test-virtual-fs-require-leaf.js160
1 files changed, 160 insertions, 0 deletions
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);
+}