Merge pull request #5815 from deepseek-harness/test/web-cot-toggle-flake-20261007
test(web): bound plugin switch waits by the profile write chain
This commit is contained in:
commit
800ae866ca
8 files changed
+43
-23
No files matched your search
@@ -6,8 +6,8 @@
|
||||
en: 3e8ecd3509fa944f
|
||||
zh: 3fd3ff9fcd1e1e90
|
||||
/apps-web-browser-e2e/completion-observations:
|
||||
en: ae500691f64d1e43
|
||||
zh: 2bfc53b52d210b24
|
||||
en: 4a06ba5463b53bb3
|
||||
zh: 70863c70e15c6746
|
||||
/apps-web-browser-e2e/these-are-host-face-tests:
|
||||
en: 8c406d423c8e2137
|
||||
zh: a66a144fd146f56c
|
||||
|
||||
@@ -20,6 +20,8 @@ State-sensitive cases use Workspace, admission, attachment, and model-stream bar
|
||||
|
||||
[Idle-submission handoff](idle-submission-handoff.e2e.ts) waits up to the scaffold's 30-second turn-completion budget for Host frames and browser placement. Vitest owns scaffold teardown so a replay-consumption error does not replace the failed scenario assertion.
|
||||
|
||||
Plugin-manager switches wait up to `PLUGIN_TOGGLE_SETTLE_MS` (10 seconds) for the Host's profile write and the store's follow-up inventory read, the two steps that republish `aria-checked`; that constant's JSDoc records the measured chain.
|
||||
|
||||
Explicit scrolling uses `scrollIntoView` from `support.ts`: it resolves the locator again when its old element detaches and checks connection in the same browser task as native scrolling. Scenarios retain their visibility and geometry assertions after scrolling.
|
||||
|
||||
## These are Host-face tests
|
||||
|
||||
@@ -20,6 +20,8 @@ pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps
|
||||
|
||||
[空闲提交交接](idle-submission-handoff.e2e.ts)对 Host 帧和浏览器中的消息归位的等待使用 scaffold 的 30 秒 Turn 完成预算。Vitest 负责 scaffold 清理,使 replay 消耗检查错误不会覆盖场景中失败的断言。
|
||||
|
||||
插件管理器的开关为 Host 的 profile 写入与 store 随后的 inventory 读取预留 `PLUGIN_TOGGLE_SETTLE_MS`(10 秒),这两步完成后才会重新发布 `aria-checked`;链路的实测值记录在该常量的 JSDoc 中。
|
||||
|
||||
显式滚动使用 `support.ts` 的 `scrollIntoView`:旧元素脱离 DOM 时重新解析 locator,并在同一个浏览器任务中检查连接状态、执行原生滚动。各场景保留滚动后的可见性与几何断言。
|
||||
|
||||
## 这些是 Host 面的测试
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, readPersistedEvents, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspaceZh, expandOwningTurnProcess, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
import {
|
||||
connectFreshWorkspaceZh, expandOwningTurnProcess, PLUGIN_TOGGLE_SETTLE_MS, saveFailureShot, ZH_BROWSER_LOCALE,
|
||||
} from './support.ts'
|
||||
|
||||
const BUNDLE_NAME = '@deepseek-ai/dsh-experimental-cot-translation-bundle'
|
||||
const BUNDLE = fileURLToPath(new URL('../../../packages/experimental/cot-translation-bundle', import.meta.url))
|
||||
@@ -168,7 +170,8 @@ it.skipIf(MODE === 'record')('reuses saved native Account Flash translations acr
|
||||
const panel = settings.locator('[data-plugin-panel]'), card = panel.locator(`[data-plugin-package="${BUNDLE_NAME}"]`)
|
||||
const toggle = card.getByRole('switch'); await toggle.waitFor()
|
||||
expect(await toggle.getAttribute('aria-checked')).toBe('false')
|
||||
await toggle.click(); await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true')
|
||||
await toggle.click()
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('true')
|
||||
await card.getByRole('button').first().click()
|
||||
const provider = panel.getByLabel('翻译服务', { exact: true })
|
||||
await provider.waitFor(); expect(await provider.inputValue()).toBe('bing')
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, readPersistedEvents, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspaceZh, expandOwningTurnProcess, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
import {
|
||||
connectFreshWorkspaceZh, expandOwningTurnProcess, PLUGIN_TOGGLE_SETTLE_MS, saveFailureShot, ZH_BROWSER_LOCALE,
|
||||
} from './support.ts'
|
||||
|
||||
const BUNDLE_NAME = '@deepseek-ai/dsh-experimental-cot-translation-bundle'
|
||||
const BUNDLE = fileURLToPath(new URL('../../../packages/experimental/cot-translation-bundle', import.meta.url))
|
||||
@@ -198,7 +200,7 @@ it.skipIf(MODE === 'record')('persists Bing reasoning translation across reopen
|
||||
await toggle.waitFor()
|
||||
expect(await toggle.getAttribute('aria-checked')).toBe('false')
|
||||
await toggle.click()
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true')
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('true')
|
||||
await card.getByRole('button').first().click()
|
||||
const provider = panel.getByLabel('翻译服务', { exact: true })
|
||||
const target = panel.getByLabel('目标语言', { exact: true })
|
||||
@@ -315,7 +317,7 @@ it.skipIf(MODE === 'record')('persists Bing reasoning translation across reopen
|
||||
|
||||
await panel.getByRole('button', { name: '返回插件列表', exact: true }).click()
|
||||
await toggle.click()
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('false')
|
||||
await body.waitFor({ state: 'detached' })
|
||||
await reasoning.getByText(ORIGINAL, { exact: true }).waitFor()
|
||||
await reasoning.locator('[data-disclosure-row]').first().click()
|
||||
|
||||
@@ -16,7 +16,9 @@ import {
|
||||
SCAFFOLD_DEFAULTS_BUNDLE, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, openSettings, saveFailureShot } from './support.ts'
|
||||
import {
|
||||
ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, openSettings, PLUGIN_TOGGLE_SETTLE_MS, 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')
|
||||
@@ -643,7 +645,7 @@ describe('web e2e: plugin manager', () => {
|
||||
await toggle.click()
|
||||
try {
|
||||
await expect.poll(() => teamRows().filter(entry => entry.fiber?.state === FiberState.ACTIVE).length, { timeout: 20_000 }).toBe(3)
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true')
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('true')
|
||||
await action.waitFor({ timeout: 20_000 })
|
||||
await action.getByRole('button', { name: '智能体团队', exact: true }).click()
|
||||
const teamPanel = teamPage.getByRole('dialog', { name: '智能体团队', exact: true })
|
||||
@@ -897,10 +899,10 @@ describe('web e2e: plugin manager', () => {
|
||||
const bundles = async () => (JSON.parse(await homeFile('profiles', 'scaffold', 'package.json')) as {
|
||||
dsh: { profile: { bundles: string[] } }
|
||||
}).dsh.profile.bundles
|
||||
await expect.poll(bundles, { timeout: 10_000 }).toEqual([...SCAFFOLD_BUNDLES, '@fixture/bundle'])
|
||||
await expect.poll(bundles, { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toEqual([...SCAFFOLD_BUNDLES, '@fixture/bundle'])
|
||||
// A live profile: the row mounts once the whole tree recomposed, the switch is on, and nothing waits for a restart.
|
||||
await expect.poll(() => mounted()?.fiber?.state, { timeout: 20_000 }).toBe(2)
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: 10_000 }).toBe('true')
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('true')
|
||||
expect(await panel.getByText(/下次启动生效/).count()).toBe(0)
|
||||
const snapshot = await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(LIVE_EXPECTED, snapshot, MODE)
|
||||
@@ -910,8 +912,8 @@ describe('web e2e: plugin manager', () => {
|
||||
await rowSwitch.waitFor({ timeout: 10_000 })
|
||||
expect(await rowSwitch.getAttribute('aria-checked')).toBe('true')
|
||||
await rowSwitch.click()
|
||||
await expect.poll(async () => (await homeFile('profiles', 'scaffold', 'cordis.patch.yml')).includes('fixture-row'), { timeout: 10_000 }).toBe(true)
|
||||
await expect.poll(() => rowSwitch.getAttribute('aria-checked'), { timeout: 10_000 }).toBe('false')
|
||||
await expect.poll(async () => (await homeFile('profiles', 'scaffold', 'cordis.patch.yml')).includes('fixture-row'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe(true)
|
||||
await expect.poll(() => rowSwitch.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('false')
|
||||
// A disabled entry keeps its disposed fiber; only an active one counts as mounted.
|
||||
await expect.poll(() => mounted()?.fiber?.state, { timeout: 20_000 }).not.toBe(2)
|
||||
await rowSwitch.click()
|
||||
@@ -920,7 +922,7 @@ describe('web e2e: plugin manager', () => {
|
||||
|
||||
await toggle.click()
|
||||
await expect.poll(() => mounted()?.fiber?.state, { timeout: 20_000 }).not.toBe(2)
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('false')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 90_000)
|
||||
|
||||
@@ -959,8 +961,8 @@ describe('web e2e: startup-applied plugin management', () => {
|
||||
expect(mounted()?.fiber?.state).toBeUndefined()
|
||||
await toggle.click()
|
||||
// The selection is saved and the switch turns on, but nothing mounts before the next start; a toast says so.
|
||||
await expect.poll(bundles).toEqual([...SCAFFOLD_BUNDLES, '@fixture/bundle'])
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true')
|
||||
await expect.poll(bundles, { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toEqual([...SCAFFOLD_BUNDLES, '@fixture/bundle'])
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('true')
|
||||
await page.getByText('更改将在下次启动生效', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
expect(mounted()?.fiber?.state).toBeUndefined()
|
||||
// The pack's page lists its rows from their declarations, with no live entry to switch.
|
||||
@@ -970,8 +972,8 @@ describe('web e2e: startup-applied plugin management', () => {
|
||||
await panel.getByRole('button', { name: '返回插件列表' }).click()
|
||||
|
||||
await toggle.click()
|
||||
await expect.poll(bundles).toEqual(SCAFFOLD_BUNDLES)
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
|
||||
await expect.poll(bundles, { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toEqual(SCAFFOLD_BUNDLES)
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('false')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
} finally {
|
||||
await browser?.close()
|
||||
|
||||
@@ -41,6 +41,15 @@ export const ZH_BROWSER_LOCALE = 'zh-CN'
|
||||
/** Same-day anchor for seeded event times and the Asia/Shanghai browser clock. */
|
||||
export const WEB_FIXTURE_TIME = Date.parse('2026-01-15T12:00:00+08:00')
|
||||
|
||||
/**
|
||||
* Budget for a plugin-manager switch to republish its state after a click: the
|
||||
* click starts a Host profile write, and the store's follow-up `load()` re-reads
|
||||
* its inventory before the switch shows the new value. That chain measured
|
||||
* 0.5-0.8 s on an idle host and 2.0-4.8 s across 32 samples in a 16-worker lane,
|
||||
* so Vitest's 1-second `expect.poll` default expires first.
|
||||
*/
|
||||
export const PLUGIN_TOGGLE_SETTLE_MS = 10_000
|
||||
|
||||
/** How often a pinned browser clock advances, in milliseconds. */
|
||||
const PINNED_CLOCK_STEP_MS = 250
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {} from '@deepseek-ai/dsh-experimental-worktree'
|
||||
import { chromium } from 'playwright'
|
||||
import { expect, it, onTestFailed, onTestFinished } from 'vitest'
|
||||
import { launchWebScaffold, watchConsole } from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, PLUGIN_TOGGLE_SETTLE_MS, saveFailureShot } from './support.ts'
|
||||
|
||||
const BUNDLE = '@deepseek-ai/dsh-experimental-tool-worktree'
|
||||
const exec = promisify(execFile)
|
||||
@@ -80,8 +80,8 @@ it('persists the Git Worktrees switch and removes its capability while retaining
|
||||
expect(tools()).not.toContain('create_worktree')
|
||||
|
||||
await toggle.click()
|
||||
await expect.poll(() => rows().length).toBe(2)
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true')
|
||||
await expect.poll(() => rows().length, { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe(2)
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('true')
|
||||
expect(await selectedBundles()).toContain(BUNDLE)
|
||||
expect(tools()).toContain('create_worktree')
|
||||
const created = await scaffold.ctx.worktrees.create(agent, { name: 'gui-opt-in' })
|
||||
@@ -95,8 +95,8 @@ it('persists the Git Worktrees switch and removes its capability while retaining
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true')
|
||||
expect(tools()).toContain('create_worktree')
|
||||
await toggle.click()
|
||||
await expect.poll(() => rows().length).toBe(0)
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
|
||||
await expect.poll(() => rows().length, { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe(0)
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('false')
|
||||
expect(await selectedBundles()).not.toContain(BUNDLE)
|
||||
expect(scaffold.ctx.get('worktrees')).toBeUndefined()
|
||||
expect(tools()).not.toContain('create_worktree')
|
||||
|
||||
Reference in new issue
Block a user