From ba3e2d2c1c1cc3c2fa07d1c3372080082121f1f2 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:11:19 +0100 Subject: [PATCH] Reproduce guest coverage confidence from cached agent metadata Exercise explicit guest-agent states alongside old version metadata, parent action ownership and same-guest replacement. Require visible backup precautions across drawer tabs without running a guest check. Contract-Neutral: Regression tests only; no runtime or public contract change. Change-source: pulse-maintainer --- .../components/Workloads/GuestDrawer.test.tsx | 36 ++++ .../GuestDrawerOverview.agent-state.test.tsx | 163 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 frontend-modern/src/components/Workloads/__tests__/GuestDrawerOverview.agent-state.test.tsx diff --git a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx index 4aa35bd42c..16d9ec2e5d 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx @@ -220,6 +220,42 @@ afterEach(() => { // ── Tests ────────────────────────────────────────────────────────────── describe('GuestDrawer', () => { + it('keeps the backup precaution visible across drawer tabs without starting a guest check', async () => { + const [guest, setGuest] = createSignal( + makeGuest({ + agentVersion: '9.2', + guestAgentStatus: 'deferred', + diskStatusReason: 'prev-vm-locked', + lock: 'backup', + }), + ); + render(() => ); + const precaution = () => screen.getByTestId('guest-read-precaution'); + expect(precaution()).toBeVisible(); + expect(precaution()).toHaveTextContent( + 'Do not run live diagnostics or restart the guest agent during a backup.', + ); + expect(precaution()).toHaveTextContent('does not prove thaw'); + expect(precaution()).toHaveTextContent('filesystems covered by the backup'); + expect( + within(precaution()).getByRole('link', { name: 'Backup safety guidance' }), + ).toHaveAttribute('href', '/docs/VM_DISK_MONITORING'); + fireEvent.click(screen.getByRole('tab', { name: 'History' })); + expect(precaution()).toBeVisible(); + setGuest({ + ...guest(), + lock: '', + guestAgentStatus: 'expected-unreachable', + diskStatusReason: 'agent-timeout', + }); + expect(precaution()).toHaveTextContent('Completion is uncertain'); + fireEvent.click(screen.getByRole('tab', { name: 'Manage' })); + expect(precaution()).toBeVisible(); + setGuest({ ...guest(), guestAgentStatus: 'available', diskStatusReason: '' }); + expect(screen.queryByTestId('guest-read-precaution')).not.toBeInTheDocument(); + expect(discoveryApiMocks.getDiscovery).not.toHaveBeenCalled(); + }); + it('keeps malformed and future backup times unknown through same-guest replacement', () => { const [guest, setGuest] = createSignal(makeGuest({ lastBackup: NaN })); render(() => ); diff --git a/frontend-modern/src/components/Workloads/__tests__/GuestDrawerOverview.agent-state.test.tsx b/frontend-modern/src/components/Workloads/__tests__/GuestDrawerOverview.agent-state.test.tsx new file mode 100644 index 0000000000..8d08002a53 --- /dev/null +++ b/frontend-modern/src/components/Workloads/__tests__/GuestDrawerOverview.agent-state.test.tsx @@ -0,0 +1,163 @@ +import { cleanup, render, screen, within } from '@solidjs/testing-library'; +import { createSignal, type ComponentProps } from 'solid-js'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { GuestDrawerOverview } from '../GuestDrawerOverview'; +import type { WorkloadGuest } from '@/types/workloads'; + +vi.mock('../GuestPhysicalDisks', () => ({ GuestPhysicalDisks: () => null })); + +const guest = (overrides: Partial = {}): WorkloadGuest => ({ + id: 'lab:pve-a:101', + vmid: 101, + name: 'backup-guest', + node: 'pve-a', + instance: 'lab', + status: 'running', + type: 'qemu', + cpu: 0.1, + cpus: 2, + memory: { total: 1024 ** 3, used: 256 * 1024 ** 2, free: 768 * 1024 ** 2, usage: 25 }, + disk: { total: 10 * 1024 ** 3, used: 5 * 1024 ** 3, usage: 50 }, + networkIn: 0, + networkOut: 0, + diskRead: 0, + diskWrite: 0, + uptime: 3600, + template: false, + tags: [], + lock: '', + lastBackup: 0, + lastSeen: '2026-10-04T14:00:00Z', + ...overrides, +}); + +function overviewProps(value: WorkloadGuest): ComponentProps { + return { + guest: value, + guestOsSummary: 'Linux', + agentHeading: value.agentKind === 'pulse' ? 'Pulse Agent' : 'Guest agent', + agentLabel: value.agentVersion ? `QEMU ${value.agentVersion}` : '', + agentTitle: 'Previously observed agent version', + hasAgentInfo: Boolean(value.agentVersion), + hasFilesystemDetails: false, + hasNetworkInterfaces: false, + hasOsInfo: true, + hasWorkloadActionAgent: false, + showInGuestAgentInstallCue: false, + ipAddresses: [], + networkInterfaces: [], + normalizedTags: [], + backupPresentation: null, + workloadActionAgentTitle: '', + }; +} + +const readRow = () => screen.getByText('Guest-agent reads').closest('tr')!; + +afterEach(cleanup); + +describe('guest agent coverage requires current state, not cached metadata', () => { + it.each([ + ['deferred', 'Deferred'], + ['expected-unreachable', 'Unreachable'], + ['not-running', 'Not running'], + ['disabled', 'Disabled'], + ['available', 'Reported available'], + ['future-state-private-detail', 'Unknown'], + ])('shows %s independently of an old version or its absence', (state, label) => { + const [value, setValue] = createSignal(guest({ agentVersion: '9.2', guestAgentStatus: state })); + render(() => ); + expect(within(readRow()).getByText(label)).toBeVisible(); + expect(screen.queryByText(/agent connected/i)).not.toBeInTheDocument(); + expect(screen.queryByText('future-state-private-detail')).not.toBeInTheDocument(); + setValue({ ...value(), agentVersion: '' }); + expect(within(readRow()).getByText(label)).toBeVisible(); + expect(screen.queryByText('QEMU 9.2')).not.toBeInTheDocument(); + }); + + it('does not turn a version-only observation into liveness', () => { + render(() => ); + expect(screen.getByText('Guest agent observed')).toBeVisible(); + expect(within(readRow()).getByText('Unknown')).toBeVisible(); + expect(screen.queryByText(/connected/i)).not.toBeInTheDocument(); + }); + + it('does not hide deferred guest reads behind parent-node action ownership', () => { + render(() => ( + + )); + expect(screen.getByText('Node agent assigned')).toBeVisible(); + expect(within(readRow()).getByText('Deferred')).toBeVisible(); + }); + + it('withdraws cached connectivity across the same guest lock, uncertainty and recovery', () => { + const [value, setValue] = createSignal( + guest({ agentVersion: '9.2', guestAgentStatus: 'available' }), + ); + render(() => ); + expect(within(readRow()).getByText('Reported available')).toBeVisible(); + setValue({ + ...value(), + guestAgentStatus: 'available', + lock: 'backup', + backupInProgress: true, + diskStatusReason: 'prev-vm-locked', + }); + expect(within(readRow()).getByText('Deferred')).toBeVisible(); + expect(screen.getByText('QEMU 9.2')).toBeVisible(); + setValue({ + ...value(), + lock: '', + backupInProgress: false, + guestAgentStatus: 'expected-unreachable', + diskStatusReason: 'agent-timeout', + }); + expect(within(readRow()).getByText('Unreachable')).toBeVisible(); + setValue({ ...value(), guestAgentStatus: 'available', diskStatusReason: '' }); + expect(within(readRow()).getByText('Reported available')).toBeVisible(); + expect(screen.queryByText(/connected/i)).not.toBeInTheDocument(); + }); + + it('keeps Pulse Agent metadata separate from QEMU read availability', () => { + render(() => ( + + )); + expect(screen.getByText('Pulse Agent observed')).toBeVisible(); + expect(within(readRow()).getByText('Unreachable')).toBeVisible(); + expect(screen.queryByText('Pulse Agent connected')).not.toBeInTheDocument(); + }); + + it('does not add QEMU state to a non-Proxmox VM or container from a version alone', () => { + const [value, setValue] = createSignal( + guest({ + type: 'vm', + platformType: 'vmware-vsphere', + platformScopes: ['vmware-vsphere'], + agentKind: 'pulse', + agentVersion: '6.4.5', + }), + ); + render(() => ); + expect(screen.getByText('Pulse Agent observed')).toBeVisible(); + expect(screen.queryByText('Guest-agent reads')).not.toBeInTheDocument(); + setValue({ + ...value(), + type: 'lxc', + platformType: 'proxmox-pve', + platformScopes: ['proxmox-pve'], + }); + expect(screen.queryByText('Guest-agent reads')).not.toBeInTheDocument(); + }); +});