fix(web): render readable ask-user transcripts

This commit is contained in:
Yichen Jiang
2026-08-27 11:28:59 +08:00
parent a24c71127f
commit 94db8e881b
14 changed files with 600 additions and 108 deletions
+115 -3
View File
@@ -17,7 +17,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
launchWebScaffold, recordFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
@@ -29,7 +29,9 @@ const COMPOSED_EXPECTED = join(SNAPSHOT_DIR, 'composed.expected.md')
// Final golden: the answered transcript — the question resolved into its tool
// round trip and the final reply, the state the composer goldens cannot see.
const ANSWERED_EXPECTED = join(SNAPSHOT_DIR, 'answered.expected.md')
const CANCELLED_EXPECTED = join(SNAPSHOT_DIR, 'cancelled.expected.md')
const MODE = webSnapshotMode()
const CANCELLED_SEED_ID = 'ask-question-cancelled-row-web-e2e'
// The composer's own growth cap, in text lines (QuestionComposer.module.css
// .fieldMirror). Asserted as TEXT lines, not as a box height: the two variants
@@ -61,6 +63,51 @@ async function capMetrics(field: Locator): Promise<{ textLines: number; scrolls:
// collapsed row painting its copy outside its own box.
const PROMPT = 'Use the ask_user_question tool to ask me exactly one multi-select question with id "color", question "Which color do you prefer?", header "Pick one", and two options: label "Blue" with description "A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.", and label "Green" with description "A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions." Set multi_select to true. After I answer, reply with the single word DONE and stop.'
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/** Replace the successful Tool settlement and omit the answer-dependent model step. */
function cancelledFixture(fixture: string): string {
let replaced = false
const lines: string[] = []
for (const line of fixture.trimEnd().split('\n')) {
const event: unknown = JSON.parse(line)
if (!isRecord(event)) throw new Error('question fixture event is invalid')
if (event.type === 'session') {
event.createdAt = Date.now()
lines.push(JSON.stringify(event))
continue
}
if (replaced) {
const data = event.data
if ((event.type === 'step/end' && isRecord(data) && data.step === 1)
|| event.type === 'turn/end') lines.push(line)
continue
}
if (event.type !== 'tool/result') {
lines.push(line)
continue
}
const data = event.data
if (!isRecord(data)) throw new Error('question fixture tool/result data is invalid')
const message = data.message
if (!isRecord(message) || !Array.isArray(message.content) || !isRecord(message.content[0])) {
throw new Error('question fixture tool/result message is invalid')
}
message.content[0].content = [{
type: 'text',
text: 'Error: the user cancelled ask_user_question',
}]
message.content[0].isError = true
data.error = { name: 'UserQuestionError', code: 'ASK_CANCELLED' }
replaced = true
lines.push(JSON.stringify(event))
}
if (!replaced) throw new Error('question fixture has no tool/result event')
return `${lines.join('\n')}\n`
}
describe('web e2e: resident question composer round trip', () => {
let scaffold: WebScaffold
let browser: Browser
@@ -237,7 +284,13 @@ describe('web e2e: resident question composer round trip', () => {
expect(await selectedRow.locator('[data-state="warning"]').count()).toBe(0)
await expect.poll(() => page.locator('[data-composer-input]').first().isEnabled(), { timeout: 10_000 }).toBe(true)
// Golden of the answered transcript: the ask_user_question round trip
// rendered as history (question tool row + DONE), composer takeover gone.
// rendered as history (expanded readable answers + DONE), composer takeover gone.
const answeredRow = page.getByRole('button', { name: 'Ask question 1/1 answered', exact: true })
await answeredRow.click()
await page.getByText('Which color do you prefer?', { exact: true }).waitFor({ timeout: 10_000 })
expect(await page.getByText('Blue', { exact: true }).count()).toBeGreaterThanOrEqual(1)
expect(await page.getByText('Include accessibility notes', { exact: true }).count()).toBe(1)
expect(await page.getByText(/"answers"/).count()).toBe(0)
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(ANSWERED_EXPECTED, snapshot, MODE)
expect(tripwire.pageErrors).toEqual([])
@@ -285,13 +338,72 @@ describe('web e2e: resident question composer round trip', () => {
await expect.poll(() => page.locator('[data-question-key]').count(), { timeout: 10_000 }).toBe(0)
}, 60_000)
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
})
describe.skipIf(MODE === 'record')('web e2e: cancelled question transcript', () => {
let cancelledScaffold: WebScaffold
let cancelledBrowser: Browser
let cancelledPage: Page
let cancelledTripwire: ReturnType<typeof watchConsole>
beforeAll(async () => {
cancelledScaffold = await launchWebScaffold({})
await seedSession(
cancelledScaffold,
cancelledFixture(await readFile(FIXTURE, 'utf8')),
CANCELLED_SEED_ID,
)
cancelledBrowser = await chromium.launch()
cancelledPage = await newEnglishPage(cancelledBrowser)
cancelledTripwire = watchConsole(cancelledPage)
await cancelledPage.goto(cancelledScaffold.authenticatedUrl, { waitUntil: 'load' })
await cancelledPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
const groupRow = cancelledPage.locator('[role="treeitem"]').first()
await groupRow.waitFor({ timeout: 15_000 })
await groupRow.click()
const sessionRow = cancelledPage.locator('[role="treeitem"]').nth(1)
await sessionRow.waitFor({ timeout: 10_000 })
await sessionRow.click()
}, 120_000)
afterAll(async () => {
await cancelledBrowser?.close()
await cancelledScaffold?.close()
})
it('expands to the cancellation verdict and original questions', async () => {
onTestFailed(() => saveFailureShot(cancelledPage, 'web-e2e-question-cancelled-row'))
const row = cancelledPage.getByRole('button', { name: 'Ask question cancelled', exact: true })
await row.waitFor({ timeout: 15_000 })
await row.click()
await cancelledPage
.getByText('This question set was cancelled before answers were submitted.', { exact: true })
.waitFor({ timeout: 10_000 })
await cancelledPage.getByText('Which color do you prefer?', { exact: true }).waitFor({ timeout: 10_000 })
expect(await cancelledPage.getByText(/"questions"/).count()).toBe(0)
expect(await cancelledPage
.getByText('Error: the user cancelled ask_user_question', { exact: true }).count()).toBe(0)
const snapshot = (await captureStableAria(
cancelledPage,
'[class*="centerCol"]',
cancelledScaffold.workspaceCwd,
)).split(CANCELLED_SEED_ID).join('{{seededId}}')
await compareOrRefreshGolden(CANCELLED_EXPECTED, snapshot, MODE)
expect(cancelledTripwire.pageErrors).toEqual([])
expect(cancelledTripwire.warnings).toEqual([])
}, 60_000)
it('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, [
'session.jsonl',
'ui.expected.md',
'sidebar.expected.md',
'composed.expected.md',
'answered.expected.md',
'cancelled.expected.md',
])
})
})