mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-01 11:53:55 +00:00
🧪 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:
parent
e2f09fbd49
commit
f15ae1ab5b
2 changed files with 91 additions and 23 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue