From 0ece3571708ad3094bf69b288890ee7920b4d3ef Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 30 Jul 2026 07:26:55 -0400 Subject: [PATCH] test(e2e): cover interrupt & steer sealing mid-stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mock Playwright suite covered every sibling during-run action — steer at a tool boundary, steer degrading to a queued follow-up, queue, and interrupt & send — but not interrupt & steer, the one this stack adds. Uses E2E_SLOW_REPLY, which streams pure text with no tools, so the scenario is the same one where an ordinary steer provably degrades to a queued follow-up turn. Injecting in-thread there is something only a mid-stream seal can do, which makes the assertion discriminating rather than incidental: the steer part lands in the response, the final chunk never arrives, the text written before the seal survives, and no follow-up turn pair is created. --- e2e/specs/mock/steering.spec.ts | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) 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); + }); });