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:
Tianyi Cui authored and GitHub committed 2026-10-07 12:53:40 +08:00
commit 800ae866ca
8 files changed
+43 -23

No files matched your search

+2 -2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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 面的测试
+5 -2
View File
@@ -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')
+5 -3
View File
@@ -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()
+13 -11
View File
@@ -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()
+9
View File
@@ -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
+5 -5
View File
@@ -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')