Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # apps/cli/cordis.yml # apps/cli/package.json # apps/cli/src/bin.ts # apps/cli/tests/args.spec.ts # packages/client/ui-conversation/README.i18n.yaml # packages/host/apiproxy/README.i18n.yaml # packages/ui/tui/README.i18n.yaml # pnpm-lock.yaml
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
|
||||
|
||||
@@ -12,6 +12,8 @@ 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 { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
@@ -117,6 +119,30 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
const toolRows = page.locator('[data-variant], [data-sample]')
|
||||
await expect.poll(() => toolRows.count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(2)
|
||||
expect(await page.getByText('a.txt', { exact: false }).count()).toBeGreaterThan(0)
|
||||
|
||||
const agent = scaffold.ctx.agents.get(SessionId(SEED_ID))
|
||||
if (agent === undefined) throw new Error('seeded session did not attach an agent')
|
||||
agent.inject(createUserMessage({
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: '<system-reminder>\n'
|
||||
+ 'The following workspace instructions may be relevant to your work. '
|
||||
+ 'Use them as guidance when applicable.\n\n'
|
||||
+ Array.from({ length: 24 }, (_, index) => `Instruction ${index + 1}: preserve the logged context contract.`).join('\n')
|
||||
+ '\n</system-reminder>',
|
||||
}],
|
||||
source: {
|
||||
kind: 'workspace-instructions',
|
||||
baseline: true,
|
||||
changes: [{
|
||||
action: 'set',
|
||||
scope: '.\u0000AGENTS.md',
|
||||
path: 'AGENTS.md',
|
||||
digest: 'context-injection-browser-snapshot',
|
||||
}],
|
||||
},
|
||||
}))
|
||||
await page.getByRole('button', { name: '上下文注入' }).waitFor({ timeout: 10_000 })
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('matches the historical conversation aria golden', async () => {
|
||||
@@ -132,6 +158,58 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('matches the Figma context disclosure geometry', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-context-injection'))
|
||||
const disclosure = page.getByRole('button', { name: '上下文注入' })
|
||||
expect(await disclosure.getAttribute('aria-expanded')).toBe('false')
|
||||
const collapsedIcon = disclosure.locator('svg').first()
|
||||
const collapsedIconBox = await collapsedIcon.boundingBox()
|
||||
expect(collapsedIconBox?.width).toBe(14)
|
||||
expect(collapsedIconBox?.height).toBe(14)
|
||||
|
||||
await disclosure.click()
|
||||
await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('true')
|
||||
const body = page.locator('[data-context-injection-body]')
|
||||
await body.waitFor({ timeout: 5_000 })
|
||||
const headerBox = await disclosure.boundingBox()
|
||||
const bodyBox = await body.boundingBox()
|
||||
if (headerBox === null || bodyBox === null) throw new Error('context disclosure geometry is not measurable')
|
||||
expect(headerBox.height).toBe(24)
|
||||
expect(bodyBox.x - headerBox.x).toBe(22)
|
||||
expect(bodyBox.y - headerBox.y - headerBox.height).toBe(4)
|
||||
expect(bodyBox.height).toBe(141)
|
||||
|
||||
const style = await body.evaluate((element) => {
|
||||
const computed = getComputedStyle(element)
|
||||
return {
|
||||
backgroundColor: computed.backgroundColor,
|
||||
borderRadius: computed.borderRadius,
|
||||
color: computed.color,
|
||||
fontSize: computed.fontSize,
|
||||
lineHeight: computed.lineHeight,
|
||||
padding: [
|
||||
computed.paddingTop,
|
||||
computed.paddingRight,
|
||||
computed.paddingBottom,
|
||||
computed.paddingLeft,
|
||||
],
|
||||
scrolls: element.scrollHeight > element.clientHeight,
|
||||
}
|
||||
})
|
||||
expect(style).toEqual({
|
||||
backgroundColor: 'rgb(249, 250, 251)',
|
||||
borderRadius: '8px',
|
||||
color: 'rgb(129, 133, 140)',
|
||||
fontSize: '11px',
|
||||
lineHeight: '16px',
|
||||
padding: ['10px', '16px', '12px', '12px'],
|
||||
scrolls: true,
|
||||
})
|
||||
|
||||
await disclosure.click()
|
||||
await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('false')
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('file-path tool rows rebuilt from the cold log stay details-inert', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-seeded-toolrow'))
|
||||
// Interaction over cold-resumed history: read summaries are host-open
|
||||
@@ -140,9 +218,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 () => {
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- button "上下文注入":
|
||||
- img
|
||||
- img
|
||||
- text: 上下文注入
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
|
||||
@@ -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