diff --git a/e2e/specs/mock/steering.spec.ts b/e2e/specs/mock/steering.spec.ts index e229a2ae6e..8e58466019 100644 --- a/e2e/specs/mock/steering.spec.ts +++ b/e2e/specs/mock/steering.spec.ts @@ -481,4 +481,57 @@ test.describe('mid-run steering and queuing', () => { // arrived (an uninterrupted slow run always ends with it). await expect(messagesView(page).getByText(SLOW_REPLY_LAST_CHUNK)).toHaveCount(0); }); + + /** + * Interrupt & steer is the only path that can inject with NO tool boundary + * ahead of it: the server asks the generating replica to seal the model + * stream at the next provider-safe chunk, keeps the partial answer, and + * resumes in the same message. + * + * The contrast with the two tests above IS the feature. `E2E_SLOW_REPLY` + * streams pure text with no tools, so an ordinary steer there provably + * degrades to a queued follow-up turn ("steer after the last tool boundary" + * above), and interrupt & send discards the half-written answer entirely. + * This path does neither: same absence of a boundary, opposite outcome. + */ + test('interrupt & steer (Cmd/Ctrl+Shift+Enter) seals mid-stream and injects with no tool boundary', async ({ + page, + }) => { + test.setTimeout(150000); + const label = uniqueLabel('preempt'); + const steerText = `Preempt steer ${label}`; + + await page.goto(NEW_CHAT_PATH, { timeout: 10000 }); + await selectMockEndpoint(page, MOCK_ENDPOINTS[0]); + await establishConversation(page, `preempt-setup-${label}`); + + const run = await sendMessage(page, `E2E_SLOW_REPLY:${label}`); + expect(run.ok()).toBeTruthy(); + // Let it visibly stream first, so the seal lands mid-generation. + await expect(messagesView(page).getByText('chunk-010')).toBeVisible({ timeout: 15000 }); + + await typeDuringRun(page, steerText); + const [steerResponse] = await Promise.all([ + page.waitForResponse(isSteerRequest, { timeout: 15000 }), + messageInput(page).press('ControlOrMeta+Shift+Enter'), + ]); + expect(steerResponse.status()).toBe(202); + + // Injected in-thread with no tool boundary available — only a mid-stream + // seal can put a steer part here. + await expect(appliedSteerParts(page).filter({ hasText: steerText })).toHaveCount(1, { + timeout: 90000, + }); + await expect(inFlightSteers(page)).toHaveCount(0); + + // Sealed, not run to completion: the last chunk never arrives. And unlike + // interrupt & send, the text written before the seal survives. + await expect(messagesView(page).getByText(SLOW_REPLY_LAST_CHUNK)).toHaveCount(0); + await expect(messagesView(page).getByText('chunk-010')).toBeVisible(); + + // Stayed INSIDE the response: the setup pair plus this pair, with no + // auto-sent follow-up pair (which both degradation paths produce). + await expect(messageTurns(page)).toHaveCount(4); + await expect(queuedRows(page)).toHaveCount(0); + }); });