From d67a6275a4f5bdf8e2a64f78b71f422baaabb84a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 14:18:06 +0000 Subject: [PATCH] test(web): cover group uploads in the main conversation The image E2E now runs in the main conversation, which is a group thread, so the browser test covers group uploads and reads from the artifact route as well as bot threads. Bot file attachments keep their run's operation id for tracing, and the non-streaming attach cap is the inline cap directly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u --- apps/web/e2e/media-pipeline.spec.ts | 9 ++++++--- packages/adapters/src/artifact-files.ts | 4 +++- packages/adapters/src/thread-artifacts.ts | 3 ++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/web/e2e/media-pipeline.spec.ts b/apps/web/e2e/media-pipeline.spec.ts index dc712cc..eb63fe8 100644 --- a/apps/web/e2e/media-pipeline.spec.ts +++ b/apps/web/e2e/media-pipeline.spec.ts @@ -54,10 +54,13 @@ test("an uploaded video streams from the artifact route and plays inline", async await captureScreenshot(page, testInfo, "media-video-block"); }); -test("the image lightbox downloads the original from the artifact route", async ({ page }) => { +test("an image in the main conversation opens and downloads from the artifact route", async ({ + page, +}) => { const stamp = Date.now(); await signup(page, `media-image-${stamp}@rakazo.test`, "password12", "Media Image"); - await completeOnboarding(page); + // The main conversation is a group thread, so this covers group uploads and reads. + await completeOnboarding(page, undefined, { mainConversation: true }); await page .locator('input[type="file"]') @@ -65,7 +68,7 @@ test("the image lightbox downloads the original from the artifact route", async await page.getByRole("combobox", { name: /Message/ }).fill("a pixel"); await page.keyboard.press("Enter"); - const thumbnail = page.getByRole("img", { name: "pixel.png" }); + const thumbnail = page.getByTestId("transcript").getByRole("img", { name: "pixel.png" }); await expect(thumbnail).toBeVisible({ timeout: 30_000 }); await expect(thumbnail).toHaveAttribute("src", /^\/api\/artifacts\/[^/?]+$/); await thumbnail.click(); diff --git a/packages/adapters/src/artifact-files.ts b/packages/adapters/src/artifact-files.ts index ad88094..b228bf1 100644 --- a/packages/adapters/src/artifact-files.ts +++ b/packages/adapters/src/artifact-files.ts @@ -47,6 +47,7 @@ export async function storeArtifactStream( stream: ArtifactStreamPut["stream"]; maxBytes?: number; signal?: AbortSignal; + operationId?: string; }, ) { validateAttachmentMimeType(input.mimeType); @@ -54,7 +55,8 @@ export async function storeArtifactStream( input.maxBytes ?? Number.POSITIVE_INFINITY, artifactMaxBytes(input.mimeType), ); - const operationId = `artifact-store:${input.runId ?? input.botId ?? input.userId}`; + const operationId = + input.operationId ?? `artifact-store:${input.runId ?? input.botId ?? input.userId}`; const context: AdapterContext = { operationId, traceId: operationId, diff --git a/packages/adapters/src/thread-artifacts.ts b/packages/adapters/src/thread-artifacts.ts index 8d82aef..f6630bd 100644 --- a/packages/adapters/src/thread-artifacts.ts +++ b/packages/adapters/src/thread-artifacts.ts @@ -37,7 +37,7 @@ const ATTACHMENT_BLOCK_KINDS: ReadonlySet = new Set(["image", "video", " export function attachableFileMaxBytes(artifacts: ArtifactStore, mimeType: string): number { return artifacts.describe().capabilities.stream ? artifactMaxBytes(mimeType) - : Math.min(ATTACHMENT_MAX_BYTES, artifactMaxBytes(mimeType)); + : ATTACHMENT_MAX_BYTES; } export function formatMiB(bytes: number): string { @@ -82,6 +82,7 @@ export async function attachWorkspaceFileToThread( yield bytes; })(), maxBytes, + operationId: input.operationId, }); return { artifactId: row.id, block: messageBlockForArtifact(row) }; }