gongxiangp2/lib/__tests__/task-image-complete-route-safety.test.ts

68 lines
3.4 KiB
TypeScript

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');
});
});