/** @jest-environment node */ import { mkdtemp, mkdir, readFile, rename, rm, writeFile } from 'fs/promises' import os from 'os' import path from 'path' import { recoverMaterialUploadConsumptionJournal, removeMaterialUploadConsumptionJournal, writeMaterialUploadConsumptionJournal, } from '@/lib/material-upload-consumption-journal' describe('普通素材上传消费 journal', () => { let uploadRoot = '' const previousUploadDir = process.env.UPLOAD_DIR beforeEach(async () => { uploadRoot = await mkdtemp(path.join(os.tmpdir(), 'material-upload-journal-')) process.env.UPLOAD_DIR = uploadRoot }) afterEach(async () => { if (previousUploadDir === undefined) { delete process.env.UPLOAD_DIR } else { process.env.UPLOAD_DIR = previousUploadDir } await rm(uploadRoot, { recursive: true, force: true }) }) test('rename 后模拟进程退出,重启读取 journal 能找回同一正式源', async () => { const uploadId = 'upload-crash-recovery' const sessionDirectory = path.join(uploadRoot, 'temp', uploadId) const mergedPath = path.join(sessionDirectory, 'merged-source.mp4') const finalPath = path.join(uploadRoot, 'video-crash-recovery.mp4') await mkdir(sessionDirectory, { recursive: true }) await writeFile(mergedPath, Buffer.from('video-source')) const record = await writeMaterialUploadConsumptionJournal({ uploadId, ownerId: 'user-1', mergedPath, finalPath, storedFinalPath: finalPath, fileName: 'video-crash-recovery.mp4', size: Buffer.byteLength('video-source'), }) // 故障注入点:rename 已成功,但还没有任何 Material 数据库记录。 await rename(mergedPath, finalPath) const recovered = await recoverMaterialUploadConsumptionJournal({ uploadId, ownerId: 'user-1', }) expect(recovered).toEqual({ record, sourcePath: finalPath, sourceLocation: 'final', }) expect(await readFile(recovered!.sourcePath, 'utf8')).toBe('video-source') }) test('owner 不匹配时不能发现或消费其他用户的恢复记录', async () => { const uploadId = 'upload-owner-scope' const sessionDirectory = path.join(uploadRoot, 'temp', uploadId) const mergedPath = path.join(sessionDirectory, 'merged-source.zip') const finalPath = path.join(uploadRoot, 'material-owner-scope.zip') await mkdir(sessionDirectory, { recursive: true }) await writeFile(mergedPath, Buffer.from('archive')) await writeMaterialUploadConsumptionJournal({ uploadId, ownerId: 'user-1', mergedPath, finalPath, storedFinalPath: finalPath, fileName: 'material-owner-scope.zip', size: Buffer.byteLength('archive'), }) await expect(recoverMaterialUploadConsumptionJournal({ uploadId, ownerId: 'user-2', })).resolves.toBeNull() }) test('正式源大小变化时拒绝恢复并保留 journal 供人工核验', async () => { const uploadId = 'upload-size-mismatch' const sessionDirectory = path.join(uploadRoot, 'temp', uploadId) const mergedPath = path.join(sessionDirectory, 'merged-source.zip') const finalPath = path.join(uploadRoot, 'material-size-mismatch.zip') await mkdir(sessionDirectory, { recursive: true }) await writeFile(mergedPath, Buffer.from('archive')) await writeMaterialUploadConsumptionJournal({ uploadId, ownerId: 'user-1', mergedPath, finalPath, storedFinalPath: finalPath, fileName: 'material-size-mismatch.zip', size: Buffer.byteLength('archive'), }) await rename(mergedPath, finalPath) await writeFile(finalPath, Buffer.from('changed-size')) await expect(recoverMaterialUploadConsumptionJournal({ uploadId, ownerId: 'user-1', })).rejects.toThrow('上传源文件恢复记录异常') await expect(removeMaterialUploadConsumptionJournal({ uploadId, ownerId: 'user-1', expectedFinalPath: finalPath, })).resolves.toBe(true) }) })