mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge pull request #3176 from deepseek-harness/worktree/web-readable-ask-question-cards
fix(web): render readable ask-user transcripts
This commit is contained in:
@@ -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,57 @@ 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') {
|
||||
// Keep the derived session's relative-time header stable as the source
|
||||
// fixture ages.
|
||||
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',
|
||||
message: 'the user cancelled ask_user_question',
|
||||
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 +290,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 +344,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',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user