// modules/test/test-vfs-checkpoint.js — run: node modules/test/test-vfs-checkpoint.js // Proves the name a checkpoint is SAVED under is the name the UI prints and the name // `rollback` resolves. RED on v4.6.101: two independent new Date().toISOString() calls // made the printed name differ from the stored one, so `checkpoint` then // `rollback ` failed with "checkpoint not found or invalid". const fsMod = require('fs'), path = require('path'); const ROOT = path.join(__dirname, '..', 'core'); // ── guarded stubs for the browser globals the VFS touches ────────────────── 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('indexedDB not available in test'); }, deleteDatabase() { throw new Error('indexedDB not available in test'); }, }; global.window = global; function loadClass(file, clsName) { const src = fsMod.readFileSync(path.join(ROOT, file), 'utf8'); return new Function(src + '\nreturn ' + clsName + ';')(); } const VirtualFileSystem = loadClass('virtual-fs.js', 'VirtualFileSystem'); const SystemModule = loadClass('module-system.js', 'SystemModule'); function mkFs() { const fs = new VirtualFileSystem(); fs.saveToStorage = () => true; // isolate from timers / compression return fs; } let fails = 0, passes = 0; function ok(name, cond, detail) { if (cond) { passes++; return; } fails++; console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); } // A. checkpoint('') must return the resolved name, and two calls must differ { const fs = mkFs(); const a = fs.checkpoint(''); const b = fs.checkpoint(''); ok('A1 checkpoint returns a name string', typeof a === 'string' && a.length > 0, 'got ' + typeof a + ' ' + a); ok('A2 second name also a string', typeof b === 'string' && b.length > 0, 'got ' + typeof b); ok('A3 two unnamed checkpoints get distinct names', a !== b, 'both=' + a); ok('A4 stored under the returned name', !!fs.fsReadJSON('/_snapshots/checkpoint-' + a + '.json')); } // B. rollback with the returned name must succeed { const fs = mkFs(); fs.mkdirRecursive('/marker'); const name = fs.checkpoint(''); ok('B1 checkpoint-list shows the returned name', fs.listCheckpoints().includes(name), 'name=' + name); ok('B2 rollback(returned name) is true', fs.rollback(name) === true, 'name=' + name); } // C. the `checkpoint` command prints exactly the name it created { const fsv = mkFs(); const out = []; const sys = new SystemModule(); sys.terminal = { print: m => out.push(String(m)), fs: fsv }; sys.registerCommands(); out.length = 0; sys.commands['checkpoint']([]); const line = out.join('\n'); const printed = (line.match(/Checkpoint created:\s*(.+)$/m) || [])[1]; ok('C1 command prints a created name', !!printed, 'output=' + JSON.stringify(line)); const listed = fsv.listCheckpoints(); ok('C2 printed name exists on disk', !!printed && listed.includes(printed.trim()), 'printed=' + printed + ' listed=' + JSON.stringify(listed)); ok('C3 rollback of the printed name succeeds', !!printed && fsv.rollback(printed.trim()) === true); // C4: an explicit name is echoed verbatim out.length = 0; sys.commands['checkpoint'](['myname']); ok('C4 explicit name echoed verbatim', /Checkpoint created:\s*myname\s*$/.test(out.join('\n')), 'output=' + JSON.stringify(out.join('\n'))); } // D. same-millisecond calls must not overwrite each other { const fs = mkFs(); const names = []; // 4 calls: one more than the cap of 3, so every name is still stored when the next one is // minted (at 6+, prune legitimately frees a name and it may be reused). for (let i = 0; i < 4; i++) names.push(fs.checkpoint('')); ok('D1 four unnamed checkpoints are four distinct names', new Set(names).size === 4, 'names=' + JSON.stringify(names)); ok('D2 prune still caps the store at three', fs.listCheckpoints().length === 3, 'stored=' + JSON.stringify(fs.listCheckpoints())); } // E. prune keeps the NEWEST three, not the alphabetically last { const fs = mkFs(); fs.checkpoint('aaa-oldest'); fs.checkpoint(''); fs.checkpoint(''); fs.checkpoint(''); // 4th -> prune const left = fs.listCheckpoints(); ok('E1 oldest custom-named checkpoint was pruned', !left.includes('aaa-oldest'), 'left=' + JSON.stringify(left)); ok('E2 three checkpoints remain', left.length === 3, 'left=' + JSON.stringify(left)); ok('E3 rollback still works after pruning', fs.rollback(left[0]) === true); } // F. rollback must not destroy scaffolding { const fs = mkFs(); fs.mkdirRecursive('/_sessions/agent1'); fs.writeFile('/_sessions/agent1/log.json', '{"turns":3}'); fs.mkdirRecursive('/.trash'); fs.writeFile('/.trash/junk.txt', 'x'); fs.writeFile('/work.txt', 'before'); const name = fs.checkpoint('keep-me'); fs.writeFile('/work.txt', 'after'); ok('F1 rollback succeeds', fs.rollback(name) === true); ok('F2 file restored', fs.readFile('/work.txt') === 'before', 'got ' + fs.readFile('/work.txt')); ok('F3 checkpoint survived rollback', fs.listCheckpoints().includes('keep-me'), 'list=' + JSON.stringify(fs.listCheckpoints())); ok('F4 sessions survived rollback', !!fs.readDir('/_sessions')); ok('F5 trash survived rollback', !!fs.readDir('/.trash')); ok('F6 rollback is repeatable', fs.rollback('keep-me') === true); } // G. a name with a slash is refused, not silently unlistable { const fs = mkFs(); let threw = false; try { fs.checkpoint('a/b'); } catch (e) { threw = true; } ok('G1 slash in name throws', threw); ok('G2 nothing was written', fs.listCheckpoints().length === 0, 'list=' + JSON.stringify(fs.listCheckpoints())); } if (fails === 0) { console.log('test-vfs-checkpoint: ' + passes + ' passed, 0 failed'); } else { console.log('test-vfs-checkpoint: ' + passes + ' passed, ' + fails + ' FAILED'); process.exit(1); }