Keep plugin-install interception for the page lifetime
让插件安装拦截器持续到页面关闭
The [plugin-install scenario](../../../../apps/web/tests/plugin-install-cancel.e2e.ts) can leave a directory read pending after cancelling an installation. The Host emits invalidations that trigger background reads while the scenario removes its Playwright routes between installation phases. A stalled read keeps the installed package out of the list, so enabling it closes the dialog but never exposes the highlighted
English
Problem
The plugin-install scenario can leave a directory read pending after cancelling an installation. The Host emits invalidations that trigger background reads while the scenario removes its Playwright routes between installation phases. A stalled read keeps the installed package out of the list, so enabling it closes the dialog but never exposes the highlighted card.
Decision
The scenario registers its installBundle and cancelInstall interceptors before navigation and retains them until the browser closes. Each interceptor calls a typed handler that the scenario replaces when it changes phases. The delivery and cancellation barriers retain their ordering, and activeReplySettled joins the lost-response handler before the next installation starts. Ordinary requests still reach the real Host.
The scenario separately waits for the highlighted card's status to become running. Keeping interception active lets the directory read complete; polling its result still establishes the state the golden records.
Alternatives considered
Remove routes with unrouteAll({ behavior: 'wait' }). Waiting for matched handlers does not settle unrelated browser requests. A captured Chromium protocol trace shows Fetch.disable immediately followed by a pluginManager/listPlugins request with no response or failure before teardown. The Host's inventory and plugin methods still answer direct calls.
Advance the browser clock or extend the locator timeout. Advancing the paused clock does not release the stalled request. The 2.4-second highlight expiry is not responsible for a package that never reaches the directory.
Add a general route-switching helper. Two endpoint handlers in one scenario do not need another fixture API. Their local delegates preserve the existing cancellation and lost-response cases without changing the browser's interception configuration.
Verification
The unchanged scenario passes alone and fails at the same highlighted-card locator in one of four concurrent independent processes. Instrumented failures place the unanswered directory request inside the second unrouteAll interval; advancing the page clock leaves it unanswered, while direct Host reads complete. The protocol trace identifies the interception transition independently of the DOM assertion.
DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/plugin-install-cancel.e2e.ts passes in four concurrent independent processes (1/1 each). One protocol capture observes one Fetch.enable, no Fetch.disable, and responses for all 33 directory requests. The existing goldens cover cancellation before delivery, cancellation of the running child, manifest and lockfile restoration, retry, enabled-card status, both themes and motion preferences, highlight expiry, and lost-result recovery.
Consequences
The browser retains two narrowly matched interceptors for its lifetime. The scenario controls their behavior without interrupting concurrent directory reads, and browser closure owns their cleanup. Product code, goldens, test deadlines, and CI worker counts are unchanged.
Live-page unrouteAll calls also remain in sidebar-terminal, queue-image, subagent-conversation, and subagent-interrupt-ui. These are candidates for separate overlap investigations; this scenario's reproduction does not establish the cause of failures in those tests.
中文
Problem
插件安装场景 在取消安装后,可能留下一次未结束的目录读取。场景在安装阶段之间移除 Playwright 路由时,Host 发出的失效通知仍会触发后台读取。停滞的读取使已安装的包无法进入列表,因此启用操作虽然关闭了对话框,却始终不显示高亮卡片。
Decision
场景在导航前注册 installBundle 和 cancelInstall 拦截器,并保留到浏览器关闭。每个拦截器调用一个带类型的处理函数,场景切换阶段时替换该函数。请求交付和取消屏障保持原有顺序,activeReplySettled 在下一次安装开始前等待丢失响应的处理函数结束。普通请求仍然到达真实 Host。
该场景还会单独等待高亮卡片的状态变为 running。持续启用拦截使目录读取能够完成;轮询其结果仍负责确立预期输出记录的状态。
Alternatives considered
用 unrouteAll({ behavior: 'wait' }) 移除路由。 等待匹配的处理函数结束并不意味着其他浏览器请求已结束。捕获的 Chromium 协议记录显示,Fetch.disable 后紧接着发出 pluginManager/listPlugins 请求,直到清理前都没有响应或失败事件。直接调用 Host 的目录和插件方法仍能得到答复。
推进浏览器时钟或延长 locator 超时。 推进已暂停的时钟不能释放停滞的请求。包始终未进入目录,不是 2.4 秒的高亮到期计时器造成的。
增加通用路由切换辅助函数。 一个场景中的两个端点处理函数不需要另一层 fixture(测试前置数据)API。局部委托函数保留了现有取消和丢失响应场景,也不改变浏览器的拦截配置。
Verification
未修改的场景单独运行通过,四个并发独立进程中有一个在同一高亮卡片 locator 处失败。插桩失败将未获响应的目录请求定位到第二次 unrouteAll 的执行区间内;推进页面时钟后请求仍未获响应,而直接读取 Host 可以完成。协议记录独立于 DOM 断言确认了拦截状态的切换。
DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/plugin-install-cancel.e2e.ts 在四个并发独立进程中全部通过,每个进程均为 1/1。一次协议记录观察到一次 Fetch.enable、零次 Fetch.disable,且全部 33 次目录请求均收到响应。现有预期输出覆盖取消先于请求交付、取消运行中的子进程、恢复 manifest(元数据清单)与 lockfile、重试、已启用卡片状态、两种主题与动画偏好、高亮到期,以及丢失结果恢复。
Consequences
浏览器在整个生命周期内保留两个精确匹配的拦截器。场景控制其行为时不会中断并发目录读取,浏览器关闭负责清理它们。产品代码、预期输出、测试超时和 CI worker 数保持不变。
sidebar-terminal、queue-image、subagent-conversation 和 subagent-interrupt-ui 中也仍有页面存续期间的 unrouteAll 调用。这些位置可分别调查请求重叠;本场景的复现并不能确定那些测试失败的原因。