Files
2026-07-13 13:12:33 +08:00

106 lines
4.8 KiB
TypeScript

import { test, expect } from '@playwright/test'
const CONTROL_URL = '/control/'
const LIVE = process.env.OPENSQUILLA_E2E_LIVE === '1'
test.describe('Compaction maintenance card', () => {
test('idle chat renders no maintenance card', async ({ page }) => {
await page.goto(CONTROL_URL + 'chat/new')
await page.waitForSelector('.conn-pill', { timeout: 10000 })
await expect(page.locator('.chat-compact-status')).toHaveCount(0)
})
test('live /compact shows the card with elapsed ticking, then settles and dismisses', async ({ page }) => {
test.skip(!LIVE, 'Live gateway test; set OPENSQUILLA_E2E_LIVE=1 to run.')
test.setTimeout(300000)
await page.goto(CONTROL_URL + 'chat/new')
await page.waitForSelector('.conn-pill', { timeout: 10000 })
// Seed the session with one short exchange so compaction has messages.
const textarea = page.locator('.chat-textarea')
await textarea.fill('Reply with the single word: ok')
await page.locator('.chat-send-btn[aria-label="Send"]').click()
await expect(page.locator('.msg-ai').first()).toBeVisible({ timeout: 120000 })
await expect(page.locator('.work-card')).toHaveCount(0, { timeout: 120000 })
// Trigger manual compaction through the slash command path.
await textarea.fill('/compact')
await page.locator('.chat-send-btn[aria-label="Send"]').click()
const card = page.locator('.chat-compact-status')
await expect(card).toBeVisible({ timeout: 10000 })
// Observe the card until it settles. Fast compactions finish in under a
// second — the elapsed chip never leaves 0s and the busy state may never
// be sampled — so tick/busy assertions are conditional on duration while
// lifecycle assertions stay unconditional.
const observed = await page.evaluate(async () => {
const t0 = Date.now()
const samples: Array<{
title: string | null
elapsed: string | null
busyDot: boolean
gaugeBreathing: boolean
gaugeDone: boolean
}> = []
while (Date.now() - t0 < 120000) {
const cardEl = document.querySelector('.chat-compact-status')
const titleEl = document.querySelector('.chat-compact-status__title')
const elapsedEl = document.querySelector('.chat-compact-status__elapsed')
samples.push({
title: titleEl ? titleEl.textContent : null,
elapsed: elapsedEl ? elapsedEl.textContent : null,
busyDot: !!document.querySelector('.chat-compact-status__dot--pulsing'),
gaugeBreathing: !!document.querySelector('.chat-compact-status__gauge-fill--breathing'),
gaugeDone: !!document.querySelector('.chat-compact-status__gauge-fill--done'),
})
const settled = cardEl !== null && !document.querySelector('.chat-compact-status__dot--pulsing')
if ((settled || cardEl === null) && samples.length > 2) break
await new Promise((resolve) => setTimeout(resolve, 150))
}
return samples
})
// Lifecycle: the card rendered a title and reached a terminal state.
const titles = observed.map((s) => s.title).filter((t): t is string => t !== null)
expect(titles.length).toBeGreaterThan(0)
expect(titles.some((t) => /^(Compacting context|Context compacted|Already within context budget)/.test(t))).toBe(true)
const last = observed[observed.length - 1]
expect(last.busyDot).toBe(false)
expect(last.title === null || /^(Context compacted|Already within context budget)/.test(last.title)).toBe(true)
const busySamples = observed.filter((s) => s.busyDot)
if (busySamples.length > 0) {
// While busy the gauge breathes (real fill or indeterminate full width).
expect(busySamples.some((s) => s.gaugeBreathing)).toBe(true)
}
// Tick proof: ~2.4s of busy time (16 samples at 150ms) must show at
// least two distinct elapsed second values.
if (busySamples.length >= 16) {
const elapsedSeen = new Set(busySamples.map((s) => s.elapsed).filter((t): t is string => t !== null))
expect(elapsedSeen.size).toBeGreaterThanOrEqual(2)
}
// Completed settle: gauge turns into the static done fill and the frozen
// elapsed stops ticking, observable inside the 5s auto-dismiss window.
if (last.title !== null && /^Context compacted/.test(last.title)) {
expect(last.gaugeDone).toBe(true)
expect(last.gaugeBreathing).toBe(false)
if (last.elapsed !== null && (await card.count()) > 0) {
const frozen = await card.locator('.chat-compact-status__elapsed').textContent().catch(() => null)
if (frozen !== null) {
await page.waitForTimeout(1200)
if ((await card.count()) > 0) {
await expect(card.locator('.chat-compact-status__elapsed')).toHaveText(frozen)
}
}
}
}
// Terminal states keep the existing auto-dismiss timing.
await expect(card).toHaveCount(0, { timeout: 15000 })
})
})