mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +00:00
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
This commit is contained in:
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
+249
@@ -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> = {}): 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([]);
|
||||
});
|
||||
});
|
||||
+251
@@ -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> = {}): 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> = {},
|
||||
): 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<InfrastructureAgentDoctorOptions> = {},
|
||||
) =>
|
||||
collectInfrastructureAgentDoctorTargets({
|
||||
rows: [],
|
||||
connections: agents,
|
||||
diagnosticsAvailable: false,
|
||||
...options,
|
||||
});
|
||||
|
||||
const runOrphans = (
|
||||
diagnostics: readonly AgentFleetAgentDiagnostic[],
|
||||
options: Partial<InfrastructureAgentDoctorOptions> = {},
|
||||
) =>
|
||||
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> = {},
|
||||
): 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/);
|
||||
});
|
||||
});
|
||||
+162
@@ -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');
|
||||
});
|
||||
});
|
||||
+197
@@ -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> = {}): StackedMemoryBarProps => ({
|
||||
used: 0,
|
||||
total: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeAnomaly = (overrides: Partial<AnomalyReport> = {}): 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user