test(web): preserve handoff failures through cleanup

This commit is contained in:
Tianyi Cui committed 2026-10-07 03:02:36 +08:00
1 parent 8c24036e20
commit 73b5769da2
3 files changed
+20 -16

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@
zh: 3fd3ff9fcd1e1e90
/apps-web-browser-e2e/completion-observations:
en: ae500691f64d1e43
zh: ef8d84b9907ab67d
zh: 2bfc53b52d210b24
/apps-web-browser-e2e/these-are-host-face-tests:
en: 8c406d423c8e2137
zh: a66a144fd146f56c
+1 -1
View File
@@ -18,7 +18,7 @@ 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 消耗检查错误不会覆盖场景中失败的断言。
[空闲提交交接](idle-submission-handoff.e2e.ts)对 Host 帧和浏览器中的消息归位的等待使用 scaffold 的 30 秒 Turn 完成预算。Vitest 负责 scaffold 清理,使 replay 消耗检查错误不会覆盖场景中失败的断言。
显式滚动使用 `support.ts` 的 `scrollIntoView`:旧元素脱离 DOM 时重新解析 locator,并在同一个浏览器任务中检查连接状态、执行原生滚动。各场景保留滚动后的可见性与几何断言。
+18 -14
View File
@@ -17,6 +17,7 @@ import { newEnglishPage } from './support.ts'
const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/live-interactions/session.v3.jsonl', import.meta.url))
const SESSION_ID = 'idle-submission-handoff'
const TEXT = 'IDLE_SUBMISSION_HANDOFF Keep this message in the conversation.'
const HANDOFF_TIMEOUT_MS = 30_000
interface Delivery {
readonly stream: string
@@ -91,7 +92,14 @@ 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 })
return expect.poll(read, { timeout: HANDOFF_TIMEOUT_MS })
}
/** The caller's await reports Turn failure; an earlier assertion may bypass that await. */
function whenTurnSettled(scaffold: Awaited<ReturnType<typeof launchWebScaffold>>) {
const settled = scaffold.whenTurnSettled(HANDOFF_TIMEOUT_MS)
void settled.catch(() => undefined)
return settled
}
async function placement(page: Page, input = TEXT) {
@@ -118,10 +126,10 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first
const releasePrompt = Promise.withResolvers<undefined>()
const promptBlocked = Promise.withResolvers<undefined>()
const gate = new HistoryDeliveryGate()
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined
try {
await seedSession(scaffold, await readFile(FIXTURE, 'utf8'), SESSION_ID)
browser = await chromium.launch()
const browser = await chromium.launch()
onTestFinished(() => browser.close())
const page = await newEnglishPage(browser)
const tripwire = watchConsole(page)
await gate.install(page)
@@ -138,7 +146,6 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first
await route.continue()
}, { times: 1 })
const trace: Array<{ phase: string } & Awaited<ReturnType<typeof placement>>> = []
const settled = scaffold.whenTurnSettled()
gate.holding = true
const input = page.locator('[data-composer-input]').first()
await input.fill(TEXT)
@@ -146,6 +153,7 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first
await promptBlocked.promise
await poll(() => placement(page)).toMatchObject({ echo: 1, dock: 0, durable: 0 })
trace.push({ phase: 'optimistic', ...await placement(page) })
const settled = whenTurnSettled(scaffold)
releasePrompt.resolve(undefined)
if (order === 'transcript-first') {
@@ -192,7 +200,6 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first
} finally {
releasePrompt.resolve(undefined)
gate.releaseAll()
await browser?.close()
}
},
)
@@ -200,23 +207,21 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first
it.skipIf(webSnapshotMode() === 'record').each(['ABC', 'ACB', 'BAC', 'BCA', 'CAB', 'CBA'])(
'hands off three rapid submissions through all Turns in Host order %s', async (hostOrder) => {
const root = await mkdtemp(join(tmpdir(), 'dsh-input-handoff-'))
onTestFinished(() => rm(root, { recursive: true, force: true }))
const releases = new Map(['A', 'B', 'C'].map(id => [id, Promise.withResolvers<undefined>()]))
const blocked = new Set<string>()
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))
expect(recorded).toHaveLength(1)
const override = join(root, 'replay.override.json')
await writeFile(override, JSON.stringify([recorded[0], recorded[0], recorded[0]]))
scaffold = await launchWebScaffold({ replayFixture: FIXTURE, replayOverride: override, compareReplaySession: false })
const scaffold = await launchWebScaffold({ replayFixture: FIXTURE, replayOverride: override, compareReplaySession: false })
onTestFinished(() => scaffold.close())
await seedSession(scaffold, fixture, SESSION_ID)
browser = await chromium.launch()
const browser = await chromium.launch()
onTestFinished(() => browser.close())
const page = await newEnglishPage(browser)
const tripwire = watchConsole(page)
await gate.install(page)
@@ -244,7 +249,7 @@ it.skipIf(webSnapshotMode() === 'record').each(['ABC', 'ACB', 'BAC', 'BCA', 'CAB
}
const entered: string[] = []
for (const id of hostOrder) {
const settled = scaffold.whenTurnSettled()
const settled = whenTurnSettled(scaffold)
releases.get(id)!.resolve(undefined)
await poll(() => gate.turnStartDelivery()).toBeDefined()
gate.releaseThrough(gate.turnStartDelivery()!)
@@ -276,7 +281,6 @@ it.skipIf(webSnapshotMode() === 'record').each(['ABC', 'ACB', 'BAC', 'BCA', 'CAB
} finally {
for (const release of releases.values()) release.resolve(undefined)
gate.releaseAll()
await browser?.close()
}
},
)