mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
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:
committed by
GitHub
co-authored by
Yif
parent
2c9c871eff
commit
8b09a0be52
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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([])
|
||||
})
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user