Parcourir la source

fix(web): distinguish plugin configuration instances

Turtle il y a 2 semaines
Parent
commit
c2a5540ddb

+ 37 - 0
apps/web/tests/expected/settings-chrome/plugin-instances.expected.md

@@ -0,0 +1,37 @@
+- list:
+  - listitem:
+    - button "tool-subagent-control, tool-subagent-control, 已启用":
+      - strong: tool-subagent-control
+      - code: tool-subagent-control
+      - text: 已启用
+      - img
+  - listitem:
+    - button "tool-subagent-control/list-agents, tool-subagent-list-agents, 已启用":
+      - strong: tool-subagent-control/list-agents
+      - code: tool-subagent-list-agents
+      - text: 已启用
+      - img
+  - listitem:
+    - button "tool-subagent, tool-subagent, 已启用":
+      - strong: tool-subagent
+      - code: tool-subagent
+      - text: 已启用
+      - img
+  - listitem:
+    - button "tool-subagent, tool-subagent-fork, 已启用":
+      - strong: tool-subagent
+      - code: tool-subagent-fork
+      - text: 已启用
+      - img
+  - listitem:
+    - button "tool-subagent, tool-subagent-codex, 已停用":
+      - strong: tool-subagent
+      - code: tool-subagent-codex
+      - text: 已停用
+      - img
+  - listitem:
+    - button "tool-subagent, tool-subagent-claude-code, 已停用":
+      - strong: tool-subagent
+      - code: tool-subagent-claude-code
+      - text: 已停用
+      - img

+ 3 - 3
apps/web/tests/expected/settings-chrome/plugins.expected.md

@@ -1,6 +1,6 @@
 - listitem:
-  - button "ui-settings, 已启用":
-    - strong: ui-settings
-    - img "运行中"
+  - button "tool-subagent, tool-subagent, 已启用":
+    - strong: tool-subagent
+    - code: tool-subagent
     - text: 已启用
     - img

+ 39 - 3
apps/web/tests/settings-chrome.e2e.ts

