1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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');
|