From 4274befe55c558043d2663e6db321ac103740ab3 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 24 Jul 2026 23:06:13 +0100 Subject: [PATCH] Cover the last cold arms in seven frontend modules Seven modules from the current source drop still had a measured branch residue their own specs never reached. Counts are uncovered branches, module scoped, existing specs versus existing plus new. - resourceDetailMappers 13 -> 0, absent nested metadata, empty arrays and the unknown-discriminator default arm. - infrastructureAgentUpdateCommandsModel 9 -> 0 on top of three prior coverage files. - stackedMemoryBarModel 9 -> 1, zero and missing totals, over-total and negative segments, the clamping boundaries. - api/vmware 9 -> 0 and api/nodes 8 -> 0, request building and response handling arms rather than mocked-fetch echoes. api/nodes also covers createHostAgentInstallToken, which had never been called. - nodeModalModel 8 -> 0, blank fields and the validation rejection arms. - utils/workloads 8 -> 0. No source file is modified. Adversarial review returned no rejects and flagged ten near-duplicate cases across the vmware and workloads files; nine were deleted and the module coverage re-measured as identical, proving they carried nothing. The tenth was restored because re-measuring showed it did carry a branch. PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no source or contract change --- .../__tests__/nodes.branchcov0724pm.test.ts | 187 +++++++++++++ .../__tests__/vmware.branchcov0724pm.test.ts | 148 +++++++++++ ...ourceDetailMappers.branchcov0724pm.test.ts | 249 +++++++++++++++++ ...pdateCommandsModel.branchcov0724pm.test.ts | 251 ++++++++++++++++++ .../nodeModalModel.branchcov0724pm.test.ts | 162 +++++++++++ ...ckedMemoryBarModel.branchcov0724pm.test.ts | 197 ++++++++++++++ .../workloads.branchcov0724pm.test.ts | 220 +++++++++++++++ 7 files changed, 1414 insertions(+) create mode 100644 frontend-modern/src/api/__tests__/nodes.branchcov0724pm.test.ts create mode 100644 frontend-modern/src/api/__tests__/vmware.branchcov0724pm.test.ts create mode 100644 frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.branchcov0724pm.test.ts create mode 100644 frontend-modern/src/components/Settings/__tests__/infrastructureAgentUpdateCommandsModel.branchcov0724pm.test.ts create mode 100644 frontend-modern/src/components/Settings/__tests__/nodeModalModel.branchcov0724pm.test.ts create mode 100644 frontend-modern/src/components/Workloads/__tests__/stackedMemoryBarModel.branchcov0724pm.test.ts create mode 100644 frontend-modern/src/utils/__tests__/workloads.branchcov0724pm.test.ts diff --git a/frontend-modern/src/api/__tests__/nodes.branchcov0724pm.test.ts b/frontend-modern/src/api/__tests__/nodes.branchcov0724pm.test.ts new file mode 100644 index 000000000..7e9e3fda5 --- /dev/null +++ b/frontend-modern/src/api/__tests__/nodes.branchcov0724pm.test.ts @@ -0,0 +1,187 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { NodesAPI } from '../nodes'; +import type { ProxmoxSetupCommandResponse } from '../nodes'; +import type { APITokenRecord } from '@/types/api'; +import { apiFetch, apiFetchJSON } from '@/utils/apiClient'; + +vi.mock('@/utils/apiClient', () => ({ + apiFetch: vi.fn(), + apiFetchJSON: vi.fn(), +})); + +describe('NodesAPI — branch coverage (createHostAgentInstallToken)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const validRecord: APITokenRecord = { + id: 'tok-host-1', + name: 'host-agent', + prefix: 'hos_', + suffix: 'AB12', + createdAt: '2026-07-24T00:00:00Z', + }; + + it('mints a host agent install token, trims it, and echoes the record when a name is supplied', async () => { + vi.mocked(apiFetchJSON).mockResolvedValueOnce({ + token: ' host-token-xyz ', + record: validRecord, + }); + + const result = await NodesAPI.createHostAgentInstallToken({ + enableCommands: true, + name: 'render-east', + }); + + expect(apiFetchJSON).toHaveBeenCalledWith( + '/api/agent-install-command', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ + type: 'host', + enableCommands: true, + name: 'render-east', + }), + }), + ); + expect(result).toEqual({ token: 'host-token-xyz', record: validRecord }); + }); + + it('omits the name field from the request body entirely when no name is supplied', async () => { + vi.mocked(apiFetchJSON).mockResolvedValueOnce({ + token: 'host-token-xyz', + record: validRecord, + }); + + await NodesAPI.createHostAgentInstallToken({ enableCommands: false }); + + const [, options] = vi.mocked(apiFetchJSON).mock.calls[0]!; + const body = JSON.parse(options!.body as string); + expect(body).toEqual({ type: 'host', enableCommands: false }); + expect(body).not.toHaveProperty('name'); + }); + + it('rejects with the contract error when the token is blank (short-circuits before checking record)', async () => { + vi.mocked(apiFetchJSON).mockResolvedValueOnce({ + token: ' ', + record: validRecord, + }); + + await expect(NodesAPI.createHostAgentInstallToken({ enableCommands: true })).rejects.toThrow( + 'Invalid host agent install token response', + ); + }); + + it('rejects with the contract error when the record is absent even though the token is valid', async () => { + vi.mocked(apiFetchJSON).mockResolvedValueOnce({ + token: 'host-token-xyz', + record: null, + }); + + await expect(NodesAPI.createHostAgentInstallToken({ enableCommands: true })).rejects.toThrow( + 'Invalid host agent install token response', + ); + }); +}); + +describe('NodesAPI — branch coverage (normalizeProxmoxSetupCommandResponse validation arms)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const validPveResponse = () => ({ + type: 'pve', + host: 'https://pve.example:8006', + url: 'https://pulse.example/api/setup-script?type=pve', + downloadURL: 'https://pulse.example/api/setup-script?type=pve&setup_token=setup-token-123', + scriptFileName: 'pulse-setup-pve.sh', + command: 'curl pve ...', + commandWithEnv: 'curl env pve ...', + setupToken: 'setup-token-123', + tokenHint: 'set…123', + expires: 1_900_000_000, + }); + + it('rejects when host is blank', async () => { + vi.mocked(apiFetchJSON).mockResolvedValueOnce({ ...validPveResponse(), host: '' }); + + await expect( + NodesAPI.getProxmoxSetupCommand({ type: 'pve', host: 'pve.example', backupPerms: true }), + ).rejects.toThrow('Invalid Proxmox setup response host'); + }); + + it('rejects when url is blank', async () => { + vi.mocked(apiFetchJSON).mockResolvedValueOnce({ ...validPveResponse(), url: ' ' }); + + await expect( + NodesAPI.getProxmoxSetupCommand({ type: 'pve', host: 'pve.example', backupPerms: true }), + ).rejects.toThrow('Invalid Proxmox setup response URL'); + }); + + it('rejects when downloadURL is blank', async () => { + vi.mocked(apiFetchJSON).mockResolvedValueOnce({ ...validPveResponse(), downloadURL: '' }); + + await expect( + NodesAPI.getProxmoxSetupCommand({ type: 'pve', host: 'pve.example', backupPerms: true }), + ).rejects.toThrow('Invalid Proxmox setup response downloadURL'); + }); + + it('rejects when command is blank', async () => { + vi.mocked(apiFetchJSON).mockResolvedValueOnce({ ...validPveResponse(), command: '' }); + + await expect( + NodesAPI.getProxmoxSetupCommand({ type: 'pve', host: 'pve.example', backupPerms: true }), + ).rejects.toThrow('Invalid Proxmox setup response command'); + }); + + it('rejects when setupToken is blank', async () => { + vi.mocked(apiFetchJSON).mockResolvedValueOnce({ ...validPveResponse(), setupToken: '' }); + + await expect( + NodesAPI.getProxmoxSetupCommand({ type: 'pve', host: 'pve.example', backupPerms: true }), + ).rejects.toThrow('Invalid Proxmox setup response setup token'); + }); +}); + +describe('NodesAPI — branch coverage (downloadProxmoxSetupScript failure arms)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const pveBootstrap = (): ProxmoxSetupCommandResponse => ({ + type: 'pve', + host: 'https://pve.example:8006', + url: 'https://pulse.example/base/api/setup-script?type=pve', + downloadURL: 'https://pulse.example/base/api/setup-script?type=pve&setup_token=setup-token-123', + scriptFileName: 'pulse-setup-pve.sh', + command: 'curl pve ...', + commandWithEnv: 'curl env pve ...', + commandWithoutEnv: 'curl bare pve ...', + expires: 1_900_000_000, + tokenHint: 'set…123', + }); + + it('rejects when the fetch response is not ok (HTTP failure)', async () => { + vi.mocked(apiFetch).mockResolvedValueOnce(new Response('', { status: 502 })); + + await expect(NodesAPI.downloadProxmoxSetupScript(pveBootstrap())).rejects.toThrow( + 'Failed to fetch setup script', + ); + }); + + it('rejects when the script body is empty/whitespace after content-type and filename checks pass', async () => { + vi.mocked(apiFetch).mockResolvedValueOnce( + new Response(' ', { + status: 200, + headers: { + 'Content-Type': 'text/x-shellscript; charset=utf-8', + 'Content-Disposition': 'attachment; filename="pulse-setup-pve.sh"', + }, + }), + ); + + await expect(NodesAPI.downloadProxmoxSetupScript(pveBootstrap())).rejects.toThrow( + 'Empty Proxmox setup script response', + ); + }); +}); diff --git a/frontend-modern/src/api/__tests__/vmware.branchcov0724pm.test.ts b/frontend-modern/src/api/__tests__/vmware.branchcov0724pm.test.ts new file mode 100644 index 000000000..548418de8 --- /dev/null +++ b/frontend-modern/src/api/__tests__/vmware.branchcov0724pm.test.ts @@ -0,0 +1,148 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { apiFetchJSON } from '@/utils/apiClient'; +import { VMwareAPI } from '@/api/vmware'; + +vi.mock('@/utils/apiClient', () => ({ + apiFetchJSON: vi.fn(), +})); + +const mockedApiFetchJSON = vi.mocked(apiFetchJSON); + +describe('VMwareAPI.listConnections — branch coverage (response-shape arms)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('coerces a non-array response to an empty list via the arrayOrUndefined(...) ?? [] fallback', async () => { + // Backend contract occasionally yields a bare object / null envelope instead of an array. + // The `?? []` arm must yield [] without throwing. + mockedApiFetchJSON.mockResolvedValueOnce(null as never); + + await expect(VMwareAPI.listConnections()).resolves.toEqual([]); + + expect(mockedApiFetchJSON).toHaveBeenCalledWith('/api/vmware/connections'); + }); + + it('normalizes a poll.lastError object through the object-guard fall-through of normalizeVMwareConnectionPollError', async () => { + // Existing specs only ever pass `poll` WITHOUT a lastError, so the `(!error || typeof error !== 'object')` + // guard always returned early. Supplying a real object exercises the fall-through that builds the trimmed error. + mockedApiFetchJSON.mockResolvedValueOnce([ + { + id: 'conn-1', + host: 'vcsa.lab.local', + insecureSkipVerify: false, + enabled: true, + poll: { + intervalSeconds: 90, + lastAttemptAt: ' 2026-04-01T10:00:00Z ', + lastSuccessAt: ' 2026-04-01T09:59:00Z ', + consecutiveFailures: 3, + lastError: { + at: ' 2026-04-01T10:00:01Z ', + message: ' connection refused ', + category: ' transport ', + }, + }, + }, + ] as never); + + const [connection] = await VMwareAPI.listConnections(); + + expect(connection.poll).toEqual({ + intervalSeconds: 90, + lastAttemptAt: '2026-04-01T10:00:00Z', + lastSuccessAt: '2026-04-01T09:59:00Z', + consecutiveFailures: 3, + lastError: { + at: '2026-04-01T10:00:01Z', + message: 'connection refused', + category: 'transport', + }, + }); + }); + + it('defaults every observed host/vm/datastore/network count to 0 when the raw values are non-finite or absent', async () => { + // finiteNumberOrUndefined(...) returns undefined for strings/NaN/Infinity/missing values, so each + // `?? 0` fallback must fire independently for hosts, vms, datastores and networks. + mockedApiFetchJSON.mockResolvedValueOnce([ + { + id: 'conn-1', + host: 'vcsa.lab.local', + insecureSkipVerify: false, + enabled: true, + observed: { + collectedAt: ' 2026-04-01T00:00:00Z ', + hosts: 'not-a-number', + vms: NaN, + datastores: Infinity, + // networks intentionally absent + viRelease: ' 8.0.3 ', + degraded: true, + issueCount: 2, + }, + }, + ] as never); + + const [connection] = await VMwareAPI.listConnections(); + + expect(connection.observed).toMatchObject({ + collectedAt: '2026-04-01T00:00:00Z', + hosts: 0, + vms: 0, + datastores: 0, + networks: 0, + viRelease: '8.0.3', + degraded: true, + issueCount: 2, + }); + }); + + it('falls back to an empty name when the raw connection omits name (optionalTrimmedString(...) ?? "" arm)', async () => { + mockedApiFetchJSON.mockResolvedValueOnce([ + { + id: ' conn-2 ', + host: ' esxi-02.lab.local ', + insecureSkipVerify: true, + enabled: false, + // name intentionally absent + }, + ] as never); + + const [connection] = await VMwareAPI.listConnections(); + + expect(connection).toMatchObject({ + id: 'conn-2', + name: '', + host: 'esxi-02.lab.local', + }); + }); +}); + +describe('VMwareAPI.testSavedConnection — branch coverage (input-absent arm)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('omits the request body entirely when input is undefined (the `: {}` arm of the input ternary)', async () => { + mockedApiFetchJSON.mockResolvedValueOnce({ success: true } as never); + + await expect(VMwareAPI.testSavedConnection('conn/1')).resolves.toEqual({ + success: true, + hosts: 0, + vms: 0, + datastores: 0, + networks: 0, + viRelease: undefined, + degraded: false, + issueCount: 0, + issues: [], + }); + + expect(mockedApiFetchJSON).toHaveBeenCalledWith('/api/vmware/connections/conn%2F1/test', { + method: 'POST', + }); + // Belt-and-braces: confirm no `body` key was attached by the spread. + const [, options] = mockedApiFetchJSON.mock.calls[0]!; + expect(options).toEqual({ method: 'POST' }); + }); +}); diff --git a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.branchcov0724pm.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.branchcov0724pm.test.ts new file mode 100644 index 000000000..70bb3eed4 --- /dev/null +++ b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.branchcov0724pm.test.ts @@ -0,0 +1,249 @@ +import { describe, expect, it } from 'vitest'; +import { + buildTemperatureRows, + toAgentFromResource, + toNodeFromProxmox, + type AgentPlatformData, +} from '@/components/Infrastructure/resourceDetailMappers'; +import type { HostSensorSummary } from '@/types/api'; +import type { Resource } from '@/types/resource'; + +/** + * Branch-coverage additions for resourceDetailMappers.ts targeting the arms + * left uncovered by the existing happy-path and prior branchcov suites. + * + * Every assertion is made against observable return values of the exported + * entry points (toAgentFromResource, toNodeFromProxmox, buildTemperatureRows). + * The module-private helpers they delegate to (getPreferredHostLabel, + * getPreferredResourceHostname, getPreferredInfrastructureDisplayName, + * formatPowerSensorLabel, formatFanSensorLabel, formatGPUStatsValue, + * formatFanRPM) are driven exclusively through those exports and never + * re-implemented or imported directly. + */ + +const baseResource = (overrides: Partial = {}): Resource => + ({ + id: 'resource:host:hash-1', + type: 'agent', + name: 'tower', + displayName: 'Tower', + platformId: 'tower', + platformType: 'proxmox-pve', + sourceType: 'hybrid', + status: 'online', + lastSeen: 1_700_000_000_000, + cpu: { current: 12 }, + memory: { current: 0.25, total: 1024, used: 256, free: 768 }, + disk: { current: 0.25, total: 2048, used: 512, free: 1536 }, + platformData: { + proxmox: { nodeName: 'pve-node-1' }, + agent: { agentId: 'agent-canonical' }, + }, + ...overrides, + }) as unknown as Resource; + +describe('toAgentFromResource — uncovered arms', () => { + it('returns null when neither an explicit agent nor platformData.agent exists', () => { + // Drives the `if (!agent) return null` consequent (branch at line 289). + const resource = baseResource({ + platformData: { proxmox: { nodeName: 'pve-node-1' } }, + }); + + expect(toAgentFromResource(resource)).toBeNull(); + }); + + it('returns null when platformData is absent entirely', () => { + expect(toAgentFromResource(baseResource({ platformData: undefined }))).toBeNull(); + }); + + it('reads proxmox.cpuInfo.cores when cpuInfo is present (non-short-circuit optional chain)', () => { + // Drives the `cpuInfo?.cores` access arm (branch at line 291) by giving + // proxmox a populated cpuInfo, and confirms the value flows through the + // cpuCount `.find` predicate (`value > 0` evaluates true). + const resource = baseResource({ + platformData: { + proxmox: { cpuInfo: { cores: 4 } }, + agent: { agentId: 'agent-canonical' }, + }, + }); + + expect(toAgentFromResource(resource)?.cpuCount).toBe(4); + }); + + it('treats a non-positive cpuCount as not-usable and falls through to undefined', () => { + // cpuCount === 0 is a number so `typeof value === 'number'` is true, but + // `value > 0` is false — drives the false arm of the `.find` predicate's + // `&&` (branch at line 297). With no other positive candidate, cpuCount + // resolves to undefined. + const resource = baseResource({ + platformData: { + proxmox: {}, + agent: { agentId: 'agent-canonical', cpuCount: 0 }, + }, + }); + + expect(toAgentFromResource(resource)?.cpuCount).toBeUndefined(); + }); + + it('accepts a positive cpuCount as the first usable candidate', () => { + const resource = baseResource({ + platformData: { + proxmox: { cpuInfo: { cores: 99 } }, + agent: { agentId: 'agent-canonical', cpuCount: 8 }, + }, + }); + + // First array element wins over the proxmox fallback. + expect(toAgentFromResource(resource)?.cpuCount).toBe(8); + }); + + it('falls back to getPreferredHostLabel when agent.hostname is absent', () => { + // Drives the `agent.hostname ?? getPreferredHostLabel(resource)` RHS + // (branch at line 299). resource.name remains set so the host label + // resolves to "tower" via getPreferredResourceHostname. + const resource = baseResource({ + platformData: { + proxmox: {}, + agent: { agentId: 'agent-canonical' }, + }, + }); + + expect(toAgentFromResource(resource)?.hostname).toBe('tower'); + }); + + it('falls through hostname and displayName to resource.id when no identity source resolves', () => { + // Drives BOTH `||` short-circuit RHS arms in getPreferredHostLabel + // (branches at lines 188 and 189): no hostname, no displayName, no name, + // no platformId, no identity — so getPreferredResourceHostname is falsy + // (line 188 RHS evaluated) and getPreferredInfrastructureDisplayName also + // bottoms out at the empty resource.id (line 189 RHS evaluated). + // + // NOTE: the line-189 arm is only reachable when resource.id is empty + // (getPreferredInfrastructureDisplayName ultimately returns resource.id), + // i.e. a degenerate identity-less resource. It is reported here, not faked. + const resource = { + id: '', + type: 'agent', + status: 'online', + lastSeen: 1_700_000_000_000, + platformData: { proxmox: {} }, + } as unknown as Resource; + + const node = toNodeFromProxmox(resource); + expect(node?.name).toBe(''); + expect(node?.host).toBe(''); + }); + + it('uses resource.id for the agent id when no actionable agent id resolves', () => { + // Drives the `getActionableAgentIdFromResource(resource) || resource.id` + // RHS (branch at line 300). The resource carries no agent id in any form + // and no discoveryTarget. + const resource = baseResource({ + platformData: { proxmox: { nodeName: 'pve-node-1' } }, + }); + const explicitAgent = { hostname: 'h' } as AgentPlatformData; + + expect(toAgentFromResource(resource, explicitAgent)?.id).toBe('resource:host:hash-1'); + }); + + it('defaults osName to "Unknown" when the agent omits it', () => { + const resource = baseResource(); + const explicitAgent = {} as AgentPlatformData; + + expect(toAgentFromResource(resource, explicitAgent)?.osName).toBe('Unknown'); + }); + + it('defaults kernelVersion to "Unknown" when the agent omits it', () => { + const resource = baseResource(); + const explicitAgent = {} as AgentPlatformData; + + expect(toAgentFromResource(resource, explicitAgent)?.kernelVersion).toBe('Unknown'); + }); + + it('resolves getPreferredHostLabel via resource.displayName when every hostname source is empty', () => { + // Drives the line-188 `||` RHS (getPreferredResourceHostname falsy) while + // keeping getPreferredInfrastructureDisplayName truthy via displayName, so + // the host label resolves to the displayName without touching resource.id. + const resource = baseResource({ + name: undefined, + platformId: undefined, + identity: undefined, + displayName: 'Display Only', + platformData: { proxmox: {} }, + }); + const explicitAgent = {} as AgentPlatformData; + + expect(toAgentFromResource(resource, explicitAgent)?.hostname).toBe('Display Only'); + }); +}); + +describe('buildTemperatureRows — uncovered sensor-label arms', () => { + const findRow = (sensors: HostSensorSummary, label: string) => + buildTemperatureRows(sensors).find((r) => r.label === label); + + it('does not append " Power" when the sensor name already carries a power keyword', () => { + // Drives the consequent of the power-keyword ternary in + // formatPowerSensorLabel (branch at line 342). "cpu_power" title-cases to + // "CPU Power", which already matches \b(power)\b, so the label is returned + // verbatim instead of becoming "CPU Power Power". + expect(findRow({ powerWatts: { cpu_power: 82.4 } }, 'CPU Power')?.value).toBe('82.4 W'); + expect( + buildTemperatureRows({ powerWatts: { cpu_power: 82.4 } }).some( + (r) => r.label === 'CPU Power Power', + ), + ).toBe(false); + }); + + it('does not append " Power" for a "watts" keyword either', () => { + expect(findRow({ powerWatts: { psu_watts: 1500 } }, 'Psu Watts')?.value).toBe('1,500 W'); + }); + + it('appends " Fan" when the fan sensor name lacks the fan keyword', () => { + // Drives the alternate of the fan-keyword ternary in formatFanSensorLabel + // (branch at line 347). "blower" has no \bfan\b match, so the suffix is + // added, yielding "Blower Fan". + expect(findRow({ fanRpm: { blower: 1000 } }, 'Blower Fan')?.value).toBe('1,000 RPM'); + }); + + it('defaults GPU memoryUsedBytes to 0 when it is absent while memoryTotalBytes is valid', () => { + // Drives the alternate of the `used` ternary inside formatGPUStatsValue + // (branch at line 371): memoryTotalBytes is a positive finite number but + // memoryUsedBytes is undefined, so `used` falls back to 0 and is formatted + // as "0 B". + const rows = buildTemperatureRows({ + gpu: [ + { + id: '0', + name: 'Card', + memoryTotalBytes: 1024 * 1024 * 1024, + }, + ], + } as HostSensorSummary); + + const gpuRow = rows.find((r) => r.label === 'GPU 0'); + expect(gpuRow?.value).toBe('Card · 0 B / 1.00 GB'); + }); + + it('defaults GPU memoryUsedBytes to 0 when it is a non-finite value', () => { + const rows = buildTemperatureRows({ + gpu: [ + { + id: '0', + name: 'Card', + memoryTotalBytes: 1024 * 1024 * 1024, + memoryUsedBytes: Number.NaN, + }, + ], + } as HostSensorSummary); + + expect(rows.find((r) => r.label === 'GPU 0')?.value).toBe('Card · 0 B / 1.00 GB'); + }); + + it('drops a fan row whose RPM is non-finite (formatFanRPM returns empty and is filtered)', () => { + // Drives the `if (!Number.isFinite(value)) return ''` consequent in + // formatFanRPM (branch at line 385); the empty value is then removed by + // the downstream `.filter(([, value]) => value)`. + expect(buildTemperatureRows({ fanRpm: { dead: Number.NaN } })).toEqual([]); + expect(buildTemperatureRows({ fanRpm: { dead: Number.POSITIVE_INFINITY } })).toEqual([]); + }); +}); diff --git a/frontend-modern/src/components/Settings/__tests__/infrastructureAgentUpdateCommandsModel.branchcov0724pm.test.ts b/frontend-modern/src/components/Settings/__tests__/infrastructureAgentUpdateCommandsModel.branchcov0724pm.test.ts new file mode 100644 index 000000000..034bd6a86 --- /dev/null +++ b/frontend-modern/src/components/Settings/__tests__/infrastructureAgentUpdateCommandsModel.branchcov0724pm.test.ts @@ -0,0 +1,251 @@ +import { describe, expect, it } from 'vitest'; +import type { Connection } from '@/api/connections'; +import type { AgentFleetAgentDiagnostic, AgentFleetDiagnosticReason } from '@/api/agentDiagnostics'; +import { + collectInfrastructureAgentDoctorTargets, + formatInfrastructureAgentDoctorReport, + getInfrastructureAgentDoctorUninstallHandoff, +} from '../infrastructureAgentUpdateCommandsModel'; +import type { + InfrastructureAgentDoctorOptions, + InfrastructureAgentDoctorTarget, +} from '../infrastructureAgentUpdateCommandsModel'; + +// ---- Fixtures --------------------------------------------------------------- +// Mirrors the sibling infrastructureAgentUpdateCommandsModel.branchcov0722am.test.ts +// factories. This file targets the final nine branch arms V8 still reported as +// zero-hit after the five sibling suites ran. Every arm is driven through the +// exported orchestrators (collectInfrastructureAgentDoctorTargets / +// formatInfrastructureAgentDoctorReport / getInfrastructureAgentDoctorUninstallHandoff). + +const agentConnection = (overrides: Partial = {}): Connection => ({ + id: 'agent:host-1', + type: 'agent', + name: 'host-1', + address: 'host-1.lab', + state: 'active', + stateReason: '', + enabled: true, + surfaces: ['host'], + scope: { host: true }, + lastSeen: '2026-07-24T09:00:00Z', + lastError: null, + source: 'agent', + agentVersion: '6.2.0', + expectedAgentVersion: '6.2.0', + agentUpdateAvailable: false, + agentIdentity: { hostname: 'host-1', platform: 'linux', architecture: 'amd64' }, + capabilities: { supportsPause: false, supportsScope: false, supportsTest: false }, + ...overrides, +}); + +const diagnostic = ( + overrides: Partial = {}, +): AgentFleetAgentDiagnostic => ({ + connectionId: 'agent:host-1', + rowKey: 'host-1', + id: 'host-1', + agentId: 'host-1', + name: 'host-1', + hostname: 'host-1', + types: ['host'], + status: 'warning', + version: '6.2.0', + profileId: 'profile-linux', + profileName: 'Linux servers', + profileVersion: 4, + deployedProfileVersion: 3, + reasons: [], + repairActions: [], + ...overrides, +}); + +const runDoctor = ( + agents: readonly Connection[], + options: Partial = {}, +) => + collectInfrastructureAgentDoctorTargets({ + rows: [], + connections: agents, + diagnosticsAvailable: false, + ...options, + }); + +const runOrphans = ( + diagnostics: readonly AgentFleetAgentDiagnostic[], + options: Partial = {}, +) => + runDoctor([], { + diagnostics, + diagnosticsAvailable: true, + ...options, + }); + +// Minimal hand-built doctor target for the pure report formatter (which does +// not re-derive fields, only reads them). +const doctorTarget = ( + overrides: Partial = {}, +): InfrastructureAgentDoctorTarget => + ({ + key: 'agent:host-1', + connectionId: 'agent:host-1', + displayName: 'host-1', + contextLabel: 'Machine', + installFlags: [], + status: 'critical', + reasons: [], + evidence: [], + needsUpdate: false, + commandPlatform: null, + source: 'ledger-fallback', + ...overrides, + }) as InfrastructureAgentDoctorTarget; + +// ---- ledgerFallbackReasons: command_channel_disconnected via the && arm ------- +// The sibling suites only ever drive this reason through the +// `remoteControl === 'disconnected'` short-circuit of the `||`, so the +// `(commandPolicy.status === 'blocked' && commandsEnabled)` right operand is +// never evaluated. Driving it with remoteControl !== 'disconnected' exercises +// both operands of the `&&`, and varying commandPolicy.reason exercises both +// arms of the evidence ternary on line 346. + +describe('collectInfrastructureAgentDoctorTargets (command_channel_disconnected && arm)', () => { + it('emits the reason via commandPolicy blocked + commandsEnabled with reason evidence', () => { + // remoteControl 'enabled' forces the || past its left operand so the && + // (status === 'blocked' && commandsEnabled) is fully evaluated and true. + const [target] = runDoctor([ + agentConnection({ + agentIdentity: { hostname: 'host-1', platform: 'linux', commandsEnabled: true }, + fleet: { + versionDrift: 'current', + remoteControl: 'enabled', + commandPolicy: { status: 'blocked', reason: 'policy disabled by admin' }, + } as Connection['fleet'], + }), + ]); + + expect(target?.status).toBe('critical'); + expect( + target?.reasons.find((reason) => reason.code === 'command_channel_disconnected')?.evidence, + ).toEqual(['policy disabled by admin']); + }); + + it('emits the reason with empty evidence when commandPolicy has no reason', () => { + // Same && arm, but commandPolicy.reason absent -> the `reason ? [reason] : []` + // ternary takes its [] alternate (the only previously-unhit arm of line 346). + const [target] = runDoctor([ + agentConnection({ + agentIdentity: { hostname: 'host-1', platform: 'linux', commandsEnabled: true }, + fleet: { + versionDrift: 'current', + remoteControl: 'enabled', + commandPolicy: { status: 'blocked' }, + } as Connection['fleet'], + }), + ]); + + expect( + target?.reasons.find((reason) => reason.code === 'command_channel_disconnected')?.evidence, + ).toEqual([]); + }); +}); + +// ---- getInfrastructureAgentDoctorUninstallHandoff: identity agentId fallback -- + +describe('getInfrastructureAgentDoctorUninstallHandoff (agentId || undefined arm)', () => { + it('resolves identity.agentId to undefined when both agentId and id are blank', () => { + // `agentId?.trim() || id?.trim() || undefined` only reaches its final + // `|| undefined` operand when both agentId and id trim to empty. A removed + // diagnostic with name/hostname present still renders and resolves a known + // platform, so the handoff is non-null and carries the undefined agentId. + const [target] = runOrphans([ + diagnostic({ + connectionId: 'agent:gone', + rowKey: 'gone-row', + id: '', + agentId: undefined, + name: 'gone', + hostname: 'gone-host', + platform: 'debian', + status: 'removed', + }), + ]); + + const handoff = getInfrastructureAgentDoctorUninstallHandoff(target!); + expect(handoff).not.toBeNull(); + expect(handoff?.identity.agentId).toBeUndefined(); + expect(handoff?.identity.hostname).toBe('gone-host'); + expect(handoff?.commands.map((command) => command.platform)).toEqual(['linux']); + }); +}); + +// ---- doctorReportLastSeen: numeric + non-finite arms ------------------------ +// The sibling formatter test only passes a parseable ISO string, so the +// `typeof value === 'number'` consequent and the `!Number.isFinite(timestamp)` +// return arms of doctorReportLastSeen are never taken. + +describe('formatInfrastructureAgentDoctorReport (doctorReportLastSeen arms)', () => { + it('formats a numeric lastSeen directly as an ISO timestamp', () => { + // typeof value === 'number' -> timestamp = value (the consequent arm). + const ms = Date.parse('2025-01-22T09:00:00Z'); + const report = formatInfrastructureAgentDoctorReport([doctorTarget({ lastSeen: ms })]); + + expect(report).toContain('Last seen 2025-01-22T09:00:00.000Z'); + }); + + it('passes a non-parseable string lastSeen through verbatim', () => { + // Date.parse('not-a-date') -> NaN -> !isFinite -> typeof === 'string' arm. + const report = formatInfrastructureAgentDoctorReport([ + doctorTarget({ lastSeen: 'not-a-date' }), + ]); + + expect(report).toContain('Last seen not-a-date'); + }); + + it('omits Last seen when lastSeen is a non-finite number', () => { + // A NaN number passes the typeof === 'number' filter, fails isFinite, and is + // not a string -> doctorReportLastSeen returns undefined (the alternate arm). + const report = formatInfrastructureAgentDoctorReport([doctorTarget({ lastSeen: Number.NaN })]); + + expect(report).not.toContain('Last seen'); + }); +}); + +// ---- formatInfrastructureAgentDoctorReport: header + profile + evidence ------ + +describe('formatInfrastructureAgentDoctorReport (header / profile / reason-evidence arms)', () => { + it('renders the header with no status breakdown for an empty target list', () => { + // No targets -> every status count is zero -> countParts empty -> the + // `countParts.length > 0 ? ... : ''` ternary takes its '' alternate. + expect(formatInfrastructureAgentDoctorReport([])).toBe('Pulse Agent Doctor report (0 agents)'); + }); + + it('renders the profile label without a version parenthetical when profileVersionLabel is absent', () => { + // profileLabel present but profileVersionLabel undefined -> the + // `profileVersionLabel ? \` (${...})\` : ''` ternary takes its '' alternate. + const report = formatInfrastructureAgentDoctorReport([ + doctorTarget({ status: 'critical', profileLabel: 'default', profileVersionLabel: undefined }), + ]); + + expect(report.split('\n')).toContain(' Profile default'); + expect(report).not.toContain('Profile default ('); + }); + + it('renders a reason whose evidence is absent without any evidence sublines', () => { + // `reason.evidence ?? []` -> when evidence is nullish the [] operand wins and + // no 4-space-indented evidence line is emitted for that reason. + const reasonWithoutEvidence: AgentFleetDiagnosticReason = { + code: 'agent_stale', + severity: 'critical', + message: 'No heartbeat.', + evidence: undefined, + }; + const report = formatInfrastructureAgentDoctorReport([ + doctorTarget({ status: 'critical', reasons: [reasonWithoutEvidence], evidence: [] }), + ]); + + expect(report).toContain(' - No heartbeat.'); + // No 4-space-indented evidence line should appear anywhere in the report. + expect(report).not.toMatch(/\n {4}\S/); + }); +}); diff --git a/frontend-modern/src/components/Settings/__tests__/nodeModalModel.branchcov0724pm.test.ts b/frontend-modern/src/components/Settings/__tests__/nodeModalModel.branchcov0724pm.test.ts new file mode 100644 index 000000000..644d70623 --- /dev/null +++ b/frontend-modern/src/components/Settings/__tests__/nodeModalModel.branchcov0724pm.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from 'vitest'; +import type { ClusterEndpoint } from '@/types/nodes'; +import { + applyClusterNodeDisplayNamesLocally, + buildClusterNodeDisplayNameOverridesPayload, +} from '../nodeModalModel'; + +// Same fixture-builder shape as the sibling nodeModalModel.test.ts suite, but +// parameterised over the two optional fields the display-name helpers branch on +// (`nodeIdentity` + `displayName`) so each uncovered arm can be driven in +// isolation. `omitIdentity: true` yields a member whose nodeIdentity is absent +// (undefined), exercising the `!nodeIdentity` guards. +const endpoint = ( + nodeName: string, + fields: { nodeIdentity?: string; displayName?: string; omitIdentity?: boolean } = {}, +): ClusterEndpoint => ({ + nodeId: `node/${nodeName}`, + nodeIdentity: fields.omitIdentity ? undefined : (fields.nodeIdentity ?? `cluster-${nodeName}`), + nodeName, + host: `https://${nodeName}.local:8006`, + ip: '10.0.0.1', + online: true, + lastSeen: '', + ...(fields.displayName !== undefined ? { displayName: fields.displayName } : {}), +}); + +// ---- buildClusterNodeDisplayNameOverridesPayload ---------------------------- +// +// The sibling suites only feed this helper non-empty endpoints whose identities +// are all present in the form map and whose values all differ from the saved +// names. The tests below drive the remaining arms: +// * `if (!endpoints?.length) return undefined` — empty + undefined endpoints. +// * `if (!nodeIdentity) return []` — a member with an absent identity. +// * `if (value === undefined || value.trim() === (endpoint.displayName ?? ''))` +// — both the `value === undefined` short-circuit (identity absent from the +// form map) and the equality-true arm (form value trims to the saved name, +// including the `(undefined ?? '')` fallback for a member with no saved +// display name). +// * `changed.length > 0 ? changed : undefined` — the falsy (`: undefined`) +// arm reached when every member is filtered out. + +describe('buildClusterNodeDisplayNameOverridesPayload', () => { + it('returns undefined for empty or undefined endpoints (early return)', () => { + expect( + buildClusterNodeDisplayNameOverridesPayload([], { 'cluster-pve1': 'Compute' }), + ).toBeUndefined(); + expect( + buildClusterNodeDisplayNameOverridesPayload(undefined, { 'cluster-pve1': 'Compute' }), + ).toBeUndefined(); + }); + + it('drops members that have no nodeIdentity while still emitting changed siblings', () => { + const endpoints = [ + endpoint('pve1', { omitIdentity: true, displayName: 'Lonely' }), + endpoint('pve2', { displayName: 'Old' }), + ]; + const payload = buildClusterNodeDisplayNameOverridesPayload(endpoints, { + // Present but unreachable: pve1 has no identity, so this entry is never + // matched against it. + 'cluster-pve1': 'Whatever', + 'cluster-pve2': 'New', + }); + expect(payload).toEqual([{ nodeIdentity: 'cluster-pve2', displayName: 'New' }]); + }); + + it('skips members whose identity has no form entry (value === undefined short-circuit)', () => { + const endpoints = [ + endpoint('pve1', { displayName: 'Alpha' }), // 'cluster-pve1' absent from the map + endpoint('pve2', { displayName: 'Beta' }), // 'cluster-pve2' present + changed + ]; + const payload = buildClusterNodeDisplayNameOverridesPayload(endpoints, { + 'cluster-pve2': 'Beta2', + }); + expect(payload).toEqual([{ nodeIdentity: 'cluster-pve2', displayName: 'Beta2' }]); + }); + + it('treats a form value that trims to the saved display name as unchanged', () => { + const payload = buildClusterNodeDisplayNameOverridesPayload( + [endpoint('pve1', { displayName: 'Compute' })], + { 'cluster-pve1': ' Compute ' }, + ); + expect(payload).toBeUndefined(); + }); + + it('treats an empty form value as unchanged when no display name is saved (?? "" fallback)', () => { + // saved displayName is absent -> (undefined ?? '') === ''; form '' trims to + // '' which equals it, so the member is filtered and the result collapses to + // undefined via the `changed.length > 0 ? changed : undefined` falsy arm. + const payload = buildClusterNodeDisplayNameOverridesPayload([endpoint('pve1')], { + 'cluster-pve1': '', + }); + expect(payload).toBeUndefined(); + }); +}); + +// ---- applyClusterNodeDisplayNamesLocally ------------------------------------ +// +// Remaining arms: +// * `if (!endpoints?.length || !overrides?.length) return endpoints` — every +// falsy operand (empty/undefined endpoints, empty/undefined overrides), +// asserting the SAME reference is handed back untouched. +// * `if (!endpoint.nodeIdentity) return endpoint` — a member with an absent +// identity is returned by reference even when an override targets the +// identity string it would have had. +// * `const value = byIdentity.get(...); if (value === undefined) return endpoint` +// — no override targets this member's identity. +// * `displayName: value || undefined` — the falsy arm where an empty-string +// override clears the saved display name. + +describe('applyClusterNodeDisplayNamesLocally', () => { + it('returns the input by reference when there is nothing to apply', () => { + const endpoints = [endpoint('pve1')]; + // overrides empty or undefined -> endpoints handed back untouched + expect(applyClusterNodeDisplayNamesLocally(endpoints, [])).toBe(endpoints); + expect(applyClusterNodeDisplayNamesLocally(endpoints, undefined)).toBe(endpoints); + // endpoints empty or undefined -> early return of that same value + expect( + applyClusterNodeDisplayNamesLocally([], [{ nodeIdentity: 'cluster-pve1', displayName: 'X' }]), + ).toEqual([]); + expect( + applyClusterNodeDisplayNamesLocally(undefined, [ + { nodeIdentity: 'cluster-pve1', displayName: 'X' }, + ]), + ).toBeUndefined(); + }); + + it('leaves members without a nodeIdentity untouched (returns the same endpoint)', () => { + const endpoints = [ + endpoint('pve1', { omitIdentity: true, displayName: 'Kept' }), + endpoint('pve2', { displayName: 'Old' }), + ]; + const result = applyClusterNodeDisplayNamesLocally(endpoints, [ + { nodeIdentity: 'cluster-pve1', displayName: 'ShouldNotApply' }, + { nodeIdentity: 'cluster-pve2', displayName: 'New' }, + ]); + // pve1 short-circuits on `!nodeIdentity`: same object, display name intact. + expect(result?.[0]).toBe(endpoints[0]); + expect(result?.[0].displayName).toBe('Kept'); + // pve2 is matched and updated. + expect(result?.[1].displayName).toBe('New'); + }); + + it('leaves a member untouched when no override targets its identity', () => { + const endpoints = [endpoint('pve1', { displayName: 'Original' })]; + const result = applyClusterNodeDisplayNamesLocally(endpoints, [ + { nodeIdentity: 'cluster-somebody-else', displayName: 'Ignored' }, + ]); + expect(result?.[0]).toBe(endpoints[0]); + expect(result?.[0].displayName).toBe('Original'); + }); + + it('clears the display name when the override value is an empty string (value || undefined)', () => { + const endpoints = [endpoint('pve1', { displayName: 'OldName' })]; + const result = applyClusterNodeDisplayNamesLocally(endpoints, [ + { nodeIdentity: 'cluster-pve1', displayName: '' }, + ]); + expect(result?.[0].displayName).toBeUndefined(); + // a fresh object is produced (spread), not the original reference + expect(result?.[0]).not.toBe(endpoints[0]); + expect(result?.[0].nodeName).toBe('pve1'); + }); +}); diff --git a/frontend-modern/src/components/Workloads/__tests__/stackedMemoryBarModel.branchcov0724pm.test.ts b/frontend-modern/src/components/Workloads/__tests__/stackedMemoryBarModel.branchcov0724pm.test.ts new file mode 100644 index 000000000..25c8480cd --- /dev/null +++ b/frontend-modern/src/components/Workloads/__tests__/stackedMemoryBarModel.branchcov0724pm.test.ts @@ -0,0 +1,197 @@ +import { describe, expect, it } from 'vitest'; + +import type { AnomalyReport } from '@/types/aiIntelligence'; + +import type { StackedMemoryBarProps } from '../stackedMemoryBarModel'; +import { buildStackedMemoryBarPresentation } from '../stackedMemoryBarModel'; + +// The four module-private helpers (getEffectiveCache, getUtilizationPercent, +// getSegments, getTooltipRows) are exercised through the exported +// buildStackedMemoryBarPresentation. The happy-path spec +// (stackedMemoryBarModel.test.ts) covers total > 0 with used/cache/balloon +// combinations but never drives: total <= 0 (the division guard that falls +// through to percentOnly), any swap input, or any anomaly input. Those nine +// branches are the sole focus below — every assertion checks a concrete +// observable value rather than a recomputed source expression. + +const GiB = 1024 ** 3; + +// Normal-band RGBA for memory (default thresholds warning 75 / critical 85); +// a 70% utilization sits below warning -> 'normal' severity color. +const NORMAL_RGBA = 'rgba(34, 197, 94, 0.6)'; + +const makeProps = (overrides: Partial = {}): StackedMemoryBarProps => ({ + used: 0, + total: 0, + ...overrides, +}); + +const makeAnomaly = (overrides: Partial = {}): AnomalyReport => ({ + resource_id: 'r1', + resource_name: 'resource-1', + resource_type: 'guest', + metric: 'memory', + current_value: 30, + baseline_mean: 10, + baseline_std_dev: 1, + z_score: 20, + severity: 'high', + description: 'Memory spike', + ...overrides, +}); + +describe('stackedMemoryBarModel (branch coverage 0724pm)', () => { + describe('getUtilizationPercent — total <= 0 division guard (percentOnly fallback)', () => { + it('uses percentOnly when total <= 0 and percentOnly is finite', () => { + // Branch 8: false arm of `if (props.total > 0)` -> percentOnly path. + // Math.max(0, Math.min(70, 100)) === 70. + const p = buildStackedMemoryBarPresentation(makeProps({ total: 0, percentOnly: 70 }), 400); + expect(p.displayPercentValue).toBe(70); + expect(p.displayLabel).toBe('70%'); + }); + + it('clamps percentOnly > 100 down to 100', () => { + // Math.max(0, Math.min(150, 100)) === 100 — the upper clamp arm. + const p = buildStackedMemoryBarPresentation(makeProps({ total: 0, percentOnly: 150 }), 400); + expect(p.displayPercentValue).toBe(100); + expect(p.displayLabel).toBe('100%'); + }); + + it('clamps percentOnly < 0 up to 0', () => { + // Math.max(0, Math.min(-5, 100)) === 0 — the lower clamp arm. The 0% + // utilization then propagates to getSegments (no segment) and to the + // tooltip else arm. + const p = buildStackedMemoryBarPresentation(makeProps({ total: 0, percentOnly: -5 }), 400); + expect(p.displayPercentValue).toBe(0); + expect(p.segments).toEqual([]); + }); + + it('returns 0 when total <= 0 and percentOnly is NaN (not finite)', () => { + // Number.isFinite(NaN) === false -> final `return 0` arm. + const p = buildStackedMemoryBarPresentation( + makeProps({ total: 0, percentOnly: Number.NaN }), + 400, + ); + expect(p.displayPercentValue).toBe(0); + }); + + it('returns 0 when total <= 0 and percentOnly is absent', () => { + // percentOnly undefined -> Number.isFinite(undefined) === false -> 0. + const p = buildStackedMemoryBarPresentation(makeProps({ total: 0 }), 400); + expect(p.displayPercentValue).toBe(0); + expect(p.displayLabel).toBe('0%'); + }); + }); + + describe('getSegments — total <= 0 path', () => { + it('emits a single Utilization segment when total <= 0 and utilization > 0', () => { + // Branch 12: true arm of `if (props.total <= 0)`; utilizationPercent 70 + // exceeds the inner `<= 0` guard -> single Utilization segment colored + // via the default memory thresholds (70 < 75 -> normal). + const p = buildStackedMemoryBarPresentation(makeProps({ total: 0, percentOnly: 70 }), 400); + expect(p.segments).toHaveLength(1); + expect(p.segments[0]).toStrictEqual({ + color: NORMAL_RGBA, + label: 'Utilization', + leftPercent: 0, + widthPercent: 70, + }); + }); + + it('emits no segments when total <= 0 and utilization <= 0', () => { + // Branch 12 entered; inner `if (utilizationPercent <= 0)` true -> []. + const p = buildStackedMemoryBarPresentation(makeProps({ total: 0 }), 400); + expect(p.segments).toEqual([]); + }); + }); + + describe('getTooltipRows — else arm (unavailable=false AND total <= 0)', () => { + it('renders a single Utilization tooltip row carrying the percent-only label', () => { + // Branch 32: the final else arm (unavailable false, total <= 0) pushes a + // Utilization row whose value is the formatted displayLabel, not a byte + // reading — distinct from both the unavailable and total > 0 arms. + const p = buildStackedMemoryBarPresentation(makeProps({ total: 0, percentOnly: 70 }), 400); + expect(p.tooltipRows).toEqual([ + { + borderTop: true, + label: 'Utilization', + labelClass: 'text-blue-300', + value: '70%', + }, + ]); + }); + }); + + describe('swap integration (showSwapBar, swapBarPercent, Swap tooltip row)', () => { + it('enables the swap bar, reports the swap ratio, and adds a Swap tooltip row', () => { + // Branches 41 (showSwapBar && true), 42 (swapTotal && >0 true), + // 43 (ternary ? arm -> Math.min computation): + // swapBarPercent = Math.min((2 GiB / 8 GiB) * 100, 100) === 25. + // Branch 33: tooltip Swap row pushed because total > 0 && hasSwap. + const p = buildStackedMemoryBarPresentation( + makeProps({ used: 4 * GiB, total: 16 * GiB, swapUsed: 2 * GiB, swapTotal: 8 * GiB }), + 400, + ); + expect(p.showSwapBar).toBe(true); + expect(p.swapBarPercent).toBe(25); + const swap = p.tooltipRows.find((row) => row.label === 'Swap'); + expect(swap).toBeDefined(); + expect(swap?.borderTop).toBe(true); + expect(swap?.labelClass).toBe('text-amber-400'); + expect(swap?.value).toBe('2.00 GB / 8.00 GB'); + }); + + it('clamps swapBarPercent to 100 when swapUsed exceeds swapTotal', () => { + // Branch 43: Math.min((16 GiB / 8 GiB) * 100, 100) === Math.min(200, 100). + const p = buildStackedMemoryBarPresentation( + makeProps({ used: 4 * GiB, total: 16 * GiB, swapUsed: 16 * GiB, swapTotal: 8 * GiB }), + 400, + ); + expect(p.swapBarPercent).toBe(100); + expect(p.showSwapBar).toBe(true); + }); + + it('defaults swapUsed to 0 when absent (Swap row still shown, swap bar suppressed)', () => { + // Drives the `|| 0` fallback arms for swapUsed at line 246 (tooltip + // value), 278 (showSwapBar), and 281 (swapBarPercent). swapTotal > 0 + // keeps the Swap tooltip row visible (hasSwap true); swapUsed absent -> + // showSwapBar false, swapBarPercent 0, tooltip reads '0 B / 8.00 GB'. + const p = buildStackedMemoryBarPresentation( + makeProps({ used: 4 * GiB, total: 16 * GiB, swapTotal: 8 * GiB }), + 400, + ); + expect(p.showSwapBar).toBe(false); + expect(p.swapBarPercent).toBe(0); + const swap = p.tooltipRows.find((row) => row.label === 'Swap'); + expect(swap).toBeDefined(); + expect(swap?.value).toBe('0 B / 8.00 GB'); + }); + }); + + describe('anomaly threading (anomalyClass, anomalyDescription, anomalyRatio)', () => { + it('maps a known severity to its anomaly class and surfaces the description', () => { + // Branches 39 (props.anomaly truthy -> ANOMALY_SEVERITY_CLASS[severity]) + // and 40 (props.anomaly?.description non-null/undefined access). + // severity 'high' -> 'text-orange-400'; ratio 30/10 = 3.0 -> '3.0x'. + const p = buildStackedMemoryBarPresentation( + makeProps({ total: 16 * GiB, anomaly: makeAnomaly() }), + 400, + ); + expect(p.anomalyClass).toBe('text-orange-400'); + expect(p.anomalyDescription).toBe('Memory spike'); + expect(p.anomalyRatio).toBe('3.0x'); + }); + + it('falls back to the default anomaly class for an unknown severity', () => { + // Branch 39 true arm + the ?? 'text-yellow-400' fallback: + // ANOMALY_SEVERITY_CLASS has no 'weird' key -> undefined ?? fallback. + const p = buildStackedMemoryBarPresentation( + makeProps({ total: 16 * GiB, anomaly: makeAnomaly({ severity: 'weird' }) }), + 400, + ); + expect(p.anomalyClass).toBe('text-yellow-400'); + // description is still threaded through the optional-chain true arm. + expect(p.anomalyDescription).toBe('Memory spike'); + }); + }); +}); diff --git a/frontend-modern/src/utils/__tests__/workloads.branchcov0724pm.test.ts b/frontend-modern/src/utils/__tests__/workloads.branchcov0724pm.test.ts new file mode 100644 index 000000000..a1d32ec9d --- /dev/null +++ b/frontend-modern/src/utils/__tests__/workloads.branchcov0724pm.test.ts @@ -0,0 +1,220 @@ +import { describe, expect, it } from 'vitest'; + +import { + buildCanonicalNodeScopedWorkloadId, + buildKubernetesWorkloadMetadataId, + getCanonicalWorkloadIdForResource, + getWorkloadMetadataId, + getWorkloadMetadataIdCandidates, + isDockerManagedAppContainer, +} from '@/utils/workloads'; +import type { WorkloadGuest } from '@/types/workloads'; + +// `type`, `platformType`, and `containerRuntime` are optional on WorkloadGuest +// (`platformType`/`containerRuntime` via `?`, `type` via the VM | Container +// base). isDockerManagedAppContainer reads them defensively with `|| ''`, so +// the malformed-input cases below opt out of the contract intentionally. +type DockerManagedAppContainerInput = Pick< + WorkloadGuest, + 'workloadType' | 'type' | 'platformType' | 'containerRuntime' +>; + +// Covers the type/runtime fallback arms of isDockerManagedAppContainer that the +// happy-path specs never reach (existing specs only exercise the platformType +// 'docker' -> true and platformType 'truenas' -> false early returns). +describe('isDockerManagedAppContainer fallback signals', () => { + it('returns true from the raw type fallback when type is docker and platform type is unset', () => { + expect( + isDockerManagedAppContainer({ + workloadType: 'app-container', + type: 'docker', + platformType: undefined, + containerRuntime: undefined, + } as unknown as DockerManagedAppContainerInput), + ).toBe(true); + }); + + it('returns true from the container runtime fallback when runtime is docker', () => { + expect( + isDockerManagedAppContainer({ + workloadType: 'app-container', + type: 'app-container', + platformType: '', + containerRuntime: 'docker', + }), + ).toBe(true); + }); + + it('returns true from the container runtime fallback when runtime is podman', () => { + expect( + isDockerManagedAppContainer({ + workloadType: 'app-container', + type: 'app-container', + platformType: '', + containerRuntime: 'podman', + }), + ).toBe(true); + }); + + it('returns false when no docker signal matches across type, platform, or runtime', () => { + expect( + isDockerManagedAppContainer({ + workloadType: 'app-container', + type: 'app-container', + platformType: '', + containerRuntime: 'containerd', + }), + ).toBe(false); + }); + + it('returns false when type is undefined and runtime is unset', () => { + expect( + isDockerManagedAppContainer({ + workloadType: 'app-container', + type: undefined, + platformType: undefined, + containerRuntime: undefined, + } as unknown as DockerManagedAppContainerInput), + ).toBe(false); + }); + + it('is case-insensitive across the type and runtime fallbacks', () => { + expect( + isDockerManagedAppContainer({ + workloadType: 'app-container', + type: ' Docker ', + platformType: ' Unraid ', + containerRuntime: '', + }), + ).toBe(true); + expect( + isDockerManagedAppContainer({ + workloadType: 'app-container', + type: 'app-container', + platformType: '', + containerRuntime: 'PODMAN', + }), + ).toBe(true); + }); +}); + +// Covers the kind-coercion ternary in buildKubernetesWorkloadMetadataId. The +// happy-path specs only use the default kind ('pod'); the deployment/service +// arms and the falsy-kind guard are uncovered. +describe('buildKubernetesWorkloadMetadataId kind coercion', () => { + const base = { + kubernetesClusterId: 'cluster-a', + namespace: 'payments', + name: 'checkout', + }; + + it('maps k8s-deployment kind to the deployment segment', () => { + expect(buildKubernetesWorkloadMetadataId({ ...base, kind: 'k8s-deployment' })).toBe( + 'k8s-workload:cluster-a:deployment:payments:checkout', + ); + }); + + it('maps deployment kind to the deployment segment', () => { + expect(buildKubernetesWorkloadMetadataId({ ...base, kind: 'deployment' })).toBe( + 'k8s-workload:cluster-a:deployment:payments:checkout', + ); + }); + + it('maps k8s-service kind to the service segment', () => { + expect(buildKubernetesWorkloadMetadataId({ ...base, kind: 'k8s-service' })).toBe( + 'k8s-workload:cluster-a:service:payments:checkout', + ); + }); + + it('maps service kind to the service segment', () => { + expect(buildKubernetesWorkloadMetadataId({ ...base, kind: 'service' })).toBe( + 'k8s-workload:cluster-a:service:payments:checkout', + ); + }); + + it('is case-insensitive when matching the kind token', () => { + expect(buildKubernetesWorkloadMetadataId({ ...base, kind: 'Deployment' })).toBe( + 'k8s-workload:cluster-a:deployment:payments:checkout', + ); + expect(buildKubernetesWorkloadMetadataId({ ...base, kind: ' K8S-Service ' })).toBe( + 'k8s-workload:cluster-a:service:payments:checkout', + ); + }); + + it('returns null when kind is empty (no workload kind segment)', () => { + expect(buildKubernetesWorkloadMetadataId({ ...base, kind: '' })).toBeNull(); + }); +}); + +// Covers the empty-candidates fallback (`candidates[0] || getCanonicalWorkloadId`) +// in getWorkloadMetadataId. The happy-path specs always resolve at least one +// candidate, so the `||` fallback is never exercised. +describe('getWorkloadMetadataId empty-candidates fallback', () => { + const baseGuest = { + id: '', + name: '', + workloadType: 'app-container' as const, + type: 'app-container', + platformType: 'truenas', + instance: '', + node: '', + vmid: 0, + }; + + it('resolves zero metadata id candidates for a non-docker-managed app-container with a blank id', () => { + expect(getWorkloadMetadataIdCandidates(baseGuest)).toEqual([]); + }); + + it('falls back to the canonical workload id (the blank id) when no candidates resolve', () => { + expect(getWorkloadMetadataId(baseGuest)).toBe(''); + }); +}); + +// Covers the non-finite vmid arm (`Number.isFinite(vmid) ? Number(vmid) : 0`) +// in buildCanonicalNodeScopedWorkloadId. The happy-path specs only pass numeric +// vmids, so the `: 0` coercion (which then short-circuits to null) is cold. +describe('buildCanonicalNodeScopedWorkloadId non-finite vmid coercion', () => { + it('coerces a null vmid to 0 and returns null', () => { + expect( + buildCanonicalNodeScopedWorkloadId({ instance: 'homelab', node: 'pve1', vmid: null }), + ).toBeNull(); + }); +}); + +// Covers the proxmox.nodeName fallback in getCanonicalWorkloadIdForResource. +// The happy-path specs populate proxmox.node, so the `node || nodeName` fallback +// to nodeName is never exercised. +describe('getCanonicalWorkloadIdForResource proxmox nodeName fallback', () => { + it('uses proxmox.nodeName when proxmox.node is absent', () => { + expect( + getCanonicalWorkloadIdForResource({ + id: 'vm-fallback', + type: 'vm', + clusterId: 'Core Fabric', + proxmox: { nodeName: 'pve3', vmid: 112 }, + } as any), + ).toBe('Core Fabric:pve3:112'); + }); + + it('prefers proxmox.node over proxmox.nodeName when both are present', () => { + expect( + getCanonicalWorkloadIdForResource({ + id: 'lxc-fallback', + type: 'system-container', + clusterId: 'Core Fabric', + proxmox: { node: 'pve1', nodeName: 'pve3', vmid: 200 }, + } as any), + ).toBe('Core Fabric:pve1:200'); + }); + + it('falls back to the resource id when only nodeName is present but it is blank', () => { + expect( + getCanonicalWorkloadIdForResource({ + id: 'vm-orphan', + type: 'vm', + clusterId: 'Core Fabric', + proxmox: { nodeName: ' ', vmid: 112 }, + } as any), + ).toBe('vm-orphan'); + }); +});