/** * Regression guard — a short video gets a real poster frame, and the keepsake never shows a broken * tile. * * Both the compression worker and the HTML export ran the same invocation: * * ffmpeg -i -vframes 1 -ss 00:00:01 -vf scale=… -y * * `-ss` AFTER `-i` is an output-side seek. Against a clip of a second or less ffmpeg exits **0 and * writes nothing**, and both call sites gated on the exit status. So: * * - the worker wrote `thumbnail_path` and logged "thumbnail generated" for a file that was never * created → `GET /upload/{id}/thumbnail` 404s in the live feed; * - the export listed `media/…_thumb.jpg` in `data.json` while the ZIP writer skipped the * unopenable file → the keepsake rendered a broken image tile. * * Any clip at or under a second, which phones produce constantly: mis-taps, Live Photos, boomerangs. * Every server-side signal stayed green throughout. * * Two fixtures on purpose, because they take different paths through the fix: * - `sample.mp4` is exactly 1.000 s. An input-side seek to 1 s is STILL past its last frame, so it * is the 0 s fallback that saves it. Moving `-ss` before `-i` alone does not fix this file. * - `sample-5s.mp4` is 5 s and succeeds on the first seek — the normal path, which no test covered * at all before, because the suite only ever had the boundary fixture. */ import { test, expect } from '../../fixtures/test'; import { uploadRaw } from '../../helpers/upload-client'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { BASE } from '../../helpers/env'; const CLIPS = [ { file: 'sample.mp4', label: '1.000s — needs the 0s fallback' }, { file: 'sample-5s.mp4', label: '5s — succeeds on the first seek' }, ]; async function uploadClip(jwt: string, file: string): Promise { const bytes = readFileSync(join(process.cwd(), 'fixtures', 'media', file)); const res = await uploadRaw(jwt, bytes, { filename: file, contentType: 'video/mp4' }); expect(res.status, `uploading ${file}`).toBe(201); return ((await res.json()) as { id: string }).id; } test.describe('Video — the poster frame is real', () => { for (const { file, label } of CLIPS) { test(`${file} (${label}) gets a fetchable poster`, async ({ guest, db }) => { test.setTimeout(60_000); const g = await guest(`Poster${file.replace(/\W/g, '')}`); const id = await uploadClip(g.jwt, file); await expect.poll(() => db.compressionStatus(id), { timeout: 45_000 }).toBe('done'); // The DB must not claim a thumbnail that isn't there — that claim IS the defect. const res = await fetch(`${BASE}/api/v1/upload/${id}/thumbnail`, { headers: { Authorization: `Bearer ${g.jwt}` }, }); expect(res.status, `${file}: the poster must exist, not just be recorded`).toBe(200); expect(res.headers.get('content-type')).toContain('image/'); expect((await res.arrayBuffer()).byteLength).toBeGreaterThan(0); }); } test('a video upload still succeeds even if no poster can be extracted', async ({ guest, db, }) => { // The mirror that keeps the fix honest. Tightening the check to "the file must exist" without // also making a missing poster non-fatal would have been far worse than the bug: the worker's // call used `?`, so every sub-second clip would fail compression, exhaust its retries and be // soft-deleted. A cosmetic defect turned into data loss. // // `compression_status = 'done'` with the upload still present is exactly that guarantee. const g = await guest('PosterSurvivor'); const id = await uploadClip(g.jwt, 'sample.mp4'); await expect.poll(() => db.compressionStatus(id), { timeout: 45_000 }).toBe('done'); expect(await db.countUploadsForUser(g.userId)).toBe(1); // And the video itself is playable regardless of the poster. const orig = await fetch(`${BASE}/api/v1/upload/${id}/original`, { headers: { Authorization: `Bearer ${g.jwt}` }, }); expect(orig.status).toBe(200); expect(orig.headers.get('content-type')).toContain('video/'); }); });