Files
miaojingAI/scripts/test-video-object-storage-actions.mjs

170 lines
8.1 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
const repoRoot = path.resolve(import.meta.dirname, '..');
function read(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
}
async function runTest(name, fn) {
try {
await fn();
console.log(`PASS ${name}`);
} catch (error) {
console.error(`FAIL ${name}`);
console.error(error);
process.exitCode = 1;
}
}
await runTest('video generation persists generated videos as object-backed media under generated/videos', () => {
const source = read('src/app/api/generate/video/route.ts');
assert.match(source, /uploadFileObjectOnly\(/);
assert.match(source, /fileName:\s*`\$\{prefix\}\/\$\{suffix\}\.\$\{ext \|\| 'mp4'\}`/);
assert.doesNotMatch(source, /uploadFromUrl\(\{\s*url,\s*timeout:\s*60000\s*\}\)/);
});
await runTest('download route can redirect object-backed local-storage downloads without buffering full videos', () => {
const source = read('src/app/api/download/route.ts');
assert.match(source, /objectFileExistsAsync\(key\)/);
assert.match(source, /generateObjectReadUrl\(key,\s*300,/);
assert.match(source, /NextResponse\.redirect\(objectUrl,\s*302\)/);
});
await runTest('video result download buttons trigger a streaming browser download instead of fetching a blob first', () => {
const utilsSource = read('src/lib/utils.ts');
const textVideoSource = read('src/components/create/text-to-video.tsx');
const imageVideoSource = read('src/components/create/image-to-video.tsx');
assert.match(utilsSource, /export function triggerDownloadFile\(/);
assert.match(utilsSource, /link\.href = proxyUrl/);
assert.doesNotMatch(utilsSource, /triggerDownloadFile[\s\S]*?response\.blob\(\)/);
assert.match(textVideoSource, /triggerDownloadFile\(url,/);
assert.match(imageVideoSource, /triggerDownloadFile\(url,/);
});
await runTest('gallery publish reuses object-backed video URLs instead of synchronously copying large videos', () => {
const routeSource = read('src/app/api/gallery/publish/route.ts');
const source = read('src/lib/gallery-publish-media.ts');
assert.match(routeSource, /resolveGalleryPublishMedia\(\{/);
assert.match(source, /if \(input\.type === 'video'\) \{/);
assert.match(source, /if \(!isStableLocalStorageUrl\(input\.resultUrl\)\) \{[\s\S]*?copyPublicUrlToFolder\(input\.resultUrl,\s*'gallery\/videos',\s*\{\s*storageTarget:\s*'object'\s*\}/);
assert.match(source, /let galleryResultUrl = input\.resultUrl/);
});
await runTest('gallery publish prefers real video frame thumbnails over stale client SVG thumbnails', () => {
const source = read('src/lib/gallery-publish-media.ts');
const videoThumbnailIndex = source.indexOf("type === 'video'");
const ensureIndex = source.indexOf('ensureLocalVideoThumbnail(');
const copyProvidedIndex = source.indexOf("copyPublicUrlToFolder(input.thumbnailUrl, 'gallery/thumbnails'");
assert.notEqual(ensureIndex, -1);
assert.notEqual(copyProvidedIndex, -1);
assert.ok(videoThumbnailIndex < ensureIndex);
assert.ok(ensureIndex < copyProvidedIndex);
assert.match(source, /thumbnailUrl: generatedVideoThumbnailUrl \|\| copiedVideoThumbnailUrl \|\| galleryThumbnailUrl/);
});
await runTest('share to gallery surfaces server publish failures before marking a work as published', () => {
const source = read('src/lib/creation-history-store.ts');
assert.match(source, /if \(!res\.ok\) \{/);
assert.match(source, /throw new Error\(typeof data\.error === 'string' \? data\.error : '分享失败,请重试'\)/);
assert.doesNotMatch(source, /catch \{\s*\/\/ Non-critical/);
const fetchIndex = source.indexOf("fetch('/api/gallery/publish'");
const markIndex = source.indexOf('markRecordAsPublished(options.url)');
assert.notEqual(fetchIndex, -1);
assert.notEqual(markIndex, -1);
assert.ok(fetchIndex < markIndex);
});
await runTest('share buttons wait for confirmed server publish and ignore stale local published flags', () => {
const storeSource = read('src/lib/creation-history-store.ts');
const detailSource = read('src/components/creation-detail-dialog.tsx');
const createSources = [
read('src/components/create/text-to-image.tsx'),
read('src/components/create/image-to-image.tsx'),
read('src/components/create/text-to-video.tsx'),
read('src/components/create/image-to-video.tsx'),
];
assert.match(storeSource, /publishedAt\?: string/);
assert.match(storeSource, /r\.url === url && r\.published && r\.publishedAt/);
assert.doesNotMatch(detailSource, /record\.published \|\| isUrlPublished\(record\.url\)/);
for (const source of createSources) {
assert.match(source, /const handleShareToGallery = useCallback\(async \(url: string\) => \{/);
assert.match(source, /await shareToGallery\(\{/);
assert.match(source, /catch \(error\) \{/);
}
});
await runTest('gallery video cards and detail use thumbnails until the user starts playback', () => {
const source = read('src/app/gallery/page.tsx');
assert.match(source, /isVideoWork\(work\)/);
assert.match(source, /const mediaPreviewUrl = work\.thumbnailUrl \|\| \(isVideoWork\(work\) \? getVideoFallbackThumbnail\(work\) : ''\)/);
assert.match(source, /isVideoWork\(selectedWork\)/);
assert.match(source, /activeVideoWorkId !== selectedWork\.id/);
assert.match(source, /setActiveVideoWorkId\(selectedWork\.id\)/);
assert.match(source, /下载\{isVideoWork\(selectedWork\) \? '视频' : '图片'\}/);
});
await runTest('video thumbnails extract a real video frame before falling back to SVG', () => {
const source = read('src/lib/media-storage.ts');
assert.match(source, /ffmpeg-static/);
assert.match(source, /extractVideoFrameThumbnail\(/);
assert.match(source, /VIDEO_FRAME_THUMBNAIL_PROFILE/);
assert.match(source, /contentType:\s*'image\/webp'/);
assert.match(source, /VIDEO_FALLBACK_THUMBNAIL_PROFILE/);
assert.doesNotMatch(source, /const VIDEO_THUMBNAIL_PROFILE = 'video-svg-v1'/);
});
await runTest('object-backed video thumbnails stream to a temporary local file before ffmpeg extraction', () => {
const source = read('src/lib/media-storage.ts');
const resolveStart = source.indexOf('async function resolveVideoThumbnailInput(');
const resolveEnd = source.indexOf('async function fetchTemporaryVideoInput(', resolveStart);
const resolveSource = source.slice(resolveStart, resolveEnd);
assert.notEqual(resolveStart, -1);
assert.notEqual(resolveEnd, -1);
assert.match(resolveSource, /writeStoredTemporaryVideoInput\(existingKey,\s*sourceKey\)/);
assert.match(resolveSource, /generateObjectReadUrl\(existingKey,\s*300\)/);
assert.match(resolveSource, /fetchTemporaryVideoInput\(objectReadUrl,\s*sourceKey\)/);
assert.doesNotMatch(resolveSource, /fileExistsAsync\(existingKey\)[\s\S]*?openFileStreamAsync\(existingKey\)/);
assert.match(source, /const VIDEO_THUMBNAIL_INPUT_ATTEMPTS/);
assert.match(source, /openFileStreamAsync\(existingKey\)/);
assert.match(source, /writeTemporaryVideoInputFromStream\(storedFile\.body/);
assert.match(source, /VIDEO_THUMBNAIL_MAX_INPUT_BYTES/);
assert.doesNotMatch(source, /return \{ input: objectReadUrl \}/);
});
await runTest('ffmpeg path resolution falls back to the runtime cwd when bundled route context is synthetic', () => {
const source = read('src/lib/media-storage.ts');
assert.match(source, /existsSync\(/);
assert.match(source, /createRequire\(path\.join\(process\.cwd\(\), 'package\.json'\)\)/);
assert.match(source, /getExistingFfmpegPath\(cwdRequire\('ffmpeg-static'\)\)/);
assert.doesNotMatch(source, /return typeof binaryPath === 'string' && binaryPath \? binaryPath : null/);
});
await runTest('creation history de-duplicates repeated video records by URL', () => {
const storeSource = read('src/lib/creation-history-store.ts');
const routeSource = read('src/app/api/creation-history/route.ts');
assert.match(storeSource, /function dedupeCreationRecordsByUrl\(/);
assert.match(storeSource, /dedupeCreationRecordsByUrl\(records\.slice\(0, MAX_RECORDS\)\)/);
assert.match(routeSource, /function dedupeRowsByResultUrl\(/);
assert.match(routeSource, /dedupeRowsByResultUrl\(result\.rows\)/);
});
if (process.exitCode) process.exit(process.exitCode);