import fs from 'node:fs'; import path from 'node:path'; const routeCases = [ ['3d-original', '任务-3D原图', '3d-original'], ['ai-original', '任务-AI原图', 'ai-original'], ['real-photo', '任务-实拍原图', 'real-photo'], ['video-screenshot', '任务-视频截图', 'video-screenshot'], ['task-page', '任务-页面', 'task-page'], ] as const; function read(relativePath: string): string { return fs.readFileSync(path.resolve(process.cwd(), relativePath), 'utf8').replace(/\s+/g, ' '); } describe.each(routeCases)('%s upload route safety', (routeName, materialType, fallbackProductModel) => { test('init persists uploading and complete delegates to the shared fail-closed state machine', () => { const initSource = read(`app/api/materials/upload/${routeName}/init/route.ts`); const completeSource = read(`app/api/materials/upload/${routeName}/complete/route.ts`); expect(initSource).toContain(`materialType: '${materialType}'`); expect(initSource).toContain("taskStatus: 'uploading'"); expect(initSource).toContain('INCOMPLETE_SHELL_CREATE_DATA'); expect(completeSource).toContain('completeTaskImageMaterial({'); expect(completeSource).toContain(`materialType: '${materialType}'`); expect(completeSource).toContain(`fallbackProductModel: '${fallbackProductModel}'`); expect(completeSource).not.toContain("taskStatus: 'processing'"); expect(completeSource).not.toContain('cleanupOnError('); }); }); describe('shared task image completion safety', () => { const completionSource = read('lib/task-image-material-completion.ts'); const processorSource = read('lib/task-material-processor.ts'); const finalizerSource = read('lib/product-package.ts'); test('holds a per-material cross-process lock while database stays retryable', () => { expect(completionSource).toContain('acquireTaskMaterialCompletionLock(materialId)'); expect(completionSource).toContain('releaseCompletionLock.assertHeld()'); expect(completionSource).not.toContain("data: { taskStatus: 'processing' }"); expect(completionSource).toContain("material.taskStatus !== 'uploading'"); }); test('uses real lstat limits and a long low-compression ZIP policy', () => { expect(processorSource).toContain('resolveOwnedTaskMaterialSourceFiles'); expect(processorSource).toContain('fileStats.isSymbolicLink()'); expect(processorSource).toContain('TASK_DETAIL_UPLOAD_LIMITS.maxSingleImageBytes'); expect(processorSource).toContain('TASK_DETAIL_UPLOAD_LIMITS.maxTotalSourceBytes'); expect(completionSource).toContain('TASK_MATERIAL_ZIP_TIMEOUT_MS'); expect(completionSource).toContain('TASK_MATERIAL_ZIP_COMPRESSION_LEVEL'); }); test('atomically writes assets, task fields, and completed status', () => { expect(completionSource).toContain('saveTaskMaterialAssetsAndComplete({'); expect(processorSource).toContain('finalizeTaskMaterialOnComplete({'); expect(finalizerSource).toContain('await applyCompletion?.(tx)'); expect(finalizerSource).toContain('taskStatus: TASK_STATUS.COMPLETED'); }); test('failure cleanup only removes this attempt derived files and preserves sources', () => { expect(completionSource).toMatch( /cleanupOnError\( uploadedPreviewUrls, zipFilePath, \[\], null, \[\], false,/, ); expect(completionSource).toContain('服务器已保留上传文件'); expect(processorSource).toContain('onPreviewUploaded'); }); });