import 'fake-indexeddb/auto' import { bindDatabase, WorklogDB } from '../src/db/schema' // node 环境的 localStorage 垫片(引擎的纪元/idMap 存取需要) const lsMap = new Map() ;(globalThis as any).localStorage = { getItem: (k: string) => (lsMap.has(k) ? lsMap.get(k)! : null), setItem: (k: string, v: string) => { lsMap.set(k, String(v)) }, removeItem: (k: string) => { lsMap.delete(k) } } const BASE = 'http://127.0.0.1:3780' let pass = 0 let fail = 0 function check(name: string, ok: boolean, detail?: string): void { if (ok) { pass++; console.log(' PASS ' + name) } else { fail++; console.log(' FAIL ' + name + (detail ? ' -> ' + detail : '')) } } async function main() { await fetch(BASE + '/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'restore', password: 'dev123456' }) }).catch(() => { /* 已存在忽略 */ }) const engine = await import('../src/sync/engine') const CONF = { baseUrl: BASE, username: 'restore', password: 'dev123456' } // ===== 设备 A:备忘录 x3 + 密码库密文 x1 + 设置 ===== bindDatabase(new WorklogDB('restore_a')) const a = (await import('../src/db/schema')).db const now = '2026-10-26T10:00:00' await a.memo.bulkAdd([ { title: '备忘一', content: '内容一', created_at: now, updated_at: now }, { title: '备忘二', content: '内容二', created_at: now, updated_at: now }, { title: '备忘三', content: '内容三', created_at: now, updated_at: now } ]) await a.credential.bulkAdd([ { system: '企业邮箱', account: 'dev@x.com', password: 'enc:v1:FAKECIPHER', remark: '', created_at: now, updated_at: now } ]) await a.setting.bulkAdd([ { key: 'work_start', value: '09:00' }, { key: 'pwd_salt', value: 'SALTSALT' }, { key: 'pwd_verifier', value: 'enc:v1:VERIFIER' } ]) const rA = await engine.runSync(CONF) console.log('设备A 首次推送:', JSON.stringify({ p: rA.pushed, e: rA.errors })) // ===== 灾难:服务器用户数据文件被删(模拟 rm -rf data / 目录重建)===== const fsx = await import('fs') const usersDir = 'server/data/users' for (const f of fsx.readdirSync(usersDir)) fsx.rmSync(usersDir + '/' + f) console.log('已删除服务器用户数据文件') // ===== 设备 A 再次同步:应触发二道防线,自动全量重推自救 ===== // (设备 A 此前从未存过纪元 —— 恰好覆盖「首次升级即遇到灾难」的窗口) const rA2 = await engine.runSync(CONF) console.log('设备A 自救同步:', JSON.stringify({ p: rA2.pushed, e: rA2.errors })) check('自救时自动重推了全部本地数据(pushed>=7)', rA2.pushed >= 7, 'pushed=' + rA2.pushed) const aMemo = await a.memo.toArray() const aCred = await a.credential.toArray() check('设备A 本地数据未被清空(备忘录仍 3 条)', aMemo.length === 3, 'len=' + aMemo.length) check('设备A 本地密码库未被清空(仍 1 条)', aCred.length === 1, 'len=' + aCred.length) // ===== 全新设备 C:拉取验证服务器已被 A 恢复 ===== bindDatabase(new WorklogDB('restore_c')) const c = (await import('../src/db/schema')).db const rC = await engine.runSync(CONF) console.log('设备C 拉取:', JSON.stringify({ p: rC.pushed, e: rC.errors })) const cMemo = await c.memo.toArray() const cCred = await c.credential.toArray() const cSet = await c.setting.toArray() check('设备C 拉到 3 条备忘录', cMemo.length === 3, 'len=' + cMemo.length) check('设备C 拉到密码库密文', cCred.length === 1 && cCred[0].system === '企业邮箱', 'len=' + cCred.length) check('设备C 拉到主密码盐与校验值(密码库可解锁前提)', cSet.some(s => s.key === 'pwd_salt' && s.value === 'SALTSALT') && cSet.some(s => s.key === 'pwd_verifier')) check('设备C 拉到考勤规则', cSet.some(s => s.key === 'work_start' && s.value === '09:00')) console.log('RESULT: ' + (fail === 0 ? 'ALL PASS ' + pass : 'FAIL ' + fail + '/' + (pass + fail))) if (fail > 0) process.exit(1) } main().catch(e => { console.error('SMOKE ERROR', e); process.exit(1) })