From 56823496a3e71329a4c2f8bfa7f5647986297e18 Mon Sep 17 00:00:00 2001 From: ethernet Date: Wed, 22 Jul 2026 16:36:28 -0400 Subject: [PATCH] test(desktop): cover submit drift through e2e Exercise the full Electron, gateway, and mock-provider submit path while same-chat route query tokens churn during session creation. Assert the mock provider receives the prompt and its streamed response reaches the transcript. --- apps/desktop/e2e/fixtures.ts | 2 + apps/desktop/e2e/mock-server.ts | 19 ++++++- apps/desktop/e2e/submit-drift.spec.ts | 75 +++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/e2e/submit-drift.spec.ts diff --git a/apps/desktop/e2e/fixtures.ts b/apps/desktop/e2e/fixtures.ts index 93acc6f86236..7d0ea6be25c9 100644 --- a/apps/desktop/e2e/fixtures.ts +++ b/apps/desktop/e2e/fixtures.ts @@ -318,6 +318,7 @@ export async function launchDesktop( export interface MockBackendFixture { app: ElectronApplication page: Page + mock: Awaited> mockUrl: string sandbox: Sandbox cleanup: () => Promise @@ -346,6 +347,7 @@ export async function setupMockBackend(): Promise { return { app, page, + mock, mockUrl: mock.url, sandbox, cleanup: async () => { diff --git a/apps/desktop/e2e/mock-server.ts b/apps/desktop/e2e/mock-server.ts index 680cba30e7af..f7a4a74b338b 100644 --- a/apps/desktop/e2e/mock-server.ts +++ b/apps/desktop/e2e/mock-server.ts @@ -22,10 +22,16 @@ const CANNED_REPLY = 'Hello from the mock inference server! The full boot chain /** * Start the mock server on an ephemeral port. * - * @returns a handle with `port`, `url`, and `close()`. + * @returns a handle with `port`, `url`, received user prompts, and `close()`. */ -export function startMockServer(): Promise<{ port: number; url: string; close: () => Promise }> { +export function startMockServer(): Promise<{ + port: number + url: string + receivedPrompts: string[] + close: () => Promise +}> { return new Promise((resolve, reject) => { + const receivedPrompts: string[] = [] const server = http.createServer((req, res) => { // CORS headers — the Electron renderer doesn't need them, but they // don't hurt and make the server usable from a browser context too. @@ -75,6 +81,14 @@ export function startMockServer(): Promise<{ port: number; url: string; close: ( // malformed JSON — treat as non-streaming with defaults } + const lastUserMessage = [...(parsed.messages ?? [])] + .reverse() + .find((message: { role?: unknown }) => message?.role === 'user') + + if (typeof lastUserMessage?.content === 'string') { + receivedPrompts.push(lastUserMessage.content) + } + const stream = parsed.stream === true const model = parsed.model || 'mock-model' @@ -187,6 +201,7 @@ export function startMockServer(): Promise<{ port: number; url: string; close: ( resolve({ port, url, + receivedPrompts, close: () => new Promise((resolveClose, rejectClose) => { server.close((err) => { diff --git a/apps/desktop/e2e/submit-drift.spec.ts b/apps/desktop/e2e/submit-drift.spec.ts new file mode 100644 index 000000000000..a3c447c9c53e --- /dev/null +++ b/apps/desktop/e2e/submit-drift.spec.ts @@ -0,0 +1,75 @@ +/** + * Regression coverage for #69578: harmless route-token churn during a send + * must not make the desktop silently drop the prompt before prompt.submit. + */ + +import { test, expect } from './test' + +import { + type MockBackendFixture, + setupMockBackend, + waitForAppReady, +} from './fixtures' + +const PROMPT = 'E2E route token drift must still submit this prompt.' + +let fixture: MockBackendFixture | null = null + +test.beforeAll(async () => { + fixture = await setupMockBackend() + await waitForAppReady(fixture!, 120_000) +}) + +test.afterAll(async () => { + await fixture?.cleanup() + fixture = null +}) + +test('submits while same-chat search tokens churn during new-session creation', async ({}, testInfo) => { + const { page, mock } = fixture! + const composer = page.locator('[contenteditable="true"]').first() + + await composer.click() + await composer.type(PROMPT, { delay: 10 }) + + // The submit pipeline snapshots the route synchronously, then awaits session + // creation. Keep changing only the query string of whichever chat route is + // current. Before #69578, comparing the raw route token treated this as a + // user chat switch and aborted before prompt.submit. + await page.evaluate(() => { + let revision = 0 + const interval = window.setInterval(() => { + const pathname = window.location.hash.slice(1).split(/[?#]/, 1)[0] || '/new' + window.location.hash = `${pathname}?e2e-route-churn=${revision++}` + }, 1) + + ;(window as typeof window & { __e2eStopRouteChurn?: () => void }).__e2eStopRouteChurn = () => { + window.clearInterval(interval) + } + }) + + try { + await page.keyboard.press('Enter') + + await expect + .poll(() => mock.receivedPrompts.includes(PROMPT), { timeout: 60_000 }) + .toBe(true) + + await page.waitForFunction( + prompt => document.querySelector('[data-slot="aui_thread-viewport"]')?.textContent?.includes(prompt) ?? false, + PROMPT, + { timeout: 15_000 }, + ) + await page.waitForFunction( + () => document.querySelector('[data-slot="aui_thread-viewport"]')?.textContent?.includes('mock inference server') ?? false, + undefined, + { timeout: 60_000 }, + ) + } finally { + await page.evaluate(() => { + ;(window as typeof window & { __e2eStopRouteChurn?: () => void }).__e2eStopRouteChurn?.() + }) + } + + await page.screenshot({ path: testInfo.outputPath('same-chat-route-churn-submitted.png') }) +}) \ No newline at end of file