🧪 test: split mock endpoints by upload UX + cover unified provider routing

The dev rebase pulled in #13550's chat.spec.ts test, which drives the legacy
3-way dropdown (Attach File Options -> Upload to Provider). This PR makes unified
mode the default, so that dropdown is gone -> the test timed out on the rebased
branch.

Fix by configuring the two mock endpoints for the two UXs (one harness, dual
coverage):
- Mock Provider A: legacyFileUploadUX true -> legacy dropdown + provider csv,
  so #13550's test passes unchanged.
- Mock Provider B: unified, per-mime routing (csv/text/xlsx -> none,
  markdown -> provider).

unified-upload.spec.ts runs on Mock Provider B and now asserts BOTH:
1. a none-routed csv persists llmDeliveryPath 'none' (kept out of LLM delivery);
2. a provider-routed markdown is STILL delivered to the model AND shown as a
   chat attachment chip — unified mode does not lose upload-to-provider.
This commit is contained in:
Danny Avila 2026-06-06 13:04:53 -04:00
parent e2f09fbd49
commit f15ae1ab5b
2 changed files with 91 additions and 23 deletions

View file

@ -40,15 +40,24 @@ mcpServers:
description: Local HTTP MCP fixture for allowlist-override e2e tests.
timeout: 30000
# Unified file upload routing. Spreadsheet/csv files resolve to llmDeliveryPath
# 'none' (kept out of LLM context + provider delivery; reachable only via tools
# like code interpreter). Exercised by e2e/specs/mock/unified-upload.spec.ts.
# Per-endpoint file upload config so both upload UXs get e2e coverage:
# - Mock Provider A keeps the legacy 3-way dropdown (chat.spec.ts exercises
# "Upload to Provider"); legacyFileUploadUX also forces llmDeliveryPath
# 'provider', so that test's CSV still reaches the model.
# - Mock Provider B uses the unified single button with spreadsheet/csv routed
# to llmDeliveryPath 'none' (unified-upload.spec.ts).
fileConfig:
defaultLLMDeliveryPath:
overrides:
'text/csv': 'none'
'text/plain': 'none'
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'none'
endpoints:
'Mock Provider A':
legacyFileUploadUX: true
'Mock Provider B':
defaultLLMDeliveryPath:
overrides:
'text/csv': 'none'
'text/plain': 'none'
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'none'
# Routed to the provider: still delivered to the model AND shown in chat.
'text/markdown': 'provider'
endpoints:
custom:

View file