@@ -24,9 +24,10 @@ import { ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
 const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/settings-chrome', import.meta.url))
 const DIALOG_EXPECTED = join(SNAPSHOT_DIR, 'dialog.expected.md')
 const PLUGINS_EXPECTED = join(SNAPSHOT_DIR, 'plugins.expected.md')
+const PLUGIN_INSTANCES_EXPECTED = join(SNAPSHOT_DIR, 'plugin-instances.expected.md')
 // The English fallback surface: a browser naming no shipped language.
 const DIALOG_EN_EXPECTED = join(SNAPSHOT_DIR, 'dialog-en.expected.md')
-const PLUGIN_ROW_SELECTOR = '[data-plugin-entry$="ui-settings"]'
+const PLUGIN_ROW_SELECTOR = '[data-plugin-scope="preset"] [data-plugin-entry="tool-subagent"]'
 const MODE = webSnapshotMode()
 
 describe('web e2e: settings modal and General preferences', () => {
@@ -114,7 +115,8 @@ describe('web e2e: settings modal and General preferences', () => {
     const expectedPluginCount = [...scaffold.ctx.loader.entries()]
       .filter(entry => !entry.options.group)
       .length
-    expect(await dialog.getByRole('searchbox', { name: '搜索插件' }).count()).toBe(1)
+    const pluginSearch = dialog.getByRole('searchbox', { name: '搜索插件' })
+    expect(await pluginSearch.count()).toBe(1)
     // Every Loader entry appears exactly once in the global group — rows the
     // presets took over included, preset compositions excluded.
     expect(await dialog.locator('[data-plugin-scope="global"] [data-plugin-entry]').count())
@@ -130,6 +132,35 @@ describe('web e2e: settings modal and General preferences', () => {
       scaffold.workspaceCwd,
     )
     await compareOrRefreshGolden(PLUGINS_EXPECTED, pluginsSnapshot, MODE)
+    await pluginSearch.fill('tool-subagent')
+    const instanceRows = [
+      ['tool-subagent', '已启用'],
+      ['tool-subagent-fork', '已启用'],
+      ['tool-subagent-codex', '已停用'],
+      ['tool-subagent-claude-code', '已停用'],
+    ] as const
+    for (const [entryId, status] of instanceRows) {
+      const row = dialog.locator(`[data-plugin-scope="preset"] [data-plugin-entry="${entryId}"]`)
+      const trigger = row.getByRole('button', { name: `tool-subagent, ${entryId}, ${status}`, exact: true })
+      await trigger.waitFor({ timeout: 10_000 })
+      expect(await trigger.getAttribute('aria-expanded')).toBe('false')
+      const identity = row.locator('code')
+      expect(await identity.textContent()).toBe(entryId)
+      expect(await identity.getAttribute('title')).toBe(entryId)
+    }
+    const instancesSnapshot = await captureStableAria(
+      page,
+      '[data-plugin-scope="preset"] ul',
+      scaffold.workspaceCwd,
+    )
+    await compareOrRefreshGolden(PLUGIN_INSTANCES_EXPECTED, instancesSnapshot, MODE)
+    await dialog.getByRole('button', {
+      name: 'tool-subagent, tool-subagent-claude-code, 已停用',
+      exact: true,
+    }).click()
+    expect(await dialog.locator('[data-plugin-entry="tool-subagent-claude-code"] button')
+      .getAttribute('aria-expanded')).toBe('true')
+    await pluginSearch.fill('')
     // Close path 1: Escape.
     await page.keyboard.press('Escape')
     await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0)
@@ -635,6 +666,11 @@ describe('web e2e: settings modal and General preferences', () => {
 
   it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
     expect(tripwire.warnings).toEqual([])
-    await assertFixtureInventory(SNAPSHOT_DIR, ['dialog-en.expected.md', 'dialog.expected.md', 'plugins.expected.md'])
+    await assertFixtureInventory(SNAPSHOT_DIR, [
+      'dialog-en.expected.md',
+      'dialog.expected.md',
+      'plugin-instances.expected.md',
+      'plugins.expected.md',
+    ])
   })
 })

+ 2 - 2
packages/client/ui-settings-plugin-inventory/README.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 packages/client/ui-settings-plugin-inventory/README.md
-README.md: 9a7ea91521c08dd1855925af59521d3af6e47b25
-README.zh.md: 2427d667d92b7a4c620fc6222507ecd8c2fabcef
+README.md: 2a7a00f037905c9e70edb283c22946a2bc6c0247
+README.zh.md: 7be131a8a1a3f1e1fbbf2fbff1ac65c1829f4c10

+ 3 - 3
packages/client/ui-settings-plugin-inventory/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-The **Plugin list** tab lets Web users inspect plugins without changing their configuration. It presents agent-preset compositions first and keeps the global inventory collapsed until needed. Cards expose enablement, provenance, runtime status, disabled conditions, and discovery failures; global entries supplied by presets are identified with the enabling presets. Search covers both groups and points to matches in other presets. The tab supports loading, empty, no-match, failure, and retry states without exposing transport details, and it still shows the global inventory when no preset roster is available.
+The **Plugin list** tab lets Web users inspect plugins without changing their configuration. It presents agent presets first and collapses the global inventory until needed. Cards retain the package name as the primary title, identify instances by stable entry id, and expose enablement, provenance, runtime status, disabled conditions, and discovery failures; preset-provided global entries name their presets. Search covers both groups and points to matches in other presets. The tab handles loading, empty, no-match, failure, and retry states without exposing transport details, and still shows the global inventory without a preset roster.
 
 ## Table of Contents
 
@@ -29,7 +29,7 @@ Open the Plugins section in Settings and select the **Plugin list** tab to inspe
 
 ### Reading a card
 
-Each collapsed card uses the short module name as its title and a small enablement tag; enabled entries also show a colored root-fiber status dot. Expanding one card reveals the declared entry id, the full module specifier, and the state facts: a preset row names the preset it comes from, its runtime status when the composition is live, and its disable condition when it carries one; a preset-provided global row explains that agent presets provide it per session, names the presets that enable it, and offers a jump into the preset group. Preset names resolve through the shared `presetDisplayText` fold (`dsh-agent-presets/display`) over [`ui-agent-preset`](../ui-agent-preset/README.md)'s dictionaries: shipped presets follow the active locale while user-authored ones keep their own metadata, so an English surface never echoes the preset files' Chinese names. Search filters both groups by module name and entry id.
+Each collapsed card uses the short module name as its primary title, shows the stable entry id underneath, and carries a small enablement tag; enabled entries also show a colored root-fiber status dot. Long entry ids truncate in the row and remain available on hover. Expanding one card reveals the declared entry id, the full module specifier, and the state facts: a preset row names the preset it comes from, its runtime status when the composition is live, and its disable condition when it carries one; a preset-provided global row explains that agent presets provide it per session, names the presets that enable it, and offers a jump into the preset group. Preset names resolve through the shared `presetDisplayText` fold (`dsh-agent-presets/display`) over [`ui-agent-preset`](../ui-agent-preset/README.md)'s dictionaries: shipped presets follow the active locale while user-authored ones keep their own metadata, so an English surface never echoes the preset files' Chinese names. Search filters both groups by module name and entry id.
 
 ### The preset switcher
 
@@ -55,7 +55,7 @@ The browser plugin registers one localized `settings.plugins.tab` contribution w
 
 ### Rendering
 
-Row keys are scope-qualified (`global:`, `preset:<id>:<index>`), so one module appearing in both scopes keeps distinct disclosure state; an entry id is shown as detail only when the row declares one and is never classified by string shape. The preset-provided marking is derived client-side: a global entry carries it when it is disabled there while at least one preset row for the same module specifier is actually enabled, so a module every preset gates off (or declares only conditionally) stays plainly disabled rather than over-claiming provision.
+Row keys are scope-qualified (`global:`, `preset:<id>:<index>`), so one module appearing in both scopes keeps distinct disclosure state; a declared entry id appears in the collapsed row and expanded detail, while a row without one stays unlabeled, and the renderer never classifies ids by string shape. The preset-provided marking is derived client-side: a global entry carries it when it is disabled there while at least one preset row for the same module specifier is actually enabled, so a module every preset gates off (or declares only conditionally) stays plainly disabled rather than over-claiming provision.
 
 </details>
 

+ 3 - 3
packages/client/ui-settings-plugin-inventory/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-**插件列表**标签页让 Web 用户查看插件,而不改变其配置。它优先展示 Agent 预设组合,并在需要前收起全局清单。卡片展示启停状态、出处、运行状态、禁用条件与发现失败;由预设提供的全局条目会标明启用它的预设。搜索覆盖两个分组,并指出其他预设中的匹配。标签页支持加载、空结果、无匹配、失败与重试状态,且不暴露传输细节;没有预设 roster 时仍会展示全局清单。
+**插件列表**标签页让 Web 用户查看插件,而不改变其配置。它优先展示 Agent 预设,并在需要前收起全局清单。卡片保留包名作为主标题,以稳定的条目 id 标识实例,并展示启停状态、出处、运行状态、禁用条件与发现失败;由预设提供的全局条目会列出对应预设。搜索覆盖两个分组,并指出其他预设中的匹配。标签页处理加载、空结果、无匹配、失败与重试状态,且不暴露传输细节;没有预设 roster 时仍会展示全局清单。
 
 ## 目录
 
@@ -29,7 +29,7 @@ kind: "package-reference"
 
 ### 阅读卡片
 
-每张收起的卡片使用模块短名称作为标题,并以小标签表示启停状态;已启用的条目还会显示彩色根 fiber 状态圆点。展开卡片后会显示声明的条目 id、完整模块标识与状态事实:预设行说明它来自哪个预设、组合存活时的运行状态,以及它携带的禁用条件;被预设提供的全局行说明它由 Agent 预设按会话提供、列出启用它的预设,并提供跳转到预设组的入口。预设名经共享的 `presetDisplayText` 纯函数(`dsh-agent-presets/display`)叠在 [`ui-agent-preset`](../ui-agent-preset/README.zh.md) 的字典上解析:内置预设走当前语言,用户自建预设保留自己的元数据,因此英文界面不会回显预设文件里的中文名。搜索按模块名称与条目 id 过滤两组。
+每张收起的卡片使用模块短名称作为标题,在下方显示稳定的条目 id,并以小标签表示启停状态;已启用的条目还会显示彩色根 fiber 状态圆点。长条目 id 会在行内截断,悬停时仍可查看完整值。展开卡片后会显示声明的条目 id、完整模块标识与状态事实:预设行说明它来自哪个预设、组合存活时的运行状态,以及它携带的禁用条件;被预设提供的全局行说明它由 Agent 预设按会话提供、列出启用它的预设,并提供跳转到预设组的入口。预设名经共享的 `presetDisplayText` 纯函数(`dsh-agent-presets/display`)叠在 [`ui-agent-preset`](../ui-agent-preset/README.zh.md) 的字典上解析:内置预设走当前语言,用户自建预设保留自己的元数据,因此英文界面不会回显预设文件里的中文名。搜索按模块名称与条目 id 过滤两组。
 
 ### 预设切换器
 
@@ -55,7 +55,7 @@ kind: "package-reference"
 
 ### 渲染
 
-行 key 按作用域限定(`global:`、`preset:<id>:<index>`),因此同一模块出现在两个作用域时保持各自的展开状态;条目 id 只在行声明了它时作为详情展示,代码不按字符串形状对它分类。预设提供标记在客户端推导:一个全局条目在全局被停用、且至少一个预设行对同一模块标识实际启用时才携带它,因此被所有预设关掉(或仅条件声明)的模块保持单纯的已停用,而不是夸大提供关系。
+行 key 按作用域限定(`global:`、`preset:<id>:<index>`),因此同一模块出现在两个作用域时保持各自的展开状态;声明的条目 id 同时出现在收起行与展开详情中,没有 id 的行不显示次级标签,渲染器也不按字符串形状对 id 分类。预设提供标记在客户端推导:一个全局条目在全局被停用、且至少一个预设行对同一模块标识实际启用时才携带它,因此被所有预设关掉(或仅条件声明)的模块保持单纯的已停用,而不是夸大提供关系。
 
 </details>
 

+ 21 - 3
packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.module.css

@@ -150,14 +150,32 @@
   outline-offset: -2px;
 }
 
-.cardTitle {
+.cardHeading {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
   min-width: 0;
   overflow: hidden;
+}
+
+.cardTitle,
+.cardIdentity {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.cardTitle {
   font-size: 14px;
   line-height: 20px;
   font-weight: 600;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+}
+
+.cardIdentity {
+  color: var(--dsw-alias-label-tertiary);
+  font-family: var(--ds-font-family-code);
+  font-size: 11px;
+  line-height: 16px;
 }
 
 .cardTrailing {

+ 6 - 3
packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.tsx

@@ -113,7 +113,10 @@ function PluginCard({ rowKey, moduleName, entryId, trailing, ariaLabel, failed,
         aria-label={ariaLabel}
         onClick={() => { onToggle(rowKey) }}
       >
-        <strong className={css.cardTitle} title={moduleName}>{moduleShortName(moduleName)}</strong>
+        <span className={css.cardHeading}>
+          <strong className={css.cardTitle} title={moduleName}>{moduleShortName(moduleName)}</strong>
+          {entryId === null ? null : <code className={css.cardIdentity} title={entryId}>{entryId}</code>}
+        </span>
         <span className={css.cardTrailing}>
           {trailing}
           <IconChevronDownOutline14 className={css.chevron} size={12} aria-hidden="true" />
@@ -280,7 +283,7 @@ export function PluginInventorySettingsTab({ list, presetName, t }: PluginInvent
         failed={failed}
         expanded={expanded}
         onToggle={toggleRow}
-        ariaLabel={`${title}, ${stateText}`}
+        ariaLabel={`${title}${row.entryId === null ? '' : `, ${row.entryId}`}, ${stateText}`}
         trailing={(
           <>
             {row.enabled === true && !failed && row.fiberPhase !== null
@@ -326,7 +329,7 @@ export function PluginInventorySettingsTab({ list, presetName, t }: PluginInvent
         failed={failed}
         expanded={expanded}
         onToggle={toggleRow}
-        ariaLabel={`${title}, ${stateText}`}
+        ariaLabel={`${title}, ${entry.entryId}, ${stateText}`}
         trailing={(
           <>
             {entry.enabled && !failed && entry.fiberPhase !== null

+ 41 - 13
packages/client/ui-settings-plugin-inventory/tests/components.client.spec.tsx

@@ -115,16 +115,16 @@ describe('PluginInventorySettingsTab', () => {
     expect(screen.getByText(`1 ${en.failedCountLabel}`)).toBeTruthy()
 
     // A preset row expands into its provenance facts.
-    fireEvent.click(screen.getByRole('button', { name: 'pwsh, Conditional' }))
+    fireEvent.click(screen.getByRole('button', { name: 'pwsh, pwsh, Conditional' }))
     expect(screen.getByText(en.fromPreset)).toBeTruthy()
     expect(screen.getByText('标准模式')).toBeTruthy()
     expect(screen.getByText(en.condition)).toBeTruthy()
     expect(screen.getByText('process.platform === \'win32\'')).toBeTruthy()
-    fireEvent.click(screen.getByRole('button', { name: 'pwsh, Conditional' }))
+    fireEvent.click(screen.getByRole('button', { name: 'pwsh, pwsh, Conditional' }))
     expect(screen.queryByText(en.condition)).toBeNull()
 
     // A failed preset row names its runtime state instead of a condition.
-    fireEvent.click(screen.getByRole('button', { name: 'crashy, Failed' }))
+    fireEvent.click(screen.getByRole('button', { name: 'crashy, crashy, Failed' }))
     expect(screen.getByText(en.runtime)).toBeTruthy()
     expect(screen.getByText('Failed to start')).toBeTruthy()
 
@@ -134,6 +134,34 @@ describe('PluginInventorySettingsTab', () => {
     expect(screen.getByText(en.moduleLabel).nextElementSibling?.textContent).toBe('@fixture/anonymous')
   })
 
+  it('distinguishes collapsed same-module rows by stable entry id', async () => {
+    const longId = 'tool-subagent-secondary-with-a-complete-stable-identity'
+    await renderReady({
+      entries: [],
+      agentPresets: [{
+        id: 'same-module',
+        trust: 'user',
+        isDefault: true,
+        rows: [
+          { entryId: 'tool-subagent-primary', moduleName: '@deepseek-ai/dsh-tool-subagent', enabled: true, fiberPhase: null },
+          { entryId: longId, moduleName: '@deepseek-ai/dsh-tool-subagent', enabled: false, fiberPhase: null },
+        ],
+      }],
+    })
+
+    expect(screen.getByRole('button', { name: 'tool-subagent, tool-subagent-primary, Enabled' })
+      .getAttribute('aria-expanded')).toBe('false')
+    const secondary = screen.getByRole('button', { name: `tool-subagent, ${longId}, Disabled` })
+    expect(secondary.getAttribute('aria-expanded')).toBe('false')
+    expect(screen.getByTitle(longId).textContent).toBe(longId)
+
+    fireEvent.change(screen.getByRole('searchbox', { name: en.search }), { target: { value: 'secondary' } })
+    expect(screen.queryByRole('button', { name: 'tool-subagent, tool-subagent-primary, Enabled' })).toBeNull()
+    const filteredSecondary = screen.getByRole('button', { name: `tool-subagent, ${longId}, Disabled` })
+    fireEvent.click(filteredSecondary)
+    expect(filteredSecondary.getAttribute('aria-expanded')).toBe('true')
+  })
+
   it('expands the global plane with failures first and preset-provided rows inline', async () => {
     const view = await renderReady()
 
@@ -148,21 +176,21 @@ describe('PluginInventorySettingsTab', () => {
     // Rows the presets took over sit inline, marked instead of plainly disabled.
     expect(screen.getAllByText(en.presetEnabledTag)).toHaveLength(2)
 
-    fireEvent.click(screen.getByRole('button', { name: 'tool-bash, Enabled via presets' }))
+    fireEvent.click(screen.getByRole('button', { name: 'tool-bash, bash-host, Enabled via presets' }))
     expect(screen.getByText(en.presetProvidedDetail)).toBeTruthy()
     expect(screen.getByText(en.enabledIn)).toBeTruthy()
     expect(screen.getByText('标准模式 · ptc')).toBeTruthy()
 
     // The failed global card reports its runtime state.
-    fireEvent.click(screen.getByRole('button', { name: 'telemetry, Failed' }))
+    fireEvent.click(screen.getByRole('button', { name: 'telemetry, telemetry, Failed' }))
     expect(screen.getByText('Failed to start')).toBeTruthy()
 
     // An enabled entry with no live fiber says so in its details, dot-free.
-    fireEvent.click(screen.getByRole('button', { name: 'unobserved-name, Enabled' }))
+    fireEvent.click(screen.getByRole('button', { name: 'unobserved-name, unobserved, Enabled' }))
     expect(screen.getByText('Not running')).toBeTruthy()
 
     // A disabled row outside every preset stays plainly disabled.
-    fireEvent.click(screen.getByRole('button', { name: 'dormant, Disabled' }))
+    fireEvent.click(screen.getByRole('button', { name: 'dormant, dormant, Disabled' }))
     expect(screen.queryByText(en.presetProvidedDetail)).toBeNull()
 
     fireEvent.click(globalToggle())
@@ -179,7 +207,7 @@ describe('PluginInventorySettingsTab', () => {
 
     pickPreset('ptc')
     expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('3')
-    fireEvent.click(screen.getAllByRole('button', { name: 'tool-bash, Enabled' })[0]!)
+    fireEvent.click(screen.getByRole('button', { name: 'tool-bash, bash, Enabled' }))
     // An unnamed preset labels provenance by its id.
     expect(screen.getByText(en.fromPreset).nextElementSibling?.textContent).toBe('ptc')
 
@@ -227,11 +255,11 @@ describe('PluginInventorySettingsTab', () => {
     ])
     fireEvent.keyDown(document, { key: 'Escape' })
 
-    fireEvent.click(screen.getByRole('button', { name: 'pwsh, Conditional' }))
+    fireEvent.click(screen.getByRole('button', { name: 'pwsh, pwsh, Conditional' }))
     expect(screen.getByText(en.fromPreset).nextElementSibling?.textContent).toBe('Localized standard')
 
     fireEvent.click(globalToggle())
-    fireEvent.click(screen.getByRole('button', { name: 'tool-bash, Enabled via presets' }))
+    fireEvent.click(screen.getByRole('button', { name: 'tool-bash, bash-host, Enabled via presets' }))
     expect(screen.getByText('Localized standard · Localized ptc')).toBeTruthy()
   })
 
@@ -241,7 +269,7 @@ describe('PluginInventorySettingsTab', () => {
     fireEvent.click(screen.getByRole('menuitem', { name: 'ptc' }))
 
     fireEvent.click(globalToggle())
-    fireEvent.click(screen.getByRole('button', { name: 'tool-bash, Enabled via presets' }))
+    fireEvent.click(screen.getByRole('button', { name: 'tool-bash, bash-host, Enabled via presets' }))
     fireEvent.click(screen.getByRole('button', { name: en.viewInPreset }))
     expect(screen.getByRole('button', { name: en.switcherLabel }).textContent)
       .toBe('标准模式 (default)')
@@ -290,10 +318,10 @@ describe('PluginInventorySettingsTab', () => {
     expect(globalToggle().getAttribute('aria-expanded')).toBe('true')
     expect(screen.getAllByRole('listitem')).toHaveLength(2)
 
-    fireEvent.click(screen.getByRole('button', { name: 'hmr, Enabled' }))
+    fireEvent.click(screen.getByRole('button', { name: 'hmr, hmr, Enabled' }))
     expect(screen.getByText(en.runtime)).toBeTruthy()
     expect(view.container.querySelector('[data-loader-entry]')?.textContent).toBe('hmr')
-    fireEvent.click(screen.getByRole('button', { name: 'off, Disabled' }))
+    fireEvent.click(screen.getByRole('button', { name: 'off, off, Disabled' }))
     expect(screen.getAllByText(en.moduleLabel).length).toBeGreaterThan(0)
     expect(screen.queryByText(en.runtime)).toBeNull()
   })