import test from 'node:test' import assert from 'node:assert/strict' import { OPERATION_ID, digest, json, planRecovery, applyRecovery, validateRecovery } from './recover-material-previews.mjs' function fixture() { const body = Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WEBP'), Buffer.alloc(128)]) const sha = digest(body) const manifest = { version: 1, reviewed: true, operationId: OPERATION_ID, previewQuality: 'temporary-160px-derived', actorId: '11111111-1111-4111-8111-111111111111', materials: Array.from({ length: 11 }, (_, i) => { const materialId = `22222222-2222-4222-8222-${String(i).padStart(12, '0')}` const destinationKey = `由壹点AI/public-display/${OPERATION_ID}/${materialId}-${sha}.webp` return { materialId, name: `material-${i}`, oldPreviewUrl: `https://legacy.example.test/${i}.webp`, sourceSha256: '0'.repeat(64), cacheSha256: sha, bodyBase64: body.toString('base64'), bytes: body.length, width: 160, height: 160, provenance: '20261019-sharp-rotate-inside160-webp80', destinationKey, newPreviewUrl: `https://s3.jiansun.vip/gongxiangpan/${destinationKey}` } }) } let records = manifest.materials.map(r => ({ id: r.materialId, productModel: r.name, materialType: '材质', deletedAt: null, previewUrl: r.oldPreviewUrl, updatedAt: '2026-01-01T00:00:00.000Z', zipFilePath: 'untouched.zip', zipFileSize: '100', assets: [{ id: 'source', fileUrl: 'original.png' }] })) let logs = [] const stored = new Map() const hooks = { puts: 0, failCas: -1, failLog: false, badPublic: false, actorActive: true } const db = { actor: async () => ({ isActive: hooks.actorActive, isSuperAdmin: true, status: hooks.actorActive ? 'active' : 'disabled', employmentStatus: 'active', archivedAt: null }), records: async () => structuredClone(records), operationCount: async () => logs.length, compareAndSet: async (row, updatedAt) => { const index = records.findIndex(r => r.id === row.materialId && r.previewUrl === row.oldPreviewUrl && r.updatedAt === updatedAt) if (index < 0 || index === hooks.failCas) return 0 records[index] = { ...records[index], previewUrl: row.newPreviewUrl, updatedAt: '2026-10-23T00:00:00.000Z' } return 1 }, log: async row => { if (hooks.failLog) throw new Error('log failure'); logs.push(row) }, transaction: async fn => { const before = structuredClone(records), previousLogs = structuredClone(logs) try { await fn(db) } catch (error) { records = before; logs = previousLogs; throw error } }, } const objects = { get: async key => stored.get(key) ?? null, putIfAbsent: async (key, bytes) => { assert.equal(stored.has(key), false); stored.set(key, bytes); hooks.puts++ }, publicGet: async url => hooks.badPublic ? Buffer.from('wrong') : stored.get(decodeURIComponent(new URL(url).pathname).slice('/gongxiangpan/'.length)) } const prepare = async () => { const plan = await planRecovery(manifest, db); return { manifest, plan, persistedPlanSha256: digest(json(plan)), db, objects } } return { manifest, db, objects, hooks, stored, prepare, records: () => records, logs: () => logs } } test('requires reviewed exact count, hashes and scoped canonical destinations', () => { const f = fixture(); assert.equal(validateRecovery(f.manifest), f.manifest) for (const mutate of [m => { m.reviewed = false }, m => m.materials.pop(), m => { m.materials[0].cacheSha256 = 'f'.repeat(64) }, m => { m.materials[0].destinationKey = 'original.webp' }, m => { m.materials[0].materialId = m.materials[1].materialId }]) { const m = structuredClone(f.manifest); mutate(m); assert.throws(() => validateRecovery(m)) } }) test('successful recovery changes previews only, preserves source fields and creates eleven audits', async () => { const f = fixture(), before = structuredClone(f.records()); const result = await applyRecovery(await f.prepare()) assert.equal(result.updated, 11); assert.equal(f.logs().length, 11); assert.equal(f.hooks.puts, 11) for (let i = 0; i < 11; i++) { assert.equal(f.records()[i].zipFilePath, before[i].zipFilePath); assert.deepEqual(f.records()[i].assets, before[i].assets); assert.match(f.logs()[i].changes, /temporary-160px-derived/) } }) test('rejects missing persisted snapshot proof and changed snapshot before any object write', async () => { const f = fixture(), args = await f.prepare(); args.persistedPlanSha256 = 'wrong' await assert.rejects(applyRecovery(args), /persisted/); assert.equal(f.hooks.puts, 0) const args2 = await f.prepare(); f.records()[0].productModel = 'concurrent edit' await assert.rejects(applyRecovery(args2), /source record changed/); assert.equal(f.hooks.puts, 0) }) test('public readback mismatch cannot change database', async () => { const f = fixture(), args = await f.prepare(); f.hooks.badPublic = true await assert.rejects(applyRecovery(args), /public object/); assert.equal(f.logs().length, 0); assert.equal(f.records()[0].previewUrl, f.manifest.materials[0].oldPreviewUrl) }) test('existing different object cannot be overwritten', async () => { const f = fixture(), args = await f.prepare(); f.stored.set(f.manifest.materials[0].destinationKey, Buffer.from('different')) await assert.rejects(applyRecovery(args), /refusing overwrite/); assert.equal(f.hooks.puts, 0); assert.equal(f.logs().length, 0) }) test('CAS conflict rolls back every row and audit, keeps named objects without deleting', async () => { const f = fixture(), before = structuredClone(f.records()), args = await f.prepare(); f.hooks.failCas = 6 await assert.rejects(applyRecovery(args), /concurrent record/); assert.deepEqual(f.records(), before); assert.equal(f.logs().length, 0); assert.equal(f.stored.size, 11) }) test('audit failure rolls back all database changes', async () => { const f = fixture(), before = structuredClone(f.records()), args = await f.prepare(); f.hooks.failLog = true await assert.rejects(applyRecovery(args), /log failure/); assert.deepEqual(f.records(), before); assert.equal(f.logs().length, 0) }) test('revoked actor blocks planning and duplicate operation never writes objects again', async () => { const f = fixture(); f.hooks.actorActive = false; await assert.rejects(f.prepare(), /actor/) f.hooks.actorActive = true; const args = await f.prepare(); await applyRecovery(args) await assert.rejects(applyRecovery(args)); assert.equal(f.hooks.puts, 11) })