From cc76627982381028b4cfb636f7a8e8c55469ccfa Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:38:52 +0000 Subject: [PATCH] fix(web): process raw uploads completed with legacy file keys --- .../__tests__/unit/multipart-presign.test.ts | 44 ++++++++++++++++++- .../unit/multipart-upload-utils.test.ts | 9 +++- .../api/upload/[...route]/multipart-utils.ts | 2 +- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/apps/web/__tests__/unit/multipart-presign.test.ts b/apps/web/__tests__/unit/multipart-presign.test.ts index 0e4b0625680..c5ba24ad4bf 100644 --- a/apps/web/__tests__/unit/multipart-presign.test.ts +++ b/apps/web/__tests__/unit/multipart-presign.test.ts @@ -661,8 +661,14 @@ describe("raw recording completion integrity", () => { ]), ), ); - mocks.database.mockReturnValue({ + const client = { update: () => ({ set: () => ({ where: async () => {} }) }), + delete: () => ({ where: vi.fn(async () => {}) }), + }; + mocks.database.mockReturnValue({ + ...client, + transaction: async (run: (tx: typeof client) => Promise) => + run(client), }); mocks.complete.mockReturnValue(Effect.succeed({ ETag: etag })); mocks.head.mockReturnValue( @@ -674,6 +680,9 @@ describe("raw recording completion integrity", () => { provider: "s3", multipart: { complete: mocks.complete }, headObject: mocks.head, + copyObject: vi.fn(() => + Effect.succeed({ CopyObjectResult: { ETag: etag } }), + ), }, ]), ); @@ -689,6 +698,39 @@ describe("raw recording completion integrity", () => { expect(mocks.head).toHaveBeenCalledWith("owner/video/raw-upload.webm"); }); + const completeLegacy = (submittedParts = parts) => + app.request("/complete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + fileKey: "owner/video/raw-upload.webm", + uploadId: "same-upload", + durationInSecs: 1, + parts: submittedParts, + }), + }); + + it("starts processing raw recordings completed with a legacy file key", async () => { + const response = await completeLegacy(); + + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + success: true, + processingStarted: true, + }); + const { startVideoProcessingWorkflow } = await import( + "@/lib/video-processing" + ); + expect(startVideoProcessingWorkflow).toHaveBeenCalledWith( + expect.objectContaining({ rawFileKey: "owner/video/raw-upload.webm" }), + ); + }); + + it("rejects incomplete legacy raw uploads before completing storage", async () => { + expect((await completeLegacy([secondPart])).status).toBe(400); + expect(mocks.complete).not.toHaveBeenCalled(); + }); + it("reconciles a lost completion response using all part identities and object size", async () => { mocks.complete.mockReturnValue(Effect.fail(new Error("NoSuchUpload"))); const response = await complete([...parts].reverse()); diff --git a/apps/web/__tests__/unit/multipart-upload-utils.test.ts b/apps/web/__tests__/unit/multipart-upload-utils.test.ts index 5a1f8068155..c0feeeb3bed 100644 --- a/apps/web/__tests__/unit/multipart-upload-utils.test.ts +++ b/apps/web/__tests__/unit/multipart-upload-utils.test.ts @@ -36,9 +36,16 @@ describe("multipart upload utils", () => { getSubpath({ fileKey: "legacy-owner/video-456/raw-upload.webm", }), - ).toBeUndefined(); + ).toBe("raw-upload.webm"); }); + it.each(["result.mp4", "raw-upload.mp4", "segments/video/0.m4s"])( + "preserves the legacy subpath %s", + (subpath) => { + expect(getSubpath({ fileKey: `owner/video/${subpath}` })).toBe(subpath); + }, + ); + it("detects raw recorder uploads", () => { expect(isRawRecorderUpload("raw-upload.webm")).toBe(true); expect(isRawRecorderUpload("raw-upload.mp4")).toBe(true); diff --git a/apps/web/app/api/upload/[...route]/multipart-utils.ts b/apps/web/app/api/upload/[...route]/multipart-utils.ts index 552ec3bbe0b..45dfcaa5755 100644 --- a/apps/web/app/api/upload/[...route]/multipart-utils.ts +++ b/apps/web/app/api/upload/[...route]/multipart-utils.ts @@ -10,7 +10,7 @@ function assertWritableFileKey(key: string) { export const getSubpath = (input: { subpath?: string; fileKey?: string }) => { if ("fileKey" in input) { - return undefined; + return input.fileKey?.split("/").slice(2).join("/"); } return input.subpath ?? "result.mp4";