test(web): align unavailable model and cold session expectations

This commit is contained in:
lsdsjy committed 2026-09-23 21:33:16 +08:00
1 parent 71f0713b86
commit 9a9948bc83
5 files changed
+20 -30

No files matched your search

@@ -90,8 +90,11 @@ describe.skipIf(webSnapshotMode() === 'record')('web e2e: DeepSeek Messages sett
await scaffold.ctx.agentDefaultModel.saveSelection({ provider: 'deepseek-official', model: 'deepseek-v4-flash' })
await page.reload({ waitUntil: 'load' })
const input = page.locator('[data-composer-input]').first()
await expect.poll(() => input.getAttribute('contenteditable')).toBe('false')
await page.getByRole('button', { name: '请选择模型', exact: true }).click()
const trigger = page.getByRole('button', { name: /^选择模型.*deepseek-official\/deepseek-v4-flash/ })
await trigger.waitFor()
await expect.poll(() => input.getAttribute('contenteditable')).toBe('true')
await trigger.click()
await page.getByRole('menuitem', { name: /模型/ }).click()
await page.getByRole('menuitemradio', { name: 'Messages Flash', exact: true }).click()
await expect.poll(() => input.getAttribute('contenteditable')).toBe('true')
await expect.poll(() => scaffold.ctx.agentDefaultModel.currentSelection().provider).toBe('deepseek-official')
+6 -20
View File
@@ -127,7 +127,7 @@ describe('web e2e: the composer model switch is the default for later sessions',
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
it('goes inert when the route the default names stops being served', async () => {
it('retains the saved route and editable composer when its provider is removed', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-default-model-blocked'))
const box = page.locator('[data-composer-input]').first()
await expect.poll(async () => box.isEnabled(), { timeout: 10_000 }).toBe(true)
@@ -139,28 +139,14 @@ describe('web e2e: the composer model switch is the default for later sessions',
baseURL: 'https://gateway.origin.example/v1', models: [{ id: START_MODEL, name: 'Origin Large' }] },
} })
await expect.poll(async () => box.isEnabled(), { timeout: 15_000 }).toBe(false)
const unset = page.getByRole('button', { name: '请选择模型', exact: true })
await unset.waitFor()
expect(await unset.evaluate(element => getComputedStyle(element).fontWeight)).toBe('400')
expect(await page.getByRole('button', { name: '发送消息', exact: true }).isEnabled()).toBe(false)
const seat = page.getByRole('button', { name: new RegExp(`^选择模型.*${ROUTE}/${MODEL}`) })
await seat.waitFor()
expect(await box.isEnabled()).toBe(true)
expect(scaffold.ctx.agentDefaultModel.currentSelection()).toMatchObject({ provider: ROUTE, model: MODEL })
const aria = await captureStableAria(page, '[data-composer-card]', scaffold.workspaceCwd)
await compareOrRefreshGolden(fileURLToPath(new URL('./expected/default-model/unselected.expected.md', import.meta.url)), aria, webSnapshotMode())
// The block is an affordance; the refusal is the Host's. A client that
// never disabled anything still cannot start a turn on a dead route.
await expect(scaffold.ctx.sessionController.prompt({
requestId: 'default-model-refused' as never,
sessionId: SessionId(await createSession('default-model-refusal')),
mode: 'queue',
content: [{ type: 'text', text: 'hi' }],
}, new AbortController().signal)).rejects.toMatchObject({ code: 'session/model-unavailable' })
// The way out stays open. Locking the model seat with everything else
// would leave the composer asking for the one thing it prevents.
const seat = page.getByRole('button', { name: '请选择模型', exact: true })
expect(await seat.isEnabled()).toBe(true)
await seat.click()
await page.getByRole('menuitem', { name: /模型/ }).click()
await page.getByRole('menuitemradio').first().click()
await expect.poll(async () => box.isEnabled(), { timeout: 15_000 }).toBe(true)
expect(tripwire.pageErrors).toEqual([])
@@ -1,5 +1,5 @@
- textbox "描述你想要构建的内容, / 调用指令, @ 文件或对话" [disabled]
- button "添加文件或调用指令" [disabled]
- button "访问模式,当前:工作区内修改" [disabled]: 工作区内修改
- button "请选择模型"
- textbox "描述你想要构建的内容, / 调用指令, @ 文件或对话"
- button "添加文件或调用指令"
- button "访问模式,当前:工作区内修改": 工作区内修改
- button "选择模型,当前 acme-gateway/acme-large": acme-gateway/acme-large
- button "发送消息" [disabled]
@@ -275,9 +275,10 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
// trigger — on the page; the scaffold boots without one.
await connectFreshWorkspaceZh(page, scaffold.workspaceCwd, 'model-fallback-e2e', false)
const modelTrigger = page.getByRole('button', { name: '请选择模型', exact: true })
const modelTrigger = page.getByRole('button', { name: /^选择模型.*deepseek-official\// })
await modelTrigger.waitFor({ timeout: 10_000 })
await modelTrigger.click()
await page.getByRole('menuitem', { name: /模型/ }).click()
expect(await page.getByText('Configured Flash', { exact: true }).count()).toBe(0)
await page.getByRole('menuitemradio', { name: 'Private Preview' }).waitFor({ timeout: 10_000 })
expect(tripwire.warnings).toEqual([])
+3 -3
View File
@@ -24,6 +24,7 @@ import {
compareOrRefreshGolden,
launchWebScaffold,
seedSession,
readPersistedEvents,
watchConsole,
webSnapshotMode,
type WebScaffold,
@@ -403,9 +404,8 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
await target.waitFor({ timeout: 15_000 })
await target.click()
await page.locator('[data-chat-flow-kind="user"]').filter({ hasText: 'Research notes' }).waitFor({ timeout: 15_000 })
const session = scaffold.ctx.sessions.get(SessionId(TARGET_SESSION_ID))
if (session === undefined) throw new Error('reference target session is unavailable')
const inputs = session.snapshotEvents().filter(event => event.type === 'user/message')
const events = await readPersistedEvents(scaffold, SessionId(TARGET_SESSION_ID))
const inputs = events.filter(event => event.type === 'user/message')
expect(inputs.map(event => event.data.source.kind)).toEqual(['user', 'session-reference'])
expect(inputs[0]?.seq).toBeLessThan(inputs[1]!.seq)
expect(JSON.stringify(inputs[1]?.data)).toContain('<referenced-sessions>snapshot</referenced-sessions>')