Remove the leftover MAX_AUTO_SAVE_SIZE_BYTES guard from handleFileChunk's in-memory path. The request gate (canReceiveAttachment) already admits 10-50 MB generic files for in-memory receive on stores without disk streaming (browser), but the chunk handler silently dropped every chunk of such files: no ack was sent, the sender's waitForAck timed out, and the receiver's GUI never changed. Receive admission is now decided once, at request time. Adds a two-browser regression e2e that sends an 11 MB generic file and asserts Request -> progress -> Download. Co-authored-by: Cursor <cursoragent@cursor.com>
115 lines
4.6 KiB
TypeScript
115 lines
4.6 KiB
TypeScript
import { test, expect } from '../../fixtures/multi-client';
|
|
import { RegisterPage } from '../../pages/register.page';
|
|
import { ServerSearchPage } from '../../pages/server-search.page';
|
|
import { ChatMessagesPage } from '../../pages/chat-messages.page';
|
|
|
|
/**
|
|
* Regression coverage for "Sending files between users doesn't really work":
|
|
* a generic (non-media) file above the 10 MB auto-save cap sent to a browser
|
|
* receiver. The receiver clicks Request; previously the chunk handler dropped
|
|
* every incoming chunk with a silent file-too-large error, the sender's ack
|
|
* wait timed out, and the GUI never changed.
|
|
*/
|
|
const LARGE_FILE_SIZE_BYTES = 11 * 1024 * 1024;
|
|
|
|
test.describe('Large generic file transfer', () => {
|
|
test.describe.configure({ timeout: 420_000, retries: 1 });
|
|
|
|
test('browser receiver can request and download a generic file above the auto-save cap', async ({ createClient }) => {
|
|
const suffix = uniqueName('largefile');
|
|
const serverName = `Large File Server ${suffix}`;
|
|
const fileName = `${suffix}-dataset.bin`;
|
|
const caption = `Large file upload ${suffix}`;
|
|
const alice = await createClient();
|
|
const bob = await createClient();
|
|
const aliceMessages = new ChatMessagesPage(alice.page);
|
|
const bobMessages = new ChatMessagesPage(bob.page);
|
|
|
|
await test.step('Alice and Bob register and meet in a server', async () => {
|
|
const aliceRegister = new RegisterPage(alice.page);
|
|
|
|
await aliceRegister.goto();
|
|
await aliceRegister.register(`alice_${suffix}`, 'Alice', 'TestPass123!');
|
|
await expect(alice.page).toHaveURL(/\/dashboard/, { timeout: 15_000 });
|
|
|
|
const bobRegister = new RegisterPage(bob.page);
|
|
|
|
await bobRegister.goto();
|
|
await bobRegister.register(`bob_${suffix}`, 'Bob', 'TestPass123!');
|
|
await expect(bob.page).toHaveURL(/\/dashboard/, { timeout: 15_000 });
|
|
|
|
const aliceSearch = new ServerSearchPage(alice.page);
|
|
|
|
await aliceSearch.createServer(serverName, { description: 'Large generic file transfer coverage' });
|
|
await expect(alice.page).toHaveURL(/\/room\//, { timeout: 15_000 });
|
|
|
|
const bobSearch = new ServerSearchPage(bob.page);
|
|
|
|
await bobSearch.joinServerFromSearch(serverName);
|
|
await expect(bob.page).toHaveURL(/\/room\//, { timeout: 15_000 });
|
|
|
|
await aliceMessages.waitForReady();
|
|
await bobMessages.waitForReady();
|
|
});
|
|
|
|
await test.step('Alice sends an 11 MB generic file', async () => {
|
|
await attachGeneratedBinaryFile(aliceMessages, fileName, LARGE_FILE_SIZE_BYTES);
|
|
await aliceMessages.sendMessage(caption);
|
|
await expect(aliceMessages.getMessageItemByText(caption)).toBeVisible({ timeout: 30_000 });
|
|
});
|
|
|
|
const bobBubble = bobMessages.getMessageItemByText(caption);
|
|
|
|
await test.step('Bob sees the attachment card with a Request button', async () => {
|
|
await expect(bobBubble).toBeVisible({ timeout: 30_000 });
|
|
await expect(bobBubble.getByText(fileName, { exact: false })).toBeVisible({ timeout: 30_000 });
|
|
await expect(bobBubble.getByRole('button', { name: /request/i })).toBeVisible({ timeout: 20_000 });
|
|
});
|
|
|
|
await test.step('Bob requests the file and it downloads to completion', async () => {
|
|
await bobBubble.getByRole('button', { name: /request/i }).click();
|
|
|
|
// The transfer must visibly progress (Cancel replaces Request) instead of
|
|
// silently stalling at 0 bytes like the original bug.
|
|
await expect(bobBubble.getByRole('button', { name: /cancel/i })).toBeVisible({ timeout: 30_000 });
|
|
|
|
await expect(bobBubble.getByRole('button', { name: /download/i })).toBeVisible({ timeout: 300_000 });
|
|
await expect(bobBubble.getByText(/too large/i)).toHaveCount(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Builds the file inside the page so the multi-megabyte payload never crosses
|
|
* the CDP protocol as a base64 string.
|
|
*/
|
|
async function attachGeneratedBinaryFile(
|
|
messages: ChatMessagesPage,
|
|
fileName: string,
|
|
sizeBytes: number
|
|
): Promise<void> {
|
|
await messages.waitForReady();
|
|
|
|
await messages.composerInput.evaluate((element, { name, size }) => {
|
|
const bytes = new Uint8Array(size);
|
|
|
|
for (let index = 0; index < size; index++) {
|
|
bytes[index] = (index * 31 + 7) & 0xff;
|
|
}
|
|
|
|
const dataTransfer = new DataTransfer();
|
|
|
|
dataTransfer.items.add(new File([bytes], name, { type: 'application/octet-stream' }));
|
|
element.dispatchEvent(new DragEvent('drop', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
dataTransfer
|
|
}));
|
|
}, { name: fileName, size: sizeBytes });
|
|
}
|
|
|
|
function uniqueName(prefix: string): string {
|
|
return `${prefix}-${Date.now()}-${Math.random().toString(36)
|
|
.slice(2, 8)}`;
|
|
}
|