mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Add focused branch coverage and repair infrastructure source contract
Contract-Neutral: test-only coverage and source-contract assertions; no runtime or contract behavior changed
This commit is contained in:
+7
-3
@@ -414,10 +414,14 @@ describe('infrastructure operations model', () => {
|
||||
expect(agentUpgradeSource).toContain(
|
||||
'| bash -s -- --update --url ${shellQuoteArg(url)} --non-interactive',
|
||||
);
|
||||
expect(operationsStateSource).toContain('getAgentConnectionUpgradeCommandRequiresToken');
|
||||
expect(operationsStateSource).toContain(
|
||||
"getConnectionUpgradePlatform(connection) === 'windows' && installState.requiresToken()",
|
||||
const requiresTokenEnd = operationsStateSource.indexOf('return {', agentUpgradeEnd);
|
||||
expect(requiresTokenEnd).toBeGreaterThan(agentUpgradeEnd);
|
||||
const requiresTokenSource = operationsStateSource.slice(agentUpgradeEnd, requiresTokenEnd);
|
||||
expect(requiresTokenSource).toContain(
|
||||
'platformOverride ?? getConnectionUpgradePlatform(connection)',
|
||||
);
|
||||
expect(requiresTokenSource).toContain("=== 'windows'");
|
||||
expect(requiresTokenSource).toContain('installState.requiresToken()');
|
||||
expect(unixUpgradeSource).not.toContain('command += ` --token ${shellQuoteArg(token)}`;');
|
||||
expect(unixUpgradeSource).not.toContain('--agent-id');
|
||||
expect(unixUpgradeSource).not.toContain('--hostname');
|
||||
|
||||
+1380
File diff suppressed because it is too large
Load Diff
+205
@@ -0,0 +1,205 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { PatrolRunRecord, PatrolStatus } from '@/api/patrol';
|
||||
import {
|
||||
schedulePatrolRunAcceptanceReconciliation,
|
||||
type PatrolRunAcceptanceOutcome,
|
||||
} from '../patrolRunAcceptance';
|
||||
|
||||
const runningStatus = (currentRunId?: string): PatrolStatus =>
|
||||
({
|
||||
runtime_state: 'running',
|
||||
running: true,
|
||||
enabled: true,
|
||||
...(currentRunId !== undefined ? { current_run_id: currentRunId } : {}),
|
||||
}) as PatrolStatus;
|
||||
|
||||
const idleStatus = (): PatrolStatus =>
|
||||
({ runtime_state: 'active', running: false, enabled: true }) as PatrolStatus;
|
||||
|
||||
const completedRun = (id: string): PatrolRunRecord =>
|
||||
({ id, status: 'healthy' }) as PatrolRunRecord;
|
||||
|
||||
interface Scenario {
|
||||
runId?: string;
|
||||
delayMs?: number;
|
||||
refreshTimeoutMs?: number;
|
||||
isCurrent?: () => boolean;
|
||||
getStatus: () => Promise<PatrolStatus | null>;
|
||||
getHistory: () => Promise<PatrolRunRecord[]>;
|
||||
}
|
||||
|
||||
const scheduleScenario = (scenario: Scenario) => {
|
||||
const onResult = vi.fn<(outcome: PatrolRunAcceptanceOutcome) => void>();
|
||||
const cancel = schedulePatrolRunAcceptanceReconciliation({
|
||||
runId: scenario.runId ?? 'run-1',
|
||||
delayMs: scenario.delayMs ?? 1_000,
|
||||
refreshTimeoutMs: scenario.refreshTimeoutMs ?? 5_000,
|
||||
getStatus: scenario.getStatus,
|
||||
getHistory: scenario.getHistory,
|
||||
isCurrent: scenario.isCurrent ?? (() => true),
|
||||
onResult,
|
||||
});
|
||||
return { onResult, cancel };
|
||||
};
|
||||
|
||||
describe('schedulePatrolRunAcceptanceReconciliation — branch coverage', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("kind 'running'", () => {
|
||||
it('accepts when status.current_run_id === runId', async () => {
|
||||
const { onResult } = scheduleScenario({
|
||||
getStatus: async () => runningStatus('run-1'),
|
||||
getHistory: async () => [],
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(onResult).toHaveBeenCalledTimes(1);
|
||||
const outcome = onResult.mock.calls[0][0];
|
||||
expect(outcome.kind).toBe('running');
|
||||
expect((outcome as { status: PatrolStatus }).status.current_run_id).toBe('run-1');
|
||||
});
|
||||
|
||||
it('accepts when runId is empty (no specific run tracked)', async () => {
|
||||
const { onResult } = scheduleScenario({
|
||||
runId: '',
|
||||
getStatus: async () => runningStatus('run-1'),
|
||||
getHistory: async () => [],
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(onResult).toHaveBeenCalledTimes(1);
|
||||
expect(onResult.mock.calls[0][0].kind).toBe('running');
|
||||
});
|
||||
|
||||
it('accepts when status has no current_run_id', async () => {
|
||||
const { onResult } = scheduleScenario({
|
||||
getStatus: async () => runningStatus(undefined),
|
||||
getHistory: async () => [],
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(onResult).toHaveBeenCalledTimes(1);
|
||||
expect(onResult.mock.calls[0][0].kind).toBe('running');
|
||||
});
|
||||
});
|
||||
|
||||
describe("kind 'recorded'", () => {
|
||||
it('records when patrol is idle but history contains the accepted run', async () => {
|
||||
const { onResult } = scheduleScenario({
|
||||
getStatus: async () => idleStatus(),
|
||||
getHistory: async () => [completedRun('run-1')],
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(onResult).toHaveBeenCalledTimes(1);
|
||||
const outcome = onResult.mock.calls[0][0];
|
||||
expect(outcome.kind).toBe('recorded');
|
||||
expect((outcome as { run: PatrolRunRecord }).run.id).toBe('run-1');
|
||||
});
|
||||
|
||||
it('records when a newer run has taken over the patrol slot', async () => {
|
||||
const { onResult } = scheduleScenario({
|
||||
getStatus: async () => runningStatus('run-newer'),
|
||||
getHistory: async () => [completedRun('run-1')],
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(onResult).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
kind: 'recorded',
|
||||
run: expect.objectContaining({ id: 'run-1' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("kind 'refresh_failed'", () => {
|
||||
it('reports the status read rejection reason', async () => {
|
||||
const statusError = new Error('status down');
|
||||
const { onResult } = scheduleScenario({
|
||||
getStatus: () => Promise.reject(statusError),
|
||||
getHistory: async () => [],
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(onResult).toHaveBeenCalledTimes(1);
|
||||
const outcome = onResult.mock.calls[0][0];
|
||||
expect(outcome.kind).toBe('refresh_failed');
|
||||
expect((outcome as { error: unknown }).error).toBe(statusError);
|
||||
});
|
||||
|
||||
it('reports the history read rejection reason', async () => {
|
||||
const historyError = new Error('history down');
|
||||
const { onResult } = scheduleScenario({
|
||||
getStatus: async () => idleStatus(),
|
||||
getHistory: () => Promise.reject(historyError),
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(onResult).toHaveBeenCalledTimes(1);
|
||||
const outcome = onResult.mock.calls[0][0];
|
||||
expect(outcome.kind).toBe('refresh_failed');
|
||||
expect((outcome as { error: unknown }).error).toBe(historyError);
|
||||
});
|
||||
|
||||
it('bounds hung reads with the refresh timeout', async () => {
|
||||
const { onResult } = scheduleScenario({
|
||||
delayMs: 1_000,
|
||||
refreshTimeoutMs: 2_000,
|
||||
getStatus: () => new Promise<PatrolStatus | null>(() => undefined),
|
||||
getHistory: () => new Promise<PatrolRunRecord[]>(() => undefined),
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(3_000);
|
||||
expect(onResult).toHaveBeenCalledTimes(1);
|
||||
expect(onResult.mock.calls[0][0].kind).toBe('refresh_failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe("kind 'missing'", () => {
|
||||
it('falls through when idle, history lacks the run, and no refresh failed', async () => {
|
||||
const { onResult } = scheduleScenario({
|
||||
getStatus: async () => idleStatus(),
|
||||
getHistory: async () => [completedRun('run-other')],
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(onResult).toHaveBeenCalledWith({ kind: 'missing' });
|
||||
});
|
||||
|
||||
it('falls through to missing when no runId is tracked and nothing is running', async () => {
|
||||
const { onResult } = scheduleScenario({
|
||||
runId: '',
|
||||
getStatus: async () => idleStatus(),
|
||||
getHistory: async () => [completedRun('run-1')],
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(onResult).toHaveBeenCalledWith({ kind: 'missing' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('guard branches', () => {
|
||||
it('suppresses onResult when isCurrent() is false after the delay', async () => {
|
||||
const getStatus = vi.fn(async () => runningStatus('run-1'));
|
||||
const getHistory = vi.fn(async () => [] as PatrolRunRecord[]);
|
||||
const { onResult } = scheduleScenario({
|
||||
getStatus,
|
||||
getHistory,
|
||||
isCurrent: () => false,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(getStatus).toHaveBeenCalledTimes(1);
|
||||
expect(getHistory).toHaveBeenCalledTimes(1);
|
||||
expect(onResult).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cancels before the delay fires: clears the timer and skips the reads', async () => {
|
||||
const getStatus = vi.fn(async () => runningStatus('run-1'));
|
||||
const getHistory = vi.fn(async () => [] as PatrolRunRecord[]);
|
||||
const { onResult, cancel } = scheduleScenario({ getStatus, getHistory });
|
||||
cancel();
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
expect(getStatus).not.toHaveBeenCalled();
|
||||
expect(getHistory).not.toHaveBeenCalled();
|
||||
expect(onResult).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
+677
@@ -0,0 +1,677 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { PhysicalDiskFieldStatus, Resource } from '@/types/resource';
|
||||
import {
|
||||
PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS,
|
||||
buildPhysicalDiskPresentationDataMap,
|
||||
buildPhysicalDiskGroupFilterOptions,
|
||||
buildPhysicalDiskRoleFilterOptions,
|
||||
comparePhysicalDiskPresentation,
|
||||
extractPhysicalDiskPresentationData,
|
||||
filterAndSortPhysicalDisks,
|
||||
getPhysicalDiskCollectionMessages,
|
||||
getPhysicalDiskEmptyStatePresentation,
|
||||
getPhysicalDiskFieldStatusMessage,
|
||||
getPhysicalDiskHealthStatus,
|
||||
getPhysicalDiskHealthSummary,
|
||||
getPhysicalDiskHostLabel,
|
||||
getPhysicalDiskLifeTextClass,
|
||||
getPhysicalDiskNormalizedHealth,
|
||||
getPhysicalDiskPlatformLabel,
|
||||
getPhysicalDiskRoleLabel,
|
||||
getPhysicalDiskSourceBadgePresentation,
|
||||
hasPhysicalDiskSmartWarning,
|
||||
hasUnraidPhysicalDiskFaultSignal,
|
||||
isUnraidPhysicalDisk,
|
||||
matchesPhysicalDiskHealthFilter,
|
||||
matchesPhysicalDiskSearch,
|
||||
type PhysicalDiskPresentationData,
|
||||
} from '@/features/storageBackups/diskPresentation';
|
||||
|
||||
function makeDiskData(
|
||||
overrides: Partial<PhysicalDiskPresentationData> = {},
|
||||
): PhysicalDiskPresentationData {
|
||||
return {
|
||||
node: '',
|
||||
instance: '',
|
||||
devPath: '',
|
||||
model: '',
|
||||
serial: '',
|
||||
wwn: '',
|
||||
size: 0,
|
||||
health: 'UNKNOWN',
|
||||
riskReasons: [],
|
||||
wearout: -1,
|
||||
type: '',
|
||||
temperature: 0,
|
||||
rpm: 0,
|
||||
used: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('diskPresentation.branchcov0720pm', () => {
|
||||
describe('getPhysicalDiskHealthFilterEmptyTitle switch arms', () => {
|
||||
it('returns the matching empty-state title for the healthy filter', () => {
|
||||
expect(
|
||||
getPhysicalDiskEmptyStatePresentation({
|
||||
selectedNodeName: null,
|
||||
searchTerm: '',
|
||||
diskCount: 2,
|
||||
hasPVENodes: true,
|
||||
healthFilter: 'healthy',
|
||||
}).title,
|
||||
).toBe('No healthy disks found');
|
||||
});
|
||||
|
||||
it('returns the matching empty-state title for the warning filter', () => {
|
||||
expect(
|
||||
getPhysicalDiskEmptyStatePresentation({
|
||||
selectedNodeName: null,
|
||||
searchTerm: '',
|
||||
diskCount: 2,
|
||||
hasPVENodes: true,
|
||||
healthFilter: 'warning',
|
||||
}).title,
|
||||
).toBe('No warning disks found');
|
||||
});
|
||||
|
||||
it('returns the matching empty-state title for the critical filter', () => {
|
||||
expect(
|
||||
getPhysicalDiskEmptyStatePresentation({
|
||||
selectedNodeName: null,
|
||||
searchTerm: '',
|
||||
diskCount: 2,
|
||||
hasPVENodes: true,
|
||||
healthFilter: 'critical',
|
||||
}).title,
|
||||
).toBe('No critical disks found');
|
||||
});
|
||||
|
||||
it('returns the matching empty-state title for the offline filter', () => {
|
||||
expect(
|
||||
getPhysicalDiskEmptyStatePresentation({
|
||||
selectedNodeName: null,
|
||||
searchTerm: '',
|
||||
diskCount: 2,
|
||||
hasPVENodes: true,
|
||||
healthFilter: 'offline',
|
||||
}).title,
|
||||
).toBe('No offline disks found');
|
||||
});
|
||||
|
||||
it('returns the matching empty-state title for the unknown filter', () => {
|
||||
expect(
|
||||
getPhysicalDiskEmptyStatePresentation({
|
||||
selectedNodeName: null,
|
||||
searchTerm: '',
|
||||
diskCount: 2,
|
||||
hasPVENodes: true,
|
||||
healthFilter: 'unknown',
|
||||
}).title,
|
||||
).toBe('No disks with unknown health');
|
||||
});
|
||||
|
||||
it('falls back to the default-empty title when healthFilter is "all" but disks are present', () => {
|
||||
expect(
|
||||
getPhysicalDiskEmptyStatePresentation({
|
||||
selectedNodeName: null,
|
||||
searchTerm: 'zzz',
|
||||
diskCount: 2,
|
||||
hasPVENodes: true,
|
||||
healthFilter: 'all',
|
||||
}).title,
|
||||
).toBe('No disks match these filters');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhysicalDiskEmptyStatePresentation ternary arms', () => {
|
||||
it('returns null nodeMessage and searchMessage when nothing is selected', () => {
|
||||
const presentation = getPhysicalDiskEmptyStatePresentation({
|
||||
selectedNodeName: null,
|
||||
searchTerm: '',
|
||||
diskCount: 0,
|
||||
hasPVENodes: false,
|
||||
});
|
||||
expect(presentation.nodeMessage).toBeNull();
|
||||
expect(presentation.searchMessage).toBeNull();
|
||||
expect(presentation.showRequirements).toBe(false);
|
||||
});
|
||||
|
||||
it('returns a searchMessage when a searchTerm is supplied', () => {
|
||||
const presentation = getPhysicalDiskEmptyStatePresentation({
|
||||
selectedNodeName: null,
|
||||
searchTerm: 'wd20',
|
||||
diskCount: 0,
|
||||
hasPVENodes: false,
|
||||
});
|
||||
expect(presentation.searchMessage).toBe('matching "wd20"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhysicalDiskFieldStatusMessage default arm', () => {
|
||||
it('returns empty string for an unrecognized state via the default switch arm', () => {
|
||||
const malformed = {
|
||||
state: 'totally-bogus',
|
||||
source: 'test',
|
||||
} as unknown as PhysicalDiskFieldStatus;
|
||||
expect(getPhysicalDiskFieldStatusMessage('Field', malformed)).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string when status is null or available', () => {
|
||||
expect(getPhysicalDiskFieldStatusMessage('Field', null)).toBe('');
|
||||
expect(
|
||||
getPhysicalDiskFieldStatusMessage('Field', { state: 'available', source: 'test' }),
|
||||
).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhysicalDiskCollectionMessages empty paths', () => {
|
||||
it('returns an empty array when the disk has no collection evidence', () => {
|
||||
expect(getPhysicalDiskCollectionMessages(makeDiskData())).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhysicalDiskPlatformLabel fallback', () => {
|
||||
it('returns "Unknown" when no fallback label is supplied', () => {
|
||||
expect(getPhysicalDiskPlatformLabel({} as Resource, '')).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhysicalDiskSourceBadgePresentation fallback arms', () => {
|
||||
it('falls back to the platform-derived label and base tone when the presentation is missing', () => {
|
||||
const badge = getPhysicalDiskSourceBadgePresentation({
|
||||
platformType: '',
|
||||
} as unknown as Resource);
|
||||
expect(badge.label).toBe('Unknown');
|
||||
expect(badge.className).toContain('text-base-content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhysicalDiskHostLabel fallback arms', () => {
|
||||
it('falls back to resource.parentName when disk.node is empty', () => {
|
||||
expect(
|
||||
getPhysicalDiskHostLabel(makeDiskData(), { parentName: 'pve-node-1' } as Resource),
|
||||
).toBe('pve-node-1');
|
||||
});
|
||||
|
||||
it('returns an empty string when neither node nor parentName is set', () => {
|
||||
expect(getPhysicalDiskHostLabel(makeDiskData(), {} as Resource)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractPhysicalDiskPresentationData fallback arms', () => {
|
||||
it('reads physicalDisk from platformData when the top-level field is absent', () => {
|
||||
const resource = {
|
||||
name: 'fallback-disk',
|
||||
platformData: { physicalDisk: { devPath: '/dev/sdz', health: 'PASSED', wearout: 88 } },
|
||||
} as unknown as Resource;
|
||||
const data = extractPhysicalDiskPresentationData(resource);
|
||||
expect(data.devPath).toBe('/dev/sdz');
|
||||
expect(data.health).toBe('PASSED');
|
||||
expect(data.wearout).toBe(88);
|
||||
});
|
||||
|
||||
it('returns defaults when neither physicalDisk nor platformData.physicalDisk is present', () => {
|
||||
const data = extractPhysicalDiskPresentationData({ name: 'empty' } as unknown as Resource);
|
||||
expect(data.devPath).toBe('');
|
||||
expect(data.model).toBe('empty');
|
||||
expect(data.health).toBe('UNKNOWN');
|
||||
expect(data.wearout).toBe(-1);
|
||||
expect(data.temperature).toBe(0);
|
||||
expect(data.rpm).toBe(0);
|
||||
expect(data.riskLevel).toBeUndefined();
|
||||
expect(data.smartAttributes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('coerces zero values through the ?? and || branches', () => {
|
||||
const data = extractPhysicalDiskPresentationData({
|
||||
name: 'zeroed',
|
||||
physicalDisk: {
|
||||
devPath: '',
|
||||
model: '',
|
||||
serial: '',
|
||||
wwn: '',
|
||||
diskType: '',
|
||||
sizeBytes: 0,
|
||||
health: '',
|
||||
wearout: 0,
|
||||
temperature: 0,
|
||||
rpm: 0,
|
||||
used: '',
|
||||
},
|
||||
} as unknown as Resource);
|
||||
expect(data.model).toBe('zeroed');
|
||||
expect(data.health).toBe('UNKNOWN');
|
||||
expect(data.size).toBe(0);
|
||||
});
|
||||
|
||||
it('returns an empty model when neither pd.model nor resource.name is set', () => {
|
||||
const data = extractPhysicalDiskPresentationData({
|
||||
name: '',
|
||||
physicalDisk: { devPath: '/dev/sda' },
|
||||
} as unknown as Resource);
|
||||
expect(data.model).toBe('');
|
||||
});
|
||||
|
||||
it('reads sources from platformData.sourceStatus object keys', () => {
|
||||
const resource = {
|
||||
name: 'src',
|
||||
platformData: { sourceStatus: { proxmox: { ok: true }, agent: { ok: false } } },
|
||||
} as unknown as Resource;
|
||||
expect(getPhysicalDiskNormalizedHealth(resource, makeDiskData({ health: 'UNKNOWN' }))).toBe(
|
||||
'unknown',
|
||||
);
|
||||
});
|
||||
|
||||
it('treats a non-object sourceStatus as no extra sources', () => {
|
||||
const resource = {
|
||||
name: 'srcstr',
|
||||
platformData: { sourceStatus: 'not-an-object' as unknown as object },
|
||||
} as unknown as Resource;
|
||||
expect(getPhysicalDiskNormalizedHealth(resource, makeDiskData({ health: 'UNKNOWN' }))).toBe(
|
||||
'unknown',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unraid detection branch arms', () => {
|
||||
it('detects Unraid disks via storageState + storageRole without the unraid-array group', () => {
|
||||
const disk = makeDiskData({
|
||||
storageRole: 'parity',
|
||||
storageState: 'online',
|
||||
storageGroup: 'other-group',
|
||||
});
|
||||
expect(isUnraidPhysicalDisk(disk)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects disks whose role is not a known Unraid role even with a storage state', () => {
|
||||
const disk = makeDiskData({
|
||||
storageRole: 'spare',
|
||||
storageState: 'online',
|
||||
storageGroup: 'other-group',
|
||||
});
|
||||
expect(isUnraidPhysicalDisk(disk)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects disks with no storageState and a non-unraid group', () => {
|
||||
const disk = makeDiskData({
|
||||
storageRole: 'data',
|
||||
storageState: '',
|
||||
storageGroup: 'other-group',
|
||||
});
|
||||
expect(isUnraidPhysicalDisk(disk)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false from the fault-signal helper for non-Unraid disks', () => {
|
||||
expect(
|
||||
hasUnraidPhysicalDiskFaultSignal(makeDiskData({ health: 'FAILED', storageRole: 'spare' })),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('flags Unraid disks via bad health states even without errorCount', () => {
|
||||
const disk = makeDiskData({
|
||||
health: 'FAULTED',
|
||||
storageRole: 'data',
|
||||
storageGroup: 'unraid-array',
|
||||
storageState: 'disabled',
|
||||
});
|
||||
expect(hasUnraidPhysicalDiskFaultSignal(disk)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPhysicalDiskSmartWarning attribute arms', () => {
|
||||
it('detects warnings from reallocated sectors', () => {
|
||||
expect(
|
||||
hasPhysicalDiskSmartWarning({
|
||||
...makeDiskData(),
|
||||
smartAttributes: { reallocatedSectors: 1 },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('detects warnings from media errors', () => {
|
||||
expect(
|
||||
hasPhysicalDiskSmartWarning({
|
||||
...makeDiskData(),
|
||||
smartAttributes: { mediaErrors: 4 },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when attrs are present but all counters are zero', () => {
|
||||
expect(
|
||||
hasPhysicalDiskSmartWarning({
|
||||
...makeDiskData(),
|
||||
smartAttributes: { reallocatedSectors: 0, pendingSectors: 0, mediaErrors: 0 },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhysicalDiskHealthStatus remaining arms', () => {
|
||||
it('returns Healthy for PASSED health on a non-Unraid disk', () => {
|
||||
const disk = makeDiskData({ health: 'PASSED', type: 'ssd', wearout: 90 });
|
||||
expect(getPhysicalDiskHealthStatus(disk)).toMatchObject({
|
||||
label: 'Healthy',
|
||||
summary: 'No active disk-health issues.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns Healthy for GOOD health', () => {
|
||||
const disk = makeDiskData({ health: 'GOOD', type: 'hdd', wearout: 90 });
|
||||
expect(getPhysicalDiskHealthStatus(disk).label).toBe('Healthy');
|
||||
});
|
||||
|
||||
it('returns Unknown for an Unraid disk that is not in the online state', () => {
|
||||
const disk = makeDiskData({
|
||||
health: 'UNKNOWN',
|
||||
storageRole: 'data',
|
||||
storageGroup: 'unraid-array',
|
||||
storageState: 'standby',
|
||||
});
|
||||
expect(getPhysicalDiskHealthStatus(disk).label).toBe('Unknown');
|
||||
});
|
||||
|
||||
it('uses "SMART counters indicate elevated risk." when low life is not the cause', () => {
|
||||
const disk = makeDiskData({
|
||||
health: 'PASSED',
|
||||
type: 'hdd',
|
||||
wearout: 60,
|
||||
smartAttributes: { pendingSectors: 5 },
|
||||
});
|
||||
expect(getPhysicalDiskHealthStatus(disk).summary).toBe(
|
||||
'SMART counters indicate elevated risk.',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses "SSD life is running low." when wearout is below 10', () => {
|
||||
const disk = makeDiskData({
|
||||
health: 'PASSED',
|
||||
type: 'ssd',
|
||||
wearout: 5,
|
||||
});
|
||||
expect(getPhysicalDiskHealthStatus(disk).summary).toBe('SSD life is running low.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhysicalDiskNormalizedHealth coverage', () => {
|
||||
it('classifies an offline resource as offline', () => {
|
||||
const disk = makeDiskData({ health: 'PASSED', type: 'ssd', wearout: 90 });
|
||||
const resource = { status: 'offline' } as Resource;
|
||||
expect(getPhysicalDiskNormalizedHealth(resource, disk)).toBe('offline');
|
||||
});
|
||||
|
||||
it('classifies a failed disk as critical', () => {
|
||||
const disk = makeDiskData({ health: 'FAILED', riskLevel: 'critical' });
|
||||
expect(getPhysicalDiskNormalizedHealth({} as Resource, disk)).toBe('critical');
|
||||
});
|
||||
|
||||
it('classifies a SMART-warning disk as warning', () => {
|
||||
const disk = makeDiskData({
|
||||
health: 'PASSED',
|
||||
type: 'hdd',
|
||||
wearout: 60,
|
||||
smartAttributes: { pendingSectors: 2 },
|
||||
});
|
||||
expect(getPhysicalDiskNormalizedHealth({} as Resource, disk)).toBe('warning');
|
||||
});
|
||||
|
||||
it('classifies a healthy disk as healthy', () => {
|
||||
const disk = makeDiskData({ health: 'PASSED', type: 'ssd', wearout: 90 });
|
||||
expect(getPhysicalDiskNormalizedHealth({} as Resource, disk)).toBe('healthy');
|
||||
});
|
||||
|
||||
it('classifies an UNKNOWN-health non-Unraid disk as unknown', () => {
|
||||
const disk = makeDiskData({ health: 'UNKNOWN' });
|
||||
expect(getPhysicalDiskNormalizedHealth({} as Resource, disk)).toBe('unknown');
|
||||
});
|
||||
|
||||
it('treats an empty/undefined health string as Unknown label', () => {
|
||||
const empty = makeDiskData({ health: '' as unknown as undefined });
|
||||
expect(getPhysicalDiskHealthStatus(empty).label).toBe('Unknown');
|
||||
const undefinedHealth = makeDiskData({
|
||||
health: undefined as unknown as string,
|
||||
});
|
||||
expect(getPhysicalDiskHealthStatus(undefinedHealth).label).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchesPhysicalDiskHealthFilter', () => {
|
||||
it('returns true for the all filter regardless of health', () => {
|
||||
expect(matchesPhysicalDiskHealthFilter('unknown', 'all')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for the attention filter against offline', () => {
|
||||
expect(matchesPhysicalDiskHealthFilter('offline', 'attention')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for the attention filter against healthy', () => {
|
||||
expect(matchesPhysicalDiskHealthFilter('healthy', 'attention')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for an exact match against the warning filter', () => {
|
||||
expect(matchesPhysicalDiskHealthFilter('warning', 'warning')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for a non-match against the critical filter', () => {
|
||||
expect(matchesPhysicalDiskHealthFilter('warning', 'critical')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhysicalDiskHealthSummary empty arm', () => {
|
||||
it('returns an empty string when the summary is whitespace', () => {
|
||||
expect(
|
||||
getPhysicalDiskHealthSummary({
|
||||
label: 'Healthy',
|
||||
summary: ' ',
|
||||
tone: 'text-base-content',
|
||||
}),
|
||||
).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhysicalDiskRoleLabel ssd/hdd arms', () => {
|
||||
it('returns SSD for type ssd', () => {
|
||||
expect(getPhysicalDiskRoleLabel(makeDiskData({ type: 'ssd' }))).toBe('SSD');
|
||||
});
|
||||
|
||||
it('returns HDD for type hdd', () => {
|
||||
expect(getPhysicalDiskRoleLabel(makeDiskData({ type: 'hdd' }))).toBe('HDD');
|
||||
});
|
||||
|
||||
it('returns a Titleized disk label for other types', () => {
|
||||
expect(getPhysicalDiskRoleLabel(makeDiskData({ type: 'usb-c' }))).toBe('Usb C disk');
|
||||
});
|
||||
|
||||
it('returns empty string when no role or type is set', () => {
|
||||
expect(getPhysicalDiskRoleLabel(makeDiskData())).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhysicalDiskLifeTextClass muted placeholder arm', () => {
|
||||
it('returns the muted placeholder class when wearout is 0', () => {
|
||||
expect(getPhysicalDiskLifeTextClass(makeDiskData({ wearout: 0 }))).toBe(
|
||||
PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the muted placeholder class when wearout is not a number', () => {
|
||||
expect(
|
||||
getPhysicalDiskLifeTextClass(makeDiskData({ wearout: 'nan' as unknown as number })),
|
||||
).toBe(PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchesPhysicalDiskSearch empty freeTerms arm', () => {
|
||||
it('returns true when only node terms match and there are no free terms', () => {
|
||||
const disk = makeDiskData({ node: 'tower' });
|
||||
const resource = { parentName: 'tower' } as Resource;
|
||||
expect(matchesPhysicalDiskSearch(resource, disk, 'node:tower')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when the search term is empty', () => {
|
||||
const disk = makeDiskData({ node: 'tower' });
|
||||
const resource = { parentName: 'tower' } as Resource;
|
||||
expect(matchesPhysicalDiskSearch(resource, disk, '')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('comparePhysicalDiskPresentation tie-breaker arms', () => {
|
||||
it('falls back to node localeCompare when priorities match', () => {
|
||||
const aDisk = makeDiskData({ node: 'alpha', devPath: '/dev/sda' });
|
||||
const bDisk = makeDiskData({ node: 'beta', devPath: '/dev/sda' });
|
||||
expect(
|
||||
comparePhysicalDiskPresentation({} as Resource, aDisk, {} as Resource, bDisk),
|
||||
).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('falls back to devPath localeCompare when priority and node match', () => {
|
||||
const aDisk = makeDiskData({ node: 'tower', devPath: '/dev/sda' });
|
||||
const bDisk = makeDiskData({ node: 'tower', devPath: '/dev/sdb' });
|
||||
expect(
|
||||
comparePhysicalDiskPresentation({} as Resource, aDisk, {} as Resource, bDisk),
|
||||
).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('falls back to resource.name when devPath is empty', () => {
|
||||
const aDisk = makeDiskData({ node: 'tower', devPath: '' });
|
||||
const bDisk = makeDiskData({ node: 'tower', devPath: '' });
|
||||
expect(
|
||||
comparePhysicalDiskPresentation(
|
||||
{ name: 'aaa' } as Resource,
|
||||
aDisk,
|
||||
{ name: 'bbb' } as Resource,
|
||||
bDisk,
|
||||
),
|
||||
).toBeLessThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildPhysicalDiskPresentationDataMap empty input arm', () => {
|
||||
it('returns an empty Map when disks is null', () => {
|
||||
expect(buildPhysicalDiskPresentationDataMap(null as unknown as Resource[]).size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterAndSortPhysicalDisks null/selectedNode arms', () => {
|
||||
it('returns an empty array when disks is null', () => {
|
||||
expect(
|
||||
filterAndSortPhysicalDisks(null as unknown as Resource[], {
|
||||
selectedNode: null,
|
||||
searchTerm: '',
|
||||
getDiskData: () => makeDiskData(),
|
||||
matchesNode: () => true,
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters by selected node via the provided matchesNode callback', () => {
|
||||
const inNode = {
|
||||
id: 'a',
|
||||
name: 'a',
|
||||
physicalDisk: { devPath: '/dev/sda', health: 'PASSED' },
|
||||
} as unknown as Resource;
|
||||
const outNode = {
|
||||
id: 'b',
|
||||
name: 'b',
|
||||
physicalDisk: { devPath: '/dev/sdb', health: 'PASSED' },
|
||||
} as unknown as Resource;
|
||||
const result = filterAndSortPhysicalDisks([inNode, outNode], {
|
||||
selectedNode: { id: 'node-1', name: 'tower' } as Resource,
|
||||
searchTerm: '',
|
||||
getDiskData: (disk) => extractPhysicalDiskPresentationData(disk),
|
||||
matchesNode: (disk, node) => disk.id === 'a' && node.id === 'node-1',
|
||||
});
|
||||
expect(result.map((d) => d.id)).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('exposes the proxmox instance from the selected node to the matchesNode callback', () => {
|
||||
const inNode = {
|
||||
id: 'a',
|
||||
name: 'a',
|
||||
physicalDisk: { devPath: '/dev/sda', health: 'PASSED' },
|
||||
} as unknown as Resource;
|
||||
const result = filterAndSortPhysicalDisks([inNode], {
|
||||
selectedNode: {
|
||||
id: 'node-1',
|
||||
name: 'tower',
|
||||
platformData: { proxmox: { instance: 'pve1' } },
|
||||
} as unknown as Resource,
|
||||
searchTerm: '',
|
||||
getDiskData: (disk) => extractPhysicalDiskPresentationData(disk),
|
||||
matchesNode: (_disk, node) => node.instance === 'pve1',
|
||||
});
|
||||
expect(result.map((d) => d.id)).toEqual(['a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildPhysicalDisk*FilterOptions null input arm', () => {
|
||||
it('returns only the "all" option when disks is null', () => {
|
||||
expect(buildPhysicalDiskRoleFilterOptions(null as unknown as Resource[])).toHaveLength(1);
|
||||
expect(buildPhysicalDiskGroupFilterOptions(null as unknown as Resource[])).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter state "all" arms', () => {
|
||||
it('skips source/health/role/group checks when filters are at their defaults', () => {
|
||||
const resource = {
|
||||
id: 'd1',
|
||||
name: 'd1',
|
||||
physicalDisk: { devPath: '/dev/sda', health: 'PASSED', diskType: 'ssd' },
|
||||
} as unknown as Resource;
|
||||
const resource2 = {
|
||||
id: 'd2',
|
||||
name: 'd2',
|
||||
physicalDisk: { devPath: '/dev/sdb', health: 'PASSED', diskType: 'hdd' },
|
||||
} as unknown as Resource;
|
||||
const result = filterAndSortPhysicalDisks([resource, resource2], {
|
||||
selectedNode: null,
|
||||
sourceFilter: 'all',
|
||||
healthFilter: 'all',
|
||||
roleFilter: 'all',
|
||||
groupFilter: 'all',
|
||||
searchTerm: '',
|
||||
getDiskData: (d) => extractPhysicalDiskPresentationData(d),
|
||||
matchesNode: () => true,
|
||||
});
|
||||
expect(result.map((d) => d.id)).toEqual(['d1', 'd2']);
|
||||
});
|
||||
|
||||
it('rejects disks whose computed role filter value differs from the selected role', () => {
|
||||
const resource = {
|
||||
id: 'd1',
|
||||
name: 'd1',
|
||||
physicalDisk: { devPath: '/dev/sda', health: 'PASSED', diskType: 'ssd' },
|
||||
} as unknown as Resource;
|
||||
const result = filterAndSortPhysicalDisks([resource], {
|
||||
selectedNode: null,
|
||||
roleFilter: 'parity',
|
||||
searchTerm: '',
|
||||
getDiskData: (d) => extractPhysicalDiskPresentationData(d),
|
||||
matchesNode: () => true,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects disks whose computed group filter value differs from the selected group', () => {
|
||||
const resource = {
|
||||
id: 'd1',
|
||||
name: 'd1',
|
||||
physicalDisk: {
|
||||
devPath: '/dev/sda',
|
||||
health: 'PASSED',
|
||||
diskType: 'ssd',
|
||||
storageGroup: 'tank',
|
||||
},
|
||||
} as unknown as Resource;
|
||||
const result = filterAndSortPhysicalDisks([resource], {
|
||||
selectedNode: null,
|
||||
groupFilter: 'other-pool',
|
||||
searchTerm: '',
|
||||
getDiskData: (d) => extractPhysicalDiskPresentationData(d),
|
||||
matchesNode: () => true,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,164 @@
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
)
|
||||
|
||||
// This file adds branch-coverage tests for findLikelyHostPeer in
|
||||
// agent_fleet_doctor.go. The function iterates the supplied hosts, SKIPS any
|
||||
// host whose ID == subject.id (skip-self takes precedence over any identity
|
||||
// match), and returns the first host for which sameAgentIdentity is true;
|
||||
// otherwise it returns the zero models.Host and false.
|
||||
//
|
||||
// Because findLikelyHostPeer forwards host.ID as both the `id` and `agentID`
|
||||
// arguments to sameAgentIdentity, an "agent id" match happens when
|
||||
// subject.agentID == host.ID. Each match case below isolates exactly one
|
||||
// identity signal so the branch of sameAgentIdentity being exercised is
|
||||
// unambiguous. Tests are selected by `-run 'HostPeer|FindLikelyHostPeer|Branchcov0720pm'`.
|
||||
|
||||
func TestFindLikelyHostPeerBranchcov0720pm(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// A host that is unrelated to the subject on every identity axis; useful as
|
||||
// a non-matching filler when building multi-host slices.
|
||||
unrelated := func(id string) models.Host {
|
||||
return models.Host{ID: id, Hostname: "totally-unrelated", TokenID: "tok-unrelated"}
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
subject agentFleetSubject
|
||||
hosts []models.Host
|
||||
wantOK bool
|
||||
wantHostID string
|
||||
wantHostHostname string
|
||||
}{
|
||||
// Branch: empty/nil hosts slice -> loop body never runs -> (zero, false).
|
||||
{
|
||||
name: "empty hosts slice returns zero and false",
|
||||
subject: agentFleetSubject{id: "self", agentID: "a1", tokenID: "t1", hostname: "h"},
|
||||
hosts: nil,
|
||||
wantOK: false,
|
||||
},
|
||||
|
||||
// Branch: skip-self (host.ID == subject.id) taken, then loop ends ->
|
||||
// (zero, false). The subject carries identity signals but the only host
|
||||
// is self, so it is skipped before sameAgentIdentity is ever consulted.
|
||||
{
|
||||
name: "only host is self id skipped returns zero and false",
|
||||
subject: agentFleetSubject{id: "self", agentID: "self", tokenID: "t1", hostname: "h"},
|
||||
hosts: []models.Host{{ID: "self", Hostname: "h", TokenID: "t1"}},
|
||||
wantOK: false,
|
||||
},
|
||||
|
||||
// Branch: match via the agentID signal. subject.agentID == host.ID
|
||||
// (both id and agentID args are host.ID). subject.tokenID/hostname are
|
||||
// empty so the match is unambiguously the agentID branch.
|
||||
{
|
||||
name: "match by agent id subject agentID equals host ID",
|
||||
subject: agentFleetSubject{id: "self", agentID: "agent-x"},
|
||||
hosts: []models.Host{{ID: "agent-x", Hostname: "unrelated", TokenID: ""}},
|
||||
wantOK: true,
|
||||
wantHostID: "agent-x",
|
||||
wantHostHostname: "unrelated",
|
||||
},
|
||||
|
||||
// Branch: match via the tokenID signal. subject.agentID is empty (so the
|
||||
// agentID branch is skipped) and subject.hostname is empty (so the
|
||||
// hostname branch is skipped); only the token branch can fire.
|
||||
{
|
||||
name: "match by token id",
|
||||
subject: agentFleetSubject{id: "self", tokenID: "tok-1"},
|
||||
hosts: []models.Host{{ID: "host-a", Hostname: "unrelated", TokenID: "tok-1"}},
|
||||
wantOK: true,
|
||||
wantHostID: "host-a",
|
||||
wantHostHostname: "unrelated",
|
||||
},
|
||||
|
||||
// Branch: match via the hostname signal, case-insensitively
|
||||
// (subject.hostname "Node1" vs host.Hostname "node1"). subject.agentID
|
||||
// and subject.tokenID are empty and host.TokenID is empty, so only the
|
||||
// hostname branch of sameAgentIdentity can match.
|
||||
{
|
||||
name: "match by hostname case insensitive",
|
||||
subject: agentFleetSubject{id: "self", hostname: "Node1"},
|
||||
hosts: []models.Host{{ID: "host-b", Hostname: "node1", TokenID: ""}},
|
||||
wantOK: true,
|
||||
wantHostID: "host-b",
|
||||
wantHostHostname: "node1",
|
||||
},
|
||||
|
||||
// Branch: skip-self takes precedence over match. The single host has
|
||||
// ID == subject.id AND would match on ALL THREE identity signals
|
||||
// (agentID, tokenID, hostname) — yet it is still skipped, yielding
|
||||
// (zero, false). This pins the ordering: the self check runs before
|
||||
// sameAgentIdentity is called.
|
||||
{
|
||||
name: "self host that would match is still skipped",
|
||||
subject: agentFleetSubject{id: "self-id", agentID: "self-id", tokenID: "tok-1", hostname: "node1"},
|
||||
hosts: []models.Host{{ID: "self-id", Hostname: "node1", TokenID: "tok-1"}},
|
||||
wantOK: false,
|
||||
},
|
||||
|
||||
// Branch: first non-self match wins. host[0] neither matches nor is
|
||||
// self (exercises a sameAgentIdentity==false iteration); host[1] is the
|
||||
// first match and must be returned; host[2] would also match but must
|
||||
// never be reached.
|
||||
{
|
||||
name: "first non-self match wins among multiple hosts",
|
||||
subject: agentFleetSubject{id: "self", hostname: "match-host"},
|
||||
hosts: []models.Host{
|
||||
unrelated("nope-1"),
|
||||
{ID: "winner", Hostname: "match-host", TokenID: ""},
|
||||
{ID: "also-match", Hostname: "match-host", TokenID: ""},
|
||||
},
|
||||
wantOK: true,
|
||||
wantHostID: "winner",
|
||||
wantHostHostname: "match-host",
|
||||
},
|
||||
|
||||
// Branch: no host matches. The single candidate is not self but
|
||||
// sameAgentIdentity returns false on every signal -> loop completes ->
|
||||
// (zero, false).
|
||||
{
|
||||
name: "no host matches returns zero and false",
|
||||
subject: agentFleetSubject{id: "self", agentID: "a1", tokenID: "t1", hostname: "real"},
|
||||
hosts: []models.Host{{ID: "other", Hostname: "different", TokenID: "t2"}},
|
||||
wantOK: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, ok := findLikelyHostPeer(tc.subject, tc.hosts)
|
||||
|
||||
if ok != tc.wantOK {
|
||||
t.Fatalf("findLikelyHostPeer ok = %v, want %v (subject=%+v, hosts=%#v)",
|
||||
ok, tc.wantOK, tc.subject, tc.hosts)
|
||||
}
|
||||
|
||||
if tc.wantOK {
|
||||
// Assert the EXACT host was returned so ordering/identity is
|
||||
// verified, not just "some host".
|
||||
if got.ID != tc.wantHostID {
|
||||
t.Fatalf("returned host ID = %q, want %q", got.ID, tc.wantHostID)
|
||||
}
|
||||
if got.Hostname != tc.wantHostHostname {
|
||||
t.Fatalf("returned host Hostname = %q, want %q", got.Hostname, tc.wantHostHostname)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Non-match path must return the zero-value models.Host exactly.
|
||||
if !reflect.DeepEqual(got, models.Host{}) {
|
||||
t.Fatalf("findLikelyHostPeer returned non-zero host on no-match: %#v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package unifiedresources
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/diskinventory"
|
||||
)
|
||||
|
||||
// TestPhysicalDiskViewBranchcov0720pm covers the five PhysicalDiskView
|
||||
// accessors that were previously 0% covered:
|
||||
// - Controller() string
|
||||
// - Target() string
|
||||
// - StorageGroup() string
|
||||
// - IO() *PhysicalDiskIOMeta
|
||||
// - Collection() *diskinventory.CollectionStatus
|
||||
//
|
||||
// Each accessor exercises three arms where they exist:
|
||||
//
|
||||
// (a) nil-receiver and nil-backing (v.r == nil, then v.r != nil but
|
||||
// v.r.PhysicalDisk == nil) — must be nil/""-safe, no panic;
|
||||
// (b) populated backing but the nested field empty/nil (whitespace-only
|
||||
// strings trim to ""; IO()/Collection() inner pointers nil → nil);
|
||||
// (c) fully populated — assert the exact returned value, and for the
|
||||
// pointer-returning accessors assert the clone is field-equal and
|
||||
// mutation-independent from the live source.
|
||||
func TestPhysicalDiskViewBranchcov0720pm(t *testing.T) {
|
||||
t.Run("Controller", func(t *testing.T) {
|
||||
// (a) nil receiver.
|
||||
var zero PhysicalDiskView
|
||||
if got := zero.Controller(); got != "" {
|
||||
t.Fatalf("nil receiver: expected %q, got %q", "", got)
|
||||
}
|
||||
// (a) populated Resource, nil PhysicalDisk backing.
|
||||
r := &Resource{ID: "pd-ctrl-1", Type: ResourceTypePhysicalDisk}
|
||||
if got := NewPhysicalDiskView(r).Controller(); got != "" {
|
||||
t.Fatalf("nil PhysicalDisk: expected %q, got %q", "", got)
|
||||
}
|
||||
// (b) PhysicalDisk present, Controller whitespace-only → trims to "".
|
||||
r.PhysicalDisk = &PhysicalDiskMeta{Controller: " "}
|
||||
if got := NewPhysicalDiskView(r).Controller(); got != "" {
|
||||
t.Fatalf("whitespace Controller: expected trimmed %q, got %q", "", got)
|
||||
}
|
||||
// (c) Fully populated → trimmed exact value.
|
||||
r.PhysicalDisk.Controller = " mega-raid-0 "
|
||||
if got, want := NewPhysicalDiskView(r).Controller(), "mega-raid-0"; got != want {
|
||||
t.Fatalf("expected Controller %q, got %q", want, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Target", func(t *testing.T) {
|
||||
// (a) nil receiver.
|
||||
var zero PhysicalDiskView
|
||||
if got := zero.Target(); got != "" {
|
||||
t.Fatalf("nil receiver: expected %q, got %q", "", got)
|
||||
}
|
||||
// (a) populated Resource, nil PhysicalDisk backing.
|
||||
r := &Resource{ID: "pd-tgt-1", Type: ResourceTypePhysicalDisk}
|
||||
if got := NewPhysicalDiskView(r).Target(); got != "" {
|
||||
t.Fatalf("nil PhysicalDisk: expected %q, got %q", "", got)
|
||||
}
|
||||
// (b) PhysicalDisk present, Target whitespace-only → trims to "".
|
||||
r.PhysicalDisk = &PhysicalDiskMeta{Target: " "}
|
||||
if got := NewPhysicalDiskView(r).Target(); got != "" {
|
||||
t.Fatalf("whitespace Target: expected trimmed %q, got %q", "", got)
|
||||
}
|
||||
// (c) Fully populated → trimmed exact value.
|
||||
r.PhysicalDisk.Target = " 0:0:0:0 "
|
||||
if got, want := NewPhysicalDiskView(r).Target(), "0:0:0:0"; got != want {
|
||||
t.Fatalf("expected Target %q, got %q", want, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("StorageGroup", func(t *testing.T) {
|
||||
// (a) nil receiver.
|
||||
var zero PhysicalDiskView
|
||||
if got := zero.StorageGroup(); got != "" {
|
||||
t.Fatalf("nil receiver: expected %q, got %q", "", got)
|
||||
}
|
||||
// (a) populated Resource, nil PhysicalDisk backing.
|
||||
r := &Resource{ID: "pd-sg-1", Type: ResourceTypePhysicalDisk}
|
||||
if got := NewPhysicalDiskView(r).StorageGroup(); got != "" {
|
||||
t.Fatalf("nil PhysicalDisk: expected %q, got %q", "", got)
|
||||
}
|
||||
// (b) PhysicalDisk present, StorageGroup whitespace-only → trims to "".
|
||||
r.PhysicalDisk = &PhysicalDiskMeta{StorageGroup: "\t "}
|
||||
if got := NewPhysicalDiskView(r).StorageGroup(); got != "" {
|
||||
t.Fatalf("whitespace StorageGroup: expected trimmed %q, got %q", "", got)
|
||||
}
|
||||
// (c) Fully populated → trimmed exact value.
|
||||
r.PhysicalDisk.StorageGroup = " sg-fast "
|
||||
if got, want := NewPhysicalDiskView(r).StorageGroup(), "sg-fast"; got != want {
|
||||
t.Fatalf("expected StorageGroup %q, got %q", want, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("IO", func(t *testing.T) {
|
||||
// (a) nil receiver.
|
||||
var zero PhysicalDiskView
|
||||
if got := zero.IO(); got != nil {
|
||||
t.Fatalf("nil receiver: expected nil, got %+v", got)
|
||||
}
|
||||
// (a) populated Resource, nil PhysicalDisk backing.
|
||||
r := &Resource{ID: "pd-io-1", Type: ResourceTypePhysicalDisk}
|
||||
if got := NewPhysicalDiskView(r).IO(); got != nil {
|
||||
t.Fatalf("nil PhysicalDisk: expected nil, got %+v", got)
|
||||
}
|
||||
// (b) PhysicalDisk present, IO pointer nil → accessor returns nil.
|
||||
r.PhysicalDisk = &PhysicalDiskMeta{} // IO is nil
|
||||
if got := NewPhysicalDiskView(r).IO(); got != nil {
|
||||
t.Fatalf("nil IO: expected nil, got %+v", got)
|
||||
}
|
||||
// (c) Fully populated → field-equal clone on a fresh allocation.
|
||||
want := &PhysicalDiskIOMeta{
|
||||
Device: "sda",
|
||||
ReadBytes: 100,
|
||||
WriteBytes: 200,
|
||||
ReadOps: 1,
|
||||
WriteOps: 2,
|
||||
ReadTimeMs: 3,
|
||||
WriteTimeMs: 4,
|
||||
IOTimeMs: 5,
|
||||
}
|
||||
r.PhysicalDisk.IO = want
|
||||
got := NewPhysicalDiskView(r).IO()
|
||||
if got == nil {
|
||||
t.Fatal("populated: expected non-nil")
|
||||
}
|
||||
if !reflect.DeepEqual(*got, *want) {
|
||||
t.Fatalf("expected field-equal clone, got %+v want %+v", *got, *want)
|
||||
}
|
||||
// Returned pointer must be a fresh allocation, not the live source.
|
||||
if got == r.PhysicalDisk.IO {
|
||||
t.Fatal("expected IO() to return a fresh pointer, not the source pointer")
|
||||
}
|
||||
// Mutating the clone must not leak back to the source.
|
||||
got.ReadBytes = 9999
|
||||
got.Device = "mutated"
|
||||
if r.PhysicalDisk.IO.ReadBytes != 100 || r.PhysicalDisk.IO.Device != "sda" {
|
||||
t.Fatalf("mutation leaked to source: got %+v", *r.PhysicalDisk.IO)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Collection", func(t *testing.T) {
|
||||
// (a) nil receiver.
|
||||
var zero PhysicalDiskView
|
||||
if got := zero.Collection(); got != nil {
|
||||
t.Fatalf("nil receiver: expected nil, got %+v", got)
|
||||
}
|
||||
// (a) populated Resource, nil PhysicalDisk backing.
|
||||
r := &Resource{ID: "pd-coll-1", Type: ResourceTypePhysicalDisk}
|
||||
if got := NewPhysicalDiskView(r).Collection(); got != nil {
|
||||
t.Fatalf("nil PhysicalDisk: expected nil, got %+v", got)
|
||||
}
|
||||
// (b) PhysicalDisk present, Collection pointer nil → accessor returns nil
|
||||
// (diskinventory.CloneStatus(nil) returns nil).
|
||||
r.PhysicalDisk = &PhysicalDiskMeta{} // Collection is nil
|
||||
if got := NewPhysicalDiskView(r).Collection(); got != nil {
|
||||
t.Fatalf("nil Collection: expected nil, got %+v", got)
|
||||
}
|
||||
// (c) Fully populated → field-equal clone on a fresh allocation.
|
||||
want := &diskinventory.CollectionStatus{
|
||||
Serial: diskinventory.Available("smartctl"),
|
||||
Temperature: diskinventory.Unavailable("smartctl", "no sensor"),
|
||||
IO: diskinventory.FieldStatus{State: diskinventory.FieldAvailable, Source: "sysfs"},
|
||||
Controller: diskinventory.Unsupported("nvme", "n/a"),
|
||||
Pool: diskinventory.Missing("zfs", "not imported"),
|
||||
}
|
||||
r.PhysicalDisk.Collection = want
|
||||
got := NewPhysicalDiskView(r).Collection()
|
||||
if got == nil {
|
||||
t.Fatal("populated: expected non-nil")
|
||||
}
|
||||
if !reflect.DeepEqual(*got, *want) {
|
||||
t.Fatalf("expected field-equal clone, got %+v want %+v", *got, *want)
|
||||
}
|
||||
// Returned pointer must be a fresh allocation, not the live source.
|
||||
if got == r.PhysicalDisk.Collection {
|
||||
t.Fatal("expected Collection() to return a fresh pointer, not the source pointer")
|
||||
}
|
||||
// Mutating the clone (scalar via value field on the cloned struct) must
|
||||
// not leak back to the source. CollectionStatus is a flat struct of
|
||||
// FieldStatus values, so mutating a nested FieldStatus proves
|
||||
// independence.
|
||||
got.Serial = diskinventory.FieldStatus{State: diskinventory.FieldMissing, Source: "mutated"}
|
||||
got.IO.Source = "leaked"
|
||||
if r.PhysicalDisk.Collection.Serial.State != diskinventory.FieldAvailable ||
|
||||
r.PhysicalDisk.Collection.Serial.Source != "smartctl" {
|
||||
t.Fatalf("Serial mutation leaked to source: %+v", r.PhysicalDisk.Collection.Serial)
|
||||
}
|
||||
if r.PhysicalDisk.Collection.IO.Source != "sysfs" {
|
||||
t.Fatalf("IO.Source mutation leaked to source: %q", r.PhysicalDisk.Collection.IO.Source)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package diskinventory
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCloneStatusBranchcov0720pm exercises CloneStatus for nil-safety,
|
||||
// deep value equality on fully-populated and zero-value inputs, and
|
||||
// bidirectional deep-copy independence (mutating the clone must not affect
|
||||
// the original and vice versa).
|
||||
func TestCloneStatusBranchcov0720pm(t *testing.T) {
|
||||
t.Run("nil input returns nil", func(t *testing.T) {
|
||||
got := CloneStatus(nil)
|
||||
if got != nil {
|
||||
t.Fatalf("CloneStatus(nil) = %v, want nil", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero value clones equal", func(t *testing.T) {
|
||||
orig := &CollectionStatus{}
|
||||
clone := CloneStatus(orig)
|
||||
if clone == nil {
|
||||
t.Fatalf("CloneStatus returned nil for non-nil zero-value input")
|
||||
}
|
||||
if clone == orig {
|
||||
t.Fatalf("CloneStatus returned identical pointer; want distinct value-equal instance")
|
||||
}
|
||||
if !reflect.DeepEqual(*clone, *orig) {
|
||||
t.Fatalf("zero-value clone not equal to original: clone=%+v orig=%+v", *clone, *orig)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fully populated clones deeply equal", func(t *testing.T) {
|
||||
orig := fullyPopulatedStatus()
|
||||
clone := CloneStatus(orig)
|
||||
if clone == orig {
|
||||
t.Fatalf("CloneStatus returned identical pointer; want distinct value-equal instance")
|
||||
}
|
||||
if !reflect.DeepEqual(*clone, *orig) {
|
||||
t.Fatalf("clone not deeply equal to original: clone=%+v orig=%+v", *clone, *orig)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("clone mutations leave original unchanged", func(t *testing.T) {
|
||||
orig := fullyPopulatedStatus()
|
||||
snapshot := *orig
|
||||
clone := CloneStatus(orig)
|
||||
|
||||
// Mutate every field on the clone. CollectionStatus contains only
|
||||
// value-typed FieldStatus fields (no slices/maps/pointers), so this
|
||||
// exercises each value field's independence directly.
|
||||
clone.Serial = Unavailable("clone-mut", "serial")
|
||||
clone.Temperature = Available("clone-mut")
|
||||
clone.IO = Unsupported("clone-mut", "io")
|
||||
clone.Controller = Missing("clone-mut", "controller")
|
||||
clone.Pool = Unavailable("clone-mut", "pool")
|
||||
|
||||
if !reflect.DeepEqual(*orig, snapshot) {
|
||||
t.Fatalf("original changed after mutating clone: orig=%+v snapshot=%+v", *orig, snapshot)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("original mutations leave clone unchanged", func(t *testing.T) {
|
||||
orig := fullyPopulatedStatus()
|
||||
clone := CloneStatus(orig)
|
||||
cloneSnapshot := *clone
|
||||
|
||||
orig.Serial = Unavailable("orig-mut", "serial")
|
||||
orig.Temperature = Available("orig-mut")
|
||||
orig.IO = Unsupported("orig-mut", "io")
|
||||
orig.Controller = Missing("orig-mut", "controller")
|
||||
orig.Pool = Unavailable("orig-mut", "pool")
|
||||
|
||||
if !reflect.DeepEqual(*clone, cloneSnapshot) {
|
||||
t.Fatalf("clone changed after mutating original: clone=%+v snapshot=%+v", *clone, cloneSnapshot)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// fullyPopulatedStatus returns a CollectionStatus with every field set to a
|
||||
// distinct, non-zero FieldStatus so equality and independence checks are
|
||||
// sensitive to every field on the struct.
|
||||
func fullyPopulatedStatus() *CollectionStatus {
|
||||
return &CollectionStatus{
|
||||
Serial: Available("smartctl"),
|
||||
Temperature: Unavailable("smartctl", "deadline exceeded"),
|
||||
IO: Missing("kernel", "counter absent"),
|
||||
Controller: Unsupported("provider", "not exposed"),
|
||||
Pool: Available("provider"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user