test(desktop): cover post-compression queueing

Exercise automatic context compression, a subsequent held normal turn,
and an Enter-submitted follow-up. Assert the follow-up remains queued
until the normal turn completes, then dispatches once.
This commit is contained in:
ethernet 2026-07-23 16:25:13 -04:00
parent 0842fdbb11
commit c0c0b4e6d9
2 changed files with 101 additions and 13 deletions

View file

@ -23,8 +23,10 @@ export const MOCK_REPLY = 'Hello from the mock inference server! The full boot c
export interface MockServerOptions {
/** Pause the matching stream after its first token for session-switch E2E coverage. */
holdFirstStreamForPrompt?: string
/** Pause the first completion whose request JSON contains this text. */
/** Pause the first completion whose request JSON contains this text. */
holdFirstCompletionContaining?: string
/** Pause the first completion after another hold whose request JSON contains this text. */
holdFirstCompletionContainingAfterHeldCompletion?: string
/** Absolute sandbox path written by the verify-on-stop scripted tool call. */
verificationWritePath?: string
}
@ -285,12 +287,20 @@ export function startMockServer(options: MockServerOptions = {}): Promise<MockSe
let resolveHeldStreamStarted: (() => void) | null = null
let releaseHeldStream: (() => void) | null = null
let heldCompletionCount = 0
const heldStreamStarted = new Promise<void>(resolveHeld => {
resolveHeldStreamStarted = resolveHeld
})
const heldStreamReleased = new Promise<void>(resolveRelease => {
releaseHeldStream = resolveRelease
})
let heldCompletionAfterHeldCompletion = false
let heldStreamStarted: Promise<void>
let heldStreamReleased: Promise<void>
const resetHeldStreamGate = (): void => {
heldStreamStarted = new Promise<void>(resolveHeld => {
resolveHeldStreamStarted = resolveHeld
})
heldStreamReleased = new Promise<void>(resolveRelease => {
releaseHeldStream = resolveRelease
})
}
resetHeldStreamGate()
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.
@ -355,6 +365,15 @@ export function startMockServer(options: MockServerOptions = {}): Promise<MockSe
heldCompletionCount === 0 &&
JSON.stringify(parsed).includes(options.holdFirstCompletionContaining),
)
const holdThisCompletionAfterHeldCompletion = Boolean(
options.holdFirstCompletionContainingAfterHeldCompletion &&
heldCompletionCount === 1 &&
!heldCompletionAfterHeldCompletion &&
JSON.stringify(parsed).includes(options.holdFirstCompletionContainingAfterHeldCompletion),
)
if (holdThisCompletionAfterHeldCompletion) {
heldCompletionAfterHeldCompletion = true
}
// Detect the interim-message test trigger: the user's message
// contains a specific keyword. The mock walks through the
@ -459,10 +478,11 @@ export function startMockServer(options: MockServerOptions = {}): Promise<MockSe
if (stream) {
const holdThisStream = Boolean(
options.holdFirstStreamForPrompt && typeof lastUserMessage?.content === 'string' &&
lastUserMessage.content.includes(options.holdFirstStreamForPrompt),
(typeof lastUserMessage?.content === 'string' && (
options.holdFirstStreamForPrompt && lastUserMessage.content.includes(options.holdFirstStreamForPrompt)
)),
)
streamTextResponse(res, model, MOCK_REPLY, holdThisStream || holdThisCompletion ? () => {
streamTextResponse(res, model, MOCK_REPLY, holdThisStream || holdThisCompletion || holdThisCompletionAfterHeldCompletion ? () => {
if (holdThisCompletion) {
heldCompletionCount++
}
@ -470,8 +490,10 @@ export function startMockServer(options: MockServerOptions = {}): Promise<MockSe
return heldStreamReleased
} : undefined)
} else {
if (holdThisCompletion) {
heldCompletionCount++
if (holdThisCompletion || holdThisCompletionAfterHeldCompletion) {
if (holdThisCompletion) {
heldCompletionCount++
}
resolveHeldStreamStarted?.()
void heldStreamReleased.then(() => nonStreamingTextResponse(res, model, MOCK_REPLY))
} else {
@ -510,7 +532,10 @@ export function startMockServer(options: MockServerOptions = {}): Promise<MockSe
receivedPrompts,
waitForHeldStream: () => heldStreamStarted,
waitForHeldCompletion: () => heldStreamStarted,
releaseHeldStream: () => releaseHeldStream?.(),
releaseHeldStream: () => {
releaseHeldStream?.()
resetHeldStreamGate()
},
heldCompletionCount: () => heldCompletionCount,
close: () =>
new Promise((resolveClose, rejectClose) => {

View file

@ -144,3 +144,66 @@ auxiliary:
expect(fixture.mock.heldCompletionCount()).toBe(1)
})
})
test.describe('session compression handoff queueing', () => {
let fixture: MockBackendFixture
test.beforeEach(async () => {
fixture = await setupMockBackend({
modelContextLength: 64_000,
extraConfig: `compression:
threshold_tokens: 22000
protect_first_n: 0
protect_last_n: 1
auxiliary:
compression:
provider: custom
model: mock-model`,
mockServer: {
holdFirstCompletionContaining: 'You are a summarization agent creating a context checkpoint.',
holdFirstCompletionContainingAfterHeldCompletion: 'E2E_POST_COMPACTION_RUNNING',
},
})
await waitForAppReady(fixture, 120_000)
})
test.afterEach(async () => {
await fixture?.cleanup()
})
test('queues an Enter-submitted prompt while the post-compression turn is still running', async ({}, testInfo) => {
const { mock, page } = fixture
const compressionTrigger = 'E2E_TRIGGER_AUTOMATIC_COMPACTION '.repeat(500)
const running = 'E2E_POST_COMPACTION_RUNNING'
const queued = 'E2E_QUEUED_AFTER_COMPACTION'
const primary = page.locator('[data-slot="composer-root"] button[type="submit"]')
// Cross the threshold, complete compaction, then start a separate normal
// turn. This mirrors the user-visible sequence rather than holding the
// turn that caused the compaction itself.
await pasteAndSend(page, 'E2E_POST_COMPACTION_HISTORY_ONE '.repeat(5))
await waitForTranscript(page, MOCK_REPLY)
await pasteAndSend(page, 'E2E_POST_COMPACTION_HISTORY_TWO '.repeat(5))
await waitForTranscript(page, MOCK_REPLY)
await pasteAndSend(page, compressionTrigger)
await mock.waitForHeldCompletion()
expect(mock.heldCompletionCount()).toBe(1)
await expect(page.getByRole('status', { name: 'Summarizing thread' }).last()).toBeVisible()
mock.releaseHeldStream()
await expect(page.getByText(MOCK_REPLY, { exact: true })).toHaveCount(4)
await pasteAndSend(page, running)
await mock.waitForHeldStream()
await expect(primary).toHaveAttribute('aria-label', 'Queue message')
await pasteAndSend(page, queued)
await expect(page.getByText('1 Queued')).toBeVisible()
expect(mock.receivedPrompts).not.toContain(queued)
await page.screenshot({ path: testInfo.outputPath('queued-after-compression-handoff.png') })
mock.releaseHeldStream()
await expect.poll(() => mock.receivedPrompts.filter(prompt => prompt === queued), { timeout: 30_000 }).toHaveLength(1)
})
})