fix(session-export): preserve streamed browser downloads

This commit is contained in:
NI0317
2026-08-12 18:44:04 +08:00
parent 8c7dab8755
commit 785f41daed
35 changed files with 226 additions and 136 deletions

View File

@@ -52,6 +52,11 @@ async function assertBaselineSucceeded(response: Response, method: string): Prom
}
async function ensureSeedOpen(page: Page): Promise<void> {
const welcome = page.locator('[class*="onboardingOverlay"]')
if (await welcome.count() > 0) {
await welcome.getByRole('button').click()
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
}
const chat = page.getByRole('tab', { name: 'Chat', exact: true })
// Search is a collapsed header action; expand it so the input is actionable.
const searchButton = page.getByRole('button', { name: 'Search sessions' })
@@ -297,7 +302,8 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
}
expect(headerBox.x + headerBox.width - (buttonBox.x + buttonBox.width)).toBeLessThanOrEqual(32)
const responsePromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/session.export', { timeout: 30_000 })
response.request().method() === 'HEAD'
&& new URL(response.url()).pathname === '/api/session.export', { timeout: 30_000 })
const downloadPromise = page.waitForEvent('download', { timeout: 30_000 })
await exportButton.click()
const response = await responsePromise
@@ -315,17 +321,61 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
expect(content).toContain('FIRST_DONE')
await dialog.getByText('Close', { exact: true }).click()
const input = page.locator('textarea').first()
const slashDownloadPromise = page.waitForEvent('download', { timeout: 30_000 })
await input.fill('/export')
await page.getByRole('option', { name: /export/u }).waitFor({ timeout: 10_000 })
await input.press('Enter')
const slashDownload = await slashDownloadPromise
expect(slashDownload.suggestedFilename()).toBe(download.suggestedFilename())
await page.getByRole('dialog', { name: 'Session download started' }).waitFor({ timeout: 30_000 })
await page.getByRole('dialog', { name: 'Session download started' })
.getByText('Close', { exact: true }).click()
}, 60_000)
const observer = await newEnglishPage(browser)
const observerTripwire = watchConsole(observer)
const observerSlotErrors: string[] = []
let observerDownloads = 0
observer.on('download', () => { observerDownloads += 1 })
observer.on('console', (message) => {
if (message.type() === 'error' && /slot entry crashed/i.test(message.text())) {
observerSlotErrors.push(message.text())
}
})
const observerSessionBaseline = baselineResponse(observer, 'session.list')
const observerWorkspaceBaseline = baselineResponse(observer, 'workspace.list')
const [, observerSessionResponse, observerWorkspaceResponse] = await Promise.all([
observer.goto(scaffold.baseUrl, { waitUntil: 'load' }),
observerSessionBaseline,
observerWorkspaceBaseline,
])
await Promise.all([
assertBaselineSucceeded(observerSessionResponse, 'observer session.list'),
assertBaselineSucceeded(observerWorkspaceResponse, 'observer workspace.list'),
])
await observer.getByText('Ungrouped', { exact: true }).waitFor({ timeout: 30_000 })
await ensureSeedOpen(observer)
try {
const input = page.locator('textarea').first()
const slashDownloadPromise = page.waitForEvent('download', { timeout: 30_000 })
await input.fill('/export')
await page.getByRole('option', { name: /export/u }).waitFor({ timeout: 10_000 })
await input.press('Enter')
const slashDownload = await slashDownloadPromise
expect(slashDownload.suggestedFilename()).toBe(download.suggestedFilename())
const slashFiles = unzipSync(await readFile(await slashDownload.path()))
const slashContent = strFromU8(slashFiles['session.jsonl'] as Uint8Array)
const slashEvents = parseSessionLog(slashContent)
const exportRun = slashEvents.findLast(event => event.type === 'command/run' && event.data.name === 'export')
if (exportRun?.type !== 'command/run') throw new Error('slash ZIP has no export command/run')
const exportDone = slashEvents.find(event =>
event.type === 'command/done' && event.data.commandId === exportRun.data.commandId)
expect(exportDone?.type).toBe('command/done')
await page.getByRole('dialog', { name: 'Session download started' }).waitFor({ timeout: 30_000 })
await page.getByRole('dialog', { name: 'Session download started' })
.getByText('Close', { exact: true }).click()
await observer.getByText('Session log download requested.', { exact: true }).waitFor({ timeout: 30_000 })
expect(observerDownloads).toBe(0)
expect(await observer.getByRole('dialog', { name: 'Session download started' }).count()).toBe(0)
expect({
pageErrors: observerTripwire.pageErrors,
slotErrors: observerSlotErrors,
warnings: observerTripwire.warnings,
}).toEqual({ pageErrors: [], slotErrors: [], warnings: [] })
} finally {
await observer.close()
}
}, 120_000)
it.skipIf(MODE === 'record')('focuses the ledger by dragging an overview interval', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-timeline'))