68 lines
3.4 KiB
TypeScript
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');
|
|
});
|
|
});
|