Browse Source

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).
Chinesezjc 1 month ago
parent
commit
d5ec1189a6

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-06-plan-narrow-viewport-regression.i18n.yaml

@@ -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

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-06-plan-narrow-viewport-regression.md

@@ -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.

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-06-plan-narrow-viewport-regression.zh.md

@@ -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
apps/web/tests/plan-control-row.e2e.ts

@@ -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
apps/web/tests/scaffold.ts

@@ -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))

+ 0 - 0
apps/web/tests/snapshots/plan-narrow-viewport/session.jsonl