Merge remote-tracking branch 'origin/master' into worktree/subagent-missing-tools-fb2359
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
name: 代码模式
|
||||
description: 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用。
|
||||
description: 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
order: 2
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
name: 创造模式
|
||||
description: 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设。
|
||||
description: 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
|
||||
order: 4
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
name: 极简模式
|
||||
description: 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
|
||||
description: 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
|
||||
order: 3
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
name: 标准模式
|
||||
description: 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
|
||||
description: 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
|
||||
order: 1
|
||||
|
||||
@@ -161,7 +161,7 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
|
||||
expect(composition).toBe(await readFile(join(SHIPPED_PRESETS, 'minimal', 'agent.cordis.yml'), 'utf8'))
|
||||
const metadata = await readFile(join(userRoot, 'my-agent', 'preset.yml'), 'utf8')
|
||||
expect(metadata).toContain('name: 我的模式')
|
||||
expect(metadata).toContain('description: 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。')
|
||||
expect(metadata).toContain('description: 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。')
|
||||
expect(metadata).not.toContain('order:')
|
||||
}, 60_000)
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import {
|
||||
SESSION_FORMAT_VERSION, SessionId as sessionId, type SessionEvent, type SessionId,
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
|
||||
import {
|
||||
captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole,
|
||||
webSnapshotMode, type WebScaffold,
|
||||
@@ -78,6 +82,60 @@ function seedLog(): string {
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist one child so the assembled header snapshot exercises both action
|
||||
* contributors whose relative order is the product contract under test.
|
||||
* @param scaffold - the booted Web scaffold.
|
||||
* @param parentId - the seeded session whose header the browser opens.
|
||||
*/
|
||||
async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise<void> {
|
||||
const childId = sessionId('agent-preset-selection-child')
|
||||
const createdAt = 1784974100100
|
||||
await scaffold.ctx.sessionPersistence.create({
|
||||
version: SESSION_FORMAT_VERSION,
|
||||
id: childId,
|
||||
createdAt,
|
||||
cwd: scaffold.workspaceCwd,
|
||||
parentSession: parentId,
|
||||
origin: 'subagent',
|
||||
delegationDepth: 1,
|
||||
agentPreset: 'minimal',
|
||||
})
|
||||
await scaffold.ctx.sessionPersistence.append(childId, [
|
||||
{
|
||||
type: 'turn/start',
|
||||
seq: 0,
|
||||
time: createdAt,
|
||||
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
|
||||
},
|
||||
{
|
||||
type: 'user/message',
|
||||
seq: 1,
|
||||
time: createdAt + 1,
|
||||
data: {
|
||||
content: [{ type: 'text', text: 'Check the session-header action order.' }],
|
||||
source: { kind: 'user' },
|
||||
},
|
||||
surfaceOp: 'append',
|
||||
},
|
||||
{
|
||||
type: 'subagent/descriptor',
|
||||
seq: 2,
|
||||
time: createdAt + 2,
|
||||
data: snapshotSubagentDescriptor({
|
||||
mode: 'one-shot', provider: 'spawn', label: 'header order probe',
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'turn/end',
|
||||
seq: 3,
|
||||
time: createdAt + 3,
|
||||
data: { turn: 1, reason: { kind: 'completed' } },
|
||||
},
|
||||
] as SessionEvent[])
|
||||
await scaffold.ctx.sessionProjectionCache.coldSnapshot(childId)
|
||||
}
|
||||
|
||||
/**
|
||||
* The preset the host reports for the blank session the workspace connect
|
||||
* produced. Addressed by id rather than by scanning the serialized list: the
|
||||
@@ -120,7 +178,8 @@ describe('web e2e: agent-preset selection', () => {
|
||||
// A resumed session runs what it was created with; seeding one that
|
||||
// records `minimal` is what makes the header label a claim about the
|
||||
// session rather than an echo of the current default.
|
||||
await seedSession(scaffold, seedLog(), SEED_ID, 'minimal')
|
||||
const seededId = await seedSession(scaffold, seedLog(), SEED_ID, 'minimal')
|
||||
await seedSubagent(scaffold, seededId)
|
||||
await seedWorkspaceSkill(scaffold.workspaceCwd)
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
@@ -143,12 +202,12 @@ describe('web e2e: agent-preset selection', () => {
|
||||
await compareOrRefreshGolden(HERO_EXPECTED, snapshot, MODE)
|
||||
// The chip opens on the deployment default, by the name that preset
|
||||
// publishes rather than its directory name.
|
||||
expect(snapshot).toContain('标准模式')
|
||||
expect(snapshot).toContain('Standard mode')
|
||||
})
|
||||
|
||||
it('names every preset and what it is for', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-menu'))
|
||||
await page.getByRole('button', { name: '标准模式' }).click()
|
||||
await page.getByRole('button', { name: 'Standard mode' }).click()
|
||||
const menu = page.getByRole('menu')
|
||||
await menu.waitFor({ timeout: 10_000 })
|
||||
|
||||
@@ -157,15 +216,15 @@ describe('web e2e: agent-preset selection', () => {
|
||||
await compareOrRefreshGolden(MENU_EXPECTED, snapshot, MODE)
|
||||
// Every shipped preset, each with the sentence saying what it composes —
|
||||
// the id alone never said what a preset does.
|
||||
expect(snapshot).toContain('极简模式')
|
||||
expect(snapshot).toContain('创造模式')
|
||||
expect(snapshot).toContain('Minimal mode')
|
||||
expect(snapshot).toContain('Creator mode')
|
||||
await page.keyboard.press('Escape')
|
||||
})
|
||||
|
||||
it('applies the staged pick to the blank session, and the host honors it', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-stage'))
|
||||
await page.getByRole('button', { name: '标准模式' }).click()
|
||||
await page.getByRole('menuitem', { name: /极简模式/ }).click()
|
||||
await page.getByRole('button', { name: 'Standard mode' }).click()
|
||||
await page.getByRole('menuitem', { name: /Minimal mode/ }).click()
|
||||
|
||||
// The chip stages; the blank session the workspace connect produced is
|
||||
// what the stage lands on. The host's own answer is what comes back.
|
||||
@@ -197,8 +256,8 @@ describe('web e2e: agent-preset selection', () => {
|
||||
// against its list row, so a row that never reprojected the first switch
|
||||
// answers "already standard" and sends nothing — and restores the catalog
|
||||
// instead of leaving the session reading the narrower composition.
|
||||
await page.getByRole('button', { name: '极简模式' }).click()
|
||||
await page.getByRole('menuitem', { name: /^标准模式/ }).first().click()
|
||||
await page.getByRole('button', { name: 'Minimal mode' }).click()
|
||||
await page.getByRole('menuitem', { name: /^Standard mode/ }).first().click()
|
||||
await expect.poll(() => livePreset(scaffold.baseUrl), { timeout: 15_000 }).toBe('standard')
|
||||
|
||||
await composer.fill('/')
|
||||
@@ -221,10 +280,12 @@ describe('web e2e: agent-preset selection', () => {
|
||||
const snapshot = await captureStableAria(page, '[class*="titleRow"]', scaffold.workspaceCwd)
|
||||
|
||||
await compareOrRefreshGolden(HEADER_EXPECTED, snapshot, MODE)
|
||||
expect(snapshot).toContain('极简模式')
|
||||
expect(snapshot).toContain('Minimal mode')
|
||||
expect(snapshot).toContain('button "1 subagent"')
|
||||
expect(snapshot.indexOf('Minimal mode')).toBeLessThan(snapshot.indexOf('button "1 subagent"'))
|
||||
// Static chrome, not a control: the header can only report a composition
|
||||
// the host would refuse to change.
|
||||
expect(snapshot).not.toContain('button "极简模式"')
|
||||
expect(snapshot).not.toContain('button "Minimal mode"')
|
||||
})
|
||||
|
||||
it('drove every surface without a page error or a stream warning', () => {
|
||||
|
||||
@@ -70,7 +70,12 @@ let unmount: (() => void) | undefined
|
||||
export function installAssembledBootEnv(): void {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
localStorage.setItem('dsh.locale', 'en')
|
||||
// The locale service derives its provisional locale from the browser and
|
||||
// takes an explicit choice only from Host settings, which this lane's
|
||||
// fixture transport does not serve; pinning the navigator is what selects
|
||||
// English here.
|
||||
Object.defineProperty(navigator, 'languages', { value: ['en-US'], configurable: true })
|
||||
Object.defineProperty(navigator, 'language', { value: 'en-US', configurable: true })
|
||||
document.title = 'DeepSeek Harness'
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
||||
@@ -88,6 +93,11 @@ export function installAssembledBootEnv(): void {
|
||||
document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
|
||||
document.title = ''
|
||||
history.replaceState(null, '', '/')
|
||||
// Deleting the own properties uncovers jsdom's own accessors again
|
||||
// (Navigator declares both readonly, hence the erased receiver).
|
||||
const ownNavigator = navigator as unknown as Record<string, unknown>
|
||||
delete ownNavigator.languages
|
||||
delete ownNavigator.language
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ installAssembledBootEnv()
|
||||
|
||||
/** Open the fixture history session (the alpha log carrying the turn-72 image pair) and wait for its gallery. */
|
||||
async function openFixtureSession(): Promise<void> {
|
||||
const tree = await screen.findByRole('tree', { name: '会话' }, { timeout: 10_000 })
|
||||
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
|
||||
const group = (await within(tree).findAllByText('fixture'))
|
||||
.map(el => el.closest<HTMLElement>('[role="treeitem"]'))
|
||||
.find(el => el?.getAttribute('aria-expanded') !== null)
|
||||
@@ -33,7 +33,6 @@ async function openFixtureSession(): Promise<void> {
|
||||
}
|
||||
|
||||
it('renders the history image pair through the authorized attachment route and opens the lightbox', async () => {
|
||||
localStorage.setItem('dsh.locale', 'zh')
|
||||
mountAssembledApp()
|
||||
await openFixtureSession()
|
||||
|
||||
@@ -73,24 +72,23 @@ it('renders the history image pair through the authorized attachment route and o
|
||||
fireEvent.doubleClick(frame)
|
||||
const lightbox = await screen.findByRole('dialog')
|
||||
expect(within(lightbox).getByRole('img').getAttribute('src')?.split(':')[0]).toBe('blob')
|
||||
fireEvent.click(within(lightbox).getByRole('button', { name: /关闭/ }))
|
||||
fireEvent.click(within(lightbox).getByRole('button', { name: /Close/ }))
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts pasted images into the composer rail in order and removes them', async () => {
|
||||
localStorage.setItem('dsh.locale', 'zh')
|
||||
mountAssembledApp()
|
||||
|
||||
const tree = await screen.findByRole('tree', { name: '会话' }, { timeout: 10_000 })
|
||||
const start = tree.querySelector<HTMLButtonElement>('button[aria-label="在“fixture”中新建会话"]')
|
||||
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
|
||||
const start = tree.querySelector<HTMLButtonElement>('button[aria-label="New session in fixture"]')
|
||||
if (start === null) throw new Error('fixture Workspace new-session action missing')
|
||||
fireEvent.click(start)
|
||||
|
||||
// Image-only send arming is pinned at package level (input-bar.spec.tsx);
|
||||
// this assembled lane pins the intake chain over the built graph.
|
||||
const textarea = await screen.findByPlaceholderText('描述你想要构建的内容', {}, { timeout: 10_000 })
|
||||
const textarea = await screen.findByPlaceholderText('Describe what you want to build', {}, { timeout: 10_000 })
|
||||
const image = new File([new Uint8Array([137, 80, 78, 71])], 'pasted.png', { type: 'image/png' })
|
||||
fireEvent.paste(textarea, {
|
||||
clipboardData: {
|
||||
@@ -102,7 +100,7 @@ it('accepts pasted images into the composer rail in order and removes them', asy
|
||||
// The rail is an accessible group holding the draft thumbnail (queried via
|
||||
// DOM: jsdom's a11y-visibility computation hides the composer subtree).
|
||||
const rail = await waitFor(() => {
|
||||
const el = document.querySelector('[role="group"][aria-label="待发送图片"]')
|
||||
const el = document.querySelector('[role="group"][aria-label="Pending images"]')
|
||||
if (el === null) throw new Error('attachment rail missing')
|
||||
return el
|
||||
}, { timeout: 5_000 })
|
||||
@@ -129,10 +127,10 @@ it('accepts pasted images into the composer rail in order and removes them', asy
|
||||
.toEqual(['pasted.png', 'second.png'])
|
||||
})
|
||||
|
||||
const remove = [...rail.querySelectorAll('button[aria-label^="移除图片"]')]
|
||||
const remove = [...rail.querySelectorAll('button[aria-label^="Remove image"]')]
|
||||
if (remove.length !== 2) throw new Error('remove buttons missing')
|
||||
for (const button of remove) fireEvent.click(button)
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[role="group"][aria-label="待发送图片"]')).toBeNull()
|
||||
expect(document.querySelector('[role="group"][aria-label="Pending images"]')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -251,6 +251,8 @@ export interface LaunchOptions {
|
||||
* 127.0.0.1; a non-resolving authority fails before Host trust is exercised.
|
||||
*/
|
||||
remoteAuthority?: string
|
||||
/** Reuse an existing harness home so a second Host can verify user settings across origins. */
|
||||
harnessHome?: string
|
||||
}
|
||||
|
||||
/** Dispose the booted tree and remove both owned temp roots, reporting every independent cleanup failure. */
|
||||
@@ -297,16 +299,19 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// Isolated harness home: the settings/credentials rows resolve $DSH_HOME
|
||||
// paths at load, and an in-process boot must NEVER touch the developer's
|
||||
// real ~/.dsh document or credential file.
|
||||
const harnessHome = join(workspaceCwd, '.dsh-home')
|
||||
const harnessHome = options.harnessHome ?? join(workspaceCwd, '.dsh-home')
|
||||
// Skill discovery is model-visible input, and its roots now resolve inside a
|
||||
// PRESET — a subtree this lane's include patches cannot reach, because the
|
||||
// roster mounts it directly per session rather than as a row of the booted
|
||||
// tree. The row's documented fallback is the environment, so pin that: the
|
||||
// whole scaffold lifetime, not just the boot, since presets mount when a
|
||||
// session is created. Without this a developer's real ~/.dsh/skills silently
|
||||
// enters replay requests and goldens while CI sees none.
|
||||
// enters replay requests and goldens while CI sees none. `DSH_HOME` follows
|
||||
// the resolved harness home so a scaffold sharing another's home — the
|
||||
// cross-port persistence scenario — pins the same roots the settings and
|
||||
// credentials rows were configured with.
|
||||
const skillRootEnvironment = {
|
||||
DSH_HOME: join(workspaceCwd, '.dsh-home'),
|
||||
DSH_HOME: harnessHome,
|
||||
DSH_AGENTS_HOME: join(workspaceCwd, '.agents-home'),
|
||||
DSH_BUNDLED_SKILL_DIR: join(workspaceCwd, '.bundled-skills'),
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Web e2e scenarios: the settings surface — the modal shell (trigger, nav,
|
||||
// section switching, both close paths), the Appearance preference row (the
|
||||
// real theme gesture — click 深色 and the whole cascade runs: ThemeService preference -> localStorage dsh.theme
|
||||
// real theme gesture — click 深色 and the whole cascade runs: ThemeService preference -> Host settings
|
||||
// -> theme/change -> ui-layout's presenter -> body attribute -> alias token +
|
||||
// browser theme-color metadata)
|
||||
// the Language row (settings-scoped localization + persisted dsh.locale),
|
||||
// the busy-state Enter preference, plus Permission as the persisted default
|
||||
// for subsequently created sessions.
|
||||
// the Language row and busy-state Enter preference (both Host-backed), plus
|
||||
// Permission as the persisted default for subsequently created sessions.
|
||||
// Zero model calls: everything is pure client + persistence state on a blank
|
||||
// frame, so there is no fixture and a stray stream would fail loud on the
|
||||
// open llm seam.
|
||||
@@ -153,23 +152,24 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('flips the theme through the Appearance cubes and persists across reload', async () => {
|
||||
it('flips the theme through the Appearance cubes and persists across reload and a distinct port', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-appearance'))
|
||||
interface ThemeState {
|
||||
attr: boolean
|
||||
background: string
|
||||
stored: string | null
|
||||
/** Pre-migration localStorage key; the Host-backed world never writes it. */
|
||||
legacy: string | null
|
||||
themeColor: string | null
|
||||
themeColorCount: number
|
||||
token: string
|
||||
}
|
||||
const readState = async (): Promise<ThemeState> => await page.evaluate(() => {
|
||||
const readState = async (target: Page = page): Promise<ThemeState> => await target.evaluate(() => {
|
||||
const metas = document.head.querySelectorAll<HTMLMetaElement>('meta[name="theme-color"]')
|
||||
const computed = getComputedStyle(document.body)
|
||||
return {
|
||||
attr: document.body.hasAttribute('data-ds-dark-theme'),
|
||||
background: computed.backgroundColor,
|
||||
stored: localStorage.getItem('dsh.theme'),
|
||||
legacy: localStorage.getItem('dsh.theme'),
|
||||
themeColor: metas[0]?.content ?? null,
|
||||
themeColorCount: metas.length,
|
||||
token: computed.getPropertyValue('--dsw-alias-bg-base').trim(),
|
||||
@@ -193,27 +193,51 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
const darkCube = dialog.getByRole('button', { name: '深色' })
|
||||
expect(await darkCube.getAttribute('aria-pressed')).toBe('false')
|
||||
await darkCube.click()
|
||||
// The full cascade: pressed state, persisted preference, body attribute,
|
||||
// The full cascade: pressed state, Host-backed preference, body attribute,
|
||||
// alias token flip — all from one real user gesture.
|
||||
await expect.poll(() => darkCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
|
||||
const dark = await readState()
|
||||
expect(dark.attr).toBe(true)
|
||||
expect(dark.stored).toBe('dark')
|
||||
expect(dark.legacy).toBeNull()
|
||||
expect(dark.token).not.toBe(light.token)
|
||||
expectThemeColorSynchronized(dark)
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/ui-theme:\n\s+preference: dark/)
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
// Reload: the preference survives boot (restore + presenter initial apply).
|
||||
// Reload: the preference survives the background Host read + presenter update.
|
||||
const warningStart = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await page.emulateMedia({ colorScheme: 'light' })
|
||||
await expect.poll(async () => (await readState()).attr, { timeout: 5_000 }).toBe(true)
|
||||
const reloaded = await readState()
|
||||
expect(reloaded.attr).toBe(true)
|
||||
expect(reloaded.stored).toBe('dark')
|
||||
expect(reloaded.legacy).toBeNull()
|
||||
expectThemeColorSynchronized(reloaded)
|
||||
|
||||
// A second live Host binds another ephemeral port but shares the same
|
||||
// user-settings home. Its fresh origin has no theme localStorage and still
|
||||
// converges to dark before the settings dialog opens.
|
||||
const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome })
|
||||
const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
const secondTripwire = watchConsole(secondPage)
|
||||
try {
|
||||
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
|
||||
await secondPage.emulateMedia({ colorScheme: 'light' })
|
||||
await secondPage.goto(second.baseUrl, { waitUntil: 'load' })
|
||||
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await expect.poll(async () => (await readState(secondPage)).attr, { timeout: 5_000 }).toBe(true)
|
||||
const secondState = await readState(secondPage)
|
||||
expect(secondState.legacy).toBeNull()
|
||||
expectThemeColorSynchronized(secondState)
|
||||
expect(secondTripwire.pageErrors).toEqual([])
|
||||
expect(secondTripwire.warnings).toEqual([])
|
||||
} finally {
|
||||
await secondPage.close()
|
||||
await second.close()
|
||||
}
|
||||
|
||||
// `system` follows the emulated OS scheme (dark stays dark, light clears).
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const systemCube = page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '跟随系统' })
|
||||
@@ -233,7 +257,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 90_000)
|
||||
|
||||
it('persists the busy-state Enter behavior across reload and restores Queue', async () => {
|
||||
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()
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
@@ -241,7 +265,9 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await dialog.getByRole('button', { name: '排队发送' }).click()
|
||||
await page.getByRole('menuitem', { name: '插话发送' }).click()
|
||||
await dialog.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBe('steer')
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/ui-conversation:\n\s+busyEnter: steer/)
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
const warningStart = tripwire.warnings.length
|
||||
@@ -251,15 +277,36 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const reloaded = page.getByRole('dialog', { name: '设置' })
|
||||
await reloaded.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
|
||||
|
||||
const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome })
|
||||
const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
const secondTripwire = watchConsole(secondPage)
|
||||
try {
|
||||
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
|
||||
await secondPage.goto(second.baseUrl, { waitUntil: 'load' })
|
||||
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await secondPage.getByRole('button', { name: '设置', exact: true }).click()
|
||||
await secondPage.getByRole('dialog', { name: '设置' })
|
||||
.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
|
||||
expect(await secondPage.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
|
||||
expect(secondTripwire.pageErrors).toEqual([])
|
||||
expect(secondTripwire.warnings).toEqual([])
|
||||
} finally {
|
||||
await secondPage.close()
|
||||
await second.close()
|
||||
}
|
||||
|
||||
await reloaded.getByRole('button', { name: '插话发送' }).click()
|
||||
await page.getByRole('menuitem', { name: '排队发送' }).click()
|
||||
await reloaded.getByRole('button', { name: '排队发送' }).waitFor({ timeout: 10_000 })
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBe('queue')
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/ui-conversation:\n\s+busyEnter: queue/)
|
||||
await page.keyboard.press('Escape')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 90_000)
|
||||
|
||||
it('switches the settings surface language and persists dsh.locale', async () => {
|
||||
it('persists the settings language across reload and a distinct port', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-language'))
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const zhDialog = page.getByRole('dialog', { name: '设置' })
|
||||
@@ -276,7 +323,9 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await enDialog.waitFor({ timeout: 10_000 })
|
||||
expect(await enDialog.getByRole('button', { name: 'General' }).getAttribute('aria-current')).toBe('true')
|
||||
await expect.poll(() => enDialog.getByText('Appearance', { exact: true }).count(), { timeout: 5_000 }).toBe(1)
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBe('en')
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/locale:\n\s+preference: en/)
|
||||
// Reload keeps English; then restore zh so shared page state (and the
|
||||
// other specs' 设置-anchored selectors + goldens) see the default again.
|
||||
const warningStart = tripwire.warnings.length
|
||||
@@ -285,24 +334,47 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
const enTrigger = page.getByRole('button', { name: 'Settings' })
|
||||
await enTrigger.waitFor({ timeout: 10_000 })
|
||||
|
||||
// A Chinese browser on another port still receives the explicit English
|
||||
// preference from the shared Host settings document.
|
||||
const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome })
|
||||
const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
const secondTripwire = watchConsole(secondPage)
|
||||
try {
|
||||
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
|
||||
await secondPage.goto(second.baseUrl, { waitUntil: 'load' })
|
||||
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await secondPage.getByRole('button', { name: 'Settings', exact: true }).click()
|
||||
await secondPage.getByRole('dialog', { name: 'Settings' })
|
||||
.getByRole('button', { name: 'English' }).waitFor({ timeout: 10_000 })
|
||||
expect(await secondPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
|
||||
expect(secondTripwire.pageErrors).toEqual([])
|
||||
expect(secondTripwire.warnings).toEqual([])
|
||||
} finally {
|
||||
await secondPage.close()
|
||||
await second.close()
|
||||
}
|
||||
|
||||
await enTrigger.click()
|
||||
await page.getByRole('dialog', { name: 'Settings' }).getByRole('button', { name: 'English' }).click()
|
||||
await page.getByRole('menuitem', { name: '中文' }).click()
|
||||
await page.getByRole('dialog', { name: '设置' }).waitFor({ timeout: 10_000 })
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBe('zh')
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/locale:\n\s+preference: zh/)
|
||||
await page.keyboard.press('Escape')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 90_000)
|
||||
|
||||
it('opens an English browser in English without any stored preference', async () => {
|
||||
// A second page under a different browser language: nothing is persisted
|
||||
// for it, so the settings surface must follow the browser rather than the
|
||||
// product fallback the shared zh page shows.
|
||||
// A fresh Host home has no locale preference, so its surface follows the
|
||||
// browser rather than the product fallback.
|
||||
const fresh = await launchWebScaffold({})
|
||||
const enPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'en-US' })
|
||||
const enTripwire = watchConsole(enPage)
|
||||
onTestFailed(() => saveFailureShot(enPage, 'web-e2e-settings-browser-language'))
|
||||
try {
|
||||
await enPage.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await enPage.goto(fresh.baseUrl, { waitUntil: 'load' })
|
||||
await enPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
expect(await enPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
|
||||
await enPage.getByRole('button', { name: 'Settings', exact: true }).click()
|
||||
@@ -315,6 +387,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(enTripwire.warnings).toEqual([])
|
||||
} finally {
|
||||
await enPage.close()
|
||||
await fresh.close()
|
||||
}
|
||||
}, 90_000)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- list:
|
||||
- listitem:
|
||||
- 'button "当前使用: 标准模式" [disabled] [pressed]':
|
||||
- text: 标准模式 内置 当前使用 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
|
||||
- text: 标准模式 内置 当前使用 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
|
||||
- code: standard
|
||||
- 'button "查看: 标准模式"':
|
||||
- img
|
||||
@@ -30,7 +30,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 代码模式"':
|
||||
- text: 代码模式 内置 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用。
|
||||
- text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
- code: code
|
||||
- 'button "查看: 代码模式"':
|
||||
- img
|
||||
@@ -40,7 +40,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 极简模式"':
|
||||
- text: 极简模式 内置 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
|
||||
- text: 极简模式 内置 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
|
||||
- code: minimal
|
||||
- 'button "查看: 极简模式"':
|
||||
- img
|
||||
@@ -50,7 +50,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 创造模式"':
|
||||
- text: 创造模式 内置 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设。
|
||||
- text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
|
||||
- code: cordis
|
||||
- 'button "查看: 创造模式"':
|
||||
- img
|
||||
@@ -62,7 +62,7 @@
|
||||
- list:
|
||||
- listitem:
|
||||
- 'button "设为默认: 我的模式"':
|
||||
- text: 我的模式 自定义 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
|
||||
- text: 我的模式 自定义 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
|
||||
- code: my-agent
|
||||
- 'button "查看路径: 我的模式"':
|
||||
- img
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- list:
|
||||
- listitem:
|
||||
- 'button "当前使用: 标准模式" [disabled] [pressed]':
|
||||
- text: 标准模式 内置 当前使用 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
|
||||
- text: 标准模式 内置 当前使用 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
|
||||
- code: standard
|
||||
- 'button "查看: 标准模式"':
|
||||
- img
|
||||
@@ -30,7 +30,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 代码模式"':
|
||||
- text: 代码模式 内置 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用。
|
||||
- text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
- code: code
|
||||
- 'button "查看: 代码模式"':
|
||||
- img
|
||||
@@ -40,7 +40,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 极简模式"':
|
||||
- text: 极简模式 内置 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
|
||||
- text: 极简模式 内置 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
|
||||
- code: minimal
|
||||
- 'button "查看: 极简模式"':
|
||||
- img
|
||||
@@ -50,7 +50,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 创造模式"':
|
||||
- text: 创造模式 内置 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设。
|
||||
- text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
|
||||
- code: cordis
|
||||
- 'button "查看: 创造模式"':
|
||||
- img
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- list:
|
||||
- listitem:
|
||||
- 'button "当前使用: 标准模式" [disabled] [pressed]':
|
||||
- text: 标准模式 内置 当前使用 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
|
||||
- text: 标准模式 内置 当前使用 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
|
||||
- code: standard
|
||||
- 'button "查看: 标准模式"':
|
||||
- img
|
||||
@@ -30,7 +30,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 代码模式"':
|
||||
- text: 代码模式 内置 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用。
|
||||
- text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
- code: code
|
||||
- 'button "查看: 代码模式"':
|
||||
- img
|
||||
@@ -40,7 +40,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 极简模式"':
|
||||
- text: 极简模式 内置 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
|
||||
- text: 极简模式 内置 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
|
||||
- code: minimal
|
||||
- 'button "查看: 极简模式"':
|
||||
- img
|
||||
@@ -50,7 +50,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 创造模式"':
|
||||
- text: 创造模式 内置 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设。
|
||||
- text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
|
||||
- code: cordis
|
||||
- 'button "查看: 创造模式"':
|
||||
- img
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Seeded turn" [disabled]
|
||||
- img
|
||||
- text: 极简模式
|
||||
- text: Minimal mode
|
||||
- button "1 subagent":
|
||||
- text: 1 subagent
|
||||
- img
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- img
|
||||
- text: workspace
|
||||
- img
|
||||
- button "标准模式":
|
||||
- button "Standard mode":
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- img
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- menu:
|
||||
- menuitem "标准模式 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。":
|
||||
- text: 标准模式 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
|
||||
- menuitem "Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.":
|
||||
- text: Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.
|
||||
- img
|
||||
- menuitem "代码模式 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用。"
|
||||
- menuitem "极简模式 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。"
|
||||
- menuitem "创造模式 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设。"
|
||||
- menuitem "Code mode All Standard mode capabilities, with tools exposed through the Code Mode SDK so the model can combine multi-step operations in one TypeScript program."
|
||||
- menuitem "Minimal mode Two-tool coding agent with only bash and str_replace_editor, for benchmarks and minimal reproductions."
|
||||
- menuitem "Creator mode Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- 'button "Using ONE run_code program: run" [disabled]'
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use only Cordis tools. First" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the bash tool to" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "workspace" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
- img
|
||||
- text: workspace
|
||||
- img
|
||||
- button "标准模式":
|
||||
- button "Standard mode":
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- img
|
||||
- textbox "Describe what you want to build"
|
||||
- button "Commands":
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
- img
|
||||
- text: workspace
|
||||
- img
|
||||
- button "标准模式":
|
||||
- button "Standard mode":
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- img
|
||||
- textbox "Describe what you want to build"
|
||||
- button "Commands":
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with the single word" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- 'button "Plan a small change: add" [disabled]'
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "workspace" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "/user-invoke-demo and confirm the fixtur" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Begin your reply with the" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Begin your reply with the" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use web_search to search exactly" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -314,7 +314,7 @@ describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
await page.getByText('标准模式', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
await page.getByText('Standard mode', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -18,18 +18,17 @@ export const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
export const ZH_BROWSER_LOCALE = 'zh-CN'
|
||||
|
||||
/**
|
||||
* Open the standard browser-test page with English selected before client
|
||||
* boot. This keeps role locators and goldens deterministic across localized
|
||||
* component migrations; the scenarios asserting the Chinese surface bypass
|
||||
* this helper and advertise {@link ZH_BROWSER_LOCALE} instead.
|
||||
* Open the standard browser-test page advertising English before client boot.
|
||||
* This keeps role locators and goldens deterministic while leaving the Host
|
||||
* settings document free to override the provisional browser-derived locale;
|
||||
* scenarios asserting the Chinese surface advertise
|
||||
* {@link ZH_BROWSER_LOCALE} instead.
|
||||
* @param browser - Playwright browser owning the page.
|
||||
* @param height - Viewport height; width is fixed to the lane baseline.
|
||||
* @returns the initialized page.
|
||||
*/
|
||||
export async function newEnglishPage(browser: Browser, height = 1000): Promise<Page> {
|
||||
const page = await browser.newPage({ viewport: { width: 1680, height } })
|
||||
await page.addInitScript(() => { localStorage.setItem('dsh.locale', 'en') })
|
||||
return page
|
||||
return await browser.newPage({ viewport: { width: 1680, height }, locale: 'en-US' })
|
||||
}
|
||||
|
||||
/** Fail loud on a stale checkout instead of testing yesterday's bundle. */
|
||||
|
||||
Reference in New Issue
Block a user