@ -4,6 +4,7 @@ import {
MOCK_ENDPOINTS,
NEW_CHAT_PATH,
fetchJson,
isAgentsStream,
getAccessToken,
selectMockEndpoint,
} from './helpers';
@ -11,19 +12,23 @@ import {
/**
* Unified file upload per-mime-type delivery routing (PR #12626).
*
* Runs against Mock Provider B, configured for unified mode in
* e2e/config/librechat.e2e.yaml (Mock Provider A stays on the legacy dropdown
* for chat.spec.ts's upload-to-provider test).
*
* What this proves end-to-end (real backend + DB), and what it deliberately can't:
* - The composer renders ONE attach button (unified mode), not the legacy 3-way
* dropdown `legacyFileUploadUX` is unset in e2e/config/librechat.e2e.yaml.
* - A spreadsheet/csv upload routes through `processAgentFileUpload` and persists
* `llmDeliveryPath: 'none'` per the configured `defaultLLMDeliveryPath` override,
* i.e. it is NOT delivered to the LLM as text/provider reachable only by tools.
* dropdown.
* - A `none`-routed upload (csv) persists `llmDeliveryPath: 'none'` and is kept
* out of LLM delivery reachable only by tools.
* - A `provider`-routed upload (markdown) is STILL delivered to the model AND
* shown as an attachment chip unified mode doesn't lose upload-to-provider.
* - The mock harness has no code-execution environment, so the "available to the
* code interpreter at tool-execute time" half is covered by jest
* (`packages/api/src/agents/resources.test.ts`), not here.
*
* `.xlsx` follows the identical code path (same `defaultLLMDeliveryPath` override,
* same standard-storage branch); csv is used here so the uploaded bytes match the
* declared mime type without synthesizing a binary spreadsheet.
* `.xlsx` follows the identical `none` code path; csv/markdown are used so the
* uploaded bytes match the declared mime type without synthesizing binaries.
*/
const uniqueName = (p: string) => `${p}-${Date.now()}-${Math.floor(Math.random() * 1e4)}`;
@ -33,8 +38,10 @@ type UploadedFile = { filename?: string; type?: string; llmDeliveryPath?: string
const isFilesUpload = (url: string, method: string) =>
method === 'POST' && /\/api\/files(?:\?|$)/.test(new URL(url).pathname);
async function uploadViaUnifiedButton(page: Page, fileName: string) {
const csv = 'name,score\nalice,1\nbob,2\n';
async function uploadViaUnifiedButton(
page: Page,
file: { name: string; mimeType: string; content: string },
) {
const uploadResponse = page.waitForResponse((r) => isFilesUpload(r.url(), r.request().method()), {
timeout: 30000,
});
@ -43,9 +50,9 @@ async function uploadViaUnifiedButton(page: Page, fileName: string) {
page.locator('#attach-file-button').click(),
]);
await fileChooser.setFiles({
name: fileName,
mimeType: 'text/csv',
buffer: Buffer.from(csv, 'utf8'),
name: file.name,
mimeType: file.mimeType,
buffer: Buffer.from(file.content, 'utf8'),
});
return uploadResponse;
}
@ -55,8 +62,8 @@ test.describe('unified file upload', () => {
test.setTimeout(120000);
await page.goto(NEW_CHAT_PATH, { timeout: 10000 });
// Default model needs a real key; pick a mock endpoint so the composer is live.
await selectMockEndpoint(page, MOCK_ENDPOINTS[0]);
// Default model needs a real key; Mock Provider B is the unified-mode endpoint.
await selectMockEndpoint(page, MOCK_ENDPOINTS[1]);
// Unified mode: one attach button, and the legacy multi-option dropdown trigger
// is not rendered at all.
@ -66,7 +73,11 @@ test.describe('unified file upload', () => {
// Upload-time routing: the configured override (csv -> none) must persist, so
// the file is kept out of LLM delivery and left for tools (code interpreter).
const fileName = `${uniqueName('data')}.csv`;
const response = await uploadViaUnifiedButton(page, fileName);
const response = await uploadViaUnifiedButton(page, {
name: fileName,
mimeType: 'text/csv',
content: 'name,score\nalice,1\nbob,2\n',
});
expect(response.ok()).toBeTruthy();
const uploaded = (await response.json()) as UploadedFile;
@ -80,4 +91,52 @@ test.describe('unified file upload', () => {
expect(persisted, `uploaded file "${fileName}" should persist`).toBeTruthy();
expect(persisted?.llmDeliveryPath).toBe('none');
});
test('single attach button still delivers a provider-routed upload and shows it in chat', async ({
page,
}) => {
test.setTimeout(120000);
await page.goto(NEW_CHAT_PATH, { timeout: 10000 });
await selectMockEndpoint(page, MOCK_ENDPOINTS[1]);
// Same single unified button — no legacy dropdown.
await expect(page.locator('#attach-file-button')).toBeVisible({ timeout: 15000 });
await expect(page.locator('#attach-file-menu-button')).toHaveCount(0);
// markdown is overridden to `provider` for Mock Provider B: it should be
// delivered to the model (unlike `none`) while still attaching to the chat.
const fileName = `${uniqueName('doc')}.md`;
const response = await uploadViaUnifiedButton(page, {
name: fileName,
mimeType: 'text/markdown',
content: '# E2E provider doc\n\nrouted to the provider via unified upload\n',
});
expect(response.ok()).toBeTruthy();
expect(((await response.json()) as UploadedFile).llmDeliveryPath).toBe('provider');
// (a) shows as an attachment chip in the composer before sending.
await expect(page.getByRole('button', { name: fileName })).toBeVisible({ timeout: 15000 });
// (b) reaches the model input: the mock LLM echoes a pass marker only when the
// provider file is present in the request content (see e2e/setup/fake-model.js).
const input = page.getByRole('textbox', { name: 'Message input' });
await input.click();
await input.fill(`E2E_ASSERT_PROVIDER_FILE:${fileName}`);
const [stream] = await Promise.all([
page.waitForResponse(isAgentsStream, { timeout: 30000 }),
page.getByTestId('send-button').click(),
]);
expect(stream.ok()).toBeTruthy();
await expect(
page
.getByTestId('messages-view')
.getByText(`E2E provider file assertion passed: ${fileName}`),
).toBeVisible({ timeout: 20000 });
// chip persists on the sent message.
await expect(
page.getByTestId('messages-view').getByRole('button', { name: fileName }),
).toBeVisible();
});
});