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-vfs-size-bytes.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-vfs-size-bytes.js')
| -rw-r--r-- | modules/test/test-vfs-size-bytes.js | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/modules/test/test-vfs-size-bytes.js b/modules/test/test-vfs-size-bytes.js new file mode 100644 index 0000000..109c58e --- /dev/null +++ b/modules/test/test-vfs-size-bytes.js @@ -0,0 +1,216 @@ +// node modules/test/test-vfs-size-bytes.js +// test-vfs-size-bytes — `vfs-size` must report REAL UTF-8 STORED BYTES, one unit, everywhere. +// +// RED on the pre-patch command (v4.6.159): it printed three different units — +// total = JSON.stringify(fs.root).length / 1MB +// per file = child.content.length (UTF-16 code units) +// system dir = JSON.stringify(readDir(dir)).length (a list of entries, not bytes) +// The total also carried every structural character of the serialisation, so a text +// tree reported ~2x its real stored size, while 'é' and '☃' were under-counted. +// +// This test pins the NUMBERS THE COMMAND PRINTS, never a helper: +// 1. unit facts on one-file trees: 'abcd' 4 B, 'é' 2 B, '☃' 3 B, 'é☃' 5 B, +// 1000 ASCII chars 1000 B, '' 0 B; +// 2. a 12-file nested tree: total == sum of the per-file rows == sum of the +// top-directory rows (each file counted once, no structure overhead); +// 3. every system-dir line == a manual sum of that dir's files; +// 4. a tree of directories with no files at all neither throws nor prints NaN. +// +// Set FS_MODULE_SRC=<path> to point the harness at another copy of module-fs.js +// (used to prove the test is RED against the pre-fix / mutated file). +const fsMod = require('fs'); +const path = require('path'); +const SRC = process.env.FS_MODULE_SRC || path.join(__dirname, '..', 'core', 'module-fs.js'); + +// ── harness ──────────────────────────────────────────────────────────────── +let pass = 0; +const fails = []; +function ok(name, cond, detail) { + if (cond) { pass++; console.log(' ok ' + name); } + else { fails.push(name + (detail ? ' — ' + detail : '')); console.log(' FAIL ' + name + (detail ? ' — ' + detail : '')); } +} +function eq(name, actual, expected) { + ok(name, actual === expected, 'got ' + JSON.stringify(actual) + ', expected ' + JSON.stringify(expected)); +} + +// ── load the real class out of the real file (no re-implementation) ──────── +const src = fsMod.readFileSync(SRC, 'utf8'); +const FileSystemModule = new Function(src + '\nreturn FileSystemModule;')(); + +// ── tree builders ────────────────────────────────────────────────────────── +function dir(children) { return { type: 'dir', content: children }; } +function file(text) { return { type: 'file', content: text }; } +const B = (s) => Buffer.byteLength(s, 'utf8'); // independent of Blob + +// ── drive the shipped command and capture what it prints ─────────────────── +function run(tree) { + const out = []; + const mod = new FileSystemModule(); + mod.terminal = { fs: { root: tree }, print: (text) => out.push(String(text)) }; + mod.registerCommands(); + mod.commands['vfs-size'](); // a throw escapes here and fails the test loudly + return out; +} + +const UNITS = { B: 1, KB: 1024, MB: 1024 * 1024 }; +function toBytes(human) { + const m = /^([\d.]+) (B|KB|MB)$/.exec(human); + if (!m) return null; + return Math.round(parseFloat(m[1]) * UNITS[m[2]]); +} +function parse(out) { + const r = { totalHuman: null, totalExact: null, files: null, rows: [], rest: null, dirs: [], sys: [] }; + for (const line of out) { + let m; + if ((m = /^ {2}Total: ([\d.]+ (?:B|KB|MB)) \((\d+) bytes\) across (\d+) files?$/.exec(line))) { + r.totalHuman = toBytes(m[1]); r.totalExact = Number(m[2]); r.files = Number(m[3]); + } else if ((m = /^ {4}\d+\. (\S+) — ([\d.]+ (?:B|KB|MB))$/.exec(line))) { + r.rows.push({ path: m[1], bytes: toBytes(m[2]) }); + } else if ((m = /^ {4}\(\+(\d+) more files? — ([\d.]+ (?:B|KB|MB))\)$/.exec(line))) { + r.rest = { count: Number(m[1]), bytes: toBytes(m[2]) }; + } else if ((m = /^ {4}(.+?) — ([\d.]+ (?:B|KB|MB)) \((\d+) files?\)$/.exec(line))) { + r.dirs.push({ name: m[1], bytes: toBytes(m[2]), files: Number(m[3]) }); + } else if ((m = /^ {2}(\/[\w.]+): (\d+) files?, ([\d.]+ (?:B|KB|MB))$/.exec(line))) { + r.sys.push({ name: m[1], files: Number(m[2]), bytes: toBytes(m[3]) }); + } + } + return r; +} +const sum = (a) => a.reduce((x, y) => x + y, 0); + +console.log('module: ' + SRC); +console.log('node ' + process.version + ', Blob is a global: ' + (typeof Blob === 'function')); + +// ── 1. unit facts, one file per tree (always row 1, never truncated) ─────── +console.log('\n── 1. one-file trees: bytes actually counted ──'); +const CASES = [ + ['abcd', 'abcd', 4, 'a file containing "abcd" counts 4 bytes'], + ['é', 'é', 2, '"é" counts 2 bytes (1 UTF-16 unit, 2 UTF-8 bytes)'], + ['☃', '☃', 3, '"☃" counts 3 bytes'], + ['é☃', 'é☃', 5, '"é☃" counts 5 bytes'], + ['x'.repeat(1000), '1000 ASCII chars', 1000, 'a 1000-char ASCII file counts 1000 bytes'], + ['', '(empty)', 0, 'an empty file counts 0 bytes'], +]; +for (const [content, label, want, name] of CASES) { + const p = parse(run(dir({ 'f.txt': file(content) }))); + console.log(' ' + String(label).slice(0, 24).padEnd(26) + String(p.totalExact).padStart(6) + ' B row1=' + (p.rows[0] ? p.rows[0].bytes : 'MISSING')); + eq(name, p.totalExact, want); + eq(' row 1 for ' + label + ' is the same number', p.rows[0] && p.rows[0].bytes, want); + eq(' file count for ' + label, p.files, 1); +} + +// ── 2. a 12-file nested tree ────────────────────────────────────────────── +const tree = dir({ + 'a.txt': file('abcd'), // 4 (root-level file -> own group row) + 'dirA': dir({ + 'e.txt': file('é'), // 2 + 'sub': dir({ 'c.txt': file('☃') }), // 3 (three levels deep) + }), + 'dirB': dir({ + 'f.txt': file('x'.repeat(1000)), // 1000 + 'deep': dir({ 'g.txt': file('é☃') }), // 5 + }), + 'dirC': dir({ 'onlySub': dir({ 'deeper': dir({}) }) }), // 0 (no files at any depth) + '_snapshots': dir({ 'cp1.json': file('{"a":1}'), 'cp2.json': file('{"b":"é"}') }), // 7 + 11 + '.trash': dir({ 'old.txt': file('☃☃') }), // 6 + '_mods': dir({ 'calc.js': file('é'), 'nested': dir({ 'x.js': file('ab☃') }) }), // 2 + 5 + '.semantic_index': dir({ 'index.json': file('[]') }), // 2 + 'empty.txt': file(''), // 0 + // '_sessions' deliberately absent -> line skipped, must not throw +}); +const EXPECT = B('abcd') + B('é') + B('☃') + B('x'.repeat(1000)) + B('é☃') + B('') + + B('{"a":1}') + B('{"b":"é"}') + B('☃☃') + B('é') + B('ab☃') + B('[]'); +const EXPECT_FILES = 12; + +let out, p; +try { out = run(tree); p = parse(out); } +catch (e) { console.log(' FAIL vfs-size threw on the nested tree: ' + e.message); process.exit(1); } +const text = out.join('\n'); +const rowSum = sum(p.rows.map(r => r.bytes)) + (p.rest ? p.rest.bytes : 0); +const dirSum = sum(p.dirs.map(d => d.bytes)); + +console.log('\n── 2. nested tree totals ──'); +console.log(' printed total (exact) ' + p.totalExact + ' B'); +console.log(' printed total (human) ' + p.totalHuman + ' B (1-decimal, rounded from the exact field)'); +console.log(' per-file rows (' + p.rows.length + ') + remainder ' + (p.rest ? p.rest.bytes : 0) + ' = ' + rowSum); +console.log(' top-dir rows ' + dirSum); +console.log(' independent expectation ' + EXPECT + ' B over ' + EXPECT_FILES + ' files'); +console.log(' old metric JSON.stringify(root).length = ' + JSON.stringify(tree).length + ' (would have been printed as MB)'); +eq('file count', p.files, EXPECT_FILES); +eq('printed total is the UTF-8 byte sum', p.totalExact, EXPECT); +ok('human total is the same number (1-decimal KB tolerance)', + Math.abs(p.totalHuman - p.totalExact) <= 51, 'human=' + p.totalHuman + ' exact=' + p.totalExact); +eq('per-file rows + remainder sum to the printed total', rowSum, p.totalExact); +eq('top-directory rows sum to the printed total', dirSum, p.totalExact); +ok('printed total is NOT the old JSON.stringify(root).length', + p.totalExact !== JSON.stringify(tree).length, + 'old metric = ' + JSON.stringify(tree).length); + +console.log('\n── 2b. per-file table ──'); +for (const r of p.rows) console.log(' ' + r.path.padEnd(22) + String(r.bytes).padStart(6) + ' B'); +if (p.rest) console.log(' (+' + p.rest.count + ' more files — ' + p.rest.bytes + ' B)'); +const rowFor = (path) => (p.rows.find(r => r.path === path) || {}).bytes; +eq('largest file wins', rowFor('/dirB/f.txt'), 1000); +eq('nested file is listed once at its own path', rowFor('/dirA/sub/c.txt'), 3); +eq('truncated tail is one row, not silently dropped', p.rest && p.rest.count, EXPECT_FILES - p.rows.length); + +console.log('\n── 2c. top directories ──'); +for (const d of p.dirs) console.log(' ' + d.name.padEnd(22) + String(d.bytes).padStart(6) + ' B ' + d.files + ' files'); +const dirFor = (n) => p.dirs.find(d => d.name === n) || {}; +eq('/dirA counts its subdir once (2 + 3)', dirFor('/dirA').bytes, 5); +eq('/dirB counts deep/g.txt once (1000 + 5)', dirFor('/dirB').bytes, 1005); +eq('/dirB file count is 2, not 3', dirFor('/dirB').files, 2); +eq('root-level files get their own row', dirFor('/(root files)').bytes, 4); +eq('directory with only subdirs is 0 bytes', dirFor('/dirC').bytes, 0); +eq('every top-level dir appears (7 dirs + the root-files row)', p.dirs.length, 8); + +// ── 3. system dirs: real bytes, equal to a manual sum ───────────────────── +console.log('\n── 3. system dirs ──'); +for (const s of p.sys) console.log(' ' + s.name.padEnd(22) + String(s.bytes).padStart(6) + ' B ' + s.files + ' files'); +const sysFor = (n) => p.sys.find(s => s.name === n) || {}; +const SYS_EXPECT = { + '/_snapshots': { files: 2, bytes: B('{"a":1}') + B('{"b":"é"}') }, + '/.trash': { files: 1, bytes: B('☃☃') }, + '/_mods': { files: 2, bytes: B('é') + B('ab☃') }, + '/.semantic_index': { files: 1, bytes: B('[]') }, +}; +for (const [name, want] of Object.entries(SYS_EXPECT)) { + eq(name + ' bytes == manual sum', sysFor(name).bytes, want.bytes); + eq(name + ' file count (recursive)', sysFor(name).files, want.files); +} +ok('missing /_sessions prints no line instead of throwing', !sysFor('/_sessions').name); +eq('exactly the four present system dirs printed', p.sys.length, 4); +ok('_mods and .semantic_index are covered (the two dirs that dominate a checkpoint)', + !!sysFor('/_mods').name && !!sysFor('/.semantic_index').name); + +// ── 4. a tree of directories with no files ──────────────────────────────── +console.log('\n── 4. directories only, no files ──'); +let out2; +try { + out2 = run(dir({ 'a': dir({ 'b': dir({ 'c': dir({}) }) }), 'd': dir({}) })); + ok('does not throw', true); +} catch (e) { + ok('does not throw', false, e.message); + out2 = []; +} +const text2 = out2.join('\n'); +const p2 = parse(out2); +eq('total is 0 bytes', p2.totalExact, 0); +eq('file count is 0', p2.files, 0); +ok('no "Largest:" block when there are no files', !/Largest:/.test(text2)); +eq('dir rows still sum to the total', sum(p2.dirs.map(d => d.bytes)), 0); + +// ── 5. no undefined / NaN anywhere ──────────────────────────────────────── +ok('main output has no "undefined"', !/undefined/.test(text)); +ok('main output has no "NaN"', !/NaN/.test(text)); +ok('dirs-only output has no "undefined"', !/undefined/.test(text2)); +ok('dirs-only output has no "NaN"', !/NaN/.test(text2)); + +// ── verdict ─────────────────────────────────────────────────────────────── +console.log('\n' + (pass + fails.length) + ' assertions, ' + pass + ' passed, ' + fails.length + ' failed'); +if (fails.length) { + fails.forEach(f => console.log(' FAILED: ' + f)); + console.log('RESULT: FAIL'); + process.exit(1); +} +console.log('RESULT: PASS — vfs-size reports UTF-8 stored bytes in one unit everywhere'); |