fix(web): mount the provider catalog for the geometry regression and assert the real model label

A bare /plan command never calls a model, so the scaffold's replay row
did not mount and the model directory was empty: the trigger rendered
the short fallback label, which fits beside the chip even on the
pre-fix layout, silently defanging the regression. The scaffold gains a
replayProvidersOnly option (provider catalog without a recorded script,
consumption check skipped), the test mounts it, and asserts the trigger
aria-label contains DeepSeek-V4-Flash before measuring — verified that
removing the wrap fix makes the test fail (click areas disjoint: false).
This commit is contained in:
Chinesezjc committed 2026-08-07 13:53:08 +08:00
1 parent aae246ef6d
commit d5ec1189a6
6 files changed
+29 -12

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-06-plan-narrow-viewport-regression.md
2026-08-06-plan-narrow-viewport-regression.md: c4d7281d09b706c1270e9c43592d558825c63250
2026-08-06-plan-narrow-viewport-regression.zh.md: aed430dc95793b8086a838ae534ffaed0256012e
2026-08-06-plan-narrow-viewport-regression.md: 45cb969bc4c3d0856c78dae49159eb41be24dc91
2026-08-06-plan-narrow-viewport-regression.zh.md: 8767e1b9f86f45942ba3ad5735e245ccfc994b9f
@@ -20,7 +20,7 @@ The geometry golden records stable facts — viewport membership on both axes an
## Alternatives considered
**Seed a cold session (composer-tab-geometry pattern).** Rejected: the exit path executes `/plan off` through `commands.execute`, which needs the live agent a cold seeded session does not have. The recorded turn keeps one, matching the product's user path.
**Seed a cold session (composer-tab-geometry pattern).** Rejected: the exit path executes `/plan off` through `commands.execute`, which needs the live agent a cold seeded session does not have; `connectFreshWorkspace` keeps one, matching the product's user path.
**Pin absolute bounding boxes in the golden.** Rejected: chip and trigger widths depend on the installed fonts, so absolute coordinates would churn across platforms without a behavior change.
@@ -30,4 +30,4 @@ The geometry golden records stable facts — viewport membership on both axes an
## Consequences
Any future change to the control row layout — fonts, gaps, media or container queries — that re-introduces overlap or moves the chip out of the viewport on either axis fails this test. The test needs no API key: plan mode toggles through the command handler without a model round, and the golden is compared in replay/refresh modes.
Any future change to the control row layout — fonts, gaps, media or container queries — that re-introduces overlap or moves the chip out of the viewport on either axis fails this test. The test needs no API key: plan mode toggles through the command handler without a model round, and a providers-only replay fixture (no recorded script, consumption check skipped) mounts the model directory so the trigger renders its real long label — the width that made the reported overlap measurable; the test asserts that label before measuring. The golden is compared in replay and record modes and rewritten in refresh mode.
@@ -20,7 +20,7 @@ Status: implemented
## 备选方案
**冷会话 seed(composer-tab-geometry 模式)。** 否决:退出路径经 `commands.execute` 执行 `/plan off`,需要 live agent,而冷 seed 会话没有。录制的回合保留一个,与产品的用户路径一致。
**冷会话 seed(composer-tab-geometry 模式)。** 否决:退出路径经 `commands.execute` 执行 `/plan off`,需要 live agent,而冷 seed 会话没有;`connectFreshWorkspace` 保留一个,与产品的用户路径一致。
**golden 固定绝对 bounding box。** 否决:chip 与 trigger 宽度依赖安装字体,绝对坐标会在平台间漂移而不反映行为变化。
@@ -30,4 +30,4 @@ Status: implemented
## 后果
任何改变控制行布局的后续改动——字体、间距、媒体查询或容器查询——一旦重新引入重叠或把 chip 沿任一轴移出视口,本测试即失败。测试无需 API key:Plan 模式经命令 handler 切换,不经模型回合;golden 在 replay/refresh 模式下比较。
任何改变控制行布局的后续改动——字体、间距、媒体查询或容器查询——一旦重新引入重叠或把 chip 沿任一轴移出视口,本测试即失败。测试无需 API key:Plan 模式经命令 handler 切换,不经模型回合;providers-only replay fixture(无录制脚本,跳过消费检查)挂载模型目录,使触发器渲染真实的长标签——正是使报告重叠可测量的宽度;测试在测量前断言该标签。golden 在 replay 与 record 模式下比较,在 refresh 模式下重写。
+10 -2
View File
@@ -35,6 +35,7 @@ import {
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/plan-narrow-viewport', import.meta.url))
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
const LAYOUT_EXPECTED = join(SNAPSHOT_DIR, 'layout.expected.md')
const MODE = webSnapshotMode()
@@ -52,7 +53,11 @@ describe('web e2e: plan chip click area at the narrow viewport', () => {
const sessionEvents: SessionEvent[] = []
beforeAll(async () => {
scaffold = await launchWebScaffold({})
// The fixture carries the deterministic provider catalog (no model call
// happens — the /plan command never steers a message), so the model
// trigger renders its real long label, which is what made the reported
// overlap measurable.
scaffold = await launchWebScaffold({ replayFixture: FIXTURE, replayProvidersOnly: true })
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
browser = await chromium.launch()
page = await newEnglishPage(browser, VIEWPORT.height)
@@ -82,6 +87,9 @@ describe('web e2e: plan chip click area at the narrow viewport', () => {
const trigger = page.getByRole('button', { name: /Select model/ })
await chip.waitFor({ timeout: 30_000 })
await trigger.waitFor({ timeout: 10_000 })
// The regression depends on the real model label width: a bare fallback
// trigger would fit beside the chip even on the pre-fix layout.
expect(await trigger.getAttribute('aria-label')).toContain('DeepSeek-V4-Flash')
const chipBox = await chip.boundingBox()
const triggerBox = await trigger.boundingBox()
expect(chipBox).not.toBeNull()
@@ -132,6 +140,6 @@ describe('web e2e: plan chip click area at the narrow viewport', () => {
}, 200_000)
it('keeps the snapshot inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, ['layout.expected.md'])
await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'layout.expected.md'])
})
})
+13 -4
View File
@@ -129,6 +129,13 @@ export interface LaunchOptions {
* mounts).
*/
replayFixture?: string
/**
* Mount the replay provider catalog (the model directory the UI shows)
* without any recorded script to consume: for scenarios that never call a
* model but need the real provider/model labels rendered. The teardown
* consumption check is skipped for this mode.
*/
replayProvidersOnly?: boolean
/**
* Recorded child logs assigned in child creation order. Each child owns its
* own positional replay cursor across initial and continuation turns.
@@ -402,10 +409,12 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
// Fixture-consumption check first, while the run's binding state is
// still authoritative — a scenario that drove fewer model calls than
// recorded fails here instead of drifting green.
try {
replayHandle?.assertConsumed()
} catch (error) {
failures.push(error)
if (!options.replayProvidersOnly) {
try {
replayHandle?.assertConsumed()
} catch (error) {
failures.push(error)
}
}
try {
failures.push(...await cleanupScaffoldWorld(ctx, workspaceCwd, persistenceRoot))
Whitespace-only changes.