From 3a061d66c4b845f3900409cacb06a6270b6775c0 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:25:29 +0100 Subject: [PATCH] Gate Proxmox tabs on observed capabilities Avoid showing unsupported Replication, Ceph and Mail Gateway sections while Proxmox resource counts are unknown, while preserving bookmarked-route hydration and legitimate PBS navigation. Cover count transitions in component tests and synthetic production-page desktop/phone browser verification; update the affected subsystem contracts and receipt. Change-source: pulse-maintainer --- .../subsystems/frontend-primitives.md | 8 ++ .../internal/subsystems/storage-recovery.md | 7 ++ .../browser-tests/proxmox-tabs-2321-hook.ts | 48 +++++++ .../browser-tests/proxmox-tabs-2321-run.json | 1 + .../browser-tests/proxmox-tabs-2321.cjs | 119 ++++++++++++++++++ .../browser-tests/proxmox-tabs-2321.html | 12 ++ .../browser-tests/proxmox-tabs-2321.tsx | 25 ++++ frontend-modern/browser-verification.json | 73 +++++------ .../features/proxmox/ProxmoxPageSurface.tsx | 16 ++- .../ProxmoxPageSurface.contract.test.tsx | 47 ++++++- 10 files changed, 305 insertions(+), 51 deletions(-) create mode 100644 frontend-modern/browser-tests/proxmox-tabs-2321-hook.ts create mode 100644 frontend-modern/browser-tests/proxmox-tabs-2321-run.json create mode 100644 frontend-modern/browser-tests/proxmox-tabs-2321.cjs create mode 100644 frontend-modern/browser-tests/proxmox-tabs-2321.html create mode 100644 frontend-modern/browser-tests/proxmox-tabs-2321.tsx diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 1bb13fb6c2..bb5a9a7429 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -403,6 +403,14 @@ and thermal facts remain discoverable from the PBS surface. `ProxmoxBackupServersTable` belongs to the Proxmox Backups tab. Proxmox Overview must not duplicate that domain table between its node and guest regions; the tab boundary owns PBS server, datastore, and artifact detail. +The shared Proxmox section rail renders only tabs backed by current capability +evidence. While resource counts are unknown it must not show every optional +tab as a loading fallback; an independently fetched positive replication-job +count may still expose Replication. A bookmarked section remains the active +hydration target until counts can distinguish unavailable from unsupported, +then unsupported sections fall back to Overview. This rule applies at desktop +and phone widths and must not rewrite the URL or discard a valid PBS-only +Backups tab when its count arrives. When that surface receives the provider-owned PBS resource and its host Agent as separate canonical resources, `ProxmoxBackupServersTable` may assemble a presentation-only drawer resource only after one unique normalized host diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 0e49660db6..5abcc7c6d4 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -795,6 +795,13 @@ state effects remain dormant and a forced Proxmox storage scope is not serialized as redundant URL state. Backups reuses the source-scoped Overview guest snapshot and adds only the PBS resource family, so it cannot repeat the large VM/LXC enumeration under a second request owner. +Unknown resource aggregations must not advertise optional Proxmox workflows as +if their providers are present. The navigation rail may expose Replication +from its independent positive job count, while Storage, Backups, Ceph, and Mail +wait for positive resource counts. A direct link keeps its requested route +hydrating while counts are unknown; after counts arrive, an unsupported route +renders Overview instead. Hiding a tab is navigation presentation, not a +verdict about backup coverage, artifact recoverability, or stored history. PBS server/datastore rows may display backup counts, but the counts must come from the PBS backup API artifact identity, not from a datastore-capacity approximation. The table owns which PBS artifact count is diff --git a/frontend-modern/browser-tests/proxmox-tabs-2321-hook.ts b/frontend-modern/browser-tests/proxmox-tabs-2321-hook.ts new file mode 100644 index 0000000000..8fdff069ff --- /dev/null +++ b/frontend-modern/browser-tests/proxmox-tabs-2321-hook.ts @@ -0,0 +1,48 @@ +// Browser-only resource-hook seam for #2321. The production page and shared +// tab rail stay real; counts/resources are synthetic and switch on command. +import { createSignal } from 'solid-js'; +import type { Resource } from '../src/types/resource'; +import type { UnifiedResourceAggregations } from '../src/hooks/useUnifiedResources'; + +export * from '../src/hooks/useUnifiedResources'; + +const pbs = { + id: 'pbs-only', type: 'pbs', name: 'pbs-only', displayName: 'pbs-only', + platformId: 'pbs-only', platformType: 'proxmox-pbs', sourceType: 'api', + sources: ['pbs'], status: 'online', lastSeen: Date.now(), + pbs: { instanceId: 'pbs-only', hostname: 'pbs-only', datastores: [] }, +} as Resource; +const pmg = { + id: 'pmg-one', type: 'pmg', name: 'pmg-one', displayName: 'pmg-one', + platformId: 'pmg-one', platformType: 'proxmox-pmg', sourceType: 'api', + sources: ['pmg'], status: 'online', lastSeen: Date.now(), +} as Resource; + +const [counts, setCounts] = createSignal(null); +(window as unknown as { __proxmoxTabProof: { setCounts: typeof setCounts } }).__proxmoxTabProof = { + setCounts, +}; + +export function useUnifiedResources(options?: { cacheKey?: string }) { + const resources = () => { + switch (options?.cacheKey) { + case 'proxmox-backups-shell': + return [pbs]; + case 'proxmox-mail': + return [pmg]; + default: + return [] as Resource[]; + } + }; + return { + resources, + aggregations: counts, + facets: () => null, + policyPosture: () => null, + resourceSnapshotChange: () => ({ version: 0, changedIds: null }), + loading: () => false, + error: () => null, + refetch: async () => resources(), + mutate: () => resources(), + }; +} diff --git a/frontend-modern/browser-tests/proxmox-tabs-2321-run.json b/frontend-modern/browser-tests/proxmox-tabs-2321-run.json new file mode 100644 index 0000000000..439e716a48 --- /dev/null +++ b/frontend-modern/browser-tests/proxmox-tabs-2321-run.json @@ -0,0 +1 @@ +{"width":1365} diff --git a/frontend-modern/browser-tests/proxmox-tabs-2321.cjs b/frontend-modern/browser-tests/proxmox-tabs-2321.cjs new file mode 100644 index 0000000000..85eef131f1 --- /dev/null +++ b/frontend-modern/browser-tests/proxmox-tabs-2321.cjs @@ -0,0 +1,119 @@ +// #2321 browser proof: production Proxmox page and tab rail with a synthetic +// reactive resource-hook seam. No live API, credentials or reporter data. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { chromium } = require('playwright'); + +(async () => { + const root = '/workspace/frontend-modern'; + const artifacts = path.join(root, 'browser-tests'); + const width = JSON.parse(fs.readFileSync(path.join(artifacts, 'proxmox-tabs-2321-run.json'), 'utf8')).width; + assert.ok(width === 1365 || width === 390); + process.chdir(root); + const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js')); + const { default: solid } = await import(path.join(root, 'node_modules/vite-plugin-solid/dist/esm/index.mjs')); + const server = await createServer({ + root, + configFile: false, + plugins: [solid()], + resolve: { alias: [ + { find: /^@\/hooks\/useUnifiedResources$/, replacement: path.join(artifacts, 'proxmox-tabs-2321-hook.ts') }, + { find: '@', replacement: path.join(root, 'src') }, + ] }, + optimizeDeps: { noDiscovery: true, include: [], esbuildOptions: { target: 'esnext' } }, + server: { host: '127.0.0.1', port: 5201, strictPort: true }, + }); + let browser; + try { + await server.listen(); + browser = await chromium.launch({ headless: true, channel: 'chromium', args: ['--no-sandbox'] }); + const page = await browser.newPage({ viewport: { width, height: width === 390 ? 844 : 900 } }); + const pageErrors = []; + const apiRequests = []; + page.on('pageerror', (error) => pageErrors.push(error.message)); + await page.routeWebSocket(/\/ws(?:\?|$)/, () => {}); + await page.route('**/*', (route) => { + const url = new URL(route.request().url()); + if (url.origin !== 'http://127.0.0.1:5201') return route.abort(); + if (url.pathname.startsWith('/api/')) { + apiRequests.push(url.pathname); + if (url.pathname === '/api/replication/jobs') return route.fulfill({ json: { data: [] } }); + return route.fulfill({ json: { data: [] } }); + } + return route.continue(); + }); + await page.goto('http://127.0.0.1:5201/browser-tests/proxmox-tabs-2321.html?tab=mail', { + waitUntil: 'domcontentloaded', timeout: 120_000, + }); + await page.getByTestId('proxmox-page').waitFor(); + const nav = page.getByRole('navigation', { name: 'Proxmox sections' }); + const labels = async () => (await nav.locator('a').allTextContents()).map((label) => label.trim()); + const setCounts = async (byType) => page.evaluate((value) => window.__proxmoxTabProof.setCounts({ total: 1, byType: value }), byType); + const screenshot = async (state) => { + await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))); + const rail = await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="Proxmox sections"] a')].map((link) => ({ + label: link.textContent?.trim(), current: link.getAttribute('aria-current'), + selectedClass: link.classList.contains('border-blue-500'), + }))); + assert.ok(rail.every((link) => link.selectedClass === (link.current === 'page')), JSON.stringify({ state, rail })); + await page.screenshot({ path: path.join(artifacts, `proxmox-tabs-2321-${state}-${width}.png`), fullPage: true }); + }; + + assert.equal(new URL(page.url()).pathname, '/proxmox/mail'); + assert.equal(await nav.count(), 0, 'unknown counts must not show ghost tabs'); + await screenshot('unknown-direct-mail'); + + await setCounts({ pbs: 1 }); + await nav.waitFor(); + assert.deepEqual(await labels(), ['Overview', 'Backups']); + assert.equal(await nav.getByRole('link', { name: 'Overview' }).getAttribute('aria-current'), 'page'); + assert.equal(new URL(page.url()).pathname, '/proxmox/mail', 'fallback keeps the bookmarked URL'); + await screenshot('pbs-only-fallback'); + + const backups = nav.getByRole('link', { name: 'Backups' }); + if (width === 390) { + await backups.focus(); + await page.keyboard.press('Enter'); + } else { + await backups.click(); + } + try { + await page.waitForURL('**/proxmox/backups/date', { timeout: 5_000 }); + } catch (error) { + throw new Error(JSON.stringify({ href: await backups.getAttribute('href'), url: page.url(), body: await page.locator('body').innerText(), pageErrors }), { cause: error }); + } + assert.equal(new URL(page.url()).pathname, '/proxmox/backups/date'); + assert.equal(await backups.getAttribute('aria-current'), 'page'); + await screenshot('pbs-only-backups'); + + await setCounts({ pbs: 1, storage: 1, ceph: 1, pmg: 1 }); + assert.deepEqual(await labels(), ['Overview', 'Storage', 'Backups', 'Ceph', 'Mail Gateway']); + await nav.getByRole('link', { name: 'Mail Gateway' }).click(); + await page.waitForURL('**/proxmox/mail'); + assert.equal(new URL(page.url()).pathname, '/proxmox/mail'); + assert.equal(await nav.getByRole('link', { name: 'Mail Gateway' }).getAttribute('aria-current'), 'page'); + await screenshot('available-mail'); + + await setCounts({ pbs: 1 }); + assert.deepEqual(await labels(), ['Overview', 'Backups']); + assert.equal(await nav.getByRole('link', { name: 'Overview' }).getAttribute('aria-current'), 'page'); + await screenshot('removed-mail'); + + await setCounts({}); + assert.equal(await nav.count(), 0); + await screenshot('no-optional-services'); + + const dimensions = await page.evaluate(() => ({ scrollWidth: document.documentElement.scrollWidth, innerWidth: window.innerWidth })); + assert.ok(dimensions.scrollWidth <= dimensions.innerWidth + 1, JSON.stringify(dimensions)); + assert.deepEqual(pageErrors, []); + console.log(JSON.stringify({ result: 'passed', source: 'production ProxmoxPageSurface and PlatformSectionTabs; synthetic useUnifiedResources', width, browser: browser.version(), playwright: '1.56.1', apiRequests, pageErrors, dimensions })); + await page.close(); + } finally { + if (browser) await browser.close(); + await server.close(); + } +})().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/frontend-modern/browser-tests/proxmox-tabs-2321.html b/frontend-modern/browser-tests/proxmox-tabs-2321.html new file mode 100644 index 0000000000..a7a63aaf59 --- /dev/null +++ b/frontend-modern/browser-tests/proxmox-tabs-2321.html @@ -0,0 +1,12 @@ + + + + + + Proxmox tabs browser fixture + + +
+ + + diff --git a/frontend-modern/browser-tests/proxmox-tabs-2321.tsx b/frontend-modern/browser-tests/proxmox-tabs-2321.tsx new file mode 100644 index 0000000000..c501ec26b6 --- /dev/null +++ b/frontend-modern/browser-tests/proxmox-tabs-2321.tsx @@ -0,0 +1,25 @@ +// Mount the production page and router; the browser runner replaces only the +// resource and replication API responses for #2321's PBS-only estate. +import { Route, Router } from '@solidjs/router'; +import { render } from 'solid-js/web'; +import { ProxmoxPageSurface } from '../src/features/proxmox/ProxmoxPageSurface'; +import { DarkModeContext, WebSocketContext } from '../src/contexts/appRuntime'; +import { getGlobalWebSocketStore } from '../src/stores/websocket-global'; +import '../src/index.css'; + +const requested = new URLSearchParams(window.location.search).get('tab'); +const tab = requested === 'mail' || requested === 'backups' ? requested : 'overview'; +window.history.replaceState(null, '', `/proxmox/${tab}`); + +render( + () => ( + + false}> + + + + + + ), + document.getElementById('root')!, +); diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 25a94eeed0..c9580fa5df 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,62 +1,47 @@ { "version": 1, - "base_sha": "8619ab2ec8f5d239d045dec1f1f719ff3c5d687e", - "verified_at": "2026-09-29T06:55:46Z", + "base_sha": "c54d361acf0d38d3f90767022513f718e05e1b69", + "verified_at": "2026-09-29T11:23:17Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/api/resourceActions.ts", - "frontend-modern/src/features/actions/ActionDecisionPacket.tsx", - "frontend-modern/src/features/actions/ActionReviewDialog.tsx", - "frontend-modern/src/features/actions/actionPresentation.ts", - "frontend-modern/src/pages/Actions.tsx" + "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx" ], "content_sha256": { - "frontend-modern/src/api/resourceActions.ts": "60e65faa74d6fffd43f97dfa6ab2eff946c666893b5201217bd3e886312ac47b", - "frontend-modern/src/features/actions/ActionDecisionPacket.tsx": "2839a0be877b906bbc717bae7ef829aaffc2e46de8242413617d078087799cb1", - "frontend-modern/src/features/actions/ActionReviewDialog.tsx": "ff35ee1912d45b3a0546bd31ed498ecf94f01a493c0a86b93252b0e5d7a529d0", - "frontend-modern/src/features/actions/actionPresentation.ts": "723a40b5b41f8645c76f54e2c0ffac3ae60757b885ee0bee6b299dc97146ebc1", - "frontend-modern/src/pages/Actions.tsx": "c43ae74844fb8d31b904fa273d4a35c0344084ef7b6e227e55d8924dfa7b248c" + "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "37ce3942c563770c3bdd494a692ac9fe22cf367479b4f788136ad5d8faaaf625" }, "routes": [ - "/action-recovery-proof.html (mock-backed shared /actions review component)" + "/proxmox/mail (direct link, synthetic capability fixture)", + "/proxmox/backups/date (tab navigation, synthetic capability fixture)" ], "viewports": [ - { - "width": 1365, - "height": 900 - }, - { - "width": 390, - "height": 844 - } + { "width": 1365, "height": 900 }, + { "width": 390, "height": 844 } ], "states": [ - "aged admin", - "terminal inconclusive", - "recent", - "read-only", - "settled", - "non-admin", - "stale re-read" + "unknown resource counts on direct Mail Gateway link", + "PBS-only resource counts with Mail Gateway unsupported", + "PBS Backups tab active", + "positive Storage, Ceph and Mail Gateway counts", + "Mail Gateway tab active", + "Mail Gateway count removed after navigation", + "no optional service counts" ], "interactions": [ - "open disclosure", - "reason and acknowledgement gates", - "force-fail post", - "refresh outcome", - "stale skip", - "phone layout" + "hydrate bookmarked Mail Gateway route before counts settle", + "reveal PBS-only Backups while hiding unsupported Replication, Ceph and Mail Gateway tabs", + "activate Backups by pointer on desktop and keyboard on phone", + "activate Mail Gateway after positive counts", + "fall back to Overview when Mail Gateway count disappears without rewriting the URL", + "verify selected-tab semantics, desktop and phone rail layout, and no document overflow" ], - "command": "pulse-worker-browser web-1891-proof.cjs from assigned workspace root; offline Vite API preview and Chromium 141.0.7390.37 / Playwright 1.56.1", + "command": "pulse-worker-browser frontend-modern/browser-tests/proxmox-tabs-2321.cjs from assigned root, with browser-tests/proxmox-tabs-2321-run.json set to 1365 then 390; Chromium 141.0.7390.37 / Playwright 1.56.1", "artifacts": [ - "/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/browser-1891-result.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/browser-1891-desktop-before.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/browser-1891-desktop-after.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/browser-1891-mobile.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/browser-1891-mobile-dialog.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/web-1891-proof.cjs", - "/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/action-recovery-proof.html", - "/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/action-recovery-proof.tsx" + "/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-2321-desktop.log", + "/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-2321-phone.log", + "/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-images/proxmox-tabs-2321-unknown-direct-mail-1365.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-images/proxmox-tabs-2321-pbs-only-backups-390.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-images/proxmox-tabs-2321-available-mail-390.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-images/proxmox-tabs-2321-removed-mail-1365.png" ], - "notes": "Shared ActionReviewDialog exercised with mock-backed API/status at exact staged runtime content; no live backend, container outcome, installed release or reporter acceptance. Component tests cover Actions history state. Screenshots visually inspected after transition." + "notes": "Production ProxmoxPageSurface and PlatformSectionTabs were mounted in a Vite/Chromium fixture. Only useUnifiedResources and API responses were synthetic; no installed server, agent, reporter topology or release was exercised. Both runs exited 0 with no page errors or horizontal overflow. Final screenshots were inspected for selected rail, table placement and narrow reachability." } diff --git a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx index 55d3bcbc34..aebcd84fae 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx @@ -116,18 +116,22 @@ export function ProxmoxPageSurface() { replicationJobs.error ? 0 : (replicationJobs() ?? []).length, ); const visibleTabs = createMemo(() => { - const aggregations = resourceAggregations(); - if (aggregations) { - return buildVisibleProxmoxTabSpecsFromCounts(aggregations.byType, replicationJobCount()); - } - return PROXMOX_TAB_SPECS; + // An unknown snapshot is not evidence that every optional integration is + // present. Keep only sections backed by an independent signal until the + // resource counts arrive (or if an older response omits them). + return buildVisibleProxmoxTabSpecsFromCounts( + resourceAggregations()?.byType ?? {}, + replicationJobCount(), + ); }); const visibleTabIds = createMemo( () => new Set(visibleTabs().map((tab) => tab.id)), ); const activeTab = createMemo(() => { const requested = requestedTab(); - return visibleTabIds().has(requested) ? requested : 'overview'; + // Do not discard a direct link while counts are still unknown: its own + // resource query must be allowed to hydrate before deciding it is absent. + return !resourceAggregations() || visibleTabIds().has(requested) ? requested : 'overview'; }); const shouldHydrateTab = (tab: ProxmoxPageTabId) => activeTab() === tab; const overviewResources = useUnifiedResources({ diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx index 5cc8ba71ab..52364765a7 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx @@ -1,6 +1,8 @@ -import { cleanup, render, screen } from '@solidjs/testing-library'; +import { cleanup, render, screen, waitFor } from '@solidjs/testing-library'; import { Route, Router } from '@solidjs/router'; +import { createSignal } from 'solid-js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { UnifiedResourceAggregations } from '@/hooks/useUnifiedResources'; import type { Resource } from '@/types/resource'; import { ProxmoxPageSurface } from '../ProxmoxPageSurface'; import proxmoxPageSurfaceSource from '../ProxmoxPageSurface.tsx?raw'; @@ -577,6 +579,49 @@ describe('ProxmoxPageSurface contract', () => { expect(screen.queryByRole('list', { name: 'Proxmox Patrol coverage' })).not.toBeInTheDocument(); }); + it('does not advertise optional sections before resource counts arrive', () => { + setResourcesSnapshot(undefined, true); + + renderSurface(); + + expect(screen.getByTestId('platform-section-tabs')).toHaveAttribute('data-tabs', 'overview'); + expect(screen.getByTestId('platform-table-loading-state')).toBeInTheDocument(); + }); + + it('hydrates a direct link while counts are unknown, then gates it on actual capabilities', async () => { + mockPathname.mockReturnValue('/proxmox/mail'); + const [aggregations, setAggregations] = createSignal(null); + mockUseUnifiedResources.mockReturnValue({ + resources: () => [], + aggregations, + loading: () => false, + error: () => null, + refetch: vi.fn(), + }); + + renderSurface(); + + const tabs = screen.getByTestId('platform-section-tabs'); + expect(tabs).toHaveAttribute('data-tabs', 'overview'); + expect(tabs).toHaveAttribute('data-active', 'mail'); + const options = mockUseUnifiedResources.mock.calls.map( + ([value]) => value as { cacheKey: string; enabled: () => boolean }, + ); + expect(options.find((option) => option.cacheKey === 'proxmox-mail')?.enabled()).toBe(true); + + setAggregations({ total: 2, byType: { pbs: 1, pmg: 1 } }); + await waitFor(() => { + expect(tabs).toHaveAttribute('data-tabs', 'overview,backups,mail'); + expect(tabs).toHaveAttribute('data-active', 'mail'); + }); + + setAggregations({ total: 1, byType: { pbs: 1 } }); + await waitFor(() => { + expect(tabs).toHaveAttribute('data-tabs', 'overview,backups'); + expect(tabs).toHaveAttribute('data-active', 'overview'); + }); + }); + it('does not surface stale-agent notices for development builds without an agent target', () => { mockVersionInfo.mockReturnValue({ version: '6.0.0-rc.6+git.172.g2c360f779.dirty',