Files
2026-10-20 10:06:38 -04:00

45 lines
3.2 KiB
TypeScript
Executable File

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp,rm,writeFile,readFile,mkdir,access } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { DatabaseSync } from 'node:sqlite';
import { Ledger } from '../server/testnet/ledger.ts';
import { Market } from '../server/market/store.ts';
import { createBackup,verifyBackup,restoreBackup } from '../server/recovery.ts';
test('stopped-node pair backup restores journal identity and replays ledger in isolated destination',async t=>{
const root=await mkdtemp(join(tmpdir(),'wikideck-recovery-'));t.after(()=>rm(root,{recursive:true,force:true}));
const source=join(root,'source');
const pow={hash:async()=>{throw new Error('Empty ledger should require no proof work');},close(){}};
const ledger=await Ledger.open(source,pow),market=await Market.open(source,ledger);
const id=market.id,snapshot=ledger.snapshot();market.close();ledger.close();
const backup=join(root,'backup');const manifest=await createBackup(source,backup);
assert.equal(manifest.files.length,2);assert.deepEqual(await verifyBackup(backup),manifest);
const dest=join(root,'restore');assert.equal((await restoreBackup(backup,dest)).proofsRevalidated,false);
const restored=await Ledger.open(dest,pow),journal=await Market.open(dest,restored);
assert.deepEqual(restored.snapshot(),snapshot);assert.equal(journal.id,id);
journal.close();restored.close();
await assert.rejects(restoreBackup(backup,dest),/EEXIST/);
await assert.rejects(createBackup(source,backup),/EEXIST/);
await assert.rejects(createBackup(source,join(source,'inside')),/outside/);
await writeFile(join(source,'testnet-node.lock'),'simulated-running');
await assert.rejects(createBackup(source,join(root,'locked-backup')),/locked/);
assert.equal(await readFile(join(source,'testnet-node.lock'),'utf8'),'simulated-running');
});
test('backup verifier rejects tampering, sidecars, network mismatch and incomplete packages',async t=>{
const root=await mkdtemp(join(tmpdir(),'wikideck-recovery-bad-'));t.after(()=>rm(root,{recursive:true,force:true}));
const source=join(root,'source'),ledger=await Ledger.open(source,{hash:async()=>'',close(){}}),market=await Market.open(source,ledger);
market.close();ledger.close();const backup=join(root,'backup');await createBackup(source,backup);
const original=await readFile(join(backup,'manifest.json'),'utf8');
await writeFile(join(backup,'ledger.sqlite-wal'),'unexpected');await assert.rejects(verifyBackup(backup),/sidecar/);
await rm(join(backup,'ledger.sqlite-wal'));
await writeFile(join(backup,'manifest.json'),original.replace('wikideck-randomx-testnet-6','wrong-network'));await assert.rejects(verifyBackup(backup));
await writeFile(join(backup,'manifest.json'),original);
const db=new DatabaseSync(join(backup,'market.sqlite'));db.exec("UPDATE market_meta SET value='tampered' WHERE key='identity'");db.close();
await assert.rejects(verifyBackup(backup),/hash\/size/);
const missing=join(root,'incomplete');await mkdir(missing);await assert.rejects(verifyBackup(missing));
await assert.rejects(access(join(source,'testnet-node.lock'))); // maintenance lock released, not silently left behind
});