From 98920fa4677109d26ab8dbc13f3231bb81e38081 Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:26:27 +0000 Subject: [PATCH] fix(web): avoid restarting processing during thumbnail generation --- .../__tests__/unit/video-processing.test.ts | 19 ++++++++++++++++--- apps/web/lib/video-processing.ts | 6 +++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/web/__tests__/unit/video-processing.test.ts b/apps/web/__tests__/unit/video-processing.test.ts index e9ae285a415..904cea30e2f 100644 --- a/apps/web/__tests__/unit/video-processing.test.ts +++ b/apps/web/__tests__/unit/video-processing.test.ts @@ -1,3 +1,4 @@ +import { MySqlDialect } from "drizzle-orm/mysql-core"; import { beforeEach, describe, expect, it, vi } from "vitest"; const updateWhereMock = vi.fn(); @@ -36,12 +37,16 @@ describe("video processing starts", () => { vi.clearAllMocks(); }); - it("does not start a duplicate workflow when processing is already running", async () => { + it.each([ + ["processing", "already-processing"], + ["generating_thumbnail", "already-processing"], + ["complete", "already-complete"], + ])("does not start a duplicate workflow during %s", async (phase, status) => { updateWhereMock.mockResolvedValueOnce({ affectedRows: 0 }); selectWhereMock.mockResolvedValueOnce([ { videoId: "video-123", - phase: "processing", + phase, rawFileKey: "user-123/video-123/raw-upload.webm", }, ]); @@ -59,9 +64,13 @@ describe("video processing starts", () => { processingMessage: "Starting video processing...", startFailureMessage: "Video processing could not start.", }), - ).resolves.toBe("already-processing"); + ).resolves.toBe(status); expect(startMock).not.toHaveBeenCalled(); + const condition = updateWhereMock.mock.calls[0]?.[0]; + const query = new MySqlDialect().sqlToQuery(condition); + expect(query.params).toContain(phase); + expect(query.sql).toContain("`video_uploads`.`phase` <> ?"); }); it("starts the workflow after claiming the upload row", async () => { @@ -131,6 +140,10 @@ describe("video processing starts", () => { ).resolves.toBe("started"); expect(startMock).toHaveBeenCalledTimes(1); + const condition = updateWhereMock.mock.calls[0]?.[0]; + expect(new MySqlDialect().sqlToQuery(condition).params).toEqual([ + "video-123", + ]); }); it("marks the upload as errored when workflow start fails", async () => { diff --git a/apps/web/lib/video-processing.ts b/apps/web/lib/video-processing.ts index 54333399da1..1e43663f6a0 100644 --- a/apps/web/lib/video-processing.ts +++ b/apps/web/lib/video-processing.ts @@ -67,6 +67,7 @@ export async function transitionVideoToProcessing({ : and( eq(videoUploads.videoId, videoId), ne(videoUploads.phase, "processing"), + ne(videoUploads.phase, "generating_thumbnail"), // "complete" is terminal for implicit starts: a multipart // completion retried after an uncertain network outcome can // land after the first processing run already finished, and @@ -89,7 +90,10 @@ export async function transitionVideoToProcessing({ throw new Error("No upload record found"); } - if (upload.phase === "processing") { + if ( + upload.phase === "processing" || + upload.phase === "generating_thumbnail" + ) { return "already-processing"; }