test(frontend): branch-coverage tests for 28 pure model/presentation modules

This commit is contained in:
rcourtman
2026-07-12 01:04:46 +01:00
parent d8066ace0b
commit 1294695d6a
28 changed files with 13518 additions and 0 deletions
@@ -0,0 +1,472 @@
import { describe, expect, it } from 'vitest';
import {
appendVisibleTextBeforeAssistantOutputArtifacts,
createAssistantOutputArtifactStreamState,
flushPendingAssistantOutputText,
stripAssistantOutputArtifacts,
} from '../assistantOutputHygiene';
// Branch-coverage companion to assistantOutputHygiene.test.ts. Every target
// function below is module-private, so each branch is driven through the public
// entry points (stripAssistantOutputArtifacts / appendVisibleTextBefore… /
// flushPendingAssistantOutputText) and asserted against concrete shapes.
const freshState = () => createAssistantOutputArtifactStreamState();
// ---------------------------------------------------------------------------
// formatInternalIdentifier — reached via normalizeAssistantVisibleInternalIdentifiers
// when an identifier is NOT in VISIBLE_INTERNAL_TOOL_IDENTIFIER_LABELS.
// ---------------------------------------------------------------------------
describe('formatInternalIdentifier branch coverage', () => {
it('strips a pulse_ prefix and turns underscores into spaces', () => {
// pulse_get_nodes is not in the labels map -> formatInternalIdentifier.
expect(stripAssistantOutputArtifacts('Use pulse_get_nodes now.')).toEqual({
text: 'Use get nodes now.',
stripped: false,
});
});
it('strips a patrol_ prefix leaving a single word with no underscores', () => {
expect(stripAssistantOutputArtifacts('Run patrol_scan.')).toEqual({
text: 'Run scan.',
stripped: false,
});
});
it('strips a patrol_ prefix and keeps internal underscores as spaces', () => {
expect(stripAssistantOutputArtifacts('Start patrol_network_scan.')).toEqual({
text: 'Start network scan.',
stripped: false,
});
});
});
// ---------------------------------------------------------------------------
// normalizeAssistantVisibleInternalIdentifiers
// ---------------------------------------------------------------------------
describe('normalizeAssistantVisibleInternalIdentifiers branch coverage', () => {
it('returns empty input untouched via the early guard', () => {
// stripAssistantOutputArtifacts('') -> idx<0 -> normalize('') -> ''.
expect(stripAssistantOutputArtifacts('')).toEqual({ text: '', stripped: false });
});
it('leaves prose without any internal identifier untouched', () => {
expect(stripAssistantOutputArtifacts('Just regular prose.')).toEqual({
text: 'Just regular prose.',
stripped: false,
});
});
it('substitutes a mapped label (patrol_collect -> collection)', () => {
expect(stripAssistantOutputArtifacts('The patrol_collect ran.')).toEqual({
text: 'The collection ran.',
stripped: false,
});
});
it('consumes surrounding backticks and a trailing label word, handling multiple matches', () => {
// The regex swallows the optional backticks and the trailing
// (tool|command|query|call) word, replacing the whole match with the label.
expect(
stripAssistantOutputArtifacts('`pulse_read` tool and run_command query'),
).toEqual({
text: 'read command and command',
stripped: false,
});
});
});
// ---------------------------------------------------------------------------
// appendVisibleTextBeforeAssistantOutputArtifacts
// ---------------------------------------------------------------------------
describe('appendVisibleTextBeforeAssistantOutputArtifacts branch coverage', () => {
it('short-circuits when both content and pendingText are empty', () => {
expect(appendVisibleTextBeforeAssistantOutputArtifacts(freshState(), '')).toEqual({
text: '',
stripped: false,
});
});
it('emits previousVisibleText + replacementText when prior text is suppressed at an artifact', () => {
const state = freshState();
// First delta establishes non-empty visibleText.
expect(appendVisibleTextBeforeAssistantOutputArtifacts(state, 'Hello.')).toEqual({
text: 'Hello.',
stripped: false,
});
// Second delta is a compacted prelude (0 whitespace, >=16 letters) glued to
// a pulse_read leak. shouldSuppress is true AND previousVisibleText is set.
// The '\n' separator before pulse_read is required: findFunctionToolCallLeak
// only matches a tool name preceded by a non-word character.
expect(
appendVisibleTextBeforeAssistantOutputArtifacts(
state,
'Illcheckthedevicenodesinsidethecontainertoanswerthat\npulse_read(target_host="x")',
),
).toEqual({
text: '',
stripped: true,
previousVisibleText: 'Hello.',
replacementText: '',
});
expect(state.visibleText).toBe('');
expect(state.rawVisibleText).toBe('');
expect(state.pendingText).toBe('');
});
it('returns a non-empty visibleDelta when new safe text grows beyond what was shown', () => {
const state = freshState();
expect(appendVisibleTextBeforeAssistantOutputArtifacts(state, 'Hello.')).toEqual({
text: 'Hello.',
stripped: false,
});
// The artifact appears after additional visible prose, so normalizedSafeText
// is longer than existingVisibleText -> visibleDelta is the new slice.
expect(
appendVisibleTextBeforeAssistantOutputArtifacts(
state,
' World.\npulse_read(target_host="x")',
),
).toEqual({ text: ' World.', stripped: true });
});
it('returns an empty visibleDelta when the re-sliced safe text does not grow', () => {
const state = freshState();
expect(appendVisibleTextBeforeAssistantOutputArtifacts(state, 'Done.')).toEqual({
text: 'Done.',
stripped: false,
});
// safeText re-slices back to 'Done.' which is no longer than what was shown.
expect(
appendVisibleTextBeforeAssistantOutputArtifacts(
state,
'\npulse_read(target_host="x")',
),
).toEqual({ text: '', stripped: true });
});
});
// ---------------------------------------------------------------------------
// flushPendingAssistantOutputText
// ---------------------------------------------------------------------------
describe('flushPendingAssistantOutputText branch coverage', () => {
it('returns an empty string when there is no pending text', () => {
expect(flushPendingAssistantOutputText(freshState())).toBe('');
});
it('releases a held partial-reasoning token (not suppressible) and updates state', () => {
const state = freshState();
// 'Think' is held as a potential reasoning prelude (prefix of "thinking").
expect(appendVisibleTextBeforeAssistantOutputArtifacts(state, 'Think')).toEqual({
text: '',
stripped: false,
});
// On flush it is NOT a complete reasoning heading -> not suppressed.
expect(flushPendingAssistantOutputText(state)).toBe('Think');
expect(state.visibleText).toBe('Think');
expect(state.rawVisibleText).toBe('Think');
expect(state.pendingText).toBe('');
});
it('suppresses a held compacted prelude on flush', () => {
const state = freshState();
expect(
appendVisibleTextBeforeAssistantOutputArtifacts(
state,
'Thisisbadmodelspacingbutitistheactualanswerbecauseitneverturnsintoatoolcall.',
),
).toEqual({ text: '', stripped: false });
expect(flushPendingAssistantOutputText(state)).toBe('');
expect(state.visibleText).toBe('');
});
it('suppresses a held complete reasoning heading on flush', () => {
const state = freshState();
// 'Thinking' alone is a complete heading -> suppressible on flush.
expect(appendVisibleTextBeforeAssistantOutputArtifacts(state, 'Thinking')).toEqual({
text: '',
stripped: false,
});
expect(flushPendingAssistantOutputText(state)).toBe('');
});
});
// ---------------------------------------------------------------------------
// isContentChannelReasoningPrelude
// ---------------------------------------------------------------------------
describe('isContentChannelReasoningPrelude branch coverage', () => {
it('returns false for empty content (reached via an empty visible prefix)', () => {
// visiblePrefix trims to '' -> shouldSuppress false -> normalize('') shown.
expect(stripAssistantOutputArtifacts('pulse_read(target_host="x")')).toEqual({
text: '',
stripped: true,
});
});
it('returns false when no reasoning heading is present', () => {
expect(stripAssistantOutputArtifacts('Just prose here\npulse_read()')).toEqual({
text: 'Just prose here',
stripped: true,
});
});
it('returns true for a bare heading with no body', () => {
expect(stripAssistantOutputArtifacts('Thinking\npulse_read()')).toEqual({
text: '',
stripped: true,
});
});
it('returns true when the body contains a reasoning cue', () => {
// cue "let me" -> internalReasoningCueRe matches -> suppress.
expect(
stripAssistantOutputArtifacts('Thoughts\nlet me count the entries first\npulse_read()'),
).toEqual({ text: '', stripped: true });
});
it('returns true for a cue-less body with >= 8 words', () => {
expect(
stripAssistantOutputArtifacts(
'Thinking\nthe quick brown fox jumps over the lazy dog now\npulse_read()',
),
).toEqual({ text: '', stripped: true });
});
it('returns false for a cue-less body with < 8 words', () => {
expect(stripAssistantOutputArtifacts('Thinking\nshort body here\npulse_read()')).toEqual({
text: 'Thinking\nshort body here',
stripped: true,
});
});
});
// ---------------------------------------------------------------------------
// isPotentialContentChannelReasoningPrelude — reached via the append hold path.
// ---------------------------------------------------------------------------
describe('isPotentialContentChannelReasoningPrelude branch coverage', () => {
it('holds a short prefix of "thinking" (partial-prefix true arm)', () => {
const state = freshState();
expect(appendVisibleTextBeforeAssistantOutputArtifacts(state, 'Think')).toEqual({
text: '',
stripped: false,
});
expect(state.pendingText).toBe('Think');
});
it('holds the full "thinking" token via the heading arm (length is not < 8)', () => {
const state = freshState();
expect(appendVisibleTextBeforeAssistantOutputArtifacts(state, 'Thinking')).toEqual({
text: '',
stripped: false,
});
expect(state.pendingText).toBe('Thinking');
});
it('holds a "reasoning" heading via the heading arm', () => {
const state = freshState();
expect(appendVisibleTextBeforeAssistantOutputArtifacts(state, 'Reasoning')).toEqual({
text: '',
stripped: false,
});
expect(state.pendingText).toBe('Reasoning');
});
it('does not hold a short non-prefix token (partial-prefix false + heading false)', () => {
const state = freshState();
// 'xyz' is < 8 chars but not a prefix of "thinking" and not a heading.
expect(appendVisibleTextBeforeAssistantOutputArtifacts(state, 'xyz')).toEqual({
text: 'xyz',
stripped: false,
});
expect(state.pendingText).toBe('');
});
});
// ---------------------------------------------------------------------------
// isCompactedToolPrelude
// ---------------------------------------------------------------------------
describe('isCompactedToolPrelude branch coverage', () => {
it('returns false for letters < 16 (short prose before a leak)', () => {
expect(stripAssistantOutputArtifacts('Hi.pulse_read()')).toEqual({
text: 'Hi.',
stripped: true,
});
});
it('returns true for >= 16 letters with zero whitespace', () => {
expect(
stripAssistantOutputArtifacts(
'Illcheckthedevicenodesinsidethecontainertoanswerthat\npulse_read()',
),
).toEqual({ text: '', stripped: true });
});
it('returns true for >= 48 letters with exactly one whitespace', () => {
// 43 letters + 1 space + 5 letters = 48 letters, 1 whitespace.
expect(
stripAssistantOutputArtifacts(
'thequickbrownfoxjumpsoverlazydogandrunsfast abcde\npulse_read()',
),
).toEqual({ text: '', stripped: true });
});
it('returns false for 16-47 letters with exactly one whitespace', () => {
// 20 letters + 1 space + 1 letter = 21 letters, 1 whitespace.
expect(
stripAssistantOutputArtifacts('aaaaaaaaaaaaaaaaaaaa b\npulse_read()'),
).toEqual({ text: 'aaaaaaaaaaaaaaaaaaaa b', stripped: true });
});
it('returns false when whitespace >= 2 even with many letters', () => {
expect(
stripAssistantOutputArtifacts('alpha beta gamma delta epsilon zeta\npulse_read()'),
).toEqual({ text: 'alpha beta gamma delta epsilon zeta', stripped: true });
});
});
// ---------------------------------------------------------------------------
// splitTrailingPotentialToolNamePrefix
// ---------------------------------------------------------------------------
describe('splitTrailingPotentialToolNamePrefix branch coverage', () => {
it('holds nothing when the trailing run is not a known tool prefix', () => {
// 'world123' is tool-name characters but not a pulse/patrol prefix.
expect(appendVisibleTextBeforeAssistantOutputArtifacts(freshState(), 'hello world123')).toEqual({
text: 'hello world123',
stripped: false,
});
});
it('keeps a leading backtick attached to a held pulse_ prefix', () => {
const state = freshState();
expect(appendVisibleTextBeforeAssistantOutputArtifacts(state, 'see `pulse_')).toEqual({
text: 'see ',
stripped: false,
});
expect(state.pendingText).toBe('`pulse_');
});
it('holds a known patrol_ partial prefix without a backtick', () => {
const state = freshState();
expect(appendVisibleTextBeforeAssistantOutputArtifacts(state, 'go pat')).toEqual({
text: 'go ',
stripped: false,
});
expect(state.pendingText).toBe('pat');
});
});
// ---------------------------------------------------------------------------
// isKnownAssistantToolNamePrefix
// ---------------------------------------------------------------------------
describe('isKnownAssistantToolNamePrefix branch coverage', () => {
it('matches a complete pulse_ tool name via the pulse_ regex arm', () => {
const state = freshState();
// 'pulse_read' (no parens) is a known prefix -> held, then normalized on flush.
expect(appendVisibleTextBeforeAssistantOutputArtifacts(state, 'use pulse_read')).toEqual({
text: 'use ',
stripped: false,
});
expect(state.pendingText).toBe('pulse_read');
expect(flushPendingAssistantOutputText(state)).toBe('read command');
});
it('matches a complete patrol_ tool name via the patrol_ regex arm', () => {
const state = freshState();
expect(
appendVisibleTextBeforeAssistantOutputArtifacts(state, 'try patrol_remediate'),
).toEqual({
text: 'try ',
stripped: false,
});
expect(state.pendingText).toBe('patrol_remediate');
// patrol_remediate IS in the labels map -> "remediation" on flush.
expect(flushPendingAssistantOutputText(state)).toBe('remediation');
});
});
// ---------------------------------------------------------------------------
// assistantOutputArtifactIndex
// ---------------------------------------------------------------------------
describe('assistantOutputArtifactIndex branch coverage', () => {
it('returns -1 for plain text with no artifacts', () => {
expect(stripAssistantOutputArtifacts('plain text only')).toEqual({
text: 'plain text only',
stripped: false,
});
});
it('detects a minimax:tool_call leak', () => {
// minimaxToolCallLeakRe matches at start of the second line.
expect(stripAssistantOutputArtifacts('Hello\nminimax:tool_call foo')).toEqual({
text: 'Hello',
stripped: true,
});
});
it('keeps the earliest marker when a later-scanned raw marker appears first', () => {
// '<DSML' is scanned before '<tool_call' but appears later in the string.
// record() must lower `first` from the <DSML index down to <tool_call's 2.
expect(stripAssistantOutputArtifacts('Hi<tool_call></tool_call><DSML')).toEqual({
text: 'Hi',
stripped: true,
});
});
it('detects a fenced ```json tool-call leak', () => {
expect(stripAssistantOutputArtifacts('```json\n{"name":"pulse_exec","a":1}\n```')).toEqual({
text: '',
stripped: true,
});
});
});
// ---------------------------------------------------------------------------
// findJSONToolCallLeak
// ---------------------------------------------------------------------------
describe('findJSONToolCallLeak branch coverage', () => {
it('skips a JSON object whose name is not tool-like and finds no other artifact', () => {
// 'helper' fails isAssistantToolLikeName -> loop continues -> returns -1.
expect(stripAssistantOutputArtifacts('{"name":"helper","x":1}')).toEqual({
text: '{"name":"helper","x":1}',
stripped: false,
});
});
it('returns the index of a tool-like JSON leak at the start of the content', () => {
expect(
stripAssistantOutputArtifacts('{"name":"pulse_query","parameters":{"action":"list"}}'),
).toEqual({ text: '', stripped: true });
});
});
// ---------------------------------------------------------------------------
// findFunctionToolCallLeak
// ---------------------------------------------------------------------------
describe('findFunctionToolCallLeak branch coverage', () => {
it('reports the tool name position, not the preceding non-word character', () => {
// 'pulse_read' is preceded by a space; the leak index must point at 'p'.
expect(stripAssistantOutputArtifacts('Run pulse_read(target_host="x") now')).toEqual({
text: 'Run',
stripped: true,
});
});
it('skips a non-tool-like function call so no artifact is reported', () => {
expect(stripAssistantOutputArtifacts('Call helper(target="x") in the example.')).toEqual({
text: 'Call helper(target="x") in the example.',
stripped: false,
});
});
});
@@ -0,0 +1,634 @@
import { describe, expect, it } from 'vitest';
import {
alertResourceSupportsMetric,
flattenAlertResourceTableResources,
getAlertResourceColumnHeaderTooltip,
getAlertResourceColumnKind,
getAlertResourceEnabledDefault,
getAlertResourceLabel,
getAlertResourceMetricBounds,
getAlertResourceMetricDelayOverride,
getAlertResourceMetricDisplayValue,
getAlertResourceMetricStep,
hasAlertResourceTableRows,
hasCustomAlertResourceGlobalDefaults,
normalizeAlertResourceMetricKey,
type AlertResourceTableResourceLike,
type AlertResourceThresholdMap,
} from '../alertResourceTableModel';
function makeResource(
overrides: Partial<AlertResourceTableResourceLike> = {},
): AlertResourceTableResourceLike {
return {
id: 'res-1',
name: 'Test VM',
...overrides,
};
}
describe('flattenAlertResourceTableResources', () => {
it('returns flattened grouped resources when groupedResources is provided', () => {
const a = makeResource({ id: 'a', name: 'A' });
const b = makeResource({ id: 'b', name: 'B' });
const c = makeResource({ id: 'c', name: 'C' });
expect(flattenAlertResourceTableResources(undefined, { nodes: [a, b], agents: [c] })).toEqual([
a,
b,
c,
]);
});
it('returns the resources array when groupedResources is undefined', () => {
const a = makeResource({ id: 'a', name: 'A' });
const b = makeResource({ id: 'b', name: 'B' });
expect(flattenAlertResourceTableResources([a, b])).toEqual([a, b]);
});
it('returns an empty array when both arguments are undefined', () => {
expect(flattenAlertResourceTableResources()).toEqual([]);
});
it('prefers groupedResources even when resources is also provided', () => {
const a = makeResource({ id: 'a', name: 'A' });
const b = makeResource({ id: 'b', name: 'B' });
expect(flattenAlertResourceTableResources([b], { group: [a] })).toEqual([a]);
});
it('returns an empty array for an empty groupedResources object (truthy but no values)', () => {
expect(flattenAlertResourceTableResources(undefined, {})).toEqual([]);
});
});
describe('hasAlertResourceTableRows', () => {
it('returns true when resources has entries (flatten length > 0)', () => {
expect(hasAlertResourceTableRows([makeResource({ id: 'a', name: 'A' })])).toBe(true);
});
it('returns true when groupedResources has keys even if all groups are empty arrays', () => {
expect(hasAlertResourceTableRows(undefined, { group: [] })).toBe(true);
});
it('returns true when only globalDefaults with a numeric value is provided', () => {
expect(hasAlertResourceTableRows(undefined, undefined, { cpu: 0 })).toBe(true);
});
it('returns false when nothing is provided', () => {
expect(hasAlertResourceTableRows()).toBe(false);
});
it('returns false when resources is empty, groupedResources is undefined, and globalDefaults is undefined', () => {
expect(hasAlertResourceTableRows([], undefined, undefined)).toBe(false);
});
it('returns true for an empty-object globalDefaults because Boolean({}) is truthy', () => {
expect(hasAlertResourceTableRows(undefined, undefined, {})).toBe(true);
});
it('returns true when groupedResources is non-empty and globalDefaults is also set', () => {
expect(hasAlertResourceTableRows(undefined, { g: [makeResource()] }, { cpu: 80 })).toBe(true);
});
});
describe('hasCustomAlertResourceGlobalDefaults', () => {
it('returns false when globalDefaults is undefined', () => {
expect(hasCustomAlertResourceGlobalDefaults(undefined, { cpu: 80 })).toBe(false);
});
it('returns false when factoryDefaults is undefined', () => {
expect(hasCustomAlertResourceGlobalDefaults({ cpu: 80 }, undefined)).toBe(false);
});
it('returns false when both are undefined', () => {
expect(hasCustomAlertResourceGlobalDefaults(undefined, undefined)).toBe(false);
});
it('returns false when all factory keys match the global values exactly', () => {
expect(
hasCustomAlertResourceGlobalDefaults({ cpu: 80, memory: 90 }, { cpu: 80, memory: 90 }),
).toBe(false);
});
it('returns true when at least one factory key differs in global', () => {
expect(
hasCustomAlertResourceGlobalDefaults({ cpu: 75, memory: 90 }, { cpu: 80, memory: 90 }),
).toBe(true);
});
it('returns false when a factory key is absent from globalDefaults (current is undefined)', () => {
expect(
hasCustomAlertResourceGlobalDefaults({ memory: 90 }, { cpu: 80, memory: 90 }),
).toBe(false);
});
it('returns true when exactly one of many keys differs', () => {
expect(
hasCustomAlertResourceGlobalDefaults(
{ cpu: 80, memory: 90, disk: 95 },
{ cpu: 80, memory: 90, disk: 85 },
),
).toBe(true);
});
it('treats a global value of 0 as defined and custom when factory is non-zero', () => {
expect(hasCustomAlertResourceGlobalDefaults({ cpu: 0 }, { cpu: 80 })).toBe(true);
});
it('returns false when global value equals factory value of 0', () => {
expect(hasCustomAlertResourceGlobalDefaults({ cpu: 0 }, { cpu: 0 })).toBe(false);
});
});
describe('normalizeAlertResourceMetricKey', () => {
describe('Map direct hits', () => {
it.each([
['cpu %', 'cpu'],
['memory %', 'memory'],
['disk %', 'disk'],
['disk r mb/s', 'diskRead'],
['disk w mb/s', 'diskWrite'],
['net in mb/s', 'networkIn'],
['net out mb/s', 'networkOut'],
['usage %', 'usage'],
['temp °c', 'temperature'],
['temperature °c', 'temperature'],
['temperature', 'temperature'],
['restart count', 'restartCount'],
['restart window', 'restartWindow'],
['restart window (s)', 'restartWindow'],
['memory warn %', 'memoryWarnPct'],
['memory critical %', 'memoryCriticalPct'],
['warning size (gib)', 'warningSizeGiB'],
['critical size (gib)', 'criticalSizeGiB'],
['disk temp °c', 'diskTemperature'],
['backup', 'backup'],
['snapshot', 'snapshot'],
])('maps %s -> %s', (input, expected) => {
expect(normalizeAlertResourceMetricKey(input)).toBe(expected);
});
});
describe('trim and lowercase normalization before Map lookup', () => {
it('trims surrounding whitespace before lookup', () => {
expect(normalizeAlertResourceMetricKey(' cpu % ')).toBe('cpu');
});
it('lowercases before lookup', () => {
expect(normalizeAlertResourceMetricKey('CPU %')).toBe('cpu');
});
it('handles mixed case and surrounding whitespace together', () => {
expect(normalizeAlertResourceMetricKey(' Memory % ')).toBe('memory');
});
});
describe('replace fallback chain (unmapped inputs)', () => {
it('strips " %" suffix', () => {
expect(normalizeAlertResourceMetricKey('foo %')).toBe('foo');
});
it('strips " °c" suffix', () => {
expect(normalizeAlertResourceMetricKey('foo °c')).toBe('foo');
});
it('strips " mb/s" suffix', () => {
expect(normalizeAlertResourceMetricKey('foo mb/s')).toBe('foo');
});
it('maps "disk r" to "diskRead" via replace (not in Map)', () => {
expect(normalizeAlertResourceMetricKey('disk r')).toBe('diskRead');
});
it('maps "disk w" to "diskWrite" via replace', () => {
expect(normalizeAlertResourceMetricKey('disk w')).toBe('diskWrite');
});
it('maps "net in" to "networkIn" via replace', () => {
expect(normalizeAlertResourceMetricKey('net in')).toBe('networkIn');
});
it('maps "net out" to "networkOut" via replace', () => {
expect(normalizeAlertResourceMetricKey('net out')).toBe('networkOut');
});
it('returns the trimmed lowercased key unchanged when no pattern matches', () => {
expect(normalizeAlertResourceMetricKey('Foobar')).toBe('foobar');
});
it('chains multiple replaces: "disk r %" -> "diskRead"', () => {
expect(normalizeAlertResourceMetricKey('disk r %')).toBe('diskRead');
});
});
});
describe('getAlertResourceMetricBounds', () => {
it.each([
['temperature', { min: -1, max: 150 }],
['diskTemperature', { min: -1, max: 150 }],
['diskRead', { min: -1, max: 10000 }],
['diskWrite', { min: -1, max: 10000 }],
['networkIn', { min: -1, max: 10000 }],
['networkOut', { min: -1, max: 10000 }],
['cpu', { min: -1, max: 100 }],
['memory', { min: -1, max: 100 }],
['disk', { min: -1, max: 100 }],
['usage', { min: -1, max: 100 }],
['memoryWarnPct', { min: -1, max: 100 }],
['memoryCriticalPct', { min: -1, max: 100 }],
['warningSizeGiB', { min: -1, max: 100000 }],
['criticalSizeGiB', { min: -1, max: 100000 }],
['restartCount', { min: -1, max: 50 }],
['restartWindow', { min: -1, max: 86400 }],
['unknownMetric', { min: -1, max: 10000 }],
])('returns correct bounds for %s', (metric, expected) => {
expect(getAlertResourceMetricBounds(metric)).toStrictEqual(expected);
});
});
describe('getAlertResourceMetricStep', () => {
it.each([
['diskRead', 'any'],
['diskWrite', 'any'],
['networkIn', 'any'],
['networkOut', 'any'],
['warningSizeGiB', 'any'],
['criticalSizeGiB', 'any'],
['cpu', 1],
['temperature', 1],
['unknownMetric', 1],
])('returns correct step for %s', (metric, expected) => {
expect(getAlertResourceMetricStep(metric)).toBe(expected);
});
});
describe('getAlertResourceEnabledDefault', () => {
it.each([
['diskRead', 100],
['diskWrite', 100],
['networkIn', 100],
['networkOut', 100],
['temperature', 80],
['diskTemperature', 55],
['restartCount', 3],
['restartWindow', 300],
['memoryWarnPct', 90],
['memoryCriticalPct', 95],
['cpu', 80],
['memory', 80],
['unknownMetric', 80],
])('returns correct default for %s', (metric, expected) => {
expect(getAlertResourceEnabledDefault(metric)).toBe(expected);
});
});
describe('getAlertResourceMetricDelayOverride', () => {
it('returns undefined when metricDelaySeconds is undefined', () => {
expect(getAlertResourceMetricDelayOverride(undefined, 'cpu')).toBeUndefined();
});
it('returns the value for a normalized (trimmed+lowercased) key match', () => {
expect(getAlertResourceMetricDelayOverride({ cpu: 30 }, 'CPU')).toBe(30);
});
it('returns the value when the metric is already normalized', () => {
expect(getAlertResourceMetricDelayOverride({ cpu: 30 }, 'cpu')).toBe(30);
});
it('falls back to the original metric key when normalized lookup misses', () => {
expect(getAlertResourceMetricDelayOverride({ CPU: 45 } as Record<string, number>, 'CPU')).toBe(
45,
);
});
it('returns undefined when neither normalized nor original key matches', () => {
expect(getAlertResourceMetricDelayOverride({ foo: 30 }, 'cpu')).toBeUndefined();
});
it('returns undefined when the looked-up value is NaN (not finite)', () => {
expect(getAlertResourceMetricDelayOverride({ cpu: NaN }, 'cpu')).toBeUndefined();
});
it('returns undefined when the looked-up value is Infinity (not finite)', () => {
expect(getAlertResourceMetricDelayOverride({ cpu: Infinity }, 'cpu')).toBeUndefined();
});
it('returns undefined when the value is not a number type', () => {
const corrupted = { cpu: '30' } as unknown as Record<string, number>;
expect(getAlertResourceMetricDelayOverride(corrupted, 'cpu')).toBeUndefined();
});
it('returns 0 for a valid finite value of 0', () => {
expect(getAlertResourceMetricDelayOverride({ cpu: 0 }, 'cpu')).toBe(0);
});
it('returns a negative finite value as-is', () => {
expect(getAlertResourceMetricDelayOverride({ cpu: -5 }, 'cpu')).toBe(-5);
});
});
describe('getAlertResourceColumnHeaderTooltip', () => {
it('returns the tooltip for an exact column match', () => {
expect(getAlertResourceColumnHeaderTooltip('cpu %')).toBe(
'Percent CPU utilization allowed before an alert fires.',
);
});
it('returns the tooltip via normalized lookup when exact misses (case/whitespace)', () => {
expect(getAlertResourceColumnHeaderTooltip(' CPU % ')).toBe(
'Percent CPU utilization allowed before an alert fires.',
);
});
it('returns undefined when no tooltip exists for the column', () => {
expect(getAlertResourceColumnHeaderTooltip('nonexistent column')).toBeUndefined();
});
it('returns the mail-queue tooltip for a known queue column', () => {
expect(getAlertResourceColumnHeaderTooltip('queue warn')).toBe(
'Early warning when total mail queue exceeds this message count.',
);
});
it('returns the snapshot-size tooltip via normalized lookup (mixed case)', () => {
expect(getAlertResourceColumnHeaderTooltip('Warning Size (GiB)')).toBe(
'Total snapshot size in GiB that raises a warning.',
);
});
});
describe('getAlertResourceColumnKind', () => {
it('returns "badge" for "backup"', () => {
expect(getAlertResourceColumnKind('backup')).toBe('badge');
});
it('returns "badge" for "snapshot"', () => {
expect(getAlertResourceColumnKind('snapshot')).toBe('badge');
});
it('returns "badge" for "Backup" (case-insensitive via normalize)', () => {
expect(getAlertResourceColumnKind('Backup')).toBe('badge');
});
it('returns "numeric-value" for a standard numeric metric', () => {
expect(getAlertResourceColumnKind('cpu %')).toBe('numeric-value');
});
it('returns "numeric-value" for an unmapped column', () => {
expect(getAlertResourceColumnKind('unknown')).toBe('numeric-value');
});
});
describe('alertResourceSupportsMetric', () => {
it('returns true when resourceType is undefined (any metric)', () => {
expect(alertResourceSupportsMetric(undefined, 'cpu')).toBe(true);
expect(alertResourceSupportsMetric(undefined, 'temperature')).toBe(true);
});
it('returns true for an empty-string resourceType', () => {
expect(alertResourceSupportsMetric('', 'cpu')).toBe(true);
});
describe('node / agent — throughput blocked', () => {
it('returns false for all throughput metrics on node', () => {
expect(alertResourceSupportsMetric('node', 'diskRead')).toBe(false);
expect(alertResourceSupportsMetric('node', 'diskWrite')).toBe(false);
expect(alertResourceSupportsMetric('node', 'networkIn')).toBe(false);
expect(alertResourceSupportsMetric('node', 'networkOut')).toBe(false);
});
it('returns true for cpu on node (falls through to default)', () => {
expect(alertResourceSupportsMetric('node', 'cpu')).toBe(true);
});
it('returns false for throughput on agent', () => {
expect(alertResourceSupportsMetric('agent', 'networkIn')).toBe(false);
});
it('returns true for memory on agent', () => {
expect(alertResourceSupportsMetric('agent', 'memory')).toBe(true);
});
});
describe('pbs — cpu/memory only', () => {
it('supports cpu and memory', () => {
expect(alertResourceSupportsMetric('pbs', 'cpu')).toBe(true);
expect(alertResourceSupportsMetric('pbs', 'memory')).toBe(true);
});
it('rejects disk and temperature', () => {
expect(alertResourceSupportsMetric('pbs', 'disk')).toBe(false);
expect(alertResourceSupportsMetric('pbs', 'temperature')).toBe(false);
});
});
describe('storage — usage only', () => {
it('supports usage', () => {
expect(alertResourceSupportsMetric('storage', 'usage')).toBe(true);
});
it('rejects cpu', () => {
expect(alertResourceSupportsMetric('storage', 'cpu')).toBe(false);
});
});
describe('kubernetesNamespace — nothing supported', () => {
it('returns false for all metrics', () => {
expect(alertResourceSupportsMetric('kubernetesNamespace', 'cpu')).toBe(false);
expect(alertResourceSupportsMetric('kubernetesNamespace', 'memory')).toBe(false);
});
});
describe('kubernetes cluster / deployment / pod', () => {
it('supports cpu/disk/throughput on kubernetesCluster', () => {
expect(alertResourceSupportsMetric('kubernetesCluster', 'cpu')).toBe(true);
expect(alertResourceSupportsMetric('kubernetesCluster', 'disk')).toBe(true);
expect(alertResourceSupportsMetric('kubernetesCluster', 'diskRead')).toBe(true);
});
it('rejects temperature on kubernetesCluster', () => {
expect(alertResourceSupportsMetric('kubernetesCluster', 'temperature')).toBe(false);
});
it('supports networkIn on kubernetesDeployment', () => {
expect(alertResourceSupportsMetric('kubernetesDeployment', 'networkIn')).toBe(true);
});
it('rejects restartCount on kubernetesDeployment', () => {
expect(alertResourceSupportsMetric('kubernetesDeployment', 'restartCount')).toBe(false);
});
it('supports disk on kubernetesPod', () => {
expect(alertResourceSupportsMetric('kubernetesPod', 'disk')).toBe(true);
});
it('rejects usage on kubernetesPod', () => {
expect(alertResourceSupportsMetric('kubernetesPod', 'usage')).toBe(false);
});
});
describe('kubernetesNode — cpu/memory/disk only', () => {
it('supports cpu/memory/disk', () => {
expect(alertResourceSupportsMetric('kubernetesNode', 'cpu')).toBe(true);
expect(alertResourceSupportsMetric('kubernetesNode', 'memory')).toBe(true);
expect(alertResourceSupportsMetric('kubernetesNode', 'disk')).toBe(true);
});
it('rejects diskRead', () => {
expect(alertResourceSupportsMetric('kubernetesNode', 'diskRead')).toBe(false);
});
});
describe('truenasSystem — extended set including temperature', () => {
it('supports temperature and cpu', () => {
expect(alertResourceSupportsMetric('truenasSystem', 'temperature')).toBe(true);
expect(alertResourceSupportsMetric('truenasSystem', 'cpu')).toBe(true);
});
it('rejects restartCount', () => {
expect(alertResourceSupportsMetric('truenasSystem', 'restartCount')).toBe(false);
});
});
describe('truenasPool / truenasDataset — usage only', () => {
it('supports usage on truenasPool', () => {
expect(alertResourceSupportsMetric('truenasPool', 'usage')).toBe(true);
});
it('rejects cpu on truenasPool', () => {
expect(alertResourceSupportsMetric('truenasPool', 'cpu')).toBe(false);
});
it('supports usage on truenasDataset', () => {
expect(alertResourceSupportsMetric('truenasDataset', 'usage')).toBe(true);
});
it('rejects temperature on truenasDataset', () => {
expect(alertResourceSupportsMetric('truenasDataset', 'temperature')).toBe(false);
});
});
describe('truenasDisk — temperature only', () => {
it('supports temperature', () => {
expect(alertResourceSupportsMetric('truenasDisk', 'temperature')).toBe(true);
});
it('rejects cpu', () => {
expect(alertResourceSupportsMetric('truenasDisk', 'cpu')).toBe(false);
});
});
describe('vmwareHost — no disk', () => {
it('supports cpu/memory/throughput', () => {
expect(alertResourceSupportsMetric('vmwareHost', 'cpu')).toBe(true);
expect(alertResourceSupportsMetric('vmwareHost', 'memory')).toBe(true);
expect(alertResourceSupportsMetric('vmwareHost', 'diskRead')).toBe(true);
});
it('rejects disk', () => {
expect(alertResourceSupportsMetric('vmwareHost', 'disk')).toBe(false);
});
});
describe('vmwareVm — disk + throughput', () => {
it('supports disk and networkOut', () => {
expect(alertResourceSupportsMetric('vmwareVm', 'disk')).toBe(true);
expect(alertResourceSupportsMetric('vmwareVm', 'networkOut')).toBe(true);
});
it('rejects temperature', () => {
expect(alertResourceSupportsMetric('vmwareVm', 'temperature')).toBe(false);
});
});
describe('vmwareDatastore — usage only', () => {
it('supports usage', () => {
expect(alertResourceSupportsMetric('vmwareDatastore', 'usage')).toBe(true);
});
it('rejects cpu', () => {
expect(alertResourceSupportsMetric('vmwareDatastore', 'cpu')).toBe(false);
});
});
describe('vmwareNetwork — nothing supported', () => {
it('returns false for any metric', () => {
expect(alertResourceSupportsMetric('vmwareNetwork', 'cpu')).toBe(false);
});
});
describe('dockerContainer — restart + memory-warn set', () => {
it('supports restart and memory-warn metrics', () => {
expect(alertResourceSupportsMetric('dockerContainer', 'restartCount')).toBe(true);
expect(alertResourceSupportsMetric('dockerContainer', 'restartWindow')).toBe(true);
expect(alertResourceSupportsMetric('dockerContainer', 'memoryWarnPct')).toBe(true);
expect(alertResourceSupportsMetric('dockerContainer', 'memoryCriticalPct')).toBe(true);
expect(alertResourceSupportsMetric('dockerContainer', 'cpu')).toBe(true);
expect(alertResourceSupportsMetric('dockerContainer', 'memory')).toBe(true);
});
it('rejects temperature and disk', () => {
expect(alertResourceSupportsMetric('dockerContainer', 'temperature')).toBe(false);
expect(alertResourceSupportsMetric('dockerContainer', 'disk')).toBe(false);
});
});
describe('unknown resource type — default true', () => {
it('returns true for any metric', () => {
expect(alertResourceSupportsMetric('mysteryType', 'cpu')).toBe(true);
expect(alertResourceSupportsMetric('mysteryType', 'anything')).toBe(true);
});
});
});
describe('getAlertResourceLabel', () => {
it('returns the displayName when present', () => {
const resource = makeResource({ displayName: 'Prod Web Server' });
expect(getAlertResourceLabel(resource)).toBe('Prod Web Server');
});
it('trims surrounding whitespace from displayName', () => {
const resource = makeResource({ displayName: ' Prod Web Server ' });
expect(getAlertResourceLabel(resource)).toBe('Prod Web Server');
});
it('falls back to name when displayName is absent', () => {
const resource = makeResource({ name: 'web-01' });
expect(getAlertResourceLabel(resource)).toBe('web-01');
});
it('falls through whitespace-only displayName to the name', () => {
const resource = makeResource({ name: 'node-7', displayName: ' ' });
expect(getAlertResourceLabel(resource)).toBe('node-7');
});
});
describe('parseAlertMetricNumber (exercised via getAlertResourceMetricDisplayValue)', () => {
it('passes a negative number through the typeof-number branch unchanged', () => {
const resource = makeResource({ thresholds: { cpu: -5 } });
expect(getAlertResourceMetricDisplayValue(resource, 'cpu')).toBe(-5);
});
it('passes a float through the typeof-number branch unchanged', () => {
const resource = makeResource({ thresholds: { cpu: 42.5 } });
expect(getAlertResourceMetricDisplayValue(resource, 'cpu')).toBe(42.5);
});
it('parses a whitespace-padded numeric string via Number() (finite branch -> number)', () => {
const thresholds = { cpu: ' 42 ' } as unknown as AlertResourceThresholdMap;
const resource = makeResource({ thresholds });
expect(getAlertResourceMetricDisplayValue(resource, 'cpu')).toBe(42);
});
it('returns 0 fallback for null (null branch -> undefined -> defaults -> 0)', () => {
const thresholds = { cpu: null } as unknown as AlertResourceThresholdMap;
const resource = makeResource({ thresholds });
expect(getAlertResourceMetricDisplayValue(resource, 'cpu')).toBe(0);
});
it('returns 0 fallback for a non-numeric string (Number() -> NaN -> undefined -> 0)', () => {
const thresholds = { cpu: 'nope' } as unknown as AlertResourceThresholdMap;
const resource = makeResource({ thresholds });
expect(getAlertResourceMetricDisplayValue(resource, 'cpu')).toBe(0);
});
});
@@ -0,0 +1,335 @@
import { describe, expect, it } from 'vitest';
import {
buildAccessSummary,
buildHostDetailCards,
buildHostDetailSummary,
buildKubernetesCapabilityBadges,
buildRelatedLinks,
buildSourceHealthSummary,
buildSourceSummary,
hasRuntimeOperationalContext,
} from '@/components/Infrastructure/resourceDetailDrawerOperationalModel';
import type { PlatformData } from '@/components/Infrastructure/resourceDetailMappers';
import type { Resource } from '@/types/resource';
// Mirrors the private badge-class constants in resourceDetailDrawerOperationalModel.ts
// so the assertions here stay brittle to drift in those exact Tailwind tokens.
const SUPPORTED_BADGE_CLASS =
'inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap bg-cyan-100 text-cyan-700 dark:bg-cyan-900 dark:text-cyan-400';
const UNSUPPORTED_BADGE_CLASS =
'inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap bg-surface-alt text-muted';
type SourceStatusMap = NonNullable<PlatformData['sourceStatus']>;
const baseResource = (overrides: Partial<Resource>): Resource => ({
id: 'resource-1',
type: 'agent',
name: 'host-1',
displayName: 'Host 1',
platformId: 'host-1',
platformType: 'agent',
sourceType: 'hybrid',
status: 'online',
lastSeen: Date.now(),
platformData: { sources: ['agent'] },
...overrides,
});
describe('buildKubernetesCapabilityBadges branch coverage', () => {
it('returns an empty array when capabilities are undefined', () => {
expect(buildKubernetesCapabilityBadges(undefined)).toEqual([]);
});
it('returns an empty array when capabilities is an empty object (all flags undefined)', () => {
// Every supported flag is undefined (falsy) AND podDiskIo is undefined,
// so the only branch that fires is the "unsupported" one.
expect(buildKubernetesCapabilityBadges({})).toEqual([
{
label: 'Pod Disk I/O Unsupported',
classes: UNSUPPORTED_BADGE_CLASS,
title:
'Pod disk read/write throughput is not collected by the Kubernetes integration path today.',
},
]);
});
it('emits only the Pod Network badge when podNetwork is the sole supported flag and podDiskIo is true', () => {
// Drives the false-arm of nodeCpuMemory/nodeTelemetry/podCpuMemory/podEphemeralDisk,
// the true-arm of podNetwork, and the true-arm of podDiskIo (which suppresses
// the "unsupported" badge).
expect(
buildKubernetesCapabilityBadges({
podNetwork: true,
podDiskIo: true,
}),
).toEqual([
{
label: 'Pod Network',
classes: SUPPORTED_BADGE_CLASS,
title: 'Pod network throughput is available.',
},
]);
});
it('emits node telemetry and pod ephemeral disk badges together without the unsupported badge', () => {
expect(
buildKubernetesCapabilityBadges({
nodeTelemetry: true,
podEphemeralDisk: true,
podDiskIo: true,
}),
).toEqual([
{
label: 'Node Telemetry (Agent)',
classes: SUPPORTED_BADGE_CLASS,
title:
'Linked Pulse agent provides node uptime, temperature, disk, network, and disk I/O.',
},
{
label: 'Pod Ephemeral Disk',
classes: SUPPORTED_BADGE_CLASS,
title: 'Pod ephemeral storage usage is available.',
},
]);
});
});
describe('buildSourceHealthSummary branch coverage', () => {
it('counts an unrecognized status as unhealthy and returns the red summary', () => {
// 'offline' matches neither the healthy nor degraded token sets, hitting the
// else branch (unhealthy += 1) and the unhealthy > 0 return arm.
expect(buildSourceHealthSummary({ agent: { status: 'offline' } })).toEqual({
label: '1/1 unhealthy',
className: 'text-red-600 dark:text-red-400',
title: 'agent:offline',
});
});
it('prefers the unhealthy summary when both unhealthy and warning counts are positive', () => {
expect(
buildSourceHealthSummary({
agent: { status: 'offline' },
docker: { status: 'degraded' },
}),
).toEqual({
label: '1/2 unhealthy',
className: 'text-red-600 dark:text-red-400',
title: 'agent:offline • docker:degraded',
});
});
it('counts multiple unhealthy entries in the label numerator', () => {
expect(
buildSourceHealthSummary({
a: { status: 'offline' },
b: { status: 'down' },
}),
).toEqual({
label: '2/2 unhealthy',
className: 'text-red-600 dark:text-red-400',
title: 'a:offline • b:down',
});
});
it('normalizes healthy/degraded tokens via trim().toLowerCase() before matching', () => {
// ' ONLINE ' normalizes to 'online' and hits the healthy continue branch.
expect(buildSourceHealthSummary({ a: { status: ' ONLINE ' } })).toBeNull();
// 'Warning' (mixed case) normalizes to 'warning' and hits the degraded branch.
expect(buildSourceHealthSummary({ a: { status: 'Warning' } })).toEqual({
label: '1/1 degraded',
className: 'text-amber-600 dark:text-amber-400',
title: 'a:warning',
});
});
it('treats an empty/whitespace status as "unknown" and counts it unhealthy', () => {
// Exercises the `|| ''` fallback in `(status?.status || '')`.
expect(buildSourceHealthSummary({ a: { status: ' ' } })).toEqual({
label: '1/1 unhealthy',
className: 'text-red-600 dark:text-red-400',
title: 'a:unknown',
});
});
it('hits the optional-chain short-circuit when an entry value is nullish', () => {
// `status?.status` short-circuits only when status itself is null/undefined;
// the cast is required because the declared value type is non-nullable.
const broken = { agent: null } as unknown as SourceStatusMap;
expect(buildSourceHealthSummary(broken)).toEqual({
label: '1/1 unhealthy',
className: 'text-red-600 dark:text-red-400',
title: 'agent:unknown',
});
});
it('returns null when every entry normalizes to a healthy token', () => {
expect(
buildSourceHealthSummary({
a: { status: 'running' },
b: { status: 'connected' },
c: { status: 'ok' },
}),
).toBeNull();
});
});
describe('buildSourceSummary branch coverage', () => {
it('delegates to buildSourceHealthSummary and surfaces the red unhealthy summary', () => {
expect(buildSourceSummary(['agent'], { agent: { status: 'offline' } })).toEqual({
label: '1/1 unhealthy',
className: 'text-red-600 dark:text-red-400',
title: 'agent:offline',
});
});
it('returns null when buildSourceHealthSummary returns null (the fallthrough arm)', () => {
expect(buildSourceSummary(['agent'], { agent: { status: 'online' } })).toBeNull();
});
});
describe('buildHostDetailCards branch coverage', () => {
it('returns an empty array when neither proxmox node nor agent details are present', () => {
expect(
buildHostDetailCards({
hasProxmoxNode: false,
hasAgentDetails: false,
networkInterfaceCount: 0,
diskCount: 0,
raidCount: 0,
temperatureRowCount: 0,
}),
).toEqual([]);
});
it('emits only system/hardware/storage when only the proxmox node flag is set', () => {
// The agent-section counts must be ignored entirely when hasAgentDetails is false.
expect(
buildHostDetailCards({
hasProxmoxNode: true,
hasAgentDetails: false,
networkInterfaceCount: 9,
diskCount: 9,
raidCount: 9,
temperatureRowCount: 9,
}),
).toEqual(['system', 'hardware', 'storage']);
});
it('emits system/hardware plus every optional agent section when all counts are positive', () => {
expect(
buildHostDetailCards({
hasProxmoxNode: false,
hasAgentDetails: true,
networkInterfaceCount: 1,
diskCount: 1,
raidCount: 1,
temperatureRowCount: 1,
}),
).toEqual(['system', 'hardware', 'network', 'disks', 'raid', 'temperatures']);
});
it('omits every optional section when agent detail counts are zero (each > 0 branch falsy)', () => {
expect(
buildHostDetailCards({
hasProxmoxNode: false,
hasAgentDetails: true,
networkInterfaceCount: 0,
diskCount: 0,
raidCount: 0,
temperatureRowCount: 0,
}),
).toEqual(['system', 'hardware']);
});
});
describe('buildHostDetailSummary branch coverage', () => {
it('returns null for an empty card list', () => {
expect(buildHostDetailSummary([])).toBeNull();
});
it('returns the single category label verbatim when there is exactly one card', () => {
expect(buildHostDetailSummary(['disks'])).toBe('Disks');
});
it('joins two categories with "and"', () => {
expect(buildHostDetailSummary(['system', 'network'])).toBe('System and Network');
});
it('joins three or more categories with Oxford comma', () => {
expect(buildHostDetailSummary(['system', 'hardware', 'network'])).toBe(
'System, Hardware, and Network',
);
});
it('passes through an unknown card name via the ?? fallback', () => {
expect(buildHostDetailSummary(['frobnicator'])).toBe('frobnicator');
});
it('dedupes repeated card names before joining', () => {
expect(buildHostDetailSummary(['system', 'system', 'hardware'])).toBe(
'System and Hardware',
);
});
});
describe('buildAccessSummary branch coverage', () => {
it('pluralizes "links" when there is more than one link and no web interface', () => {
const links = [
{ href: '/a', label: 'A', compactLabel: 'A', ariaLabel: 'A' },
{ href: '/b', label: 'B', compactLabel: 'B', ariaLabel: 'B' },
];
expect(buildAccessSummary({ hasWebInterface: false, links })).toBe('2 links');
});
it('joins web interface and plural links with the " · " separator', () => {
const links = [
{ href: '/a', label: 'A', compactLabel: 'A', ariaLabel: 'A' },
{ href: '/b', label: 'B', compactLabel: 'B', ariaLabel: 'B' },
];
expect(buildAccessSummary({ hasWebInterface: true, links })).toBe(
'Web interface · 2 links',
);
});
it('returns the bare link count when web interface is absent and exactly one link is present', () => {
const links = [{ href: '/a', label: 'A', compactLabel: 'A', ariaLabel: 'A' }];
expect(buildAccessSummary({ hasWebInterface: false, links })).toBe('1 link');
});
});
describe('buildRelatedLinks branch coverage', () => {
it('returns the PMG thresholds link for a pmg resource', () => {
// buildServiceDetailLinks produces a single link for type==='pmg';
// the seen-set dedup then admits it (first occurrence -> true arm).
const resource = baseResource({
type: 'pmg',
platformType: 'proxmox-pmg',
name: 'mail',
displayName: 'Mail Gateway',
platformData: { sources: ['proxmox-pmg'] },
});
expect(buildRelatedLinks(resource, 'Mail Gateway')).toEqual([
{
href: '/alerts/thresholds/mail-gateway',
label: 'Open PMG thresholds',
compactLabel: 'Thresholds',
ariaLabel: 'Open PMG thresholds for Mail Gateway',
},
]);
});
});
describe('hasRuntimeOperationalContext branch coverage', () => {
it('returns true when the badge list is non-empty', () => {
expect(
hasRuntimeOperationalContext([
{
label: 'K8s Node CPU/Memory',
classes: SUPPORTED_BADGE_CLASS,
title: 'Node CPU and memory metrics are available.',
},
]),
).toBe(true);
});
});
@@ -0,0 +1,601 @@
import { describe, expect, it } from 'vitest';
import type { APITokenRecord } from '@/api/security';
import type { Resource } from '@/types/resource';
import { DOCKER_REPORT_SCOPE } from '@/constants/apiScopes';
import {
API_TOKEN_PULSE_INTELLIGENCE_AGENT_PRESET_ID,
agentActionIdForResource,
buildAgentTokenUsage,
buildDockerTokenUsage,
dockerActionIdForResource,
getAPITokenDialogName,
getAPITokenHint,
getAPITokenScopePresets,
hasAgentScopeResource,
matchesScopePreset,
tokenRevokedAtForResource,
} from '../apiTokenManagerModel';
// ---- Fixtures ---------------------------------------------------------------
// Mirrors the sibling APITokenManager.test.tsx fixture builders so the private
// helpers under test (readPlatformData / readNestedPlatformField /
// readPlatformNumber / normalizePresetScopes / appendUsageEntry) are driven
// through the single exported orchestrators that compose them.
const makeResource = (overrides: Partial<Resource> = {}): Resource => ({
id: 'resource-1',
type: 'agent',
name: 'Resource One',
displayName: 'Resource One',
platformId: 'agent-1',
platformType: 'agent',
sourceType: 'agent',
status: 'online',
lastSeen: Date.now(),
tags: [],
...overrides,
});
const makeToken = (overrides: Partial<APITokenRecord> = {}): APITokenRecord => ({
id: 'token-1',
name: 'Runtime token',
prefix: 'pulse',
suffix: '1234',
createdAt: '2026-03-12T10:00:00.000Z',
lastUsedAt: '2026-03-12T11:00:00.000Z',
scopes: [DOCKER_REPORT_SCOPE],
...overrides,
});
// ---- readPlatformNumber (private) via tokenRevokedAtForResource -------------
//
// readPlatformNumber is module-private and only reachable through
// tokenRevokedAtForResource. Placing `tokenRevokedAt` at the TOP level of
// platformData makes readNestedPlatformField return it verbatim (the
// `field in platformData` arm), so readPlatformNumber receives the raw value
// and every typeof/Number.isFinite arm is exercised.
describe('readPlatformNumber (via tokenRevokedAtForResource)', () => {
it('returns a finite number verbatim', () => {
expect(
tokenRevokedAtForResource(
makeResource({ platformData: { tokenRevokedAt: 1_700_000_000_000 } }),
),
).toBe(1_700_000_000_000);
});
it('returns 0 for a finite zero (not treated as missing)', () => {
expect(
tokenRevokedAtForResource(makeResource({ platformData: { tokenRevokedAt: 0 } })),
).toBe(0);
});
it('returns a negative finite number verbatim', () => {
expect(
tokenRevokedAtForResource(makeResource({ platformData: { tokenRevokedAt: -42 } })),
).toBe(-42);
});
it.each([Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY])(
'returns undefined for a number that is not finite (%s)',
(value) => {
expect(
tokenRevokedAtForResource(makeResource({ platformData: { tokenRevokedAt: value } })),
).toBeUndefined();
},
);
it.each(['1700000000000', '', [], { ms: 1 }, true, null])(
'returns undefined for a non-number value (%s)',
(value) => {
expect(
tokenRevokedAtForResource(
makeResource({ platformData: { tokenRevokedAt: value } as Record<string, unknown> }),
),
).toBeUndefined();
},
);
});
// ---- readPlatformData (private) via tokenRevokedAtForResource ---------------
//
// readPlatformData is module-private; its two arms (platformData absent ->
// undefined; present -> unwrap-and-return) are both hit through
// tokenRevokedAtForResource.
describe('readPlatformData (via tokenRevokedAtForResource)', () => {
it('returns undefined when platformData is absent (no platformData key)', () => {
const resource = makeResource();
expect(resource.platformData).toBeUndefined();
expect(tokenRevokedAtForResource(resource)).toBeUndefined();
});
it('unwraps a populated platformData record so nested fields are reachable', () => {
expect(
tokenRevokedAtForResource(
makeResource({ platformData: { tokenRevokedAt: 555, unrelated: true } }),
),
).toBe(555);
});
});
// ---- readNestedPlatformField (private) via tokenRevokedAtForResource --------
//
// readNestedPlatformField is module-private; every arm is driven via
// tokenRevokedAtForResource by relocating `tokenRevokedAt` through the
// platformData graph (top-level -> agent -> docker -> absent).
describe('readNestedPlatformField (via tokenRevokedAtForResource)', () => {
it('returns the field directly from platformData when present at the top level', () => {
expect(
tokenRevokedAtForResource(makeResource({ platformData: { tokenRevokedAt: 111 } })),
).toBe(111);
});
it('falls through to platformData.agent.<field> when not at the top level', () => {
expect(
tokenRevokedAtForResource(
makeResource({ platformData: { agent: { tokenRevokedAt: 222 } } }),
),
).toBe(222);
});
it('falls through to platformData.docker.<field> when neither top-level nor agent has it', () => {
expect(
tokenRevokedAtForResource(
makeResource({ platformData: { docker: { tokenRevokedAt: 333 } } }),
),
).toBe(333);
});
it('skips a non-object agent value and still resolves from docker', () => {
// agent is a truthy string, so `typeof agent === 'object'` is false and the
// agent branch is skipped; docker carries the field.
expect(
tokenRevokedAtForResource(
makeResource({
platformData: { agent: 'not-an-object', docker: { tokenRevokedAt: 444 } },
}),
),
).toBe(444);
});
it('returns undefined when agent is an object without the field and docker is absent', () => {
expect(
tokenRevokedAtForResource(
makeResource({ platformData: { agent: { unrelated: true } } }),
),
).toBeUndefined();
});
it('returns undefined when docker is a non-object value (object guard skips it)', () => {
expect(
tokenRevokedAtForResource(makeResource({ platformData: { docker: 'oops' } })),
).toBeUndefined();
});
it('returns undefined when the field is nowhere in the platformData graph', () => {
expect(
tokenRevokedAtForResource(
makeResource({
platformData: { agent: { other: 1 }, docker: { other: 2 } },
}),
),
).toBeUndefined();
});
it('returns undefined when platformData itself is undefined', () => {
expect(tokenRevokedAtForResource(makeResource({ platformData: undefined }))).toBeUndefined();
});
});
// ---- tokenRevokedAtForResource (exported composition) ----------------------
describe('tokenRevokedAtForResource', () => {
it('composes the three private readers to surface a revokedAt timestamp', () => {
expect(
tokenRevokedAtForResource(
makeResource({
platformData: { docker: { tokenId: 'tok', tokenRevokedAt: 9_999 } },
}),
),
).toBe(9_999);
});
});
// ---- hasAgentScopeResource --------------------------------------------------
describe('hasAgentScopeResource', () => {
it('returns false for a docker-host even when it carries an agent facet (early return)', () => {
const dockerHostWithAgent = makeResource({
type: 'docker-host',
platformType: 'docker',
agent: { agentId: 'would-otherwise-match' },
});
expect(hasAgentScopeResource(dockerHostWithAgent)).toBe(false);
});
it.each(['agent', 'pbs', 'pmg'] as const)(
'returns true for the canonical agent-scoped type %s',
(type) => {
expect(hasAgentScopeResource(makeResource({ type }))).toBe(true);
},
);
it('returns true for a non-canonical type that carries an agent facet', () => {
// 'vm' is not agent/pbs/pmg/docker-host, but resourceHasAgentFacet is true
// because resource.agent is set.
expect(
hasAgentScopeResource(
makeResource({ type: 'vm', platformType: 'proxmox-pve', agent: { agentId: 'vm-agent' } }),
),
).toBe(true);
});
it('returns false for a non-canonical type with no agent facet', () => {
expect(
hasAgentScopeResource(
makeResource({ type: 'storage', platformType: 'proxmox-pve', agent: undefined }),
),
).toBe(false);
});
});
// ---- getAPITokenHint --------------------------------------------------------
describe('getAPITokenHint', () => {
it('returns the em-dash placeholder for null', () => {
expect(getAPITokenHint(null)).toBe('—');
});
it('returns the em-dash placeholder for undefined', () => {
expect(getAPITokenHint(undefined)).toBe('—');
});
it('combines prefix and suffix with an ellipsis when both are present', () => {
expect(getAPITokenHint(makeToken({ prefix: 'pulse', suffix: '4321' }))).toBe('pulse…4321');
});
it('uses only the prefix when the suffix is blank', () => {
expect(getAPITokenHint(makeToken({ prefix: 'pulse', suffix: '' }))).toBe('pulse…');
});
it('falls back to the em-dash when only a suffix is present (prefix is falsy)', () => {
expect(getAPITokenHint(makeToken({ prefix: '', suffix: '9999' }))).toBe('—');
});
it('falls back to the em-dash when neither prefix nor suffix is present', () => {
expect(getAPITokenHint(makeToken({ prefix: '', suffix: '' }))).toBe('—');
});
});
// ---- getAPITokenDialogName --------------------------------------------------
describe('getAPITokenDialogName', () => {
it('returns the trimmed name when the name has non-whitespace content', () => {
expect(getAPITokenDialogName(makeToken({ name: ' Container automation ' }))).toBe(
'Container automation',
);
});
it('falls through when the name is only whitespace (trim() is empty -> falsy)', () => {
expect(getAPITokenDialogName(makeToken({ name: ' ', prefix: 'pulse', suffix: '1234' }))).toBe(
'pulse…1234',
);
});
it('exercises the optional-chain arm when name is undefined at runtime', () => {
expect(
getAPITokenDialogName(
makeToken({ name: undefined as unknown as string, prefix: 'pulse', suffix: '' }),
),
).toBe('pulse…');
});
it('combines prefix and suffix when the name is blank and both are set', () => {
expect(getAPITokenDialogName(makeToken({ name: '', prefix: 'pl', suffix: 'xy' }))).toBe('pl…xy');
});
it('returns "untitled token" when name, prefix, and suffix are all blank', () => {
expect(getAPITokenDialogName(makeToken({ name: '', prefix: '', suffix: '' }))).toBe(
'untitled token',
);
});
});
// ---- normalizePresetScopes (private) via getAPITokenScopePresets -----------
//
// normalizePresetScopes is module-private; its only caller is
// getAPITokenScopePresets, which feeds the normalized result to the Pulse
// Intelligence preset (and gates that preset on length > 0). Asserting that
// preset's `scopes` and presence exercises every normalizePresetScopes branch:
// trim(), filter(Boolean), Set dedup, and the scopes ?? [] fallback.
describe('normalizePresetScopes (via getAPITokenScopePresets)', () => {
const PULSE_PRESET_ID = API_TOKEN_PULSE_INTELLIGENCE_AGENT_PRESET_ID;
it('adds no Pulse Intelligence preset when the required-scopes arg defaults to []', () => {
const presets = getAPITokenScopePresets();
expect(presets.find((preset) => preset.id === PULSE_PRESET_ID)).toBeUndefined();
expect(presets).toHaveLength(7);
});
it('adds no Pulse Intelligence preset when every provided scope is blank/whitespace', () => {
const presets = getAPITokenScopePresets(['', ' ', '\t']);
expect(presets.find((preset) => preset.id === PULSE_PRESET_ID)).toBeUndefined();
expect(presets).toHaveLength(7);
});
it('trims, deduplicates, and drops empties, preserving first-seen order', () => {
const presets = getAPITokenScopePresets([' b ', 'b', '', 'a']);
const pulsePreset = presets.find((preset) => preset.id === PULSE_PRESET_ID);
expect(pulsePreset?.scopes).toStrictEqual(['b', 'a']);
expect(presets).toHaveLength(8);
});
it('keeps a single non-empty scope after normalization', () => {
const presets = getAPITokenScopePresets([' monitoring:read ']);
expect(presets.find((preset) => preset.id === PULSE_PRESET_ID)?.scopes).toStrictEqual([
'monitoring:read',
]);
});
});
// ---- agentActionIdForResource -----------------------------------------------
describe('agentActionIdForResource', () => {
it('returns the actionable agent id when an explicit agent id is resolvable', () => {
expect(
agentActionIdForResource(
makeResource({
id: 'fallback-id',
platformData: { agent: { agentId: 'agent-007' } },
}),
),
).toBe('agent-007');
});
it('falls back to resource.id when no agent id is resolvable anywhere', () => {
expect(
agentActionIdForResource(
makeResource({ id: 'fallback-id', type: 'vm', platformType: 'proxmox-pve' }),
),
).toBe('fallback-id');
});
});
// ---- dockerActionIdForResource ----------------------------------------------
describe('dockerActionIdForResource', () => {
it('returns the docker runtime id when hostSourceId is resolvable', () => {
expect(
dockerActionIdForResource(
makeResource({
id: 'fallback-id',
type: 'docker-host',
platformType: 'docker',
platformData: { docker: { hostSourceId: 'docker-runtime-1' } },
}),
),
).toBe('docker-runtime-1');
});
it('falls back to resource.id when no docker runtime id is resolvable', () => {
expect(
dockerActionIdForResource(
makeResource({ id: 'fallback-id', platformData: { agent: { agentId: 'a1' } } }),
),
).toBe('fallback-id');
});
});
// ---- appendUsageEntry (private) via buildDockerTokenUsage / buildAgentTokenUsage
//
// appendUsageEntry is module-private; its three arms (first entry / duplicate
// item-id no-op / new item-id append) are driven through the two exported
// usage builders, which also exercise their own skip/iterate branches.
describe('appendUsageEntry (via buildDockerTokenUsage)', () => {
it('returns an empty map for an empty resource list', () => {
expect(buildDockerTokenUsage([])).toStrictEqual(new Map());
});
it('skips resources that have no tokenId', () => {
const withoutToken = makeResource({
id: 'no-token',
type: 'docker-host',
platformType: 'docker',
displayName: 'No Token',
platformData: { docker: { hostSourceId: 'rt-9' } },
});
expect(buildDockerTokenUsage([withoutToken])).toStrictEqual(new Map());
});
it('creates a count=1 entry for the first resource with a given tokenId', () => {
const resource = makeResource({
id: 'd-1',
type: 'docker-host',
platformType: 'docker',
displayName: 'Docker One',
platformData: { docker: { hostSourceId: 'rt-1', tokenId: 't1' } },
});
expect(buildDockerTokenUsage([resource])).toStrictEqual(
new Map([['t1', { count: 1, items: [{ id: 'rt-1', label: 'Docker One' }] }]]),
);
});
it('appends a distinct item and increments the count for a shared tokenId', () => {
const a = makeResource({
id: 'd-1',
type: 'docker-host',
platformType: 'docker',
displayName: 'Docker One',
platformData: { docker: { hostSourceId: 'rt-1', tokenId: 't1' } },
});
const b = makeResource({
id: 'd-2',
type: 'docker-host',
platformType: 'docker',
displayName: 'Docker Two',
platformData: { docker: { hostSourceId: 'rt-2', tokenId: 't1' } },
});
expect(buildDockerTokenUsage([a, b])).toStrictEqual(
new Map([
[
't1',
{
count: 2,
items: [
{ id: 'rt-1', label: 'Docker One' },
{ id: 'rt-2', label: 'Docker Two' },
],
},
],
]),
);
});
it('no-ops when a second resource maps to an already-recorded item id (dedup)', () => {
// Both resources resolve to dockerActionId 'rt-shared' (same hostSourceId),
// so appendUsageEntry's duplicate-item guard keeps count at 1 and preserves
// the first-seen label.
const a = makeResource({
id: 'd-1',
type: 'docker-host',
platformType: 'docker',
displayName: 'Docker One',
platformData: { docker: { hostSourceId: 'rt-shared', tokenId: 't1' } },
});
const b = makeResource({
id: 'd-2',
type: 'docker-host',
platformType: 'docker',
displayName: 'Docker Two',
platformData: { docker: { hostSourceId: 'rt-shared', tokenId: 't1' } },
});
expect(buildDockerTokenUsage([a, b])).toStrictEqual(
new Map([
['t1', { count: 1, items: [{ id: 'rt-shared', label: 'Docker One' }] }],
]),
);
});
});
describe('appendUsageEntry (via buildAgentTokenUsage)', () => {
it('creates a count=1 entry for the first agent resource with a tokenId', () => {
const resource = makeResource({
id: 'a-1',
type: 'agent',
platformType: 'agent',
displayName: 'Agent One',
platformData: { agent: { agentId: 'a1', tokenId: 't1' } },
});
expect(buildAgentTokenUsage([resource])).toStrictEqual(
new Map([['t1', { count: 1, items: [{ id: 'a1', label: 'Agent One' }] }]]),
);
});
it('appends a distinct agent item and increments the count', () => {
const a = makeResource({
id: 'a-1',
type: 'agent',
platformType: 'agent',
displayName: 'Agent One',
platformData: { agent: { agentId: 'a1', tokenId: 't1' } },
});
const b = makeResource({
id: 'a-2',
type: 'agent',
platformType: 'agent',
displayName: 'Agent Two',
platformData: { agent: { agentId: 'a2', tokenId: 't1' } },
});
expect(buildAgentTokenUsage([a, b])).toStrictEqual(
new Map([
[
't1',
{
count: 2,
items: [
{ id: 'a1', label: 'Agent One' },
{ id: 'a2', label: 'Agent Two' },
],
},
],
]),
);
});
it('dedups agent resources that resolve to the same actionable agent id', () => {
const a = makeResource({
id: 'a-1',
type: 'agent',
platformType: 'agent',
displayName: 'Agent One',
platformData: { agent: { agentId: 'shared', tokenId: 't1' } },
});
const b = makeResource({
id: 'a-2',
type: 'agent',
platformType: 'agent',
displayName: 'Agent Two',
platformData: { agent: { agentId: 'shared', tokenId: 't1' } },
});
expect(buildAgentTokenUsage([a, b])).toStrictEqual(
new Map([['t1', { count: 1, items: [{ id: 'shared', label: 'Agent One' }] }]]),
);
});
it('skips agent resources that have no tokenId', () => {
const withoutToken = makeResource({
id: 'a-1',
type: 'agent',
platformType: 'agent',
displayName: 'Agent One',
platformData: { agent: { agentId: 'a1' } },
});
expect(buildAgentTokenUsage([withoutToken])).toStrictEqual(new Map());
});
});
// ---- matchesScopePreset -----------------------------------------------------
describe('matchesScopePreset', () => {
it('matches regardless of input order for an exact non-empty set', () => {
expect(matchesScopePreset(['b', 'a'], ['a', 'b'])).toBe(true);
});
it('returns false when the selection is a strict subset of the preset', () => {
expect(matchesScopePreset(['a'], ['a', 'b'])).toBe(false);
});
it('returns false when the selection is a strict superset of the preset', () => {
expect(matchesScopePreset(['a', 'b', 'c'], ['a', 'b'])).toBe(false);
});
it('returns false for a disjoint set of the same length', () => {
expect(matchesScopePreset(['a'], ['b'])).toBe(false);
});
it('returns true for an empty preset against an empty selection', () => {
expect(matchesScopePreset([], [])).toBe(true);
});
it('returns true for an empty preset when the selection is just the wildcard', () => {
expect(matchesScopePreset(['*'], [])).toBe(true);
});
it('returns false for an empty preset when the selection has a non-wildcard scope', () => {
expect(matchesScopePreset(['monitoring:read'], [])).toBe(false);
});
it('ignores the wildcard when comparing against a non-empty preset that otherwise matches', () => {
// The '*' is filtered out before the length/equality check, so ['a','*']
// matches preset ['a'].
expect(matchesScopePreset(['a', '*'], ['a'])).toBe(true);
});
it('still returns false when a wildcard is present but the remaining scope does not match', () => {
expect(matchesScopePreset(['b', '*'], ['a'])).toBe(false);
});
});
@@ -0,0 +1,492 @@
import { describe, expect, it } from 'vitest';
import type { Connection, ConnectionType } from '@/api/connections';
import type {
InfrastructureSystemMemberRow,
InfrastructureSystemRow,
} from '../connectionsTableModel';
import { collectInfrastructureAgentUpdateTargets } from '../infrastructureAgentUpdateCommandsModel';
// ---- Fixtures ---------------------------------------------------------------
// Mirrors the sibling infrastructureAgentUpdateCommandsModel.test.ts factory
// shape. The six functions under test (rowContextLabel, updateInstallFlagsForRow,
// normalizeAgentConnectionID, connectionDisplayName, expectedVersionFor,
// pushTarget) are all module-private, so every branch is driven through the
// single exported orchestrator `collectInfrastructureAgentUpdateTargets`.
const connection = (overrides: Partial<Connection> = {}): Connection => ({
id: 'pve:homelab',
type: 'pve',
name: 'homelab',
address: 'https://pve.lab:8006',
state: 'active',
stateReason: '',
enabled: true,
surfaces: ['vms'],
scope: { vms: true },
lastSeen: new Date().toISOString(),
lastError: null,
source: 'manual',
capabilities: { supportsPause: true, supportsScope: true, supportsTest: true },
...overrides,
});
const emptyFleetRow = {
fleetSignals: [],
fleetHighlights: [],
} satisfies Pick<InfrastructureSystemRow, 'fleetSignals' | 'fleetHighlights'>;
const emptyFleetMember = {
fleetSignals: [],
fleetHighlights: [],
} satisfies Pick<InfrastructureSystemMemberRow, 'fleetSignals' | 'fleetHighlights'>;
const row = (overrides: Partial<InfrastructureSystemRow> = {}): InfrastructureSystemRow => {
const primary = connection();
return {
id: primary.id,
ownerType: 'pve',
name: 'homelab',
subtitle: 'Cluster · 2 nodes',
source: 'both',
host: primary.address,
coverageLabels: ['VMs', 'Host telemetry'],
statusLabel: 'Active',
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 0,
lastActivityText: '1m ago',
...emptyFleetRow,
enabled: true,
canEdit: true,
canPause: true,
canRemove: true,
isAgent: false,
isCluster: false,
attachedConnections: [],
members: [],
connection: primary,
...overrides,
};
};
const member = (
overrides: Partial<InfrastructureSystemMemberRow> = {},
): InfrastructureSystemMemberRow => ({
id: 'node-1',
name: 'node-1',
subtitle: 'Primary node',
source: 'both',
host: 'https://node-1:8006',
coverageLabels: ['Host telemetry'],
statusLabel: 'Active',
statusClassName: 'bg-green-100 text-green-800',
lastActivityText: '1m ago',
...emptyFleetMember,
primary: true,
...overrides,
});
// A stale agent is an agent-type connection that connectionNeedsUpdate() reports
// true for. agentUpdateAvailable: true short-circuits the version comparison in
// connectionNeedsUpdate, so the agent always reaches the target-building body
// of pushTarget unless stated otherwise.
const staleAgent = (overrides: Partial<Connection> = {}): Connection =>
connection({
id: 'agent:agent-1',
type: 'agent',
name: 'agent-1',
address: 'agent-1',
surfaces: ['host'],
scope: { host: true },
source: 'agent',
agentVersion: '5.1.34',
expectedAgentVersion: '6.0.0',
agentUpdateAvailable: true,
agentIdentity: { hostname: 'agent-1', platform: 'linux' },
capabilities: { supportsPause: false, supportsScope: false, supportsTest: false },
...overrides,
});
// ---- normalizeAgentConnectionID --------------------------------------------
// Reachable via scopedAgentIds.map(...) and via the scope filter
// (normalizeAgentConnectionID(target.key)). Branches: the (value || '') falsy
// arm, the !trimmed early return, and both ternary arms of the agent: prefix
// check.
describe('normalizeAgentConnectionID (via scopedAgentIds + scope filter)', () => {
it('drops null/undefined/blank scope entries, keeps already-prefixed ids as-is, and prefixes bare ids', () => {
const agentA = staleAgent({
id: 'agent:host-1',
name: 'host-1',
address: 'host-1',
agentIdentity: { hostname: 'host-1', platform: 'linux' },
});
const agentB = staleAgent({
id: 'host-2',
name: 'host-2',
address: 'host-2',
agentIdentity: { hostname: 'host-2', platform: 'linux' },
});
const agentC = staleAgent({
id: 'agent:host-3',
name: 'host-3',
address: 'host-3',
agentIdentity: { hostname: 'host-3', platform: 'linux' },
});
const targets = collectInfrastructureAgentUpdateTargets(
[row({ attachedConnections: [agentA, agentB, agentC] })],
undefined,
// null/undefined exercise the (value || '') falsy operand; '' and ' '
// exercise the !trimmed early return; 'agent:host-1' exercises the
// already-prefixed ternary arm; 'host-2' exercises the prefix-adding arm.
[null, undefined, '', ' ', 'agent:host-1', 'host-2'] as unknown as readonly string[],
);
// Only host-1 and host-2 are in the normalised scope set; host-3 is filtered
// out. Results are sorted by displayName ascending.
expect(targets.map((target) => target.key)).toEqual(['agent:host-1', 'host-2']);
});
it('retains a target whose raw id is not agent-prefixed when a bare scope entry normalises to the same key', () => {
// target.key 'host-9' normalises to 'agent:host-9' inside the filter; the
// scope entry 'host-9' also normalises to 'agent:host-9', so both sides hit
// the prefix-adding ternary arm and the target is retained.
const agent = staleAgent({
id: 'host-9',
name: 'host-9',
address: 'host-9',
agentIdentity: { hostname: 'host-9', platform: 'linux' },
});
const targets = collectInfrastructureAgentUpdateTargets(
[row({ attachedConnections: [agent] })],
undefined,
['host-9'],
);
expect(targets).toHaveLength(1);
expect(targets[0]?.key).toBe('host-9');
});
});
// ---- updateInstallFlagsForRow -----------------------------------------------
// Reachable via pushTarget -> installFlags. Branches: every switch arm (pve,
// pbs, docker, kubernetes) plus the implicit default (no case matches).
describe('updateInstallFlagsForRow (via target.installFlags)', () => {
it.each([
['pve', ['--enable-proxmox', '--proxmox-type pve']],
['pbs', ['--enable-proxmox', '--proxmox-type pbs']],
['docker', ['--enable-docker']],
['kubernetes', ['--enable-kubernetes']],
['agent', []],
['pmg', []],
['vmware', []],
['truenas', []],
['availability', []],
] as const satisfies ReadonlyArray<[ConnectionType, string[]]>)(
'emits the expected install flags for ownerType %s',
(ownerType, expectedFlags) => {
const agent = staleAgent({
id: `agent:${ownerType}-node`,
name: `${ownerType}-node`,
address: `${ownerType}-node`,
agentIdentity: { hostname: `${ownerType}-node`, platform: 'linux' },
});
const targets = collectInfrastructureAgentUpdateTargets([
row({ ownerType, attachedConnections: [agent] }),
]);
expect(targets).toHaveLength(1);
expect(targets[0]?.installFlags).toStrictEqual(expectedFlags);
},
);
});
// ---- connectionDisplayName --------------------------------------------------
// Reachable via pushTarget -> displayName. Branches: every operand of the
// short-circuit || chain (hostname, name, address, id) plus the optional-chain
// miss when agentIdentity is undefined.
describe('connectionDisplayName (via target.displayName)', () => {
it('prefers the agentIdentity hostname when it is present and non-empty', () => {
const agent = staleAgent({
id: 'agent:h1',
name: 'named-host',
address: '10.0.0.1',
agentIdentity: { hostname: 'real-hostname', platform: 'linux' },
});
const targets = collectInfrastructureAgentUpdateTargets([row({ attachedConnections: [agent] })]);
expect(targets[0]?.displayName).toBe('real-hostname');
});
it('falls back to connection.name when agentIdentity is undefined (optional-chain miss)', () => {
const agent = staleAgent({
id: 'agent:h2',
name: 'named-host',
address: '10.0.0.2',
agentIdentity: undefined,
});
const targets = collectInfrastructureAgentUpdateTargets([row({ attachedConnections: [agent] })]);
expect(targets[0]?.displayName).toBe('named-host');
});
it('falls back to connection.name when hostname trims to empty', () => {
const agent = staleAgent({
id: 'agent:h3',
name: 'named-host',
address: '10.0.0.3',
agentIdentity: { hostname: ' ', platform: 'linux' },
});
const targets = collectInfrastructureAgentUpdateTargets([row({ attachedConnections: [agent] })]);
expect(targets[0]?.displayName).toBe('named-host');
});
it('falls back to address when both hostname and name trim to empty', () => {
const agent = staleAgent({
id: 'agent:h4',
name: ' ',
address: '10.0.0.4',
agentIdentity: { hostname: ' ', platform: 'linux' },
});
const targets = collectInfrastructureAgentUpdateTargets([row({ attachedConnections: [agent] })]);
expect(targets[0]?.displayName).toBe('10.0.0.4');
});
it('falls back to the connection id when hostname, name, and address are all blank', () => {
const agent = staleAgent({
id: 'agent:h5',
name: '',
address: '',
agentIdentity: { hostname: '', platform: 'linux' },
});
const targets = collectInfrastructureAgentUpdateTargets([row({ attachedConnections: [agent] })]);
expect(targets[0]?.displayName).toBe('agent:h5');
});
});
// ---- rowContextLabel --------------------------------------------------------
// Reachable via pushTarget -> contextLabel. Branches: the cluster arm
// (isCluster && name.trim()), the ownerType==='agent' arm, and the three-way
// return fallback (row.name.trim() || connection.name || ownerType).
describe('rowContextLabel (via target.contextLabel)', () => {
it('returns the raw (untrimmed) row name on the cluster arm', () => {
const agent = staleAgent({ id: 'agent:c1' });
const targets = collectInfrastructureAgentUpdateTargets([
row({ isCluster: true, name: ' my-cluster ', attachedConnections: [agent] }),
]);
// The cluster arm returns row.name verbatim (no .trim()), unlike the
// non-cluster return arm. See GLM_REPORT for the suspected inconsistency.
expect(targets[0]?.contextLabel).toBe(' my-cluster ');
});
it('falls through the cluster arm when the cluster name trims to empty', () => {
const agent = staleAgent({ id: 'agent:c2' });
const targets = collectInfrastructureAgentUpdateTargets([
row({ isCluster: true, name: ' ', attachedConnections: [agent] }),
]);
// isCluster true but name.trim() falsy -> cluster condition fails; ownerType
// pve (not agent); row.name.trim() falsy; falls back to connection.name.
expect(targets[0]?.contextLabel).toBe('homelab');
});
it("returns 'Machine' for an agent-type row", () => {
const agent = staleAgent({ id: 'agent:c3' });
const targets = collectInfrastructureAgentUpdateTargets([
row({ ownerType: 'agent', isAgent: true, name: 'whatever', attachedConnections: [agent] }),
]);
expect(targets[0]?.contextLabel).toBe('Machine');
});
it('returns the trimmed row name for a non-cluster, non-agent row with a name', () => {
const agent = staleAgent({ id: 'agent:c4' });
const targets = collectInfrastructureAgentUpdateTargets([
row({ ownerType: 'docker', name: ' docker-host ', attachedConnections: [agent] }),
]);
expect(targets[0]?.contextLabel).toBe('docker-host');
});
it('falls back to connection.name when the row name is blank', () => {
const primary = connection({ name: 'fallback-name' });
const agent = staleAgent({ id: 'agent:c5' });
const targets = collectInfrastructureAgentUpdateTargets([
row({ ownerType: 'docker', name: '', connection: primary, attachedConnections: [agent] }),
]);
expect(targets[0]?.contextLabel).toBe('fallback-name');
});
it('falls back to ownerType when both row name and connection name are blank', () => {
const primary = connection({ name: '' });
const agent = staleAgent({ id: 'agent:c6' });
const targets = collectInfrastructureAgentUpdateTargets([
row({ ownerType: 'docker', name: '', connection: primary, attachedConnections: [agent] }),
]);
expect(targets[0]?.contextLabel).toBe('docker');
});
});
// ---- expectedVersionFor -----------------------------------------------------
// Reachable via pushTarget -> expectedVersion. Branches: every operand of the
// short-circuit || chain (expectedAgentVersion, formatAgentVersionDisplay,
// undefined).
describe('expectedVersionFor (via target.expectedVersion)', () => {
it('returns the trimmed expectedAgentVersion when the connection carries one', () => {
const agent = staleAgent({
id: 'agent:e1',
expectedAgentVersion: ' 6.0.1 ',
agentUpdateAvailable: true,
});
const targets = collectInfrastructureAgentUpdateTargets([row({ attachedConnections: [agent] })]);
expect(targets[0]?.expectedVersion).toBe('6.0.1');
});
it('formats targetVersion when expectedAgentVersion is absent but targetVersion parses', () => {
const agent = staleAgent({
id: 'agent:e2',
expectedAgentVersion: undefined,
agentVersion: '6.0.0-rc.5',
agentUpdateAvailable: false,
});
const targets = collectInfrastructureAgentUpdateTargets(
[row({ attachedConnections: [agent] })],
'6.0.0-rc.6',
);
// connectionNeedsUpdate compares 6.0.0-rc.5 < 6.0.0-rc.6 -> true;
// formatAgentVersionDisplay normalises to a leading 'v'.
expect(targets[0]?.expectedVersion).toBe('v6.0.0-rc.6');
});
it('returns undefined when neither expectedAgentVersion nor a targetVersion is present', () => {
const agent = staleAgent({
id: 'agent:e3',
expectedAgentVersion: undefined,
agentUpdateAvailable: true,
});
const targets = collectInfrastructureAgentUpdateTargets([row({ attachedConnections: [agent] })]);
expect(targets[0]?.expectedVersion).toBeUndefined();
});
it('returns undefined when expectedAgentVersion is absent and targetVersion is unparseable', () => {
const agent = staleAgent({
id: 'agent:e4',
expectedAgentVersion: undefined,
agentUpdateAvailable: true,
});
const targets = collectInfrastructureAgentUpdateTargets(
[row({ attachedConnections: [agent] })],
'not-a-version',
);
// agentUpdateAvailable true keeps the agent eligible; formatAgentVersionDisplay
// ('not-a-version') returns '' so the final || undefined operand wins.
expect(targets).toHaveLength(1);
expect(targets[0]?.expectedVersion).toBeUndefined();
});
});
// ---- pushTarget -------------------------------------------------------------
// Branches: the !connection guard, the type!=='agent' guard, the
// !connectionNeedsUpdate guard, the has(key) dedupe guard, and the happy-path
// set.
describe('pushTarget (guard + dedupe branches)', () => {
it('skips a member whose agentConnection is undefined (the !connection guard)', () => {
const targets = collectInfrastructureAgentUpdateTargets([
row({ members: [member({ agentConnection: undefined })] }),
]);
expect(targets).toEqual([]);
});
it('skips a non-agent primary connection (the connection.type !== "agent" guard)', () => {
// Default row.connection is the pve primary; no attached/members agents.
const targets = collectInfrastructureAgentUpdateTargets([row()]);
expect(targets).toEqual([]);
});
it('skips a current agent whose version equals the target (the !connectionNeedsUpdate guard)', () => {
const current = staleAgent({
id: 'agent:current',
agentVersion: '6.0.0',
expectedAgentVersion: undefined,
agentUpdateAvailable: false,
});
const targets = collectInfrastructureAgentUpdateTargets(
[row({ attachedConnections: [current] })],
'v6.0.0',
);
// compareAgentVersions('6.0.0', 'v6.0.0') === 0 -> not < 0 -> needsUpdate
// false -> pushTarget returns early.
expect(targets).toEqual([]);
});
it('dedupes an agent that appears in both attachedConnections and a member (the has(key) guard)', () => {
const shared = staleAgent({
id: 'agent:shared',
name: 'shared',
address: 'shared',
agentIdentity: { hostname: 'shared', platform: 'linux' },
});
const targets = collectInfrastructureAgentUpdateTargets([
row({
attachedConnections: [shared],
members: [member({ agentConnection: shared })],
}),
]);
expect(targets).toHaveLength(1);
expect(targets[0]?.key).toBe('agent:shared');
});
it('pushes when the primary connection itself is a stale agent (happy path via row.connection)', () => {
const agent = staleAgent({
id: 'agent:primary-agent',
name: 'primary-agent',
address: 'primary-agent',
agentIdentity: { hostname: 'primary-agent', platform: 'linux' },
});
const targets = collectInfrastructureAgentUpdateTargets([
row({ ownerType: 'agent', isAgent: true, connection: agent }),
]);
expect(targets).toHaveLength(1);
expect(targets[0]?.key).toBe('agent:primary-agent');
});
});
@@ -0,0 +1,609 @@
/**
* Branch-coverage tests for the exported helpers in reportingSchedulesModel.
* Each block targets a specific function and drives both arms of every
* conditional / optional-chain / nullish-coalescing branch that is reachable
* from the public surface.
*/
import { describe, expect, it } from 'vitest';
import type { SelectedResource } from '@/components/Settings/ResourcePicker';
import {
DEFAULT_REPORT_SCHEDULE_FORM,
buildReportSchedulePayload,
formatReportScheduleTime,
normalizeReportSchedule,
parseCommaList,
parseReportSchedulesResponse,
reportScheduleCadenceLabel,
reportScheduleDeliveryLabel,
reportScheduleLastRunLabel,
reportScheduleScopeLabel,
scheduleToForm,
scheduleToSelectedResources,
type ReportSchedule,
type ReportScheduleFormState,
} from '../reportingSchedulesModel';
// ---- Fixtures ---------------------------------------------------------------
const makeSchedule = (overrides: Partial<ReportSchedule> = {}): ReportSchedule => ({
id: 'sched-1',
name: 'Nightly ops digest',
enabled: true,
cadence: {
type: 'monthly',
day_of_month: 15,
weekday: 'monday',
time: '09:00',
timezone: 'UTC',
},
scope: {
resources: [],
tags: [],
},
format: 'pdf',
delivery: {
method: 'email',
to: [],
attach: true,
save_to_disk: true,
},
retention_count: 12,
...overrides,
});
const makeForm = (overrides: Partial<ReportScheduleFormState> = {}): ReportScheduleFormState => ({
...DEFAULT_REPORT_SCHEDULE_FORM(),
...overrides,
});
const makeResource = (overrides: Partial<SelectedResource> = {}): SelectedResource => ({
id: 'res-1',
type: 'vm',
name: 'web-01',
...overrides,
});
// ---- parseReportSchedulesResponse ------------------------------------------
describe('parseReportSchedulesResponse', () => {
it('returns an empty array for a null input (!value arm)', () => {
expect(parseReportSchedulesResponse(null)).toStrictEqual([]);
});
it('returns an empty array for an undefined input (!value arm)', () => {
expect(parseReportSchedulesResponse(undefined)).toStrictEqual([]);
});
it('returns an empty array for a non-object primitive (typeof !== "object" arm)', () => {
expect(parseReportSchedulesResponse('not-an-object')).toStrictEqual([]);
expect(parseReportSchedulesResponse(42)).toStrictEqual([]);
});
it('returns an empty array when the value is an object without a schedules field', () => {
expect(parseReportSchedulesResponse({ foo: 'bar' })).toStrictEqual([]);
});
it('returns an empty array when schedules is present but not an array', () => {
expect(parseReportSchedulesResponse({ schedules: 'nope' })).toStrictEqual([]);
expect(parseReportSchedulesResponse({ schedules: null })).toStrictEqual([]);
});
it('maps each entry through normalizeReportSchedule (Array.isArray truthy arm)', () => {
const result = parseReportSchedulesResponse({
schedules: [
makeSchedule({ id: 'a', enabled: false }),
makeSchedule({ id: 'b', format: 'csv' }),
],
});
expect(result).toHaveLength(2);
expect(result[0]?.id).toBe('a');
expect(result[0]?.enabled).toBe(false);
expect(result[1]?.format).toBe('csv');
});
it('normalizes a malformed schedule carried inside the response', () => {
const result = parseReportSchedulesResponse({
schedules: [
{
id: 'c',
name: 'sparse',
enabled: true,
scope: {},
format: 'pdf',
delivery: { method: 'email', attach: true, save_to_disk: true },
} as unknown as ReportSchedule,
],
});
expect(result[0]?.cadence).toStrictEqual({
type: 'monthly',
day_of_month: 1,
weekday: 'monday',
time: '09:00',
timezone: 'UTC',
});
});
});
// ---- normalizeReportSchedule -----------------------------------------------
describe('normalizeReportSchedule', () => {
it('passes through fully-populated truthy values unchanged on the happy path', () => {
const schedule = makeSchedule({
enabled: true,
cadence: { type: 'weekly', day_of_month: 20, weekday: 'friday', time: '17:30', timezone: 'Europe/Paris' },
scope: { resources: [{ resourceType: 'vm', resourceId: 'v1', name: 'web' }], tags: ['tier1'] },
format: 'csv',
delivery: { method: 'disk', to: ['ops@x'], attach: true, save_to_disk: true },
retention_count: 7,
});
expect(normalizeReportSchedule(schedule)).toMatchObject({
enabled: true,
cadence: { type: 'weekly', day_of_month: 20, weekday: 'friday', time: '17:30', timezone: 'Europe/Paris' },
format: 'csv',
delivery: { method: 'disk', to: ['ops@x'], attach: true, save_to_disk: true },
retention_count: 7,
});
});
it('coerces enabled to false only when explicitly false, and to true otherwise', () => {
expect(normalizeReportSchedule(makeSchedule({ enabled: false })).enabled).toBe(false);
// The `!== false` arm: any non-false value becomes true.
expect(
normalizeReportSchedule(makeSchedule({ enabled: 'true' as unknown as boolean })).enabled,
).toBe(true);
});
it('keeps day_of_month of 0 (nullish coalescing does not default on falsy numbers)', () => {
expect(
normalizeReportSchedule(makeSchedule({ cadence: { type: 'monthly', day_of_month: 0, time: '09:00', timezone: 'UTC' } })).cadence
.day_of_month,
).toBe(0);
});
it('applies every cadence default when cadence is entirely missing', () => {
const schedule = {
id: 'x',
name: 'x',
enabled: true,
scope: {},
format: 'pdf',
delivery: { method: 'email', attach: true, save_to_disk: true },
} as unknown as ReportSchedule;
expect(normalizeReportSchedule(schedule).cadence).toStrictEqual({
type: 'monthly',
day_of_month: 1,
weekday: 'monday',
time: '09:00',
timezone: 'UTC',
});
});
it('classifies an unknown cadence type as monthly', () => {
const schedule = makeSchedule({
cadence: { type: 'daily' as unknown as 'monthly', day_of_month: 3, time: '08:00', timezone: 'UTC' },
});
expect(normalizeReportSchedule(schedule).cadence.type).toBe('monthly');
});
it('defaults weekday/time/timezone via || when they are empty strings', () => {
const schedule = makeSchedule({
cadence: { type: 'monthly', day_of_month: 1, weekday: '', time: '', timezone: '', day_of_month_ignored: true } as unknown as ReportSchedule['cadence'],
});
expect(normalizeReportSchedule(schedule).cadence).toMatchObject({
weekday: 'monday',
time: '09:00',
timezone: 'UTC',
});
});
it('keeps scope arrays when they are arrays, and replaces them when not', () => {
const kept = normalizeReportSchedule(
makeSchedule({ scope: { resources: [{ resourceType: 'vm', resourceId: 'v1' }], tags: ['a'] } }),
);
expect(kept.scope.resources).toHaveLength(1);
expect(kept.scope.tags).toEqual(['a']);
const replaced = normalizeReportSchedule(
makeSchedule({ scope: { resources: null as unknown as [], tags: 'nope' as unknown as string[] } }),
);
expect(replaced.scope.resources).toStrictEqual([]);
expect(replaced.scope.tags).toStrictEqual([]);
});
it('classifies format as csv only for csv, pdf otherwise', () => {
expect(normalizeReportSchedule(makeSchedule({ format: 'csv' })).format).toBe('csv');
expect(
normalizeReportSchedule(makeSchedule({ format: 'xlsx' as unknown as 'pdf' })).format,
).toBe('pdf');
});
it('classifies delivery method as disk only for disk, email otherwise', () => {
expect(normalizeReportSchedule(makeSchedule({ delivery: { method: 'disk', attach: true, save_to_disk: true } })).delivery.method).toBe(
'disk',
);
expect(
normalizeReportSchedule(
makeSchedule({ delivery: { method: 'carrier-pigeon' as unknown as 'email', attach: true, save_to_disk: true } }),
).delivery.method,
).toBe('email');
});
it('defaults delivery.to to [] when missing and keeps it when an array', () => {
expect(
normalizeReportSchedule(
makeSchedule({ delivery: { method: 'email', to: undefined as unknown as string[], attach: true, save_to_disk: true } }),
).delivery.to,
).toStrictEqual([]);
expect(
normalizeReportSchedule(makeSchedule({ delivery: { method: 'email', to: ['a@b'], attach: true, save_to_disk: true } })).delivery.to,
).toEqual(['a@b']);
});
it('coerces attach and save_to_disk to false only when explicitly false', () => {
const allFalse = normalizeReportSchedule(
makeSchedule({ delivery: { method: 'email', attach: false, save_to_disk: false } }),
);
expect(allFalse.delivery.attach).toBe(false);
expect(allFalse.delivery.save_to_disk).toBe(false);
const defaults = normalizeReportSchedule(
makeSchedule({ delivery: { method: 'email', attach: undefined as unknown as boolean, save_to_disk: undefined as unknown as boolean } }),
);
expect(defaults.delivery.attach).toBe(true);
expect(defaults.delivery.save_to_disk).toBe(true);
});
it('defaults retention_count to 12 when missing', () => {
const schedule = { ...makeSchedule(), retention_count: undefined } as unknown as ReportSchedule;
delete (schedule as Partial<ReportSchedule>).retention_count;
expect(normalizeReportSchedule(schedule).retention_count).toBe(12);
});
});
// ---- scheduleToForm ---------------------------------------------------------
//
// scheduleToForm consumes the *normalized* schedule, whose fields are always
// populated by normalizeReportSchedule. The internal `?? 1` / `|| 'monday'` /
// `?? []` defensives therefore never fire from the public surface; here we
// cover the reachable mapping logic for both cadence arms.
describe('scheduleToForm', () => {
it('maps a populated monthly schedule into the form shape', () => {
const schedule = makeSchedule({
cadence: { type: 'monthly', day_of_month: 27, time: '23:00', timezone: 'UTC' },
delivery: { method: 'email', to: ['a@b.com', 'c@d.com'], attach: true, save_to_disk: true },
scope: { resources: [], tags: ['tier1', 'tier2'] },
retention_count: 5,
});
expect(scheduleToForm(schedule)).toStrictEqual({
id: 'sched-1',
name: 'Nightly ops digest',
enabled: true,
cadenceType: 'monthly',
dayOfMonth: 27,
weekday: 'monday',
time: '23:00',
timezone: 'UTC',
format: 'pdf',
deliveryMethod: 'email',
recipients: 'a@b.com, c@d.com',
attach: true,
saveToDisk: true,
tagFilter: 'tier1, tier2',
retentionCount: 5,
});
});
it('maps a weekly schedule and surfaces the weekday field', () => {
const schedule = makeSchedule({
cadence: { type: 'weekly', weekday: 'friday', time: '17:30', timezone: 'Europe/Paris' },
});
const form = scheduleToForm(schedule);
expect(form.cadenceType).toBe('weekly');
expect(form.weekday).toBe('friday');
expect(form.dayOfMonth).toBe(1); // monthly default carried through normalization
expect(form.timezone).toBe('Europe/Paris');
});
it('joins recipients and tags into empty strings when both lists are empty', () => {
const form = scheduleToForm(makeSchedule());
expect(form.recipients).toBe('');
expect(form.tagFilter).toBe('');
});
});
// ---- scheduleToSelectedResources -------------------------------------------
describe('scheduleToSelectedResources', () => {
it('returns an empty array when scope.resources is undefined (?? [] arm)', () => {
const schedule = makeSchedule({ scope: {} });
expect(scheduleToSelectedResources(schedule)).toStrictEqual([]);
});
it('maps each resource, using the explicit name when present', () => {
const schedule = makeSchedule({
scope: {
resources: [
{ resourceType: 'vm', resourceId: 'v1', name: 'web-01' },
{ resourceType: 'agent', resourceId: 'a1', name: 'host-01' },
],
},
});
expect(scheduleToSelectedResources(schedule)).toStrictEqual([
{ id: 'v1', type: 'vm', name: 'web-01' },
{ id: 'a1', type: 'agent', name: 'host-01' },
]);
});
it('falls back to resourceId when name is missing (|| arm)', () => {
const schedule = makeSchedule({
scope: { resources: [{ resourceType: 'vm', resourceId: 'v9' }] },
});
expect(scheduleToSelectedResources(schedule)).toStrictEqual([
{ id: 'v9', type: 'vm', name: 'v9' },
]);
});
it('falls back to resourceId when name is an empty string', () => {
const schedule = makeSchedule({
scope: { resources: [{ resourceType: 'vm', resourceId: 'v9', name: '' }] },
});
expect(scheduleToSelectedResources(schedule)[0]?.name).toBe('v9');
});
});
// ---- buildReportSchedulePayload --------------------------------------------
describe('buildReportSchedulePayload', () => {
it('emits day_of_month and omits weekday for a monthly form', () => {
const payload = buildReportSchedulePayload(makeForm({ cadenceType: 'monthly', dayOfMonth: 12 }), [
makeResource(),
]);
expect(payload.cadence).toMatchObject({ type: 'monthly', day_of_month: 12, weekday: undefined });
});
it('emits weekday and omits day_of_month for a weekly form', () => {
const payload = buildReportSchedulePayload(makeForm({ cadenceType: 'weekly', weekday: 'wednesday' }), []);
expect(payload.cadence).toMatchObject({ type: 'weekly', weekday: 'wednesday', day_of_month: undefined });
});
it('falls back to UTC when the timezone trims to empty', () => {
const payload = buildReportSchedulePayload(makeForm({ timezone: ' ' }), []);
expect(payload.cadence.timezone).toBe('UTC');
});
it('trims the schedule name and maps resources into the scope', () => {
const payload = buildReportSchedulePayload(
makeForm({ name: ' trimmed ' }),
[makeResource({ id: 'r1', type: 'agent', name: 'host' })],
);
expect(payload.name).toBe('trimmed');
expect(payload.scope.resources).toStrictEqual([
{ resourceType: 'agent', resourceId: 'r1', name: 'host' },
]);
});
it('parses recipients and tag filters via parseCommaList', () => {
const payload = buildReportSchedulePayload(
makeForm({ recipients: 'a@b.com, c@d.com, a@b.com', tagFilter: 'tier1,tier2' }),
[],
);
expect(payload.delivery.to).toEqual(['a@b.com', 'c@d.com']);
expect(payload.scope.tags).toEqual(['tier1', 'tier2']);
});
it('forwards format, delivery flags, and retention_count verbatim', () => {
const payload = buildReportSchedulePayload(
makeForm({ format: 'csv', deliveryMethod: 'disk', attach: false, saveToDisk: false, retentionCount: 3 }),
[],
);
expect(payload.format).toBe('csv');
expect(payload.delivery).toStrictEqual({ method: 'disk', to: [], attach: false, save_to_disk: false });
expect(payload.retention_count).toBe(3);
});
});
// ---- parseCommaList ---------------------------------------------------------
describe('parseCommaList', () => {
it('returns an empty array for an empty string', () => {
expect(parseCommaList('')).toStrictEqual([]);
});
it('returns a single trimmed item', () => {
expect(parseCommaList(' alpha ')).toStrictEqual(['alpha']);
});
it('splits, trims, and preserves order for multiple items', () => {
expect(parseCommaList('a, b ,c')).toStrictEqual(['a', 'b', 'c']);
});
it('drops empty entries produced by trailing/leading/double commas', () => {
expect(parseCommaList(',a,,b,')).toStrictEqual(['a', 'b']);
});
it('dedupes case-insensitively while keeping the first-seen casing', () => {
expect(parseCommaList('Alpha, ALPHA, alpha, Beta, beta')).toStrictEqual(['Alpha', 'Beta']);
});
});
// ---- reportScheduleCadenceLabel --------------------------------------------
describe('reportScheduleCadenceLabel', () => {
it('labels a monthly schedule with its day-of-month and time', () => {
const schedule = makeSchedule({
cadence: { type: 'monthly', day_of_month: 15, time: '09:00', timezone: 'UTC' },
});
expect(reportScheduleCadenceLabel(schedule)).toBe('Monthly on day 15 at 09:00');
});
it('preserves a day_of_month of 0 in the monthly label', () => {
const schedule = makeSchedule({
cadence: { type: 'monthly', day_of_month: 0, time: '09:00', timezone: 'UTC' },
});
expect(reportScheduleCadenceLabel(schedule)).toBe('Monthly on day 0 at 09:00');
});
it('labels a weekly schedule with the prettified weekday and time', () => {
const schedule = makeSchedule({
cadence: { type: 'weekly', weekday: 'friday', time: '17:30', timezone: 'UTC' },
});
expect(reportScheduleCadenceLabel(schedule)).toBe('Friday at 17:30');
});
it('falls back to the raw weekday string when it is not in WEEKDAY_LABELS', () => {
const schedule = makeSchedule({
cadence: { type: 'weekly', weekday: 'funday', time: '08:00', timezone: 'UTC' },
});
expect(reportScheduleCadenceLabel(schedule)).toBe('funday at 08:00');
});
});
// ---- reportScheduleScopeLabel ----------------------------------------------
describe('reportScheduleScopeLabel', () => {
it('returns "No scope" when both resources and tags are absent (parts.length falsy arm)', () => {
expect(reportScheduleScopeLabel(makeSchedule({ scope: {} }))).toBe('No scope');
});
it('renders the singular form for exactly one resource', () => {
expect(
reportScheduleScopeLabel(
makeSchedule({ scope: { resources: [{ resourceType: 'vm', resourceId: 'v1' }] } }),
),
).toBe('1 resource');
});
it('renders the plural form for multiple resources', () => {
expect(
reportScheduleScopeLabel(
makeSchedule({
scope: {
resources: [
{ resourceType: 'vm', resourceId: 'v1' },
{ resourceType: 'vm', resourceId: 'v2' },
],
},
}),
),
).toBe('2 resources');
});
it('renders the singular vs plural form for tags', () => {
expect(reportScheduleScopeLabel(makeSchedule({ scope: { tags: ['tier1'] } }))).toBe('1 tag');
expect(reportScheduleScopeLabel(makeSchedule({ scope: { tags: ['tier1', 'tier2'] } }))).toBe(
'2 tags',
);
});
it('joins resources and tags with a comma', () => {
expect(
reportScheduleScopeLabel(
makeSchedule({
scope: {
resources: [{ resourceType: 'vm', resourceId: 'v1' }],
tags: ['tier1', 'tier2'],
},
}),
),
).toBe('1 resource, 2 tags');
});
it('treats undefined resources as zero (?.length ?? 0 arm)', () => {
expect(reportScheduleScopeLabel(makeSchedule({ scope: { tags: ['t1'] } }))).toBe('1 tag');
});
});
// ---- reportScheduleDeliveryLabel -------------------------------------------
describe('reportScheduleDeliveryLabel', () => {
it('returns "Save to disk" for the disk method', () => {
expect(
reportScheduleDeliveryLabel(makeSchedule({ delivery: { method: 'disk', attach: true, save_to_disk: true } })),
).toBe('Save to disk');
});
it('renders the singular recipient form for one email recipient', () => {
expect(
reportScheduleDeliveryLabel(
makeSchedule({ delivery: { method: 'email', to: ['ops@x'], attach: true, save_to_disk: true } }),
),
).toBe('1 email recipient');
});
it('renders the plural recipient form for multiple email recipients', () => {
expect(
reportScheduleDeliveryLabel(
makeSchedule({ delivery: { method: 'email', to: ['a@x', 'b@x'], attach: true, save_to_disk: true } }),
),
).toBe('2 email recipients');
});
it('falls back to the generic email copy when there are no recipients', () => {
expect(
reportScheduleDeliveryLabel(
makeSchedule({ delivery: { method: 'email', to: [], attach: true, save_to_disk: true } }),
),
).toBe('Email config recipients');
});
});
// ---- reportScheduleLastRunLabel --------------------------------------------
describe('reportScheduleLastRunLabel', () => {
it('returns "Not run yet" when last_run_status is empty (!status arm)', () => {
expect(reportScheduleLastRunLabel(makeSchedule({ last_run_status: '' }))).toBe('Not run yet');
});
it('returns "Not run yet" when last_run_status is undefined', () => {
const schedule = makeSchedule();
delete schedule.last_run_status;
expect(reportScheduleLastRunLabel(schedule)).toBe('Not run yet');
});
it('returns "Last run OK" for the ok status', () => {
expect(reportScheduleLastRunLabel(makeSchedule({ last_run_status: 'ok' }))).toBe('Last run OK');
});
it('surfaces last_error for a failed run', () => {
expect(
reportScheduleLastRunLabel(makeSchedule({ last_run_status: 'failed', last_error: 'timeout' })),
).toBe('Failed: timeout');
});
it('falls back to the generic failed copy when last_error is missing', () => {
const schedule = makeSchedule({ last_run_status: 'failed' });
delete schedule.last_error;
expect(reportScheduleLastRunLabel(schedule)).toBe('Last run failed');
});
});
// ---- formatReportScheduleTime ----------------------------------------------
describe('formatReportScheduleTime', () => {
it('returns an empty string for a falsy value', () => {
expect(formatReportScheduleTime(undefined)).toBe('');
expect(formatReportScheduleTime('')).toBe('');
});
it('returns an empty string for an unparseable date (NaN arm)', () => {
expect(formatReportScheduleTime('not-a-date')).toBe('');
});
it('formats a valid timestamp into a localized, non-empty string', () => {
const formatted = formatReportScheduleTime('2026-07-15T10:30:00Z');
// The exact locale formatting is environment-dependent, but a real
// localized date string always contains digits and is longer than the
// empty fallback returned for invalid input.
expect(formatted).toMatch(/\d/);
expect(formatted.length).toBeGreaterThan(4);
});
it('distinguishes two different dates in the formatted output', () => {
const jan = formatReportScheduleTime('2026-01-05T10:30:00Z');
const jul = formatReportScheduleTime('2026-07-15T10:30:00Z');
expect(jan).not.toBe(jul);
expect(jan).not.toBe('');
expect(jul).not.toBe('');
});
});
@@ -0,0 +1,551 @@
import { describe, expect, it } from 'vitest';
import type { SettingsTab } from '../settingsNavigationModel';
import {
isSettingsNavItemLocked,
shouldBlockSettingsRouteItem,
shouldHideSettingsNavItem,
type SettingsNavVisibilityContext,
} from '../settingsNavVisibility';
// ---- Helpers ---------------------------------------------------------------
// Mirrors the sibling `settingsNavigation.integration.test.tsx` hasFeatures
// factory and builds a fully-typed SettingsNavVisibilityContext with sensible
// defaults so each case only overrides the branch-relevant field.
const hasFeatures =
(features: string[]) =>
(feature: string): boolean =>
features.includes(feature);
const createContext = (
overrides: Partial<SettingsNavVisibilityContext> = {},
): SettingsNavVisibilityContext => ({
hasFeature: hasFeatures([]),
runtimeCapabilitiesLoaded: () => true,
hostedModeEnabled: false,
...overrides,
});
// A SettingsTab value that is NOT present in the nav catalog, used to drive
// the defensive `!item` early-returns via a controlled cast.
const UNKNOWN_TAB = 'nonexistent-tab' as unknown as SettingsTab;
// ---- shouldHideSettingsNavItem --------------------------------------------
describe('shouldHideSettingsNavItem', () => {
it('returns false for an unknown tab (defensive !item branch)', () => {
expect(shouldHideSettingsNavItem(UNKNOWN_TAB, createContext())).toBe(false);
});
it('returns false for a tab whose item has no visibility gates', () => {
expect(shouldHideSettingsNavItem('system-general', createContext())).toBe(false);
});
describe('hostedOnly gate (lines 53-55)', () => {
it('hides a hostedOnly tab when hosted mode is disabled', () => {
expect(
shouldHideSettingsNavItem(
'organization-billing-admin',
createContext({
hasFeature: hasFeatures(['multi_tenant']),
hostedModeEnabled: false,
settingsCapabilities: { billingAdmin: true },
settingsCapabilitiesResolved: true,
}),
),
).toBe(true);
});
it('skips the hostedOnly gate when hosted mode is enabled', () => {
// hostedOnly + hostedModeEnabled true -> gate skipped; multi_tenant present
// and capability granted -> no other gate fires -> not hidden.
expect(
shouldHideSettingsNavItem(
'organization-billing-admin',
createContext({
hasFeature: hasFeatures(['multi_tenant']),
hostedModeEnabled: true,
settingsCapabilities: { billingAdmin: true },
settingsCapabilitiesResolved: true,
presentationPolicyResolved: true,
}),
),
).toBe(false);
});
});
describe('hideWhenOrganizationHidden gate (lines 57-65)', () => {
it('hides when presentation policy is unresolved', () => {
expect(
shouldHideSettingsNavItem(
'organization-overview',
createContext({
hasFeature: hasFeatures(['multi_tenant']),
presentationPolicyResolved: false,
}),
),
).toBe(true);
});
it('hides when organizations are hidden by presentation policy', () => {
expect(
shouldHideSettingsNavItem(
'organization-overview',
createContext({
hasFeature: hasFeatures(['multi_tenant']),
presentationPolicyResolved: true,
presentationPolicyHidesOrganizations: true,
}),
),
).toBe(true);
});
it('falls through when organizations are resolved and visible', () => {
expect(
shouldHideSettingsNavItem(
'organization-overview',
createContext({
hasFeature: hasFeatures(['multi_tenant']),
presentationPolicyResolved: true,
presentationPolicyHidesOrganizations: false,
}),
),
).toBe(false);
});
});
describe('hideWhenCommercialHidden gate (lines 67-75)', () => {
it('hides a commercial-hidden tab when policy is unresolved', () => {
expect(
shouldHideSettingsNavItem(
'system-billing',
createContext({ presentationPolicyResolved: false }),
),
).toBe(true);
});
it('hides a commercial-hidden tab when commercial is hidden', () => {
expect(
shouldHideSettingsNavItem(
'system-billing',
createContext({
presentationPolicyResolved: true,
presentationPolicyHidesCommercial: true,
}),
),
).toBe(true);
});
it('falls through when commercial is resolved and visible', () => {
expect(
shouldHideSettingsNavItem(
'system-billing',
createContext({
presentationPolicyResolved: true,
presentationPolicyHidesCommercial: false,
}),
),
).toBe(false);
});
});
describe('hideWhenDemoMode gate (lines 77-85)', () => {
it('hides a demo-hidden tab when policy is unresolved', () => {
expect(
shouldHideSettingsNavItem(
'support-diagnostics',
createContext({ presentationPolicyResolved: false }),
),
).toBe(true);
});
it('hides a demo-hidden tab in demo mode', () => {
expect(
shouldHideSettingsNavItem(
'support-diagnostics',
createContext({
presentationPolicyResolved: true,
presentationPolicyIsDemoMode: true,
}),
),
).toBe(true);
});
it('falls through when demo mode is resolved off', () => {
expect(
shouldHideSettingsNavItem(
'support-diagnostics',
createContext({
presentationPolicyResolved: true,
presentationPolicyIsDemoMode: false,
}),
),
).toBe(false);
});
});
describe('requiredCapability gate (lines 97-103)', () => {
it('hides when resolved and the required capability is false', () => {
expect(
shouldHideSettingsNavItem(
'api',
createContext({
settingsCapabilitiesResolved: true,
settingsCapabilities: { apiAccessRead: false },
}),
),
).toBe(true);
});
it('does not hide when capability state is unresolved', () => {
expect(
shouldHideSettingsNavItem(
'api',
createContext({ settingsCapabilitiesResolved: false, settingsCapabilities: null }),
),
).toBe(false);
});
it('does not hide when the required capability is granted', () => {
expect(
shouldHideSettingsNavItem(
'api',
createContext({
settingsCapabilitiesResolved: true,
settingsCapabilities: { apiAccessRead: true },
}),
),
).toBe(false);
});
it('hides when resolved but settingsCapabilities is undefined (optional-chain -> undefined)', () => {
// context.settingsCapabilities?.[cap] -> undefined -> undefined !== true -> hidden.
expect(
shouldHideSettingsNavItem(
'api',
createContext({
settingsCapabilitiesResolved: true,
settingsCapabilities: undefined,
}),
),
).toBe(true);
});
});
describe('hideWhenUnavailable gate -> hasRequiredFeatures / missingFeaturesArePaidRuntimeBlocked', () => {
it('does not hide when all required features are present (hasRequiredFeatures every()->true)', () => {
expect(
shouldHideSettingsNavItem(
'organization-overview',
createContext({
hasFeature: hasFeatures(['multi_tenant']),
presentationPolicyResolved: true,
}),
),
).toBe(false);
});
it('hides when required features are missing and not runtime-blocked (missing-blocked -> false)', () => {
expect(
shouldHideSettingsNavItem(
'organization-overview',
createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
isRuntimeCapabilityBlocked: () => false,
}),
),
).toBe(true);
});
it('keeps visible when every missing feature is paid-runtime-blocked (missing-blocked -> true)', () => {
expect(
shouldHideSettingsNavItem(
'organization-overview',
createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
isRuntimeCapabilityBlocked: (feature, reason) =>
feature === 'multi_tenant' && reason === 'paid_runtime_required',
}),
),
).toBe(false);
});
it('hides when isRuntimeCapabilityBlocked is omitted (optional-chain ?. yields undefined -> every()->false)', () => {
// missingFeaturesArePaidRuntimeBlocked reads context.isRuntimeCapabilityBlocked?.(...);
// when undefined, every() returns false -> hide block falls through to `return true`.
expect(
shouldHideSettingsNavItem(
'organization-overview',
createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
// isRuntimeCapabilityBlocked deliberately omitted
}),
),
).toBe(true);
});
it('hides when the blocker accepts the feature but a different reason (reason-mismatch arm)', () => {
// The source always passes 'paid_runtime_required'; a callback that only
// blocks on a different reason returns false -> every()->false -> hidden.
expect(
shouldHideSettingsNavItem(
'organization-overview',
createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
isRuntimeCapabilityBlocked: (_feature, reason) => reason === 'some_other_reason',
}),
),
).toBe(true);
});
});
});
// ---- shouldBlockSettingsRouteItem ------------------------------------------
describe('shouldBlockSettingsRouteItem', () => {
it('returns false for an unknown tab (defensive !item branch)', () => {
expect(shouldBlockSettingsRouteItem(UNKNOWN_TAB, createContext())).toBe(false);
});
it('mirrors the hostedOnly behavior for routing', () => {
expect(
shouldBlockSettingsRouteItem(
'organization-billing-admin',
createContext({
hasFeature: hasFeatures(['multi_tenant']),
hostedModeEnabled: false,
settingsCapabilities: { billingAdmin: true },
settingsCapabilitiesResolved: true,
}),
),
).toBe(true);
});
it('blocks a demo-hidden route while demo policy is unresolved', () => {
expect(
shouldBlockSettingsRouteItem(
'support-diagnostics',
createContext({ presentationPolicyResolved: false }),
),
).toBe(true);
});
it('blocks a commercial-hidden route when commercial is hidden', () => {
expect(
shouldBlockSettingsRouteItem(
'system-billing',
createContext({
presentationPolicyResolved: true,
presentationPolicyHidesCommercial: true,
}),
),
).toBe(true);
});
it('does not block a capability-gated route while capability state is unresolved', () => {
expect(
shouldBlockSettingsRouteItem(
'api',
createContext({ settingsCapabilitiesResolved: false, settingsCapabilities: null }),
),
).toBe(false);
});
describe('PANEL_OWNED_FEATURE_GATE_TABS bypass (lines 174-183)', () => {
it.each([
'system-relay',
'support-reporting',
'security-roles',
'security-users',
'security-audit',
'security-webhooks',
] as const)(
'does NOT block panel-owned tab %s even when its features are missing',
(tab) => {
expect(
shouldBlockSettingsRouteItem(
tab,
createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
settingsCapabilitiesResolved: false,
}),
),
).toBe(false);
},
);
});
describe('non-panel-owned hideWhenUnavailable tabs are feature-gated for routing', () => {
// organization-overview is hideWhenUnavailable + NOT in PANEL_OWNED_FEATURE_GATE_TABS,
// so it is the canonical tab to exercise the route-level feature gate.
it('blocks when required features are missing and not runtime-blocked', () => {
const ctx = createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
isRuntimeCapabilityBlocked: () => false,
});
expect(shouldBlockSettingsRouteItem('organization-overview', ctx)).toBe(true);
// Contrast: the same context hides the item from the nav.
expect(shouldHideSettingsNavItem('organization-overview', ctx)).toBe(true);
});
it('does not block when required features are present', () => {
expect(
shouldBlockSettingsRouteItem(
'organization-overview',
createContext({
hasFeature: hasFeatures(['multi_tenant']),
presentationPolicyResolved: true,
}),
),
).toBe(false);
});
it('does not block when missing features are paid-runtime-blocked', () => {
expect(
shouldBlockSettingsRouteItem(
'organization-overview',
createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
isRuntimeCapabilityBlocked: () => true,
}),
),
).toBe(false);
});
it('does not block when isRuntimeCapabilityBlocked is omitted', () => {
// ?. yields undefined -> every()->false -> missing-blocked returns false
// -> but the feature-gate block then returns true... wait: missing-blocked
// false means we DO block. So this asserts the omitted-callback path leads
// to a block (every()->false).
expect(
shouldBlockSettingsRouteItem(
'organization-overview',
createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
}),
),
).toBe(true);
});
});
});
// ---- isSettingsNavItemLocked -----------------------------------------------
describe('isSettingsNavItemLocked', () => {
it('returns false for an unknown tab (defensive !item branch)', () => {
expect(isSettingsNavItemLocked(UNKNOWN_TAB, createContext())).toBe(false);
});
it('returns false for a tab with hideWhenUnavailable (early `item.hideWhenUnavailable` branch)', () => {
// organization-overview, security-roles, system-relay, support-reporting all
// carry hideWhenUnavailable -> short-circuit before isTabLocked is consulted.
expect(
isSettingsNavItemLocked(
'organization-overview',
createContext({ hasFeature: hasFeatures([]) }),
),
).toBe(false);
expect(
isSettingsNavItemLocked('security-roles', createContext({ hasFeature: hasFeatures([]) })),
).toBe(false);
expect(
isSettingsNavItemLocked('system-relay', createContext({ hasFeature: hasFeatures([]) })),
).toBe(false);
});
it('returns false for a tab without feature requirements (isTabLocked -> isFeatureLocked false branch)', () => {
expect(
isSettingsNavItemLocked('system-general', createContext({ hasFeature: hasFeatures([]) })),
).toBe(false);
});
it('returns false while runtime capabilities are not loaded (isFeatureLoaded -> false branch)', () => {
expect(
isSettingsNavItemLocked(
'system-general',
createContext({ runtimeCapabilitiesLoaded: () => false }),
),
).toBe(false);
});
it('never reports a real catalog tab as locked (every feature-gated tab also has hideWhenUnavailable)', () => {
// See GLM_REPORT: the `return isTabLocked(...)` -> true outcome is unreachable
// for real catalog items, so every real tab resolves to false here.
const tabs: SettingsTab[] = [
'system-relay',
'security-webhooks',
'organization-overview',
'organization-access',
'organization-sharing',
'organization-billing',
'organization-billing-admin',
'system-general',
'api',
'support-diagnostics',
];
for (const tab of tabs) {
expect(
isSettingsNavItemLocked(tab, createContext({ hasFeature: hasFeatures([]) })),
).toBe(false);
}
});
});
// ---- hasRequiredFeatures / missingFeaturesArePaidRuntimeBlocked (private) ---
// These helpers are not exported; they are exercised indirectly through the
// three public functions above. This block pins the branch mapping so the
// coverage intent is explicit and self-documenting.
describe('private helpers hasRequiredFeatures / missingFeaturesArePaidRuntimeBlocked (indirect)', () => {
it('hasRequiredFeatures: every()->true when features are present, every()->false when missing', () => {
// every()->true: features present -> availability gate skipped -> not hidden.
expect(
shouldHideSettingsNavItem(
'organization-overview',
createContext({
hasFeature: hasFeatures(['multi_tenant']),
presentationPolicyResolved: true,
}),
),
).toBe(false);
// every()->false: feature missing -> availability gate engages.
expect(
shouldHideSettingsNavItem(
'organization-overview',
createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
isRuntimeCapabilityBlocked: () => false,
}),
),
).toBe(true);
});
it('missingFeaturesArePaidRuntimeBlocked: every()->true keeps visible, every()->false hides/blocks', () => {
// every()->true over the blocker -> not hidden, not blocked.
const blockedCtx = createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
isRuntimeCapabilityBlocked: () => true,
});
expect(shouldHideSettingsNavItem('organization-overview', blockedCtx)).toBe(false);
expect(shouldBlockSettingsRouteItem('organization-overview', blockedCtx)).toBe(false);
// every()->false over the blocker -> hidden AND blocked.
const notBlockedCtx = createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
isRuntimeCapabilityBlocked: () => false,
});
expect(shouldHideSettingsNavItem('organization-overview', notBlockedCtx)).toBe(true);
expect(shouldBlockSettingsRouteItem('organization-overview', notBlockedCtx)).toBe(true);
});
});
@@ -0,0 +1,523 @@
/**
* Branch-coverage tests for the still-uncovered named helpers in
* ssoProvidersModel:
* buildProviderTestPayload, buildMetadataPreviewPayload,
* mapProviderDetailsToForm, buildProviderPayload, canTestProviderForm.
*
* Every `||`, optional-chain, and if/else arm is driven with concrete inputs
* and asserted against the exact emitted shape (no truthiness-only checks).
*/
import { describe, expect, it } from 'vitest';
import {
buildMetadataPreviewPayload,
buildProviderPayload,
buildProviderTestPayload,
canTestProviderForm,
createEmptyProviderForm,
mapProviderDetailsToForm,
type ProviderForm,
} from '../ssoProvidersModel';
// ---- Fixtures ---------------------------------------------------------------
const oidcForm = (overrides: Partial<ProviderForm> = {}): ProviderForm => ({
...createEmptyProviderForm(),
type: 'oidc' as const,
...overrides,
});
const samlForm = (overrides: Partial<ProviderForm> = {}): ProviderForm => ({
...createEmptyProviderForm(),
type: 'saml' as const,
...overrides,
});
// ---- buildProviderTestPayload ----------------------------------------------
describe('buildProviderTestPayload', () => {
it('emits the oidc arm with a populated issuerUrl and clientId', () => {
const payload = buildProviderTestPayload(
oidcForm({
oidcIssuerUrl: ' https://idp.example.com ',
oidcClientId: ' pulse ',
}),
);
expect(payload).toStrictEqual({
type: 'oidc',
oidc: {
issuerUrl: 'https://idp.example.com',
clientId: 'pulse',
},
});
});
it('keeps issuerUrl as an empty string and collapses a blank oidc clientId to undefined', () => {
// issuerUrl is emitted unconditionally (only trimmed); a blank clientId
// falls through the `|| undefined` arm.
const payload = buildProviderTestPayload(
oidcForm({ oidcIssuerUrl: ' ', oidcClientId: ' ' }),
);
expect(payload).toStrictEqual({
type: 'oidc',
oidc: { issuerUrl: '', clientId: undefined },
});
});
it('emits the saml arm with every populated field', () => {
const payload = buildProviderTestPayload(
samlForm({
samlIdpMetadataUrl: 'https://idp.example.com/metadata',
samlIdpMetadataXml: '<EntityDescriptor/>',
samlIdpSsoUrl: 'https://idp.example.com/sso',
samlIdpCertificate: 'cert-data',
}),
);
expect(payload).toStrictEqual({
type: 'saml',
saml: {
idpMetadataUrl: 'https://idp.example.com/metadata',
idpMetadataXml: '<EntityDescriptor/>',
idpSsoUrl: 'https://idp.example.com/sso',
idpCertificate: 'cert-data',
},
});
});
it('collapses every blank saml field to undefined in the saml arm', () => {
const payload = buildProviderTestPayload(
samlForm({
samlIdpMetadataUrl: ' ',
samlIdpMetadataXml: ' ',
samlIdpSsoUrl: ' ',
samlIdpCertificate: ' ',
}),
);
expect(payload).toStrictEqual({
type: 'saml',
saml: {
idpMetadataUrl: undefined,
idpMetadataXml: undefined,
idpSsoUrl: undefined,
idpCertificate: undefined,
},
});
});
});
// ---- buildMetadataPreviewPayload --------------------------------------------
describe('buildMetadataPreviewPayload', () => {
it('always reports type "saml" and trims the metadata url', () => {
const payload = buildMetadataPreviewPayload(
samlForm({ samlIdpMetadataUrl: ' https://idp.example.com/metadata ' }),
);
expect(payload).toStrictEqual({
type: 'saml',
metadataUrl: 'https://idp.example.com/metadata',
});
});
it('preserves an empty metadata url as an empty string (not undefined)', () => {
expect(buildMetadataPreviewPayload(samlForm({ samlIdpMetadataUrl: ' ' }))).toStrictEqual({
type: 'saml',
metadataUrl: '',
});
});
});
// ---- mapProviderDetailsToForm -----------------------------------------------
describe('mapProviderDetailsToForm', () => {
it('maps a fully-populated SAML provider details response into the form', () => {
const form = mapProviderDetailsToForm({
id: 'saml-1',
name: 'Corp SAML',
type: 'saml',
enabled: true,
displayName: 'Corp',
priority: 7,
saml: {
idpMetadataUrl: 'https://idp.example.com/metadata',
idpMetadataXml: '<EntityDescriptor/>',
idpSsoUrl: 'https://idp.example.com/sso',
idpEntityId: 'https://idp.example.com',
idpCertificate: 'cert-data',
spEntityId: 'https://sp.example.com',
signRequests: true,
allowIdpInitiated: true,
usernameAttr: 'user',
emailAttr: 'mail',
groupsAttr: 'memberOf',
},
groupsClaim: 'ignored-when-groupsAttr-set',
allowedGroups: ['admins', 'operators'],
allowedDomains: ['example.com'],
allowedEmails: ['u@example.com'],
groupRoleMappings: { admins: 'admin' },
});
expect(form).toStrictEqual({
id: 'saml-1',
name: 'Corp SAML',
type: 'saml',
enabled: true,
displayName: 'Corp',
priority: 7,
oidcIssuerUrl: '',
oidcClientId: '',
oidcClientSecret: '',
oidcRedirectUrl: '',
oidcLogoutUrl: '',
oidcScopes: 'openid profile email',
samlIdpMetadataUrl: 'https://idp.example.com/metadata',
samlIdpMetadataXml: '<EntityDescriptor/>',
samlIdpSsoUrl: 'https://idp.example.com/sso',
samlIdpEntityId: 'https://idp.example.com',
samlIdpCertificate: 'cert-data',
samlSpEntityId: 'https://sp.example.com',
samlSignRequests: true,
samlAllowIdpInitiated: true,
samlUsernameAttr: 'user',
samlEmailAttr: 'mail',
groupsClaim: 'memberOf',
allowedGroups: 'admins, operators',
allowedDomains: 'example.com',
allowedEmails: 'u@example.com',
groupRoleMappings: 'admins=admin',
});
});
it('defaults displayName to "" and priority to 0 when they are absent', () => {
const form = mapProviderDetailsToForm({
id: 'p',
name: 'P',
type: 'oidc',
enabled: false,
});
expect(form.displayName).toBe('');
expect(form.priority).toBe(0);
expect(form.enabled).toBe(false);
});
it('defaults the saml emailAttr to "email" when saml is present without emailAttr', () => {
const form = mapProviderDetailsToForm({
id: 'p',
name: 'P',
type: 'saml',
enabled: true,
saml: { signRequests: false },
});
expect(form.samlEmailAttr).toBe('email');
expect(form.samlSignRequests).toBe(false);
expect(form.samlAllowIdpInitiated).toBe(false);
});
it('defaults the saml emailAttr to "email" when saml is entirely absent (oidc provider)', () => {
const form = mapProviderDetailsToForm({
id: 'p',
name: 'P',
type: 'oidc',
enabled: true,
});
expect(form.samlEmailAttr).toBe('email');
expect(form.samlSpEntityId).toBe('');
});
it('falls back to the top-level groupsClaim when saml.groupsAttr is absent', () => {
const form = mapProviderDetailsToForm({
id: 'p',
name: 'P',
type: 'oidc',
enabled: true,
groupsClaim: 'roles',
});
expect(form.groupsClaim).toBe('roles');
});
it('defaults groupsClaim to "" when neither saml.groupsAttr nor top-level groupsClaim is set', () => {
const form = mapProviderDetailsToForm({
id: 'p',
name: 'P',
type: 'oidc',
enabled: true,
});
expect(form.groupsClaim).toBe('');
});
it('defaults oidcScopes to "openid profile email" when scopes is an empty array (join -> "")', () => {
const form = mapProviderDetailsToForm({
id: 'p',
name: 'P',
type: 'oidc',
enabled: true,
oidc: { scopes: [] },
});
expect(form.oidcScopes).toBe('openid profile email');
});
it('joins a populated oidc scopes array with single spaces', () => {
const form = mapProviderDetailsToForm({
id: 'p',
name: 'P',
type: 'oidc',
enabled: true,
oidc: { scopes: ['openid', 'groups', 'custom_claim'] },
});
expect(form.oidcScopes).toBe('openid groups custom_claim');
});
it('serializes a populated oidc block (issuer/clientId/redirect/logout) into the form', () => {
const form = mapProviderDetailsToForm({
id: 'p',
name: 'P',
type: 'oidc',
enabled: true,
oidc: {
issuerUrl: 'https://idp.example.com',
clientId: 'pulse',
redirectUrl: 'https://app.example.com/cb',
logoutUrl: 'https://idp.example.com/logout',
},
});
expect(form.oidcIssuerUrl).toBe('https://idp.example.com');
expect(form.oidcClientId).toBe('pulse');
expect(form.oidcRedirectUrl).toBe('https://app.example.com/cb');
expect(form.oidcLogoutUrl).toBe('https://idp.example.com/logout');
expect(form.oidcClientSecret).toBe('');
});
it('renders allowedGroups/Domains/Emails as "" for both absent and empty-array inputs', () => {
// Both arms of listToString (`values && values.length > 0`) collapse to ''.
const absent = mapProviderDetailsToForm({
id: 'p',
name: 'P',
type: 'oidc',
enabled: true,
});
expect(absent.allowedGroups).toBe('');
expect(absent.allowedDomains).toBe('');
expect(absent.allowedEmails).toBe('');
const emptyArrays = mapProviderDetailsToForm({
id: 'p',
name: 'P',
type: 'oidc',
enabled: true,
allowedGroups: [],
allowedDomains: [],
allowedEmails: [],
});
expect(emptyArrays.allowedGroups).toBe('');
expect(emptyArrays.allowedDomains).toBe('');
expect(emptyArrays.allowedEmails).toBe('');
});
});
// ---- buildProviderPayload ---------------------------------------------------
describe('buildProviderPayload', () => {
it('emits a fully-populated oidc payload including identity-restricted fields', () => {
const payload = buildProviderPayload(
oidcForm({
id: 'oidc-1',
name: ' Corp OIDC ',
displayName: ' Corp ',
priority: 3,
oidcIssuerUrl: 'https://idp.example.com',
oidcClientId: 'pulse',
oidcClientSecret: 'shh',
oidcRedirectUrl: 'https://app.example.com/cb',
oidcLogoutUrl: 'https://idp.example.com/logout',
oidcScopes: 'openid profile email groups',
groupsClaim: 'groups',
allowedGroups: 'admins, operators',
allowedDomains: 'example.com',
allowedEmails: 'u@example.com',
groupRoleMappings: 'admins=admin, operators=operator',
}),
);
expect(payload).toStrictEqual({
id: 'oidc-1',
name: 'Corp OIDC',
type: 'oidc',
enabled: true,
displayName: 'Corp',
priority: 3,
allowedGroups: ['admins', 'operators'],
allowedDomains: ['example.com'],
allowedEmails: ['u@example.com'],
groupRoleMappings: { admins: 'admin', operators: 'operator' },
oidc: {
issuerUrl: 'https://idp.example.com',
clientId: 'pulse',
clientSecret: 'shh',
redirectUrl: 'https://app.example.com/cb',
logoutUrl: 'https://idp.example.com/logout',
scopes: ['openid', 'profile', 'email', 'groups'],
},
groupsClaim: 'groups',
});
});
it('collapses every blank oidc field to undefined in the oidc arm', () => {
// Default empty form, but force scopes to '' so splitList yields [].
const payload = buildProviderPayload({ ...oidcForm(), oidcScopes: '' });
expect(payload).toStrictEqual({
id: undefined,
name: '',
type: 'oidc',
enabled: true,
displayName: undefined,
priority: 0,
allowedGroups: [],
allowedDomains: [],
allowedEmails: [],
groupRoleMappings: {},
oidc: {
issuerUrl: '',
clientId: '',
clientSecret: undefined,
redirectUrl: undefined,
logoutUrl: undefined,
scopes: [],
},
groupsClaim: undefined,
});
});
it('emits a fully-populated saml payload (keeps groupsClaim aligned with groupsAttr)', () => {
const payload = buildProviderPayload(
samlForm({
id: 'saml-1',
name: ' Corp SAML ',
displayName: ' Corp ',
priority: 5,
samlIdpMetadataUrl: 'https://idp.example.com/metadata',
samlIdpMetadataXml: '<EntityDescriptor/>',
samlIdpSsoUrl: 'https://idp.example.com/sso',
samlIdpEntityId: 'https://idp.example.com',
samlIdpCertificate: 'cert-data',
samlSpEntityId: 'https://sp.example.com',
samlSignRequests: true,
samlAllowIdpInitiated: true,
samlUsernameAttr: 'user',
samlEmailAttr: 'mail',
groupsClaim: 'memberOf',
allowedGroups: 'admins',
allowedDomains: 'example.com',
allowedEmails: 'u@example.com',
groupRoleMappings: 'admins=admin',
}),
);
expect(payload).toStrictEqual({
id: 'saml-1',
name: 'Corp SAML',
type: 'saml',
enabled: true,
displayName: 'Corp',
priority: 5,
allowedGroups: ['admins'],
allowedDomains: ['example.com'],
allowedEmails: ['u@example.com'],
groupRoleMappings: { admins: 'admin' },
saml: {
idpMetadataUrl: 'https://idp.example.com/metadata',
idpMetadataXml: '<EntityDescriptor/>',
idpSsoUrl: 'https://idp.example.com/sso',
idpEntityId: 'https://idp.example.com',
idpCertificate: 'cert-data',
spEntityId: 'https://sp.example.com',
signRequests: true,
allowIdpInitiated: true,
usernameAttr: 'user',
emailAttr: 'mail',
groupsAttr: 'memberOf',
},
groupsClaim: 'memberOf',
});
});
it('collapses every blank saml field to undefined in the saml arm', () => {
const payload = buildProviderPayload(samlForm());
expect(payload).toStrictEqual({
id: undefined,
name: '',
type: 'saml',
enabled: true,
displayName: undefined,
priority: 0,
allowedGroups: [],
allowedDomains: [],
allowedEmails: [],
groupRoleMappings: {},
saml: {
idpMetadataUrl: undefined,
idpMetadataXml: undefined,
idpSsoUrl: undefined,
idpEntityId: undefined,
idpCertificate: undefined,
spEntityId: undefined,
signRequests: false,
allowIdpInitiated: false,
usernameAttr: undefined,
// The empty form seeds samlEmailAttr with 'email' (createEmptyProviderForm),
// so it survives the `|| undefined` collapse.
emailAttr: 'email',
groupsAttr: undefined,
},
groupsClaim: undefined,
});
});
});
// ---- canTestProviderForm ----------------------------------------------------
describe('canTestProviderForm', () => {
it('returns true for an oidc form with a populated issuerUrl', () => {
expect(canTestProviderForm(oidcForm({ oidcIssuerUrl: 'https://idp.example.com' }))).toBe(true);
});
it('returns false for an oidc form with an empty issuerUrl', () => {
expect(canTestProviderForm(oidcForm({ oidcIssuerUrl: '' }))).toBe(false);
});
it('returns false for an oidc form with a whitespace-only issuerUrl (trims to empty)', () => {
expect(canTestProviderForm(oidcForm({ oidcIssuerUrl: ' ' }))).toBe(false);
});
it('returns true for a saml form with a populated metadataUrl', () => {
expect(
canTestProviderForm(samlForm({ samlIdpMetadataUrl: 'https://idp.example.com/metadata' })),
).toBe(true);
});
it('returns true for a saml form with only a populated metadataXml (metadataUrl absent)', () => {
expect(canTestProviderForm(samlForm({ samlIdpMetadataXml: '<EntityDescriptor/>' }))).toBe(true);
});
it('returns true for a saml form with only a populated idpSsoUrl (first two blank)', () => {
expect(canTestProviderForm(samlForm({ samlIdpSsoUrl: 'https://idp.example.com/sso' }))).toBe(
true,
);
});
it('returns false for a saml form with every test field blank', () => {
expect(canTestProviderForm(samlForm())).toBe(false);
});
it('returns false for a saml form with every test field whitespace-only (trims to empty)', () => {
expect(
canTestProviderForm(
samlForm({
samlIdpMetadataUrl: ' ',
samlIdpMetadataXml: ' ',
samlIdpSsoUrl: ' ',
}),
),
).toBe(false);
});
});
@@ -0,0 +1,657 @@
import { describe, expect, it } from 'vitest';
import type { AggregatedMetricPoint } from '@/api/charts';
import type { WorkloadGuest } from '@/types/workloads';
import {
buildGuestDrawerHistoryPath,
getGuestDrawerAgentLabel,
getGuestDrawerAgentTitle,
getGuestDrawerBackupPresentation,
getGuestDrawerHistoryFallbackMetrics,
getGuestDrawerHistoryRangeBounds,
getGuestDrawerHistoryScale,
getGuestDrawerHistoryTarget,
getGuestDrawerHistoryValueLabel,
getGuestDrawerMemoryRows,
getGuestDrawerNetworkInterfaces,
hasGuestDrawerFilesystemDetails,
hasGuestDrawerOsInfo,
isGuestDrawerVM,
normalizeGuestDrawerHistoryPoints,
normalizeGuestDrawerTags,
} from '../guestDrawerModel';
const makeGuest = (overrides?: Partial<WorkloadGuest>): WorkloadGuest =>
({
id: 'guest-0',
vmid: 100,
name: 'workload-0',
node: 'pve',
instance: 'cluster-a',
status: 'running',
type: 'qemu',
cpu: 0.5,
cpus: 2,
memory: { total: 4096, used: 1024, free: 3072, usage: 0.25 },
disk: { total: 102400, used: 10240, free: 92160, usage: 0.1 },
networkIn: 100,
networkOut: 200,
diskRead: 10,
diskWrite: 5,
uptime: 3600,
template: false,
lastBackup: 0,
tags: [],
lock: '',
lastSeen: new Date().toISOString(),
workloadType: 'vm',
...overrides,
}) as WorkloadGuest;
// AggregatedMetricPoint declares min/max as required numbers, but the runtime
// contract (and the code under test) tolerates missing/non-finite min/max, so
// build points with optional min/max and cast to satisfy the declared type.
const pt = (
timestamp: number,
value: number,
min?: number,
max?: number,
): AggregatedMetricPoint => ({ timestamp, value, min, max }) as AggregatedMetricPoint;
describe('guestDrawerModel (branch coverage)', () => {
describe('isGuestDrawerVM', () => {
it('returns true for a qemu/vm guest', () => {
expect(isGuestDrawerVM(makeGuest({ type: 'qemu', workloadType: 'vm' }))).toBe(true);
});
it('returns false for a system-container (lxc) guest', () => {
expect(isGuestDrawerVM(makeGuest({ type: 'lxc', workloadType: 'system-container' }))).toBe(
false,
);
});
it('returns false for an app-container', () => {
expect(
isGuestDrawerVM(
makeGuest({ type: 'app-container', workloadType: 'app-container', id: 'c1' }),
),
).toBe(false);
});
});
describe('getGuestDrawerHistoryFallbackMetrics', () => {
it('scales a fractional cpu (<=1.5) by 100 and returns finite metric values', () => {
const guest = makeGuest({
cpu: 1.0,
memory: { total: 100, used: 40, free: 60, usage: 0.4 },
disk: { total: 100, used: 30, free: 70, usage: 0.3 },
networkIn: 100,
networkOut: 200,
diskRead: 10,
diskWrite: 5,
});
expect(getGuestDrawerHistoryFallbackMetrics(guest)).toStrictEqual({
cpu: 100,
memory: 0.4,
disk: 0.3,
netin: 100,
netout: 200,
diskread: 10,
diskwrite: 5,
});
});
it('passes a cpu value greater than 1.5 through unchanged', () => {
expect(getGuestDrawerHistoryFallbackMetrics(makeGuest({ cpu: 2.0 })).cpu).toBe(2);
});
it('treats the cpu boundary of exactly 1.5 as a ratio (150)', () => {
expect(getGuestDrawerHistoryFallbackMetrics(makeGuest({ cpu: 1.5 })).cpu).toBe(150);
});
it('drops cpu when it is a non-finite number', () => {
expect(getGuestDrawerHistoryFallbackMetrics(makeGuest({ cpu: Number.NaN })).cpu).toBeUndefined();
});
it('drops cpu when it is not a number (typeof guard)', () => {
expect(
getGuestDrawerHistoryFallbackMetrics(
makeGuest({ cpu: 'busy' as unknown as number }),
).cpu,
).toBeUndefined();
});
it('drops memory/disk when those objects are absent (optional-chain arm)', () => {
const result = getGuestDrawerHistoryFallbackMetrics(
makeGuest({
memory: undefined as unknown as WorkloadGuest['memory'],
disk: undefined as unknown as WorkloadGuest['disk'],
}),
);
expect(result.memory).toBeUndefined();
expect(result.disk).toBeUndefined();
});
it('drops a non-finite network value via the finite() guard', () => {
const result = getGuestDrawerHistoryFallbackMetrics(
makeGuest({ networkIn: Number.POSITIVE_INFINITY }),
);
expect(result.netin).toBeUndefined();
expect(result.netout).toBe(200);
});
});
describe('normalizeGuestDrawerHistoryPoints (and internal clampHistoryPointValue)', () => {
it('returns an empty array for undefined points', () => {
expect(normalizeGuestDrawerHistoryPoints(undefined, '%')).toEqual([]);
});
it('filters non-finite timestamp/value points, clamps % values, and sorts by timestamp', () => {
const points = [
pt(3, 150, 200, -10), // value->100, min->100, max->0 (% clamp)
pt(1, 50), // min/max undefined -> fall back to clamped value (50)
pt(2, Number.NaN), // filtered: non-finite value
pt(Number.NaN, 40), // filtered: non-finite timestamp
pt(4, 60, Number.NaN, 'x' as unknown as number), // min NaN -> value; max non-number -> value
];
expect(normalizeGuestDrawerHistoryPoints(points, '%')).toEqual([
{ timestamp: 1, value: 50, min: 50, max: 50 },
{ timestamp: 3, value: 100, min: 100, max: 0 },
{ timestamp: 4, value: 60, min: 60, max: 60 },
]);
});
it('clamps non-% values to >=0 without the 100 cap', () => {
const points = [pt(1, -50, -20, 250)];
expect(normalizeGuestDrawerHistoryPoints(points, 'B/s')).toEqual([
{ timestamp: 1, value: 0, min: 0, max: 250 },
]);
});
});
describe('getGuestDrawerHistoryScale', () => {
it('returns a fixed 0-100 scale for percent units', () => {
expect(
getGuestDrawerHistoryScale(
[{ points: [pt(1, 150, 0, 999)] }],
'%',
),
).toStrictEqual({ minValue: 0, maxValue: 100 });
});
describe("unit 'C'", () => {
it('falls back to 0-100 when no finite points are present', () => {
expect(getGuestDrawerHistoryScale([], 'C')).toStrictEqual({ minValue: 0, maxValue: 100 });
expect(
getGuestDrawerHistoryScale(
[{ points: [pt(1, Number.NaN, Number.NaN, Number.NaN)] }],
'C',
),
).toStrictEqual({ minValue: 0, maxValue: 100 });
});
it('expands a single distinct value (min===max) symmetrically around it', () => {
expect(
getGuestDrawerHistoryScale([{ points: [pt(1, 50, 50, 50)] }], 'C'),
).toStrictEqual({ minValue: 45, maxValue: 55 });
});
it('clamps the symmetric lower bound to 0 when the value is near zero', () => {
expect(
getGuestDrawerHistoryScale([{ points: [pt(1, 3, 3, 3)] }], 'C'),
).toStrictEqual({ minValue: 0, maxValue: 8 });
});
it('applies 15%% padding around distinct min/max using finite min/max fields', () => {
// min=50, max=80 -> padding=max(2, 30*0.15=4.5)=4.5 -> {45.5, 84.5}
expect(
getGuestDrawerHistoryScale([{ points: [pt(1, 55, 50, 80)] }], 'C'),
).toStrictEqual({ minValue: 45.5, maxValue: 84.5 });
});
it('falls back to point.value for low/high when min/max are absent', () => {
// value=40 only -> min===max=40 -> {35, 45}
expect(
getGuestDrawerHistoryScale([{ points: [pt(1, 40)] }], 'C'),
).toStrictEqual({ minValue: 35, maxValue: 45 });
});
});
describe('default unit (e.g. B/s)', () => {
it('returns {0,1} when there are no finite points', () => {
expect(getGuestDrawerHistoryScale([], 'B/s')).toStrictEqual({ minValue: 0, maxValue: 1 });
expect(
getGuestDrawerHistoryScale(
[
{
points: [
pt(1, 100, undefined, Number.POSITIVE_INFINITY),
pt(2, Number.NaN),
],
},
],
'B/s',
),
).toStrictEqual({ minValue: 0, maxValue: 1 });
});
it('scales to 1.15x of the largest finite max (prefers max over value)', () => {
// max=200 -> maxValue = max(1, 200*1.15) (IEEE-754 -> 229.99999999999997)
const expectedMax = Math.max(1, 200 * 1.15);
expect(
getGuestDrawerHistoryScale([{ points: [pt(1, 100, 0, 200)] }], 'B/s'),
).toStrictEqual({ minValue: 0, maxValue: expectedMax });
});
it('falls back to point.value when max is absent', () => {
// value=150 -> maxValue = max(1, 150*1.15) = 172.5
expect(
getGuestDrawerHistoryScale([{ points: [pt(1, 150)] }], 'B/s'),
).toStrictEqual({ minValue: 0, maxValue: 172.5 });
});
});
});
describe('buildGuestDrawerHistoryPath', () => {
it('returns an empty string for fewer than two points', () => {
expect(buildGuestDrawerHistoryPath([], { minValue: 0, maxValue: 100 }, 0, 100)).toBe('');
expect(
buildGuestDrawerHistoryPath([pt(1, 50)], { minValue: 0, maxValue: 100 }, 0, 100),
).toBe('');
});
it('builds an M.. L.. path with default geometry across the full value range', () => {
const path = buildGuestDrawerHistoryPath(
[pt(0, 0), pt(100, 100)],
{ minValue: 0, maxValue: 100 },
0,
100,
);
expect(path).toBe('M34.00,74.00 L352.00,8.00');
});
it('clamps values above/below the scale to the plot edges', () => {
const path = buildGuestDrawerHistoryPath(
[pt(0, 150), pt(100, -20)],
{ minValue: 0, maxValue: 100 },
0,
100,
);
expect(path).toBe('M34.00,8.00 L352.00,74.00');
});
it('honors custom width/height', () => {
// width=200 -> plotWidth=158; height=100 -> plotHeight=74
const path = buildGuestDrawerHistoryPath(
[pt(0, 0), pt(100, 100)],
{ minValue: 0, maxValue: 100 },
0,
100,
200,
100,
);
expect(path).toBe('M34.00,82.00 L192.00,8.00');
});
it('uses Math.max(1,...) for degenerate time/value spans', () => {
// startTime===endTime -> timeSpan=1; minValue===maxValue -> valueSpan=1
const path = buildGuestDrawerHistoryPath(
[pt(5, 5), pt(5, 5)],
{ minValue: 5, maxValue: 5 },
5,
5,
);
expect(path).toBe('M34.00,74.00 L34.00,74.00');
});
});
describe('getGuestDrawerHistoryValueLabel', () => {
it("returns '-' when there are no points", () => {
expect(getGuestDrawerHistoryValueLabel([], '%')).toBe('-');
});
it('formats the latest point value for each unit arm', () => {
expect(getGuestDrawerHistoryValueLabel([pt(1, 10), pt(2, 50)], '%')).toBe('50.0%');
expect(getGuestDrawerHistoryValueLabel([pt(1, 1024)], 'B/s')).toBe('1.00 KB/s');
expect(getGuestDrawerHistoryValueLabel([pt(1, 1024)], '')).toBe('1.00 KB');
expect(getGuestDrawerHistoryValueLabel([pt(1, 22.7)], 'C')).toBe('23°C');
expect(getGuestDrawerHistoryValueLabel([pt(1, 3)], 'ops')).toBe('3 ops');
expect(getGuestDrawerHistoryValueLabel([pt(1, 3.5)], 'ops')).toBe('3.5 ops');
});
});
describe('getGuestDrawerHistoryRangeBounds', () => {
it('returns null when every group has no points', () => {
expect(getGuestDrawerHistoryRangeBounds([{ points: [] }, { points: [] }])).toBeNull();
});
it('returns the combined min/max timestamps across groups', () => {
expect(
getGuestDrawerHistoryRangeBounds([
{ points: [pt(10, 1), pt(30, 2)] },
{ points: [pt(5, 3), pt(20, 4)] },
]),
).toStrictEqual({ startTime: 5, endTime: 30 });
});
});
describe('getGuestDrawerHistoryTarget', () => {
it('returns null when the canonical id trims to empty', () => {
expect(
getGuestDrawerHistoryTarget(
makeGuest({ id: ' ', type: 'app-container', workloadType: 'app-container' }),
),
).toBeNull();
});
it('maps a vm to the canonical node-scoped id', () => {
expect(
getGuestDrawerHistoryTarget(
makeGuest({ type: 'qemu', workloadType: 'vm', instance: 'c', node: 'pve', vmid: 101 }),
),
).toStrictEqual({ resourceType: 'vm', resourceId: 'c:pve:101' });
});
it('maps a system-container to the canonical node-scoped id', () => {
expect(
getGuestDrawerHistoryTarget(
makeGuest({ type: 'lxc', workloadType: 'system-container', instance: 'c', node: 'pve', vmid: 102 }),
),
).toStrictEqual({ resourceType: 'system-container', resourceId: 'c:pve:102' });
});
it('maps an app-container to its plain id', () => {
expect(
getGuestDrawerHistoryTarget(
makeGuest({ id: 'app-1', type: 'app-container', workloadType: 'app-container' }),
),
).toStrictEqual({ resourceType: 'app-container', resourceId: 'app-1' });
});
it('maps a pod to its plain id', () => {
expect(
getGuestDrawerHistoryTarget(makeGuest({ id: 'pod-1', type: 'pod', workloadType: 'pod' })),
).toStrictEqual({ resourceType: 'pod', resourceId: 'pod-1' });
});
});
describe('hasGuestDrawerOsInfo', () => {
it('returns false when both os fields are absent', () => {
expect(hasGuestDrawerOsInfo(makeGuest())).toBe(false);
});
it('returns true when osName is a non-empty string', () => {
expect(hasGuestDrawerOsInfo(makeGuest({ osName: 'Ubuntu' }))).toBe(true);
});
it('returns true when only osVersion is set', () => {
expect(hasGuestDrawerOsInfo(makeGuest({ osVersion: '22.04' }))).toBe(true);
});
it('returns false for an empty-string osName (length 0)', () => {
expect(hasGuestDrawerOsInfo(makeGuest({ osName: '' }))).toBe(false);
});
});
describe('getGuestDrawerAgentLabel', () => {
it("returns '' when agentVersion is missing or blank", () => {
expect(getGuestDrawerAgentLabel(makeGuest())).toBe('');
expect(getGuestDrawerAgentLabel(makeGuest({ agentVersion: ' ' }))).toBe('');
});
it('prefixes the version with QEMU for a vm', () => {
expect(getGuestDrawerAgentLabel(makeGuest({ type: 'qemu', agentVersion: '1.0' }))).toBe(
'QEMU 1.0',
);
});
it('returns the bare version for a non-vm', () => {
expect(
getGuestDrawerAgentLabel(makeGuest({ type: 'lxc', workloadType: 'system-container', agentVersion: '1.0' })),
).toBe('1.0');
});
});
describe('getGuestDrawerAgentTitle', () => {
it("returns '' when agentVersion is missing", () => {
expect(getGuestDrawerAgentTitle(makeGuest())).toBe('');
});
it('builds the full QEMU guest-agent title for a vm', () => {
expect(getGuestDrawerAgentTitle(makeGuest({ type: 'qemu', agentVersion: '1.0' }))).toBe(
'QEMU guest agent 1.0',
);
});
it('returns the bare version for a non-vm', () => {
expect(
getGuestDrawerAgentTitle(
makeGuest({ type: 'lxc', workloadType: 'system-container', agentVersion: '1.0' }),
),
).toBe('1.0');
});
});
describe('getGuestDrawerMemoryRows', () => {
it('returns an empty array when memory is absent', () => {
expect(
getGuestDrawerMemoryRows(
makeGuest({ memory: undefined as unknown as WorkloadGuest['memory'] }),
),
).toEqual([]);
});
it('returns an empty array when total is not positive and no balloon/swap', () => {
expect(
getGuestDrawerMemoryRows(
makeGuest({ memory: { total: 0, used: 0, free: 0, usage: 0 } }),
),
).toEqual([]);
});
it('emits only Usage and Total when total>0 and no cache/free/balloon/swap', () => {
expect(
getGuestDrawerMemoryRows(
makeGuest({
memory: { total: 4096, used: 1024, usage: 0.25 } as unknown as WorkloadGuest['memory'],
}),
),
).toEqual([
{ label: 'Usage', value: '25% · 1.00 KB' },
{ label: 'Total', value: '4.00 KB' },
]);
});
it('appends Reclaimable cache (>0) and Free (when free is a number)', () => {
expect(
getGuestDrawerMemoryRows(
makeGuest({
memory: { total: 4096, used: 1024, free: 3072, usage: 0.25, cache: 2048 },
}),
),
).toEqual([
{ label: 'Usage', value: '25% · 1.00 KB' },
{ label: 'Total', value: '4.00 KB' },
{ label: 'Reclaimable cache', value: '2.00 KB' },
{ label: 'Free', value: '3.00 KB' },
]);
});
it('omits the Free row when free is not a number', () => {
expect(
getGuestDrawerMemoryRows(
makeGuest({
memory: { total: 4096, used: 1024, usage: 0.25, free: 'x' as unknown as number },
}),
),
).toEqual([
{ label: 'Usage', value: '25% · 1.00 KB' },
{ label: 'Total', value: '4.00 KB' },
]);
});
it('appends a Balloon row only when balloon>0 and differs from total', () => {
expect(
getGuestDrawerMemoryRows(
makeGuest({
memory: { total: 4096, used: 1024, usage: 0.25, balloon: 2048 } as unknown as WorkloadGuest['memory'],
}),
),
).toEqual([
{ label: 'Usage', value: '25% · 1.00 KB' },
{ label: 'Total', value: '4.00 KB' },
{ label: 'Balloon', value: '2.00 KB' },
]);
// balloon === total -> row suppressed
expect(
getGuestDrawerMemoryRows(
makeGuest({
memory: { total: 4096, used: 1024, usage: 0.25, balloon: 4096 } as unknown as WorkloadGuest['memory'],
}),
),
).toEqual([
{ label: 'Usage', value: '25% · 1.00 KB' },
{ label: 'Total', value: '4.00 KB' },
]);
});
it('appends a Swap row and defaults swapUsed to 0 when absent', () => {
expect(
getGuestDrawerMemoryRows(
makeGuest({
memory: {
total: 4096,
used: 1024,
usage: 0.25,
swapTotal: 1024,
swapUsed: 512,
} as unknown as WorkloadGuest['memory'],
}),
),
).toEqual([
{ label: 'Usage', value: '25% · 1.00 KB' },
{ label: 'Total', value: '4.00 KB' },
{ label: 'Swap', value: '512 B / 1.00 KB' },
]);
expect(
getGuestDrawerMemoryRows(
makeGuest({
memory: {
total: 4096,
used: 1024,
usage: 0.25,
swapTotal: 1024,
} as unknown as WorkloadGuest['memory'],
}),
),
).toEqual([
{ label: 'Usage', value: '25% · 1.00 KB' },
{ label: 'Total', value: '4.00 KB' },
{ label: 'Swap', value: '0 B / 1.00 KB' },
]);
});
});
describe('hasGuestDrawerFilesystemDetails', () => {
it('returns false when disks is absent', () => {
expect(hasGuestDrawerFilesystemDetails(makeGuest({ disks: undefined }))).toBe(false);
});
it('returns false for an empty disks array', () => {
expect(hasGuestDrawerFilesystemDetails(makeGuest({ disks: [] }))).toBe(false);
});
it('returns true when at least one disk is present', () => {
expect(
hasGuestDrawerFilesystemDetails(
makeGuest({ disks: [{ total: 1, used: 0, free: 1, usage: 0 }] }),
),
).toBe(true);
});
});
describe('getGuestDrawerNetworkInterfaces', () => {
it('returns an empty array when absent (|| fallback)', () => {
expect(getGuestDrawerNetworkInterfaces(makeGuest({ networkInterfaces: undefined }))).toEqual([]);
});
it('returns the same array reference when present', () => {
const ifaces = [{ name: 'eth0', mac: 'aa:bb' }];
expect(getGuestDrawerNetworkInterfaces(makeGuest({ networkInterfaces: ifaces }))).toBe(ifaces);
});
});
describe('normalizeGuestDrawerTags', () => {
it('trims and filters an array of tags', () => {
expect(normalizeGuestDrawerTags([' a ', '', 'b'])).toEqual(['a', 'b']);
expect(normalizeGuestDrawerTags([])).toEqual([]);
});
it('splits, trims, and filters a comma-separated string', () => {
expect(normalizeGuestDrawerTags(' a , , b ')).toEqual(['a', 'b']);
expect(normalizeGuestDrawerTags('')).toEqual([]);
});
it('returns an empty array for null', () => {
expect(normalizeGuestDrawerTags(null)).toEqual([]);
});
});
describe('getGuestDrawerBackupPresentation', () => {
const now = new Date(2024, 5, 15, 12, 0, 0); // 2024-06-15T12:00:00 local
const dayMs = 1000 * 60 * 60 * 24;
const backupNDaysAgo = (days: number): Date => new Date(now.getTime() - days * dayMs);
const expectPresentation = (days: number) => {
const lastBackup = backupNDaysAgo(days);
const result = getGuestDrawerBackupPresentation(lastBackup, now);
expect(result.dateLabel).toBe(new Date(lastBackup).toLocaleDateString());
return result;
};
it('labels day 0 as Today and uses the green (fresh) class', () => {
expect(expectPresentation(0)).toStrictEqual({
ageClass: 'text-green-600 dark:text-green-400',
ageLabel: 'Today',
dateLabel: new Date(backupNDaysAgo(0)).toLocaleDateString(),
});
});
it('labels day 1 as Yesterday', () => {
const result = expectPresentation(1);
expect(result.ageLabel).toBe('Yesterday');
expect(result.ageClass).toBe('text-green-600 dark:text-green-400');
});
it('uses the green class up to and including day 7 (isOld = daysSince > 7)', () => {
expect(expectPresentation(7).ageClass).toBe('text-green-600 dark:text-green-400');
expect(expectPresentation(7).ageLabel).toBe('7d ago');
});
it('switches to the amber class at day 8', () => {
const result = expectPresentation(8);
expect(result.ageClass).toBe('text-amber-600 dark:text-amber-400');
expect(result.ageLabel).toBe('8d ago');
});
it('keeps the amber class at day 30 (isCritical = daysSince > 30)', () => {
expect(expectPresentation(30).ageClass).toBe('text-amber-600 dark:text-amber-400');
expect(expectPresentation(30).ageLabel).toBe('30d ago');
});
it('switches to the red (critical) class at day 31', () => {
const result = expectPresentation(31);
expect(result.ageClass).toBe('text-red-600 dark:text-red-400');
expect(result.ageLabel).toBe('31d ago');
});
it('accepts a numeric (ms) lastBackup value', () => {
const lastBackup = backupNDaysAgo(10).getTime();
const result = getGuestDrawerBackupPresentation(lastBackup, now);
expect(result.ageLabel).toBe('10d ago');
expect(result.ageClass).toBe('text-amber-600 dark:text-amber-400');
expect(result.dateLabel).toBe(new Date(lastBackup).toLocaleDateString());
});
});
});
@@ -0,0 +1,460 @@
import { describe, expect, it } from 'vitest';
import type { Connection, ConnectionFleetGovernance } from '@/api/connections';
import { buildWorkloadInventorySourceIssues } from '../workloadInventorySourceIssues';
const fleet = (overrides: Partial<ConnectionFleetGovernance> = {}): ConnectionFleetGovernance => ({
enrollmentState: 'configured',
livenessState: 'active',
versionDrift: 'not-applicable',
adapterHealth: 'healthy',
configRollout: 'configured',
credentialStatus: 'verified',
updateStatus: 'not-applicable',
remoteControl: 'not-applicable',
...overrides,
});
const connection = (overrides: Partial<Connection>): Connection =>
({
id: 'pve:node',
type: 'pve',
name: 'node',
address: 'https://node:8006',
state: 'active',
stateReason: '',
enabled: true,
surfaces: ['vms'],
scope: { vms: true },
lastSeen: null,
lastError: null,
source: 'agent',
fleet: fleet(),
capabilities: {
supportsPause: true,
supportsScope: true,
supportsTest: true,
},
...overrides,
}) as Connection;
describe('workloadInventorySourceIssues (branch coverage)', () => {
describe('credentialInvalid', () => {
it('is true when state is unauthorized (first OR arm)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:unauth',
name: 'unauth',
state: 'unauthorized',
fleet: fleet({ credentialStatus: 'verified' }),
}),
]);
expect(issues).toHaveLength(1);
expect(issues[0]?.stateLabel).toBe('Credentials invalid');
});
it('is true when fleet.credentialStatus is invalid (second OR arm, active state)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:badstatus',
name: 'badstatus',
state: 'active',
fleet: fleet({ credentialStatus: 'invalid' }),
}),
]);
expect(issues).toHaveLength(1);
expect(issues[0]?.stateLabel).toBe('Credentials invalid');
});
it('is true when fleet.credentialHealth.status is invalid (third OR arm)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:healthbad',
name: 'healthbad',
state: 'active',
fleet: fleet({ credentialHealth: { status: 'invalid' } }),
}),
]);
expect(issues).toHaveLength(1);
expect(issues[0]?.stateLabel).toBe('Credentials invalid');
});
it('is true when fleet.credentialHealth.status is expired (fourth OR arm)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:expired',
name: 'expired',
state: 'active',
fleet: fleet({ credentialHealth: { status: 'expired' } }),
}),
]);
expect(issues).toHaveLength(1);
expect(issues[0]?.stateLabel).toBe('Credentials invalid');
});
it('short-circuits gracefully when fleet is undefined (optional-chain false arm)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:nofleet',
name: 'nofleet',
state: 'paused',
fleet: undefined as unknown as Connection['fleet'],
}),
]);
expect(issues).toHaveLength(1);
expect(issues[0]?.stateLabel).toBe('Collection paused');
});
});
describe('stateLabelFor switch arms (credentialInvalid false)', () => {
it('maps pending to "Collection pending"', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({ id: 'pve:pend', name: 'pend', state: 'pending' }),
]);
expect(issues[0]?.stateLabel).toBe('Collection pending');
});
it('maps stale to "Collection stale"', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({ id: 'pve:stale', name: 'stale', state: 'stale' }),
]);
expect(issues[0]?.stateLabel).toBe('Collection stale');
});
it('maps unreachable to "Source unreachable"', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'vmware:down',
type: 'vmware',
name: 'down',
state: 'unreachable',
}),
]);
expect(issues[0]?.stateLabel).toBe('Source unreachable');
});
});
describe('descriptionFor branches', () => {
it('credentialInvalid arm names the type-label API credentials', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'docker:bad',
type: 'docker',
name: 'dockerhost',
state: 'active',
surfaces: ['containers'],
scope: { containers: true },
fleet: fleet({ credentialStatus: 'invalid' }),
}),
]);
expect(issues[0]?.description).toBe(
'Pulse has containers enabled for dockerhost, but its Docker API credentials are invalid.',
);
});
it('paused arm says collection is paused', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({ id: 'pve:paused', name: 'paused', state: 'paused' }),
]);
expect(issues[0]?.description).toBe(
'Pulse has VMs enabled for paused, but collection is paused.',
);
});
it('pending arm says collection has not completed yet', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({ id: 'pve:pend', name: 'pend', state: 'pending' }),
]);
expect(issues[0]?.description).toBe(
'Pulse has VMs enabled for pend, but collection has not completed yet.',
);
});
it('stale arm says inventory data is stale', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({ id: 'pve:stale', name: 'stale', state: 'stale' }),
]);
expect(issues[0]?.description).toBe(
'Pulse has VMs enabled for stale, but the last inventory data is stale.',
);
});
it('unreachable arm interpolates the type label before "API is unreachable"', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'vmware:down',
type: 'vmware',
name: 'vc1',
state: 'unreachable',
}),
]);
expect(issues[0]?.description).toBe(
'Pulse has VMs enabled for vc1, but the VMware vCenter API is unreachable.',
);
});
});
describe('formatCoverage', () => {
it('returns the single label unchanged for one surface (length === 1 arm)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({ id: 'pve:one', name: 'one', state: 'paused' }),
]);
expect(issues[0]?.coverageLabel).toBe('VMs');
});
it('joins two labels with "and" (length === 2 arm)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:two',
name: 'two',
state: 'paused',
surfaces: ['vms', 'containers'],
scope: { vms: true, containers: true },
}),
]);
expect(issues[0]?.coverageLabel).toBe('VMs and containers');
});
it('joins three-plus labels with Oxford comma (length >= 3 arm)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:three',
type: 'kubernetes',
name: 'three',
state: 'paused',
surfaces: ['vms', 'containers', 'kubernetes'],
scope: { vms: true, containers: true, kubernetes: true },
}),
]);
expect(issues[0]?.coverageLabel).toBe(
'VMs, containers, and Kubernetes workloads',
);
});
});
describe('activeWorkloadSurfaces', () => {
it('keeps only truthy scope entries and drops surfaces with no label', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:scoped',
name: 'scoped',
state: 'paused',
surfaces: ['vms', 'containers', 'storage'],
scope: { vms: true, containers: false, storage: true },
}),
]);
expect(issues[0]?.coverageLabel).toBe('VMs');
});
it('falls back to connection.surfaces when scope is empty', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:fallback',
name: 'fallback',
state: 'paused',
surfaces: ['vms', 'pods'],
scope: {},
}),
]);
expect(issues[0]?.coverageLabel).toBe('VMs and pods');
});
it('falls back to surfaces when scope is undefined (?? {} arm)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:noscope',
name: 'noscope',
state: 'paused',
surfaces: ['vms'],
scope: undefined as unknown as Connection['scope'],
}),
]);
expect(issues[0]?.coverageLabel).toBe('VMs');
});
it('deduplicates surfaces that map to the same label (seen.has arm)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'docker:dup',
type: 'docker',
name: 'dup',
state: 'paused',
surfaces: ['containers', 'docker'],
scope: {},
}),
]);
expect(issues[0]?.coverageLabel).toBe('containers');
});
it('sorts an unknown surface via the -1 rank normalization branch', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:unknown',
name: 'unknown',
state: 'paused',
surfaces: ['zzz', 'vms'],
scope: {},
}),
]);
expect(issues[0]?.coverageLabel).toBe('VMs');
});
});
describe('compactDetail', () => {
it('returns undefined when no error message is available (!formatted arm)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:nomsg',
name: 'nomsg',
state: 'paused',
stateReason: '',
lastError: null,
}),
]);
expect(issues[0]?.detail).toBeUndefined();
});
it('formats a short lastError.message (left ?? operand non-null)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:shorterr',
name: 'shorterr',
state: 'paused',
stateReason: 'should-not-be-used',
lastError: {
at: '2026-07-12T00:00:00Z',
message: 'no such host',
},
}),
]);
expect(issues[0]?.detail).toBe('Host not found. Check the hostname or IP address.');
});
it('falls back to stateReason when lastError is null (right ?? operand)', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:reason',
name: 'reason',
state: 'paused',
stateReason: 'connection refused',
lastError: null,
}),
]);
expect(issues[0]?.detail).toBe(
'Connection refused. The host is reachable but rejected the connection on this port. Check the port is correct and the service is running.',
);
});
it('truncates a formatted message longer than 220 characters (>220 arm)', () => {
const longMessage = 'x'.repeat(300);
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'pve:longerr',
name: 'longerr',
state: 'paused',
lastError: {
at: '2026-07-12T00:00:00Z',
message: longMessage,
},
}),
]);
const detail = issues[0]?.detail;
expect(detail).toBeDefined();
expect(detail?.length).toBe(220);
expect(detail).toBe(`${'x'.repeat(217)}...`);
expect(detail?.endsWith('...')).toBe(true);
});
});
describe('buildWorkloadInventorySourceIssues pipeline', () => {
it('excludes disabled, non-workload-type, and active-valid connections', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({ id: 'pve:disabled', name: 'disabled', enabled: false, state: 'unauthorized' }),
connection({
id: 'pbs:tower',
type: 'pbs',
name: 'tower',
state: 'unreachable',
surfaces: ['backups'],
scope: { backups: true },
}),
connection({ id: 'pve:healthy', name: 'healthy', state: 'active' }),
connection({ id: 'pve:blocked', name: 'blocked', state: 'stale' }),
]);
expect(issues).toHaveLength(1);
expect(issues[0]?.id).toBe('pve:blocked');
});
it('orders by descending STATE_RANK when states differ', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({ id: 'pve:aaa', name: 'aaa', state: 'paused' }),
connection({ id: 'pve:zzz', name: 'zzz', state: 'unreachable' }),
]);
expect(issues.map((issue) => issue.state)).toEqual(['unreachable', 'paused']);
expect(issues.map((issue) => issue.name)).toEqual(['zzz', 'aaa']);
});
it('breaks state-rank ties with name localeCompare', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({ id: 'pve:zeta', name: 'zeta', state: 'paused' }),
connection({ id: 'pve:alpha', name: 'alpha', state: 'paused' }),
]);
expect(issues.map((issue) => issue.name)).toEqual(['alpha', 'zeta']);
});
it('emits a fully-shaped issue for a kubernetes source', () => {
const issues = buildWorkloadInventorySourceIssues([
connection({
id: 'kubernetes:k1',
type: 'kubernetes',
name: 'k1',
state: 'pending',
surfaces: ['kubernetes'],
scope: { kubernetes: true },
}),
]);
expect(issues).toStrictEqual([
{
id: 'kubernetes:k1',
name: 'k1',
type: 'kubernetes',
typeLabel: 'Kubernetes',
state: 'pending',
stateLabel: 'Collection pending',
coverageLabel: 'Kubernetes workloads',
description:
'Pulse has Kubernetes workloads enabled for k1, but collection has not completed yet.',
detail: undefined,
},
]);
});
});
});
@@ -0,0 +1,407 @@
import { describe, expect, it } from 'vitest';
import type { WorkloadGuest } from '@/types/workloads';
import {
computeWorkloadStats,
createWorkloadSortComparator,
filterWorkloads,
getWorkloadGroupLabel,
} from '@/components/Workloads/workloadSelectors';
const makeGuest = (i: number, overrides?: Partial<WorkloadGuest>): WorkloadGuest => ({
id: `guest-${i}`,
vmid: 100 + i,
name: `workload-${i}`,
node: `node-${i % 5}`,
instance: `cluster-${i % 3}`,
status: 'running',
type: 'vm',
cpu: (i % 100) / 100,
cpus: 2,
memory: { total: 4096, used: ((i % 80) / 100) * 4096, free: 0, usage: (i % 80) / 100 },
disk: { total: 102400, used: ((i % 60) / 100) * 102400, free: 0, usage: (i % 60) / 100 },
networkIn: i * 100,
networkOut: i * 50,
diskRead: i * 10,
diskWrite: i * 5,
uptime: i * 3600,
template: false,
lastBackup: 0,
tags: [],
lock: '',
lastSeen: new Date().toISOString(),
workloadType: 'vm',
...overrides,
});
const baseFilterParams = {
viewMode: 'all' as const,
statusMode: 'all',
searchTerm: '',
selectedNode: null,
selectedHostHint: null,
selectedKubernetesContext: null,
};
describe('workloadSelectors (branch coverage 2)', () => {
describe('createWorkloadSortComparator', () => {
it('routes both-null disk values through the name/id tiebreaker and orders empties last', () => {
const nullDisk = { total: 0, used: 50, free: 0, usage: NaN };
const guests = [
makeGuest(1, { id: 'zz', name: 'zebra', disk: nullDisk }),
makeGuest(2, { id: 'aa', name: 'alpha', disk: nullDisk }),
makeGuest(3, { id: 'mm', name: 'mid', disk: { total: 100, used: 20, free: 80, usage: 0.2 } }),
makeGuest(4, { id: 'bb', name: 'bravo', disk: { total: 100, used: 90, free: 10, usage: 0.9 } }),
];
const diskAsc = createWorkloadSortComparator('disk', 'asc');
const diskDesc = createWorkloadSortComparator('disk', 'desc');
// Non-empty values sort first (asc: mid=20, bravo=90); both-null pair falls
// through aIsEmpty && bIsEmpty -> tiebreak (alpha < zebra).
expect([...guests].sort(diskAsc!).map((g) => g.id)).toEqual(['mm', 'bb', 'aa', 'zz']);
// desc only flips the non-empty comparison; empties still sort last by name asc.
expect([...guests].sort(diskDesc!).map((g) => g.id)).toEqual(['bb', 'mm', 'aa', 'zz']);
});
it('uses the name/id tiebreaker when a generic key is undefined on every guest', () => {
const guests = [
makeGuest(1, { id: 'b', name: 'beta' }),
makeGuest(2, { id: 'a', name: 'alpha' }),
];
const cmp = createWorkloadSortComparator('nonexistentField', 'asc');
// Every value is undefined -> both empty -> tiebreak by name (alpha < beta).
expect([...guests].sort(cmp!).map((g) => g.id)).toEqual(['a', 'b']);
});
it('returns 0 when numeric values are equal and both names and ids match', () => {
const a = makeGuest(1, { id: 'dup', name: 'same', cpu: 0.5 });
const b = makeGuest(2, { id: 'dup', name: 'same', cpu: 0.5 });
const cmp = createWorkloadSortComparator('cpu', 'asc');
// Equal numeric values -> tiebreak; equal names -> id compare; equal ids -> 0.
expect(cmp!(a, b)).toBe(0);
expect(cmp!(b, a)).toBe(0);
});
});
describe('filterWorkloads', () => {
it('skips the node-scope filter in pod view mode so pods on other nodes survive', () => {
const guests = [
makeGuest(1, {
id: 'pod-a',
name: 'api',
type: 'pod',
workloadType: 'pod',
node: 'worker-a',
instance: 'ctx',
contextLabel: 'ctx',
namespace: 'default',
status: 'running',
}),
makeGuest(2, {
id: 'pod-b',
name: 'web',
type: 'pod',
workloadType: 'pod',
node: 'worker-b',
instance: 'ctx',
contextLabel: 'ctx',
namespace: 'default',
status: 'running',
}),
];
const result = filterWorkloads({
...baseFilterParams,
guests,
viewMode: 'pod',
selectedNode: 'worker-a',
});
// workloadHostScopeId(pod) === '' would remove both if the node filter ran;
// because viewMode === 'pod' the guard is skipped and both pods remain.
expect(result.map((g) => g.id)).toEqual(['pod-a', 'pod-b']);
});
it('excludes non-pod guests during kubernetes namespace filtering in pod view', () => {
const guests = [
makeGuest(1, {
id: 'pod-a',
name: 'api',
type: 'pod',
workloadType: 'pod',
node: 'worker-a',
instance: 'ctx',
contextLabel: 'ctx',
namespace: 'payments',
status: 'running',
}),
makeGuest(2, {
id: 'vm-a',
name: 'vm',
type: 'vm',
workloadType: 'vm',
node: 'node-a',
instance: 'inst-a',
namespace: 'payments',
status: 'running',
}),
];
const result = filterWorkloads({
...baseFilterParams,
guests,
viewMode: 'pod',
selectedKubernetesNamespace: 'payments',
});
// The vm hits `resolveWorkloadType(g) !== 'pod' -> return false` in the
// namespace filter and is dropped (it would also be dropped by the later
// view-mode filter, but this exercises the guard's true arm).
expect(result.map((g) => g.id)).toEqual(['pod-a']);
});
it('excludes system-containers from the runtime filter under the combined container view mode', () => {
const guests = [
makeGuest(1, {
id: 'app-1',
name: 'redis',
type: 'app-container',
workloadType: 'app-container',
containerRuntime: 'docker',
contextLabel: 'host-a',
node: '',
instance: '',
status: 'running',
}),
makeGuest(2, {
id: 'lxc-1',
name: 'db',
type: 'lxc',
workloadType: 'system-container',
instance: 'inst-a',
node: 'node-a',
containerRuntime: 'docker',
status: 'running',
}),
];
const result = filterWorkloads({
...baseFilterParams,
guests,
viewMode: 'container',
containerRuntime: 'docker',
});
// 'container' view mode keeps both app- and system-containers via
// workloadMatchesViewMode, so the system-container reaches the runtime
// filter where `resolveWorkloadType(g) !== 'app-container' -> return false`.
expect(result.map((g) => g.id)).toEqual(['app-1']);
});
it('does not apply the platform filter when the normalized platform is "all"', () => {
const guests = [
makeGuest(1, {
id: 'truenas-1',
name: 'nextcloud',
type: 'app-container',
workloadType: 'app-container',
platformScopes: ['truenas'],
contextLabel: 'host-a',
node: '',
instance: '',
status: 'running',
}),
makeGuest(2, {
id: 'docker-1',
name: 'grafana',
type: 'app-container',
workloadType: 'app-container',
platformScopes: ['docker'],
contextLabel: 'host-b',
node: '',
instance: '',
status: 'running',
}),
];
const result = filterWorkloads({
...baseFilterParams,
guests,
selectedPlatform: 'all',
});
// normalizeSourcePlatformQueryValue('all') === 'all' -> filter skipped.
expect(result.map((g) => g.id)).toEqual(['truenas-1', 'docker-1']);
});
it('counts an empty-status guest as degraded via the (status || "") fallback in degraded mode', () => {
const guests = [
makeGuest(1, { id: 'empty', name: 'empty', status: '' }),
makeGuest(2, { id: 'running', name: 'running', status: 'running' }),
makeGuest(3, { id: 'offline', name: 'offline', status: 'offline' }),
];
const result = filterWorkloads({
...baseFilterParams,
guests,
statusMode: 'degraded',
});
// '' -> toLowerCase '' -> not DEGRADED, !== 'running', not OFFLINE -> degraded.
expect(result.map((g) => g.id)).toEqual(['empty']);
});
it('matches status case-sensitively in running mode (capital "Running" is excluded)', () => {
const guests = [
makeGuest(1, { id: 'capital', name: 'capital', status: 'Running' }),
makeGuest(2, { id: 'lower', name: 'lower', status: 'running' }),
];
const result = filterWorkloads({
...baseFilterParams,
guests,
statusMode: 'running',
});
// g.status === 'running' is an exact, case-sensitive comparison.
expect(result.map((g) => g.id)).toEqual(['lower']);
});
it('matches the numeric vmid and string status/instance text-search candidates', () => {
const guests = [
makeGuest(1, { id: 'g1', name: 'numhost', vmid: 7777, status: 'running' }),
makeGuest(2, { id: 'g2', name: 'stathost', vmid: 1, status: 'quarantined' }),
makeGuest(3, { id: 'g3', name: 'insthost', vmid: 2, status: 'running', instance: 'insttoken' }),
];
// vmid is a number candidate -> filtered into the String(value) match.
expect(
filterWorkloads({ ...baseFilterParams, guests, searchTerm: '7777' }).map((g) => g.id),
).toEqual(['g1']);
// status candidate.
expect(
filterWorkloads({ ...baseFilterParams, guests, searchTerm: 'quarantined' }).map((g) => g.id),
).toEqual(['g2']);
// instance candidate.
expect(
filterWorkloads({ ...baseFilterParams, guests, searchTerm: 'insttoken' }).map((g) => g.id),
).toEqual(['g3']);
});
});
describe('matchesWorkloadTextSearch (exercised via filterWorkloads)', () => {
it('excludes a guest whose only matching candidate is an empty platformScopes array', () => {
// platformScopes is joined with ' ' -> '' for an empty array; the type
// guard inside matchesWorkloadTextSearch filters non-string/number values,
// and an empty joined string never satisfies .includes(needle).
const guests = [
makeGuest(1, {
id: 'scope-1',
name: 'alpha',
type: 'app-container',
workloadType: 'app-container',
platformScopes: [],
status: 'running',
}),
makeGuest(2, {
id: 'scope-2',
name: 'beta',
type: 'app-container',
workloadType: 'app-container',
platformScopes: ['uniquematch'],
status: 'running',
}),
];
expect(
filterWorkloads({ ...baseFilterParams, guests, searchTerm: 'uniquematch' }).map((g) => g.id),
).toEqual(['scope-2']);
});
});
describe('getWorkloadGroupLabel', () => {
it('returns empty type and the raw context for a vm guest with no node and no cluster', () => {
const guests = [
makeGuest(1, {
id: 'solo',
name: 'solo',
type: 'vm',
workloadType: 'vm',
instance: 'solo',
node: '',
clusterName: '',
}),
];
// groupKey = 'solo-' (no colon) -> prefix not recognized -> first guest has
// no node and no cluster -> final fallback { type: '', name: context }.
expect(getWorkloadGroupLabel('', guests)).toStrictEqual({ type: '', name: 'solo-' });
});
it('joins a multi-segment context after a recognized prefix for an empty guests array', () => {
// normalizedGroupKey = groupKey; split(':') -> ['pod', 'ctx', 'extra'];
// rest joins back to 'ctx:extra'.
expect(getWorkloadGroupLabel('pod:ctx:extra', [])).toStrictEqual({
type: 'Pods',
name: 'ctx:extra',
});
});
});
describe('computeWorkloadStats', () => {
it('classifies empty and non-standard statuses as degraded via the second condition', () => {
const guests = [
makeGuest(1, { id: 'empty', name: 'empty', status: '', type: 'vm', workloadType: 'vm' }),
makeGuest(2, {
id: 'custom',
name: 'custom',
status: 'quarantined',
type: 'vm',
workloadType: 'vm',
}),
makeGuest(3, { id: 'running', name: 'running', status: 'running', type: 'vm', workloadType: 'vm' }),
makeGuest(4, { id: 'offline', name: 'offline', status: 'offline', type: 'vm', workloadType: 'vm' }),
];
// '' and 'quarantined' are neither DEGRADED nor OFFLINE nor running, so the
// (status !== 'running' && !OFFLINE) second condition counts them degraded.
expect(computeWorkloadStats(guests)).toStrictEqual({
total: 4,
running: 1,
degraded: 2,
stopped: 1,
vms: 4,
containers: 0,
appContainers: 0,
pods: 0,
});
});
it('classifies a capitalized "Running" status as stopped due to case-sensitive running check', () => {
const guests = [
makeGuest(1, {
id: 'capital',
name: 'capital',
status: 'Running',
type: 'vm',
workloadType: 'vm',
}),
];
// g.status === 'running' is exact; the degraded check lowercases, so
// 'Running' is neither running nor degraded and lands in stopped.
expect(computeWorkloadStats(guests)).toStrictEqual({
total: 1,
running: 0,
degraded: 0,
stopped: 1,
vms: 1,
containers: 0,
appContainers: 0,
pods: 0,
});
});
});
});
@@ -0,0 +1,394 @@
import { describe, expect, it, vi } from 'vitest';
import type { DockerContainerUpdateStatus } from '@/types/api';
import {
getContainerUpdateBadgeTooltip,
getUpdateIconTooltip,
getUpdateButtonTooltip,
getUpdateButtonLabel,
getUpdateButtonClass,
getContainerUpdateErrorTooltip,
getContainerUpdateCurrentTooltip,
} from '@/components/shared/containerUpdateBadgeModel';
import type { UpdateState } from '@/components/shared/containerUpdateBadgeModel';
// Mirrors the module-private UPDATE_BUTTON_BASE_CLASS so the class assertions
// are exact-string equality rather than substring/truthiness checks.
const UPDATE_BUTTON_BASE_CLASS =
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium transition-all';
// A digest comfortably longer than the longest preview length used (19) so the
// truncation arms are genuinely exercised. 'sha256:' is 7 chars.
const LONG_DIGEST = 'sha256:' + 'a'.repeat(40);
const DIGEST_FIRST_19 = 'sha256:' + 'a'.repeat(12); // 7 + 12 = 19
const DIGEST_FIRST_12 = 'sha256:' + 'a'.repeat(5); // 7 + 5 = 12
function makeUpdateStatus(
overrides: Partial<DockerContainerUpdateStatus> = {},
): DockerContainerUpdateStatus {
return {
updateAvailable: true,
lastChecked: 1_000,
...overrides,
};
}
describe('containerUpdateBadgeModel.branchcov2', () => {
describe('getContainerUpdateBadgeTooltip', () => {
it('falls back to "unknown" for both digests when updateStatus is undefined', () => {
expect(getContainerUpdateBadgeTooltip(undefined)).toBe(
'Image update available\nCurrent: unknown...\nLatest: unknown...',
);
});
it('falls back to "unknown" when both digests are absent on a defined status', () => {
expect(getContainerUpdateBadgeTooltip(makeUpdateStatus())).toBe(
'Image update available\nCurrent: unknown...\nLatest: unknown...',
);
});
it('truncates both digests to 19 characters', () => {
const status = makeUpdateStatus({
currentDigest: LONG_DIGEST,
latestDigest: LONG_DIGEST,
});
expect(getContainerUpdateBadgeTooltip(status)).toBe(
`Image update available\nCurrent: ${DIGEST_FIRST_19}...\nLatest: ${DIGEST_FIRST_19}...`,
);
});
it('mixes a present currentDigest with an absent latestDigest', () => {
const status = makeUpdateStatus({
currentDigest: LONG_DIGEST,
latestDigest: undefined,
});
expect(getContainerUpdateBadgeTooltip(status)).toBe(
`Image update available\nCurrent: ${DIGEST_FIRST_19}...\nLatest: unknown...`,
);
});
it('treats empty-string digests as unknown (|| fallback)', () => {
const status = makeUpdateStatus({ currentDigest: '', latestDigest: '' });
expect(getContainerUpdateBadgeTooltip(status)).toBe(
'Image update available\nCurrent: unknown...\nLatest: unknown...',
);
});
});
describe('getUpdateIconTooltip', () => {
it('returns the short tooltip via the early return when updateStatus is undefined', () => {
expect(getUpdateIconTooltip(undefined)).toBe('Image update available');
});
it('falls back to "unknown" for both digests on a defined status with no digests', () => {
expect(getUpdateIconTooltip(makeUpdateStatus())).toBe(
'Update available\nCurrent: unknown...\nLatest: unknown...',
);
});
it('truncates both digests to 12 characters (icon uses a shorter preview than the badge)', () => {
const status = makeUpdateStatus({
currentDigest: LONG_DIGEST,
latestDigest: LONG_DIGEST,
});
expect(getUpdateIconTooltip(status)).toBe(
`Update available\nCurrent: ${DIGEST_FIRST_12}...\nLatest: ${DIGEST_FIRST_12}...`,
);
});
it('handles empty-string digests via the || fallback', () => {
const status = makeUpdateStatus({ currentDigest: '', latestDigest: '' });
expect(getUpdateIconTooltip(status)).toBe(
'Update available\nCurrent: unknown...\nLatest: unknown...',
);
});
});
describe('getUpdateButtonTooltip', () => {
it('returns the confirm prompt for the "confirming" state', () => {
expect(
getUpdateButtonTooltip({ state: 'confirming', now: 1000 }),
).toBe('Click again to confirm update');
});
it('returns the success message for the "success" state', () => {
expect(getUpdateButtonTooltip({ state: 'success', now: 1000 })).toBe(
'✓ Update completed successfully!',
);
});
it('defaults step to "Processing..." and elapsed to 0 when storeState is missing (updating)', () => {
expect(
getUpdateButtonTooltip({ state: 'updating', now: 100_000 }),
).toBe('Processing... (0s)');
});
it('uses storeState.message and computes elapsed seconds when elapsed <= 60', () => {
const result = getUpdateButtonTooltip({
state: 'updating',
now: 100_000,
storeState: { startedAt: 95_000, message: 'Pulling layers' },
});
expect(result).toBe('Pulling layers (5s)');
});
it('falls back to "Processing..." when storeState exists but message is absent/empty', () => {
expect(
getUpdateButtonTooltip({
state: 'updating',
now: 100_000,
storeState: { startedAt: 100_000 },
}),
).toBe('Processing... (0s)');
expect(
getUpdateButtonTooltip({
state: 'updating',
now: 100_000,
storeState: { startedAt: 95_000, message: '' },
}),
).toBe('Processing... (5s)');
});
it('formats as minutes+seconds when elapsed > 60', () => {
const result = getUpdateButtonTooltip({
state: 'updating',
now: 1_000_000,
storeState: { startedAt: 875_000, message: 'Extracting' },
});
// elapsed = round(125000/1000) = 125 -> 2m 5s
expect(result).toBe('Extracting (2m 5s)');
});
it('keeps the seconds format exactly at the 60-second boundary (not > 60)', () => {
const result = getUpdateButtonTooltip({
state: 'updating',
now: 1_000_000,
storeState: { startedAt: 940_000, message: 'Working' },
});
// elapsed = 60 -> not > 60 -> seconds form
expect(result).toBe('Working (60s)');
});
it('falls back to Date.now() for `now` when omitted (?? right arm) — observed via updating elapsed', () => {
const spy = vi.spyOn(Date, 'now').mockReturnValue(200_000);
try {
const result = getUpdateButtonTooltip({
state: 'updating',
storeState: { startedAt: 150_000, message: 'Working' },
});
// elapsed = round((200000 - 150000)/1000) = 50
expect(result).toBe('Working (50s)');
} finally {
spy.mockRestore();
}
});
it('uses storeState.message for the error tooltip when present', () => {
expect(
getUpdateButtonTooltip({
state: 'error',
now: 1000,
storeState: { startedAt: 0, message: 'image not found' },
}),
).toBe('✗ Update failed: image not found');
});
it('falls back to errorMessage when storeState.message is missing (error)', () => {
expect(
getUpdateButtonTooltip({
state: 'error',
now: 1000,
errorMessage: 'network timeout',
}),
).toBe('✗ Update failed: network timeout');
});
it('falls back to "Unknown error" when neither storeState.message nor errorMessage is set', () => {
expect(getUpdateButtonTooltip({ state: 'error', now: 1000 })).toBe(
'✗ Update failed: Unknown error',
);
});
it('returns "Update container" for the default state when updateStatus is absent', () => {
expect(getUpdateButtonTooltip({ state: 'idle', now: 1000 })).toBe(
'Update container',
);
});
it('truncates digests to 12 chars for the default state when updateStatus is present', () => {
const result = getUpdateButtonTooltip({
state: 'idle',
now: 1000,
updateStatus: makeUpdateStatus({
currentDigest: LONG_DIGEST,
latestDigest: LONG_DIGEST,
}),
});
expect(result).toBe(
`Click to update\nCurrent: ${DIGEST_FIRST_12}...\nLatest: ${DIGEST_FIRST_12}...`,
);
});
});
describe('getUpdateButtonLabel', () => {
it('short-circuits to "Update" when settingsLoaded is false, regardless of state', () => {
const states: UpdateState[] = [
'confirming',
'updating',
'success',
'error',
'idle',
];
for (const state of states) {
expect(getUpdateButtonLabel(state, false)).toBe('Update');
}
});
it('returns "Confirm?" for the confirming state', () => {
expect(getUpdateButtonLabel('confirming', true)).toBe('Confirm?');
});
it('returns "Updating..." for the updating state', () => {
expect(getUpdateButtonLabel('updating', true)).toBe('Updating...');
});
it('returns "Queued!" for the success state', () => {
expect(getUpdateButtonLabel('success', true)).toBe('Queued!');
});
it('returns "Failed" for the error state', () => {
expect(getUpdateButtonLabel('error', true)).toBe('Failed');
});
it('returns "Update" for the default (idle) state', () => {
expect(getUpdateButtonLabel('idle', true)).toBe('Update');
});
});
describe('getUpdateButtonClass', () => {
it('returns the amber confirming classes with pointer hover', () => {
expect(getUpdateButtonClass('confirming')).toBe(
`${UPDATE_BUTTON_BASE_CLASS} bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300 cursor-pointer hover:bg-amber-200 dark:hover:bg-amber-900`,
);
});
it('returns the blue updating classes with cursor-wait', () => {
expect(getUpdateButtonClass('updating')).toBe(
`${UPDATE_BUTTON_BASE_CLASS} bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 cursor-wait`,
);
});
it('returns the green success classes with no special cursor', () => {
expect(getUpdateButtonClass('success')).toBe(
`${UPDATE_BUTTON_BASE_CLASS} bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300`,
);
});
it('returns the red error classes with cursor-help', () => {
expect(getUpdateButtonClass('error')).toBe(
`${UPDATE_BUTTON_BASE_CLASS} bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300 cursor-help`,
);
});
it('returns the blue idle/default classes with pointer hover', () => {
expect(getUpdateButtonClass('idle')).toBe(
`${UPDATE_BUTTON_BASE_CLASS} bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-900`,
);
});
});
// getDigestPreview is module-private (not exported), so its branches are
// exercised transitively through the public callers above and below. The
// empty-string / undefined / truncation arms are all reached via the badge,
// icon, current, and button-default tooltip tests.
describe('getDigestPreview (via public callers)', () => {
it('returns "unknown" for an undefined digest (badge tooltip, undefined status)', () => {
expect(getContainerUpdateBadgeTooltip(undefined)).toContain(
'Current: unknown...',
);
});
it('returns the full digest when it is shorter than the preview length', () => {
const shortDigest = 'sha256:abc'; // 10 chars < 19
const status = makeUpdateStatus({
currentDigest: shortDigest,
latestDigest: shortDigest,
});
expect(getContainerUpdateBadgeTooltip(status)).toBe(
'Image update available\nCurrent: sha256:abc...\nLatest: sha256:abc...',
);
});
it('truncates at exactly the requested length (12 for icon, 19 for badge)', () => {
const status = makeUpdateStatus({
currentDigest: LONG_DIGEST,
latestDigest: LONG_DIGEST,
});
expect(getContainerUpdateBadgeTooltip(status)).toContain(
`Current: ${DIGEST_FIRST_19}...`,
);
expect(getUpdateIconTooltip(status)).toContain(
`Current: ${DIGEST_FIRST_12}...`,
);
});
});
describe('getContainerUpdateErrorTooltip', () => {
it('falls back to "Unknown error" when updateStatus is undefined', () => {
expect(getContainerUpdateErrorTooltip(undefined)).toBe(
'Update check failed: Unknown error',
);
});
it('falls back to "Unknown error" when error is absent on a defined status', () => {
expect(getContainerUpdateErrorTooltip(makeUpdateStatus())).toBe(
'Update check failed: Unknown error',
);
});
it('embeds the concrete error message when present', () => {
const status = makeUpdateStatus({ error: 'registry unreachable' });
expect(getContainerUpdateErrorTooltip(status)).toBe(
'Update check failed: registry unreachable',
);
});
it('falls back to "Unknown error" for an empty-string error (|| falsy arm)', () => {
const status = makeUpdateStatus({ error: '' });
expect(getContainerUpdateErrorTooltip(status)).toBe(
'Update check failed: Unknown error',
);
});
});
describe('getContainerUpdateCurrentTooltip', () => {
it('returns the plain "Image is current" via the early return when updateStatus is undefined', () => {
expect(getContainerUpdateCurrentTooltip(undefined)).toBe('Image is current');
});
it('takes the early return when currentDigest is absent', () => {
expect(getContainerUpdateCurrentTooltip(makeUpdateStatus())).toBe(
'Image is current',
);
});
it('takes the early return for an empty-string currentDigest (falsy guard)', () => {
const status = makeUpdateStatus({ currentDigest: '' });
expect(getContainerUpdateCurrentTooltip(status)).toBe('Image is current');
});
it('appends a 12-char digest preview when currentDigest is present', () => {
const status = makeUpdateStatus({ currentDigest: LONG_DIGEST });
expect(getContainerUpdateCurrentTooltip(status)).toBe(
`Image is current\nDigest: ${DIGEST_FIRST_12}...`,
);
});
it('shows the full digest (no truncation effect) when it is shorter than 12', () => {
const status = makeUpdateStatus({ currentDigest: 'sha256:xy' });
expect(getContainerUpdateCurrentTooltip(status)).toBe(
'Image is current\nDigest: sha256:xy...',
);
});
});
});
@@ -0,0 +1,385 @@
import { describe, expect, it } from 'vitest';
import type { AggregatedMetricPoint, HistoryTimeRange } from '@/api/charts';
import {
createHistoryChartGeometry,
findHistoryChartClosestPoint,
formatHistoryChartTimeLabel,
formatHistoryChartTooltipValue,
getHistoryChartDataMax,
getHistoryChartDataMin,
getHistoryChartDefaultColor,
getHistoryChartRefreshIntervalMs,
getHistoryChartScale,
getHistoryChartTooltipLayout,
getHistoryChartYAxisLabels,
} from '@/components/shared/historyChartModel';
const pt = (
timestamp: number,
value: number,
min: number,
max: number,
): AggregatedMetricPoint => ({ timestamp, value, min, max });
describe('formatHistoryChartTooltipValue', () => {
it('formats percentage units with one decimal', () => {
expect(formatHistoryChartTooltipValue(42.35, '%')).toBe('42.4%');
});
it('formats byte-rate units via formatBytes with a /s suffix', () => {
expect(formatHistoryChartTooltipValue(1024, 'B/s')).toBe('1.00 KB/s');
});
it('formats celsius units by rounding to the nearest degree', () => {
expect(formatHistoryChartTooltipValue(23.6, 'C')).toBe('24°C');
});
it('falls back to raw formatBytes output when unit is undefined', () => {
expect(formatHistoryChartTooltipValue(0)).toBe('0 B');
expect(formatHistoryChartTooltipValue(2048)).toBe('2.00 KB');
});
it('treats an empty-string unit the same as no unit', () => {
expect(formatHistoryChartTooltipValue(2048, '')).toBe('2.00 KB');
});
it('renders integer values for arbitrary units without decimals', () => {
expect(formatHistoryChartTooltipValue(42, 'rpm')).toBe('42 rpm');
});
it('renders fractional values for arbitrary units with one decimal', () => {
expect(formatHistoryChartTooltipValue(42.5, 'rpm')).toBe('42.5 rpm');
});
});
describe('getHistoryChartRefreshIntervalMs', () => {
it.each<[HistoryTimeRange, number]>([
['7d', 30000],
['14d', 30000],
['30d', 60000],
['90d', 120000],
['1h', 10000],
['30m', 10000],
])('returns the expected refresh interval for range %s', (range, expected) => {
expect(getHistoryChartRefreshIntervalMs(range)).toBe(expected);
});
});
describe('getHistoryChartDefaultColor', () => {
it('returns the explicit color override when provided', () => {
expect(getHistoryChartDefaultColor('cpu', '#custom')).toBe('#custom');
});
it.each<[string, string]>([
['cpu', '#8b5cf6'],
['memory', '#f59e0b'],
['disk', '#10b981'],
['network', '#3b82f6'],
])('uses the metric-specific default color for %s', (metric, color) => {
expect(getHistoryChartDefaultColor(metric)).toBe(color);
});
});
describe('getHistoryChartDataMin', () => {
it('returns null for an empty point set', () => {
expect(getHistoryChartDataMin([])).toBeNull();
});
it('uses point.min when it is present (including zero)', () => {
const points = [pt(1, 100, 10, 200), pt(2, 50, 5, 60)];
expect(getHistoryChartDataMin(points)).toBe(5);
});
it('keeps a zero min because the null-check guards against falsy zero', () => {
const points = [pt(1, 100, 0, 200), pt(2, 50, 5, 60)];
expect(getHistoryChartDataMin(points)).toBe(0);
});
it('falls back to point.value when min is null', () => {
const nullMin = { timestamp: 1, value: 7, min: null, max: null } as unknown as AggregatedMetricPoint;
const value = { timestamp: 2, value: 3, min: null, max: null } as unknown as AggregatedMetricPoint;
expect(getHistoryChartDataMin([nullMin, value])).toBe(3);
});
});
describe('getHistoryChartDataMax', () => {
it('returns null for an empty point set', () => {
expect(getHistoryChartDataMax([])).toBeNull();
});
it('uses point.max when it is present (including zero)', () => {
const points = [pt(1, 100, 10, 200), pt(2, 50, 5, 60)];
expect(getHistoryChartDataMax(points)).toBe(200);
});
it('keeps a zero max because the null-check guards against falsy zero', () => {
const points = [pt(1, 100, 0, 0), pt(2, 50, 5, 60)];
expect(getHistoryChartDataMax(points)).toBe(60);
});
it('falls back to point.value when max is null', () => {
const low = { timestamp: 1, value: 7, min: null, max: null } as unknown as AggregatedMetricPoint;
const high = { timestamp: 2, value: 99, min: null, max: null } as unknown as AggregatedMetricPoint;
expect(getHistoryChartDataMax([low, high])).toBe(99);
});
});
describe('getHistoryChartScale', () => {
it('returns the 0..100 baseline when there are no points and no unit (byte-like)', () => {
expect(getHistoryChartScale([])).toStrictEqual({
isPercentLike: false,
isByteLike: true,
minValue: 0,
maxValue: 100,
});
});
it('clamps percent-like scales to at least 100 even with low rawMax', () => {
expect(getHistoryChartScale([pt(1, 10, 0, 30)], '%')).toStrictEqual({
isPercentLike: true,
isByteLike: false,
minValue: 0,
maxValue: 100,
});
});
it('lets percent-like scales exceed 100 when rawMax is larger', () => {
expect(getHistoryChartScale([pt(1, 10, 0, 150)], '%').maxValue).toBe(150);
});
it('applies the 1.15 headroom factor for byte-rate units', () => {
expect(getHistoryChartScale([pt(1, 0, 0, 100)], 'B/s').maxValue).toBeCloseTo(115, 10);
});
it('treats an undefined unit as byte-like and applies the headroom factor', () => {
const scale = getHistoryChartScale([pt(1, 0, 0, 10)]);
expect(scale.isByteLike).toBe(true);
expect(scale.maxValue).toBeCloseTo(11.5, 6);
});
it('falls back to point.value for rawMax when point.max is falsy zero', () => {
expect(getHistoryChartScale([pt(1, 50, 0, 0)], 'B/s').maxValue).toBeCloseTo(57.5, 10);
});
it('clamps non-byte, non-percent scales to a minimum of 1', () => {
expect(getHistoryChartScale([pt(1, 0, 0, 0)], 'rpm').maxValue).toBe(1);
});
it('marks arbitrary units as neither byte-like nor percent-like', () => {
expect(getHistoryChartScale([pt(1, 0, 0, 100)], 'rpm').isPercentLike).toBe(false);
expect(getHistoryChartScale([pt(1, 0, 0, 100)], 'rpm').isByteLike).toBe(false);
});
});
describe('getHistoryChartYAxisLabels', () => {
it('renders percent labels at the three tick positions', () => {
expect(
getHistoryChartYAxisLabels({ minValue: 0, maxValue: 100, isPercentLike: true, isByteLike: false }),
).toStrictEqual([
{ pct: 0, label: '0%' },
{ pct: 0.5, label: '50%' },
{ pct: 1, label: '100%' },
]);
});
it('renders byte-like 0/avg/max labels', () => {
expect(
getHistoryChartYAxisLabels({ minValue: 0, maxValue: 100, isPercentLike: false, isByteLike: true }),
).toStrictEqual([
{ pct: 0, label: '0' },
{ pct: 0.5, label: 'Avg' },
{ pct: 1, label: 'Max' },
]);
});
it('renders computed numeric labels for other unit kinds', () => {
expect(
getHistoryChartYAxisLabels({ minValue: 10, maxValue: 110, isPercentLike: false, isByteLike: false }),
).toStrictEqual([
{ pct: 0, label: '0' },
{ pct: 0.5, label: '60' },
{ pct: 1, label: '110' },
]);
});
});
describe('formatHistoryChartTimeLabel', () => {
const ts = new Date(2024, 0, 15, 9, 30).getTime();
it.each<HistoryTimeRange>(['7d', '14d', '30d', '90d'])(
'renders a calendar date for the %s range',
(range) => {
expect(formatHistoryChartTimeLabel(ts, range)).toBe(
new Date(ts).toLocaleDateString([], { month: 'short', day: 'numeric' }),
);
},
);
it.each<HistoryTimeRange>(['30m', '1h', '6h', '12h', '24h'])(
'renders a clock time for the %s range',
(range) => {
expect(formatHistoryChartTimeLabel(ts, range)).toBe(
new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
);
},
);
it('produces different output for the day vs time branches at the same timestamp', () => {
expect(formatHistoryChartTimeLabel(ts, '7d')).not.toBe(formatHistoryChartTimeLabel(ts, '1h'));
});
});
describe('createHistoryChartGeometry', () => {
it('maps timestamps and values onto pixel coordinates', () => {
const geo = createHistoryChartGeometry({
width: 200,
height: 100,
startTime: 0,
endTime: 100,
minValue: 0,
maxValue: 10,
});
expect(geo.timeSpan).toBe(100);
expect(geo.getX(50)).toBe(120);
expect(geo.getY(5)).toBe(50);
});
it('clamps a negative/inverted time span down to 1', () => {
const geo = createHistoryChartGeometry({
width: 200,
height: 100,
startTime: 100,
endTime: 50,
minValue: 0,
maxValue: 10,
});
expect(geo.timeSpan).toBe(1);
expect(geo.getX(100)).toBe(40);
});
it('left-pads the first timestamp to the chart origin', () => {
const geo = createHistoryChartGeometry({
width: 200,
height: 100,
startTime: 1000,
endTime: 1100,
minValue: 0,
maxValue: 10,
});
expect(geo.getX(1000)).toBe(40);
expect(geo.getX(1100)).toBe(200);
});
it('inverts the value axis so the max sits at the top padding', () => {
const geo = createHistoryChartGeometry({
width: 200,
height: 100,
startTime: 0,
endTime: 100,
minValue: 0,
maxValue: 10,
});
expect(geo.getY(0)).toBe(80);
expect(geo.getY(10)).toBe(20);
});
});
describe('findHistoryChartClosestPoint', () => {
it('returns the first point when its timestamp is the exact match', () => {
const points = [pt(100, 1, 0, 0), pt(200, 2, 0, 0), pt(300, 3, 0, 0)];
expect(findHistoryChartClosestPoint(points, 100)).toStrictEqual(points[0]);
});
it('updates the closest point as a nearer timestamp is found', () => {
const points = [pt(100, 1, 0, 0), pt(200, 2, 0, 0), pt(300, 3, 0, 0)];
expect(findHistoryChartClosestPoint(points, 210)).toStrictEqual(points[1]);
});
it('keeps the earlier point on an exact tie (strictly-less comparison)', () => {
const points = [pt(100, 1, 0, 0), pt(200, 2, 0, 0)];
expect(findHistoryChartClosestPoint(points, 150)).toStrictEqual(points[0]);
});
it('keeps the first point when no later point is closer', () => {
const points = [pt(100, 1, 0, 0), pt(500, 2, 0, 0)];
expect(findHistoryChartClosestPoint(points, 90)).toStrictEqual(points[0]);
});
});
describe('getHistoryChartTooltipLayout', () => {
it('places the tooltip to the right when only the right side has room', () => {
const layout = getHistoryChartTooltipLayout({
hoveredPoint: { x: 150, y: 70, timestamp: 0, value: 42 },
chartWidth: 420,
chartHeight: 180,
});
expect(layout).toStrictEqual({ x: 162, y: 47, width: 156, height: 46 });
});
it('places the tooltip to the left when only the left side has room', () => {
const layout = getHistoryChartTooltipLayout({
hoveredPoint: { x: 380, y: 70, timestamp: 0, value: 42 },
chartWidth: 420,
chartHeight: 180,
});
expect(layout.x).toBe(212);
expect(layout.x + layout.width).toBeLessThan(380);
});
it('prefers the right side when both sides fit and right room is >= left room', () => {
const layout = getHistoryChartTooltipLayout({
hoveredPoint: { x: 250, y: 70, timestamp: 0, value: 42 },
chartWidth: 500,
chartHeight: 180,
});
expect(layout.x).toBe(262);
});
it('prefers the left side when both sides fit but left room is greater', () => {
const layout = getHistoryChartTooltipLayout({
hoveredPoint: { x: 250, y: 70, timestamp: 0, value: 42 },
chartWidth: 490,
chartHeight: 180,
});
expect(layout.x).toBe(82);
});
it('centers and clamps the tooltip when neither side can fit', () => {
const layout = getHistoryChartTooltipLayout({
hoveredPoint: { x: 90, y: 70, timestamp: 0, value: 42 },
chartWidth: 180,
chartHeight: 180,
});
expect(layout).toStrictEqual({ x: 12, y: 12, width: 156, height: 46 });
});
it('pushes an overlapping tooltip above the hovered point when there is headroom above', () => {
const layout = getHistoryChartTooltipLayout({
hoveredPoint: { x: 90, y: 30, timestamp: 0, value: 42 },
chartWidth: 180,
chartHeight: 180,
});
expect(layout.x).toBe(12);
expect(layout.y).toBe(42);
});
it('clamps the tooltip y to the bottom edge when the hovered point is near the bottom', () => {
const layout = getHistoryChartTooltipLayout({
hoveredPoint: { x: 150, y: 200, timestamp: 0, value: 42 },
chartWidth: 420,
chartHeight: 180,
});
expect(layout.y).toBe(126);
});
});
@@ -0,0 +1,947 @@
import { describe, expect, it } from 'vitest';
import type { Alert } from '@/types/api';
import type { Resource } from '@/types/resource';
import {
MS_PER_HOUR,
applyAlertHistoryWindow,
buildAlertHistoryItems,
buildAlertHistoryParams,
buildAlertRangeSummary,
buildAlertAxisTicks,
buildAlertTrends,
filterAlertHistoryItems,
formatAlertAxisTickLabel,
formatAlertBucketRange,
formatAlertHistoryDuration,
formatAlertHistoryGroupLabel,
getAlertBucketDurationLabel,
getAlertHistoryDaySuffix,
getIncidentRowKey,
groupAlertHistoryItems,
resolveAlertHistoryResourceType,
type AlertHistoryRange,
type AlertTrendSeries,
type HistoryItem,
} from '../alertHistoryModel';
// ---------------------------------------------------------------------------
// Shared fixtures — keep these minimal but real-shaped so the production
// code paths (resource-type resolution, severity filtering, etc.) exercise
// the same branches the UI triggers.
// ---------------------------------------------------------------------------
function makeItem(overrides: Partial<HistoryItem> = {}): HistoryItem {
return {
id: 'alert-1',
source: 'alert',
status: 'resolved',
startTime: '2026-03-22T09:00:00.000Z',
duration: '1h',
resourceName: 'vm-101',
resourceType: 'VM',
severity: 'critical',
title: 'CPU High',
...overrides,
};
}
function makeAlert(overrides: Partial<Alert> = {}): Alert {
return {
id: 'alert-1',
type: 'cpu',
level: 'critical',
resourceId: 'resource-1',
resourceName: 'vm-101',
node: 'px1',
instance: 'cpu',
message: 'CPU high',
value: 90,
threshold: 80,
startTime: '2026-03-22T09:00:00.000Z',
acknowledged: false,
...overrides,
} as Alert;
}
function makeResource(overrides: Partial<Resource> = {}): Resource {
return {
id: 'resource-1',
type: 'vm',
name: 'vm-101',
displayName: 'vm-101',
...overrides,
} as unknown as Resource;
}
// ---------------------------------------------------------------------------
// buildAlertHistoryParams — exact limit/startTime for each switch arm.
// ---------------------------------------------------------------------------
describe('buildAlertHistoryParams', () => {
const now = Date.UTC(2026, 2, 22, 12, 0, 0);
it('emits limit 2000 and a startTime exactly 24h before now for "24h"', () => {
expect(buildAlertHistoryParams('24h', now)).toStrictEqual({
limit: 2000,
startTime: new Date(now - 24 * MS_PER_HOUR).toISOString(),
});
});
it('emits limit 10000 and a startTime exactly 7d before now for "7d"', () => {
expect(buildAlertHistoryParams('7d', now)).toStrictEqual({
limit: 10000,
startTime: new Date(now - 7 * 24 * MS_PER_HOUR).toISOString(),
});
});
it('emits limit 10000 and a startTime exactly 30d before now for "30d"', () => {
expect(buildAlertHistoryParams('30d', now)).toStrictEqual({
limit: 10000,
startTime: new Date(now - 30 * 24 * MS_PER_HOUR).toISOString(),
});
});
it('emits limit 0 and no startTime for "all"', () => {
expect(buildAlertHistoryParams('all', now)).toStrictEqual({ limit: 0 });
});
it('falls back to limit 1000 with no startTime for an unrecognised range', () => {
expect(
buildAlertHistoryParams('bogus' as AlertHistoryRange, now),
).toStrictEqual({ limit: 1000 });
});
});
// ---------------------------------------------------------------------------
// formatAlertHistoryDuration — negative-duration guard, now-fallback, and
// the three magnitude arms at their boundaries.
// ---------------------------------------------------------------------------
describe('formatAlertHistoryDuration', () => {
it('returns "0m" when end is before start (negative duration)', () => {
expect(
formatAlertHistoryDuration('2026-03-22T10:00:00.000Z', '2026-03-22T09:00:00.000Z'),
).toBe('0m');
});
it('uses the provided `now` when endTime is omitted', () => {
const start = '2026-03-22T09:00:00.000Z';
const now = Date.UTC(2026, 2, 22, 9, 30, 0);
expect(formatAlertHistoryDuration(start, undefined, now)).toBe('30m');
});
it('returns "0m" for a zero-length duration (start === end)', () => {
const start = '2026-03-22T09:00:00.000Z';
expect(formatAlertHistoryDuration(start, start)).toBe('0m');
});
it('formats the minute-only arm with leading minutes under one hour', () => {
expect(
formatAlertHistoryDuration('2026-03-22T09:00:00.000Z', '2026-03-22T09:05:00.000Z'),
).toBe('5m');
});
it('renders the residual minutes even when hours divide evenly', () => {
// exactly 1 hour → minutes=60, hours=1, days=0 → "1h 0m"
expect(
formatAlertHistoryDuration('2026-03-22T09:00:00.000Z', '2026-03-22T10:00:00.000Z'),
).toBe('1h 0m');
});
it('renders the residual hours even when days divide evenly', () => {
// exactly 1 day → minutes=1440, hours=24, days=1 → "1d 0h"
expect(
formatAlertHistoryDuration('2026-03-22T09:00:00.000Z', '2026-03-23T09:00:00.000Z'),
).toBe('1d 0h');
});
});
// ---------------------------------------------------------------------------
// formatAlertBucketRange — same-day, cross-day, and cross-year (which forces
// the `year: 'numeric'` option on the start formatter).
// ---------------------------------------------------------------------------
describe('formatAlertBucketRange', () => {
it('uses the en-dash separator and a single date for an intra-day bucket', () => {
const start = Date.UTC(2026, 2, 22, 12, 0, 0);
const end = Date.UTC(2026, 2, 22, 13, 0, 0);
const label = formatAlertBucketRange(start, end, 'en-US');
expect(label).toContain('\u2013'); //
expect(label).not.toContain('\u2192'); // →
expect(label).toContain('Mar 22');
expect(label.startsWith('Mar 22,')).toBe(true);
});
it('uses the arrow separator and both dates when the bucket spans midnight', () => {
const start = Date.UTC(2026, 2, 22, 22, 0, 0);
const end = Date.UTC(2026, 2, 23, 4, 0, 0);
const label = formatAlertBucketRange(start, end, 'en-US');
expect(label).toContain('\u2192'); // →
expect(label).not.toContain('\u2013'); //
expect(label).toContain('Mar 22');
expect(label).toContain('Mar 23');
});
it('adds a year to the start segment when start and end fall in different years', () => {
const start = Date.UTC(2025, 11, 31, 22, 0, 0);
const end = Date.UTC(2026, 0, 1, 2, 0, 0);
const label = formatAlertBucketRange(start, end, 'en-US');
expect(label).toContain('2025');
expect(label).toContain('Dec 31');
expect(label).toContain('Jan 1');
expect(label).toContain('2026');
// Cross-day so the arrow separator is used.
expect(label).toContain('\u2192');
});
});
// ---------------------------------------------------------------------------
// resolveAlertHistoryResourceType — every early-return and fallback arm.
// ---------------------------------------------------------------------------
describe('resolveAlertHistoryResourceType', () => {
it('returns the metadata resourceType when it is a non-empty string', () => {
expect(
resolveAlertHistoryResourceType({
resourceName: 'vm-101',
metadata: { resourceType: 'Custom' },
resourceId: 'resource-1',
getResource: () => makeResource({ type: 'vm' }),
allResources: [makeResource({ type: 'vm' })],
}),
).toBe('Custom');
});
it('falls through when metadata.resourceType is only whitespace', () => {
const result = resolveAlertHistoryResourceType({
resourceName: 'vm-101',
metadata: { resourceType: ' ' },
resourceId: 'resource-1',
getResource: () => makeResource({ type: 'vm' }),
allResources: [],
});
expect(result).toBe('VM');
});
it('falls through when metadata.resourceType is not a string (number)', () => {
const result = resolveAlertHistoryResourceType({
resourceName: 'vm-101',
metadata: { resourceType: 42 },
resourceId: 'resource-1',
getResource: () => makeResource({ type: 'vm' }),
allResources: [],
});
expect(result).toBe('VM');
});
it('falls through when metadata is undefined', () => {
const result = resolveAlertHistoryResourceType({
resourceName: 'vm-101',
metadata: undefined,
resourceId: 'resource-1',
getResource: () => makeResource({ type: 'vm' }),
allResources: [],
});
expect(result).toBe('VM');
});
it('resolves via getResource when resourceId is provided and the lookup hits', () => {
expect(
resolveAlertHistoryResourceType({
resourceName: 'whatever',
resourceId: 'resource-1',
getResource: (id) => (id === 'resource-1' ? makeResource({ type: 'vm' }) : undefined),
allResources: [],
}),
).toBe('VM');
});
it('falls through to a name match when getResource returns undefined', () => {
const result = resolveAlertHistoryResourceType({
resourceName: 'vm-101',
resourceId: 'missing',
getResource: () => undefined,
allResources: [makeResource({ name: 'vm-101', type: 'vm' })],
});
expect(result).toBe('VM');
});
it('matches a resource by displayName when the name match misses', () => {
const result = resolveAlertHistoryResourceType({
resourceName: 'pretty-vm',
resourceId: 'missing',
getResource: () => undefined,
allResources: [
makeResource({ name: 'other', displayName: 'pretty-vm', type: 'app-container' }),
],
});
expect(result).toBe('Container');
});
it('returns "Unknown" when no resolution path succeeds', () => {
expect(
resolveAlertHistoryResourceType({
resourceName: 'lonely',
resourceId: undefined,
getResource: () => undefined,
allResources: [],
}),
).toBe('Unknown');
});
it('returns "Unknown" even with a resourceId when getResource misses and no name matches', () => {
expect(
resolveAlertHistoryResourceType({
resourceName: 'lonely',
resourceId: 'also-missing',
getResource: () => undefined,
allResources: [makeResource({ name: 'someone-else' })],
}),
).toBe('Unknown');
});
});
// ---------------------------------------------------------------------------
// buildAlertHistoryItems — active vs history, acknowledged vs resolved, and
// the active-id suppression of duplicate history rows.
// ---------------------------------------------------------------------------
describe('buildAlertHistoryItems', () => {
const getResource = (id: string): Resource | undefined =>
id === 'resource-1' ? makeResource({ id: 'resource-1', type: 'vm' }) : undefined;
it('returns an empty array when both activeAlerts and alertHistory are empty', () => {
expect(
buildAlertHistoryItems({
activeAlerts: {},
alertHistory: [],
getResource,
allResources: [],
}),
).toStrictEqual([]);
});
it('marks active alerts with status "active" and acknowledged:false', () => {
const items = buildAlertHistoryItems({
activeAlerts: { 'alert-1': makeAlert({ id: 'alert-1', acknowledged: true }) },
alertHistory: [],
getResource,
allResources: [],
now: Date.UTC(2026, 2, 22, 10, 0, 0),
});
expect(items).toHaveLength(1);
expect(items[0]).toMatchObject({
id: 'alert-1',
status: 'active',
acknowledged: false,
resourceType: 'VM',
title: 'CPU',
});
// Active alerts carry no end time.
expect(items[0].endTime).toBeUndefined();
});
it('marks an unacknowledged historical alert as "resolved"', () => {
const items = buildAlertHistoryItems({
activeAlerts: {},
alertHistory: [
makeAlert({
id: 'h-1',
acknowledged: false,
lastSeen: '2026-03-22T09:30:00.000Z',
}),
],
getResource,
allResources: [],
now: Date.UTC(2026, 2, 22, 10, 0, 0),
});
expect(items).toHaveLength(1);
expect(items[0]).toMatchObject({ id: 'h-1', status: 'resolved', acknowledged: false });
expect(items[0].endTime).toBe('2026-03-22T09:30:00.000Z');
});
it('marks an acknowledged historical alert as "acknowledged"', () => {
const items = buildAlertHistoryItems({
activeAlerts: {},
alertHistory: [
makeAlert({
id: 'h-2',
acknowledged: true,
lastSeen: '2026-03-22T09:30:00.000Z',
}),
],
getResource,
allResources: [],
now: Date.UTC(2026, 2, 22, 10, 0, 0),
});
expect(items).toHaveLength(1);
expect(items[0]).toMatchObject({ id: 'h-2', status: 'acknowledged', acknowledged: true });
});
it('does not duplicate an alert that is present in both active and history', () => {
const items = buildAlertHistoryItems({
activeAlerts: { 'alert-1': makeAlert({ id: 'alert-1' }) },
alertHistory: [makeAlert({ id: 'alert-1', lastSeen: '2026-03-22T09:30:00.000Z' })],
getResource,
allResources: [],
now: Date.UTC(2026, 2, 22, 10, 0, 0),
});
expect(items).toHaveLength(1);
expect(items[0].status).toBe('active');
});
it('interleaves active and historical rows preserving both sets', () => {
const items = buildAlertHistoryItems({
activeAlerts: { 'a-1': makeAlert({ id: 'a-1' }) },
alertHistory: [makeAlert({ id: 'h-1', lastSeen: '2026-03-22T09:30:00.000Z' })],
getResource,
allResources: [],
now: Date.UTC(2026, 2, 22, 10, 0, 0),
});
expect(items.map((i) => i.id)).toEqual(['a-1', 'h-1']);
expect(items.map((i) => i.status)).toEqual(['active', 'resolved']);
});
});
// ---------------------------------------------------------------------------
// filterAlertHistoryItems — pass-through identity, multi-field de-dupe, and
// the undefined-field safe-access arms.
// ---------------------------------------------------------------------------
describe('filterAlertHistoryItems', () => {
it('returns the same array reference when no filter is applied', () => {
const items = [makeItem()];
expect(filterAlertHistoryItems(items, 'all', '')).toBe(items);
});
it('matches a search term against the title only when other fields are blank', () => {
const items = [
makeItem({ id: '1', title: 'CPU High', resourceName: 'x', description: '', node: '' }),
makeItem({ id: '2', title: 'Disk Full', resourceName: 'y', description: '', node: '' }),
];
const result = filterAlertHistoryItems(items, 'all', 'cpu');
expect(result.map((i) => i.id)).toEqual(['1']);
});
it('returns a single item even when the term matches multiple of its fields', () => {
const items = [
makeItem({
id: '1',
resourceName: 'cpu-thing',
title: 'CPU',
description: 'cpu spike',
node: 'cpunode',
}),
];
const result = filterAlertHistoryItems(items, 'all', 'cpu');
expect(result).toHaveLength(1);
expect(result[0].id).toBe('1');
});
it('safely filters items whose title/description/node are undefined', () => {
const item = makeItem({ id: 'u', title: undefined, description: undefined, node: undefined });
expect(filterAlertHistoryItems([item], 'all', '')).toHaveLength(1);
expect(filterAlertHistoryItems([item], 'all', 'missing')).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// buildAlertTrends — exercises the `?? rawBucketSize` fallback by forcing a
// range so wide that no nice bucket size is large enough.
// ---------------------------------------------------------------------------
describe('buildAlertTrends (nice-size fallback)', () => {
const now = Date.UTC(2026, 2, 22, 12, 0, 0);
it('falls back to rawBucketSize when the range exceeds the largest nice value', () => {
// ~1801 days ago → rawRangeHours ≈ 43224 → rawBucketSize = ceil(43224/30) = 1441
// which is larger than the max nice value (1440), so `.find()` returns undefined
// and the `?? rawBucketSize` fallback engages.
const alerts = [
makeItem({ startTime: new Date(now - 1801 * 24 * MS_PER_HOUR).toISOString() }),
];
const trends = buildAlertTrends(alerts, 'all', now);
expect(trends.bucketSize).toBe(1441);
expect(trends.rangeHours).toBe(30 * 1441);
expect(trends.buckets).toHaveLength(30);
});
it('caps the bucket count at maxBuckets (30) for a very wide range', () => {
const alerts = [
makeItem({ startTime: new Date(now - 5000 * 24 * MS_PER_HOUR).toISOString() }),
];
const trends = buildAlertTrends(alerts, 'all', now);
expect(trends.buckets.length).toBeLessThanOrEqual(30);
expect(trends.rangeHours).toBe(trends.buckets.length * trends.bucketSize);
});
});
// ---------------------------------------------------------------------------
// applyAlertHistoryWindow — 24h/30d cutoff arms, selected-bar precedence
// over an "all" timeFilter, and the id-equality tiebreaker.
// ---------------------------------------------------------------------------
describe('applyAlertHistoryWindow (cutoff + tiebreak arms)', () => {
const now = Date.UTC(2026, 2, 22, 12, 0, 0);
it('applies the 24h cutoff when no bar is selected', () => {
const trends = buildAlertTrends([], '24h', now);
const items = [
makeItem({ id: 'within', startTime: new Date(now - MS_PER_HOUR).toISOString() }),
makeItem({ id: 'beyond', startTime: new Date(now - 25 * MS_PER_HOUR).toISOString() }),
];
const result = applyAlertHistoryWindow({
filteredItems: items,
timeFilter: '24h',
selectedBarIndex: null,
trends,
now,
});
expect(result.map((i) => i.id)).toEqual(['within']);
});
it('applies the 30d cutoff when no bar is selected', () => {
const trends = buildAlertTrends([], '30d', now);
const items = [
makeItem({ id: 'within', startTime: new Date(now - 10 * 24 * MS_PER_HOUR).toISOString() }),
makeItem({ id: 'beyond', startTime: new Date(now - 60 * 24 * MS_PER_HOUR).toISOString() }),
];
const result = applyAlertHistoryWindow({
filteredItems: items,
timeFilter: '30d',
selectedBarIndex: null,
trends,
now,
});
expect(result.map((i) => i.id)).toEqual(['within']);
});
it('prefers the selected bar over the timeFilter cutoff (timeFilter "all")', () => {
const trends = buildAlertTrends([], 'all', now);
const bucketIndex = 0;
const bucketStart = trends.bucketTimes[bucketIndex];
const bucketEnd = bucketStart + trends.bucketSize * MS_PER_HOUR;
const items = [
makeItem({ id: 'in-bucket', startTime: new Date(bucketStart + 1000).toISOString() }),
makeItem({ id: 'out-of-bucket', startTime: new Date(bucketEnd + MS_PER_HOUR).toISOString() }),
];
const result = applyAlertHistoryWindow({
filteredItems: items,
timeFilter: 'all',
selectedBarIndex: bucketIndex,
trends,
now,
});
expect(result.map((i) => i.id)).toEqual(['in-bucket']);
});
it('returns 0 from the comparator when two items share both startTime and id', () => {
const trends = buildAlertTrends([], 'all', now);
const sharedStart = new Date(now - MS_PER_HOUR).toISOString();
// Identical id + startTime — the `a.id > b.id` branch is skipped and 0 is returned.
const items = [
makeItem({ id: 'same', startTime: sharedStart }),
makeItem({ id: 'same', startTime: sharedStart }),
];
const result = applyAlertHistoryWindow({
filteredItems: items,
timeFilter: 'all',
selectedBarIndex: null,
trends,
now,
});
expect(result).toHaveLength(2);
expect(result.every((i) => i.id === 'same')).toBe(true);
});
});
// ---------------------------------------------------------------------------
// getAlertHistoryDaySuffix — every ordinal arm including the 1113 special case.
// ---------------------------------------------------------------------------
describe('getAlertHistoryDaySuffix', () => {
it('returns "st" for 1, 21, 31, 101', () => {
expect(getAlertHistoryDaySuffix(1)).toBe('st');
expect(getAlertHistoryDaySuffix(21)).toBe('st');
expect(getAlertHistoryDaySuffix(31)).toBe('st');
expect(getAlertHistoryDaySuffix(101)).toBe('st');
});
it('returns "nd" for 2, 22', () => {
expect(getAlertHistoryDaySuffix(2)).toBe('nd');
expect(getAlertHistoryDaySuffix(22)).toBe('nd');
});
it('returns "rd" for 3, 23', () => {
expect(getAlertHistoryDaySuffix(3)).toBe('rd');
expect(getAlertHistoryDaySuffix(23)).toBe('rd');
});
it('returns "th" for the default arm (e.g. 4, 25)', () => {
expect(getAlertHistoryDaySuffix(4)).toBe('th');
expect(getAlertHistoryDaySuffix(25)).toBe('th');
});
it('returns "th" for the 1113 special case even though they end in 1/2/3', () => {
expect(getAlertHistoryDaySuffix(11)).toBe('th');
expect(getAlertHistoryDaySuffix(12)).toBe('th');
expect(getAlertHistoryDaySuffix(13)).toBe('th');
});
it('documents current behaviour for 111113 (guard only covers 1113)', () => {
// The 1113 guard is `day >= 11 && day <= 13`, so 111/112/113 fall through
// to the `% 10` switch and are *not* special-cased. This pins the current
// output; see GLM_REPORT.md for the suspected source bug.
expect(getAlertHistoryDaySuffix(111)).toBe('st');
expect(getAlertHistoryDaySuffix(112)).toBe('nd');
expect(getAlertHistoryDaySuffix(113)).toBe('rd');
});
});
// ---------------------------------------------------------------------------
// formatAlertHistoryGroupLabel — direct calls for all three label arms.
// ---------------------------------------------------------------------------
describe('formatAlertHistoryGroupLabel', () => {
it('labels a date equal to todayStart with the "Today (...)" prefix', () => {
const todayStart = Date.UTC(2026, 2, 22);
const date = new Date(todayStart);
expect(formatAlertHistoryGroupLabel(date, todayStart, 0)).toBe('Today (March 22nd)');
});
it('labels a date equal to yesterdayStart with the "Yesterday (...)" prefix', () => {
const yesterdayStart = Date.UTC(2026, 2, 21);
const date = new Date(yesterdayStart);
expect(formatAlertHistoryGroupLabel(date, 0, yesterdayStart)).toBe('Yesterday (March 21st)');
});
it('uses the absolute "Month DaySuffix" label for any other date', () => {
const date = new Date(Date.UTC(2026, 0, 2));
// Neither todayStart nor yesterdayStart match.
expect(formatAlertHistoryGroupLabel(date, 0, 0)).toBe('January 2nd');
});
it('applies the correct suffix for an 11th day', () => {
const date = new Date(Date.UTC(2026, 0, 11));
expect(formatAlertHistoryGroupLabel(date, 0, 0)).toBe('January 11th');
});
});
// ---------------------------------------------------------------------------
// getIncidentRowKey
// ---------------------------------------------------------------------------
describe('getIncidentRowKey', () => {
it('joins id and startTime with "::" into a stable composite key', () => {
expect(
getIncidentRowKey(
makeItem({ id: 'inc-9', startTime: '2026-03-22T09:00:00.000Z' }),
),
).toBe('inc-9::2026-03-22T09:00:00.000Z');
});
});
// ---------------------------------------------------------------------------
// groupAlertHistoryItems — multi-day grouping, add-to-existing arm, ordering.
// ---------------------------------------------------------------------------
describe('groupAlertHistoryItems', () => {
it('appends subsequent same-day alerts to an already-created group', () => {
const items = [
makeItem({ id: 'a', startTime: '2026-01-15T08:00:00.000Z' }),
makeItem({ id: 'b', startTime: '2026-01-15T20:00:00.000Z' }),
];
const groups = groupAlertHistoryItems(items);
expect(groups).toHaveLength(1);
expect(groups[0].alerts.map((a) => a.id)).toEqual(['a', 'b']);
});
it('produces one group per distinct calendar day, newest first', () => {
const items = [
makeItem({ id: 'oldest', startTime: '2026-01-10T08:00:00.000Z' }),
makeItem({ id: 'mid', startTime: '2026-02-10T08:00:00.000Z' }),
makeItem({ id: 'newest', startTime: '2026-03-10T08:00:00.000Z' }),
];
const groups = groupAlertHistoryItems(items);
expect(groups.map((g) => g.alerts[0].id)).toEqual(['newest', 'mid', 'oldest']);
});
});
// ---------------------------------------------------------------------------
// getAlertBucketDurationLabel — every arm of the guard + day/hour formatters.
// ---------------------------------------------------------------------------
describe('getAlertBucketDurationLabel', () => {
it('returns the em-dash placeholder for non-finite input', () => {
expect(getAlertBucketDurationLabel(Number.NaN)).toBe('—');
});
it('returns the em-dash placeholder for zero and negative input', () => {
expect(getAlertBucketDurationLabel(0)).toBe('—');
expect(getAlertBucketDurationLabel(-3)).toBe('—');
});
it('uses the singular "1 day" form for exactly 24 hours', () => {
expect(getAlertBucketDurationLabel(24)).toBe('1 day');
});
it('uses the plural "N days" form for whole-day buckets > 24h', () => {
expect(getAlertBucketDurationLabel(48)).toBe('2 days');
expect(getAlertBucketDurationLabel(72)).toBe('3 days');
});
it('uses the singular "1 hour" form for exactly one hour', () => {
expect(getAlertBucketDurationLabel(1)).toBe('1 hour');
});
it('uses the plural "N hours" form for non-whole-day hour buckets', () => {
expect(getAlertBucketDurationLabel(6)).toBe('6 hours');
expect(getAlertBucketDurationLabel(12)).toBe('12 hours');
});
});
// ---------------------------------------------------------------------------
// formatAlertAxisTickLabel — invalid-timestamp guard, "Now" end tick, and the
// three totalHours formatting tiers (with/without the hour option).
// ---------------------------------------------------------------------------
describe('formatAlertAxisTickLabel', () => {
it('returns the em-dash placeholder for a non-finite timestamp', () => {
expect(
formatAlertAxisTickLabel({
timestamp: Number.NaN,
bucketHours: 1,
totalHours: 24,
locale: 'en-US',
}),
).toBe('—');
});
it('returns "Now" for an end tick within 0.75 * bucketHours of now', () => {
const now = Date.UTC(2026, 2, 22, 12, 0, 0);
expect(
formatAlertAxisTickLabel({
timestamp: now - 10 * 60 * 1000, // 10 min ago, within 0.75h
bucketHours: 1,
totalHours: 24,
locale: 'en-US',
isEnd: true,
now,
}),
).toBe('Now');
});
it('does NOT return "Now" for a non-end tick even when close to now', () => {
const now = Date.UTC(2026, 2, 22, 12, 0, 0);
const label = formatAlertAxisTickLabel({
timestamp: now - 10 * 60 * 1000,
bucketHours: 1,
totalHours: 24,
locale: 'en-US',
isEnd: false,
now,
});
expect(label).not.toBe('Now');
expect(label).toContain('Mar');
});
it('does NOT return "Now" for an end tick that is far from now', () => {
const now = Date.UTC(2026, 2, 22, 12, 0, 0);
const label = formatAlertAxisTickLabel({
timestamp: now - 20 * MS_PER_HOUR, // 20h ago, outside 0.75h window for 1h bucket
bucketHours: 1,
totalHours: 24,
locale: 'en-US',
isEnd: true,
now,
});
expect(label).not.toBe('Now');
expect(label).toContain('Mar');
});
it('uses month/day/hour/minute options for totalHours <= 48', () => {
const ts = Date.UTC(2026, 2, 22, 9, 30, 0);
const label = formatAlertAxisTickLabel({
timestamp: ts,
bucketHours: 1,
totalHours: 24,
locale: 'en-US',
});
// Short range includes the time-of-day.
expect(label).toContain('Mar 22');
expect(label).toContain('9:30');
expect(label).toMatch(/AM|PM/);
});
it('includes the hour for a mid-range total with a small bucket', () => {
const ts = Date.UTC(2026, 2, 22, 9, 30, 0);
const label = formatAlertAxisTickLabel({
timestamp: ts,
bucketHours: 6,
totalHours: 7 * 24, // 168h, <= 24*90 and bucketHours <= 12 → hour shown
locale: 'en-US',
});
expect(label).toContain('Mar 22');
// The mid-range branch sets only `hour` (no `minute`), so the time-of-day
// token is a bare hour like "09 AM" with no colon.
expect(label).toMatch(/09/);
expect(label).toMatch(/AM|PM/);
expect(label).not.toMatch(/\d{1,2}:\d{2}/);
});
it('omits the hour for a mid-range total with a large bucket and long span', () => {
const ts = Date.UTC(2026, 2, 22, 0, 0, 0);
const label = formatAlertAxisTickLabel({
timestamp: ts,
bucketHours: 24, // > 12
totalHours: 24 * 60, // 1440h: <= 24*90 (2160) but > 24*14 (336) → no hour
locale: 'en-US',
});
expect(label).toContain('Mar 22');
// No time-of-day token should appear.
expect(label).not.toMatch(/\d{1,2}:\d{2}/);
});
it('uses year/month/day options for very long ranges (totalHours > 24*90)', () => {
const ts = Date.UTC(2026, 2, 22, 0, 0, 0);
const label = formatAlertAxisTickLabel({
timestamp: ts,
bucketHours: 24,
totalHours: 24 * 120, // 2880h > 2160 → year branch
locale: 'en-US',
});
expect(label).toContain('2026');
expect(label).toContain('Mar 22');
// Year branch never includes time-of-day.
expect(label).not.toMatch(/\d{1,2}:\d{2}/);
});
});
// ---------------------------------------------------------------------------
// buildAlertRangeSummary — null guard, normal output shape, and the
// `rangeHours ?? bucketHours` nullish-coalescing fallback.
// ---------------------------------------------------------------------------
describe('buildAlertRangeSummary', () => {
it('returns null when bucketTimes is empty', () => {
const trends: AlertTrendSeries = {
buckets: [],
max: 0,
bucketSize: 1,
bucketTimes: [],
rangeStart: 0,
rangeHours: 0,
};
expect(buildAlertRangeSummary(trends, 'en-US')).toBeNull();
});
it('returns null when bucketSize is zero', () => {
const trends: AlertTrendSeries = {
buckets: [1],
max: 1,
bucketSize: 0,
bucketTimes: [Date.UTC(2026, 2, 22)],
rangeStart: 0,
rangeHours: 1,
};
expect(buildAlertRangeSummary(trends, 'en-US')).toBeNull();
});
it('returns concrete startLabel/endLabel for a normal series', () => {
const start = Date.UTC(2026, 2, 22, 0, 0, 0);
const trends: AlertTrendSeries = {
buckets: [0, 0],
max: 1,
bucketSize: 6,
bucketTimes: [start, start + 6 * MS_PER_HOUR],
rangeStart: start,
rangeHours: 12,
};
const summary = buildAlertRangeSummary(trends, 'en-US');
expect(summary).not.toBeNull();
expect(summary!.startLabel).toContain('Mar 22');
expect(summary!.endLabel).toContain('Mar 22');
});
it('falls back to bucketHours when rangeHours is undefined', () => {
const start = Date.UTC(2026, 2, 22, 0, 0, 0);
const trends = {
buckets: [0],
max: 1,
bucketSize: 6,
bucketTimes: [start],
rangeStart: start,
rangeHours: undefined,
} as unknown as AlertTrendSeries;
// Should not throw; totalHours resolves to bucketHours (6) via the `??` arm.
const summary = buildAlertRangeSummary(trends, 'en-US');
expect(summary).not.toBeNull();
expect(typeof summary!.startLabel).toBe('string');
expect(summary!.startLabel.length).toBeGreaterThan(0);
});
});
// ---------------------------------------------------------------------------
// buildAlertAxisTicks — push-vs-replace last-tick arms and the align mapping.
// (The first-tick `unshift` arm is suspected dead code — see GLM_REPORT.md.)
// ---------------------------------------------------------------------------
describe('buildAlertAxisTicks (last-tick + align arms)', () => {
const now = Date.UTC(2026, 2, 22, 12, 0, 0);
it('pushes a new last tick when the loop does not reach position 1', () => {
const trends = buildAlertTrends([], '24h', now);
const ticks = buildAlertAxisTicks(trends, 'en-US');
expect(ticks.length).toBeGreaterThanOrEqual(2);
expect(ticks[0].position).toBe(0);
expect(ticks[ticks.length - 1].position).toBe(1);
// First and last carry the start/end align markers.
expect(ticks[0].align).toBe('start');
expect(ticks[ticks.length - 1].align).toBe('end');
});
it('replaces the existing last tick when the loop already lands on position 1', () => {
// 2 bucket times, 1 bucket → loop hits index 0 and 1; index 1 maps to
// position 1.0, triggering the replace-last-tick branch.
const trends: AlertTrendSeries = {
buckets: [0],
max: 1,
bucketSize: 1,
bucketTimes: [now, now + MS_PER_HOUR],
rangeStart: now,
rangeHours: 1,
};
const ticks = buildAlertAxisTicks(trends, 'en-US');
expect(ticks).toHaveLength(2);
expect(ticks[1].position).toBe(1);
expect(ticks[1].align).toBe('end');
});
it('marks every non-edge tick as center-aligned', () => {
const trends = buildAlertTrends([], '30d', now);
const ticks = buildAlertAxisTicks(trends, 'en-US');
for (let i = 1; i < ticks.length - 1; i++) {
expect(ticks[i].align).toBe('center');
}
});
it('handles a single-bucket series by still emitting start and end ticks', () => {
const start = Date.UTC(2026, 2, 22, 0, 0, 0);
const trends: AlertTrendSeries = {
buckets: [0],
max: 1,
bucketSize: 6,
bucketTimes: [start],
rangeStart: start,
rangeHours: 6,
};
const ticks = buildAlertAxisTicks(trends, 'en-US');
expect(ticks.length).toBeGreaterThanOrEqual(2);
expect(ticks[0].position).toBe(0);
expect(ticks[ticks.length - 1].position).toBe(1);
});
});
@@ -0,0 +1,176 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { Resource } from '@/types/resource';
import {
alertTypeDisplayLabel,
getAlertResourceDisplayLabel,
getLocalTimezone,
normalizeMetricDelayMap,
unifiedTypeToAlertDisplayType,
} from '@/features/alerts/helpers';
describe('alerts helpers — branch coverage (batch 2)', () => {
describe('alertTypeDisplayLabel', () => {
it('maps the remaining standard metric arms and aliases', () => {
expect(alertTypeDisplayLabel('disk-usage')).toBe('Disk');
expect(alertTypeDisplayLabel('usage')).toBe('Usage');
expect(alertTypeDisplayLabel('network')).toBe('Network');
expect(alertTypeDisplayLabel('load')).toBe('Load');
});
it('maps all three temperature aliases to the same label', () => {
expect(alertTypeDisplayLabel('temperature')).toBe('Temperature');
expect(alertTypeDisplayLabel('disk_temperature')).toBe('Temperature');
expect(alertTypeDisplayLabel('diskTemperature')).toBe('Temperature');
});
it('maps the remaining docker-container and docker-host arms', () => {
expect(alertTypeDisplayLabel('docker-container-cpu')).toBe('Container CPU');
expect(alertTypeDisplayLabel('docker-container-disk')).toBe('Container Disk');
expect(alertTypeDisplayLabel('docker-container-update')).toBe('Update Available');
expect(alertTypeDisplayLabel('docker-host-offline')).toBe('Host Offline');
});
it('maps the remaining infrastructure and storage arms', () => {
expect(alertTypeDisplayLabel('node')).toBe('Node');
expect(alertTypeDisplayLabel('zfs-device')).toBe('ZFS Device');
expect(alertTypeDisplayLabel('raid')).toBe('RAID');
expect(alertTypeDisplayLabel('resource-incident')).toBe('Resource Health');
});
it('maps the remaining standalone arms', () => {
expect(alertTypeDisplayLabel('pbs')).toBe('PBS');
expect(alertTypeDisplayLabel('message-age')).toBe('Message Age');
});
it('title-cases unknown types containing a mix of hyphens and underscores', () => {
expect(alertTypeDisplayLabel('snapshot_disk-usage')).toBe('Snapshot Disk Usage');
});
it('returns the empty string for an empty unknown type', () => {
expect(alertTypeDisplayLabel('')).toBe('');
});
});
describe('unifiedTypeToAlertDisplayType', () => {
it('returns the canonical resource-type label for known types', () => {
expect(unifiedTypeToAlertDisplayType('vm')).toBe('VM');
expect(unifiedTypeToAlertDisplayType('pbs')).toBe('PBS');
expect(unifiedTypeToAlertDisplayType('storage')).toBe('Storage');
expect(unifiedTypeToAlertDisplayType('ceph')).toBe('Ceph');
});
it('falls back to the raw type string when no canonical label resolves', () => {
expect(unifiedTypeToAlertDisplayType('' as unknown as Parameters<typeof unifiedTypeToAlertDisplayType>[0])).toBe('');
});
});
describe('getLocalTimezone', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('falls back to UTC when the resolved timezone is empty', () => {
const spy = vi.spyOn(Intl, 'DateTimeFormat').mockImplementation(
(() => ({ resolvedOptions: () => ({ timeZone: '' }) })) as unknown as typeof Intl.DateTimeFormat,
);
expect(getLocalTimezone()).toBe('UTC');
spy.mockRestore();
});
it('returns the resolved IANA timezone when one is available', () => {
const spy = vi.spyOn(Intl, 'DateTimeFormat').mockImplementation(
(() => ({ resolvedOptions: () => ({ timeZone: 'Australia/Sydney' }) })) as unknown as typeof Intl.DateTimeFormat,
);
expect(getLocalTimezone()).toBe('Australia/Sydney');
spy.mockRestore();
});
});
describe('normalizeMetricDelayMap', () => {
it('returns an empty object for nullish input', () => {
expect(normalizeMetricDelayMap(undefined)).toEqual({});
expect(normalizeMetricDelayMap(null)).toEqual({});
});
it('returns an empty object for an empty record', () => {
expect(normalizeMetricDelayMap({})).toEqual({});
});
it('trims and lowercases type and metric keys and rounds fractional values', () => {
const input = { ' VM ': { ' CPU ': 3.6, 'Memory': 7 } };
expect(normalizeMetricDelayMap(input)).toEqual({ vm: { cpu: 4, memory: 7 } });
});
it('rounds 0.5 up and 0.4 down', () => {
expect(normalizeMetricDelayMap({ vm: { a: 0.5, b: 0.4 } })).toEqual({ vm: { a: 1, b: 0 } });
});
it('skips entries whose metrics value is null', () => {
expect(
normalizeMetricDelayMap({ vm: null } as unknown as Parameters<typeof normalizeMetricDelayMap>[0]),
).toEqual({});
});
it('skips entries with a whitespace-only type key', () => {
expect(normalizeMetricDelayMap({ ' ': { cpu: 5 }, vm: { cpu: 1 } })).toEqual({
vm: { cpu: 1 },
});
});
it('drops non-number, NaN, and negative metric values but keeps zero', () => {
const input = {
vm: {
cpu: 5,
badString: 'x' as unknown as number,
nanVal: Number.NaN,
negative: -1,
zero: 0,
},
};
expect(normalizeMetricDelayMap(input)).toEqual({ vm: { cpu: 5, zero: 0 } });
});
it('skips metric entries with a whitespace-only metric key', () => {
expect(normalizeMetricDelayMap({ vm: { ' ': 5, cpu: 1 } })).toEqual({ vm: { cpu: 1 } });
});
it('omits a type entirely when none of its metrics survive validation', () => {
expect(
normalizeMetricDelayMap({
vm: { onlyBad: -1 },
host: { cpu: 2 },
}),
).toEqual({ host: { cpu: 2 } });
});
});
describe('getAlertResourceDisplayLabel', () => {
const makeResource = (overrides: Partial<Resource>): Resource =>
({ id: 'r1', name: 'r1', type: 'vm', ...overrides }) as Resource;
it('returns the preferred display name when it differs from the id', () => {
const resource = makeResource({ id: 'node-1', displayName: 'Tower' });
expect(getAlertResourceDisplayLabel(resource)).toBe('Tower');
});
it('returns the fallback when the preferred name equals the id', () => {
const resource = makeResource({ id: 'r1', name: 'r1', displayName: '' });
expect(getAlertResourceDisplayLabel(resource, 'fallback-label')).toBe('fallback-label');
});
it('falls through to the preferred (= id) when no fallback is supplied', () => {
const resource = makeResource({ id: 'r1', name: 'r1', displayName: '' });
expect(getAlertResourceDisplayLabel(resource)).toBe('r1');
});
it('returns the empty id when nothing resolves and no fallback is given', () => {
const resource = makeResource({ id: '', name: '', displayName: '' });
expect(getAlertResourceDisplayLabel(resource)).toBe('');
});
it('prefers a non-id display name over a supplied fallback', () => {
const resource = makeResource({ id: 'r1', displayName: 'My Host' });
expect(getAlertResourceDisplayLabel(resource, 'ignored')).toBe('My Host');
});
});
});
@@ -0,0 +1,475 @@
import { describe, expect, it } from 'vitest';
import type { Resource, ResourceStatus } from '@/types/resource';
import {
dockerContainerLifecycleName,
getDockerContainerLifecycleDisabledReason,
} from '../dockerContainerLifecycleActions';
const ALL_CAPABILITIES = [
{ name: 'start', type: 'common', platform: 'docker', minimumApprovalLevel: 'admin' },
{ name: 'stop', type: 'common', platform: 'docker', minimumApprovalLevel: 'admin' },
{ name: 'restart', type: 'common', platform: 'docker', minimumApprovalLevel: 'admin' },
];
const resource = (overrides: Partial<Resource> = {}): Resource => ({
id: 'app-container:docker-host:web',
name: 'web',
displayName: 'web',
platformId: 'docker-1',
platformType: 'docker',
sourceType: 'agent',
sources: ['docker'],
status: 'running',
type: 'app-container',
lastSeen: 1_700_000_000_000,
docker: {
runtime: 'docker',
agentId: 'agent-1',
containerId: 'abc123',
containerState: 'running',
},
capabilities: ALL_CAPABILITIES,
...overrides,
});
describe('dockerContainerLifecycleActions.branchcov2', () => {
describe('dockerContainerLifecycleName', () => {
it('returns the trimmed resource name when present', () => {
expect(dockerContainerLifecycleName(resource({ name: ' web-app ' }))).toBe('web-app');
});
it('falls back to displayName when name is blank', () => {
expect(
dockerContainerLifecycleName(resource({ name: ' ', displayName: 'Web App' })),
).toBe('Web App');
});
it('falls back to docker.displayName when name and displayName are blank', () => {
expect(
dockerContainerLifecycleName(
resource({
name: '',
displayName: ' ',
docker: { runtime: 'docker', displayName: 'Container Display' },
}),
),
).toBe('Container Display');
});
it('falls back to docker.containerId when name, displayName, and docker.displayName are blank', () => {
expect(
dockerContainerLifecycleName(
resource({
name: '',
displayName: '',
docker: { runtime: 'docker', containerId: 'cid-9' },
}),
),
).toBe('cid-9');
});
it('falls back to resource.id when every name source is blank', () => {
expect(
dockerContainerLifecycleName(
resource({
name: '',
displayName: '',
docker: { runtime: 'docker' },
}),
),
).toBe('app-container:docker-host:web');
});
});
describe('getDockerContainerLifecycleDisabledReason', () => {
describe('runtime gating', () => {
it('reports an unsupported runtime using the runtime label', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({ docker: { runtime: 'containerd', agentId: 'agent-1' } }),
'restart',
),
).toBe('containerd is not supported for governed container lifecycle actions.');
});
it('treats podman as supported and reaches the agent check with a Podman label', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({ docker: { runtime: 'podman' } }),
'restart',
),
).toBe('No reporting Pulse agent is attached to this Podman host.');
});
it('reports a missing runtime when docker is absent entirely', () => {
expect(
getDockerContainerLifecycleDisabledReason(resource({ docker: undefined }), 'restart'),
).toBe('Container runtime is not reported.');
});
it('reports a missing runtime when the runtime token is whitespace-only', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
docker: { runtime: ' ', agentId: 'agent-1', containerState: 'running' },
}),
'restart',
),
).toBe('Container runtime is not reported.');
});
});
describe('sourceStatusDisabledReason (exercised via orchestrator)', () => {
it('blocks on an offline inventory status', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({ sourceStatus: { docker: { status: 'offline' } } }),
'restart',
),
).toBe('Docker inventory is offline; refresh inventory before running lifecycle actions.');
});
it('blocks on a missing inventory status', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({ sourceStatus: { docker: { status: 'missing' } } }),
'restart',
),
).toBe('Docker inventory is missing; refresh inventory before running lifecycle actions.');
});
it('blocks on an inventory error when the status itself is healthy', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
sourceStatus: { docker: { status: 'healthy', error: 'connection refused' } },
}),
'restart',
),
).toBe('Docker inventory is not healthy: connection refused');
});
it('blocks when lastSeen is zero (falsy timestamp)', () => {
expect(
getDockerContainerLifecycleDisabledReason(resource({ lastSeen: 0 }), 'restart'),
).toBe('Docker inventory has not reported a valid last-seen timestamp.');
});
it('blocks when lastSeen is negative (truthy but <= 0)', () => {
expect(
getDockerContainerLifecycleDisabledReason(resource({ lastSeen: -5 }), 'restart'),
).toBe('Docker inventory has not reported a valid last-seen timestamp.');
});
});
describe('security gating', () => {
it('uses the default host-policy copy when mutatingCommandsBlockedReason is absent', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
docker: {
runtime: 'docker',
agentId: 'agent-1',
containerId: 'abc123',
containerState: 'running',
security: { mutatingCommandsBlocked: true },
},
}),
'restart',
),
).toBe('Docker host policy blocks mutating container lifecycle commands.');
});
});
describe('stateDisabledReason (exercised via orchestrator)', () => {
it('reports a non-startable, non-running state for start', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
docker: {
runtime: 'docker',
agentId: 'agent-1',
containerId: 'abc',
containerState: 'paused',
},
}),
'start',
),
).toBe('Container state paused is not startable.');
});
it('prefers containerState over status for the state lookup', () => {
// status 'stopped' would be startable; containerState 'paused' is not -> proves containerState wins.
expect(
getDockerContainerLifecycleDisabledReason(
resource({
status: 'stopped',
docker: {
runtime: 'docker',
agentId: 'agent-1',
containerId: 'abc',
containerState: 'paused',
},
}),
'start',
),
).toBe('Container state paused is not startable.');
});
it('falls back to status when containerState is blank', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
status: 'paused',
docker: {
runtime: 'docker',
agentId: 'agent-1',
containerId: 'abc',
containerState: '',
},
}),
'start',
),
).toBe('Container state paused is not startable.');
});
it('reports an unknown state for start when both containerState and status are blank', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
status: '' as unknown as ResourceStatus,
docker: {
runtime: 'docker',
agentId: 'agent-1',
containerId: 'abc',
containerState: '',
},
}),
'start',
),
).toBe('Container state is unknown.');
});
it('reports that the container must be running before stop', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
docker: {
runtime: 'docker',
agentId: 'agent-1',
containerId: 'abc',
containerState: 'exited',
},
}),
'stop',
),
).toBe('Container must be running before stop.');
});
it('reports an unknown state for stop when both containerState and status are blank', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
status: '' as unknown as ResourceStatus,
docker: {
runtime: 'docker',
agentId: 'agent-1',
containerId: 'abc',
containerState: '',
},
}),
'stop',
),
).toBe('Container state is unknown.');
});
it('does not block start for a startable state (created)', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
status: 'stopped',
docker: {
runtime: 'docker',
agentId: 'agent-1',
containerId: 'abc',
containerState: 'created',
},
}),
'start',
),
).toBeUndefined();
});
});
describe('actionReadinessDisabledReason (exercised via orchestrator)', () => {
it('prefers an explicit reason over the reasonCode switch', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
actionReadiness: [
{
name: 'restart',
available: false,
reasonCode: 'command_agent_disconnected',
reason: 'Agent reboot in progress',
},
],
}),
'restart',
),
).toBe('Agent reboot in progress');
});
it('maps command_agent_disconnected to the not-connected copy', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
actionReadiness: [
{ name: 'restart', available: false, reasonCode: 'command_agent_disconnected' },
],
}),
'restart',
),
).toBe('Docker / Podman command agent is not connected.');
});
it('maps command_agent_unavailable to the execution-unavailable copy', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
actionReadiness: [
{ name: 'restart', available: false, reasonCode: 'command_agent_unavailable' },
],
}),
'restart',
),
).toBe('Docker / Podman command execution is not available.');
});
it('maps stale_inventory to the not-fresh copy', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
actionReadiness: [
{ name: 'restart', available: false, reasonCode: 'stale_inventory' },
],
}),
'restart',
),
).toBe('Docker / Podman inventory is not fresh enough to run lifecycle actions.');
});
it('maps host_policy_blocked to the policy-blocked copy', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
actionReadiness: [
{ name: 'restart', available: false, reasonCode: 'host_policy_blocked' },
],
}),
'restart',
),
).toBe('Docker / Podman host policy blocks mutating lifecycle actions.');
});
it('maps unsupported_handler to the unsupported-executor copy', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
actionReadiness: [
{ name: 'restart', available: false, reasonCode: 'unsupported_handler' },
],
}),
'restart',
),
).toBe('This container action is not routed through the supported lifecycle executor.');
});
it('falls through the default switch arm without blocking when a capability is advertised', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
actionReadiness: [
{ name: 'restart', available: false, reasonCode: 'totally_unknown_code' },
],
}),
'restart',
),
).toBeUndefined();
});
it('does not block when the matched readiness item is still available', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
actionReadiness: [
{ name: 'restart', available: true, reasonCode: 'command_agent_disconnected' },
],
}),
'restart',
),
).toBeUndefined();
});
it('does not block when readiness is reported for a different action', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
actionReadiness: [
{ name: 'start', available: false, reasonCode: 'command_agent_disconnected' },
],
}),
'restart',
),
).toBeUndefined();
});
it('matches readiness names case-insensitively', () => {
expect(
getDockerContainerLifecycleDisabledReason(
resource({
actionReadiness: [
{ name: 'RESTART', available: false, reasonCode: 'stale_inventory' },
],
}),
'restart',
),
).toBe('Docker / Podman inventory is not fresh enough to run lifecycle actions.');
});
});
describe('capability gating', () => {
it('reports the missing capability using the requested action name', () => {
expect(
getDockerContainerLifecycleDisabledReason(resource({ capabilities: [] }), 'stop'),
).toBe(
'Pulse does not currently advertise a fresh stop command capability for this container.',
);
});
});
describe('happy path', () => {
it('returns undefined for stop and restart on a fully healthy running container', () => {
const healthy = resource();
expect(getDockerContainerLifecycleDisabledReason(healthy, 'stop')).toBeUndefined();
expect(getDockerContainerLifecycleDisabledReason(healthy, 'restart')).toBeUndefined();
});
it('returns undefined for start on a stopped container with healthy inventory', () => {
const stopped = resource({
status: 'offline',
docker: {
runtime: 'docker',
agentId: 'agent-1',
containerId: 'abc',
containerState: 'stopped',
},
});
expect(getDockerContainerLifecycleDisabledReason(stopped, 'start')).toBeUndefined();
});
});
});
});
@@ -0,0 +1,668 @@
import { describe, expect, it } from 'vitest';
import type { PatrolRunStatus } from '@/api/patrol';
import {
getMonitorContextPatrolProtectionPosture,
getPatrolQueueWorkspaceDescription,
getPatrolSetupIssueReason,
getPatrolWorkspaceWorkGroups,
isPatrolCoverageStale,
} from '../patrolControlPresentation';
// Five of the nine target functions (`normalizeStatus`, `getPatrolCoverageLabel`,
// `formatMonitorCoverageLabel`, `getPatrolQueueActionDetail`,
// `shouldSuppressMonitorContextPatrolPosture`) are module-private, so this file
// drives them through their exported callers and asserts on the concrete
// observable outputs that those private branches produce.
describe('patrolControlPresentation branch coverage (set 2)', () => {
const NOW_MS = Date.parse('2026-06-30T15:00:00Z');
describe('getMonitorContextPatrolProtectionPosture', () => {
it('returns no summaries when neither a latest run nor patrol status exists', () => {
// Branch: `(!input.latestRun && !patrolStatus)` early-return [].
expect(getMonitorContextPatrolProtectionPosture({ monitoredResourceCount: 4 })).toEqual([]);
});
it('uses warning tones throughout when the latest Patrol status is unhealthy', () => {
// Branch: `patrolStatus?.healthy === false` -> healthyTone 'warning'.
expect(
getMonitorContextPatrolProtectionPosture({
findingCount: 0,
latestRun: {
error_count: 0,
resources_checked: 2,
status: 'healthy',
},
monitoredResourceCount: 3,
nowMs: NOW_MS,
patrolStatus: {
enabled: true,
error_count: 0,
findings_count: 0,
healthy: false,
next_patrol_at: '2026-07-01T10:00:00Z',
resources_checked: 2,
running: false,
runtime_state: 'active',
},
pendingApprovalCount: 0,
workTypeComposition: {
total: 0,
approval: 0,
failed: 0,
inProgress: 0,
recurring: 0,
newIssues: 0,
},
}),
).toEqual([
{
detail: 'Latest Patrol evidence is available while you review this monitor view.',
id: 'coverage',
label: 'Patrol checked 2 resources',
tone: 'warning',
},
{
detail: 'Current Patrol findings and approvals stay in Patrol; none are waiting now.',
id: 'open-work',
label: 'No Patrol work waiting',
tone: 'warning',
},
{
detail: 'Patrol is scheduled to check monitored resources again.',
id: 'schedule',
label: 'Next check scheduled',
tone: 'info',
},
]);
});
it('surfaces a paused-schedule summary when scheduled checks are disabled', () => {
// Branch: `patrolStatus?.enabled === false` schedule arm. next_patrol_at is
// intentionally set to prove the enabled===false check wins.
expect(
getMonitorContextPatrolProtectionPosture({
findingCount: 0,
latestRun: {
error_count: 0,
resources_checked: 2,
status: 'healthy',
},
monitoredResourceCount: 3,
nowMs: NOW_MS,
patrolStatus: {
enabled: false,
error_count: 0,
findings_count: 0,
healthy: true,
next_patrol_at: '2026-07-01T10:00:00Z',
resources_checked: 2,
running: false,
runtime_state: 'active',
},
pendingApprovalCount: 0,
workTypeComposition: {
total: 0,
approval: 0,
failed: 0,
inProgress: 0,
recurring: 0,
newIssues: 0,
},
}),
).toEqual([
{
detail: 'Latest Patrol evidence is available while you review this monitor view.',
id: 'coverage',
label: 'Patrol checked 2 resources',
tone: 'success',
},
{
detail: 'Current Patrol findings and approvals stay in Patrol; none are waiting now.',
id: 'open-work',
label: 'No Patrol work waiting',
tone: 'success',
},
{
detail: 'Run Patrol manually or enable scheduled checks to keep coverage fresh.',
id: 'schedule',
label: 'Scheduled checks paused',
tone: 'warning',
},
]);
});
it('falls back to the ready-to-run schedule summary when no next check is set', () => {
// Branch: schedule `else` arm (enabled !== false && no next_patrol_at).
expect(
getMonitorContextPatrolProtectionPosture({
findingCount: 0,
latestRun: null,
monitoredResourceCount: 3,
nowMs: NOW_MS,
patrolStatus: {
enabled: true,
error_count: 0,
findings_count: 0,
healthy: true,
next_patrol_at: undefined,
resources_checked: 0,
running: false,
runtime_state: 'active',
},
pendingApprovalCount: 0,
workTypeComposition: {
total: 0,
approval: 0,
failed: 0,
inProgress: 0,
recurring: 0,
newIssues: 0,
},
}),
).toEqual([
{
detail: 'Run Patrol to refresh current coverage for monitored resources.',
id: 'coverage',
label: 'Patrol coverage needs refresh',
tone: 'warning',
},
{
detail: 'Current Patrol findings and approvals stay in Patrol; none are waiting now.',
id: 'open-work',
label: 'No Patrol work waiting',
tone: 'success',
},
{
detail: 'Run Patrol from the Patrol page any time to refresh coverage.',
id: 'schedule',
label: 'Ready to run Patrol',
tone: 'info',
},
]);
});
});
describe('shouldSuppressMonitorContextPatrolPosture (via getMonitorContextPatrolProtectionPosture)', () => {
const baseMonitorInput = {
findingCount: 0,
latestRun: {
error_count: 0,
resources_checked: 4,
status: 'healthy' as const,
},
monitoredResourceCount: 4,
nowMs: NOW_MS,
patrolStatus: {
enabled: true,
error_count: 0,
findings_count: 0,
healthy: true,
next_patrol_at: '2026-07-01T10:00:00Z',
resources_checked: 4,
running: false,
runtime_state: 'active' as const,
},
pendingApprovalCount: 0,
workTypeComposition: {
total: 0,
approval: 0,
failed: 0,
inProgress: 0,
recurring: 0,
newIssues: 0,
},
};
it('does not suppress when every work, runtime, and schedule signal is clear', () => {
// Baseline: every OR operand in shouldSuppress... evaluates false, so the
// monitor-context posture is returned in full.
expect(getMonitorContextPatrolProtectionPosture(baseMonitorInput)).toHaveLength(3);
});
it.each([
['failed work-type composition', { failed: 1 }],
['approval work-type composition', { approval: 1 }],
['in-progress work-type composition', { inProgress: 1 }],
['recurring work-type composition', { recurring: 1 }],
] as const)(
'suppresses the posture when %s is present',
(_label, compositionOverride) => {
expect(
getMonitorContextPatrolProtectionPosture({
...baseMonitorInput,
workTypeComposition: {
total: 1,
approval: 0,
failed: 0,
inProgress: 0,
recurring: 0,
newIssues: 0,
...compositionOverride,
},
}),
).toEqual([]);
},
);
it('suppresses the posture when the status reports open findings', () => {
// OR operand: `statusFindingCount > 0`.
expect(
getMonitorContextPatrolProtectionPosture({
...baseMonitorInput,
patrolStatus: { ...baseMonitorInput.patrolStatus, findings_count: 1 },
}),
).toEqual([]);
});
it('suppresses the posture while a Patrol run is in flight', () => {
// OR operand: `patrolStatus?.running`.
expect(
getMonitorContextPatrolProtectionPosture({
...baseMonitorInput,
patrolStatus: { ...baseMonitorInput.patrolStatus, running: true },
}),
).toEqual([]);
});
it('suppresses the posture when the status carries runtime errors', () => {
// OR operand: `statusErrorCount > 0` (latest run stays healthy).
expect(
getMonitorContextPatrolProtectionPosture({
...baseMonitorInput,
patrolStatus: { ...baseMonitorInput.patrolStatus, error_count: 1 },
}),
).toEqual([]);
});
it('suppresses the posture when the runtime is not active', () => {
// OR operand: `!isActivePatrolRuntime(patrolStatus)`.
expect(
getMonitorContextPatrolProtectionPosture({
...baseMonitorInput,
patrolStatus: { ...baseMonitorInput.patrolStatus, runtime_state: 'disabled' },
}),
).toEqual([]);
});
it('suppresses the posture when a scheduled patrol is overdue', () => {
// OR operand: `isScheduledPatrolOverdue(patrolStatus, nowMs)`.
expect(
getMonitorContextPatrolProtectionPosture({
...baseMonitorInput,
patrolStatus: {
...baseMonitorInput.patrolStatus,
next_patrol_at: '2026-06-30T14:00:00Z',
},
}),
).toEqual([]);
});
});
describe('getPatrolCoverageLabel (via getMonitorContextPatrolProtectionPosture)', () => {
it('derives the coverage label from status resources when no latest run is available', () => {
// Branch: latestRun null -> latestRunCoverage '' -> status resources > 0 ->
// `Checked N resource(s)`.
const summaries = getMonitorContextPatrolProtectionPosture({
findingCount: 0,
latestRun: null,
monitoredResourceCount: 3,
nowMs: NOW_MS,
patrolStatus: {
enabled: true,
error_count: 0,
findings_count: 0,
healthy: true,
next_patrol_at: '2026-07-01T10:00:00Z',
resources_checked: 3,
running: false,
runtime_state: 'active',
},
pendingApprovalCount: 0,
workTypeComposition: {
total: 0,
approval: 0,
failed: 0,
inProgress: 0,
recurring: 0,
newIssues: 0,
},
});
expect(summaries[0]).toStrictEqual({
detail: 'Latest Patrol evidence is available while you review this monitor view.',
id: 'coverage',
label: 'Patrol checked 3 resources',
tone: 'success',
});
});
it('returns undefined coverage when neither latest run nor status has checked resources', () => {
// Branch: latestRun null AND statusResourcesChecked === 0 -> undefined.
const summaries = getMonitorContextPatrolProtectionPosture({
findingCount: 0,
latestRun: null,
monitoredResourceCount: 3,
nowMs: NOW_MS,
patrolStatus: {
enabled: true,
error_count: 0,
findings_count: 0,
healthy: true,
next_patrol_at: undefined,
resources_checked: 0,
running: false,
runtime_state: 'active',
},
pendingApprovalCount: 0,
workTypeComposition: {
total: 0,
approval: 0,
failed: 0,
inProgress: 0,
recurring: 0,
newIssues: 0,
},
});
expect(summaries[0]).toStrictEqual({
detail: 'Run Patrol to refresh current coverage for monitored resources.',
id: 'coverage',
label: 'Patrol coverage needs refresh',
tone: 'warning',
});
});
});
describe('formatMonitorCoverageLabel (via getMonitorContextPatrolProtectionPosture)', () => {
it('lowercases the first character of a non-empty coverage label', () => {
// Branch: coverageLabel truthy -> `Patrol ` + lowercase-first + rest.
// 'Checked 3 resources' (status-derived) becomes 'Patrol checked 3 resources'.
const [coverage] = getMonitorContextPatrolProtectionPosture({
findingCount: 0,
latestRun: null,
monitoredResourceCount: 3,
nowMs: NOW_MS,
patrolStatus: {
enabled: true,
error_count: 0,
findings_count: 0,
healthy: true,
next_patrol_at: '2026-07-01T10:00:00Z',
resources_checked: 5,
running: false,
runtime_state: 'active',
},
pendingApprovalCount: 0,
workTypeComposition: {
total: 0,
approval: 0,
failed: 0,
inProgress: 0,
recurring: 0,
newIssues: 0,
},
});
expect(coverage?.label).toBe('Patrol checked 5 resources');
});
});
describe('normalizeStatus (via getPatrolWorkspaceWorkGroups)', () => {
it('treats a whitespace-padded, mixed-case "error" status as a failed check', () => {
// normalizeStatus(' Error ') -> 'error', which feeds hasFailedPatrolCheck.
// resources_checked 0 also exercises the generic failed-check detail arm.
expect(
getPatrolWorkspaceWorkGroups({
latestRun: {
error_count: 0,
resources_checked: 0,
status: ' Error ' as unknown as PatrolRunStatus,
},
}),
).toEqual([
{
detail: 'The last Patrol check ended with runtime issues.',
id: 'failed-check',
label: 'Latest check needs review',
tone: 'danger',
},
]);
});
it('does not mark a multi-word non-error status as a failed check', () => {
// normalizeStatus('Error Recovery') -> 'error_recovery' (!== 'error'), and
// with error_count 0 the run is not failed.
expect(
getPatrolWorkspaceWorkGroups({
latestRun: {
error_count: 0,
resources_checked: 5,
status: 'Error Recovery' as unknown as PatrolRunStatus,
},
}),
).toEqual([]);
});
});
describe('isPatrolCoverageStale', () => {
it('never treats a running patrol as stale', () => {
expect(
isPatrolCoverageStale({
latestRun: { completed_at: '2020-01-01T00:00:00Z' },
nowMs: NOW_MS,
patrolStatus: { running: true },
}),
).toBe(false);
});
it('treats an overdue scheduled patrol as stale before checking freshness', () => {
// isScheduledPatrolOverdue short-circuits ahead of the lastCheckAt logic.
expect(
isPatrolCoverageStale({
nowMs: NOW_MS,
patrolStatus: {
next_patrol_at: '2026-06-30T14:05:00Z',
running: false,
},
}),
).toBe(true);
});
it('is not stale when there is no last-check timestamp to compare against', () => {
// Branch: `!lastCheckAt` -> false.
expect(
isPatrolCoverageStale({
latestRun: { completed_at: undefined },
nowMs: NOW_MS,
patrolStatus: { running: false },
}),
).toBe(false);
});
it('is not stale when the only timestamp is in the future', () => {
// Branch: `lastCheckMs > nowMs` -> false.
expect(
isPatrolCoverageStale({
latestRun: { completed_at: '2026-07-02T00:00:00Z' },
nowMs: NOW_MS,
patrolStatus: { running: false },
}),
).toBe(false);
});
it('is not stale when the last-check timestamp cannot be parsed', () => {
// Branch: `!Number.isFinite(lastCheckMs)` -> false.
expect(
isPatrolCoverageStale({
latestRun: { completed_at: 'not-a-real-date' },
nowMs: NOW_MS,
patrolStatus: { running: false },
}),
).toBe(false);
});
it('falls back to patrol status last_patrol_at when the run lacks completed_at', () => {
// Branch: `completed_at || last_patrol_at` picks the status timestamp.
expect(
isPatrolCoverageStale({
latestRun: { completed_at: undefined },
nowMs: NOW_MS,
patrolStatus: {
interval_ms: 6 * 60 * 60 * 1000,
last_patrol_at: '2026-06-28T12:00:00Z',
running: false,
},
}),
).toBe(true);
});
it('uses the 24h minimum freshness window when no interval is configured', () => {
// 25h-old check exceeds the minimum window (interval_ms 0 -> max(24h, 0)).
expect(
isPatrolCoverageStale({
latestRun: { completed_at: '2026-06-29T14:00:00Z' },
nowMs: NOW_MS,
patrolStatus: { running: false },
}),
).toBe(true);
// 12h-old check is inside the 24h minimum window.
expect(
isPatrolCoverageStale({
latestRun: { completed_at: '2026-06-30T03:00:00Z' },
nowMs: NOW_MS,
patrolStatus: { running: false },
}),
).toBe(false);
});
});
describe('getPatrolSetupIssueReason', () => {
it('returns the readiness summary verbatim when it has no tool-call wording', () => {
// Branch: readinessSummary truthy but neither regex matches -> return it.
expect(getPatrolSetupIssueReason({ readinessSummary: 'Provider rate limit hit.' })).toBe(
'Provider rate limit hit.',
);
});
it('maps a "run tools" readiness summary to the fixed tool-call reason', () => {
// Branch: second regex /\brun tools?\b/i matches.
expect(
getPatrolSetupIssueReason({
readinessSummary: 'The selected model cannot reliably run tools.',
}),
).toBe('Selected model cannot run Patrol tools.');
});
it('falls through to the blocked reason when the trigger reason is blank', () => {
// Branch: triggerDisabledReason empty -> blockedReason wins.
expect(
getPatrolSetupIssueReason({
triggerDisabledReason: ' ',
blockedReason: 'Patrol config is invalid',
}),
).toBe('Patrol config is invalid');
});
it('skips a whitespace-only readiness summary and falls through to other reasons', () => {
// Branch: normalizeText(readinessSummary) -> '' fails the truthy guard.
expect(
getPatrolSetupIssueReason({
readinessSummary: ' ',
triggerDisabledReason: 'Patrol is paused',
}),
).toBe('Patrol is paused');
});
it('skips a whitespace-only setup finding title to reach the readiness summary', () => {
// Branch: normalizeText(setupFindingTitle) -> '' fails the truthy guard.
expect(
getPatrolSetupIssueReason({
setupFindingTitle: ' ',
readinessSummary: 'Model quota exhausted',
}),
).toBe('Model quota exhausted');
});
});
describe('getPatrolQueueActionDetail (via getPatrolQueueWorkspaceDescription)', () => {
it('returns the locked-control copy even when autonomy would allow fixes', () => {
// Branch: `input.autonomyLocked` short-circuits before the autonomy switch.
expect(
getPatrolQueueWorkspaceDescription({
autonomyLevel: 'full',
autonomyLocked: true,
findingCount: 2,
affectedResourceCount: 1,
}),
).toBe(
'Patrol found 2 issues on 1 affected resource. Open a row to review evidence and record the outcome.',
);
});
});
describe('getPatrolWorkspaceWorkGroups', () => {
it('surfaces a failed-check group from status error count alone, with checked-resource detail', () => {
// Branch: `latestRunFailed || statusErrorCount > 0` -> statusErrorCount path
// (latest run healthy), and checkedResources > 0 -> detailed detail string.
expect(
getPatrolWorkspaceWorkGroups({
latestRun: {
error_count: 0,
resources_checked: 3,
status: 'healthy',
},
nowMs: NOW_MS,
patrolStatus: {
error_count: 1,
next_patrol_at: '2026-07-01T10:00:00Z',
running: false,
},
}),
).toEqual([
{
detail: 'Patrol checked 3 resources but ended with runtime issues.',
id: 'failed-check',
label: 'Latest check needs review',
tone: 'danger',
},
]);
});
it('uses the generic failed-check detail when no resources were checked', () => {
// Branch: statusErrorCount > 0 with latestRun null -> checkedResources 0.
expect(
getPatrolWorkspaceWorkGroups({
latestRun: null,
nowMs: NOW_MS,
patrolStatus: {
error_count: 1,
next_patrol_at: '2026-07-01T10:00:00Z',
running: false,
},
}),
).toEqual([
{
detail: 'The last Patrol check ended with runtime issues.',
id: 'failed-check',
label: 'Latest check needs review',
tone: 'danger',
},
]);
});
it('does not add a failed-check group for an error status that normalizes away from "error"', () => {
// status 'errored_retry' normalizes to 'errored_retry' (!== 'error'); with
// no status error count and no other triggers, no group is produced.
expect(
getPatrolWorkspaceWorkGroups({
latestRun: {
error_count: 0,
resources_checked: 4,
status: 'errored_retry' as unknown as PatrolRunStatus,
},
}),
).toEqual([]);
});
});
});
@@ -0,0 +1,261 @@
import { describe, expect, it } from 'vitest';
import type { PBSBackup } from '@/types/api';
import {
classifyTaskStatus,
cmpBool,
cmpNumber,
cmpString,
pbsRepositoryLabel,
pbsWorkloadLabel,
} from '../proxmoxBackupsTableModel';
// Same loose fixture builder the sibling test file uses: Partial overrides
// cast whole as PBSBackup so omitted required fields are undefined at runtime,
// which is exactly what lets us drive the defensive `??` / `?.` / `||` arms.
const pbs = (overrides: Partial<PBSBackup>): PBSBackup => overrides as PBSBackup;
// ---------------------------------------------------------------------------
// pbsWorkloadLabel — sibling tests cover ct/vm and the host arms. These cover
// the fallback branch (unknown/absent backupType) and the toLowerCase path.
// ---------------------------------------------------------------------------
describe('pbsWorkloadLabel — uncovered branches', () => {
it('falls back to "Backup <vmid>" when backupType is absent and vmid is set', () => {
// backupType omitted -> runtime undefined -> `backup.backupType ?? ''`
// right operand fires; `backup.backupType?.trim()` short-circuits to
// undefined -> `|| 'Backup'` defaults the kind; vmid truthy -> `${kind} ${vmid}`.
expect(pbsWorkloadLabel(pbs({ vmid: '500' }))).toBe('Backup 500');
});
it('uppercases an unknown backupType and appends vmid', () => {
// Fallback: trim().toUpperCase() yields a non-empty kind; vmid truthy arm.
expect(pbsWorkloadLabel(pbs({ backupType: 'qemu', vmid: '555' }))).toBe('QEMU 555');
});
it('returns just the uppercased kind when vmid is empty for an unknown type', () => {
// Fallback return `: kind` arm (vmid falsy).
expect(pbsWorkloadLabel(pbs({ backupType: 'qemu', vmid: '' }))).toBe('QEMU');
});
it('defaults a whitespace-only backupType to "Backup" with no vmid', () => {
// backupType present but trim() -> '' -> `|| 'Backup'`; empty vmid -> `: kind`.
expect(pbsWorkloadLabel(pbs({ backupType: ' ', vmid: '' }))).toBe('Backup');
});
it('matches backupType case-insensitively on the ct arm', () => {
// Exercises `(backup.backupType ?? '').toLowerCase()` normalizing 'CT' -> 'ct'.
expect(pbsWorkloadLabel(pbs({ backupType: 'CT', vmid: '707' }))).toBe('LXC 707');
});
});
// ---------------------------------------------------------------------------
// cmpString — sibling tests cover the av<bv / av>bv and single-blank asc arms.
// These cover both-blank, both-undefined, single-undefined, equal, and desc.
// ---------------------------------------------------------------------------
describe('cmpString — uncovered branches', () => {
it('returns 0 when both values are empty strings', () => {
// `!av && !bv` arm.
expect(cmpString('', '', 'asc')).toBe(0);
expect(cmpString('', '', 'desc')).toBe(0);
});
it('returns 0 when both values are undefined', () => {
// Both `(a ?? '')` and `(b ?? '')` right operands fire, then `!av && !bv`.
expect(cmpString(undefined, undefined, 'asc')).toBe(0);
expect(cmpString(undefined, undefined, 'desc')).toBe(0);
});
it('pushes an undefined first value to the end regardless of direction', () => {
// `if (!av) return 1` arm (a undefined -> av '').
expect(cmpString(undefined, 'beta', 'asc')).toBe(1);
expect(cmpString(undefined, 'beta', 'desc')).toBe(1);
});
it('pushes an undefined second value to the end regardless of direction', () => {
// `if (!bv) return -1` arm (b undefined -> bv '').
expect(cmpString('alpha', undefined, 'asc')).toBe(-1);
expect(cmpString('alpha', undefined, 'desc')).toBe(-1);
});
it('returns 0 for equal non-empty values (desc negation yields -0)', () => {
// `av === bv ? 0` arm. asc returns +0; desc does `-cmp` = `-0`. The two are
// equal under `===` (sort-safe) but distinct under Object.is — asserted
// explicitly below to pin the real desc-arm output.
const asc = cmpString('alpha', 'alpha', 'asc');
const desc = cmpString('alpha', 'alpha', 'desc');
expect(asc).toBe(0);
expect(desc === 0).toBe(true);
expect(Object.is(desc, -0)).toBe(true);
});
it('keeps blank values last even under desc', () => {
// Blank guards intentionally ignore direction; sibling only asserted asc.
expect(cmpString('', 'beta', 'desc')).toBe(1);
expect(cmpString('beta', '', 'desc')).toBe(-1);
});
});
// ---------------------------------------------------------------------------
// cmpNumber — sibling tests cover a<b asc/desc, a=undefined, and a=NaN. These
// cover both-missing, b-missing, equal, non-finite coercion, and a>b desc.
// ---------------------------------------------------------------------------
describe('cmpNumber — uncovered branches', () => {
it('returns 0 when both values are undefined', () => {
// `av === undefined && bv === undefined` arm.
expect(cmpNumber(undefined, undefined, 'asc')).toBe(0);
expect(cmpNumber(undefined, undefined, 'desc')).toBe(0);
});
it('pushes an undefined second value to the end regardless of direction', () => {
// `if (bv === undefined) return -1` arm (sibling only covers a undefined).
expect(cmpNumber(5, undefined, 'asc')).toBe(-1);
expect(cmpNumber(5, undefined, 'desc')).toBe(-1);
});
it('returns 0 for equal finite numbers (desc negation yields -0)', () => {
// asc returns +0; desc does `-cmp` = `-0` for an equal pair.
const asc = cmpNumber(5, 5, 'asc');
const desc = cmpNumber(5, 5, 'desc');
expect(asc).toBe(0);
expect(desc === 0).toBe(true);
expect(Object.is(desc, -0)).toBe(true);
});
it('treats +Infinity, -Infinity, and NaN as missing', () => {
// `typeof === "number" && Number.isFinite(...)` rejects all three.
expect(cmpNumber(Number.POSITIVE_INFINITY, 5, 'asc')).toBe(1);
expect(cmpNumber(Number.NEGATIVE_INFINITY, 5, 'asc')).toBe(1);
expect(cmpNumber(5, Number.NaN, 'asc')).toBe(-1);
});
it('negates the comparison under desc when the first value is larger', () => {
// a > b -> cmp = +3; desc -> -3. Sibling desc case only had a < b (>0).
expect(cmpNumber(5, 2, 'asc')).toBe(3);
expect(cmpNumber(5, 2, 'desc')).toBe(-3);
});
});
// ---------------------------------------------------------------------------
// classifyTaskStatus — sibling tests cover ok/SUCCESS, running, error, empty,
// and a lowercase unknown. These cover 'completed', 'failed', case-insensitive
// warning, null status, and original-case preservation in the fallback label.
// ---------------------------------------------------------------------------
describe('classifyTaskStatus — uncovered branches', () => {
it('maps "completed" to the success variant with the exact full shape', () => {
// Third OR operand of the success condition (sibling covers ok/SUCCESS).
expect(classifyTaskStatus('completed')).toStrictEqual({
variant: 'success',
label: 'OK',
toneClass: 'text-emerald-600 dark:text-emerald-300',
});
});
it('maps "failed" to the danger variant with the exact full shape', () => {
// Other OR operand of the danger condition (sibling only covers 'error').
expect(classifyTaskStatus('failed')).toStrictEqual({
variant: 'danger',
label: 'Failed',
toneClass: 'text-red-600 dark:text-red-300',
});
});
it('maps "RUNNING" (upper) to warning via toLowerCase normalization', () => {
// `normalized === 'running'` arm reached only because of `.toLowerCase()`.
expect(classifyTaskStatus('RUNNING')).toStrictEqual({
variant: 'warning',
label: 'Running',
toneClass: 'text-amber-600 dark:text-amber-300',
});
});
it('treats a null status as empty and returns the muted em-dash', () => {
// `(status ?? '')` right operand -> '' -> `!normalized` early-return arm.
expect(
classifyTaskStatus(null as unknown as Parameters<typeof classifyTaskStatus>[0]),
).toStrictEqual({
variant: 'muted',
label: '—',
toneClass: 'text-muted',
});
});
it('preserves the original case in the fallback label for unknown statuses', () => {
// Terminal fallback returns `label: status` (the original, not normalized).
expect(classifyTaskStatus('Paused')).toStrictEqual({
variant: 'muted',
label: 'Paused',
toneClass: 'text-muted',
});
});
});
// ---------------------------------------------------------------------------
// pbsRepositoryLabel — sibling tests cover datastore+namespace and datastore
// with an absent namespace. These cover the em-dash datastore fallback, the
// whitespace/trim namespace paths, and the both-absent combination.
// ---------------------------------------------------------------------------
describe('pbsRepositoryLabel — uncovered branches', () => {
it('falls back to an em-dash when datastore is absent', () => {
// `backup.datastore || '—'` right operand.
expect(pbsRepositoryLabel(pbs({ namespace: 'team' }))).toBe('— / team');
});
it('falls back to an em-dash when datastore is an empty string', () => {
expect(pbsRepositoryLabel(pbs({ datastore: '', namespace: 'team' }))).toBe('— / team');
});
it('treats a whitespace-only namespace as root', () => {
// `namespace?.trim()` -> '' (falsy) -> '(root)'.
expect(pbsRepositoryLabel(pbs({ datastore: 'main', namespace: ' ' }))).toBe(
'main / (root)',
);
});
it('trims surrounding whitespace from a named namespace', () => {
// `namespace?.trim()` -> 'team' (truthy, trimmed).
expect(pbsRepositoryLabel(pbs({ datastore: 'main', namespace: ' team ' }))).toBe(
'main / team',
);
});
it('falls back fully when both datastore and namespace are absent', () => {
// namespace undefined -> `?.` short-circuits -> '(root)'; datastore -> '—'.
expect(pbsRepositoryLabel(pbs({}))).toBe('— / (root)');
});
});
// ---------------------------------------------------------------------------
// cmpBool — sibling tests cover true/false desc, true/false asc, and true/true
// asc. These cover false/false, false/true (both directions), and true/true desc.
// ---------------------------------------------------------------------------
describe('cmpBool — uncovered branches', () => {
it('returns 0 for false vs false (desc negation yields -0)', () => {
// Both `(a ? 1 : 0)` and `(b ? 1 : 0)` take their falsy arms -> cmp 0.
const asc = cmpBool(false, false, 'asc');
const desc = cmpBool(false, false, 'desc');
expect(asc).toBe(0);
expect(desc === 0).toBe(true);
expect(Object.is(desc, -0)).toBe(true);
});
it('sorts false ahead of true under asc and behind under desc', () => {
// a falsy / b truthy: cmp = 0 - 1 = -1; asc keeps -1, desc negates to +1.
expect(cmpBool(false, true, 'asc')).toBe(-1);
expect(cmpBool(false, true, 'desc')).toBe(1);
});
it('returns 0 for true vs true under desc (desc negation yields -0)', () => {
// Sibling covers true/true asc; this nails the desc arm, whose `-cmp` of a
// zero produces -0.
const desc = cmpBool(true, true, 'desc');
expect(desc === 0).toBe(true);
expect(Object.is(desc, -0)).toBe(true);
});
});
@@ -0,0 +1,795 @@
import { describe, expect, it } from 'vitest';
import type { PlatformType, Resource, ResourceAlert, ResourceMetric, ResourceType } from '@/types/resource';
import {
buildProxmoxPageModel,
getMetricPercent,
getResourceClusterLabel,
getResourceLastBackup,
getResourceNodeName,
getResourceVersion,
getResourceVmid,
isProxmoxStorageResource,
resolveProxmoxPlatformScope,
} from '../proxmoxPageModel';
// ---------------------------------------------------------------------------
// Fixture builder — mirrors the sibling proxmoxPageModel.test.ts factory so
// import style and default platform posture stay aligned.
// ---------------------------------------------------------------------------
const makeResource = (resource: Partial<Resource> & Pick<Resource, 'id' | 'type'>): Resource => ({
name: resource.id,
displayName: resource.id,
platformId: 'lab',
platformType: 'proxmox-pve',
sourceType: 'api',
status: 'online',
lastSeen: 1_700_000_000_000,
...resource,
});
const alert = (overrides: Partial<ResourceAlert> = {}): ResourceAlert => ({
id: 'alert-1',
type: 'cpu',
level: 'warning',
message: 'high load',
value: 95,
threshold: 90,
startTime: 1_700_000_000_000,
...overrides,
});
// ===========================================================================
// getPlatformSources — module-private, exercised transitively through
// resolveProxmoxPlatformScope (its only call site).
// ===========================================================================
describe('getPlatformSources branches (via resolveProxmoxPlatformScope)', () => {
it('falls back to resource.sources when platformData.sources is not an array', () => {
// platformData.sources is a string -> not Array.isArray -> returns resource.sources.
const resource = makeResource({
id: 'r',
type: 'agent',
platformType: 'generic',
sources: ['pbs'],
platformData: { sources: 'not-an-array' },
});
expect(resolveProxmoxPlatformScope(resource)).toBe('proxmox-pbs');
});
it('returns [] when platformData.sources is not an array and resource.sources is undefined', () => {
// `resource.sources ?? []` -> [] -> no source match -> null.
const resource = makeResource({
id: 'r',
type: 'agent',
platformType: 'generic',
platformData: { sources: 'not-an-array' },
});
expect(resolveProxmoxPlatformScope(resource)).toBeNull();
});
it('filters platformData.sources to strings only (non-strings would crash toLowerCase)', () => {
// If the filter did not strip the number/boolean/null, the downstream
// `.toLowerCase()` in resolveProxmoxPlatformScope would throw.
const resource = makeResource({
id: 'r',
type: 'agent',
platformType: 'generic',
platformData: { sources: ['proxmox-pmg', 123, true, null, 'noise'] },
});
expect(resolveProxmoxPlatformScope(resource)).toBe('proxmox-pmg');
});
it('returns null when the filtered source list contains no known scope hint', () => {
const resource = makeResource({
id: 'r',
type: 'agent',
platformType: 'generic',
platformData: { sources: ['docker', 'kubernetes'] },
});
expect(resolveProxmoxPlatformScope(resource)).toBeNull();
});
});
// ===========================================================================
// resolveProxmoxPlatformScope — direct arms beyond getPlatformSources.
// ===========================================================================
describe('resolveProxmoxPlatformScope direct arms', () => {
it.each<[string, ResourceType, PlatformType]>([
['resolves platformType proxmox-pve', 'agent', 'proxmox-pve'],
['resolves platformType proxmox-pbs for a datastore', 'datastore', 'proxmox-pbs'],
['resolves platformType proxmox-pmg', 'agent', 'proxmox-pmg'],
])('%s', (_label, type, platformType) => {
const resource = makeResource({ id: 'r', type, platformType });
expect(resolveProxmoxPlatformScope(resource)).toBe(platformType);
});
it('resolves proxmox-pve when resource.proxmox is a truthy object', () => {
const resource = makeResource({
id: 'r',
type: 'agent',
platformType: 'generic',
proxmox: { nodeName: 'n1' },
});
expect(resolveProxmoxPlatformScope(resource)).toBe('proxmox-pve');
});
it('resolves proxmox-pve when platformData.proxmox is a record (resource.proxmox absent)', () => {
const resource = makeResource({
id: 'r',
type: 'agent',
platformType: 'generic',
platformData: { proxmox: { hint: 'pve' } },
});
expect(resolveProxmoxPlatformScope(resource)).toBe('proxmox-pve');
});
it.each<[string, string]>([
['pbs short hint', 'pbs'],
['PBS uppercase hint (case-insensitive)', 'PBS'],
['proxmox-pbs long hint', 'proxmox-pbs'],
])('resolves proxmox-pbs from source %s', (_label, source) => {
const resource = makeResource({
id: 'r',
type: 'agent',
platformType: 'generic',
sources: [source],
});
expect(resolveProxmoxPlatformScope(resource)).toBe('proxmox-pbs');
});
it.each<[string, string]>([
['pmg short hint', 'pmg'],
['proxmox-pmg long hint', 'proxmox-pmg'],
])('resolves proxmox-pmg from source %s', (_label, source) => {
const resource = makeResource({
id: 'r',
type: 'agent',
platformType: 'generic',
sources: [source],
});
expect(resolveProxmoxPlatformScope(resource)).toBe('proxmox-pmg');
});
it.each<[string, string]>([
['pve short hint', 'pve'],
['proxmox-pve long hint', 'proxmox-pve'],
])('resolves proxmox-pve from source %s', (_label, source) => {
const resource = makeResource({
id: 'r',
type: 'agent',
platformType: 'generic',
sources: [source],
});
expect(resolveProxmoxPlatformScope(resource)).toBe('proxmox-pve');
});
it('returns null when no scope signal is present', () => {
const resource = makeResource({ id: 'r', type: 'agent', platformType: 'generic' });
expect(resolveProxmoxPlatformScope(resource)).toBeNull();
});
it('prefers type pbs over a contradicting platformType', () => {
const resource = makeResource({ id: 'r', type: 'pbs', platformType: 'proxmox-pve' });
expect(resolveProxmoxPlatformScope(resource)).toBe('proxmox-pbs');
});
});
// ===========================================================================
// isProxmoxStorageResource
// ===========================================================================
describe('isProxmoxStorageResource branches', () => {
it('returns false for a ceph-typed resource', () => {
const resource = makeResource({ id: 'ceph-1', type: 'ceph' });
expect(isProxmoxStorageResource(resource)).toBe(false);
});
it('returns false when storage.isCeph is true even on a storage type', () => {
const resource = makeResource({
id: 'stor-ceph',
type: 'storage',
storage: { isCeph: true },
});
expect(isProxmoxStorageResource(resource)).toBe(false);
});
it('returns true for a storage resource under proxmox-pve', () => {
const resource = makeResource({
id: 'stor-1',
type: 'storage',
storage: { isCeph: false },
});
expect(isProxmoxStorageResource(resource)).toBe(true);
});
it('returns true for a datastore resource under proxmox-pbs', () => {
const resource = makeResource({
id: 'ds-1',
type: 'datastore',
platformType: 'proxmox-pbs',
});
expect(isProxmoxStorageResource(resource)).toBe(true);
});
it('returns false for a storage resource under proxmox-pmg', () => {
const resource = makeResource({
id: 'stor-pmg',
type: 'storage',
platformType: 'proxmox-pmg',
});
expect(isProxmoxStorageResource(resource)).toBe(false);
});
it('returns false for a storage resource with no resolved scope', () => {
const resource = makeResource({
id: 'stor-orphan',
type: 'storage',
platformType: 'generic',
});
expect(isProxmoxStorageResource(resource)).toBe(false);
});
it('returns false for a non-storage proxmox-pve resource', () => {
const resource = makeResource({ id: 'agent-1', type: 'agent' });
expect(isProxmoxStorageResource(resource)).toBe(false);
});
});
// ===========================================================================
// getMetricPercent
// ===========================================================================
describe('getMetricPercent branches', () => {
it('returns 0 when no metric is provided', () => {
expect(getMetricPercent(undefined)).toBe(0);
});
it('returns the finite current value unchanged within range', () => {
expect(getMetricPercent({ current: 42.7 })).toBe(42.7);
});
it('clamps an over-range current to 100', () => {
expect(getMetricPercent({ current: 150 })).toBe(100);
});
it('clamps a negative current to 0', () => {
expect(getMetricPercent({ current: -20 })).toBe(0);
});
it('does not clamp the exact boundary 100', () => {
expect(getMetricPercent({ current: 100 })).toBe(100);
});
it('falls back to the used/total ratio when current is NaN', () => {
const metric: ResourceMetric = { current: NaN, total: 200, used: 50 };
expect(getMetricPercent(metric)).toBe(25);
});
it('falls back to the used/total ratio clamped to 100', () => {
const metric: ResourceMetric = { current: NaN, total: 10, used: 30 };
expect(getMetricPercent(metric)).toBe(100);
});
it('uses used/total when current is a non-number (defensive cast)', () => {
const metric = {
current: 'bad' as unknown as number,
total: 100,
used: 100,
} satisfies ResourceMetric;
expect(getMetricPercent(metric)).toBe(100);
});
it('returns 0 when current is not a finite number and total/used are absent', () => {
const metric: ResourceMetric = { current: NaN };
expect(getMetricPercent(metric)).toBe(0);
});
});
// ===========================================================================
// getResourceVmid
// ===========================================================================
describe('getResourceVmid branches', () => {
it('returns the proxmox.vmid as a string when it is a finite number', () => {
const resource = makeResource({ id: 'vm-1', type: 'vm', proxmox: { vmid: 101 } });
expect(getResourceVmid(resource)).toBe('101');
});
it('ignores a non-finite proxmox.vmid (NaN) and reads platformData.proxmox.vmid', () => {
const resource = makeResource({
id: 'vm-2',
type: 'vm',
proxmox: { vmid: NaN },
platformData: { proxmox: { vmid: 202 } },
});
expect(getResourceVmid(resource)).toBe('202');
});
it('ignores a wrong-typed proxmox.vmid (string) when no platformData vmid exists', () => {
const resource = makeResource({
id: 'vm-3',
type: 'vm',
proxmox: { vmid: '103' as unknown as number },
});
expect(getResourceVmid(resource)).toBe('');
});
it('reads vmid from platformData.proxmox when no meta proxmox block exists', () => {
const resource = makeResource({
id: 'vm-4',
type: 'vm',
platformData: { proxmox: { vmid: 303 } },
});
expect(getResourceVmid(resource)).toBe('303');
});
it('returns empty string when neither source carries a numeric vmid', () => {
const resource = makeResource({ id: 'vm-5', type: 'vm' });
expect(getResourceVmid(resource)).toBe('');
});
it('returns empty string when platformData.proxmox is not a record', () => {
const resource = makeResource({
id: 'vm-6',
type: 'vm',
platformData: { proxmox: 'not-a-record' },
});
expect(getResourceVmid(resource)).toBe('');
});
});
// ===========================================================================
// getResourceVersion
// ===========================================================================
describe('getResourceVersion branches', () => {
it('skips a non-formatable platformData.proxmox.pveVersion and falls through to pbs.version', () => {
// formatProxmoxVersion('unknown') -> '' -> inner `if (version)` is false.
const resource = makeResource({
id: 'r',
type: 'agent',
platformData: { proxmox: { pveVersion: 'unknown' } },
pbs: { version: '3.0' },
});
expect(getResourceVersion(resource)).toBe('3.0');
});
it('returns resource.pbs.version when no pve signal is present', () => {
const resource = makeResource({
id: 'pbs-1',
type: 'pbs',
platformType: 'proxmox-pbs',
pbs: { version: '3.2' },
});
expect(getResourceVersion(resource)).toBe('3.2');
});
it('returns platformData.pbs.version when meta pbs.version is absent', () => {
const resource = makeResource({
id: 'pbs-2',
type: 'pbs',
platformType: 'proxmox-pbs',
platformData: { pbs: { version: '3.3' } },
});
expect(getResourceVersion(resource)).toBe('3.3');
});
it('returns platformData.pmg.version for a pmg resource', () => {
const resource = makeResource({
id: 'pmg-1',
type: 'pmg',
platformType: 'proxmox-pmg',
platformData: { pmg: { version: '8.1' } },
});
expect(getResourceVersion(resource)).toBe('8.1');
});
it('formats agent.osVersion when agent.osName mentions proxmox', () => {
const resource = makeResource({
id: 'agent-1',
type: 'agent',
agent: { osName: 'Proxmox VE', osVersion: 'pve-manager/9.0/abc' },
});
expect(getResourceVersion(resource)).toBe('9.0');
});
it('falls back to the raw osVersion when it is not formatable', () => {
// formatProxmoxVersion('unknown') === '' -> returns raw osVersion.
const resource = makeResource({
id: 'agent-2',
type: 'agent',
agent: { osName: 'proxmox', osVersion: 'unknown' },
});
expect(getResourceVersion(resource)).toBe('unknown');
});
it('does not take the agent branch when osName omits proxmox', () => {
const resource = makeResource({
id: 'agent-3',
type: 'agent',
agent: { osName: 'Debian', osVersion: '12' },
});
expect(getResourceVersion(resource)).toBe('');
});
it('returns empty string when no version signal is present at all', () => {
const resource = makeResource({ id: 'agent-4', type: 'agent' });
expect(getResourceVersion(resource)).toBe('');
});
});
// ===========================================================================
// getResourceClusterLabel
// ===========================================================================
describe('getResourceClusterLabel branches', () => {
it('prefers proxmox.clusterName', () => {
const resource = makeResource({
id: 'r',
type: 'agent',
proxmox: { clusterName: 'alpha' },
identity: { clusterName: 'shadow' },
clusterId: 'cid',
});
expect(getResourceClusterLabel(resource)).toBe('alpha');
});
it('falls back to identity.clusterName when proxmox.clusterName is absent', () => {
const resource = makeResource({
id: 'r',
type: 'agent',
identity: { clusterName: 'beta' },
clusterId: 'cid',
});
expect(getResourceClusterLabel(resource)).toBe('beta');
});
it('falls back to clusterId when no clusterName is present', () => {
const resource = makeResource({ id: 'r', type: 'agent', clusterId: 'clus-1' });
expect(getResourceClusterLabel(resource)).toBe('clus-1');
});
it('returns "Standalone" when nothing is set', () => {
const resource = makeResource({ id: 'r', type: 'agent' });
expect(getResourceClusterLabel(resource)).toBe('Standalone');
});
});
// ===========================================================================
// getResourceNodeName
// ===========================================================================
describe('getResourceNodeName branches', () => {
it('prefers proxmox.nodeName', () => {
const resource = makeResource({
id: 'r',
type: 'agent',
proxmox: { nodeName: 'n1', node: 'shadow' },
parentName: 'p',
identity: { hostname: 'h' },
});
expect(getResourceNodeName(resource)).toBe('n1');
});
it('falls back to proxmox.node when nodeName is absent', () => {
const resource = makeResource({
id: 'r',
type: 'agent',
proxmox: { node: 'n2' },
parentName: 'p',
});
expect(getResourceNodeName(resource)).toBe('n2');
});
it('falls back to parentName when no proxmox node field is set', () => {
const resource = makeResource({
id: 'r',
type: 'vm',
parentName: 'p1',
identity: { hostname: 'h' },
});
expect(getResourceNodeName(resource)).toBe('p1');
});
it('falls back to identity.hostname when no node/parent is set', () => {
const resource = makeResource({
id: 'r',
type: 'vm',
identity: { hostname: 'h1' },
});
expect(getResourceNodeName(resource)).toBe('h1');
});
it('falls back to resource.name when nothing else is set', () => {
const resource = makeResource({ id: 'fallback-id', type: 'vm' });
// makeResource defaults name === id.
expect(getResourceNodeName(resource)).toBe('fallback-id');
});
});
// ===========================================================================
// getResourceLastBackup
// ===========================================================================
describe('getResourceLastBackup branches', () => {
it('returns null when platformData is absent', () => {
const resource = makeResource({ id: 'r', type: 'vm' });
expect(getResourceLastBackup(resource)).toBeNull();
});
it('returns null when platformData.proxmox is not a record', () => {
const resource = makeResource({
id: 'r',
type: 'vm',
platformData: { proxmox: 'not-a-record' },
});
expect(getResourceLastBackup(resource)).toBeNull();
});
it('returns the lastBackup string verbatim', () => {
const resource = makeResource({
id: 'r',
type: 'vm',
platformData: { proxmox: { lastBackup: '2024-01-01T00:00:00Z' } },
});
expect(getResourceLastBackup(resource)).toBe('2024-01-01T00:00:00Z');
});
it('returns the lastBackup number verbatim', () => {
const resource = makeResource({
id: 'r',
type: 'vm',
platformData: { proxmox: { lastBackup: 1_700_000_000_000 } },
});
expect(getResourceLastBackup(resource)).toBe(1_700_000_000_000);
});
it('returns null when lastBackup is a boolean', () => {
const resource = makeResource({
id: 'r',
type: 'vm',
platformData: { proxmox: { lastBackup: true } },
});
expect(getResourceLastBackup(resource)).toBeNull();
});
it('returns null when lastBackup is an object', () => {
const resource = makeResource({
id: 'r',
type: 'vm',
platformData: { proxmox: { lastBackup: { ts: 1 } } },
});
expect(getResourceLastBackup(resource)).toBeNull();
});
});
// ===========================================================================
// buildProxmoxPageModel
// ===========================================================================
describe('buildProxmoxPageModel empty input', () => {
it('returns a fully empty model', () => {
expect(buildProxmoxPageModel([])).toEqual({
resources: [],
pveNodes: [],
guests: [],
storage: [],
pbs: [],
pmg: [],
ceph: [],
physicalDisks: [],
clusterGroups: [],
summary: {
clusterCount: 0,
nodeCount: 0,
guestCount: 0,
runningGuestCount: 0,
degradedGuestCount: 0,
stoppedGuestCount: 0,
storageCount: 0,
pbsCount: 0,
pmgCount: 0,
cephCount: 0,
alertCount: 0,
},
});
});
});
describe('buildProxmoxPageModel estate classification, status counts, and alert sum', () => {
const nodeA = makeResource({
id: 'node-a',
type: 'agent',
proxmox: { nodeName: 'node-a', clusterName: 'cluster-x' },
alerts: [alert()],
incidentCount: 2,
});
const guests = [
makeResource({ id: 'vm-r', type: 'vm', status: 'running', proxmox: { vmid: 1, nodeName: 'node-a' } }),
makeResource({ id: 'vm-o', type: 'vm', status: 'online', proxmox: { vmid: 2, nodeName: 'node-a' } }),
makeResource({
id: 'vm-d',
type: 'vm',
status: 'degraded',
proxmox: { vmid: 3, nodeName: 'node-a' },
}),
makeResource({
id: 'ct-w',
type: 'system-container',
status: 'warning',
proxmox: { vmid: 4, nodeName: 'node-a' },
}),
makeResource({
id: 'vm-off',
type: 'vm',
status: 'offline',
proxmox: { vmid: 5, nodeName: 'node-a' },
}),
makeResource({
id: 'vm-st',
type: 'vm',
status: 'stopped',
proxmox: { vmid: 6, nodeName: 'node-a' },
}),
];
it('classifies resources, excludes non-proxmox, and aggregates summary + alerts', () => {
const model = buildProxmoxPageModel([
makeResource({ id: 'dh', type: 'docker-host', platformType: 'docker' }),
nodeA,
...guests,
]);
expect(model.resources.map((r) => r.id)).not.toContain('dh');
expect(model.pveNodes.map((r) => r.id)).toEqual(['node-a']);
expect(model.guests.map((r) => r.id)).toEqual([
'vm-r',
'vm-o',
'vm-d',
'ct-w',
'vm-off',
'vm-st',
]);
expect(model.summary).toMatchObject({
guestCount: 6,
runningGuestCount: 2,
degradedGuestCount: 2,
stoppedGuestCount: 2,
nodeCount: 1,
clusterCount: 1,
alertCount: 3,
});
});
});
describe('buildProxmoxPageModel pbs / pmg / ceph / physicalDisks sections', () => {
it('routes resources into the pbs, pmg, ceph, and physicalDisks arrays', () => {
const model = buildProxmoxPageModel([
makeResource({ id: 'pbs-1', type: 'pbs', platformType: 'proxmox-pbs' }),
makeResource({ id: 'pmg-1', type: 'pmg', platformType: 'proxmox-pmg' }),
makeResource({
id: 'ceph-typed',
type: 'ceph',
platformData: { ceph: { healthStatus: 'healthy' } },
}),
makeResource({
id: 'stor-ceph',
type: 'storage',
storage: { isCeph: true },
}),
makeResource({
id: 'disk-1',
type: 'physical_disk',
parentName: 'node-a',
}),
]);
expect(model.pbs.map((r) => r.id)).toEqual(['pbs-1']);
expect(model.pmg.map((r) => r.id)).toEqual(['pmg-1']);
// ceph-typed (type ceph) and stor-ceph (storage.isCeph) both count as ceph.
expect(model.ceph.map((r) => r.id)).toEqual(['ceph-typed', 'stor-ceph']);
expect(model.physicalDisks.map((r) => r.id)).toEqual(['disk-1']);
// A Ceph-backed storage is intentionally NOT a Proxmox storage resource.
expect(model.storage.map((r) => r.id)).not.toContain('stor-ceph');
// physical_disk is a Proxmox storage type, so it appears in storage too.
expect(model.storage.map((r) => r.id)).toEqual(['disk-1']);
expect(model.summary).toMatchObject({
pbsCount: 1,
pmgCount: 1,
cephCount: 2,
storageCount: 1,
});
});
});
describe('buildProxmoxPageModel cluster grouping', () => {
it('assigns guests and storage to a named cluster via the owning node', () => {
const nodeA = makeResource({
id: 'node-a',
type: 'agent',
proxmox: { nodeName: 'node-a', clusterName: 'cluster-x' },
});
const guestOnNode = makeResource({
id: 'vm-1',
type: 'vm',
status: 'running',
proxmox: { vmid: 1, nodeName: 'node-a' },
});
const storageOnNode = makeResource({
id: 'stor-1',
type: 'storage',
parentName: 'node-a',
storage: { isCeph: false },
});
const model = buildProxmoxPageModel([nodeA, guestOnNode, storageOnNode]);
expect(model.clusterGroups.map((g) => g.id)).toEqual(['cluster-x']);
const group = model.clusterGroups[0];
expect(group.nodes.map((r) => r.id)).toEqual(['node-a']);
expect(group.guests.map((r) => r.id)).toEqual(['vm-1']);
expect(group.storage.map((r) => r.id)).toEqual(['stor-1']);
});
it('drops orphan guests/storage into the standalone bucket using their own cluster label', () => {
const nodeA = makeResource({
id: 'node-a',
type: 'agent',
proxmox: { nodeName: 'node-a', clusterName: 'cluster-x' },
});
const orphanGuest = makeResource({
id: 'vm-orphan',
type: 'vm',
status: 'running',
proxmox: { vmid: 9 },
parentName: 'ghost-node',
});
const orphanStorage = makeResource({
id: 'stor-orphan',
type: 'storage',
parentName: 'ghost-node',
storage: {},
});
const model = buildProxmoxPageModel([nodeA, orphanGuest, orphanStorage]);
expect(model.clusterGroups.map((g) => g.id)).toEqual(['cluster-x', '__standalone__']);
const standalone = model.clusterGroups[1];
expect(standalone.id).toBe('__standalone__');
expect(standalone.label).toBe('Standalone');
expect(standalone.nodes).toEqual([]);
expect(standalone.guests.map((r) => r.id)).toEqual(['vm-orphan']);
expect(standalone.storage.map((r) => r.id)).toEqual(['stor-orphan']);
// Standalone never counts as a real cluster.
expect(model.summary.clusterCount).toBe(1);
});
it('sorts named clusters by label and keeps standalone last', () => {
const nodeBravo = makeResource({
id: 'node-bravo',
type: 'agent',
proxmox: { nodeName: 'node-bravo', clusterName: 'bravo' },
});
const nodeAlpha = makeResource({
id: 'node-alpha',
type: 'agent',
proxmox: { nodeName: 'node-alpha', clusterName: 'alpha' },
});
const orphanGuest = makeResource({
id: 'vm-solo',
type: 'vm',
status: 'running',
proxmox: { vmid: 1 },
parentName: 'solo-node',
});
const model = buildProxmoxPageModel([nodeBravo, nodeAlpha, orphanGuest]);
expect(model.clusterGroups.map((g) => g.label)).toEqual(['alpha', 'bravo', 'Standalone']);
expect(model.clusterGroups.map((g) => g.id)).toEqual(['alpha', 'bravo', '__standalone__']);
});
});
@@ -0,0 +1,474 @@
import { describe, expect, it } from 'vitest';
import type { Resource } from '@/types/resource';
import {
getStorageCapabilitiesForResource,
getStorageCategoryFromType,
isCanonicalDatastoreStorageResource,
readResourceStorageMeta,
type ResourceStorageMeta,
type StorageClassificationContext,
} from '@/features/storageBackups/resourceStorageMapping';
// ---------------------------------------------------------------------------
// Shared fixtures — mirror the conventions in resourceStorageMapping.test.ts:
// a `makeResource` factory cast as `Resource`, plus a `platformData` record.
// ---------------------------------------------------------------------------
const makeResource = (overrides: Partial<Resource> = {}): Resource =>
({
id: 'storage-1',
type: 'storage',
name: 'tank',
platformType: 'truenas',
sourceType: 'api',
...overrides,
}) as Resource;
// Sentinel used in platformData.storage so that, when resource.storage is
// rejected by normalizeStorageMeta (returns null), readResourceStorageMeta
// falls through and we can observe the fallback shape — proving the null arm.
const FALLBACK_PLATFORM_STORAGE = { type: 'fallback-marker' } as const;
// Drive the (private) normalizeStorageMeta through the public
// readResourceStorageMeta. resource.storage is the unit under test; the
// sentinel in platformData.storage lets us detect when normalizeStorageMeta
// returned null (fallthrough) vs. returned the normalized direct meta.
const normalizeOf = (input: unknown): ResourceStorageMeta | undefined =>
readResourceStorageMeta(
{ ...makeResource(), storage: input } as unknown as Resource,
{ storage: FALLBACK_PLATFORM_STORAGE },
);
// ===========================================================================
// normalizeStorageMeta (module-private — exercised via readResourceStorageMeta)
// ===========================================================================
describe('normalizeStorageMeta (via readResourceStorageMeta)', () => {
// ---- Branch: `!value || typeof value !== 'object'` → return null -------
it('returns null for null/undefined/non-object inputs (observed via platformData fallthrough)', () => {
// null → null
expect(normalizeOf(null)).toEqual({ type: 'fallback-marker' });
// undefined → null
expect(normalizeOf(undefined)).toEqual({ type: 'fallback-marker' });
// primitive number → null
expect(normalizeOf(42)).toEqual({ type: 'fallback-marker' });
// primitive string → null
expect(normalizeOf('not-an-object')).toEqual({ type: 'fallback-marker' });
// boolean → null
expect(normalizeOf(true)).toEqual({ type: 'fallback-marker' });
});
it('returns undefined when both direct and platform storage normalize to null', () => {
// Both inputs non-object → both normalize to null → readResourceStorageMeta
// resolves to `null || undefined` → undefined (the `nestedMeta || undefined`
// arm of readResourceStorageMeta, exercised through normalizeStorageMeta).
expect(
readResourceStorageMeta(
{ ...makeResource(), storage: null } as unknown as Resource,
{ storage: 'not-an-object-either' },
),
).toBeUndefined();
});
// ---- Branch: each `typeof candidate.X === 'string' ? X : undefined` ----
it('preserves every string-valued field and returns the full normalized shape', () => {
const normalized = normalizeOf({
type: 'rbd',
platform: 'proxmox-pve',
topology: 'pool',
content: 'images',
protection: 'protected',
arrayState: 'online',
syncAction: 'syncing',
path: '/dev/sda',
});
expect(normalized).toEqual({
type: 'rbd',
platform: 'proxmox-pve',
topology: 'pool',
content: 'images',
protection: 'protected',
arrayState: 'online',
syncAction: 'syncing',
path: '/dev/sda',
contentTypes: undefined,
shared: undefined,
syncProgress: undefined,
numProtected: undefined,
numDisabled: undefined,
numInvalid: undefined,
numMissing: undefined,
isCeph: undefined,
isZfs: undefined,
zfsPool: undefined,
});
});
it('coerces wrong-typed string fields to undefined (defensive ternary false arm)', () => {
const normalized = normalizeOf({
type: 123,
platform: 456,
topology: true,
content: { x: 1 },
protection: ['no'],
arrayState: 7,
syncAction: 8,
path: 9,
});
// Each defensive `typeof X === 'string'` is false → undefined.
expect(normalized?.type).toBeUndefined();
expect(normalized?.platform).toBeUndefined();
expect(normalized?.topology).toBeUndefined();
expect(normalized?.content).toBeUndefined();
expect(normalized?.protection).toBeUndefined();
expect(normalized?.arrayState).toBeUndefined();
expect(normalized?.syncAction).toBeUndefined();
expect(normalized?.path).toBeUndefined();
});
// ---- Branch: each `typeof candidate.X === 'boolean' ? X : undefined` ---
it('preserves boolean fields and coerces non-booleans to undefined', () => {
expect(normalizeOf({ shared: true, isCeph: false, isZfs: true })?.shared).toBe(true);
expect(normalizeOf({ shared: true, isCeph: false, isZfs: true })?.isCeph).toBe(false);
expect(normalizeOf({ shared: true, isCeph: false, isZfs: true })?.isZfs).toBe(true);
const wrongBool = normalizeOf({ shared: 'yes', isCeph: 1, isZfs: null });
expect(wrongBool?.shared).toBeUndefined();
expect(wrongBool?.isCeph).toBeUndefined();
expect(wrongBool?.isZfs).toBeUndefined();
});
// ---- Branch: each `typeof candidate.X === 'number' ? X : undefined` ----
it('preserves numeric fields and coerces non-numbers to undefined', () => {
const ok = normalizeOf({
syncProgress: 50,
numProtected: 3,
numDisabled: 1,
numInvalid: 2,
numMissing: 0,
});
expect(ok?.syncProgress).toBe(50);
expect(ok?.numProtected).toBe(3);
expect(ok?.numDisabled).toBe(1);
expect(ok?.numInvalid).toBe(2);
expect(ok?.numMissing).toBe(0);
const wrong = normalizeOf({
syncProgress: '50',
numProtected: true,
numDisabled: '1',
numInvalid: null,
numMissing: undefined,
});
expect(wrong?.syncProgress).toBeUndefined();
expect(wrong?.numProtected).toBeUndefined();
expect(wrong?.numDisabled).toBeUndefined();
expect(wrong?.numInvalid).toBeUndefined();
expect(wrong?.numMissing).toBeUndefined();
});
// ---- Branch: `Array.isArray(candidate.contentTypes) ? filter : undefined`
it('returns contentTypes undefined when the value is not an array', () => {
expect(normalizeOf({ contentTypes: 'not-array' })?.contentTypes).toBeUndefined();
expect(normalizeOf({ contentTypes: { 0: 'a' } })?.contentTypes).toBeUndefined();
expect(normalizeOf({ contentTypes: 42 })?.contentTypes).toBeUndefined();
expect(normalizeOf({})?.contentTypes).toBeUndefined();
});
it('keeps only non-empty trimmed strings from contentTypes and drops everything else', () => {
// The filter keeps `typeof item === 'string' && item.trim().length > 0`.
// Mixed: valid string, empty string, whitespace-only, numbers, null,
// booleans, object — only the genuinely-populated strings survive.
expect(
normalizeOf({ contentTypes: ['images', '', ' ', 42, null, false, {}, 'rootdir'] })
?.contentTypes,
).toEqual(['images', 'rootdir']);
// An array of only rejectable items collapses to an empty array
// (NOT undefined — the array branch was taken).
expect(normalizeOf({ contentTypes: ['', ' ', 1, null] })?.contentTypes).toEqual([]);
});
// ---- Branch: `candidate.zfsPool && typeof ... === 'object' ? cast : undef`
it('returns zfsPool as-is when it is an object, undefined otherwise', () => {
const pool = { name: 'tank', state: 'ONLINE' };
expect(normalizeOf({ zfsPool: pool })?.zfsPool).toEqual(pool);
// null → falsy short-circuit → undefined
expect(normalizeOf({ zfsPool: null })?.zfsPool).toBeUndefined();
// primitive → typeof !== 'object' → undefined
expect(normalizeOf({ zfsPool: 'tank' })?.zfsPool).toBeUndefined();
expect(normalizeOf({ zfsPool: 42 })?.zfsPool).toBeUndefined();
});
});
// ===========================================================================
// isCanonicalDatastoreStorageResource
// ===========================================================================
describe('isCanonicalDatastoreStorageResource', () => {
// ---- Branch: isBackupRepositoryStorageResource(...) === true → false ----
it('returns false when the resource is classified as a backup repository', () => {
expect(isCanonicalDatastoreStorageResource('pbs')).toBe(false);
// via context.resourceType
expect(
isCanonicalDatastoreStorageResource('whatever', undefined, { resourceType: 'pbs' }),
).toBe(false);
// via context.platform (substring match)
expect(
isCanonicalDatastoreStorageResource('whatever', undefined, { platform: 'proxmox-pbs' }),
).toBe(false);
// via context.topology exact match
expect(
isCanonicalDatastoreStorageResource('whatever', undefined, { topology: 'backup-target' }),
).toBe(false);
});
// ---- Branch: resourceType === 'datastore' → true ----------------------
it('returns true when context.resourceType is datastore', () => {
expect(
isCanonicalDatastoreStorageResource('mystery', undefined, { resourceType: 'datastore' }),
).toBe(true);
});
// ---- Branch: topology === 'datastore' (context OR storageMeta) --------
it('returns true when topology is datastore from context or storageMeta', () => {
expect(
isCanonicalDatastoreStorageResource('mystery', undefined, { topology: 'datastore' }),
).toBe(true);
expect(isCanonicalDatastoreStorageResource('mystery', { topology: 'datastore' })).toBe(true);
});
// ---- Branch: entityType === 'datastore' → true -----------------------
it('returns true when context.entityType is datastore', () => {
expect(
isCanonicalDatastoreStorageResource('mystery', undefined, { entityType: 'datastore' }),
).toBe(true);
});
// ---- Branch: (platform.includes('vmware') && value.length > 0) -------
it('returns true for vmware platform with a non-empty type, false when type is empty', () => {
// context.platform path
expect(
isCanonicalDatastoreStorageResource('vmfs', undefined, { platform: 'vmware-vsphere' }),
).toBe(true);
// storageMeta.platform path
expect(isCanonicalDatastoreStorageResource('vmfs', { platform: 'vmware-vsphere' })).toBe(true);
// value.length === 0 → false even with vmware platform
expect(
isCanonicalDatastoreStorageResource('', undefined, { platform: 'vmware-vsphere' }),
).toBe(false);
expect(isCanonicalDatastoreStorageResource('', { platform: 'vmware-vsphere' })).toBe(false);
});
// ---- Branch: `context?.platform || storageMeta?.platform` fallback ----
it('uses context.platform when set, falling back to storageMeta.platform only when context is absent', () => {
// context.platform truthy + non-vmware wins; storageMeta.platform ignored.
expect(
isCanonicalDatastoreStorageResource(
'vmfs',
{ platform: 'vmware-vsphere' },
{ platform: 'proxmox-pve' },
),
).toBe(false);
// context.platform undefined → fallback to storageMeta.platform.
expect(
isCanonicalDatastoreStorageResource('vmfs', { platform: 'vmware-vsphere' }, {}),
).toBe(true);
});
// ---- Branch: fall-through → false ------------------------------------
it('returns false when nothing matches', () => {
expect(isCanonicalDatastoreStorageResource('mystery')).toBe(false);
expect(isCanonicalDatastoreStorageResource(undefined)).toBe(false);
expect(isCanonicalDatastoreStorageResource('mystery', undefined, {})).toBe(false);
});
});
// ===========================================================================
// getStorageCategoryFromType
// ===========================================================================
describe('getStorageCategoryFromType', () => {
// ---- Branch: `!value` → 'other' --------------------------------------
it('returns "other" for empty or undefined type', () => {
expect(getStorageCategoryFromType(undefined)).toBe('other');
expect(getStorageCategoryFromType('')).toBe('other');
});
// ---- Branch: isBackupRepositoryStorageResource(type, undefined, ctx) --
it('returns "backup-repository" via any backup signal in context', () => {
// value.includes('pbs')
expect(getStorageCategoryFromType('pbs')).toBe('backup-repository');
// context.resourceType === 'pbs'
expect(getStorageCategoryFromType('mystery', { resourceType: 'pbs' })).toBe('backup-repository');
// context.platform includes 'pbs'
expect(getStorageCategoryFromType('mystery', { platform: 'proxmox-pbs' })).toBe(
'backup-repository',
);
// context.topology === 'backup-target'
expect(getStorageCategoryFromType('mystery', { topology: 'backup-target' })).toBe(
'backup-repository',
);
});
// ---- Branch: isCanonicalDatastoreStorageResource(type, undefined, ctx)
it('returns "datastore" via each canonical signal in context', () => {
expect(getStorageCategoryFromType('mystery', { resourceType: 'datastore' })).toBe('datastore');
expect(getStorageCategoryFromType('mystery', { topology: 'datastore' })).toBe('datastore');
expect(getStorageCategoryFromType('mystery', { entityType: 'datastore' })).toBe('datastore');
// platform.includes('vmware') && value.length > 0
expect(getStorageCategoryFromType('vmfs', { platform: 'vmware-vsphere' })).toBe('datastore');
});
// ---- Branch: pool substring detection (zfs | lvm | ceph | pool) ------
it('returns "pool" for each pool-indicating substring', () => {
expect(getStorageCategoryFromType('myzfs')).toBe('pool');
expect(getStorageCategoryFromType('mylvm')).toBe('pool');
expect(getStorageCategoryFromType('myceph')).toBe('pool');
expect(getStorageCategoryFromType('mypool')).toBe('pool');
});
// ---- Branch: dataset / share / filesystem substrings -----------------
it('returns the right category for dataset, share, and filesystem substrings', () => {
expect(getStorageCategoryFromType('mydataset')).toBe('dataset');
expect(getStorageCategoryFromType('mynfs')).toBe('share');
expect(getStorageCategoryFromType('mycifs')).toBe('share');
expect(getStorageCategoryFromType('mysmb')).toBe('share');
expect(getStorageCategoryFromType('mydir')).toBe('filesystem');
expect(getStorageCategoryFromType('myfilesystem')).toBe('filesystem');
});
// ---- Branch: ordering — pool beats dataset when both substrings match
it('prefers "pool" over "dataset" when the type contains both substrings', () => {
// 'pooldataset' triggers the pool branch first.
expect(getStorageCategoryFromType('pooldataset')).toBe('pool');
});
// ---- Branch: fallback → 'other' --------------------------------------
it('returns "other" for an unrecognized type with no matching context', () => {
expect(getStorageCategoryFromType('mystery')).toBe('other');
expect(getStorageCategoryFromType('mystery', {} as StorageClassificationContext)).toBe('other');
});
});
// ===========================================================================
// getStorageCapabilitiesForResource
// ===========================================================================
describe('getStorageCapabilitiesForResource', () => {
// ---- Branch: base caps always present --------------------------------
it('returns only the base caps when no signal matches', () => {
expect(getStorageCapabilitiesForResource('mystery')).toEqual(['capacity', 'health']);
expect(getStorageCapabilitiesForResource(undefined)).toEqual(['capacity', 'health']);
});
// ---- Branch: isBackupRepositoryStorageResource(...) === true ----------
it('adds backup-repository caps for a PBS-classified resource', () => {
expect(getStorageCapabilitiesForResource('pbs')).toEqual([
'capacity',
'health',
'backup-repository',
'deduplication',
'namespaces',
]);
// Same caps via context signal rather than the type substring.
expect(getStorageCapabilitiesForResource('mystery', undefined, { resourceType: 'pbs' })).toEqual([
'capacity',
'health',
'backup-repository',
'deduplication',
'namespaces',
]);
});
// ---- Branch: `storageMeta?.isZfs || value.includes('zfs')` ------------
it('adds snapshots + compression when isZfs flag is set OR the type contains zfs', () => {
// value.includes('zfs') arm
expect(getStorageCapabilitiesForResource('zfsmirror')).toEqual([
'capacity',
'health',
'snapshots',
'compression',
]);
// storageMeta.isZfs arm
expect(getStorageCapabilitiesForResource('mystery', { isZfs: true })).toEqual([
'capacity',
'health',
'snapshots',
'compression',
]);
});
// ---- Branch: `storageMeta?.isCeph || value.includes('ceph')` ----------
it('adds replication + multi-node when isCeph flag is set OR the type contains ceph', () => {
// value.includes('ceph') arm
expect(getStorageCapabilitiesForResource('cephrbd')).toEqual([
'capacity',
'health',
'replication',
'multi-node',
]);
// storageMeta.isCeph arm
expect(getStorageCapabilitiesForResource('mystery', { isCeph: true })).toEqual([
'capacity',
'health',
'replication',
'multi-node',
]);
});
// ---- Branch: `(context?.shared ?? storageMeta?.shared) === true` ------
it('adds multi-node only when shared resolves to true via context or storageMeta', () => {
// context.shared true
expect(getStorageCapabilitiesForResource('mystery', undefined, { shared: true })).toEqual([
'capacity',
'health',
'multi-node',
]);
// context.shared undefined → ?? fallback to storageMeta.shared true
expect(getStorageCapabilitiesForResource('mystery', { shared: true }, {})).toEqual([
'capacity',
'health',
'multi-node',
]);
// storageMeta only
expect(getStorageCapabilitiesForResource('mystery', { shared: true })).toEqual([
'capacity',
'health',
'multi-node',
]);
// context.shared === false is NOT nullish, so ?? does NOT fall back to
// storageMeta.shared — multi-node must NOT be added.
expect(
getStorageCapabilitiesForResource('mystery', { shared: true }, { shared: false }),
).toEqual(['capacity', 'health']);
// shared === false explicitly → no multi-node
expect(getStorageCapabilitiesForResource('mystery', undefined, { shared: false })).toEqual([
'capacity',
'health',
]);
});
// ---- Branch: dedupe collapses duplicate multi-node -------------------
it('deduplicates multi-node when both ceph and shared branches add it', () => {
const caps = getStorageCapabilitiesForResource('ceph', undefined, { shared: true });
// ceph branch pushes 'multi-node'; shared branch pushes it again; the
// Set-based dedupe must leave exactly one occurrence.
expect(caps).toEqual(['capacity', 'health', 'replication', 'multi-node']);
expect(caps.filter((c) => c === 'multi-node')).toHaveLength(1);
});
it('combines backup-repository caps with shared multi-node without duplicates', () => {
expect(getStorageCapabilitiesForResource('pbs', undefined, { shared: true })).toEqual([
'capacity',
'health',
'backup-repository',
'deduplication',
'namespaces',
'multi-node',
]);
});
});
@@ -0,0 +1,510 @@
import { describe, expect, it } from 'vitest';
import type { StorageRecord } from '@/features/storageBackups/models';
import {
getCompactStoragePoolImpactLabel,
getCompactStoragePoolIssueLabel,
getCompactStoragePoolIssueSummary,
getStoragePoolIssueTextClass,
getStoragePoolProtectionTextClass,
getStoragePoolStateLabel,
getStoragePoolStateTextClass,
getStoragePoolStateTitle,
} from '@/features/storageBackups/rowPresentation';
const baseRecord = (): StorageRecord =>
({
id: 'storage-1',
name: 'tank',
source: {
platform: 'truenas',
type: 'storage',
label: 'TrueNAS',
},
category: 'pool',
statusLabel: 'Healthy',
health: 'healthy',
capacity: { totalBytes: 0, usedBytes: 0, freeBytes: 0 },
location: { label: 'tower', scope: 'node' },
capabilities: ['capacity'],
observedAt: Date.now(),
refs: {},
}) as unknown as StorageRecord;
// ---------------------------------------------------------------------------
// getStoragePoolIssueTextClass
// ---------------------------------------------------------------------------
describe('getStoragePoolIssueTextClass branch coverage', () => {
it('returns red tone when incidentSeverity is "critical"', () => {
const record = { ...baseRecord(), incidentSeverity: 'critical' };
expect(getStoragePoolIssueTextClass(record)).toBe('text-red-700 dark:text-red-300');
});
it('returns red tone when incidentSeverity is "offline"', () => {
const record = { ...baseRecord(), incidentSeverity: 'offline' };
expect(getStoragePoolIssueTextClass(record)).toBe('text-red-700 dark:text-red-300');
});
it('falls back to record.health when incidentSeverity is absent and lowercases it', () => {
// " WARNING " exercises both the `record.health` arm of the `||` chain and trim()/toLowerCase().
const record = {
...baseRecord(),
incidentSeverity: undefined,
health: ' WARNING ',
} as unknown as StorageRecord;
expect(getStoragePoolIssueTextClass(record)).toBe('text-amber-700 dark:text-amber-300');
});
it('falls back to the empty-string default when both incidentSeverity and health are falsy', () => {
// Drives the `|| ''` tail of the `||` chain so severity becomes ''.
const record = {
...baseRecord(),
incidentSeverity: undefined,
health: '' as unknown as StorageRecord['health'],
} as unknown as StorageRecord;
expect(getStoragePoolIssueTextClass(record)).toBe('text-base-content');
});
it('returns base-content for an unmatched severity like "unknown"', () => {
const record = { ...baseRecord(), incidentSeverity: 'info' };
expect(getStoragePoolIssueTextClass(record)).toBe('text-base-content');
});
});
// ---------------------------------------------------------------------------
// getCompactStoragePoolIssueSummary
// ---------------------------------------------------------------------------
describe('getCompactStoragePoolIssueSummary branch coverage', () => {
it('returns "" when the issue label resolves to "—"', () => {
// Default healthy record: getCompactStoragePoolIssueLabel -> '—'.
expect(getCompactStoragePoolIssueSummary(baseRecord())).toBe('');
});
it('returns the canonical issue summary when it is present and not "healthy"', () => {
const record = {
...baseRecord(),
issueLabel: 'Degraded',
issueSummary: 'Pool is degraded',
};
expect(getCompactStoragePoolIssueSummary(record)).toBe('Pool is degraded');
});
it('falls through a "healthy" summary to the zfs read-errors branch', () => {
const record = {
...baseRecord(),
issueLabel: 'Healthy',
issueSummary: 'Healthy',
details: {
zfsPool: {
state: 'DEGRADED',
devices: [],
readErrors: 3,
writeErrors: 0,
checksumErrors: 0,
},
},
} as unknown as StorageRecord;
expect(getCompactStoragePoolIssueSummary(record)).toBe('3 read errors');
});
it('formats only write errors when read/checksum are zero', () => {
const record = {
...baseRecord(),
issueLabel: 'Healthy',
issueSummary: ' ',
details: {
zfsPool: {
state: 'DEGRADED',
devices: [],
readErrors: 0,
writeErrors: 4,
checksumErrors: 0,
},
},
} as unknown as StorageRecord;
expect(getCompactStoragePoolIssueSummary(record)).toBe('4 write errors');
});
it('combines all three error kinds in the documented order', () => {
const record = {
...baseRecord(),
issueLabel: 'Healthy',
issueSummary: '',
details: {
zfsPool: {
state: 'FAULTED',
devices: [],
readErrors: 1,
writeErrors: 2,
checksumErrors: 3,
},
},
} as unknown as StorageRecord;
expect(getCompactStoragePoolIssueSummary(record)).toBe('1 read, 2 write, 3 checksum errors');
});
it('returns "" when the pool is absent and the issue label is derived from a non-safe status', () => {
// getCompactStoragePoolIssueLabel: empty issueLabel, no pool, statusLabel 'Failed' -> 'Failed' (not '—').
// Then summary: issueSummary empty -> pool null -> ''.
const record = {
...baseRecord(),
statusLabel: 'Failed',
} as unknown as StorageRecord;
expect(getCompactStoragePoolIssueSummary(record)).toBe('');
});
it('returns "" when the pool exists but has no errors and the summary is "healthy"', () => {
const record = {
...baseRecord(),
issueLabel: 'Healthy',
issueSummary: 'Healthy',
details: {
zfsPool: {
state: 'DEGRADED',
devices: [],
readErrors: 0,
writeErrors: 0,
checksumErrors: 0,
},
},
} as unknown as StorageRecord;
expect(getCompactStoragePoolIssueSummary(record)).toBe('');
});
});
// ---------------------------------------------------------------------------
// getStoragePoolProtectionTextClass
// ---------------------------------------------------------------------------
describe('getStoragePoolProtectionTextClass branch coverage', () => {
it('returns the recoverability red tone via incidentCategory alone', () => {
const record = {
...baseRecord(),
incidentCategory: 'recoverability',
};
expect(getStoragePoolProtectionTextClass(record)).toBe('text-red-700 dark:text-red-300');
});
it('returns base-content for a healthy, fully-protected pool', () => {
const record = {
...baseRecord(),
protectionLabel: 'Healthy',
};
expect(getStoragePoolProtectionTextClass(record)).toBe('text-base-content');
});
it('returns the rebuild blue tone regardless of protection label', () => {
const record = {
...baseRecord(),
rebuildInProgress: true,
protectionLabel: 'No parity',
};
expect(getStoragePoolProtectionTextClass(record)).toBe('text-blue-700 dark:text-blue-300');
});
it('treats "no parity" as factual (base-content) even when protectionReduced is set', () => {
const record = {
...baseRecord(),
protectionLabel: 'No parity',
protectionReduced: true,
};
expect(getStoragePoolProtectionTextClass(record)).toBe('text-base-content');
});
});
// ---------------------------------------------------------------------------
// getStoragePoolStateLabel
// ---------------------------------------------------------------------------
describe('getStoragePoolStateLabel branch coverage', () => {
it('titleizes a present details.arrayState', () => {
const record = {
...baseRecord(),
details: { arrayState: 'STARTED' },
} as unknown as StorageRecord;
expect(getStoragePoolStateLabel(record)).toBe('Started');
});
it('returns "Online" when only the zfs pool state is "ONLINE"', () => {
const record = {
...baseRecord(),
details: {
zfsPool: {
state: 'ONLINE',
devices: [],
},
},
} as unknown as StorageRecord;
expect(getStoragePoolStateLabel(record)).toBe('Online');
});
it('returns the raw pool state when it is anything other than "ONLINE"', () => {
const record = {
...baseRecord(),
details: {
zfsPool: {
state: 'FAULTED',
devices: [],
},
},
} as unknown as StorageRecord;
expect(getStoragePoolStateLabel(record)).toBe('FAULTED');
});
it('titleizes the derived status when arrayState and pool are both absent', () => {
// No details.status; health 'critical' -> getStorageRecordStatus -> 'critical' -> 'Critical'.
const record = {
...baseRecord(),
health: 'critical',
} as unknown as StorageRecord;
expect(getStoragePoolStateLabel(record)).toBe('Critical');
});
it('reads details.status (titleized) when arrayState and pool are absent', () => {
const record = {
...baseRecord(),
details: { status: 'available' },
} as unknown as StorageRecord;
expect(getStoragePoolStateLabel(record)).toBe('Available');
});
it('exercises the `details || {}` defensive arm when details is undefined', () => {
// With no details at all, getRecordDetails returns {} via the `|| {}` branch;
// arrayState -> '' -> pool none -> status from health 'warning' -> 'Degraded'.
const record = {
...baseRecord(),
details: undefined,
health: 'warning',
} as unknown as StorageRecord;
expect(getStoragePoolStateLabel(record)).toBe('Degraded');
});
});
// ---------------------------------------------------------------------------
// getCompactStoragePoolIssueLabel
// ---------------------------------------------------------------------------
describe('getCompactStoragePoolIssueLabel branch coverage', () => {
it('collapses to "—" when the issue label matches the derived protection label', () => {
const record = {
...baseRecord(),
issueLabel: 'No parity',
protectionLabel: 'No parity',
protectionReduced: true,
};
expect(getCompactStoragePoolIssueLabel(record)).toBe('—');
});
it('returns the raw issue label when it differs from the protection label', () => {
const record = {
...baseRecord(),
issueLabel: 'Scrub failed',
protectionLabel: 'Healthy',
};
expect(getCompactStoragePoolIssueLabel(record)).toBe('Scrub failed');
});
it('falls back to the zfs pool state when the issue label is empty and pool is not ONLINE', () => {
const record = {
...baseRecord(),
details: {
zfsPool: {
state: 'DEGRADED',
devices: [],
},
},
} as unknown as StorageRecord;
expect(getCompactStoragePoolIssueLabel(record)).toBe('DEGRADED');
});
it('skips an ONLINE zfs pool and falls back to a non-safe statusLabel', () => {
const record = {
...baseRecord(),
details: {
zfsPool: {
state: 'ONLINE',
devices: [],
},
},
statusLabel: 'Faulted',
} as unknown as StorageRecord;
expect(getCompactStoragePoolIssueLabel(record)).toBe('Faulted');
});
it('returns "—" when issue label, non-ONLINE pool, and a safe status are all absent', () => {
const record = {
...baseRecord(),
statusLabel: 'online',
} as unknown as StorageRecord;
expect(getCompactStoragePoolIssueLabel(record)).toBe('—');
});
it('returns "—" for a default healthy record with no issue signal', () => {
expect(getCompactStoragePoolIssueLabel(baseRecord())).toBe('—');
});
});
// ---------------------------------------------------------------------------
// getStoragePoolStateTextClass
// ---------------------------------------------------------------------------
describe('getStoragePoolStateTextClass branch coverage', () => {
it('maps a "critical" state label to the red tone', () => {
const record = {
...baseRecord(),
details: { arrayState: 'CRITICAL' },
} as unknown as StorageRecord;
expect(getStoragePoolStateTextClass(record)).toBe('text-red-700 dark:text-red-300');
});
it('maps a raw "FAULTED" pool state to the red tone', () => {
const record = {
...baseRecord(),
details: {
zfsPool: {
state: 'FAULTED',
devices: [],
},
},
} as unknown as StorageRecord;
expect(getStoragePoolStateTextClass(record)).toBe('text-red-700 dark:text-red-300');
});
it('maps an "offline" derived status to the red tone', () => {
const record = {
...baseRecord(),
health: 'offline',
} as unknown as StorageRecord;
expect(getStoragePoolStateTextClass(record)).toBe('text-red-700 dark:text-red-300');
});
it('maps a "Warning" arrayState to the amber tone', () => {
const record = {
...baseRecord(),
details: { arrayState: 'WARNING' },
} as unknown as StorageRecord;
expect(getStoragePoolStateTextClass(record)).toBe('text-amber-700 dark:text-amber-300');
});
it('maps a "Warn" arrayState (the warn alias) to the amber tone', () => {
const record = {
...baseRecord(),
details: { arrayState: 'WARN' },
} as unknown as StorageRecord;
expect(getStoragePoolStateTextClass(record)).toBe('text-amber-700 dark:text-amber-300');
});
it('maps a "Degraded" derived status (health warning) to the amber tone', () => {
const record = {
...baseRecord(),
health: 'warning',
} as unknown as StorageRecord;
expect(getStoragePoolStateTextClass(record)).toBe('text-amber-700 dark:text-amber-300');
});
it('keeps a healthy/online state at base-content', () => {
const record = {
...baseRecord(),
details: { arrayState: 'STARTED' },
} as unknown as StorageRecord;
expect(getStoragePoolStateTextClass(record)).toBe('text-base-content');
});
});
// ---------------------------------------------------------------------------
// getStoragePoolStateTitle
// ---------------------------------------------------------------------------
describe('getStoragePoolStateTitle branch coverage', () => {
it('prefers a non-healthy summary when the label is not "Started"', () => {
const record = {
...baseRecord(),
issueLabel: 'Degraded',
issueSummary: 'One disk is offline',
details: { arrayState: 'DEGRADED' },
} as unknown as StorageRecord;
expect(getStoragePoolStateTitle(record)).toBe('One disk is offline');
});
it('returns the label when the summary equals the label "Started" (suppresses summary)', () => {
const record = {
...baseRecord(),
issueLabel: 'Degraded',
issueSummary: 'Some warning',
details: { arrayState: 'STARTED' },
} as unknown as StorageRecord;
expect(getStoragePoolStateTitle(record)).toBe('Started');
});
it('falls back to record.issueSummary when getCompactStoragePoolIssueSummary is empty', () => {
// issueLabel 'Healthy' -> getCompactStoragePoolIssueSummary returns '' (label is '—' upstream).
// But issueSummary is non-healthy, so the `|| getStorageRecordIssueSummary` arm feeds the summary.
const record = {
...baseRecord(),
issueLabel: 'Healthy',
issueSummary: 'Manual maintenance',
} as unknown as StorageRecord;
expect(getStoragePoolStateTitle(record)).toBe('Manual maintenance');
});
it('returns the resolved label when there is no summary at all', () => {
const record = {
...baseRecord(),
details: { arrayState: 'STARTED' },
} as unknown as StorageRecord;
expect(getStoragePoolStateTitle(record)).toBe('Started');
});
});
// ---------------------------------------------------------------------------
// getCompactStoragePoolImpactLabel
// ---------------------------------------------------------------------------
describe('getCompactStoragePoolImpactLabel branch coverage', () => {
it('returns the impact summary when consumerCount > 0', () => {
const record = {
...baseRecord(),
consumerCount: 2,
impactSummary: '2 consumers',
};
expect(getCompactStoragePoolImpactLabel(record)).toBe('2 consumers');
});
it('falls back to "—" when protectedWorkloadCount > 0 but impactSummary is blank', () => {
const record = {
...baseRecord(),
protectedWorkloadCount: 1,
impactSummary: ' ',
};
expect(getCompactStoragePoolImpactLabel(record)).toBe('—');
});
it('returns the impact summary when affectedDatastoreCount > 0', () => {
const record = {
...baseRecord(),
affectedDatastoreCount: 3,
impactSummary: '3 datastores affected',
};
expect(getCompactStoragePoolImpactLabel(record)).toBe('3 datastores affected');
});
it('returns "—" when all three counts are zero', () => {
const record = {
...baseRecord(),
consumerCount: 0,
protectedWorkloadCount: 0,
affectedDatastoreCount: 0,
impactSummary: 'ignored',
};
expect(getCompactStoragePoolImpactLabel(record)).toBe('—');
});
it('returns "—" when all counts are absent and impactSummary is set', () => {
const record = {
...baseRecord(),
impactSummary: 'No dependent resources',
};
expect(getCompactStoragePoolImpactLabel(record)).toBe('—');
});
});
@@ -0,0 +1,328 @@
import { describe, expect, it } from 'vitest';
import {
formatActionApprovalPolicyLabel,
formatActionCapabilityLabel,
getActionAuditRecordStatePresentation,
getActionAuditRefusalPresentation,
getActionAuditResultPresentation,
getActionAuditStatePresentation,
getActionAuditVerification,
getActionAuditVerificationOutcomePresentation,
shouldRenderActionAuditVerification,
} from '@/utils/actionAuditPresentation';
// `getActionAuditResultMessage` is module-private (not exported); its branches
// are exercised transitively through the failure path of
// `getActionAuditResultPresentation`, which is the only caller.
describe('actionAuditPresentation branch coverage (supplemental)', () => {
describe('getActionAuditStatePresentation', () => {
it('resolves every canonical lifecycle state that the sibling test omits', () => {
expect(getActionAuditStatePresentation('planned')).toStrictEqual({
label: 'Planned',
className: 'bg-surface-alt text-muted border-border',
});
expect(getActionAuditStatePresentation('approved')).toStrictEqual({
label: 'Approved',
className:
'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900 dark:text-blue-200 dark:border-blue-700',
});
expect(getActionAuditStatePresentation('rejected')).toStrictEqual({
label: 'Rejected',
className:
'bg-red-100 text-red-800 border-red-200 dark:bg-red-900 dark:text-red-200 dark:border-red-700',
});
expect(getActionAuditStatePresentation('executing')).toStrictEqual({
label: 'Executing',
className:
'bg-sky-100 text-sky-800 border-sky-200 dark:bg-sky-900 dark:text-sky-200 dark:border-sky-700',
});
});
it('falls back to the Unknown badge for undefined and unrecognised states (?? arm)', () => {
const unknown = {
label: 'Unknown',
className: 'bg-surface-alt text-muted border-border',
};
expect(getActionAuditStatePresentation(undefined)).toStrictEqual(unknown);
expect(getActionAuditStatePresentation('frobnicated')).toStrictEqual(unknown);
});
});
describe('formatActionCapabilityLabel', () => {
it('returns the bare "Action" fallback for nullish and whitespace-only input', () => {
// `(capabilityName || '')` short-circuit on undefined, then trim -> empty -> early return.
expect(formatActionCapabilityLabel(undefined)).toBe('Action');
expect(formatActionCapabilityLabel(' ')).toBe('Action');
});
it('title-cases a single lowercase word without touching separators', () => {
expect(formatActionCapabilityLabel('reboot')).toBe('Reboot');
});
it('splits on hyphen runs the same way as underscores and dots', () => {
// Exercises the `/[._-]+/g` replace arm for the hyphen class.
expect(formatActionCapabilityLabel('restart-service')).toBe('Restart Service');
});
it('collapses consecutive separators and trims surrounding whitespace', () => {
// `a__b` -> replace -> `a b` -> split(/\s+/) -> ['a','b'].
expect(formatActionCapabilityLabel(' a__b ')).toBe('A B');
});
it('only re-cases the first character of each word, leaving the tail verbatim', () => {
// Documents the behaviour: slice(0,1).toUpperCase() + slice(1) preserves the
// rest verbatim, so 'updateContainer' becomes 'UpdateContainer' (camelCase
// tail is kept, not lowercased).
expect(formatActionCapabilityLabel('docker.updateContainer')).toBe(
'Docker UpdateContainer',
);
});
});
describe('formatActionApprovalPolicyLabel', () => {
it('maps the "none" policy arm the sibling test skips', () => {
expect(formatActionApprovalPolicyLabel('none')).toBe('No approval');
});
it('trims before matching, so a padded canonical value still hits its case', () => {
expect(formatActionApprovalPolicyLabel(' none ')).toBe('No approval');
});
it('routes nullish/empty policy through the default arm onto "Policy"', () => {
// default -> formatActionCapabilityLabel(policy || 'Policy').
expect(formatActionApprovalPolicyLabel(undefined)).toBe('Policy');
expect(formatActionApprovalPolicyLabel('')).toBe('Policy');
});
it('title-cases an unknown policy name via the default arm', () => {
expect(formatActionApprovalPolicyLabel('break_glass')).toBe('Break Glass');
expect(formatActionApprovalPolicyLabel('custom-policy')).toBe('Custom Policy');
});
});
describe('getActionAuditResultMessage (via getActionAuditResultPresentation failure path)', () => {
it('uses result.output when errorMessage is absent (|| arm)', () => {
// Private helper branch: `result?.errorMessage || result?.output`.
const presentation = getActionAuditResultPresentation({
result: { success: false, output: 'stderr dump from executor' },
});
expect(presentation).toStrictEqual({
kind: 'failure',
label: 'Execution failed',
detail: 'stderr dump from executor',
className:
'border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950/40 dark:text-red-300',
});
});
it('returns an empty message when neither errorMessage nor output is set', () => {
// Final `|| ''` fallback inside the helper, then `|| undefined` on detail.
const presentation = getActionAuditResultPresentation({
result: { success: false },
});
expect(presentation).toStrictEqual({
kind: 'failure',
label: 'Execution failed',
detail: undefined,
className:
'border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950/40 dark:text-red-300',
});
});
});
describe('getActionAuditRefusalPresentation', () => {
it('returns undefined when there is no result at all (!audit.result arm)', () => {
expect(getActionAuditRefusalPresentation({})).toBeUndefined();
});
it('returns undefined when the result was successful (audit.result.success arm)', () => {
expect(
getActionAuditRefusalPresentation({ result: { success: true, output: 'ok' } }),
).toBeUndefined();
});
it('matches a refusal prefix case-insensitively via the toLowerCase() normaliser', () => {
// normalizedMessage = message.toLowerCase(); 'PLAN_DRIFT:' lowercases to the prefix.
const presentation = getActionAuditRefusalPresentation({
result: { success: false, errorMessage: 'PLAN_DRIFT: policy version changed' },
});
expect(presentation).toStrictEqual({
prefix: 'plan_drift:',
label: 'Plan changed',
detail:
'Pulse refused the action before dispatch because the approved plan no longer matched the current resource or policy state.',
recordedDetail: 'policy version changed',
className:
'border-rose-200 bg-rose-50 text-rose-800 dark:border-rose-800 dark:bg-rose-950/40 dark:text-rose-300',
});
});
it('drops recordedDetail when the message has no detail after the prefix', () => {
// `recordedDetail || undefined` falsy arm.
const presentation = getActionAuditRefusalPresentation({
result: { success: false, errorMessage: 'plan_drift:' },
});
expect(presentation?.recordedDetail).toBeUndefined();
expect(presentation?.label).toBe('Plan changed');
});
it('returns undefined when the failure message carries no recognised prefix', () => {
// ACTION_REFUSAL_PREFIXES.find() returns undefined -> second early return.
expect(
getActionAuditRefusalPresentation({
result: { success: false, errorMessage: 'totally unknown failure mode' },
}),
).toBeUndefined();
});
});
describe('getActionAuditRecordStatePresentation', () => {
it('keeps the generic Failed badge when a failed result is not a refusal', () => {
// state === 'failed' && refusal -> falsey refusal -> falls through to state lookup.
expect(
getActionAuditRecordStatePresentation({
state: 'failed',
result: { success: false, errorMessage: 'executor exited with code 1' },
}),
).toStrictEqual({
label: 'Failed',
className:
'bg-red-100 text-red-800 border-red-200 dark:bg-red-900 dark:text-red-200 dark:border-red-700',
});
});
it('delegates to the state lookup for non-failed states', () => {
// state !== 'failed' -> skips the refusal check entirely.
expect(
getActionAuditRecordStatePresentation({ state: 'executing' }),
).toStrictEqual({
label: 'Executing',
className:
'bg-sky-100 text-sky-800 border-sky-200 dark:bg-sky-900 dark:text-sky-200 dark:border-sky-700',
});
});
});
describe('getActionAuditResultPresentation', () => {
it('returns undefined when the record has no result (!result arm)', () => {
expect(getActionAuditResultPresentation({})).toBeUndefined();
});
it('renders the success branch with a trimmed output detail', () => {
const presentation = getActionAuditResultPresentation({
result: { success: true, output: ' reloaded ' },
});
expect(presentation).toStrictEqual({
kind: 'success',
label: 'Result',
detail: 'reloaded',
className: 'border-border bg-surface text-base-content',
});
});
it('omits detail on the success branch when output is missing or blank', () => {
// `result.output?.trim() || undefined` -> undefined for absent and whitespace-only.
expect(
getActionAuditResultPresentation({ result: { success: true } })?.detail,
).toBeUndefined();
expect(
getActionAuditResultPresentation({ result: { success: true, output: ' ' } })
?.detail,
).toBeUndefined();
});
});
describe('getActionAuditVerificationOutcomePresentation', () => {
it('returns undefined when there is no verification outcome (!status arm)', () => {
// outcome undefined -> status '' -> early return.
expect(getActionAuditVerificationOutcomePresentation({})).toBeUndefined();
expect(
getActionAuditVerificationOutcomePresentation({
verificationOutcome: { status: '' },
}),
).toBeUndefined();
// whitespace-only status trims to '' and also returns undefined.
expect(
getActionAuditVerificationOutcomePresentation({
verificationOutcome: { status: ' ' },
}),
).toBeUndefined();
});
it('normalises uppercase status to the canonical map key via toLowerCase()', () => {
expect(
getActionAuditVerificationOutcomePresentation({
verificationOutcome: { status: 'VERIFIED' },
}),
).toStrictEqual({
label: 'Verification confirmed',
detail: 'Pulse confirmed the intended state after execution.',
evidenceSummary: undefined,
className:
'border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300',
});
});
it('collapses a whitespace-only evidenceSummary to undefined', () => {
// `evidenceSummary?.trim()` -> '' -> `|| undefined`.
const presentation = getActionAuditVerificationOutcomePresentation({
verificationOutcome: { status: 'failed', evidenceSummary: ' ' },
});
expect(presentation?.evidenceSummary).toBeUndefined();
expect(presentation?.label).toBe('Verification failed');
});
});
describe('getActionAuditVerification', () => {
it('returns undefined when neither top-level nor result verification exists', () => {
// `audit.verification ?? audit.result?.verification` -> undefined ?? undefined.
expect(getActionAuditVerification({})).toBeUndefined();
expect(getActionAuditVerification({ result: { success: true } })).toBeUndefined();
});
it('prefers the top-level verification over the result-embedded one (?? left operand)', () => {
const top = { ran: true, success: true, command: 'top-level' };
const result = { ran: false, success: false, command: 'embedded' };
expect(
getActionAuditVerification({
verification: top,
result: { success: true, verification: result },
}),
).toStrictEqual(top);
});
});
describe('shouldRenderActionAuditVerification', () => {
it('returns false when no verification object exists at all', () => {
// `undefined?.ran === true` -> false.
expect(shouldRenderActionAuditVerification({})).toBe(false);
});
it('returns true from the result-embedded fallback verification', () => {
// Exercises the ?? right operand of getActionAuditVerification plus the true arm.
expect(
shouldRenderActionAuditVerification({
result: { success: true, verification: { ran: true, success: true } },
}),
).toBe(true);
});
it('uses strict equality against the boolean true (truthy non-boolean does not count)', () => {
// `ran` is typed boolean; we deliberately coerce a non-boolean to prove the
// `=== true` strictness rather than a truthiness check.
expect(
shouldRenderActionAuditVerification({
result: {
success: true,
verification: {
ran: 1 as unknown as boolean,
success: true,
},
},
}),
).toBe(false);
});
});
});
@@ -0,0 +1,585 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { Resource, ResourceAvailabilityMeta } from '@/types/resource';
import {
getAvailabilityProbeMethodLabel,
getAvailabilityProbePresentation,
getAvailabilityProbeTargetLabel,
} from '@/utils/availabilityProbePresentation';
// `normalizeAvailabilityProtocol`, `getAvailabilityProbeResultLabel`,
// `getAvailabilityProbeToneClassName`, and `getFailureCountLabel` are module-private
// (non-exported) helpers, so they are exercised indirectly through the three exported
// entry points below, asserting on their observable outputs.
const makeAvailability = (
overrides?: Partial<ResourceAvailabilityMeta>,
): ResourceAvailabilityMeta => ({
protocol: 'tcp',
port: 443,
available: true,
latencyMillis: 5,
lastChecked: '2026-05-06T13:00:00Z',
...overrides,
});
const makeResource = (overrides?: Partial<Resource>): Resource => ({
id: 'availability:probe-1',
type: 'network-endpoint',
name: 'probe-target',
displayName: 'probe-target',
platformId: 'probe-1',
platformType: 'availability',
sourceType: 'api',
status: 'online',
lastSeen: 1,
availability: makeAvailability(),
platformData: { sources: ['availability'] },
...overrides,
});
const AT = new Date('2026-05-06T13:00:20Z').getTime();
describe('normalizeAvailabilityProtocol — branch coverage (via getAvailabilityProbeMethodLabel)', () => {
// The helper is `(protocol ?? '').trim().toLowerCase()`; each case proves a distinct
// input shape flows through the nullish-coalesce, trim, and lowercase steps.
afterEach(() => vi.restoreAllMocks());
it('coerces an undefined protocol to an empty (unknown) protocol', () => {
expect(getAvailabilityProbeMethodLabel({ protocol: undefined })).toBe('Probe');
});
it('coerces a null protocol to an empty (unknown) protocol', () => {
expect(
getAvailabilityProbeMethodLabel({ protocol: null as unknown as string }),
).toBe('Probe');
});
it('trims and lowercases a cased/whitespace-padded protocol before matching', () => {
// ' IcMp ' -> normalize -> 'icmp' -> 'ICMP'
expect(getAvailabilityProbeMethodLabel({ protocol: ' IcMp ' })).toBe('ICMP');
});
it('treats a whitespace-only protocol as empty', () => {
expect(getAvailabilityProbeMethodLabel({ protocol: ' ' })).toBe('Probe');
});
});
describe('getAvailabilityProbeMethodLabel — branch coverage', () => {
it('returns plain "TCP" when the protocol is tcp but no port is set', () => {
expect(getAvailabilityProbeMethodLabel({ protocol: 'tcp' })).toBe('TCP');
});
it('returns "TCP <port>" when tcp and a port is set', () => {
expect(getAvailabilityProbeMethodLabel({ protocol: 'tcp', port: 22 })).toBe('TCP 22');
});
it('returns bare "HTTP" when http has no path', () => {
expect(getAvailabilityProbeMethodLabel({ protocol: 'http' })).toBe('HTTP');
});
it('returns bare "HTTPS" when https has no path', () => {
expect(getAvailabilityProbeMethodLabel({ protocol: 'https' })).toBe('HTTPS');
});
it('returns "HTTPS <path>" when https has a path', () => {
expect(getAvailabilityProbeMethodLabel({ protocol: 'https', path: '/healthz' })).toBe(
'HTTPS /healthz',
);
});
it('uppercases an unrecognized-but-non-empty protocol', () => {
expect(getAvailabilityProbeMethodLabel({ protocol: 'snmp' })).toBe('SNMP');
});
it('falls back to "Probe" when the availability object has no protocol', () => {
expect(getAvailabilityProbeMethodLabel({})).toBe('Probe');
});
it('falls back to "Probe" when availability is undefined', () => {
expect(getAvailabilityProbeMethodLabel(undefined)).toBe('Probe');
});
it('falls back to "Probe" when availability is null', () => {
expect(getAvailabilityProbeMethodLabel(null)).toBe('Probe');
});
});
describe('getAvailabilityProbeTargetLabel — branch coverage', () => {
it('returns the port string for tcp with a positive finite port', () => {
expect(getAvailabilityProbeTargetLabel({ protocol: 'tcp', port: 443 })).toBe('443');
});
it('returns null for tcp with port 0 (port > 0 guard)', () => {
expect(getAvailabilityProbeTargetLabel({ protocol: 'tcp', port: 0 })).toBeNull();
});
it('returns null for tcp with a negative port', () => {
expect(getAvailabilityProbeTargetLabel({ protocol: 'tcp', port: -1 })).toBeNull();
});
it('returns null for tcp with a non-finite port (Infinity)', () => {
expect(getAvailabilityProbeTargetLabel({ protocol: 'tcp', port: Infinity })).toBeNull();
});
it('returns null for tcp with a non-finite port (NaN)', () => {
expect(getAvailabilityProbeTargetLabel({ protocol: 'tcp', port: NaN })).toBeNull();
});
it('returns null for tcp when port is the wrong type (string)', () => {
expect(
getAvailabilityProbeTargetLabel({ protocol: 'tcp', port: 'abc' as unknown as number }),
).toBeNull();
});
it('returns null for tcp when no port is set', () => {
expect(getAvailabilityProbeTargetLabel({ protocol: 'tcp' })).toBeNull();
});
it('returns the trimmed path for http with a path', () => {
expect(getAvailabilityProbeTargetLabel({ protocol: 'http', path: '/status' })).toBe(
'/status',
);
});
it('returns the trimmed path for https with a path', () => {
expect(getAvailabilityProbeTargetLabel({ protocol: 'https', path: '/ready' })).toBe(
'/ready',
);
});
it('returns null for http with no path', () => {
expect(getAvailabilityProbeTargetLabel({ protocol: 'http' })).toBeNull();
});
it('returns null for http with a whitespace-only path', () => {
expect(getAvailabilityProbeTargetLabel({ protocol: 'http', path: ' ' })).toBeNull();
});
it('returns null for icmp (non-tcp/http protocol)', () => {
expect(getAvailabilityProbeTargetLabel({ protocol: 'icmp' })).toBeNull();
});
it('returns null for an unrecognized protocol', () => {
expect(getAvailabilityProbeTargetLabel({ protocol: 'snmp' })).toBeNull();
});
it('returns null when availability is undefined', () => {
expect(getAvailabilityProbeTargetLabel(undefined)).toBeNull();
});
});
describe('getAvailabilityProbeResultLabel — branch coverage (via presentation.resultLabel)', () => {
afterEach(() => vi.restoreAllMocks());
it('returns "reachable" when available is true with no usable latency', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'online',
availability: { protocol: 'icmp', available: true, lastChecked: '2026-05-06T13:00:00Z' },
}),
);
expect(presentation?.resultLabel).toBe('reachable');
});
it('returns "not checked" when not failed, no latency, and not reachable', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'unknown',
availability: { protocol: 'icmp', lastChecked: '2026-05-06T13:00:00Z' },
}),
);
expect(presentation?.resultLabel).toBe('not checked');
});
it('returns "failed" on the failure path when lastError has no timeout/HTTP-status token', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'degraded',
availability: {
protocol: 'icmp',
lastChecked: '2026-05-06T13:00:00Z',
lastError: 'route unreachable',
},
}),
);
expect(presentation?.resultLabel).toBe('failed');
});
it('maps a "timeout" token (not "timed out") to "timed out"', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'online',
availability: {
protocol: 'http',
available: false,
lastChecked: '2026-05-06T13:00:00Z',
lastError: 'Connection timeout exceeded',
},
}),
);
expect(presentation?.resultLabel).toBe('timed out');
});
it('extracts a 4xx HTTP status code from lastError', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'online',
availability: {
protocol: 'http',
available: false,
lastChecked: '2026-05-06T13:00:00Z',
lastError: 'probe got 404 Not Found',
},
}),
);
expect(presentation?.resultLabel).toBe('404');
});
it('rounds fractional latency to the nearest millisecond', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'online',
availability: { protocol: 'tcp', port: 443, latencyMillis: 7.4 },
}),
);
expect(presentation?.resultLabel).toBe('7 ms');
});
it('reports "0 ms" at the latency >= 0 boundary', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'online',
availability: { protocol: 'tcp', port: 443, latencyMillis: 0 },
}),
);
expect(presentation?.resultLabel).toBe('0 ms');
});
it('falls through to "reachable" when latency is NaN (non-finite) but available is true', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'online',
availability: { protocol: 'tcp', port: 443, available: true, latencyMillis: NaN },
}),
);
expect(presentation?.resultLabel).toBe('reachable');
});
it('falls through to "reachable" when latency is negative but available is true', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'online',
availability: { protocol: 'tcp', port: 443, available: true, latencyMillis: -5 },
}),
);
expect(presentation?.resultLabel).toBe('reachable');
});
it('falls through to "reachable" when latency is the wrong type but available is true', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'online',
availability: {
protocol: 'tcp',
port: 443,
available: true,
latencyMillis: 'fast' as unknown as number,
},
}),
);
expect(presentation?.resultLabel).toBe('reachable');
});
it('honours the latency path even when status is "online" and available is true', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'online',
availability: makeAvailability({ latencyMillis: 9.5 }),
}),
);
expect(presentation?.resultLabel).toBe('10 ms');
});
});
describe('getAvailabilityProbeToneClassName — branch coverage (via presentation.toneClassName)', () => {
afterEach(() => vi.restoreAllMocks());
it('returns the warning class for a degraded status that is not hard-failed', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'degraded',
availability: { protocol: 'icmp', lastChecked: '2026-05-06T13:00:00Z' },
}),
);
expect(presentation?.toneClassName).toBe('text-amber-600 dark:text-amber-300');
});
it('returns the unknown class when not failed/degraded and not reachable', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'unknown',
availability: { protocol: 'icmp', lastChecked: '2026-05-06T13:00:00Z' },
}),
);
expect(presentation?.toneClassName).toBe('text-muted');
});
it('returns the error class when available is false even if status is online', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'online',
availability: {
protocol: 'http',
available: false,
lastChecked: '2026-05-06T13:00:00Z',
lastError: '503 Service Unavailable',
},
}),
);
expect(presentation?.toneClassName).toBe('text-red-600 dark:text-red-300');
});
it('returns the success class when available is true even if status is not online', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'unknown',
availability: {
protocol: 'tcp',
port: 443,
available: true,
latencyMillis: 5,
lastChecked: '2026-05-06T13:00:00Z',
},
}),
);
expect(presentation?.toneClassName).toBe('text-emerald-600 dark:text-emerald-300');
});
});
describe('getFailureCountLabel — branch coverage (via presentation.detailLabel)', () => {
afterEach(() => vi.restoreAllMocks());
it('renders "1 failure" for a single failure with no threshold', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
availability: makeAvailability({ consecutiveFailures: 1 }),
}),
);
expect(presentation?.detailLabel).toContain('1 failure');
expect(presentation?.detailLabel).not.toContain('1 failures');
expect(presentation?.detailLabel).not.toContain('/');
});
it('renders "N failures" for multiple failures with no threshold', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
availability: makeAvailability({ consecutiveFailures: 2 }),
}),
);
expect(presentation?.detailLabel).toContain('2 failures');
expect(presentation?.detailLabel).not.toContain('/');
});
it('omits any failure count when consecutiveFailures is 0', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
availability: makeAvailability({ consecutiveFailures: 0 }),
}),
);
expect(presentation?.detailLabel).not.toMatch(/failure/);
});
it('omits any failure count when consecutiveFailures is negative', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
availability: makeAvailability({ consecutiveFailures: -3 }),
}),
);
expect(presentation?.detailLabel).not.toMatch(/failure/);
});
it('omits any failure count when consecutiveFailures is NaN', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
availability: makeAvailability({ consecutiveFailures: NaN }),
}),
);
expect(presentation?.detailLabel).not.toMatch(/failure/);
});
it('omits any failure count when consecutiveFailures is the wrong type', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
availability: makeAvailability({
consecutiveFailures: 'bad' as unknown as number,
}),
}),
);
expect(presentation?.detailLabel).not.toMatch(/failure/);
});
it('falls back to "N failures" when failureThreshold is present but invalid (0)', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
availability: makeAvailability({ consecutiveFailures: 3, failureThreshold: 0 }),
}),
);
expect(presentation?.detailLabel).toContain('3 failures');
expect(presentation?.detailLabel).not.toContain('/');
});
it('renders the "N/threshold failures" form when both are positive finite numbers', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
availability: makeAvailability({ consecutiveFailures: 3, failureThreshold: 4 }),
}),
);
expect(presentation?.detailLabel).toContain('3/4 failures');
});
});
describe('getAvailabilityProbePresentation — branch coverage', () => {
afterEach(() => vi.restoreAllMocks());
it('returns null when neither availability nor platformData.availability is set', () => {
expect(
getAvailabilityProbePresentation(
makeResource({ availability: undefined, platformData: { sources: ['availability'] } }),
),
).toBeNull();
});
it('returns null when availability is unset and platformData itself is missing (optional chain)', () => {
expect(
getAvailabilityProbePresentation(
makeResource({ availability: undefined, platformData: undefined }),
),
).toBeNull();
});
it('prefers resource.availability over platformData.availability (?? left operand wins)', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
availability: makeAvailability({ port: 443 }),
platformData: {
sources: ['availability'],
availability: makeAvailability({ port: 8080 }),
},
}),
);
expect(presentation?.methodLabel).toBe('TCP 443');
expect(presentation?.targetLabel).toBe('443');
});
it('builds netIoLabel as "<target>: <result>" when a target exists', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(makeResource());
expect(presentation?.netIoLabel).toBe('443: 5 ms');
});
it('builds netIoLabel from just the result when there is no target (icmp)', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
availability: { protocol: 'icmp', available: true, lastChecked: '2026-05-06T13:00:00Z' },
}),
);
expect(presentation?.targetLabel).toBeNull();
expect(presentation?.netIoLabel).toBe(presentation?.resultLabel);
expect(presentation?.netIoLabel).toBe('reachable');
});
it('drops the "checked ..." segment from both rowLabel and detailLabel when lastChecked is missing', () => {
const presentation = getAvailabilityProbePresentation(
makeResource({
availability: makeAvailability({ lastChecked: undefined }),
}),
);
expect(presentation?.rowLabel).toBe(presentation?.netIoLabel);
expect(presentation?.rowLabel).toBe('443: 5 ms');
expect(presentation?.detailLabel).not.toContain('checked');
});
it('includes a deterministic "checked ..." segment when lastChecked is set', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(makeResource());
expect(presentation?.rowLabel).toBe('443: 5 ms - checked 20s ago');
expect(presentation?.detailLabel).toContain('checked 20s ago');
});
it('appends "last success ..." when available is false and lastSuccess parses to a relative time', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'offline',
availability: {
protocol: 'icmp',
available: false,
lastChecked: '2026-05-06T13:00:00Z',
lastSuccess: '2026-05-06T12:51:20Z',
consecutiveFailures: 1,
},
}),
);
expect(presentation?.detailLabel).toContain('last success 9 mins ago');
});
it('omits "last success ..." when lastSuccess is set but does not parse (sub-guard false)', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'offline',
availability: {
protocol: 'icmp',
available: false,
lastChecked: '2026-05-06T13:00:00Z',
lastSuccess: 'not-a-real-date',
},
}),
);
expect(presentation?.detailLabel).not.toContain('last success');
});
it('omits "last success ..." when lastSuccess is set but available is not false (guard skipped)', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'online',
availability: {
protocol: 'icmp',
available: true,
latencyMillis: 4,
lastChecked: '2026-05-06T13:00:00Z',
lastSuccess: '2026-05-06T12:51:20Z',
},
}),
);
expect(presentation?.detailLabel).not.toContain('last success');
});
it('appends lastError verbatim to detailLabel when present', () => {
vi.spyOn(Date, 'now').mockReturnValue(AT);
const presentation = getAvailabilityProbePresentation(
makeResource({
status: 'offline',
availability: {
protocol: 'icmp',
available: false,
lastChecked: '2026-05-06T13:00:00Z',
lastError: 'dial tcp: i/o timeout',
},
}),
);
expect(presentation?.detailLabel).toContain('dial tcp: i/o timeout');
});
});
@@ -0,0 +1,124 @@
import { describe, expect, it } from 'vitest';
import type { RecoveryOutcome } from '@/types/recovery';
import {
getRecoveryOutcomeBadgeClass,
getRecoveryOutcomeBarClass,
getRecoveryOutcomeLabel,
getRecoveryOutcomeTextClass,
normalizeRecoveryOutcome,
} from '@/utils/recoveryOutcomePresentation';
// A value that is not part of the canonical outcome set; used to drive the
// `default` arms of the switch statements and the fallthrough branch of
// normalizeRecoveryOutcome. Cast through `unknown` to satisfy strict typing.
const NON_CANONICAL = 'partial' as unknown as RecoveryOutcome;
const BADGE_BASE = 'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium';
describe('recoveryOutcomePresentation — branch coverage (branchcov2)', () => {
describe('normalizeRecoveryOutcome', () => {
it('treats null/undefined/empty as falsy and falls back to unknown', () => {
// Exercises the `(value || '')` defensive branch for every falsy input.
expect(normalizeRecoveryOutcome(null)).toBe('unknown');
expect(normalizeRecoveryOutcome(undefined)).toBe('unknown');
expect(normalizeRecoveryOutcome('')).toBe('unknown');
});
it('treats whitespace-only input as unknown after trim', () => {
expect(normalizeRecoveryOutcome(' ')).toBe('unknown');
});
it('matches the exact canonical tokens (case-insensitive)', () => {
// Each of these hits a distinct early-return arm.
expect(normalizeRecoveryOutcome('SUCCESS')).toBe('success');
expect(normalizeRecoveryOutcome('Warning')).toBe('warning');
expect(normalizeRecoveryOutcome('RUNNING')).toBe('running');
expect(normalizeRecoveryOutcome('Unknown')).toBe('unknown');
});
it('maps the "error" alias onto failed', () => {
// "error" is the only failed-alias not exercised by the sibling test.
expect(normalizeRecoveryOutcome('error')).toBe('failed');
expect(normalizeRecoveryOutcome(' ERROR ')).toBe('failed');
});
it('hits the explicit unknown arm distinctly from the default fallthrough', () => {
// 'unknown' takes the explicit `if (normalized === 'unknown')` arm while
// a non-canonical token reaches the final default return.
expect(normalizeRecoveryOutcome('unknown')).toBe('unknown');
expect(normalizeRecoveryOutcome('nope')).toBe('unknown');
});
});
describe('getRecoveryOutcomeLabel', () => {
it('returns a human label for every canonical outcome', () => {
expect(getRecoveryOutcomeLabel('success')).toBe('Healthy');
expect(getRecoveryOutcomeLabel('warning')).toBe('Warning');
expect(getRecoveryOutcomeLabel('failed')).toBe('Failed');
expect(getRecoveryOutcomeLabel('running')).toBe('Running');
expect(getRecoveryOutcomeLabel('unknown')).toBe('Unknown');
});
it('routes non-canonical outcomes through the default arm', () => {
expect(getRecoveryOutcomeLabel(NON_CANONICAL)).toBe('Unknown');
});
});
describe('getRecoveryOutcomeBadgeClass', () => {
it('renders the full badge class for success and warning', () => {
expect(getRecoveryOutcomeBadgeClass('success')).toBe(
`${BADGE_BASE} bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300`,
);
expect(getRecoveryOutcomeBadgeClass('warning')).toBe(
`${BADGE_BASE} bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300`,
);
});
it('renders the full badge class for failed and running (strict equality)', () => {
// Sibling test only asserted substring membership; pin the whole string.
expect(getRecoveryOutcomeBadgeClass('failed')).toBe(
`${BADGE_BASE} bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300`,
);
expect(getRecoveryOutcomeBadgeClass('running')).toBe(
`${BADGE_BASE} bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300`,
);
});
it('falls back to the neutral badge for the default arm', () => {
expect(getRecoveryOutcomeBadgeClass('unknown')).toBe(
`${BADGE_BASE} bg-surface-alt text-muted`,
);
expect(getRecoveryOutcomeBadgeClass(NON_CANONICAL)).toBe(
`${BADGE_BASE} bg-surface-alt text-muted`,
);
});
});
describe('getRecoveryOutcomeBarClass', () => {
it('returns a solid bar color for each canonical outcome', () => {
expect(getRecoveryOutcomeBarClass('success')).toBe('bg-emerald-500');
expect(getRecoveryOutcomeBarClass('warning')).toBe('bg-amber-400');
expect(getRecoveryOutcomeBarClass('failed')).toBe('bg-red-500');
expect(getRecoveryOutcomeBarClass('running')).toBe('bg-blue-500');
});
it('falls back to gray for the default arm', () => {
expect(getRecoveryOutcomeBarClass('unknown')).toBe('bg-gray-400');
expect(getRecoveryOutcomeBarClass(NON_CANONICAL)).toBe('bg-gray-400');
});
});
describe('getRecoveryOutcomeTextClass', () => {
it('returns a dark-mode-aware text tone for each canonical outcome', () => {
expect(getRecoveryOutcomeTextClass('success')).toBe('text-emerald-600 dark:text-emerald-400');
expect(getRecoveryOutcomeTextClass('warning')).toBe('text-amber-600 dark:text-amber-400');
expect(getRecoveryOutcomeTextClass('failed')).toBe('text-red-600 dark:text-red-400');
expect(getRecoveryOutcomeTextClass('running')).toBe('text-blue-600 dark:text-blue-400');
});
it('falls back to text-muted for the default arm', () => {
expect(getRecoveryOutcomeTextClass('unknown')).toBe('text-muted');
expect(getRecoveryOutcomeTextClass(NON_CANONICAL)).toBe('text-muted');
});
});
});
@@ -0,0 +1,314 @@
import { describe, expect, it } from 'vitest';
import {
getRecoveryPointPlatform,
getRecoveryRollupPlatforms,
normalizeRecoveryPoint,
normalizeRecoveryPointsResponse,
normalizeRecoveryRollup,
normalizeRecoveryRollupsResponse,
} from '@/utils/recoveryPlatformModel';
/**
* Branch-coverage companion to recoveryPlatformModel.test.ts.
*
* The non-exported helpers (toTrimmedString, normalizeRecoveryDisplay,
* getRecoveryItemResourceId, getRecoveryItemRef, normalizeRecoveryMeta) are
* exercised indirectly through the exported normalizers, since they are
* module-private. Each test below targets specific arms of the conditionals
* documented in recoveryPlatformModel.ts.
*/
describe('recoveryPlatformModel.branchcov2', () => {
describe('toTrimmedString (via exported wrappers)', () => {
it('trims surrounding whitespace from string inputs', () => {
// Branch: typeof value === 'string' -> value.trim()
expect(getRecoveryPointPlatform({ platform: ' truenas ' })).toBe('truenas');
expect(getRecoveryPointPlatform({ provider: '\tproxmox-pbs\n' })).toBe('proxmox-pbs');
});
it('returns empty string for non-string values', () => {
// Branch: typeof value !== 'string' -> '' (hit via a numeric subject id).
const malformed = {
id: 'p',
kind: 'snapshot',
mode: 'snapshot',
outcome: 'success',
subjectResourceId: 12345,
} as unknown as Parameters<typeof normalizeRecoveryPoint>[0];
const result = normalizeRecoveryPoint(malformed);
expect(result).toStrictEqual({
id: 'p',
kind: 'snapshot',
mode: 'snapshot',
outcome: 'success',
});
expect('itemResourceId' in result).toBe(false);
});
});
describe('getRecoveryPointPlatform', () => {
it('returns empty string when both platform and provider are absent', () => {
expect(getRecoveryPointPlatform(null)).toBe('');
expect(getRecoveryPointPlatform(undefined)).toBe('');
expect(getRecoveryPointPlatform({})).toBe('');
});
});
describe('getRecoveryRollupPlatforms', () => {
it('falls back to providers when platforms is an empty array', () => {
expect(getRecoveryRollupPlatforms({ platforms: [], providers: ['x'] })).toEqual(['x']);
});
it('falls back to providers when platforms is not an array', () => {
const malformed = {
platforms: 'nope',
providers: ['kubernetes'],
} as unknown as Parameters<typeof getRecoveryRollupPlatforms>[0];
expect(getRecoveryRollupPlatforms(malformed)).toEqual(['kubernetes']);
});
it('returns an empty array when both platforms and providers are absent', () => {
expect(getRecoveryRollupPlatforms({})).toEqual([]);
expect(getRecoveryRollupPlatforms(null)).toEqual([]);
expect(getRecoveryRollupPlatforms(undefined)).toEqual([]);
});
it('trims and filters blank entries from the resolved values', () => {
expect(
getRecoveryRollupPlatforms({ platforms: [' a ', '', ' ', 'b'] }),
).toEqual(['a', 'b']);
// All entries normalize to empty -> filter(Boolean) yields [].
expect(getRecoveryRollupPlatforms({ platforms: [' ', ''] })).toEqual([]);
});
});
describe('normalizeRecoveryDisplay (via normalizeRecoveryPoint)', () => {
const basePoint = {
id: 'p1',
kind: 'snapshot' as const,
mode: 'snapshot' as const,
outcome: 'success' as const,
};
it('passes a null display through unchanged', () => {
// Branch: display == null -> return display (null).
expect(normalizeRecoveryPoint({ ...basePoint, display: null })).toStrictEqual({
...basePoint,
display: null,
});
});
it('omits the display key entirely when display is undefined', () => {
// Branch: display !== undefined === false -> display key not spread.
const result = normalizeRecoveryPoint(basePoint);
expect(result).toStrictEqual(basePoint);
expect('display' in result).toBe(false);
});
it('uses canonical itemLabel/itemType when present without falling back', () => {
expect(
normalizeRecoveryPoint({
...basePoint,
display: { itemLabel: 'IL', itemType: 'IT', isWorkload: true },
}),
).toStrictEqual({
...basePoint,
display: { itemLabel: 'IL', itemType: 'IT', isWorkload: true },
});
});
it('falls back to subjectLabel/subjectType when itemLabel/itemType are absent', () => {
expect(
normalizeRecoveryPoint({
...basePoint,
display: { subjectLabel: 'SL', subjectType: 'ST', detailsSummary: 'ds' },
}),
).toStrictEqual({
...basePoint,
display: { itemLabel: 'SL', itemType: 'ST', detailsSummary: 'ds' },
});
});
it('falls back to subjectLabel when itemLabel is blank but subjectLabel is set', () => {
expect(
normalizeRecoveryPoint({
...basePoint,
display: { itemLabel: ' ', subjectLabel: 'SL2' },
}),
).toStrictEqual({
...basePoint,
display: { itemLabel: 'SL2' },
});
});
it('omits itemLabel and itemType keys when both canonical and subject fields are blank', () => {
// Exercises the falsy arms of the `...(x ? {itemLabel} : {})` spreads.
expect(
normalizeRecoveryPoint({
...basePoint,
display: { itemLabel: ' ', subjectLabel: '', itemType: '', subjectType: ' ' },
}),
).toStrictEqual({
...basePoint,
display: {},
});
});
});
describe('getRecoveryItemResourceId (via normalizeRecoveryPoint)', () => {
const basePoint = {
id: 'p2',
kind: 'backup' as const,
mode: 'remote' as const,
outcome: 'success' as const,
};
it('prefers itemResourceId over subjectResourceId', () => {
expect(
normalizeRecoveryPoint({ ...basePoint, itemResourceId: 'a', subjectResourceId: 'b' }),
).toStrictEqual({ ...basePoint, itemResourceId: 'a' });
});
it('falls back to subjectResourceId when itemResourceId is absent', () => {
expect(normalizeRecoveryPoint({ ...basePoint, subjectResourceId: 'b' })).toStrictEqual({
...basePoint,
itemResourceId: 'b',
});
});
it('omits itemResourceId when both ids are absent', () => {
// Branch: toTrimmedString(undefined) || toTrimmedString(undefined) -> ''.
const result = normalizeRecoveryPoint(basePoint);
expect(result).toStrictEqual(basePoint);
expect('itemResourceId' in result).toBe(false);
});
});
describe('getRecoveryItemRef (via normalizeRecoveryPoint)', () => {
const basePoint = {
id: 'p3',
kind: 'snapshot' as const,
mode: 'snapshot' as const,
outcome: 'success' as const,
};
it('prefers itemRef over subjectRef', () => {
expect(
normalizeRecoveryPoint({
...basePoint,
itemRef: { type: 'item' },
subjectRef: { type: 'subject' },
}),
).toStrictEqual({ ...basePoint, itemRef: { type: 'item' } });
});
it('falls back to subjectRef when itemRef is absent', () => {
expect(
normalizeRecoveryPoint({ ...basePoint, subjectRef: { type: 'subject' } }),
).toStrictEqual({ ...basePoint, itemRef: { type: 'subject' } });
});
it('omits itemRef when both refs are absent (resolves to null)', () => {
// Branch: value?.itemRef || value?.subjectRef || null -> null (falsy, not spread).
const result = normalizeRecoveryPoint(basePoint);
expect(result).toStrictEqual(basePoint);
expect('itemRef' in result).toBe(false);
});
});
describe('normalizeRecoveryPoint', () => {
it('returns a minimal point unchanged when optional fields are absent', () => {
const minimal = {
id: 'min',
kind: 'snapshot' as const,
mode: 'local' as const,
outcome: 'failed' as const,
};
expect(normalizeRecoveryPoint(minimal)).toStrictEqual(minimal);
});
});
describe('normalizeRecoveryRollup', () => {
it('returns a minimal rollup unchanged when optional fields are absent', () => {
const minimal = { rollupId: 'r-min', lastOutcome: 'warning' as const };
expect(normalizeRecoveryRollup(minimal)).toStrictEqual(minimal);
});
it('passes a null display through unchanged', () => {
expect(
normalizeRecoveryRollup({ rollupId: 'r1', lastOutcome: 'success', display: null }),
).toStrictEqual({ rollupId: 'r1', lastOutcome: 'success', display: null });
});
});
describe('normalizeRecoveryMeta (via response normalizers)', () => {
it('passes valid finite numeric meta through unchanged', () => {
const meta = { page: 2, limit: 50, total: 7, totalPages: 1 };
expect(normalizeRecoveryPointsResponse({ data: [], meta })).toStrictEqual({
data: [],
meta,
});
});
it('applies defaults when meta is null or undefined', () => {
const defaults = { page: 1, limit: 0, total: 0, totalPages: 1 };
expect(
normalizeRecoveryPointsResponse({
data: [],
} as unknown as Parameters<typeof normalizeRecoveryPointsResponse>[0]),
).toStrictEqual({ data: [], meta: defaults });
expect(
normalizeRecoveryRollupsResponse({
data: [],
meta: null,
} as unknown as Parameters<typeof normalizeRecoveryRollupsResponse>[0]),
).toStrictEqual({ data: [], meta: defaults });
});
});
describe('normalizeRecoveryPointsResponse', () => {
it('coerces non-finite and non-number meta fields to defaults and non-array data to []', () => {
expect(
normalizeRecoveryPointsResponse({
data: 'not-an-array',
meta: { page: NaN, limit: Infinity, total: 'twenty', totalPages: undefined },
} as unknown as Parameters<typeof normalizeRecoveryPointsResponse>[0]),
).toStrictEqual({
data: [],
meta: { page: 1, limit: 0, total: 0, totalPages: 1 },
});
});
});
describe('normalizeRecoveryRollupsResponse', () => {
it('normalizes an array of rollups and preserves finite meta', () => {
expect(
normalizeRecoveryRollupsResponse({
data: [
{
rollupId: 'r1',
lastOutcome: 'success',
providers: ['truenas'],
},
],
meta: { page: 1, limit: 10, total: 1, totalPages: 1 },
}),
).toStrictEqual({
data: [{ rollupId: 'r1', lastOutcome: 'success', platforms: ['truenas'] }],
meta: { page: 1, limit: 10, total: 1, totalPages: 1 },
});
});
it('returns empty data and default meta for a malformed payload', () => {
expect(
normalizeRecoveryRollupsResponse({
data: null,
meta: undefined,
} as unknown as Parameters<typeof normalizeRecoveryRollupsResponse>[0]),
).toStrictEqual({
data: [],
meta: { page: 1, limit: 0, total: 0, totalPages: 1 },
});
});
});
});
@@ -0,0 +1,524 @@
import { describe, expect, it } from 'vitest';
import {
getContainerRuntimeBadgeForRuntime,
getInfrastructurePlatformBadges,
getInfrastructureSystemIdentityBadges,
getInfrastructureSystemIdentitySortLabel,
getPlatformBadge,
getTypeBadge,
getUnifiedSourceBadges,
} from '@/utils/resourceBadgePresentation';
import type { Resource } from '@/types/resource';
const makeResource = (overrides: Partial<Resource> = {}): Resource =>
({
id: 'resource-1',
type: 'agent',
name: 'host-1',
displayName: 'host-1',
platformId: 'host-1',
platformType: 'agent',
sourceType: 'agent',
status: 'online',
lastSeen: 1,
...overrides,
}) as Resource;
describe('getPlatformBadge (branch coverage)', () => {
it('returns null when no platform type is supplied', () => {
expect(getPlatformBadge()).toBeNull();
expect(getPlatformBadge(undefined)).toBeNull();
expect(getPlatformBadge('' as never)).toBeNull();
});
it('returns the shared availability badge for the availability platform type', () => {
expect(getPlatformBadge('availability')).toStrictEqual({
label: 'Availability',
classes:
'inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300',
title: 'Availability',
});
});
it('returns shared presentation badges for non-availability platform types', () => {
expect(getPlatformBadge('kubernetes')?.label).toBe('K8s');
expect(getPlatformBadge('truenas')?.label).toBe('TrueNAS');
expect(getPlatformBadge('vmware-vsphere')?.label).toBe('vSphere');
});
});
describe('getTypeBadge (branch coverage)', () => {
it('returns null when no resource type is supplied', () => {
expect(getTypeBadge()).toBeNull();
expect(getTypeBadge('')).toBeNull();
});
it('emits a canonical label and title for a known resource type', () => {
const badge = getTypeBadge('host');
expect(badge).not.toBeNull();
expect(badge?.label).toBe('Agent');
expect(badge?.title).toBe('agent');
expect(badge?.classes).toContain('inline-flex');
expect(badge?.classes).toContain('bg-orange-100');
});
});
describe('buildUnifiedSourceBadges via getUnifiedSourceBadges (branch coverage)', () => {
it('maps canonical aliases through the shared platform badge presentation', () => {
expect(
getUnifiedSourceBadges(['kubernetes', 'vmware', 'synology-dsm']).map((b) => b.label),
).toEqual(['K8s', 'vSphere', 'Synology']);
});
it('renders the availability and generic source presentations', () => {
expect(getUnifiedSourceBadges(['availability']).map((b) => b.label)).toEqual(['Availability']);
expect(getUnifiedSourceBadges(['generic']).map((b) => b.label)).toEqual(['Generic']);
});
});
describe('getInfrastructurePlatformBadges (branch coverage)', () => {
it('returns an empty array when no sources normalize to a known platform', () => {
expect(getInfrastructurePlatformBadges([])).toEqual([]);
expect(getInfrastructurePlatformBadges(undefined)).toEqual([]);
expect(getInfrastructurePlatformBadges(['totally-unknown-source'])).toEqual([]);
});
it('keeps every non-agent platform source when multiple infrastructure platforms remain', () => {
expect(
getInfrastructurePlatformBadges(['docker', 'kubernetes']).map((b) => b.label),
).toEqual(['Docker / Podman', 'K8s']);
});
});
describe('getContainerRuntimeTone via getContainerRuntimeBadgeForRuntime (branch coverage)', () => {
it('returns null for an empty or whitespace-only runtime', () => {
expect(getContainerRuntimeBadgeForRuntime('')).toBeNull();
expect(getContainerRuntimeBadgeForRuntime(' ')).toBeNull();
});
it('normalizes Docker casing before selecting the docker tone', () => {
const badge = getContainerRuntimeBadgeForRuntime('DOCKER');
expect(badge?.label).toBe('Docker');
expect(badge?.title).toBe('Runtime: Docker');
expect(badge?.classes).toContain('bg-sky-100');
});
it('falls back to the neutral type tone for an unrecognized runtime label', () => {
const badge = getContainerRuntimeBadgeForRuntime('containerd');
expect(badge?.label).toBe('containerd');
expect(badge?.title).toBe('Runtime: containerd');
expect(badge?.classes).toContain('bg-surface-alt');
expect(badge?.classes).toContain('text-base-content');
});
});
describe('proxmoxLxcDockerVmid via getInfrastructureSystemIdentityBadges (branch coverage)', () => {
it('surfaces the trailing VMID when the host source id has multiple colon segments', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'docker-host',
platformType: 'docker',
sourceType: 'agent',
docker: {
hostSourceId: 'proxmox-lxc-docker:pve-a:node-a:250',
hostname: 'svc',
runtime: 'docker',
},
}),
);
expect(badges.map((b) => b.label)).toEqual(['LXC']);
expect(badges[0]?.title).toBe('Docker running inside Proxmox LXC 250');
});
it('omits the VMID from the tooltip when the trailing segment is zero', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'docker-host',
platformType: 'docker',
sourceType: 'agent',
docker: {
hostSourceId: 'proxmox-lxc-docker:0',
hostname: 'svc',
runtime: 'docker',
},
}),
);
expect(badges.map((b) => b.label)).toEqual(['LXC']);
expect(badges[0]?.title).toBe('Docker running inside a Proxmox LXC');
});
it('omits the VMID from the tooltip when the trailing segment is a negative integer', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'docker-host',
platformType: 'docker',
sourceType: 'agent',
docker: {
hostSourceId: 'proxmox-lxc-docker:-5',
hostname: 'svc',
runtime: 'docker',
},
}),
);
expect(badges.map((b) => b.label)).toEqual(['LXC']);
expect(badges[0]?.title).toBe('Docker running inside a Proxmox LXC');
});
});
describe('getAvailabilitySystemIdentityBadge via getInfrastructureSystemIdentityBadges (branch coverage)', () => {
it('maps http and https protocols to uppercased labels with port suffixes', () => {
const http = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'network-endpoint',
platformType: 'generic',
sourceType: 'api',
platformData: {
sources: ['availability'],
availability: { protocol: 'http', address: 'health.local', port: 80 },
},
}),
);
expect(http.map((b) => b.label)).toEqual(['HTTP']);
expect(http[0]?.title).toBe('HTTP availability probe health.local:80');
const https = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'network-endpoint',
platformType: 'generic',
sourceType: 'api',
platformData: {
sources: ['availability'],
availability: { protocol: 'https', address: 'secure.local' },
},
}),
);
expect(https.map((b) => b.label)).toEqual(['HTTPS']);
expect(https[0]?.title).toBe('HTTPS availability probe secure.local');
});
it('uppercases an unrecognized non-empty protocol', () => {
const snmp = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'network-endpoint',
platformType: 'generic',
sourceType: 'api',
platformData: {
sources: ['availability'],
availability: { protocol: 'snmp', address: 'switch-1' },
},
}),
);
expect(snmp.map((b) => b.label)).toEqual(['SNMP']);
expect(snmp[0]?.title).toBe('SNMP availability probe switch-1');
});
it('drops the port suffix when the port is zero and falls back to the Probe label when protocol is empty', () => {
const tcp = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'network-endpoint',
platformType: 'generic',
sourceType: 'api',
platformData: {
sources: ['availability'],
availability: { protocol: 'tcp', address: 'host-a', port: 0 },
},
}),
);
expect(tcp.map((b) => b.label)).toEqual(['TCP']);
expect(tcp[0]?.title).toBe('TCP availability probe host-a');
});
it('treats a raw availability source as an availability endpoint even without a facet', () => {
const probe = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'agent',
platformType: 'agent',
sourceType: 'api',
sources: ['availability'],
platformData: { sources: ['availability'] },
}),
);
expect(probe.map((b) => b.label)).toEqual(['Probe']);
expect(probe[0]?.title).toBe('Probe availability probe');
});
});
describe('getStorageSystemIdentityBadge via getInfrastructureSystemIdentityBadges (branch coverage)', () => {
it('appends a TrueNAS storage version and uses storageType in the title when topology is absent', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'storage',
platformType: 'truenas',
sourceType: 'api',
sources: ['truenas'],
platformData: {
sources: ['truenas'],
storage: { platform: 'truenas', type: 'zfs-pool' },
truenas: { version: '24.10.0' },
},
}),
);
expect(badges.map((b) => b.label)).toEqual(['TrueNAS 24.10.0']);
expect(badges[0]?.title).toBe('TrueNAS zfs-pool 24.10.0');
});
it('resolves the unraid storage version from the agent osVersion', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'storage',
platformType: 'agent',
sourceType: 'agent',
sources: ['agent'],
platformData: {
sources: ['agent'],
storage: { platform: 'unraid' },
agent: { hostProfile: 'unraid', osVersion: '7.0.0' },
},
}),
);
expect(badges.map((b) => b.label)).toEqual(['Unraid 7.0.0']);
});
it('resolves the docker version arm when storage is owned by a docker platform', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'storage',
platformType: 'agent',
sourceType: 'agent',
sources: ['agent'],
platformData: {
sources: ['agent'],
storage: { platform: 'docker' },
docker: { dockerVersion: '27.0.0' },
},
}),
);
expect(badges.map((b) => b.label)).toEqual(['Docker / Podman 27.0.0']);
});
});
describe('getAgentSystemIdentityBadge via getInfrastructureSystemIdentityBadges (branch coverage)', () => {
it('uses the known platform source branch when the reported OS text matches a governed platform token', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'agent',
platformType: 'agent',
sourceType: 'agent',
sources: ['agent'],
platformData: {
sources: ['agent'],
agent: { platform: 'linux', osName: 'VMware ESXi' },
},
}),
);
expect(badges.map((b) => b.label)).toEqual(['vSphere']);
expect(badges[0]?.title).toBe('VMware ESXi');
});
it('keeps the agent host-profile identity ahead of a co-reported docker runtime', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'agent',
platformType: 'agent',
sourceType: 'hybrid',
sources: ['agent'],
platformData: {
sources: ['agent', 'docker'],
docker: { runtime: 'docker' },
agent: { platform: 'linux', hostProfile: 'unraid', osName: 'Unraid', osVersion: '7.3.0' },
},
}),
);
expect(badges.map((b) => b.label)).toEqual(['Unraid 7.3.0']);
});
});
describe('getDockerHostOsIdentityBadge via getInfrastructureSystemIdentityBadges (branch coverage)', () => {
it('derives a host OS label from docker metadata when no agent identity is present', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'docker-host',
platformType: 'docker',
sourceType: 'api',
platformData: { sources: ['docker'] },
docker: { os: 'Ubuntu 24.04.2 LTS' },
}),
);
expect(badges.map((b) => b.label)).toEqual(['Ubuntu']);
expect(badges[0]?.title).toBe('Ubuntu 24.04.2 LTS');
});
it('maps a docker OS string that matches a governed platform to the shared platform badge', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'docker-host',
platformType: 'docker',
sourceType: 'api',
platformData: { sources: ['docker'] },
docker: { os: 'TrueNAS SCALE' },
}),
);
expect(badges.map((b) => b.label)).toEqual(['TrueNAS']);
});
it('falls back to the docker runtime badge when the docker OS string is unrecognized', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'docker-host',
platformType: 'docker',
sourceType: 'api',
platformData: { sources: ['docker'] },
docker: { os: 'acme-custom-os' },
}),
);
expect(badges.map((b) => b.label)).toEqual(['Docker / Podman']);
});
it('falls back to the docker runtime badge when docker metadata has no os field', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'docker-host',
platformType: 'docker',
sourceType: 'api',
platformData: { sources: ['docker'] },
docker: {},
}),
);
expect(badges.map((b) => b.label)).toEqual(['Docker / Podman']);
});
});
describe('getVersionedSourceBadge via getInfrastructureSystemIdentityBadges (branch coverage)', () => {
it('unwraps a Proxmox pve-manager version wrapper into a dotted version segment', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'agent',
platformType: 'proxmox-pve',
sourceType: 'api',
sources: ['proxmox'],
platformData: {
sources: ['proxmox'],
proxmox: { pveVersion: 'pve-manager/9.1.9/ee7bad0a3d1546c9' },
},
}),
);
expect(badges.map((b) => b.label)).toEqual(['PVE 9.1.9']);
});
it('drops an unknown sentinel version instead of appending it', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'agent',
platformType: 'proxmox-pve',
sourceType: 'api',
sources: ['proxmox'],
platformData: {
sources: ['proxmox'],
proxmox: { pveVersion: 'unknown' },
},
}),
);
expect(badges.map((b) => b.label)).toEqual(['PVE']);
});
it('versions PBS and PMG identities from their respective facet versions', () => {
const pbs = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'pbs',
platformType: 'proxmox-pbs',
sourceType: 'api',
sources: ['pbs'],
platformData: { sources: ['pbs'], pbs: { version: '3.3.2' } },
}),
);
expect(pbs.map((b) => b.label)).toEqual(['PBS 3.3.2']);
const pmg = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'pmg',
platformType: 'proxmox-pmg',
sourceType: 'api',
sources: ['pmg'],
platformData: { sources: ['pmg'], pmg: { version: '9.1.2' } },
}),
);
expect(pmg.map((b) => b.label)).toEqual(['PMG 9.1.2']);
});
it('versions TrueNAS, vSphere and K8s identities from their facet versions', () => {
const truenas = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'agent',
platformType: 'truenas',
sourceType: 'api',
sources: ['truenas'],
platformData: { sources: ['truenas'], truenas: { version: '24.10.0' } },
}),
);
expect(truenas.map((b) => b.label)).toEqual(['TrueNAS 24.10.0']);
const vsphere = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'vm',
platformType: 'vmware-vsphere',
sourceType: 'api',
sources: ['vmware'],
platformData: { sources: ['vmware'], vmware: { version: '8.0.3' } },
}),
);
expect(vsphere.map((b) => b.label)).toEqual(['vSphere 8.0.3']);
const k8s = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'k8s-cluster',
platformType: 'kubernetes',
sourceType: 'api',
sources: ['kubernetes'],
platformData: { sources: ['kubernetes'], kubernetes: { version: '1.31.0' } },
}),
);
expect(k8s.map((b) => b.label)).toEqual(['K8s 1.31.0']);
});
it('versions a presentation-only platform through the default version-resolution arm', () => {
const badges = getInfrastructureSystemIdentityBadges(
makeResource({
type: 'agent',
platformType: 'agent',
sourceType: 'api',
sources: ['synology-dsm'],
platformData: {
sources: ['synology-dsm'],
'synology-dsm': { version: '7.2.1' },
},
}),
);
expect(badges.map((b) => b.label)).toEqual(['Synology 7.2.1']);
});
});
describe('getInfrastructureSystemIdentityBadges and getInfrastructureSystemIdentitySortLabel (branch coverage)', () => {
it('falls back to the platform badge for a bare agent resource with no other identity', () => {
const resource = makeResource({
type: 'agent',
platformType: 'agent',
sourceType: 'agent',
sources: [],
});
expect(getInfrastructureSystemIdentityBadges(resource).map((b) => b.label)).toEqual(['Agent']);
expect(getInfrastructureSystemIdentitySortLabel(resource)).toBe('Agent');
});
it('returns an empty badge set and empty sort label when nothing resolves', () => {
const resource = makeResource({
id: 'lonely',
name: 'lonely',
type: 'vm',
platformType: undefined,
sourceType: 'api',
sources: [],
});
expect(getInfrastructureSystemIdentityBadges(resource)).toEqual([]);
expect(getInfrastructureSystemIdentitySortLabel(resource)).toBe('');
});
});
@@ -0,0 +1,421 @@
import { describe, expect, it } from 'vitest';
import type {
ResourceRedactionHint,
ResourceRoutingScope,
ResourceSensitivity,
} from '@/types/resource';
import {
getResourcePolicyDisplayLabel,
getResourcePolicyGovernedSummary,
getResourcePolicyTableBadges,
getResourceRedactionHintLabel,
getResourceRoutingScopeLabel,
getResourceSensitivityLabel,
hasDefaultResourcePolicyPosture,
shouldShowResourceAlternateName,
} from '@/utils/resourcePolicyPresentation';
import type { ResourcePolicyDisplayResource } from '@/utils/resourcePolicyPresentation';
/**
* Branch-coverage supplement for `resourcePolicyPresentation.ts`.
*
* The existing sibling test (`resourcePolicyPresentation.test.ts`) exercises the
* happy paths. This file targets the defensive / fallback arms of each named
* function: undefined inputs, non-blocking postures, empty/whitespace strings,
* the `?? hint` redaction fallback, the `?? 0` redact-length arm, the
* sensitivity-vs-routing primary selection, and the concise-summary status
* absence / all-empty-parts / no-semicolon branches.
*
* NOTE: `getConciseGovernedDisplaySummary` is a module-private (non-exported)
* helper, so it is covered indirectly through `getResourcePolicyDisplayLabel`,
* which is the only call site — matching the existing test's approach.
*/
const governedPolicy = (overrides: {
sensitivity?: ResourceSensitivity;
scope?: ResourceRoutingScope;
redact?: ResourceRedactionHint[];
} = {}): NonNullable<ResourcePolicyDisplayResource['policy']> => ({
sensitivity: overrides.sensitivity ?? 'restricted',
routing: {
scope: overrides.scope ?? 'local-only',
redact: overrides.redact,
},
});
// The static type marks `name` and `displayName` as required, but the source
// functions defendively optional-chain (`?.trim()`) on both. This helper lets
// us hand in deliberately-partial resources to exercise those fallback arms.
const asResource = (
r: Partial<ResourcePolicyDisplayResource>,
): ResourcePolicyDisplayResource => r as unknown as ResourcePolicyDisplayResource;
describe('getResourceSensitivityLabel — branch coverage', () => {
it('returns the canonical label for every known sensitivity', () => {
expect(getResourceSensitivityLabel('public')).toBe('Public');
expect(getResourceSensitivityLabel('internal')).toBe('Internal');
expect(getResourceSensitivityLabel('sensitive')).toBe('Sensitive');
expect(getResourceSensitivityLabel('restricted')).toBe('Restricted');
});
it('returns "Unclassified" when no sensitivity is supplied (falsy arm)', () => {
expect(getResourceSensitivityLabel(undefined)).toBe('Unclassified');
});
});
describe('getResourceRoutingScopeLabel — branch coverage', () => {
it('returns the canonical label for every known routing scope', () => {
expect(getResourceRoutingScopeLabel('cloud-summary')).toBe('Cloud Summary');
expect(getResourceRoutingScopeLabel('local-first')).toBe('Local First');
expect(getResourceRoutingScopeLabel('local-only')).toBe('Local Only');
});
it('returns "Unrouted" when no scope is supplied (falsy arm)', () => {
expect(getResourceRoutingScopeLabel(undefined)).toBe('Unrouted');
});
});
describe('getResourceRedactionHintLabel — branch coverage', () => {
it('returns the canonical label for every known redaction hint', () => {
expect(getResourceRedactionHintLabel('hostname')).toBe('Hostname');
expect(getResourceRedactionHintLabel('ip-address')).toBe('IP Address');
expect(getResourceRedactionHintLabel('platform-id')).toBe('Platform ID');
expect(getResourceRedactionHintLabel('alias')).toBe('Alias');
expect(getResourceRedactionHintLabel('path')).toBe('Path');
});
it('returns "Unclassified" when no hint is supplied (falsy arm)', () => {
expect(getResourceRedactionHintLabel(undefined)).toBe('Unclassified');
});
it('falls back to the raw hint string when the hint is not in the label map (?? hint arm)', () => {
expect(
getResourceRedactionHintLabel('custom-redaction' as ResourceRedactionHint),
).toBe('custom-redaction');
});
});
describe('hasDefaultResourcePolicyPosture — branch coverage', () => {
it('returns false when no policy is supplied (short-circuit before fields)', () => {
expect(hasDefaultResourcePolicyPosture(undefined)).toBe(false);
});
it('returns false when sensitivity is not "internal"', () => {
expect(
hasDefaultResourcePolicyPosture({
sensitivity: 'restricted',
routing: { scope: 'cloud-summary' },
}),
).toBe(false);
});
it('returns false when scope is not "cloud-summary" even with internal sensitivity', () => {
expect(
hasDefaultResourcePolicyPosture({
sensitivity: 'internal',
routing: { scope: 'local-first' },
}),
).toBe(false);
});
it('returns false when redact is a non-empty array (exercises redact?.length with defined array)', () => {
expect(
hasDefaultResourcePolicyPosture({
sensitivity: 'internal',
routing: { scope: 'cloud-summary', redact: ['hostname'] },
}),
).toBe(false);
});
it('returns true when redact is an empty array (defined but length 0)', () => {
expect(
hasDefaultResourcePolicyPosture({
sensitivity: 'internal',
routing: { scope: 'cloud-summary', redact: [] },
}),
).toBe(true);
});
it('returns true for the canonical default posture (redact undefined hits ?? 0)', () => {
expect(
hasDefaultResourcePolicyPosture({
sensitivity: 'internal',
routing: { scope: 'cloud-summary' },
}),
).toBe(true);
});
});
describe('getResourcePolicyTableBadges — branch coverage', () => {
it('returns an empty array when no policy is supplied', () => {
expect(getResourcePolicyTableBadges(undefined)).toEqual([]);
});
it('returns an empty array for a non-blocking public posture', () => {
expect(
getResourcePolicyTableBadges({
sensitivity: 'public',
routing: { scope: 'cloud-summary' },
}),
).toEqual([]);
});
it('uses the sensitivity badge as primary when scope is not local-only (restricted + cloud-summary)', () => {
const badges = getResourcePolicyTableBadges({
sensitivity: 'restricted',
routing: { scope: 'cloud-summary' },
});
expect(badges).toHaveLength(1);
expect(badges[0]?.label).toBe('Restricted');
// No redactions -> redactionTitle is undefined and filtered out of the title.
expect(badges[0]?.title).toBe(
'Restricted: Resource data is tightly restricted and requires guarded handling. Cloud Summary: This resource may use cloud summarization within policy limits.',
);
expect(badges[0]?.title).not.toContain('Redacts');
});
it('uses the sensitivity badge as primary when restricted with local-first scope and redactions', () => {
const badges = getResourcePolicyTableBadges({
sensitivity: 'restricted',
routing: { scope: 'local-first', redact: ['hostname', 'path'] },
});
expect(badges).toHaveLength(1);
expect(badges[0]?.label).toBe('Restricted');
expect(badges[0]?.title).toContain('Local First');
expect(badges[0]?.title).toContain('Redacts Hostname, Path.');
});
it('uses the routing badge as primary when scope is local-only even with non-restricted sensitivity', () => {
const badges = getResourcePolicyTableBadges({
sensitivity: 'sensitive',
routing: { scope: 'local-only', redact: ['alias'] },
});
expect(badges).toHaveLength(1);
expect(badges[0]?.label).toBe('Local Only');
expect(badges[0]?.title).toContain('Sensitive');
expect(badges[0]?.title).toContain('Redacts Alias.');
});
});
describe('getResourcePolicyGovernedSummary — branch coverage', () => {
it('returns empty string when no resource is supplied', () => {
expect(getResourcePolicyGovernedSummary(undefined)).toBe('');
expect(getResourcePolicyGovernedSummary(null)).toBe('');
});
it('falls back to displayName/name when policy is absent', () => {
expect(
getResourcePolicyGovernedSummary({
name: 'host-1',
displayName: 'Host One',
}),
).toBe('Host One');
});
it('falls back to name when displayName is absent and policy is not governed', () => {
expect(
getResourcePolicyGovernedSummary(
asResource({
name: 'host-1',
policy: { sensitivity: 'internal', routing: { scope: 'cloud-summary' } },
}),
),
).toBe('host-1');
});
it('returns empty string when neither displayName nor name is present and policy is not governed', () => {
expect(
getResourcePolicyGovernedSummary(
asResource({
name: '',
displayName: ' ',
policy: { sensitivity: 'internal', routing: { scope: 'cloud-summary' } },
}),
),
).toBe('');
});
it('returns the trimmed aiSafeSummary for a governed resource', () => {
expect(
getResourcePolicyGovernedSummary(
asResource({
name: 'n',
policy: governedPolicy(),
aiSafeSummary: ' governed summary text ',
}),
),
).toBe('governed summary text');
});
it('returns "redacted by policy" for a governed resource with an empty aiSafeSummary', () => {
expect(
getResourcePolicyGovernedSummary(
asResource({
name: 'n',
policy: governedPolicy(),
aiSafeSummary: ' ',
}),
),
).toBe('redacted by policy');
});
it('returns "redacted by policy" for a governed resource with no aiSafeSummary at all', () => {
expect(
getResourcePolicyGovernedSummary(
asResource({
name: 'n',
policy: governedPolicy(),
}),
),
).toBe('redacted by policy');
});
});
describe('getResourcePolicyDisplayLabel — branch coverage', () => {
it('returns empty string when no resource is supplied', () => {
expect(getResourcePolicyDisplayLabel(undefined)).toBe('');
expect(getResourcePolicyDisplayLabel(null)).toBe('');
});
it('falls back to displayName when policy is absent', () => {
expect(
getResourcePolicyDisplayLabel({
name: 'host-1',
displayName: 'Host One',
}),
).toBe('Host One');
});
it('falls back to name when displayName is absent and policy is absent', () => {
expect(getResourcePolicyDisplayLabel(asResource({ name: 'host-1' }))).toBe('host-1');
});
it('returns empty string when neither name nor displayName is present and policy is absent', () => {
expect(getResourcePolicyDisplayLabel({ name: ' ', displayName: '' })).toBe('');
});
it('falls back to displayName/name when policy is present but not governed', () => {
expect(
getResourcePolicyDisplayLabel({
name: 'host-1',
displayName: 'Host One',
policy: { sensitivity: 'public', routing: { scope: 'cloud-summary' } },
}),
).toBe('Host One');
});
// The following cases drive the private getConciseGovernedDisplaySummary helper.
it('concise: returns the trimmed summary unchanged when it has no semicolon', () => {
expect(
getResourcePolicyDisplayLabel(
asResource({
name: 'n',
policy: governedPolicy(),
aiSafeSummary: ' plain governed label ',
}),
),
).toBe('plain governed label');
});
it('concise: strips a trailing " resource" suffix from the base label and appends a status', () => {
expect(
getResourcePolicyDisplayLabel(
asResource({
name: 'n',
policy: governedPolicy(),
aiSafeSummary: 'backup server resource; status online; sources pbs',
}),
),
).toBe('backup server (online)');
});
it('concise: returns only the base label when no status part is present', () => {
expect(
getResourcePolicyDisplayLabel(
asResource({
name: 'n',
policy: governedPolicy(),
aiSafeSummary: 'web node; count 5; sources api',
}),
),
).toBe('web node');
});
it('concise: returns empty string when every semicolon-delimited part is empty', () => {
expect(
getResourcePolicyDisplayLabel(
asResource({
name: 'n',
policy: governedPolicy(),
// governed summary trims to "; ;", which has no non-empty parts -> ''
aiSafeSummary: ' ; ; ',
}),
),
).toBe('');
});
it('concise: keeps the base label verbatim when the " resource" regex does not match', () => {
expect(
getResourcePolicyDisplayLabel(
asResource({
name: 'n',
policy: governedPolicy(),
aiSafeSummary: 'storage array; status degraded',
}),
),
).toBe('storage array (degraded)');
});
});
describe('shouldShowResourceAlternateName — branch coverage', () => {
it('returns false when no resource is supplied', () => {
expect(shouldShowResourceAlternateName(undefined)).toBe(false);
expect(shouldShowResourceAlternateName(null)).toBe(false);
});
it('returns false when displayName is absent', () => {
expect(shouldShowResourceAlternateName(asResource({ name: 'host-1' }))).toBe(false);
});
it('returns false when name is absent', () => {
expect(shouldShowResourceAlternateName(asResource({ displayName: 'Host One' }))).toBe(false);
});
it('treats a whitespace-only displayName as present (guard does not trim) and thus differing from name', () => {
// The guard `!resource?.displayName` does not trim, so ' ' is truthy and
// passes; then ''.toLowerCase() !== 'host-1' -> true.
expect(
shouldShowResourceAlternateName({ name: 'host-1', displayName: ' ' }),
).toBe(true);
});
it('returns false when the policy requires governed handling', () => {
expect(
shouldShowResourceAlternateName({
name: 'host-1',
displayName: 'Host One',
policy: governedPolicy(),
}),
).toBe(false);
});
it('returns false when displayName and name match case-insensitively after trimming', () => {
expect(
shouldShowResourceAlternateName({
name: 'HOST-1',
displayName: ' host-1 ',
}),
).toBe(false);
});
it('returns true when displayName and name differ', () => {
expect(
shouldShowResourceAlternateName({
name: 'host-1',
displayName: 'Primary Node',
}),
).toBe(true);
});
});
@@ -0,0 +1,396 @@
/**
* Branch-coverage tests for status.ts — second pass.
*
* Focused on branches of the named indicator/label helpers that the sibling
* status.test.ts does not yet reach: the `?? `/`||` coalescing arms, the
* `connection || status` label-priority ternaries, the unknown/empty fallthrough
* arms, and the badge-tone class table lookup (including its `|| muted` guard).
*
* Fixtures use `Partial<Node>`/`Partial<...>` exactly like the public types the
* functions accept; a single deliberately-malformed badge-variant input is cast
* via `as unknown as StatusIndicatorVariant` to exercise the defensive `||`.
*/
import { describe, expect, it } from 'vitest';
import type { StatusIndicatorVariant } from '@/utils/status';
import {
formatStatusLabel,
getCanonicalStatusLabel,
getStatusIndicatorBadgeToneClasses,
isNodeOnline,
getNodeStatusIndicator,
getDockerHostStatusIndicator,
getDockerContainerStatusIndicator,
getDockerServiceStatusIndicator,
getAgentStatusIndicator,
getPBSStatusIndicator,
getReplicationJobStatusIndicator,
} from '@/utils/status';
describe('formatStatusLabel (branch coverage)', () => {
it('returns the fallback when value is null', () => {
// `if (!value)` arm — null is falsy.
expect(formatStatusLabel(null, 'N/A')).toBe('N/A');
});
it('returns the fallback for a whitespace-only value (post-trim guard)', () => {
// `if (!normalized)` arm — trim() yields '' which is falsy.
expect(formatStatusLabel(' ', 'Unavailable')).toBe('Unavailable');
});
it('uses the default "Unknown" fallback when none is provided for empty input', () => {
expect(formatStatusLabel('')).toBe('Unknown');
});
it('capitalizes the first character without lowercasing the rest of a multi-word value', () => {
// capitalize arm: only charAt(0).toUpperCase() is applied; the tail keeps case.
expect(formatStatusLabel('sync IN progress')).toBe('Sync IN progress');
});
});
describe('getCanonicalStatusLabel (branch coverage)', () => {
it('returns the default fallback for undefined and the custom fallback for whitespace', () => {
// `if (!normalized)` arm via normalize('') on undefined and whitespace.
expect(getCanonicalStatusLabel(undefined)).toBe('Unknown');
expect(getCanonicalStatusLabel(' ', 'Pending')).toBe('Pending');
});
it('resolves every canonical label in STATUS_LABELS through the lowercase lookup', () => {
// STATUS_LABELS[normalized] hit arm, including mixed-case normalization.
expect(getCanonicalStatusLabel('ONLINE')).toBe('Online');
expect(getCanonicalStatusLabel('Degraded')).toBe('Degraded');
expect(getCanonicalStatusLabel('PAUSED')).toBe('Paused');
expect(getCanonicalStatusLabel('Stopped')).toBe('Stopped');
expect(getCanonicalStatusLabel('UNKNOWN')).toBe('Unknown');
});
it('falls back to the trimmed raw value (original case preserved) when the status is not canonical', () => {
// `|| raw` arm — `raw` is the pre-lowercase trim(), so case is preserved.
expect(getCanonicalStatusLabel('CustomState')).toBe('CustomState');
expect(getCanonicalStatusLabel('waiting-for-quorum')).toBe('waiting-for-quorum');
});
});
describe('getStatusIndicatorBadgeToneClasses (branch coverage)', () => {
it('returns the exact tone class string for every declared variant', () => {
// Direct table-hit arm for all five variants, with full string assertion
// (the sibling test only uses toContain for four of them).
expect(getStatusIndicatorBadgeToneClasses('success')).toBe(
'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',
);
expect(getStatusIndicatorBadgeToneClasses('warning')).toBe(
'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300',
);
expect(getStatusIndicatorBadgeToneClasses('danger')).toBe(
'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
);
expect(getStatusIndicatorBadgeToneClasses('info')).toBe(
'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
);
expect(getStatusIndicatorBadgeToneClasses('muted')).toBe(
'bg-surface-alt text-base-content',
);
});
it('falls back to the muted tone class for a variant not present in the table', () => {
// `|| STATUS_INDICATOR_BADGE_TONE_CLASSES.muted` defensive arm — driven by a
// deliberately-malformed variant cast to satisfy the nominal type.
const invalid = 'chartreuse' as unknown as StatusIndicatorVariant;
expect(getStatusIndicatorBadgeToneClasses(invalid)).toBe(
'bg-surface-alt text-base-content',
);
});
});
describe('isNodeOnline (branch coverage)', () => {
it('treats an undefined uptime as zero via the `?? 0` coalescing (returns false)', () => {
// `(node.uptime ?? 0) <= 0` arm when uptime is absent.
expect(isNodeOnline({ status: 'online' })).toBe(false);
});
it('returns true when connectionHealth is absent (normalize("") is neither offline nor error)', () => {
// Happy-path fall-through: connection is undefined -> normalize('') -> ''.
expect(isNodeOnline({ status: 'online', uptime: 1000, connectionHealth: undefined })).toBe(
true,
);
});
it('normalizes connectionHealth case-insensitively before the offline/error check', () => {
// normalize() lowercases, so 'OFFLINE' / 'ERROR' hit the reject arm.
expect(isNodeOnline({ status: 'online', uptime: 1000, connectionHealth: 'OFFLINE' })).toBe(
false,
);
expect(isNodeOnline({ status: 'online', uptime: 1000, connectionHealth: 'Error' })).toBe(
false,
);
});
it('rejects a whitespace-only connectionHealth neither matches offline nor error, but is still treated as connected', () => {
// ' ' normalizes to '' — not rejected, so an otherwise-healthy node is online.
expect(isNodeOnline({ status: 'online', uptime: 1000, connectionHealth: ' ' })).toBe(true);
});
});
describe('getNodeStatusIndicator (branch coverage)', () => {
it('flags danger from an offline connectionHealth even when status is online', () => {
// OFFLINE_HEALTH_STATUSES.has(connection) arm; label prefers connection.
const result = getNodeStatusIndicator({
status: 'online',
uptime: 1000,
connectionHealth: 'disconnected',
});
expect(result).toEqual({ variant: 'danger', label: 'Disconnected' });
});
it('prefers connection over status when both are offline for the danger label', () => {
// `formatStatusLabel(connection || status, 'Offline')` — connection wins.
const result = getNodeStatusIndicator({
status: 'offline',
uptime: 0,
connectionHealth: 'timeout',
});
expect(result).toEqual({ variant: 'danger', label: 'Timeout' });
});
it('falls back to the status for the danger label when connection is absent', () => {
// connection '' -> `'' || status` -> status drives the label.
const result = getNodeStatusIndicator({ status: 'unreachable', uptime: 1000 });
expect(result).toEqual({ variant: 'danger', label: 'Unreachable' });
});
it('flags warning from a degraded connectionHealth that is not in the offline set', () => {
// DEGRADED_HEALTH_STATUSES.has(connection) arm; label prefers connection.
const result = getNodeStatusIndicator({
status: 'online',
uptime: 1000,
connectionHealth: 'syncing',
});
expect(result).toEqual({ variant: 'warning', label: 'Syncing' });
});
it('returns muted default indicator when status is neither offline, degraded, nor online', () => {
// Final `return defaultIndicator` arm — 'paused' is in no health-status set
// and isNodeOnline is false because status !== 'online'.
const result = getNodeStatusIndicator({ status: 'paused', uptime: 1000 });
expect(result).toEqual({ variant: 'muted', label: 'Unknown' });
});
});
describe('getDockerHostStatusIndicator (branch coverage)', () => {
it('returns warning for a degraded status via the host object', () => {
// DEGRADED_HEALTH_STATUSES.has(status) arm.
expect(getDockerHostStatusIndicator({ status: 'degraded' })).toEqual({
variant: 'warning',
label: 'Degraded',
});
});
it('returns a muted indicator with a formatted label for an unknown non-empty status string', () => {
// `status ? { muted, label }` truthy arm — 'paused' is in no status set and
// not a connected-health status, so it falls to the muted-with-label branch.
expect(getDockerHostStatusIndicator('paused')).toEqual({
variant: 'muted',
label: 'Paused',
});
});
it('returns the muted default indicator when no status is available', () => {
// `: defaultIndicator` arm — empty normalized status is falsy.
expect(getDockerHostStatusIndicator(undefined)).toEqual({
variant: 'muted',
label: 'Unknown',
});
expect(getDockerHostStatusIndicator({ status: undefined })).toEqual({
variant: 'muted',
label: 'Unknown',
});
});
});
describe('getDockerContainerStatusIndicator (branch coverage)', () => {
it('falls to the warning branch when a running container has a non-healthy, non-unhealthy health', () => {
// The running-success guard requires (!health || health === 'healthy');
// 'starting' is truthy and not 'healthy', so it skips success and falls to
// the trailing `if (state)` warning arm.
expect(getDockerContainerStatusIndicator({ state: 'running', health: 'starting' })).toEqual({
variant: 'warning',
label: 'Running',
});
});
it('labels an error state with a capitalized version of the state name', () => {
// ERROR_CONTAINER_STATES.has(state) arm where health !== 'unhealthy', so the
// label is formatStatusLabel(state, 'Error').
expect(getDockerContainerStatusIndicator({ state: 'oomkilled' })).toEqual({
variant: 'danger',
label: 'Oomkilled',
});
});
it('prioritizes the unhealthy label over a stopped state', () => {
// 'created' is in STOPPED_CONTAINER_STATES, but the unhealthy check comes
// first and forces the 'Unhealthy' label.
expect(getDockerContainerStatusIndicator({ state: 'created', health: 'unhealthy' })).toEqual({
variant: 'danger',
label: 'Unhealthy',
});
});
it('returns a warning indicator from a health-only signal when state is empty', () => {
// `if (!state && health)` arm — state absent, health present.
expect(getDockerContainerStatusIndicator({ state: undefined, health: 'starting' })).toEqual({
variant: 'warning',
label: 'Starting',
});
});
it('returns a warning indicator for a custom state that is in no state set', () => {
// `if (state)` trailing warning arm — 'deploying' is not running, not in the
// error set, not in the stopped set.
expect(getDockerContainerStatusIndicator({ state: 'deploying' })).toEqual({
variant: 'warning',
label: 'Deploying',
});
});
it('returns the muted default indicator when neither state nor health is present', () => {
// Final `return defaultIndicator` arm.
expect(getDockerContainerStatusIndicator({})).toEqual({
variant: 'muted',
label: 'Unknown',
});
});
});
describe('getDockerServiceStatusIndicator (branch coverage)', () => {
it('coalesces undefined task counts to zero and reports "No tasks"', () => {
// `service.desiredTasks ?? 0` and `service.runningTasks ?? 0` defaulting arms.
expect(getDockerServiceStatusIndicator({})).toEqual({
variant: 'muted',
label: 'No tasks',
});
});
it('uses the singular "task" form when exactly one task is running with none desired', () => {
// `running === 1 ? '' : 's'` singular arm inside the desired<=0 warning.
expect(getDockerServiceStatusIndicator({ desiredTasks: 0, runningTasks: 1 })).toEqual({
variant: 'warning',
label: 'Running 1 task',
});
});
it('reports healthy when running exceeds desired (>= arm, strictly-greater path)', () => {
// `if (running >= desired)` arm with running > desired.
expect(getDockerServiceStatusIndicator({ desiredTasks: 2, runningTasks: 5 })).toEqual({
variant: 'success',
label: 'Healthy',
});
});
});
describe('getAgentStatusIndicator (branch coverage)', () => {
it('returns success with the "Online" label for a running agent', () => {
// `status === RUNNING_STATUS` arm of the online/running check.
expect(getAgentStatusIndicator({ status: 'running' })).toEqual({
variant: 'success',
label: 'Online',
});
});
it('labels offline statuses from the OFFLINE set with a capitalized status', () => {
// OFFLINE arm with a non-'offline' member to exercise formatStatusLabel.
expect(getAgentStatusIndicator({ status: 'unreachable' })).toEqual({
variant: 'danger',
label: 'Unreachable',
});
});
it('labels degraded statuses from the DEGRADED set with a capitalized status', () => {
// DEGRADED arm with a non-'degraded' member.
expect(getAgentStatusIndicator({ status: 'syncing' })).toEqual({
variant: 'warning',
label: 'Syncing',
});
});
it('returns a muted indicator with a formatted label for an unknown non-empty status', () => {
// `status ? { muted, label }` truthy arm — 'idle' is in no status set.
expect(getAgentStatusIndicator({ status: 'idle' })).toEqual({
variant: 'muted',
label: 'Idle',
});
});
it('returns the muted default indicator when agent status is empty', () => {
// `: defaultIndicator` arm — normalized status is '' (falsy).
expect(getAgentStatusIndicator({ status: '' })).toEqual({
variant: 'muted',
label: 'Unknown',
});
});
});
describe('getPBSStatusIndicator (branch coverage)', () => {
it('flags warning from a degraded connectionHealth with a non-canonical status', () => {
// DEGRADED arm driven by connection alone (status 'paused' is not healthy/
// online and not in the degraded set, so without connection it would fall
// through to defaultIndicator). Label prefers connection.
expect(
getPBSStatusIndicator({ status: 'paused', connectionHealth: 'maintenance' }),
).toEqual({ variant: 'warning', label: 'Maintenance' });
});
it('prefers connection over status when both are degraded for the warning label', () => {
// `formatStatusLabel(connection || status, 'Degraded')` — connection wins.
expect(
getPBSStatusIndicator({ status: 'degraded', connectionHealth: 'recovering' }),
).toEqual({ variant: 'warning', label: 'Recovering' });
});
it('returns the muted default indicator for a status that is neither offline, healthy/online, nor degraded', () => {
// Final `return defaultIndicator` arm.
expect(getPBSStatusIndicator({ status: 'paused' })).toEqual({
variant: 'muted',
label: 'Unknown',
});
});
});
describe('getReplicationJobStatusIndicator (branch coverage)', () => {
it('coalesces status from the state field when status is absent', () => {
// `job.status || job.state` arm — status undefined falls through to state.
expect(getReplicationJobStatusIndicator({ state: 'syncing' })).toEqual({
variant: 'warning',
label: 'Syncing',
});
expect(getReplicationJobStatusIndicator({ state: 'idle' })).toEqual({
variant: 'success',
label: 'Idle',
});
});
it('flags danger from lastSyncStatus alone when status is empty, labelling from lastStatus', () => {
// lastStatus.includes('error') arm; status is '' so `status || lastStatus`
// yields lastStatus for the label.
expect(getReplicationJobStatusIndicator({ lastSyncStatus: 'backup-error' })).toEqual({
variant: 'danger',
label: 'Backup-error',
});
});
it('prioritizes the error check over the sync check for a status containing both', () => {
// Order of checks: error is evaluated before sync.
expect(getReplicationJobStatusIndicator({ status: 'sync-error' })).toEqual({
variant: 'danger',
label: 'Sync-error',
});
});
it('returns the success/idle indicator for an empty status with no lastSyncStatus', () => {
// Trailing success arm with status '' -> formatStatusLabel('', 'Idle') -> 'Idle'.
expect(getReplicationJobStatusIndicator({})).toEqual({
variant: 'success',
label: 'Idle',
});
});
});