feat(web): route onboarding to Models settings

This commit is contained in:
Yichen Jiang
2026-07-30 13:11:07 +08:00
parent 7fc1b5a777
commit 42d0f3c7ba
16 changed files with 83 additions and 349 deletions
@@ -1,7 +1,6 @@
// Keyless browser e2e: the shipped DeepSeek adapter stays mounted while its
// credential is absent, onboarding writes the effective reference through
// the real wire into an isolated harness home, and the live page converges
// without a reload or model call.
// credential is absent, onboarding routes to the real Models editor, and its
// write lands in an isolated harness home without a reload or model call.
import { randomBytes } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
@@ -43,16 +42,23 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
it('stores a key write-only and observes configured state without restarting', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-config'))
const dialog = page.getByRole('dialog', { name: '添加 DeepSeek API 密钥' })
const dialog = page.getByRole('dialog', { name: '添加一个 API Key 开始使用' })
await dialog.waitFor({ timeout: 15_000 })
expect(await dialog.getByLabel('提供方').inputValue()).toBe('DeepSeek')
expect(await dialog.getByRole('textbox').count()).toBe(0)
const initial = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(MISSING_EXPECTED, initial, MODE)
const secret = `dsh_onboarding_${randomBytes(12).toString('hex')}`
await dialog.getByLabel('API 密钥', { exact: true }).fill(secret)
await dialog.getByRole('button', { name: '保存并继续' }).click()
await dialog.getByRole('button', { name: '前往配置' }).click()
await dialog.waitFor({ state: 'detached', timeout: 15_000 })
const settings = page.getByRole('dialog', { name: '设置' })
await settings.waitFor({ timeout: 10_000 })
const keyInput = settings.getByLabel('API 密钥', { exact: true })
await keyInput.waitFor({ timeout: 10_000 })
const secret = `dsh_onboarding_${randomBytes(12).toString('hex')}`
await keyInput.fill(secret)
await settings.getByRole('button', { name: '保存', exact: true }).click()
await keyInput.waitFor({ state: 'detached', timeout: 15_000 })
const stored = await readFile(join(scaffold.harnessHome, '.env'), 'utf8')
expect(stored.includes(`DEEPSEEK_API_KEY=${secret}`)).toBe(true)
@@ -60,18 +66,15 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
expect((await page.locator('body').ariaSnapshot()).includes(secret)).toBe(false)
expect(browserConsole.some(line => line.includes(secret))).toBe(false)
// The same running composition reuses the refreshed join. Opening Models
// and its write-only key field proves the configured view without reload.
await page.getByRole('button', { name: '设置', exact: true }).click()
const settings = page.getByRole('dialog', { name: '设置' })
await settings.getByRole('button', { name: '模型' }).click()
// The same open Models surface reuses the refreshed join and exposes the
// configured write-only placeholder without a reload.
const deepSeekRow = settings.getByText('DeepSeek', { exact: true }).first()
await deepSeekRow.waitFor({ timeout: 10_000 })
await deepSeekRow.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click()
const keyInput = settings.getByLabel('API 密钥', { exact: true })
await keyInput.waitFor({ timeout: 10_000 })
const configuredInput = settings.getByLabel('API 密钥', { exact: true })
await configuredInput.waitFor({ timeout: 10_000 })
await expect.poll(
() => keyInput.getAttribute('placeholder'),
() => configuredInput.getAttribute('placeholder'),
{ timeout: 10_000 },
).toBe('已配置——输入新值可替换')
@@ -1,12 +1,6 @@
- dialog "添加 DeepSeek API 密钥":
- heading "添加 DeepSeek API 密钥" [level=2]
- dialog "添加一个 API Key 开始使用":
- heading "添加一个 API Key 开始使用" [level=2]
- button "稍后配置":
- img
- paragraph: 配置 DeepSeek 官方模型,即可开始使用。
- text: 提供方
- textbox "提供方": DeepSeek
- text: API 密钥
- textbox "API 密钥":
- /placeholder: 输入 DeepSeek API 密钥
- button "模型高级设置"
- button "保存并继续" [disabled]
- button "前往配置"