/** @jest-environment node */ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'fs/promises' import os from 'os' import path from 'path' import { createZipFromFiles } from '@/lib/task-material-processor' import { deleteStoredUploadFileFromRemote, syncStoredUploadFileToRemote } from '@/lib/remote-upload-sync' import { extractZipEntriesToDirectory, listZipEntries } from '@/lib/zip-entry-reader' import { groupPreviewRowsIntoRuns, matchRunsToZipEntries } from '@/lib/task-detail-asset-edit' jest.mock('@/lib/prisma', () => ({ prisma: {} })) jest.mock('@/lib/s3', () => ({ deleteFromS3: jest.fn() })) jest.mock('@/lib/public-display', () => ({ buildMaterialPublicDisplayPath: jest.fn(), uploadPublicDisplayToS3: jest.fn(), })) jest.mock('@/lib/image-processor', () => ({ compressImage: jest.fn() })) jest.mock('@/lib/remote-upload-sync', () => ({ deleteStoredUploadFileFromRemote: jest.fn().mockResolvedValue(undefined), syncStoredUploadFileToRemote: jest.fn(), })) jest.mock('@/lib/material-upload-merge', () => ({ acquireUploadMergeDiskReservation: jest.fn().mockResolvedValue(() => undefined), })) /** * 生产链路(archiver)并发打包不保证中央目录顺序等于传入顺序。 * 单图编辑因此不依赖位置映射,而是按"来源文件名"锚定匹配。 * 这里用真实打包产物验证:条目顺序无论怎样,名字匹配都能对上。 */ describe('生产打包链路的单图编辑前提', () => { let uploadRoot: string let previousUploadDir: string | undefined beforeEach(async () => { previousUploadDir = process.env.UPLOAD_DIR uploadRoot = await mkdtemp(path.join(os.tmpdir(), 'entry-order-')) 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 }) }) function row(id: string, fileName: string, sortOrder: number): any { return { id, assetType: 'main-preview', fileUrl: `s3://${id}`, fileName, sortOrder, skuCode: null } } test('条目内容完整解压;预览行与条目按名字正确配对(与顺序无关)', async () => { const names = ['z-last.jpg', 'a-first.jpg', '中图.png', 'm-middle.jpg'] const files: Array<{ path: string; name: string; size: number }> = [] const sourceDir = path.join(uploadRoot, 'src') await mkdir(sourceDir, { recursive: true }) for (let index = 0; index < names.length; index += 1) { const name = names[index] const filePath = path.join(sourceDir, `file-${index}.bin`) const bytes = Buffer.from(`${name}-${index}`) await writeFile(filePath, bytes) files.push({ path: filePath, name, size: bytes.length }) } const result = await createZipFromFiles( files, 'material-order-check', 'MODEL', '任务-详情', '主图', '作者', '店铺', ) const zipPath = path.join(uploadRoot, 'materials', result.fileName) // 解压:每个条目内容完整 const destination = path.join(uploadRoot, 'extracted') const extracted = await extractZipEntriesToDirectory(zipPath, destination) expect(extracted.map((item) => item.entryName).sort()).toEqual([...names].sort()) const byName = new Map(extracted.map((item) => [item.entryName, item])) expect((await readFile(byName.get('中图.png')!.filePath)).toString()).toBe('中图.png-2') // 匹配:即使中央目录顺序被打乱,预览行仍能按基础名找到自己的条目 const entries = await listZipEntries(zipPath) const entryNames = entries.map((entry) => entry.name) const runs = groupPreviewRowsIntoRuns([ row('p0', 'z-last.webp', 0), row('p1', 'a-first.webp', 1), row('p2', '中图.webp', 2), row('p3', 'm-middle.webp', 3), ])! const assigned = matchRunsToZipEntries(runs, entryNames) expect(entryNames[assigned[0]]).toBe('z-last.jpg') expect(entryNames[assigned[1]]).toBe('a-first.jpg') expect(entryNames[assigned[2]]).toBe('中图.png') expect(entryNames[assigned[3]]).toBe('m-middle.jpg') }) })