diff --git a/apps/web/tests/agent-team-panel.e2e.ts b/apps/web/tests/agent-team-panel.e2e.ts index 3e5ad5e0c2..54a75ee28b 100644 --- a/apps/web/tests/agent-team-panel.e2e.ts +++ b/apps/web/tests/agent-team-panel.e2e.ts @@ -158,7 +158,7 @@ describe('web e2e: Agent Teams panel', () => { expect(await trigger.evaluate(element => element === document.activeElement)).toBe(true) await page.keyboard.press('Enter') await panel.waitFor() - const outside = page.getByRole('button', { name: 'Settings', exact: true }) + const outside = page.getByRole('button', { name: 'Account menu', exact: true }) await outside.focus() await panel.waitFor({ state: 'detached' }) expect(await outside.evaluate(element => element === document.activeElement)).toBe(true) diff --git a/apps/web/tests/clickable-links-gallery.e2e.ts b/apps/web/tests/clickable-links-gallery.e2e.ts index 380ca8fd2e..75d6772f27 100644 --- a/apps/web/tests/clickable-links-gallery.e2e.ts +++ b/apps/web/tests/clickable-links-gallery.e2e.ts @@ -35,7 +35,7 @@ import { webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { newEnglishPage, saveFailureShot } from './support.ts' +import { openSettingsFromAccountMenu, newEnglishPage, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/clickable-links-gallery', import.meta.url)) const UI_EXPECTED = fileURLToPath(new URL('./expected/clickable-links-gallery/ui.expected.md', import.meta.url)) @@ -455,7 +455,7 @@ describe('web e2e: clickable links gallery', () => { await markdown.locator(`a[href="${HTTP_URL}"]`).click() await expect.poll(() => browserAddress.inputValue()).toBe(HTTP_URL) - await page.getByRole('button', { name: 'Settings', exact: true }).click() + await openSettingsFromAccountMenu(page, 'en') await page.getByRole('button', { name: 'Built-in browser', exact: true }).click() await page.getByRole('menuitem', { name: 'New browser tab', exact: true }).click() await expect.poll(() => scaffold.ctx.settings.describe().find(row => row.ns === 'ui-chat')?.value).toMatchObject({ linkOpening: 'new-tab' }) @@ -472,7 +472,7 @@ describe('web e2e: clickable links gallery', () => { } await page.reload() - await page.getByRole('button', { name: 'Settings', exact: true }).click() + await openSettingsFromAccountMenu(page, 'en') await page.getByRole('button', { name: 'New browser tab', exact: true }).click() await page.getByRole('menuitem', { name: 'Built-in browser', exact: true }).click() await expect.poll(() => scaffold.ctx.settings.describe().find(row => row.ns === 'ui-chat')?.value).toMatchObject({ linkOpening: 'sidebar' }) diff --git a/apps/web/tests/expected/onboarding-native/models.expected.md b/apps/web/tests/expected/onboarding-native/models.expected.md index 5676e6d44d..ce149bad3e 100644 --- a/apps/web/tests/expected/onboarding-native/models.expected.md +++ b/apps/web/tests/expected/onboarding-native/models.expected.md @@ -8,7 +8,7 @@ - button "打开配置文件" - button "关闭" - heading "模型" [level=2] - - paragraph: 填入各提供方的 API 密钥即可使用其模型。 + - paragraph: 填入各提供商的 API 密钥即可使用其模型。 - list: - listitem: - text: DeepSeek deepseek-official API 密钥 @@ -17,5 +17,4 @@ - group: 自定义设置 - button "取消" - button "保存" - - button "添加提供方" - - button "添加自定义提供方" + - button "添加模型提供商" diff --git a/apps/web/tests/plugin-manager.e2e.ts b/apps/web/tests/plugin-manager.e2e.ts index e20b5b93ed..f3ed5e5f19 100644 --- a/apps/web/tests/plugin-manager.e2e.ts +++ b/apps/web/tests/plugin-manager.e2e.ts @@ -14,7 +14,7 @@ import { SCAFFOLD_DEFAULTS_BUNDLE, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts' +import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, openSettingsFromAccountMenu, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/plugin-manager', import.meta.url)) const MANAGER_EXPECTED = join(SNAPSHOT_DIR, 'manager.expected.md') @@ -64,7 +64,7 @@ describe('web e2e: plugin manager', () => { const source = language === 'en' ? '中文' : 'English' const target = language === 'en' ? 'English' : '中文' if (await page.getByRole('dialog', { name: settings }).count() === 0) { - await page.getByRole('button', { name: settings, exact: true }).click() + await openSettingsFromAccountMenu(page, language === 'en' ? 'zh' : 'en') } await page.getByRole('dialog', { name: settings }).getByRole('button', { name: source }).click() await page.getByRole('menuitem', { name: target }).click() diff --git a/apps/web/tests/preview-boot.e2e.ts b/apps/web/tests/preview-boot.e2e.ts index 3e92ed1e34..b416eea019 100644 --- a/apps/web/tests/preview-boot.e2e.ts +++ b/apps/web/tests/preview-boot.e2e.ts @@ -316,6 +316,7 @@ async function bootPreview(origin: string, browser: Browser): Promise { const continueButton = page.getByRole('button', { name: 'Continue' }) await continueButton.waitFor({ timeout: HERO_TIMEOUT_MS }) await continueButton.click() + await page.getByRole('button', { name: 'Add API Key', exact: true }).click() const configureLater = page.getByRole('button', { name: 'Configure later' }) await configureLater.waitFor({ timeout: 30_000 }) await configureLater.click() diff --git a/packages/llm/llm-retry/tests/transport-recovery.spec.ts b/packages/llm/llm-retry/tests/transport-recovery.spec.ts index 8fd91ece9e..1f2a3262e4 100644 --- a/packages/llm/llm-retry/tests/transport-recovery.spec.ts +++ b/packages/llm/llm-retry/tests/transport-recovery.spec.ts @@ -15,8 +15,15 @@ import * as Retry from '../src/index.ts' let context: Context | undefined const servers: Promise[] = [] +let failureDiagnostics: (() => object) | undefined -afterEach(async () => { +afterEach(async (test) => { + const diagnose = failureDiagnostics + failureDiagnostics = undefined + // Capture the pending request before disposal aborts it and closes its socket. + if (test.task.result?.state === 'fail' && diagnose !== undefined) { + console.error('transport recovery failure:', JSON.stringify(diagnose())) + } const ownedContext = context context = undefined // A failed transport can still await a stalled socket while its context disposes. @@ -207,19 +214,44 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => { }) it('turns a stalled body into TIMEOUT and succeeds on the next request', async () => { + const started = performance.now() + let phase = 'starting server' + const observed: { server?: MockLlmServer; agent?: Agent } = {} + let idleSettled = false + failureDiagnostics = () => { + const events = observed.agent?.session.snapshotEvents() ?? [] + return { + node: process.version, + phase, + elapsedMs: Math.round(performance.now() - started), + idleSettled, + agentStatus: observed.agent?.status, + requestCount: observed.server?.requests.length ?? 0, + requests: observed.server?.requests.map(({ attempt, behavior, outcome, chunksSent }) => + ({ attempt, behavior, outcome, chunksSent })), + retries: events.filter(event => event.type === 'llm/retry').map(event => event.data), + lastEvents: events.slice(-10).map(event => ({ seq: event.seq, type: event.type })), + } + } const server = await start(['stall', 'success'], { apiKey: 'mock-key', successText: 'recovered after timeout', }) + observed.server = server + phase = 'mounting context' // This crosses the real HTTP idle timer, so leave scheduler slack between // the stalled attempt and the mock server's immediate successful response. context = await harness(server.baseURL, { streamIdleTimeoutMs: 1_000 }) + phase = 'creating agent' const agent = await context.agentLoop.create(SessionId('wire-stall'), { provider: 'deepseek-official', model: 'mock-model', }) - await sendAndWait(context, agent) + observed.agent = agent + phase = 'awaiting agent idle' + await sendAndWait(context, agent).finally(() => { idleSettled = true }) + phase = 'asserting recovery' expect(server.requests.map(record => record.behavior)).toEqual(['stall', 'success']) expect(agent.session.snapshotEvents().filter(event => event.type === 'llm/retry').map(event => event.data.failure.code)) diff --git a/snapshots/web/lifecycle-chrome/connection-error.expected.md b/snapshots/web/lifecycle-chrome/connection-error.expected.md index 6674078cb3..51ccb442fa 100644 --- a/snapshots/web/lifecycle-chrome/connection-error.expected.md +++ b/snapshots/web/lifecycle-chrome/connection-error.expected.md @@ -1,2 +1,2 @@ -- button "Settings" +- button "Account menu": Not signed in - button "Reconnecting, reconnect now": Reconnecting diff --git a/snapshots/web/lifecycle-chrome/hero.expected.md b/snapshots/web/lifecycle-chrome/hero.expected.md index ce6698be33..4d1f24ef42 100644 --- a/snapshots/web/lifecycle-chrome/hero.expected.md +++ b/snapshots/web/lifecycle-chrome/hero.expected.md @@ -11,7 +11,7 @@ - tree "Sessions": - treeitem "workspace" [expanded] - treeitem "New Session" [selected] -- button "Settings" +- button "Account menu": Not signed in - banner: - button "Open right sidebar" - text: Into the Unknown Preview diff --git a/snapshots/web/lifecycle-chrome/plan-active-zh.expected.md b/snapshots/web/lifecycle-chrome/plan-active-zh.expected.md index c01c19bc4b..67147659df 100644 --- a/snapshots/web/lifecycle-chrome/plan-active-zh.expected.md +++ b/snapshots/web/lifecycle-chrome/plan-active-zh.expected.md @@ -11,7 +11,7 @@ - tree "会话": - treeitem "workspace" [expanded] - treeitem "新会话" [selected] -- button "设置" +- button "账号菜单": 尚未登录 - banner: - button "打开右侧边栏" - text: 探索未至之境 预览版 diff --git a/snapshots/web/lifecycle-chrome/plan-active.expected.md b/snapshots/web/lifecycle-chrome/plan-active.expected.md index dee04dc9e8..9d0ee73cde 100644 --- a/snapshots/web/lifecycle-chrome/plan-active.expected.md +++ b/snapshots/web/lifecycle-chrome/plan-active.expected.md @@ -11,7 +11,7 @@ - tree "Sessions": - treeitem "workspace" [expanded] - treeitem "New Session" [selected] -- button "Settings" +- button "Account menu": Not signed in - banner: - button "Open right sidebar" - text: Into the Unknown Preview