// modules/test/test-memory-graph-recovery.js — run: node // MAINT-003: a corrupt memory-graph store must not look like a fresh install. The graph still // starts from what remains, but the unreadable file is kept aside and the user is told. const fsMod = require('fs'), path = require('path'); const ROOT = path.join(__dirname, '..', 'core'); const store = {}; global.localStorage = { getItem: k => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: k => { delete store[k]; } }; global.indexedDB = { open() { throw new Error('no idb'); }, deleteDatabase() { throw new Error('no idb'); } }; global.window = global; const load = (f, c) => new Function(fsMod.readFileSync(path.join(ROOT, f), 'utf8') + '\nreturn ' + c + ';')(); const VFS = load('virtual-fs.js', 'VirtualFileSystem'); const MG = load('module-memory-graph.js', 'MemoryGraphModule'); let fails = 0, passes = 0; function ok(name, cond, detail) { if (cond) { passes++; return; } fails++; console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); } function mk() { const out = []; const vfs = new VFS(); vfs.saveToStorage = () => true; const term = { print: (m, t) => out.push({ text: String(m), type: t || '' }), log: () => {}, fs: vfs }; const mg = new MG(); mg.init(term); return { mg, vfs, out }; } const text = out => out.map(o => o.text).join(' | '); // A. a healthy store survives a reload { const a = mk(); for (let i = 0; i < 4; i++) a.mg.addNode('concept', 'keep-' + i, 'keep ' + i); a.mg._saveNow(); const seeded = Object.keys(a.mg._nodes).length; const out = []; const mg2 = new MG(); mg2.init({ print: (m, t) => out.push({ text: String(m), type: t || '' }), log: () => {}, fs: a.vfs }); ok('A1 nodes survive a reload', Object.keys(mg2._nodes).length === seeded, 'seeded ' + seeded + ' reloaded ' + Object.keys(mg2._nodes).length); ok('A2 no quarantine warning on a healthy store', !/unreadable/.test(text(out)), text(out)); } // B. a corrupt store is quarantined and reported, not silently discarded { const a = mk(); for (let i = 0; i < 4; i++) a.mg.addNode('concept', 'lose-' + i, 'lose ' + i); a.mg._saveNow(); const original = a.vfs.readFile('/.memory_graph/nodes.json'); a.vfs.writeFile('/.memory_graph/nodes.json', '{"nodes":['); // truncated write a.vfs.writeFile('/.memory_graph/export.json', 'not json at all'); const out = []; const mg2 = new MG(); mg2.init({ print: (m, t) => out.push({ text: String(m), type: t || '' }), log: () => {}, fs: a.vfs }); ok('B1 the user is warned', /unreadable/.test(text(out)), text(out).slice(0, 200)); ok('B2 the corrupt file is kept aside', a.vfs.readFile('/.memory_graph/nodes.json.corrupt') === '{"nodes":[', String(a.vfs.readFile('/.memory_graph/nodes.json.corrupt'))); ok('B3 the quarantine copy held the broken text, not the old good one', original !== a.vfs.readFile('/.memory_graph/nodes.json.corrupt')); ok('B4 the graph still starts', Object.keys(mg2._nodes).length >= 1, String(Object.keys(mg2._nodes).length)); } // C. the export path is quarantined too, and the nodes.json fallback still loads { const a = mk(); a.mg.addNode('concept', 'fallback', 'fallback node'); a.mg._saveNow(); // the loader expects {nodes: [...], edges: [...]} a.vfs.fsWriteJSON('/.memory_graph/export.json', { nodes: Object.values(a.mg._nodes), edges: [] }); a.vfs.writeFile('/.memory_graph/nodes.json', 'broken'); // corrupt fallback const before = Object.keys(a.mg._nodes).length; const out = []; const mg2 = new MG(); mg2.init({ print: (m, t) => out.push({ text: String(m), type: t || '' }), log: () => {}, fs: a.vfs }); ok('C1 corrupt nodes.json is quarantined', !!a.vfs.readFile('/.memory_graph/nodes.json.corrupt')); ok('C2 the valid export still loads the graph', Object.keys(mg2._nodes).length >= before, 'before ' + before + ' after ' + Object.keys(mg2._nodes).length); } if (fails === 0) { console.log('test-memory-graph-recovery: ' + passes + ' passed, 0 failed'); } else { console.log('test-memory-graph-recovery: ' + passes + ' passed, ' + fails + ' FAILED'); process.exit(1); }