feat(ui-conversation): fold turn process before final answer (#2547)

* feat(ui-conversation): fold turn process before final answer

* fix(ui-chat): polish turn-process control row from review

* test(web): drive preset slash catalog with gestures

* fix(ui-chat): keep turn process order stable

* fix(ui-chat): preserve prompt order after pagination

Co-authored-by: Yif <877193178@qq.com>
This commit is contained in:
lsdsjy
2026-08-27 10:12:39 +00:00
committed by GitHub
co-authored by Yif
parent 2c9c871eff
commit 8b09a0be52
132 changed files with 4513 additions and 461 deletions
@@ -18,7 +18,9 @@ import {
webSnapshotMode,
type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, conversationContextKey, newEnglishPage, saveFailureShot } from './support.ts'
import {
connectFreshWorkspace, conversationContextKey, expandOwningTurnProcess, newEnglishPage, saveFailureShot,
} from './support.ts'
const MODE = webSnapshotMode()
const TURN_COUNT = 12
@@ -315,6 +317,7 @@ describe('web e2e: continuous conversation grown through the composer', () => {
const toolRow = page.locator(`[data-chat-call-id="${spec.callId}"]`)
await expect.poll(() => toolRow.count(), { timeout: 10_000 }).toBe(1)
expect(await toolRow.textContent()).toContain(spec.toolResultMarker)
await expandOwningTurnProcess(page, toolRow)
const disclosure = toolRow.locator('[data-sample="bash"]')
expect(await disclosure.getAttribute('aria-expanded')).toBe('false')
await disclosure.click()
@@ -332,9 +335,10 @@ describe('web e2e: continuous conversation grown through the composer', () => {
))).toHaveLength(TURN_COUNT)
expect(sessionEvents.flatMap(event =>
event.type === 'request/header' ? [event.data.reason] : [])).toEqual(['initial'])
await expect.poll(() => page.getByRole('button', { name: 'System prompt' }).count(), {
timeout: 10_000,
}).toBe(1)
expect(await page.getByRole('button', { name: 'System prompt' }).count()).toBe(1)
expect(await page.locator(
'[data-chat-flow-kind="system-prompt"][hidden="until-found"]',
).count()).toBe(0)
expect(specs.at(-1)?.prompt.length).toBeGreaterThan(4_000)
expect(sessionEvents.filter(event => (
event.type === 'assistant/chunk' && event.data.turn === TURN_COUNT
+2 -1
View File
@@ -19,7 +19,7 @@ import {
webSnapshotMode,
type WebScaffold,
} from './scaffold.ts'
import { conversationContextKey, newEnglishPage, saveFailureShot } from './support.ts'
import { conversationContextKey, expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
const MODE = webSnapshotMode()
const SESSION_ID = 'chat-long-interactions-e2e'
@@ -278,6 +278,7 @@ describe('web e2e: long Chat interaction contract', () => {
const summary1 = call1.locator('[data-sample="bash"]')
const summary2 = call2.locator('[data-sample="bash"]')
await expandOwningTurnProcess(page, call2)
expect(await summary1.getAttribute('aria-expanded')).toBe('false')
expect(await summary2.getAttribute('aria-expanded')).toBe('false')
await summary2.focus()
+12 -3
View File
@@ -20,7 +20,7 @@ import {
webSnapshotMode,
type WebScaffold,
} from './scaffold.ts'
import { newEnglishPage, saveFailureShot } from './support.ts'
import { expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
const MODE = webSnapshotMode()
const HISTORY_SESSION_ID = 'chat-scroll-history-e2e'
@@ -355,7 +355,7 @@ async function wheelUntilVisible(page: Page, selector: string, deltaY: number):
function visibleFlowAnchor(page: Page): Promise<FlowAnchor> {
return page.locator('[data-conversation-scroll]').evaluate((host) => {
const rows = [...host.querySelectorAll<HTMLElement>('[data-chat-anchor-key]')]
const rows = [...host.querySelectorAll<HTMLElement>('[data-chat-anchor-key]:not([hidden])')]
const viewport = host.getBoundingClientRect()
const composer = host.querySelector<HTMLElement>('[data-composer-seat]')
const visibleBottom = composer?.getBoundingClientRect().top ?? viewport.bottom
@@ -431,12 +431,19 @@ async function expectMarkerAboveComposer(page: Page, marker: string): Promise<vo
async function loadEarlierWithAnchor(page: Page): Promise<void> {
await wheelToHistoryStart(page)
const older = page.getByRole('button', { name: 'Load earlier', exact: true })
const loading = page.getByRole('button', { name: 'Loading…', exact: true })
await older.waitFor({ timeout: 10_000 })
const anchor = await visibleFlowAnchor(page)
const before = await loadedFlowRows(page)
await older.click()
await expect.poll(() => loadedFlowRows(page), { timeout: 30_000 }).toBeGreaterThan(before)
await expect.poll(async () => (
await loadedFlowRows(page) > before && await loading.count() === 0
), { timeout: 30_000 }).toBe(true)
await nextPaint(page)
if (await page.getByRole('button', { name: 'Load earlier', exact: true }).count() === 0) {
expect(await page.locator('[data-turn-process][aria-expanded="false"]').count()).toBeGreaterThan(0)
return
}
await expectSameFlowTop(page, anchor)
}
@@ -614,6 +621,7 @@ describe('web e2e: long Chat scroll contract', () => {
const liveRowSelector = `[data-chat-call-id="${LIVE_TOOL_CALL_ID}"] [data-sample="bash"]`
const liveRow = world.page.locator(liveRowSelector)
await expandOwningTurnProcess(world.page, liveRow)
await wheelUntilVisible(world.page, liveRowSelector, -300)
const toolAnchor = await liveRow.evaluate((row) => {
const flow = row.closest<HTMLElement>('[data-chat-anchor-key]')
@@ -762,6 +770,7 @@ describe('web e2e: long Chat scroll contract', () => {
const lastToolRow = world.page.locator(
`[data-chat-call-id="chat-scroll-${String(INPUTS_FIXTURE.turns).padStart(3, '0')}-1"] [data-sample="bash"]`,
)
await expandOwningTurnProcess(world.page, lastToolRow)
await lastToolRow.focus()
await world.page.keyboard.press('End')
await expectBottom(world.page)
+10 -4
View File
@@ -8,10 +8,10 @@ import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import {
captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
captureExpandedTurnProcessAria, compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
import { connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/code-mode-round/session.jsonl', import.meta.url))
const UI_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/code-mode-round/ui.expected.md', import.meta.url))
@@ -94,6 +94,7 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => {
// The parent run_code row wears the code variant with the model-authored
// description as its summary (the presentCall contract).
const codeRow = page.locator('[data-variant="code"]').first()
await expandOwningTurnProcess(page, codeRow)
await codeRow.waitFor({ timeout: 10_000 })
const nest = page.locator('[data-subcalls]').first()
await nest.waitFor({ timeout: 10_000 })
@@ -106,13 +107,18 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => {
const nest = page.locator('[data-subcalls]').first()
const frame = page.locator('[style*="grid-template-columns"]').first()
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
await expandOwningTurnProcess(page, nest)
await nest.locator('[data-sample="bash"]').first().click()
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
})
it.skipIf(MODE === 'record')('matches the conversation aria golden with stable anchors', async () => {
it.skipIf(MODE === 'record')('matches the expanded conversation aria golden with stable anchors', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-code-mode-aria'))
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
const snapshot = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
})
+1 -1
View File
@@ -130,7 +130,7 @@ function measureTab(page: Page): Promise<TabMetrics> {
async function showTab(page: Page, tab: 'Chat' | 'Trajectory'): Promise<void> {
await page.getByRole('tab', { name: tab, exact: true }).click()
if (tab === 'Trajectory') await page.getByLabel('Trajectory timeline').waitFor({ timeout: 30_000 })
else await page.locator('[data-conversation-scroll] [data-chat-anchor-key]').first().waitFor({ timeout: 30_000 })
else await page.locator('[data-conversation-scroll] [data-chat-anchor-key]:visible').first().waitFor({ timeout: 30_000 })
// Both measurements are taken after a paint, so a rectangle read mid-transition
// cannot be reported as a shift the cascade did not cause.
await page.evaluate(() => new Promise<void>((settle) => {
+5 -1
View File
@@ -18,7 +18,7 @@ import {
captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
import { connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/cordis-tool-round/session.jsonl', import.meta.url))
const UI_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/cordis-tool-round/ui.expected.md', import.meta.url))
@@ -162,6 +162,7 @@ describe('web e2e: Cordis tools use their owned cards', () => {
.toBeGreaterThanOrEqual(1)
const inspectRow = page.locator('[data-tool="cordis_inspect_self"]').filter({ hasText: 'Inspect' }).first()
await expandOwningTurnProcess(page, inspectRow)
await inspectRow.waitFor({ timeout: 10_000 })
// cordis_define does NOT go through the generic row: ui-cordis registers a
@@ -169,6 +170,7 @@ describe('web e2e: Cordis tools use their owned cards', () => {
// title here is the CARD's ("Cordis Plugin"), and the expanded body is the
// card's own two code sections rather than a generic args dump.
const defineRow = page.locator('[data-tool="cordis_define"]').filter({ hasText: 'Cordis Plugin' }).first()
await expandOwningTurnProcess(page, defineRow)
await defineRow.waitFor({ timeout: 10_000 })
// The whole summary row is the expand toggle (unified tool-row interaction).
await defineRow.locator('[aria-expanded]').first().click()
@@ -177,10 +179,12 @@ describe('web e2e: Cordis tools use their owned cards', () => {
await expect.poll(() => defineRow.textContent()).toContain(PACKAGE_CODE)
const runRow = page.locator('[data-tool="cordis_run"]').filter({ hasText: 'Run Cordis Plugin' }).first()
await expandOwningTurnProcess(page, runRow)
await runRow.waitFor({ timeout: 10_000 })
await expect.poll(() => runRow.textContent()).toContain('snap-')
const stopRow = page.locator('[data-tool="cordis_stop"]').filter({ hasText: 'Stop Cordis Plugin' }).first()
await expandOwningTurnProcess(page, stopRow)
await stopRow.waitFor({ timeout: 10_000 })
await expect.poll(() => stopRow.textContent()).toContain('snap-')
await expect(stopRow.getAttribute('data-state')).resolves.toBe('ok')
@@ -0,0 +1,56 @@
- tree "Sessions":
- treeitem "{{workspace}}" [expanded]:
- img
- text: {{workspace}}
- treeitem "Review deepseek-harness/deepseek-harness#314 Session actions for Review deepseek-harness/deepseek-harness#314" [selected]:
- text: Review deepseek-harness/deepseek-harness#314
- button "Session actions for Review deepseek-harness/deepseek-harness#314":
- img
---
- banner:
- navigation "Session hierarchy":
- button "Review deepseek-harness/deepseek-harness#314" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
- button "System prompt":
- img
- img
- text: System prompt
- button "Thought for a while" [expanded]:
- text: Thought for a while
- img
- button "Context injection webhook github webhook handled by review-pr-when-ready":
- img
- img
- text: Context injection webhook github webhook handled by review-pr-when-ready
- button "Context injection @deepseek-ai/dsh-system-prompt":
- img
- img
- text: Context injection @deepseek-ai/dsh-system-prompt
- paragraph: "Review complete: no actionable findings."
- button "Copy":
- img
- button "Good response":
- img
- button "Bad response":
- img
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}}
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Read Only"': Read Only
- button "Select model, current github-webhook-review-test/reply":
- text: github-webhook-review-test/reply
- img
- button "Send message" [disabled]
- text: 1 turns · 1 steps LLM {{duration}}
@@ -24,14 +24,9 @@
- img
- img
- text: System prompt
- button "Context injection webhook github webhook handled by review-pr-when-ready":
- button "Thought for a while":
- text: Thought for a while
- img
- img
- text: Context injection webhook github webhook handled by review-pr-when-ready
- button "Context injection @deepseek-ai/dsh-system-prompt":
- img
- img
- text: Context injection @deepseek-ai/dsh-system-prompt
- paragraph: "Review complete: no actionable findings."
- button "Copy":
- img
@@ -44,7 +44,11 @@
- img
- button "Decrease font size":
- img
- text: px Enter behavior while busy Busy only; Cmd/Ctrl+Enter uses the other behavior
- text: px Conversation display Controls process content in completed turns
- button "Compact":
- text: Compact
- img
- text: Enter behavior while busy Busy only; Cmd/Ctrl+Enter uses the other behavior
- button "Queue":
- text: Queue
- img
@@ -44,7 +44,11 @@
- img
- button "减小字号":
- img
- text: px 繁忙时 Enter 键行为 仅在智能体运行时生效;Cmd/Ctrl+Enter 使用另一行为
- text: px 对话显示 控制已完成轮次的过程内容
- button "Compact":
- text: Compact
- img
- text: 繁忙时 Enter 键行为 仅在智能体运行时生效;Cmd/Ctrl+Enter 使用另一行为
- button "排队发送":
- text: 排队发送
- img
@@ -0,0 +1,49 @@
- banner:
- navigation "Session hierarchy":
- button "/user-invoke-demo and confirm the fixtur" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
- button "System prompt":
- img
- img
- text: System prompt
- text: /user-invoke-demo and confirm the fixture wiring {{clock}}
- button "Copy":
- img
- button "Thought for a while" [expanded]:
- text: Thought for a while
- img
- button "Context injection @deepseek-ai/dsh-system-prompt":
- img
- img
- text: Context injection @deepseek-ai/dsh-system-prompt
- button "Context injection user-invoke-demo":
- img
- img
- text: Context injection user-invoke-demo
- paragraph: USER_INVOKE_REPLY acknowledged; following the injected skill.
- button "Copy":
- img
- button "Good response":
- img
- button "Bad response":
- img
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img
- button "0% of context used"
- button "Send message" [disabled]
- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 256 tok · Output 16 tok
@@ -16,14 +16,9 @@
- text: /user-invoke-demo and confirm the fixture wiring {{clock}}
- button "Copy":
- img
- button "Context injection @deepseek-ai/dsh-system-prompt":
- button "Thought for a while":
- text: Thought for a while
- img
- img
- text: Context injection @deepseek-ai/dsh-system-prompt
- button "Context injection user-invoke-demo":
- img
- img
- text: Context injection user-invoke-demo
- paragraph: USER_INVOKE_REPLY acknowledged; following the injected skill.
- button "Copy":
- img
@@ -0,0 +1,59 @@
- banner:
- navigation "Session hierarchy":
- button "Use the ask_user_question tool to" [disabled]
- img
- text: Standard mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
- button "System prompt":
- img
- img
- text: System prompt
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
- button "Copy":
- img
- button "1 tool call" [expanded]:
- text: 1 tool call
- img
- button "Context injection @deepseek-ai/dsh-system-prompt":
- img
- img
- text: Context injection @deepseek-ai/dsh-system-prompt
- button "Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that.":
- img
- img
- text: Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that.
- button "Ask question 1/1 answered":
- img
- img
- text: Ask question 1/1 answered
- text: "Interjection: include the word BANANA in your final reply. {{clock}}"
- button "Copy":
- img
- text: "Interjection: include the word ORANGE in your final reply. {{clock}}"
- button "Copy":
- img
- paragraph: "Got it: BANANA and ORANGE."
- button "Copy":
- img
- button "Good response":
- img
- button "Bad response":
- img
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img
- button "0% of context used"
- button "Send message" [disabled]
- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 20 tok · Output 20 tok
@@ -16,18 +16,9 @@
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
- button "Copy":
- img
- button "Context injection @deepseek-ai/dsh-system-prompt":
- button "1 tool call":
- text: 1 tool call
- img
- img
- text: Context injection @deepseek-ai/dsh-system-prompt
- button "Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that.":
- img
- img
- text: Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that.
- button "Ask question 1/1 answered":
- img
- img
- text: Ask question 1/1 answered
- text: "Interjection: include the word BANANA in your final reply. {{clock}}"
- button "Copy":
- img
+12 -2
View File
@@ -15,7 +15,8 @@ import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria,
compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
@@ -23,6 +24,7 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/feedback-command', import.meta.url))
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
const ACK_EXPECTED = join(SNAPSHOT_DIR, 'ack.expected.md')
const ACK_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'ack-expanded.expected.md')
const MODE = webSnapshotMode()
// Discard port: loopback listener never binds, so FULL telemetry discloses
// the shipped default policy without any record reaching a collector.
@@ -91,12 +93,20 @@ describe('web e2e: /feedback command acknowledgement', () => {
expect(await page.getByText(/Session sharing is enabled/).count()).toBe(1)
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(ACK_EXPECTED, snapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(ACK_EXPANDED_EXPECTED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'ack.expected.md'])
await assertFixtureInventory(SNAPSHOT_DIR, [
'session.jsonl', 'ack.expected.md', 'ack-expanded.expected.md',
])
})
})
+10 -2
View File
@@ -16,7 +16,8 @@ import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria,
compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
@@ -27,6 +28,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/feedback-rele
// manifest's `session.source`) instead of recording a duplicate.
const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/feedback-command/session.jsonl', import.meta.url))
const ACK_EXPECTED = join(SNAPSHOT_DIR, 'ack.expected.md')
const ACK_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'ack-expanded.expected.md')
const MODE = webSnapshotMode()
const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.'
@@ -115,6 +117,12 @@ describe('web e2e: feedback-gated release under the shipped default mode', () =>
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(ACK_EXPECTED, snapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(ACK_EXPANDED_EXPECTED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
@@ -132,6 +140,6 @@ describe('web e2e: feedback-gated release under the shipped default mode', () =>
}, 60_000)
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, ['ack.expected.md'])
await assertFixtureInventory(SNAPSHOT_DIR, ['ack.expected.md', 'ack-expanded.expected.md'])
})
})
+10
View File
@@ -11,6 +11,7 @@ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-webhook'
import {
captureExpandedTurnProcessAria,
captureStableAria,
compareOrRefreshGolden,
launchWebScaffold,
@@ -23,6 +24,9 @@ import { saveFailureShot } from './support.ts'
const MODE = webSnapshotMode()
const OVERLAY = fileURLToPath(new URL('../../cli/config/examples/github-review/cordis.yml', import.meta.url))
const EXPECTED = fileURLToPath(new URL('./expected/github-ready-review/conversation.expected.md', import.meta.url))
const EXPANDED_EXPECTED = fileURLToPath(
new URL('./expected/github-ready-review/conversation-expanded.expected.md', import.meta.url),
)
const PROVIDER = 'github-webhook-review-test'
const MODEL = 'reply'
const SECRET = 'github-webhook-review-secret'
@@ -157,6 +161,12 @@ describe.skipIf(MODE === 'record')('web e2e: GitHub ready-for-review', () => {
const tree = await captureStableAria(page, '[role="tree"][aria-label="Sessions"]', scaffold.workspaceCwd)
const conversation = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(EXPECTED, `${tree}\n\n---\n\n${conversation}`, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(EXPANDED_EXPECTED, `${tree}\n\n---\n\n${expanded}`, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
+17 -6
View File
@@ -11,7 +11,7 @@ import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-goal'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
@@ -20,6 +20,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/goal-multi-tu
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
const OVERRIDE = join(SNAPSHOT_DIR, 'replay.override.json')
const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
const UI_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'ui-expanded.expected.md')
const MODE = webSnapshotMode()
const PROMPT = '做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的'
@@ -29,7 +30,7 @@ const PACKAGE_FILES: Readonly<Record<string, string>> = {
'packages/client/ui-conversation/README.md': '# UI conversation\n',
'packages/client/ui-conversation/package.json': '{"name":"@deepseek-ai/dsh-client-ui-conversation"}\n',
'packages/client/ui-conversation/src/client.ts': 'export {}\n',
'packages/client/ui-conversation/tests/chat-view.client.spec.tsx': 'export {}\n',
'packages/client/ui-chat/tests/chat-view.client.spec.tsx': 'export {}\n',
'packages/context/session-reference/README.md': '# Session reference\n',
'packages/context/session-reference/package.json': '{"name":"@deepseek-ai/dsh-session-reference"}\n',
'packages/context/session-reference/src/index.ts': 'export {}\n',
@@ -153,9 +154,11 @@ describe('web e2e: Goal keeps one assistant action row per completed turn', () =
expect(goalRounds(sessionEvents)).toEqual([1, 2])
expect(sessionEvents.flatMap(event =>
event.type === 'request/header' ? [event.data.reason] : [])).toEqual(['initial', 'series'])
await expect.poll(() => page.getByRole('button', { name: 'System prompt' }).count(), {
timeout: 15_000,
}).toBe(2)
await expect.poll(() => page.locator('[data-turn-process]').count(), { timeout: 15_000 }).toBe(2)
expect(await page.getByRole('button', { name: 'System prompt' }).count()).toBe(2)
expect(await page.locator(
'[data-chat-flow-kind="system-prompt"][hidden="until-found"]',
).count()).toBe(0)
const branchButtons = page.getByRole('button', { name: 'Branch into a new conversation' })
await expect.poll(() => branchButtons.count(), { timeout: 15_000 }).toBe(2)
expect(await branchButtons.evaluateAll(buttons => buttons.map(button => button.getAttribute('aria-disabled'))))
@@ -163,11 +166,19 @@ describe('web e2e: Goal keeps one assistant action row per completed turn', () =
await branchButtons.last().focus()
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold!.workspaceCwd,
)
await compareOrRefreshGolden(UI_EXPANDED_EXPECTED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 140_000)
it.skipIf(MODE === 'record')('keeps a closed fixture inventory', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, ['replay.override.json', 'session.jsonl', 'ui.expected.md'])
await assertFixtureInventory(SNAPSHOT_DIR, [
'replay.override.json', 'session.jsonl', 'ui.expected.md', 'ui-expanded.expected.md',
])
})
})
+12 -2
View File
@@ -18,7 +18,8 @@ import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import {
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureExpandedTurnProcessAria,
captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot, writeComposerDraft } from './support.ts'
@@ -33,6 +34,7 @@ const PLAN_ACTIVE_EXPECTED = join(SNAPSHOT_DIR, 'plan-active.expected.md')
// Post-reload golden: the same settled conversation rebuilt purely from
// persistence + history — byte-equal rendering is exactly the recovery claim.
const RELOADED_EXPECTED = join(SNAPSHOT_DIR, 'reloaded.expected.md')
const RELOADED_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'reloaded-expanded.expected.md')
const MODE = webSnapshotMode()
const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.'
@@ -239,6 +241,12 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
// must render the same settled transcript the live turn produced.
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(RELOADED_EXPECTED, snapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(RELOADED_EXPANDED_EXPECTED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
}, 90_000)
@@ -277,7 +285,9 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
expect(tripwire.warnings).toEqual([])
await assertFixtureInventory(SNAPSHOT_DIR, [
'session.jsonl', 'replay.override.json', 'command-menu.expected.md', 'command-menu-fuzzy.expected.md', 'hero.expected.md', 'plan-active.expected.md', 'reloaded.expected.md',
'session.jsonl', 'replay.override.json', 'command-menu.expected.md',
'command-menu-fuzzy.expected.md', 'hero.expected.md', 'plan-active.expected.md',
'reloaded.expected.md', 'reloaded-expanded.expected.md',
])
})
})
+19 -3
View File
@@ -23,7 +23,8 @@ import { deriveReplayScript, parseSessionLog } from '@deepseek-ai/dsh-llm-replay
import type { ReplayEntry, ReplayOverrideDoc } from '@deepseek-ai/dsh-llm-replay'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria,
compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
@@ -34,10 +35,12 @@ const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
// state, and the other four capture what remains after cancel, after a
// non-retryable failure, after retry recovery, and after retry exhaustion.
const CANCEL_EXPECTED = join(SNAPSHOT_DIR, 'cancel.expected.md')
const CANCEL_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'cancel-expanded.expected.md')
const LOADING_EXPECTED = join(SNAPSHOT_DIR, 'loading.expected.md')
const RUNNING_DRAFT_EXPECTED = join(SNAPSHOT_DIR, 'running-draft.expected.md')
const ERROR_EXPECTED = join(SNAPSHOT_DIR, 'error-auth.expected.md')
const RETRY_EXPECTED = join(SNAPSHOT_DIR, 'retry.expected.md')
const RETRY_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'retry-expanded.expected.md')
const RETRY_EXHAUSTED_EXPECTED = join(SNAPSHOT_DIR, 'retry-exhausted.expected.md')
const MODE = webSnapshotMode()
const AUTH_PROVIDER_MESSAGE = 'Authentication Fails, Your api key: sk-preview-secret is invalid'
@@ -185,6 +188,12 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
// partial ('partial' is the hang entry's replayed prefix) and no more.
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
await compareOrRefreshGolden(CANCEL_EXPECTED, snapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold!.workspaceCwd,
)
await compareOrRefreshGolden(CANCEL_EXPANDED_EXPECTED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 120_000)
@@ -268,6 +277,12 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
// while the settled retry row remains as durable recovery context.
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
await compareOrRefreshGolden(RETRY_EXPECTED, snapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold!.workspaceCwd,
)
await compareOrRefreshGolden(RETRY_EXPANDED_EXPECTED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 120_000)
@@ -307,8 +322,9 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, [
'session.jsonl', 'cancel.expected.md', 'loading.expected.md', 'running-draft.expected.md',
'error-auth.expected.md', 'retry.expected.md', 'retry-exhausted.expected.md',
'session.jsonl', 'cancel.expected.md', 'cancel-expanded.expected.md',
'loading.expected.md', 'running-draft.expected.md', 'error-auth.expected.md',
'retry.expected.md', 'retry-expanded.expected.md', 'retry-exhausted.expected.md',
])
})
})
@@ -154,6 +154,12 @@ describe('minimal agent preset', () => {
await sessionRow.click()
await page.getByText('MINIMAL_PRESET_REQUEST_OK', { exact: true }).waitFor({ timeout: 15_000 })
const process = page.locator('[data-turn-process]')
await process.waitFor({ timeout: 15_000 })
await expect.poll(() => process.getAttribute('aria-expanded')).toBe('false')
await process.click()
await expect.poll(() => process.getAttribute('aria-expanded')).toBe('true')
const row = page.locator('[data-sample="bash"]').first()
await row.waitFor({ timeout: 15_000 })
await expect.poll(() => row.getAttribute('aria-expanded')).toBe('false')
+3 -1
View File
@@ -19,7 +19,7 @@ import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { newEnglishPage, saveFailureShot } from './support.ts'
import { expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/navigation-panes', import.meta.url))
const SEED = join(SNAPSHOT_DIR, 'session.jsonl')
@@ -390,6 +390,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-details'))
await ensureSeedOpen(page)
const bashRow = page.locator('[data-sample="bash"]').first()
await expandOwningTurnProcess(page, bashRow)
await bashRow.waitFor({ timeout: 15_000 })
const frame = page.locator('[style*="grid-template-columns"]').first()
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
@@ -425,6 +426,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
// Expanded, the recorded command's own output sits in the message flow,
// derived from the logged call/result presentations alone.
const bashRow = page.locator('[data-sample="bash"]').first()
await expandOwningTurnProcess(page, bashRow)
await bashRow.waitFor({ timeout: 15_000 })
if (await bashRow.getAttribute('aria-expanded') !== 'true') await bashRow.click()
const card = page.locator('[data-sample="bash"] ~ div [data-terminal]').first()
+11 -2
View File
@@ -15,7 +15,8 @@ import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria,
compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
@@ -27,6 +28,7 @@ const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
const REVIEW_EXPECTED = join(SNAPSHOT_DIR, 'review.expected.md')
const SIDEBAR_EXPECTED = join(SNAPSHOT_DIR, 'sidebar.expected.md')
const APPROVED_EXPECTED = join(SNAPSHOT_DIR, 'approved.expected.md')
const APPROVED_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'approved-expanded.expected.md')
const MODE = webSnapshotMode()
// One command line: /plan enters plan mode and submits the rest as the turn's
@@ -111,13 +113,20 @@ describe('web e2e: plan review takeover round trip', () => {
await expect.poll(() => page.locator('[data-composer-input]').first().isEnabled(), { timeout: 10_000 }).toBe(true)
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(APPROVED_EXPECTED, snapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(APPROVED_EXPANDED_EXPECTED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 200_000)
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, [
'session.jsonl', 'review.expected.md', 'sidebar.expected.md', 'approved.expected.md',
'session.jsonl', 'review.expected.md', 'sidebar.expected.md',
'approved.expected.md', 'approved-expanded.expected.md',
])
})
})
+13 -5
View File
@@ -19,7 +19,9 @@ import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
import {
connectFreshWorkspace, expandTurnProcesses, newEnglishPage, saveFailureShot,
} from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/question-composer', import.meta.url))
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
@@ -30,6 +32,7 @@ const COMPOSED_EXPECTED = join(SNAPSHOT_DIR, 'composed.expected.md')
// 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 ANSWERED_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'answered-expanded.expected.md')
const MODE = webSnapshotMode()
const CANCELLED_SEED_ID = 'ask-question-cancelled-row-web-e2e'
@@ -289,16 +292,20 @@ describe('web e2e: resident question composer round trip', () => {
expect(await page.locator('[data-question-key]').count()).toBe(0)
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 (expanded readable answers + DONE), composer takeover gone.
// The default golden pins Compact mode before process disclosure.
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(ANSWERED_EXPECTED, snapshot, MODE)
// Keep the ask_user_question card's readable answer in the expanded golden
// even though Compact mode hides the process by default.
await expandTurnProcesses(page)
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)
const expanded = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(ANSWERED_EXPANDED_EXPECTED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 200_000)
@@ -410,6 +417,7 @@ describe.skipIf(MODE === 'record')('web e2e: cancelled question transcript', ()
'composed.expected.md',
'answered.expected.md',
'cancelled.expected.md',
'answered-expanded.expected.md',
])
})
})
+12 -2
View File
@@ -13,7 +13,7 @@ import { afterEach, describe, expect, it, onTestFailed } from 'vitest'
import { deriveReplayScript, parseSessionLog, type ReplayEntry } from '@deepseek-ai/dsh-llm-replay'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
@@ -24,6 +24,7 @@ const COLLAPSED_EXPECTED = join(SNAPSHOT_DIR, 'collapsed.expected.md')
const EDITING_EXPECTED = join(SNAPSHOT_DIR, 'editing.expected.md')
const LAYOUT_EXPECTED = join(SNAPSHOT_DIR, 'layout.expected.md')
const PRESERVED_EXPECTED = join(SNAPSHOT_DIR, 'preserved.expected.md')
const PRESERVED_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'preserved-expanded.expected.md')
const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
const MODE = webSnapshotMode()
@@ -168,6 +169,12 @@ describe('web e2e: queue row actions', () => {
const preservedSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(PRESERVED_EXPECTED, preservedSnapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(PRESERVED_EXPANDED_EXPECTED, expanded, MODE)
const settled = scaffold.whenTurnSettled()
await input.fill(WAKE)
@@ -272,7 +279,10 @@ describe('web e2e: queue row actions', () => {
it.skipIf(MODE === 'record')('keeps its snapshot inventory closed', async () => {
await assertFixtureInventory(
SNAPSHOT_DIR,
['collapsed.expected.md', 'editing.expected.md', 'layout.expected.md', 'preserved.expected.md', 'ui.expected.md'],
[
'collapsed.expected.md', 'editing.expected.md', 'layout.expected.md',
'preserved.expected.md', 'preserved-expanded.expected.md', 'ui.expected.md',
],
)
})
})
+18 -3
View File
@@ -17,15 +17,21 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { ToolCallId } from '@deepseek-ai/dsh-llm'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria,
compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, REPO_ROOT, saveFailureShot } from './support.ts'
import {
connectFreshWorkspace, expandTurnProcesses, newEnglishPage, REPO_ROOT, saveFailureShot,
} from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/fresh-round-trip', import.meta.url))
const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/fresh-round-trip/session.jsonl', import.meta.url))
const UI_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/fresh-round-trip/ui.expected.md', import.meta.url))
const ECHO_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/fresh-round-trip/submission-echo.expected.md', import.meta.url))
const UI_EXPANDED_EXPECTED = fileURLToPath(
new URL('../../../snapshots/web/fresh-round-trip/ui-expanded.expected.md', import.meta.url),
)
const WEB_CONTEXT_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/fresh-round-trip/web-context.expected.md', import.meta.url))
const MODE = webSnapshotMode()
@@ -163,10 +169,17 @@ describe('web e2e: fresh round trip through the real assembly', () => {
}).waitFor({ timeout: 10_000 })
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(UI_EXPANDED_EXPECTED, expanded, MODE)
})
it.skipIf(MODE === 'record')('renders the system prompt as a collapsed expandable disclosure', async () => {
it.skipIf(MODE === 'record')('renders the system prompt disclosure inside the expanded Turn process', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-round-trip-system-prompt'))
await expandTurnProcesses(page)
const disclosure = page.getByRole('button', { name: 'System prompt', exact: true })
const body = page.locator('[data-system-prompt-body]')
await expect.poll(() => disclosure.count(), { timeout: 10_000 }).toBe(1)
@@ -190,6 +203,7 @@ describe('web e2e: fresh round trip through the real assembly', () => {
// tier pins the same gesture against FixtureApiClient; this one runs on
// follow-stream-fed state). Runs after the golden capture so the committed
// aria surface stays the untouched settled state.
await expandTurnProcesses(page)
const think = page.getByRole('button', { name: /^Think/ }).first()
expect(await think.getAttribute('aria-expanded')).toBe('false')
await think.click()
@@ -208,6 +222,7 @@ describe('web e2e: fresh round trip through the real assembly', () => {
'tool-schemas.expected.json',
'web-context.expected.md',
'ui.expected.md',
'ui-expanded.expected.md',
])
})
})
+33
View File
@@ -1169,6 +1169,39 @@ export async function captureStableAria(
return previous
}
/**
* Capture a stable aria snapshot with every eligible Turn process expanded,
* then restore the controls that were closed before the capture.
* @param page - the page under test.
* @param selector - the region locator selector.
* @param workspaceCwd - normalization input.
* @returns the stable normalized expanded snapshot.
*/
export async function captureExpandedTurnProcessAria(
page: Page,
selector: string,
workspaceCwd: string,
): Promise<string> {
const controls = page.locator('[data-turn-process]')
const count = await controls.count()
expect(count).toBeGreaterThan(0)
const opened: number[] = []
for (let index = 0; index < count; index++) {
const control = controls.nth(index)
if (!await control.isVisible() || await control.getAttribute('aria-expanded') === 'true') continue
await control.click()
opened.push(index)
}
try {
return await captureStableAria(page, selector, workspaceCwd)
} finally {
for (const index of opened.reverse()) {
const control = controls.nth(index)
if (await control.getAttribute('aria-expanded') === 'true') await control.click()
}
}
}
/**
* Compare a normalized golden, or rewrite it under refresh. Refresh is the
* ONLY writer: a missing golden in replay mode fails with the healing command
+25 -3
View File
@@ -22,15 +22,19 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { TokenMeter } from '@deepseek-ai/dsh-token-meter'
import { join } from 'node:path'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria,
compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, parseSeedFixture, realizeSeedFixture, recordFixture, renderSeedFixture, seedSession, watchConsole,
webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { newEnglishPage, saveFailureShot } from './support.ts'
import { expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/seeded-history', import.meta.url))
const SEED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/session.jsonl', import.meta.url))
const UI_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/ui.expected.md', import.meta.url))
const UI_EXPANDED_EXPECTED = fileURLToPath(
new URL('../../../snapshots/web/seeded-history/ui-expanded.expected.md', import.meta.url),
)
// Command-row goldens over the same conversation after direct host commands.
const COMMAND_ROW_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/command-row.expected.md', import.meta.url))
const FEEDBACK_ROW_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/feedback-row.expected.md', import.meta.url))
@@ -278,6 +282,14 @@ describe('web e2e: seeded history renders through cold resume', () => {
await expect.poll(() => page.getByText(/^Compacted \d+ history items \(~\d+ tokens\)$/).count(), {
timeout: 10_000,
}).toBe(1)
const process = page.locator('[data-turn-process="1"]')
await process.waitFor({ state: 'visible', timeout: 10_000 })
expect(await process.getAttribute('aria-expanded')).toBe('false')
const processBottom = await process.evaluate(element => element.getBoundingClientRect().bottom)
const answerTop = await page.getByText('DONE', { exact: true }).evaluate(element =>
element.getBoundingClientRect().top)
// Collapsed control row keeps its own 8px margin plus the 8px flow gap.
expect(answerTop).toBe(processBottom + 16)
expect(await page.getByText('Context compacted', { exact: true }).count()).toBe(0)
// Tool cards render from logged tool/call + tool/result alone (views are
// host-recomputed per page; the generic card is the documented default).
@@ -331,6 +343,12 @@ describe('web e2e: seeded history renders through cold resume', () => {
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
.split(SEED_ID).join('{{seededId}}')
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
const expanded = (await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)).split(SEED_ID).join('{{seededId}}')
await compareOrRefreshGolden(UI_EXPANDED_EXPECTED, expanded, MODE)
})
it.skipIf(MODE === 'record')('matches the Figma context disclosure geometry', async () => {
@@ -395,6 +413,7 @@ describe('web e2e: seeded history renders through cold resume', () => {
// file links (not expand-in-place / not details). Runs after the golden
// capture; still zero model calls.
const fileLink = page.locator('[data-variant="read"] button').first()
await expandOwningTurnProcess(page, fileLink)
await fileLink.waitFor({ timeout: 10_000 })
const frame = page.locator('[style*="grid-template-columns"]').first()
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
@@ -552,6 +571,9 @@ describe('web e2e: seeded history renders through cold resume', () => {
// stream would have failed the turn loudly. Cleanliness pins the wire.
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
await assertFixtureInventory(SNAPSHOT_DIR, ['command-row.expected.md', 'feedback-row.expected.md', 'file-open-failure.expected.md', 'session.jsonl', 'ui.expected.md'])
await assertFixtureInventory(SNAPSHOT_DIR, [
'command-row.expected.md', 'feedback-row.expected.md', 'file-open-failure.expected.md',
'session.jsonl', 'ui.expected.md', 'ui-expanded.expected.md',
])
})
})
+30
View File
@@ -408,6 +408,36 @@ describe('web e2e: settings modal and General preferences', () => {
expect(tripwire.pageErrors).toEqual([])
}, 90_000)
it('persists the completed-Turn transcript mode across reload', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-transcript-view'))
await page.getByRole('button', { name: '设置', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByText('对话显示', { exact: true }).waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: 'Compact', exact: true }).click()
await page.getByRole('menuitem', { name: 'Normal', exact: true }).click()
await dialog.getByRole('button', { name: 'Normal', exact: true }).waitFor({ timeout: 10_000 })
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
.toMatch(/ui-chat:\n\s+transcriptView: normal/)
await page.keyboard.press('Escape')
const warningStart = tripwire.warnings.length
await page.reload({ waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
acknowledgeReloadConnectionLoss(tripwire, warningStart)
await page.getByRole('button', { name: '设置', exact: true }).click()
const reloaded = page.getByRole('dialog', { name: '设置' })
await reloaded.getByRole('button', { name: 'Normal', exact: true }).waitFor({ timeout: 10_000 })
await reloaded.getByRole('button', { name: 'Normal', exact: true }).click()
await page.getByRole('menuitem', { name: 'Compact', exact: true }).click()
await reloaded.getByRole('button', { name: 'Compact', exact: true }).waitFor({ timeout: 10_000 })
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
.toMatch(/ui-chat:\n\s+transcriptView: compact/)
await page.keyboard.press('Escape')
expect(tripwire.pageErrors).toEqual([])
}, 90_000)
it('persists the busy-state Enter behavior across reload and a distinct port', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-enter-behavior'))
await page.getByRole('button', { name: '设置', exact: true }).click()
+4 -2
View File
@@ -10,7 +10,7 @@ import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { newEnglishPage, saveFailureShot } from './support.ts'
import { expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
const FIXTURE = fileURLToPath(new URL('../../../snapshots/session/skill-load/session.jsonl', import.meta.url))
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/skill-tool-row', import.meta.url))
@@ -42,7 +42,9 @@ describe.skipIf(MODE === 'record')('web e2e: dedicated Skill tool row', () => {
const sessionRow = page.locator('[role="treeitem"]').nth(1)
await sessionRow.waitFor({ timeout: 10_000 })
await sessionRow.click()
await page.locator('[data-tool="skill"]').waitFor({ timeout: 15_000 })
const skillRow = page.locator('[data-tool="skill"]')
await expandOwningTurnProcess(page, skillRow)
await skillRow.waitFor({ timeout: 15_000 })
}, 120_000)
afterAll(async () => {
+20 -9
View File
@@ -14,6 +14,7 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { ReplayOverrideDoc } from '@deepseek-ai/dsh-llm-replay'
import {
assertFixtureInventory,
captureExpandedTurnProcessAria,
captureStableAria,
compareOrRefreshGolden,
launchWebScaffold,
@@ -21,10 +22,11 @@ import {
webSnapshotMode,
type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
import { connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/skill-user-invoke', import.meta.url))
const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
const UI_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'ui-expanded.expected.md')
const MODE = webSnapshotMode()
const SKILL_NAME = 'user-invoke-demo'
@@ -120,10 +122,16 @@ describe.skipIf(MODE === 'record')('web e2e: user-explicit skill invocation thro
expect(await bubble.textContent()).toBe(`/${SKILL_NAME}`)
// The rendered body arrives as a context-injection row named after the
// skill; expanding it reveals the canonical <skill_content> block, and
// the user's text is NOT folded into it.
// skill. Context plus the final answer contributes no summary count, so
// the Turn uses the fallback title while the row's own disclosure remains usable.
const injectionFlow = page.locator('[data-chat-flow-kind="context"]').filter({ hasText: SKILL_NAME })
await injectionFlow.waitFor({ state: 'attached', timeout: 15_000 })
await page.getByText('USER_INVOKE_REPLY', { exact: false }).first().waitFor({ timeout: 20_000 })
await settled
const process = page.getByRole('button', { name: 'Thought for a while', exact: true })
await process.waitFor({ state: 'visible', timeout: 10_000 })
await expandOwningTurnProcess(page, injectionFlow)
const injectionRow = page.getByRole('button', { name: `Context injection ${SKILL_NAME}` })
await injectionRow.waitFor({ timeout: 15_000 })
await injectionRow.click()
const injectionBody = page
.locator('[data-context-injection-body]')
@@ -133,18 +141,21 @@ describe.skipIf(MODE === 'record')('web e2e: user-explicit skill invocation thro
expect(injected).toContain('Reply with the fixture acknowledgement line.')
expect(injected).not.toContain(ARGS_TEXT)
await injectionRow.click()
// The injection started a turn; the replay adapter answers it.
await page.getByText('USER_INVOKE_REPLY', { exact: false }).first().waitFor({ timeout: 20_000 })
await settled
await process.click()
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(UI_EXPANDED_EXPECTED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
it('keeps its snapshot inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md', 'ui-expanded.expected.md'])
})
})
+21 -3
View File
@@ -11,7 +11,8 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria,
compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
@@ -24,6 +25,7 @@ const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
// from user/message beside the reply that obeys it.
const MID_EXPECTED = join(SNAPSHOT_DIR, 'mid-steer.expected.md')
const SETTLED_EXPECTED = join(SNAPSHOT_DIR, 'settled.expected.md')
const SETTLED_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'settled-expanded.expected.md')
const MODE = webSnapshotMode()
// The question composer replaces the textarea, so fill → Queue row → Steer
// starts only after request/context and must finish before the first replay
@@ -43,6 +45,7 @@ const STEER_ALL_FIXTURE = join(STEER_ALL_DIR, 'session.jsonl')
const STEER_ALL_OVERRIDE = join(STEER_ALL_DIR, 'replay.override.json')
const STEER_ALL_MID = join(STEER_ALL_DIR, 'mid-steer.expected.md')
const STEER_ALL_SETTLED = join(STEER_ALL_DIR, 'settled.expected.md')
const STEER_ALL_SETTLED_EXPANDED = join(STEER_ALL_DIR, 'settled-expanded.expected.md')
const STEER_ONE = 'Interjection: include the word BANANA in your final reply.'
const STEER_TWO = 'Interjection: include the word ORANGE in your final reply.'
@@ -171,12 +174,20 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
// obeying reply, composer takeover gone.
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(SETTLED_EXPECTED, snapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(SETTLED_EXPANDED_EXPECTED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 200_000)
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'mid-steer.expected.md', 'settled.expected.md'])
await assertFixtureInventory(SNAPSHOT_DIR, [
'session.jsonl', 'mid-steer.expected.md', 'settled.expected.md', 'settled-expanded.expected.md',
])
})
})
@@ -392,13 +403,20 @@ describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
expect(await page.locator('[data-pending-steering]').count()).toBe(0)
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(STEER_ALL_SETTLED, snapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(STEER_ALL_SETTLED_EXPANDED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 200_000)
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(STEER_ALL_DIR, [
'replay.override.json', 'mid-steer.expected.md', 'settled.expected.md',
'replay.override.json', 'mid-steer.expected.md',
'settled.expected.md', 'settled-expanded.expected.md',
])
})
})
+12 -2
View File
@@ -11,7 +11,8 @@ import {
import type {} from '@deepseek-ai/dsh-agent'
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import {
acknowledgeReloadConnectionLoss, captureStableAria, compareOrRefreshGolden,
acknowledgeReloadConnectionLoss, captureExpandedTurnProcessAria, captureStableAria,
compareOrRefreshGolden,
launchWebScaffold, watchConsole,
webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
@@ -19,6 +20,9 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor
const BASE_FIXTURE = fileURLToPath(new URL('../../../snapshots/web/live-interactions/session.jsonl', import.meta.url))
const AVAILABLE_CHILD_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/subagent-conversation/ui.expected.md', import.meta.url))
const AVAILABLE_CHILD_EXPANDED_EXPECTED = fileURLToPath(
new URL('../../../snapshots/web/subagent-conversation/ui-expanded.expected.md', import.meta.url),
)
const TREE_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/subagent-conversation/tree.expected.md', import.meta.url))
const BRANCHLESS_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/subagent-conversation/branchless.expected.md', import.meta.url))
const STALE_CATALOG_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/subagent-conversation/stale-catalog.expected.md', import.meta.url))
@@ -243,7 +247,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
const warningStart = tripwire.warnings.length
await page.reload({ waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
const catalogButton = page.getByRole('button', { name: /subagents/ })
const catalogButton = page.getByRole('button', { name: '3 subagents', exact: true })
await catalogButton.waitFor({ timeout: 15_000 })
await catalogButton.hover()
const catalogTree = page.getByRole('tree', { name: 'Subagent sessions' })
@@ -434,6 +438,12 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-aria'))
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(AVAILABLE_CHILD_EXPECTED, snapshot, MODE)
const expanded = await captureExpandedTurnProcessAria(
page,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(AVAILABLE_CHILD_EXPANDED_EXPECTED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
})
+30 -1
View File
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync } from 'node:fs'
import { createServer } from 'node:net'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Browser, Page } from 'playwright'
import type { Browser, Locator, Page } from 'playwright'
/** The built page under test; `pnpm run test:web` rebuilds it before running. */
export const DIST_INDEX = fileURLToPath(new URL('../dist/index.html', import.meta.url))
@@ -31,6 +31,35 @@ export async function newEnglishPage(browser: Browser, height = 1000): Promise<P
return await browser.newPage({ viewport: { width: 1680, height }, locale: 'en-US' })
}
/**
* Expand every currently eligible Turn-process group so a Tool-focused
* scenario can exercise the original row contract beneath product-default
* compact Chat presentation.
* @param page - page containing the Chat view.
*/
export async function expandTurnProcesses(page: Page): Promise<void> {
const controls = page.locator('[data-turn-process]')
await controls.first().waitFor({ state: 'visible', timeout: 10_000 })
const count = await controls.count()
for (let index = 0; index < count; index++) {
const control = controls.nth(index)
if (await control.getAttribute('aria-expanded') !== 'true') await control.click()
}
}
/**
* Expand the Turn-process group containing one possibly hidden descendant.
* @param page - page containing the Chat view.
* @param target - descendant whose owning Turn process should open.
*/
export async function expandOwningTurnProcess(page: Page, target: Locator): Promise<void> {
const turn = await target.evaluate(element => element.closest<HTMLElement>('[data-chat-turn]')?.dataset.chatTurn)
if (turn === undefined || await target.isVisible()) return
const control = page.locator(`[data-turn-process="${turn}"]`)
await control.waitFor({ state: 'visible', timeout: 10_000 })
if (await control.getAttribute('aria-expanded') !== 'true') await control.click()
}
/** Fail loud on a stale checkout instead of testing yesterday's bundle. */
export function requireDist(): void {
if (!existsSync(DIST_INDEX)) {
+94 -7
View File
@@ -24,10 +24,12 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/turn-tail-actions', import.meta.url))
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
// Two goldens for the same message: parked mid-turn, then settled.
// Three goldens for the same message: parked mid-turn, aborted, and completed.
const RUNNING_EXPECTED = join(SNAPSHOT_DIR, 'running.expected.md')
const SETTLED_EXPECTED = join(SNAPSHOT_DIR, 'settled.expected.md')
const USAGE_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'usage-expanded.expected.md')
const COMPLETED_EXPECTED = join(SNAPSHOT_DIR, 'completed.expected.md')
const FOCUSED_EXPECTED = join(SNAPSHOT_DIR, 'focused.expected.md')
const MODE = webSnapshotMode()
// The recording must carry text in the SAME assistant message as the tool
@@ -60,7 +62,10 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
})
/** Boot scaffold + page, materializing the sidecar before the replay row installs. */
async function launch(buildOverride?: (sidecarHome: string) => ReplayOverrideDoc): Promise<void> {
async function launch(
buildOverride?: (sidecarHome: string) => ReplayOverrideDoc,
paceMs?: number,
): Promise<void> {
sessionEvents = []
let overridePath: string | undefined
if (buildOverride !== undefined) {
@@ -75,6 +80,7 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
replayFixture: FIXTURE,
...(overridePath === undefined ? {} : { replayOverride: overridePath }),
compareReplaySession: overridePath === undefined,
...(paceMs === undefined ? {} : { paceMs }),
},
)
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
@@ -122,13 +128,13 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-turn-tail-actions'))
// The barrier is armed before the park and awaited only after the stop
// click, so its budget must cover the whole parked phase: marker poll,
// three UI polls, and two captures with their stability windows. The
// live-state polls, and two captures with their stability windows. The
// replay default (30s) leaves no headroom on a slow runner.
const { settled } = await sendPrompt(120_000)
// The marker IS the synchronization: the second call is provably parked,
// so the first step's message and tool result are already durable.
await expect.poll(() => existsSync(marker), { timeout: 20_000 }).toBe(true)
await expect.poll(() => page.getByText(NARRATION, { exact: true }).count(), { timeout: 10_000 }).toBe(1)
expect(await page.locator('[data-turn-process]').count()).toBe(0)
await expect.poll(
() => page.getByRole('status').filter({ hasText: 'Deep diving...' }).isVisible(),
{ timeout: 10_000 },
@@ -148,6 +154,7 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
await page.getByRole('button', { name: 'Stop generating' }).click()
await settled
expect(sessionEvents.filter(e => e.type === 'turn/end').map(e => e.data.reason.kind)).toEqual(['aborted'])
await page.locator('[data-turn-process]').waitFor({ timeout: 10_000 })
await expect.poll(() => copyButtons.count(), { timeout: 10_000 }).toBe(2)
await expect.poll(() => page.locator('[data-streaming="true"]').count(), { timeout: 10_000 }).toBe(0)
await copyButtons.last().focus()
@@ -182,9 +189,89 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
expect(tripwire.warnings).toEqual([])
}, 120_000)
it.skipIf(MODE === 'record')('folds the Turn process after the completed reply becomes the answer', async () => {
await launch()
onTestFailed(() => saveFailureShot(page, 'web-e2e-turn-tail-actions-completed'))
const { settled } = await sendPrompt()
await settled
await expect.poll(() => page.getByText('DONE', { exact: true }).count(), { timeout: 10_000 }).toBe(1)
const process = page.locator('[data-turn-process]')
await expect.poll(() => process.count(), { timeout: 10_000 }).toBe(1)
expect(await process.getAttribute('aria-expanded')).toBe('false')
expect(await process.evaluate(element => getComputedStyle(element).borderBottomWidth)).toBe('1px')
const processBottom = await process.evaluate(element =>
element.closest<HTMLElement>('[data-chat-flow-kind="turn-process"]')?.getBoundingClientRect().bottom)
const answerTop = await page.getByText('DONE', { exact: true }).evaluate(element =>
element.closest<HTMLElement>('[data-chat-flow-kind="assistant-step"]')?.getBoundingClientRect().top)
expect(answerTop).toBe((processBottom ?? 0) + 8)
await process.focus()
const completed = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
await compareOrRefreshGolden(COMPLETED_EXPECTED, completed, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
it.skipIf(MODE === 'record')('switches a completed Turn between Compact and Normal', async () => {
await launch()
onTestFailed(() => saveFailureShot(page, 'web-e2e-turn-process-setting'))
const { settled } = await sendPrompt()
await settled
const process = page.locator('[data-turn-process]')
const tool = page.getByRole('button', { name: 'Bash Print alpha to stdout' })
await process.waitFor({ timeout: 10_000 })
expect(await process.getAttribute('aria-expanded')).toBe('false')
expect(await tool.isVisible()).toBe(false)
await page.getByRole('button', { name: 'Settings', exact: true }).click()
const dialog = page.getByRole('dialog', { name: 'Settings' })
await dialog.getByRole('button', { name: 'Compact', exact: true }).click()
await page.getByRole('menuitem', { name: 'Normal', exact: true }).click()
await page.keyboard.press('Escape')
await expect.poll(() => process.count(), { timeout: 10_000 }).toBe(0)
await tool.waitFor({ state: 'visible', timeout: 10_000 })
await expect.poll(async () => readFile(join(scaffold!.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
.toMatch(/ui-chat:\n\s+transcriptView: normal/)
await page.getByRole('button', { name: 'Settings', exact: true }).click()
const restored = page.getByRole('dialog', { name: 'Settings' })
await restored.getByRole('button', { name: 'Normal', exact: true }).click()
await page.getByRole('menuitem', { name: 'Compact', exact: true }).click()
await page.keyboard.press('Escape')
await process.waitFor({ timeout: 10_000 })
expect(await process.getAttribute('aria-expanded')).toBe('false')
expect(await tool.isVisible()).toBe(false)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
it.skipIf(MODE === 'record')('keeps a focused process member open when the completed reply arrives', async () => {
await launch(undefined, 200)
onTestFailed(() => saveFailureShot(page, 'web-e2e-turn-tail-actions-focused'))
const { settled } = await sendPrompt()
const tool = page.getByRole('button', { name: 'Bash Print alpha to stdout' })
await tool.waitFor({ timeout: 30_000 })
await tool.focus()
expect(await tool.evaluate(element => element.ownerDocument.activeElement === element)).toBe(true)
await settled
await expect.poll(() => page.getByText('DONE', { exact: true }).count(), { timeout: 10_000 }).toBe(1)
const process = page.locator('[data-turn-process]')
await expect.poll(() => process.count(), { timeout: 10_000 }).toBe(1)
expect(await process.getAttribute('aria-expanded')).toBe('true')
expect(await tool.evaluate(element => element.ownerDocument.activeElement === element)).toBe(true)
const focused = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
await compareOrRefreshGolden(FOCUSED_EXPECTED, focused, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
it.skipIf(MODE === 'record')('keeps a closed fixture inventory', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, [
'running.expected.md', 'session.jsonl', 'settled.expected.md', 'usage-expanded.expected.md',
])
await assertFixtureInventory(
SNAPSHOT_DIR,
[
'completed.expected.md', 'focused.expected.md', 'running.expected.md', 'session.jsonl',
'settled.expected.md', 'usage-expanded.expected.md',
],
)
})
})
+6 -2
View File
@@ -16,7 +16,7 @@ import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
import { connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/web-search-round', import.meta.url))
const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/web-search-round/session.jsonl', import.meta.url))
@@ -263,7 +263,9 @@ describe('web e2e: shipped default web search', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-search-aria'))
await expect.poll(() => page.getByText('SEARCH_DONE', { exact: true }).count(), { timeout: 15_000 })
.toBeGreaterThanOrEqual(1)
await page.locator('[data-tool="web_search"]').waitFor({ timeout: 10_000 })
const searchTool = page.locator('[data-tool="web_search"]')
await expandOwningTurnProcess(page, searchTool)
await searchTool.waitFor({ timeout: 10_000 })
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
})
@@ -271,6 +273,7 @@ describe('web e2e: shipped default web search', () => {
it.skipIf(MODE === 'record')('scrolls the capped source list inside the fixed-height container', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-search-sources-scroll'))
const row = page.locator('[data-tool="web_search"] [data-expandable]').first()
await expandOwningTurnProcess(page, row)
await row.click()
await expect.poll(() => row.getAttribute('aria-expanded'), { timeout: 5_000 }).toBe('true')
@@ -299,6 +302,7 @@ describe('web e2e: shipped default web search', () => {
it.skipIf(MODE === 'record')('reserves marker room a scroll container cannot clip back', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-search-marker-room'))
await expandOwningTurnProcess(page, page.locator('[data-tool="web_search"]'))
// `overflow-y: auto` clips inline-start overflow with no way to scroll it
// back, and markers are right-aligned to the content edge, so a marker wider
// than `padding-left` silently loses its leading digits. `searchMaxResults`
+3 -1
View File
@@ -15,7 +15,7 @@ import {
type WebScaffold,
} from './scaffold.ts'
import {
connectFreshWorkspace, newEnglishPage, REPO_ROOT, saveFailureShot,
connectFreshWorkspace, expandTurnProcesses, newEnglishPage, REPO_ROOT, saveFailureShot,
} from './support.ts'
const MODE = webSnapshotMode()
@@ -161,6 +161,7 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () =
const sessions = page.getByRole('tree', { name: 'Sessions' })
await sessions.getByRole('treeitem', { name: /Use the workflow tool exactly/ }).click()
await settled
await expandTurnProcesses(page)
await page.locator('[data-workflow-run][data-run-status="completed"]').waitFor()
expect(await page.locator('[data-chat-flow-kind="tool-call"]').count()).toBeGreaterThanOrEqual(1)
@@ -186,6 +187,7 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () =
onTestFailed(() => saveFailureShot(page, 'web-e2e-workflow-run-history'))
await page.reload({ waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await expandTurnProcesses(page)
const workflow = page.getByRole('button', { name: /^snapshot-flow/ })
await workflow.waitFor({ timeout: 15_000 })
expect(await workflow.getAttribute('aria-expanded')).toBe('false')