test(e2e): cover interrupt & steer sealing mid-stream

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.
This commit is contained in:
Danny Avila 2026-07-30 07:26:55 -04:00
parent 1a84222fca
commit 0ece357170

View file

@ -481,4 +481,57 @@ test.describe('mid-run steering and queuing', () => {
// arrived (an uninterrupted slow run always ends with it). // arrived (an uninterrupted slow run always ends with it).
await expect(messagesView(page).getByText(SLOW_REPLY_LAST_CHUNK)).toHaveCount(0); 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);
});
}); });