Merge remote-tracking branch 'origin/master' into worktree/approval-panel-max-height
This commit is contained in:
@@ -118,14 +118,14 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => {
|
||||
expect(await nest.locator('[data-state="error"]').count()).toBeGreaterThanOrEqual(1)
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('a bash sub-row click leaves the default details panel open', async () => {
|
||||
it.skipIf(MODE === 'record')('a bash sub-row click leaves the default details panel closed', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-code-mode-details'))
|
||||
const nest = page.locator('[data-subcalls]').first()
|
||||
const frame = page.locator('[style*="grid-template-columns"]').first()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBeNull()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
|
||||
await nest.locator('[data-sample="bash-global"]').first().click()
|
||||
// Tool rows do not drive layout geometry; the Session's default panel stays open.
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
// Tool rows do not drive layout geometry; the Session's default panel stays closed.
|
||||
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 () => {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// Keyless browser regression for the details column's Session ownership.
|
||||
// The shipped composition retains geometry through unselected states and closes it only when a different Session takes ownership.
|
||||
// Keyless browser regression for the details column's default visibility and Session ownership.
|
||||
// The shipped composition starts closed after selection and reload, retains an explicitly opened width through
|
||||
// unselected states, and closes it only when a different Session takes ownership.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import {
|
||||
fixtureUserPrompts, launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
acknowledgeReloadConnectionLoss, fixtureUserPrompts, launchWebScaffold, seedSession, watchConsole,
|
||||
webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
@@ -52,7 +54,7 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('retains geometry through hero and closes it for a different Session', async () => {
|
||||
it('starts and reloads closed, then stays closed across Session ownership changes', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-details-session-lifecycle'))
|
||||
const settled = scaffold.whenTurnSettled()
|
||||
const input = page.locator('textarea').first()
|
||||
@@ -61,10 +63,18 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
|
||||
await settled
|
||||
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(360)
|
||||
expect(await page.getByText('详情', { exact: true }).count()).toBe(1)
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
|
||||
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
|
||||
|
||||
await page.getByRole('button', { name: 'New session', exact: true }).last().click()
|
||||
const warningStart = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await appFrame(page).waitFor({ timeout: 30_000 })
|
||||
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
|
||||
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
|
||||
|
||||
await page.getByRole('button', { name: /^(?:New session|新.*会话)$/ }).last().click()
|
||||
await page.getByText("Let's start building", { exact: false }).waitFor({ timeout: 15_000 })
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
|
||||
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
|
||||
@@ -72,8 +82,8 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
|
||||
const original = page.locator('[role=treeitem]').filter({ hasText: 'Reply with the single word' }).first()
|
||||
await original.click()
|
||||
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(360)
|
||||
expect(await page.getByText('详情', { exact: true }).count()).toBe(1)
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
|
||||
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
|
||||
|
||||
const ungrouped = page.getByText('Ungrouped', { exact: true })
|
||||
const ungroupedRow = ungrouped.locator('..').locator('..')
|
||||
|
||||
@@ -173,24 +173,24 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
await expect.poll(() => page.locator('tr[data-timeline-focus]').count(), { timeout: 10_000 }).toBe(0)
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('bash and file-path rows leave the default details column open', async () => {
|
||||
it.skipIf(MODE === 'record')('bash and file-path rows leave the default details column closed', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-details'))
|
||||
await page.getByRole('tab', { name: 'Chat' }).click()
|
||||
const bashRow = page.locator('[data-sample="bash-global"]').first()
|
||||
await bashRow.waitFor({ timeout: 15_000 })
|
||||
const frame = page.locator('[style*="grid-template-columns"]').first()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBeNull()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
|
||||
await bashRow.click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
|
||||
// The card's own controls are outside the summary row and must not open
|
||||
// details either — the terminal card is read in place.
|
||||
await page.locator('[data-sample="bash-global"] ~ [data-terminal] [class*="_copyButton_"]').first().click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
|
||||
// Read summaries are host-open file links; they also must not open details.
|
||||
const fileLink = page.locator('[data-variant="read"] button').first()
|
||||
await fileLink.waitFor({ timeout: 10_000 })
|
||||
await fileLink.click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('renders the bash row as a terminal card in the real browser', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Shared scaffold for the keyless browser e2e lane (Agent Note:
|
||||
// .agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md).
|
||||
// Boots the REAL web composition — the shipped apps/cli/cordis.yml through
|
||||
// Boots the REAL web composition — the shipped base plus web overlay through
|
||||
// the vendored Loader (the same include boot AppCLIEntry drives), patched the
|
||||
// snapshot way — so a real chromium exercises the real HTTP/SSE wire, the
|
||||
// api-gateway, agent loop, tools, and persistence. Modes ride $DSH_SNAPSHOT:
|
||||
@@ -9,7 +9,7 @@
|
||||
// from live session memory), refresh (keyless replay that rewrites goldens).
|
||||
//
|
||||
// Composition divergences from `dsh web`, all deliberate, all via include
|
||||
// patches over the SAME tree (never a second yml): temp persistenceRoot;
|
||||
// patches after the shipped surface overlay: temp persistenceRoot;
|
||||
// workspace-context disabled (recorded fixtures must not embed this repo's
|
||||
// AGENTS.md); session-title-llm disabled (its fire-and-forget title call
|
||||
// would race the loop for the session's replay cursor); webserver pinned to
|
||||
@@ -28,7 +28,7 @@ import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import Include, { type PatchOptions } from '@cordisjs/plugin-include'
|
||||
import { scrubRequestHeaders } from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import { assertEntriesLoaded } from '@deepseek-ai/dsh-app-boot'
|
||||
import { assertEntriesLoaded, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
|
||||
import type { ReplayHandle } from '@deepseek-ai/dsh-llm-replay'
|
||||
import { installLlmReplay, parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
|
||||
import SessionStore, {
|
||||
@@ -60,8 +60,9 @@ export function webSnapshotMode(): WebSnapshotMode {
|
||||
throw new Error(`DSH_SNAPSHOT must be replay, record, or refresh; got ${JSON.stringify(value)}`)
|
||||
}
|
||||
|
||||
/** The shipped composition under test: apps/cli's config tree. */
|
||||
const CONFIG_PATH = join(REPO_ROOT, 'apps/cli/cordis.yml')
|
||||
/** The shipped composition under test: apps/cli's shared base and web overlay. */
|
||||
const CONFIG_PATH = join(REPO_ROOT, 'apps/cli/config/base.cordis.yml')
|
||||
const WEB_OVERLAY_PATH = join(REPO_ROOT, 'apps/cli/config/web.cordis.yml')
|
||||
|
||||
// Replay publishes the provider catalog the gateway routes to (providers
|
||||
// mode, never catch-all: with llm-deepseek disabled no adapter exists, so a
|
||||
@@ -163,7 +164,9 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// The include patch set — the same mechanism AppCLIEntry and the ACP
|
||||
// snapshot overlay use, applied over the SAME shipped tree (a patch id that
|
||||
// stops matching a row fails the boot sweep loudly instead of drifting).
|
||||
const surfacePatches = loadOverlayPatches('web e2e scaffold', WEB_OVERLAY_PATH)
|
||||
const patches: PatchOptions[] = [
|
||||
...surfacePatches,
|
||||
{ id: 'session-persistence-jsonl', config: { root: persistenceRoot } },
|
||||
// storage-json's './.storages' yml default is cwd-relative and resolves
|
||||
// per write; the scaffold restores the original cwd after boot, so the
|
||||
|
||||
@@ -140,9 +140,9 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
const fileLink = page.locator('[data-variant="read"] button').first()
|
||||
await fileLink.waitFor({ timeout: 10_000 })
|
||||
const frame = page.locator('[style*="grid-template-columns"]').first()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBeNull()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
|
||||
await fileLink.click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
|
||||
// Path label survives from the recorded args (a.txt).
|
||||
await expect.poll(() => page.getByText('a.txt', { exact: false }).count(), { timeout: 5_000 }).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
@@ -454,7 +454,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
await screen(page, '07-back-to-chat')
|
||||
})
|
||||
|
||||
it('5 bash differential rendering: tool row click leaves the default details column open', async () => {
|
||||
it('5 bash differential rendering: tool row click leaves the default details column closed', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'w5-tool-details'))
|
||||
const input = page.locator('textarea').first()
|
||||
await input.fill('请用 bash 工具运行命令 echo w5marker 然后告诉我结果')
|
||||
@@ -466,11 +466,11 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
const toolRow = page.locator('[data-sample="bash-global"]')
|
||||
await toolRow.waitFor({ timeout: 120_000 })
|
||||
await screen(page, '08-bash-round')
|
||||
expect(await detailsTrack(page)).toBe(360)
|
||||
expect(await detailsTrack(page)).toBe(0)
|
||||
await toolRow.click()
|
||||
// Tool rows no longer drive layout.openDetails; the default column stays open.
|
||||
expect(await detailsTrack(page)).toBe(360)
|
||||
await screen(page, '09-details-open')
|
||||
// Tool rows no longer drive layout.openDetails; the default column stays closed.
|
||||
expect(await detailsTrack(page)).toBe(0)
|
||||
await screen(page, '09-details-closed')
|
||||
}, 150_000)
|
||||
|
||||
it('6 sidebar drag widens the column and resets across reload', async () => {
|
||||
|
||||
@@ -5,6 +5,37 @@
|
||||
- img
|
||||
- button "browse-golden"
|
||||
- button "Edit path"
|
||||
- list:
|
||||
- listitem:
|
||||
- button "adopted":
|
||||
- img
|
||||
- text: adopted
|
||||
- img
|
||||
- listitem:
|
||||
- button "alpha-ws":
|
||||
- img
|
||||
- text: alpha-ws
|
||||
- img
|
||||
- listitem:
|
||||
- button "beta-ws":
|
||||
- img
|
||||
- text: beta-ws
|
||||
- img
|
||||
- listitem:
|
||||
- button "browse-golden":
|
||||
- img
|
||||
- text: browse-golden
|
||||
- img
|
||||
- listitem:
|
||||
- button "same-name":
|
||||
- img
|
||||
- text: same-name
|
||||
- img
|
||||
- listitem:
|
||||
- button "workspace":
|
||||
- img
|
||||
- text: workspace
|
||||
- img
|
||||
- list:
|
||||
- listitem:
|
||||
- button "alpha":
|
||||
@@ -19,5 +50,6 @@
|
||||
- button "New folder":
|
||||
- img
|
||||
- text: New folder
|
||||
- button "Show hidden files"
|
||||
- button "Cancel"
|
||||
- button "Open"
|
||||
|
||||
Reference in New Issue
Block a user