test(web): stabilize idle submission handoff waits
This commit is contained in:
1 parent
41ede2f572
commit
8c24036e20
4 files changed
+39
-28
No files matched your search
@@ -6,8 +6,8 @@
|
||||
en: 3e8ecd3509fa944f
|
||||
zh: 3fd3ff9fcd1e1e90
|
||||
/apps-web-browser-e2e/completion-observations:
|
||||
en: 263a34ba5c63e316
|
||||
zh: ac1b79f39dde721f
|
||||
en: ae500691f64d1e43
|
||||
zh: ef8d84b9907ab67d
|
||||
/apps-web-browser-e2e/these-are-host-face-tests:
|
||||
en: 8c406d423c8e2137
|
||||
zh: a66a144fd146f56c
|
||||
|
||||
@@ -18,6 +18,8 @@ Ordinary scenarios begin with no registered Workspace or Session and a durable m
|
||||
|
||||
State-sensitive cases use Workspace, admission, attachment, and model-stream barriers to separate visible intermediate states from completed operations. Model-picker persistence assertions wait for the saved default, independently of menu closure. Details close waits for frame transitions; archive verification assigns an explicit title to the seeded Session and follows that identity across reload. The [CI test reliability workflow](../../../.agents/skills/dsh-ci-test-reliability/SKILL.md#synchronize-on-state) owns these synchronization rules.
|
||||
|
||||
[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.
|
||||
|
||||
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
|
||||
|
||||
@@ -18,6 +18,8 @@ pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps
|
||||
|
||||
依赖状态的用例使用 Workspace、接纳、附件和模型流屏障,区分可见中间状态与已完成操作。模型选择器的持久化断言等待默认设置保存完成,不以菜单关闭作为完成信号。详情关闭等待框架过渡结束;归档验证为 seed Session 设置显式标题,并跨重载跟踪该身份。[CI 测试可靠性工作流](../../../.agents/skills/dsh-ci-test-reliability/SKILL.md#synchronize-on-state)负责这些同步规则。
|
||||
|
||||
[空闲提交交接](idle-submission-handoff.e2e.ts)对 Host 帧和浏览器位置的等待使用 scaffold 的 30 秒 Turn 完成预算。Vitest 负责 scaffold 清理,使 replay 消耗检查错误不会覆盖场景中失败的断言。
|
||||
|
||||
显式滚动使用 `support.ts` 的 `scrollIntoView`:旧元素脱离 DOM 时重新解析 locator,并在同一个浏览器任务中检查连接状态、执行原生滚动。各场景保留滚动后的可见性与几何断言。
|
||||
|
||||
## 这些是 Host 面的测试
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium, type Page } from 'playwright'
|
||||
import { expect, it } from 'vitest'
|
||||
import { expect, it, onTestFinished } from 'vitest'
|
||||
import type { InboxState } from '@deepseek-ai/dsh-agent/types'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import { deriveReplayScript, parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
|
||||
@@ -89,6 +89,11 @@ class HistoryDeliveryGate {
|
||||
}
|
||||
}
|
||||
|
||||
/** Match the scaffold's turn-completion budget for Host frames and browser rendering. */
|
||||
function poll<T>(read: () => T) {
|
||||
return expect.poll(read, { timeout: 30_000 })
|
||||
}
|
||||
|
||||
async function placement(page: Page, input = TEXT) {
|
||||
return page.evaluate((text) => {
|
||||
const matching = (selector: string) => [...document.querySelectorAll<HTMLElement>(selector)]
|
||||
@@ -109,6 +114,7 @@ async function placement(page: Page, input = TEXT) {
|
||||
it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first'] as const)(
|
||||
'keeps an idle submission in Chat under CPU throttling (%s)', async (order) => {
|
||||
const scaffold = await launchWebScaffold({ replayFixture: FIXTURE, compareReplaySession: false })
|
||||
onTestFinished(() => scaffold.close())
|
||||
const releasePrompt = Promise.withResolvers<undefined>()
|
||||
const promptBlocked = Promise.withResolvers<undefined>()
|
||||
const gate = new HistoryDeliveryGate()
|
||||
@@ -138,42 +144,42 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first
|
||||
await input.fill(TEXT)
|
||||
await input.press('Enter')
|
||||
await promptBlocked.promise
|
||||
await expect.poll(() => placement(page)).toMatchObject({ echo: 1, dock: 0, durable: 0 })
|
||||
await poll(() => placement(page)).toMatchObject({ echo: 1, dock: 0, durable: 0 })
|
||||
trace.push({ phase: 'optimistic', ...await placement(page) })
|
||||
releasePrompt.resolve(undefined)
|
||||
|
||||
if (order === 'transcript-first') {
|
||||
await expect.poll(() => gate.turnStartDelivery()).toBeDefined()
|
||||
await poll(() => gate.turnStartDelivery()).toBeDefined()
|
||||
gate.releaseThrough(gate.turnStartDelivery()!)
|
||||
await page.locator('[data-chat-flow-kind="turn-process"]').last().waitFor({ state: 'attached' })
|
||||
await expect.poll(() => placement(page)).toMatchObject({ echo: 1, dock: 0, durable: 0 })
|
||||
await poll(() => placement(page)).toMatchObject({ echo: 1, dock: 0, durable: 0 })
|
||||
trace.push({ phase: 'turn-start', ...await placement(page) })
|
||||
await expect.poll(() => gate.admissionDelivery()).toBeDefined()
|
||||
await poll(() => gate.admissionDelivery()).toBeDefined()
|
||||
gate.releaseThrough(gate.admissionDelivery()!)
|
||||
await expect.poll(() => placement(page)).toMatchObject({ echo: 0, dock: 0, durable: 1 })
|
||||
await poll(() => placement(page)).toMatchObject({ echo: 0, dock: 0, durable: 1 })
|
||||
trace.push({ phase: 'durable-before-inbox', ...await placement(page) })
|
||||
}
|
||||
|
||||
await expect.poll(() => gate.inboxDelivery(true)).toBeDefined()
|
||||
await poll(() => gate.inboxDelivery(true)).toBeDefined()
|
||||
gate.releaseThrough(gate.inboxDelivery(true)!)
|
||||
await expect.poll(() => placement(page)).toMatchObject({ echo: order === 'inbox-first' ? 1 : 0, dock: 0, durable: order === 'transcript-first' ? 1 : 0 })
|
||||
await poll(() => placement(page)).toMatchObject({ echo: order === 'inbox-first' ? 1 : 0, dock: 0, durable: order === 'transcript-first' ? 1 : 0 })
|
||||
trace.push({ phase: 'inbox', ...await placement(page) })
|
||||
|
||||
await expect.poll(() => gate.inboxDelivery(false)).toBeDefined()
|
||||
await poll(() => gate.inboxDelivery(false)).toBeDefined()
|
||||
gate.releaseThrough(gate.inboxDelivery(false)!)
|
||||
await expect.poll(() => placement(page)).toMatchObject({ echo: order === 'inbox-first' ? 1 : 0, dock: 0, durable: order === 'transcript-first' ? 1 : 0 })
|
||||
await poll(() => placement(page)).toMatchObject({ echo: order === 'inbox-first' ? 1 : 0, dock: 0, durable: order === 'transcript-first' ? 1 : 0 })
|
||||
trace.push({ phase: 'claim-projection', ...await placement(page) })
|
||||
|
||||
if (order === 'inbox-first') {
|
||||
await expect.poll(() => gate.turnStartDelivery()).toBeDefined()
|
||||
await poll(() => gate.turnStartDelivery()).toBeDefined()
|
||||
gate.releaseThrough(gate.turnStartDelivery()!)
|
||||
await page.locator('[data-chat-flow-kind="turn-process"]').last().waitFor({ state: 'attached' })
|
||||
await expect.poll(() => placement(page)).toMatchObject({ echo: 1, dock: 0, durable: 0 })
|
||||
await poll(() => placement(page)).toMatchObject({ echo: 1, dock: 0, durable: 0 })
|
||||
trace.push({ phase: 'turn-start', ...await placement(page) })
|
||||
}
|
||||
|
||||
gate.releaseAll()
|
||||
await expect.poll(() => placement(page)).toMatchObject({ echo: 0, dock: 0, durable: 1 })
|
||||
await poll(() => placement(page)).toMatchObject({ echo: 0, dock: 0, durable: 1 })
|
||||
trace.push({ phase: 'durable', ...await placement(page) })
|
||||
for (const entry of trace) {
|
||||
expect(entry.messageTop, entry.phase).toBe(trace[0]?.messageTop)
|
||||
@@ -186,7 +192,7 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first
|
||||
} finally {
|
||||
releasePrompt.resolve(undefined)
|
||||
gate.releaseAll()
|
||||
try { await browser?.close() } finally { await scaffold.close() }
|
||||
await browser?.close()
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -199,6 +205,9 @@ it.skipIf(webSnapshotMode() === 'record').each(['ABC', 'ACB', 'BAC', 'BCA', 'CAB
|
||||
const gate = new HistoryDeliveryGate()
|
||||
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined
|
||||
let scaffold: Awaited<ReturnType<typeof launchWebScaffold>> | undefined
|
||||
onTestFinished(async () => {
|
||||
try { await scaffold?.close() } finally { await rm(root, { recursive: true, force: true }) }
|
||||
})
|
||||
try {
|
||||
const fixture = await readFile(FIXTURE, 'utf8')
|
||||
const recorded = deriveReplayScript(parseSessionLog(fixture))
|
||||
@@ -230,30 +239,30 @@ it.skipIf(webSnapshotMode() === 'record').each(['ABC', 'ACB', 'BAC', 'BCA', 'CAB
|
||||
for (const id of releases.keys()) {
|
||||
await input.fill(text(id))
|
||||
await input.press('Enter')
|
||||
await expect.poll(() => blocked.has(id)).toBe(true)
|
||||
await expect.poll(() => placement(page, text(id))).toMatchObject({ echo: 1, dock: 0, durable: 0 })
|
||||
await poll(() => blocked.has(id)).toBe(true)
|
||||
await poll(() => placement(page, text(id))).toMatchObject({ echo: 1, dock: 0, durable: 0 })
|
||||
}
|
||||
const entered: string[] = []
|
||||
for (const id of hostOrder) {
|
||||
const settled = scaffold.whenTurnSettled()
|
||||
releases.get(id)!.resolve(undefined)
|
||||
await expect.poll(() => gate.turnStartDelivery()).toBeDefined()
|
||||
await poll(() => gate.turnStartDelivery()).toBeDefined()
|
||||
gate.releaseThrough(gate.turnStartDelivery()!)
|
||||
await expect.poll(() => gate.admissionDelivery(text(id))).toBeDefined()
|
||||
await poll(() => gate.admissionDelivery(text(id))).toBeDefined()
|
||||
gate.releaseThrough(gate.admissionDelivery(text(id))!)
|
||||
entered.push(id)
|
||||
for (const candidate of releases.keys()) {
|
||||
const admitted = entered.includes(candidate)
|
||||
await expect.poll(() => placement(page, text(candidate))).toMatchObject({
|
||||
await poll(() => placement(page, text(candidate))).toMatchObject({
|
||||
echo: admitted ? 0 : 1, dock: 0, durable: admitted ? 1 : 0,
|
||||
})
|
||||
}
|
||||
await expect.poll(() => gate.inboxDelivery(true, text(id))).toBeDefined()
|
||||
await poll(() => gate.inboxDelivery(true, text(id))).toBeDefined()
|
||||
gate.releaseThrough(gate.inboxDelivery(true, text(id))!)
|
||||
await expect.poll(() => placement(page, text(id))).toMatchObject({ echo: 0, dock: 0, durable: 1 })
|
||||
await expect.poll(() => gate.inboxDelivery(false, text(id))).toBeDefined()
|
||||
await poll(() => placement(page, text(id))).toMatchObject({ echo: 0, dock: 0, durable: 1 })
|
||||
await poll(() => gate.inboxDelivery(false, text(id))).toBeDefined()
|
||||
gate.releaseThrough(gate.inboxDelivery(false, text(id))!)
|
||||
await expect.poll(() => gate.turnEndDelivery()).toBeDefined()
|
||||
await poll(() => gate.turnEndDelivery()).toBeDefined()
|
||||
gate.releaseThrough(gate.turnEndDelivery()!)
|
||||
await settled
|
||||
}
|
||||
@@ -267,9 +276,7 @@ it.skipIf(webSnapshotMode() === 'record').each(['ABC', 'ACB', 'BAC', 'BCA', 'CAB
|
||||
} finally {
|
||||
for (const release of releases.values()) release.resolve(undefined)
|
||||
gate.releaseAll()
|
||||
try { await browser?.close() } finally {
|
||||
try { await scaffold?.close() } finally { await rm(root, { recursive: true, force: true }) }
|
||||
}
|
||||
await browser?.close()
|
||||
}
|
||||
},
|
||||
)
|
||||
Reference in new issue
Block a user