mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/master' into docs/post-v3-release-proofreading
# Conflicts: # packages/client/ui-models/README.i18n.yaml # packages/client/ui-models/README.zh.md
This commit is contained in:
128
apps/web/tests/onboarding-usable-provider.e2e.ts
Normal file
128
apps/web/tests/onboarding-usable-provider.e2e.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
// Keyless browser e2e: a user who configures some OTHER provider is not asked
|
||||
// for the official DeepSeek key again, and the first-run setup card is a card
|
||||
// they can close. The shipped DeepSeek adapter stays mounted without a
|
||||
// credential throughout, so the only thing that ends onboarding here is the
|
||||
// pi-ai route the user configures through the real wire. Zero model calls:
|
||||
// configuration is pure settings/credentials/llm-domain traffic.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
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 {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/onboarding-usable-provider', import.meta.url))
|
||||
const DISMISSED_EXPECTED = join(SNAPSHOT_DIR, 'dismissed.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
const CREDENTIAL_STEP = '添加一个 API Key 开始使用'
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: another usable provider ends first-run onboarding', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true })
|
||||
browser = await chromium.launch()
|
||||
// The scenario asserts the shipped Chinese copy, so the browser asks for it.
|
||||
page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE })
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('closes the setup card without discarding the add card beside it', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-setup-card-cancel'))
|
||||
const credentialStep = page.getByRole('region', { name: CREDENTIAL_STEP })
|
||||
await credentialStep.waitFor({ timeout: 15_000 })
|
||||
await credentialStep.getByRole('button', { name: '前往配置' }).click()
|
||||
await credentialStep.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.waitFor({ timeout: 10_000 })
|
||||
// Nothing is reachable yet, so DeepSeek presents itself as its open card.
|
||||
const setupKey = settings.getByRole('textbox', { name: 'API 密钥', exact: true })
|
||||
await setupKey.waitFor({ timeout: 10_000 })
|
||||
|
||||
const add = settings.getByRole('button', { name: '添加提供方' })
|
||||
await expect.poll(async () => add.isEnabled(), { timeout: 10_000 }).toBe(true)
|
||||
await add.click()
|
||||
const pick = settings.getByLabel('提供方')
|
||||
await pick.waitFor({ timeout: 10_000 })
|
||||
await pick.selectOption('minimax-cn')
|
||||
await expect.poll(
|
||||
async () => settings.getByRole('textbox', { name: 'API 密钥', exact: true }).count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(2)
|
||||
|
||||
// Cancelling the setup card is the regression: it used to leave itself open
|
||||
// and close the add card, discarding that draft.
|
||||
await settings.getByRole('button', { name: '取消', exact: true }).first().click()
|
||||
expect(await settings.getByLabel('提供方').count()).toBe(1)
|
||||
await expect.poll(
|
||||
async () => settings.getByRole('textbox', { name: 'API 密钥', exact: true }).count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(1)
|
||||
// DeepSeek is now an ordinary row: a missing-key dot and an Edit button.
|
||||
await settings.getByRole('button', { name: '编辑 DeepSeek (deepseek-official)' }).waitFor({ timeout: 10_000 })
|
||||
const dismissed = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(DISMISSED_EXPECTED, dismissed, MODE)
|
||||
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('stops prompting for DeepSeek once the other provider can serve requests', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-other-provider'))
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.getByRole('textbox', { name: 'API 密钥', exact: true }).fill('sk-e2e-minimax')
|
||||
await settings.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await settings.getByText('已保存 minimax-cn。', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
|
||||
// Only minimax-cn is reachable; DeepSeek still holds no credential.
|
||||
const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
|
||||
expect(document).toContain('apiKeyEnv: MINIMAX_CN_API_KEY')
|
||||
const credentials = await readFile(join(scaffold.harnessHome, '.credentials.yaml'), 'utf8')
|
||||
expect(credentials).toContain('MINIMAX_CN_API_KEY: sk-e2e-minimax')
|
||||
expect(credentials).not.toContain('DEEPSEEK_API_KEY')
|
||||
|
||||
const warningsBefore = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 15_000 })
|
||||
// The regression: the step read only the official route's credential, so a
|
||||
// fully configured user was taken over on every blank session.
|
||||
await expect.poll(
|
||||
async () => page.getByRole('region', { name: CREDENTIAL_STEP }).count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(0)
|
||||
expect(await page.locator('[class*="onboardingStage"]').count()).toBe(0)
|
||||
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(false)
|
||||
|
||||
// The Models page agrees: DeepSeek stays a row rather than reopening its
|
||||
// setup card over a user who already has somewhere to send a request.
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
await settings.waitFor({ timeout: 10_000 })
|
||||
await settings.getByRole('button', { name: '模型' }).click()
|
||||
await settings.getByRole('button', { name: '编辑 DeepSeek (deepseek-official)' }).waitFor({ timeout: 10_000 })
|
||||
expect(await settings.getByRole('textbox', { name: 'API 密钥', exact: true }).count()).toBe(0)
|
||||
|
||||
expect((await page.content()).includes('sk-e2e-minimax')).toBe(false)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['dismissed.expected.md'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
- dialog "设置":
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置":
|
||||
- img
|
||||
- text: 通用设置
|
||||
- button "模型":
|
||||
- img
|
||||
- text: 模型
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件配置":
|
||||
- img
|
||||
- text: 插件配置
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
- text: 关闭
|
||||
- heading "模型" [level=2]
|
||||
- paragraph: 填入各提供方的 API 密钥即可使用其模型。
|
||||
- list:
|
||||
- listitem:
|
||||
- text: DeepSeek
|
||||
- img "API 密钥缺失"
|
||||
- button "编辑 DeepSeek (deepseek-official)": 编辑
|
||||
- text: 提供方
|
||||
- combobox "提供方":
|
||||
- option "amazon-bedrock"
|
||||
- option "ant-ling"
|
||||
- option "anthropic"
|
||||
- option "azure-openai-responses"
|
||||
- option "cerebras"
|
||||
- option "cloudflare-ai-gateway"
|
||||
- option "cloudflare-workers-ai"
|
||||
- option "deepseek"
|
||||
- option "fireworks"
|
||||
- option "github-copilot"
|
||||
- option "google"
|
||||
- option "google-vertex"
|
||||
- option "groq"
|
||||
- option "huggingface"
|
||||
- option "kimi-coding"
|
||||
- option "minimax"
|
||||
- option "minimax-cn" [selected]
|
||||
- option "mistral"
|
||||
- option "moonshotai"
|
||||
- option "moonshotai-cn"
|
||||
- option "nvidia"
|
||||
- option "openai"
|
||||
- option "openai-codex"
|
||||
- option "opencode"
|
||||
- option "opencode-go"
|
||||
- option "openrouter"
|
||||
- option "qwen-token-plan"
|
||||
- option "qwen-token-plan-cn"
|
||||
- option "together"
|
||||
- option "vercel-ai-gateway"
|
||||
- option "xai"
|
||||
- option "xiaomi"
|
||||
- option "xiaomi-token-plan-ams"
|
||||
- option "xiaomi-token-plan-cn"
|
||||
- option "xiaomi-token-plan-sgp"
|
||||
- option "zai"
|
||||
- option "zai-coding-cn"
|
||||
- text: API 密钥
|
||||
- textbox "API 密钥":
|
||||
- /placeholder: 输入 API 密钥,或留空使用环境认证
|
||||
- group: 自定义设置
|
||||
- button "取消"
|
||||
- button "保存"
|
||||
@@ -74,12 +74,16 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () =
|
||||
await input.fill(prompt)
|
||||
await input.press('Enter')
|
||||
|
||||
const workflow = page.getByRole('button', { name: /^snapshot-flow/ })
|
||||
const workflow = page.locator('[data-workflow-run][data-run-status="running"]')
|
||||
await workflow.waitFor({ timeout: 30_000 })
|
||||
expect(await workflow.getAttribute('aria-expanded')).toBe('true')
|
||||
const phase = page.getByRole('button', { name: /^Run/ })
|
||||
await phase.waitFor({ timeout: 15_000 })
|
||||
await phase.click()
|
||||
const disclosures = workflow.locator('[data-disclosure-row]')
|
||||
await disclosures.nth(1).waitFor({ timeout: 15_000 })
|
||||
expect(await disclosures.nth(0).getAttribute('role')).toBeNull()
|
||||
expect(await disclosures.nth(0).getAttribute('aria-expanded')).toBeNull()
|
||||
expect(await disclosures.nth(1).getAttribute('role')).toBeNull()
|
||||
expect(await disclosures.nth(1).getAttribute('aria-expanded')).toBeNull()
|
||||
expect(await disclosures.nth(0).evaluate(element => getComputedStyle(element).cursor)).not.toBe('pointer')
|
||||
expect(await disclosures.nth(1).evaluate(element => getComputedStyle(element).cursor)).not.toBe('pointer')
|
||||
const member = page.getByRole('button', { name: /^Open Reply with exactly the word/ })
|
||||
await member.waitFor({ timeout: 15_000 })
|
||||
await member.focus()
|
||||
@@ -139,15 +143,20 @@ 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 page.locator('[data-workflow-run][data-run-status="completed"]').waitFor()
|
||||
|
||||
expect(await page.locator('[data-chat-flow-kind="tool-call"]').count()).toBeGreaterThanOrEqual(1)
|
||||
expect(await page.locator('[data-chat-flow-kind="workflow-run"]').count()).toBe(1)
|
||||
const terminalWorkflow = page.getByRole('button', { name: /^snapshot-flow/ })
|
||||
await terminalWorkflow.waitFor()
|
||||
if (await terminalWorkflow.getAttribute('aria-expanded') !== 'true') await terminalWorkflow.click()
|
||||
expect(await terminalWorkflow.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(await terminalWorkflow.evaluate(element => getComputedStyle(element).cursor)).toBe('pointer')
|
||||
await terminalWorkflow.click()
|
||||
const terminalPhase = page.getByRole('button', { name: /^Run/ })
|
||||
await terminalPhase.waitFor()
|
||||
if (await terminalPhase.getAttribute('aria-expanded') !== 'true') await terminalPhase.click()
|
||||
expect(await terminalPhase.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(await terminalPhase.evaluate(element => getComputedStyle(element).cursor)).toBe('pointer')
|
||||
await terminalPhase.click()
|
||||
await page.getByText(CHILD_PROMPT, { exact: false }).waitFor()
|
||||
await expect.poll(
|
||||
() => page.getByRole('button', { name: /^Open Reply with exactly the word/ }).count(),
|
||||
@@ -165,6 +174,7 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () =
|
||||
await workflow.click()
|
||||
const phase = page.getByRole('button', { name: /^Run/ })
|
||||
await phase.waitFor()
|
||||
expect(await phase.getAttribute('aria-expanded')).toBe('false')
|
||||
await phase.click()
|
||||
await page.getByText(CHILD_PROMPT, { exact: false }).waitFor()
|
||||
expect(await page.getByRole('button', { name: /^Open Reply with exactly the word/ }).count()).toBe(0)
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"tests/default-model.e2e.ts",
|
||||
"tests/declared-reasoning.e2e.ts",
|
||||
"tests/onboarding-deepseek-config.e2e.ts",
|
||||
"tests/onboarding-usable-provider.e2e.ts",
|
||||
"tests/remote-welcome.e2e.ts",
|
||||
"tests/workspace-management.e2e.ts",
|
||||
"tests/replay-round-trip.e2e.ts",
|
||||
|
||||
Reference in New Issue
Block a user