test(web): cover empty-draft queue flush with a keyless replay scenario
Also pin the queue-item-not-found convergence arm through the hub and document the repeated-trigger contract on steerQueue.
This commit is contained in:
@@ -34,6 +34,18 @@ const REPLAY_PACE_MS = 100
|
||||
const PROMPT = 'Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop.'
|
||||
const STEER = 'Interjection: include the word BANANA in your final reply.'
|
||||
|
||||
// Empty-draft flush scenario: an override-only fixture. The whole-script
|
||||
// replacement answers both model calls of a FRESH session (no recorded
|
||||
// session.jsonl exists — call 0 keeps the turn open with a question-tool
|
||||
// call, call 1 is the reply after both steerings drain).
|
||||
const STEER_ALL_DIR = fileURLToPath(new URL('./snapshots/steer-all', import.meta.url))
|
||||
const STEER_ALL_FIXTURE = join(STEER_ALL_DIR, 'session.jsonl')
|
||||
const STEER_ALL_OVERRIDE = join(STEER_ALL_DIR, 'replay.override.json')
|
||||
const STEER_ALL_MID = join(STEER_ALL_DIR, 'mid-steer.expected.md')
|
||||
const STEER_ALL_SETTLED = join(STEER_ALL_DIR, 'settled.expected.md')
|
||||
const STEER_ONE = 'Interjection: include the word BANANA in your final reply.'
|
||||
const STEER_TWO = 'Interjection: include the word ORANGE in your final reply.'
|
||||
|
||||
/** Concatenated assistant text deltas — the model-visible reply body. */
|
||||
function assistantText(events: SessionEvent[]): string {
|
||||
return events
|
||||
@@ -278,3 +290,98 @@ describe('web e2e: composer shortcut follows the swapped busy behavior', () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 90_000)
|
||||
})
|
||||
|
||||
describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
const sessionEvents: SessionEvent[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
// The scenario boots a fresh session against the override-only fixture;
|
||||
// the replay.override.json sidecar replaces the derived script, so the
|
||||
// (deliberately absent) session.jsonl is never read.
|
||||
scaffold = await launchWebScaffold({
|
||||
replayFixture: STEER_ALL_FIXTURE,
|
||||
replayOverride: STEER_ALL_OVERRIDE,
|
||||
paceMs: REPLAY_PACE_MS,
|
||||
})
|
||||
scaffold.ctx.on('session/event', (_session, event) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('queues two messages, then flushes both with an empty-draft Cmd+Enter', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-steer-all'))
|
||||
const input = page.locator('textarea').first()
|
||||
await input.waitFor({ timeout: 10_000 })
|
||||
const settled = scaffold.whenTurnSettled(30_000)
|
||||
|
||||
// Call 0 streams a question-tool call; the fills must land inside the
|
||||
// first replay window, before the question composer replaces the textarea.
|
||||
await input.fill(PROMPT)
|
||||
await input.press('Enter')
|
||||
await input.fill(STEER_ONE)
|
||||
await input.press('Enter')
|
||||
await input.fill(STEER_TWO)
|
||||
await input.press('Enter')
|
||||
const dock = page.locator('[data-queue-dock]')
|
||||
// Both messages queued: the two-row dock shows a collapsed count header,
|
||||
// and Playwright text matching skips the hidden rows — expand the list,
|
||||
// then assert each row's content.
|
||||
await dock.getByText('2 queued messages').waitFor({ timeout: 10_000 })
|
||||
await dock.getByRole('button').click()
|
||||
await dock.getByText(STEER_ONE, { exact: true }).waitFor({ timeout: 10_000 })
|
||||
await dock.getByText(STEER_TWO, { exact: true }).waitFor({ timeout: 10_000 })
|
||||
expect(await page.locator('[data-pending-steering]').count()).toBe(0)
|
||||
|
||||
// Empty draft + Cmd+Enter: both queued rows steer in FIFO order, the dock
|
||||
// empties, and the pending steering renders at the conversation tail.
|
||||
await input.press('Meta+Enter')
|
||||
await expect.poll(
|
||||
() => page.locator('[data-pending-steering]').filter({ hasText: /BANANA|ORANGE/ }).count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(2)
|
||||
expect(await page.locator('[data-queue-dock]').count()).toBe(0)
|
||||
const mid = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(STEER_ALL_MID, mid, MODE)
|
||||
|
||||
// Answer the question; the step closes, the loop drains both steerings
|
||||
// into one next-step request, and the final reply obeys both markers.
|
||||
const composer = page.locator('[data-question-key]')
|
||||
await composer.waitFor({ timeout: 30_000 })
|
||||
await composer.getByRole('radio', { name: 'Yes' }).click()
|
||||
await composer.getByRole('radio', { name: 'Yes' }).press('Enter')
|
||||
await settled
|
||||
|
||||
const first = claimedMessages(sessionEvents, STEER_ONE)
|
||||
const second = claimedMessages(sessionEvents, STEER_TWO)
|
||||
expect(first).toHaveLength(1)
|
||||
expect(second).toHaveLength(1)
|
||||
expect(assistantText(sessionEvents)).toContain('BANANA')
|
||||
expect(assistantText(sessionEvents)).toContain('ORANGE')
|
||||
await expect.poll(() => page.getByText(STEER_ONE, { exact: true }).count(), { timeout: 15_000 }).toBe(1)
|
||||
await expect.poll(() => page.getByText(STEER_TWO, { exact: true }).count(), { timeout: 15_000 }).toBe(1)
|
||||
expect(await page.locator('[data-pending-steering]').count()).toBe(0)
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(STEER_ALL_SETTLED, snapshot, MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 200_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(STEER_ALL_DIR, [
|
||||
'replay.override.json', 'mid-steer.expected.md', 'settled.expected.md',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user