mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +00:00
Cover infrastructure operations, thresholds and chart API branch arms
Four more frontend modules with a measured residue their existing specs never reached, including seven functions no test called at all. Deltas are uncovered counts, module scoped, existing specs versus existing plus new. - infrastructureOperationsModel: branches 38 -> 1 uncovered and createSurfaceScopedRow, previously never called, is now covered. This was the largest proportional branch gap left in the frontend. - summaryTableFocus: branches 26 -> 11 uncovered and scheduleViewportRefresh, previously never called, is now covered. - thresholdsResourceModel: hasThresholdDiff, dockerContainerOverrideIdCandidates, findOverrideByCandidates and buildAgentHeaderMeta were all uncalled; covered branches move 114 -> 130. - api/charts: getStorageSummaryCharts and timeRangeToMinutes were uncalled; covered branches move 41 -> 55. The fetch mocking follows the existing specs and asserts parsed results, not that the mock was called. No source file is modified. Adversarial review found no rejects; one subtest duplicating an arm a sibling already covered was removed before committing and the coverage was re-measured unchanged. A fifth candidate, resourceBadgePresentation, was written and discarded rather than landed: 263 tests moved the module by a single branch, because its residual arms are unreachable defensive fallbacks. PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no source or contract change Contract-Neutral: test-only branch coverage, no source or contract change
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Branch-coverage tests for the two currently-uncalled ChartsAPI surface points:
|
||||
* - ChartsAPI.getStorageSummaryCharts (function never invoked by existing specs)
|
||||
* - timeRangeToMinutes (module-private helper, never invoked;
|
||||
* not exported, so exercised solely
|
||||
* through getStorageSummaryCharts by
|
||||
* asserting the resulting ?range=<minutes>
|
||||
* query value for every switch arm)
|
||||
*
|
||||
* Mock harness matches charts.branchcov0718.test.ts / chartsApi.test.ts:
|
||||
* vi.mock('@/utils/apiClient', () => ({ apiFetchJSON: vi.fn() }))
|
||||
* No real network call is made. Nothing already covered by charts.test.ts,
|
||||
* chartsApi.test.ts or charts.branchcov0718.test.ts is re-asserted here.
|
||||
*
|
||||
* Branches exercised:
|
||||
* getStorageSummaryCharts:
|
||||
* - range_ default ('1h') when called with no args
|
||||
* - signal present vs undefined (always wrapped in { signal } either way)
|
||||
* - options undefined entirely -> `node=` omitted
|
||||
* - options.nodeId undefined -> `node=` omitted
|
||||
* - options.nodeId '' -> `node=` omitted (falsy branch)
|
||||
* - options.nodeId truthy string -> `node=<id>` appended
|
||||
* - special chars in nodeId URL-encoded by URLSearchParams.toString
|
||||
* - combined range + node + signal in a single request
|
||||
* - response with EMPTY pools/disks returned verbatim (empty arm)
|
||||
* - fully-populated response returned verbatim
|
||||
* - rejection from apiFetchJSON propagates verbatim (error arm)
|
||||
* timeRangeToMinutes (observed via the ?range=<minutes> value):
|
||||
* - every recognised token: 5m/15m/30m/1h/4h/12h/24h/7d/30d
|
||||
* - default fallback for an UNRECOGNISED token -> 60
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@/utils/apiClient', () => ({
|
||||
apiFetchJSON: vi.fn(),
|
||||
}));
|
||||
|
||||
import { ChartsAPI, type StorageSummaryChartsResponse, type TimeRange } from '@/api/charts';
|
||||
import { apiFetchJSON } from '@/utils/apiClient';
|
||||
|
||||
describe('ChartsAPI.getStorageSummaryCharts — branch coverage', () => {
|
||||
const apiFetchJSONMock = vi.mocked(apiFetchJSON);
|
||||
|
||||
beforeEach(() => {
|
||||
apiFetchJSONMock.mockReset();
|
||||
});
|
||||
|
||||
it('routes to /api/storage-charts with range=60 (default 1h) and signal undefined when called with no args', async () => {
|
||||
// Exercises BOTH the default `range_ = '1h'` parameter AND the default
|
||||
// `signal = undefined` / `options = undefined` branches together.
|
||||
apiFetchJSONMock.mockResolvedValueOnce({} as never);
|
||||
|
||||
await ChartsAPI.getStorageSummaryCharts();
|
||||
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledTimes(1);
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/storage-charts?range=60', {
|
||||
signal: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards an AbortSignal through to apiFetchJSON wrapped in { signal }', async () => {
|
||||
apiFetchJSONMock.mockResolvedValueOnce({} as never);
|
||||
const controller = new AbortController();
|
||||
|
||||
await ChartsAPI.getStorageSummaryCharts('1h', controller.signal);
|
||||
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/storage-charts?range=60', {
|
||||
signal: controller.signal,
|
||||
});
|
||||
});
|
||||
|
||||
it('omits the node query param when options is undefined entirely', async () => {
|
||||
apiFetchJSONMock.mockResolvedValueOnce({} as never);
|
||||
|
||||
await ChartsAPI.getStorageSummaryCharts('1h', undefined, undefined);
|
||||
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/storage-charts?range=60', {
|
||||
signal: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('omits the node query param when options.nodeId is undefined', async () => {
|
||||
apiFetchJSONMock.mockResolvedValueOnce({} as never);
|
||||
|
||||
await ChartsAPI.getStorageSummaryCharts('1h', undefined, { nodeId: undefined });
|
||||
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/storage-charts?range=60', {
|
||||
signal: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('omits the node query param when options.nodeId is an empty string (falsy branch)', async () => {
|
||||
apiFetchJSONMock.mockResolvedValueOnce({} as never);
|
||||
|
||||
await ChartsAPI.getStorageSummaryCharts('1h', undefined, { nodeId: '' });
|
||||
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/storage-charts?range=60', {
|
||||
signal: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('appends node=<id> when options.nodeId is a non-empty string', async () => {
|
||||
apiFetchJSONMock.mockResolvedValueOnce({} as never);
|
||||
|
||||
await ChartsAPI.getStorageSummaryCharts('1h', undefined, { nodeId: 'pve1' });
|
||||
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/storage-charts?range=60&node=pve1', {
|
||||
signal: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('URL-encodes special characters in the node id (URLSearchParams.toString)', async () => {
|
||||
apiFetchJSONMock.mockResolvedValueOnce({} as never);
|
||||
|
||||
await ChartsAPI.getStorageSummaryCharts('1h', undefined, { nodeId: 'node a/b' });
|
||||
|
||||
// space -> '+', '/' -> '%2F'
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/storage-charts?range=60&node=node+a%2Fb', {
|
||||
signal: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('combines range + node + signal in a single request with range-first/node-second ordering', async () => {
|
||||
apiFetchJSONMock.mockResolvedValueOnce({} as never);
|
||||
const controller = new AbortController();
|
||||
|
||||
await ChartsAPI.getStorageSummaryCharts('4h', controller.signal, { nodeId: 'pve1' });
|
||||
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/storage-charts?range=240&node=pve1', {
|
||||
signal: controller.signal,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a payload with EMPTY pools/disks verbatim from apiFetchJSON (empty arm)', async () => {
|
||||
const emptyPayload: StorageSummaryChartsResponse = {
|
||||
pools: {},
|
||||
disks: {},
|
||||
stats: { oldestDataTimestamp: 1733700000000 },
|
||||
};
|
||||
apiFetchJSONMock.mockResolvedValueOnce(emptyPayload as never);
|
||||
|
||||
const result = await ChartsAPI.getStorageSummaryCharts('1h');
|
||||
|
||||
expect(result).toBe(emptyPayload);
|
||||
expect(result.pools).toEqual({});
|
||||
expect(result.disks).toEqual({});
|
||||
});
|
||||
|
||||
it('returns a fully-populated StorageSummaryChartsResponse payload verbatim', async () => {
|
||||
const payload: StorageSummaryChartsResponse = {
|
||||
pools: {
|
||||
'local-zfs': {
|
||||
name: 'local-zfs',
|
||||
usage: [{ timestamp: 1700000000000, value: 42 }],
|
||||
used: [{ timestamp: 1700000000000, value: 2100 }],
|
||||
avail: [{ timestamp: 1700000000000, value: 2900 }],
|
||||
},
|
||||
},
|
||||
disks: {
|
||||
'serial-1': {
|
||||
name: 'sda',
|
||||
node: 'pve1',
|
||||
temperature: [{ timestamp: 1700000000000, value: 38 }],
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
oldestDataTimestamp: 1699900000000,
|
||||
range: '1h',
|
||||
rangeSeconds: 3600,
|
||||
metricsStoreEnabled: true,
|
||||
},
|
||||
};
|
||||
apiFetchJSONMock.mockResolvedValueOnce(payload as never);
|
||||
|
||||
const result = await ChartsAPI.getStorageSummaryCharts('1h');
|
||||
|
||||
expect(result).toBe(payload);
|
||||
expect(result.pools['local-zfs'].used![0].value).toBe(2100);
|
||||
expect(result.disks['serial-1'].temperature[0].value).toBe(38);
|
||||
});
|
||||
|
||||
it('propagates a rejection from apiFetchJSON verbatim (error arm)', async () => {
|
||||
const error = new Error('storage-charts 503');
|
||||
apiFetchJSONMock.mockRejectedValueOnce(error);
|
||||
|
||||
await expect(ChartsAPI.getStorageSummaryCharts('1h')).rejects.toBe(error);
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/storage-charts?range=60', {
|
||||
signal: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeRangeToMinutes — every switch arm (observed via getStorageSummaryCharts)', () => {
|
||||
const apiFetchJSONMock = vi.mocked(apiFetchJSON);
|
||||
|
||||
beforeEach(() => {
|
||||
apiFetchJSONMock.mockReset();
|
||||
});
|
||||
|
||||
// Each entry asserts the specific minute value the helper computes for a
|
||||
// given range token, observed as the `?range=<minutes>` query value. This
|
||||
// is real observable behaviour, not a tautology: the source maps tokens to
|
||||
// distinct magic numbers and we pin each one down.
|
||||
const cases: Array<{ token: TimeRange; minutes: number }> = [
|
||||
{ token: '5m', minutes: 5 },
|
||||
{ token: '15m', minutes: 15 },
|
||||
{ token: '30m', minutes: 30 },
|
||||
{ token: '1h', minutes: 60 },
|
||||
{ token: '4h', minutes: 240 },
|
||||
{ token: '12h', minutes: 720 },
|
||||
{ token: '24h', minutes: 1440 },
|
||||
{ token: '7d', minutes: 10080 },
|
||||
{ token: '30d', minutes: 43200 },
|
||||
];
|
||||
|
||||
it.each(cases)(
|
||||
'maps TimeRange="$token" to range=$minutes minutes in the storage-charts URL',
|
||||
async ({ token, minutes }) => {
|
||||
apiFetchJSONMock.mockResolvedValueOnce({} as never);
|
||||
|
||||
await ChartsAPI.getStorageSummaryCharts(token);
|
||||
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith(`/api/storage-charts?range=${minutes}`, {
|
||||
signal: undefined,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('falls back to range=60 for an UNRECOGNISED range token (default switch arm)', async () => {
|
||||
// '2h' is not in the TimeRange union and not handled by any case in
|
||||
// timeRangeToMinutes -> hits the `default: return 60` branch.
|
||||
const unknownToken = '2h' as unknown as TimeRange;
|
||||
apiFetchJSONMock.mockResolvedValueOnce({} as never);
|
||||
|
||||
await ChartsAPI.getStorageSummaryCharts(unknownToken);
|
||||
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/storage-charts?range=60', {
|
||||
signal: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
+613
@@ -0,0 +1,613 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { AgentCapability } from '@/utils/agentCapabilityPresentation';
|
||||
import type { ConnectedInfrastructureItem, ConnectedInfrastructureSurface } from '@/types/api';
|
||||
import type { UnifiedAgentRow, UnifiedAgentSurface } from '../infrastructureOperationsModel';
|
||||
import {
|
||||
createSurfaceScopedRow,
|
||||
getCapabilityManagementPath,
|
||||
getCapabilitySurfaceLabel,
|
||||
getPlatformConnectionsViewForCapability,
|
||||
getStopMonitoringScopeLabel,
|
||||
getStopMonitoringSurfaces,
|
||||
hasMachineInstallActions,
|
||||
isPlatformConnectionsCapability,
|
||||
rowFromConnectedInfrastructureItem,
|
||||
} from '../infrastructureOperationsModel';
|
||||
|
||||
type SurfaceKey = Parameters<typeof createSurfaceScopedRow>[1];
|
||||
|
||||
const makeRow = (overrides: Partial<UnifiedAgentRow>): UnifiedAgentRow => ({
|
||||
rowKey: 'row-1',
|
||||
id: 'agent-1',
|
||||
name: 'node-a',
|
||||
capabilities: [],
|
||||
status: 'active',
|
||||
upgradePlatform: 'linux',
|
||||
scope: { label: 'Default', category: 'default' },
|
||||
installFlags: [],
|
||||
searchText: '',
|
||||
surfaces: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const surface = (
|
||||
label: string,
|
||||
kind: AgentCapability,
|
||||
overrides: Partial<UnifiedAgentSurface> = {},
|
||||
): UnifiedAgentSurface => ({
|
||||
key: kind,
|
||||
kind,
|
||||
label,
|
||||
detail: '',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const connectedSurface = (
|
||||
kind: ConnectedInfrastructureSurface['kind'],
|
||||
overrides: Partial<ConnectedInfrastructureSurface> = {},
|
||||
): ConnectedInfrastructureSurface => ({
|
||||
id: `surface-${kind}`,
|
||||
kind,
|
||||
label: `${kind} label`,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeItem = (
|
||||
overrides: Partial<ConnectedInfrastructureItem>,
|
||||
): ConnectedInfrastructureItem => ({
|
||||
id: 'agent-1',
|
||||
name: 'node-a',
|
||||
status: 'active',
|
||||
surfaces: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('createSurfaceScopedRow', () => {
|
||||
// A row that carries every surface kind plus every action id / linked node
|
||||
// so each scope block can be distinguished by what it clears vs preserves.
|
||||
const fullRow = makeRow({
|
||||
rowKey: 'host-1',
|
||||
capabilities: ['agent', 'docker', 'kubernetes', 'proxmox', 'pbs', 'pmg', 'truenas'],
|
||||
agentActionId: 'agent-act',
|
||||
dockerActionId: 'docker-act',
|
||||
kubernetesActionId: 'k8s-act',
|
||||
linkedNodeId: 'node-99',
|
||||
surfaces: [
|
||||
surface('Host telemetry', 'agent'),
|
||||
surface('Docker runtime data', 'docker'),
|
||||
surface('Kubernetes cluster data', 'kubernetes'),
|
||||
surface('Proxmox data', 'proxmox'),
|
||||
surface('PBS data', 'pbs'),
|
||||
surface('PMG data', 'pmg'),
|
||||
surface('TrueNAS data', 'truenas'),
|
||||
],
|
||||
});
|
||||
|
||||
it('scopes to docker: clears agent + k8s action ids and the linked node, keeps docker', () => {
|
||||
const scoped = createSurfaceScopedRow(fullRow, 'docker');
|
||||
expect(scoped.rowKey).toBe('host-1-docker-surface');
|
||||
expect(scoped.capabilities).toEqual(['docker']);
|
||||
expect(scoped.agentActionId).toBeUndefined();
|
||||
expect(scoped.kubernetesActionId).toBeUndefined();
|
||||
expect(scoped.linkedNodeId).toBeUndefined();
|
||||
expect(scoped.dockerActionId).toBe('docker-act');
|
||||
expect(scoped.surfaces.map((s) => s.kind)).toEqual(['docker']);
|
||||
});
|
||||
|
||||
it('scopes to kubernetes: clears agent + docker action ids and the linked node, keeps k8s', () => {
|
||||
const scoped = createSurfaceScopedRow(fullRow, 'kubernetes');
|
||||
expect(scoped.rowKey).toBe('host-1-kubernetes-surface');
|
||||
expect(scoped.capabilities).toEqual(['kubernetes']);
|
||||
expect(scoped.agentActionId).toBeUndefined();
|
||||
expect(scoped.dockerActionId).toBeUndefined();
|
||||
expect(scoped.linkedNodeId).toBeUndefined();
|
||||
expect(scoped.kubernetesActionId).toBe('k8s-act');
|
||||
expect(scoped.surfaces.map((s) => s.kind)).toEqual(['kubernetes']);
|
||||
});
|
||||
|
||||
it('scopes to pbs: clears docker + k8s action ids but keeps the agent action id and node', () => {
|
||||
const scoped = createSurfaceScopedRow(fullRow, 'pbs');
|
||||
expect(scoped.rowKey).toBe('host-1-pbs-surface');
|
||||
expect(scoped.capabilities).toEqual(['pbs']);
|
||||
expect(scoped.dockerActionId).toBeUndefined();
|
||||
expect(scoped.kubernetesActionId).toBeUndefined();
|
||||
expect(scoped.agentActionId).toBe('agent-act');
|
||||
expect(scoped.linkedNodeId).toBe('node-99');
|
||||
expect(scoped.surfaces.map((s) => s.kind)).toEqual(['pbs']);
|
||||
});
|
||||
|
||||
it('scopes to pmg: clears docker + k8s action ids but keeps the agent action id and node', () => {
|
||||
const scoped = createSurfaceScopedRow(fullRow, 'pmg');
|
||||
expect(scoped.rowKey).toBe('host-1-pmg-surface');
|
||||
expect(scoped.capabilities).toEqual(['pmg']);
|
||||
expect(scoped.dockerActionId).toBeUndefined();
|
||||
expect(scoped.kubernetesActionId).toBeUndefined();
|
||||
expect(scoped.agentActionId).toBe('agent-act');
|
||||
expect(scoped.linkedNodeId).toBe('node-99');
|
||||
expect(scoped.surfaces.map((s) => s.kind)).toEqual(['pmg']);
|
||||
});
|
||||
|
||||
it('scopes to proxmox via the explicit surfaceKey', () => {
|
||||
const scoped = createSurfaceScopedRow(fullRow, 'proxmox');
|
||||
expect(scoped.rowKey).toBe('host-1-proxmox-surface');
|
||||
expect(scoped.capabilities).toEqual(['proxmox']);
|
||||
expect(scoped.dockerActionId).toBeUndefined();
|
||||
expect(scoped.kubernetesActionId).toBeUndefined();
|
||||
expect(scoped.surfaces.map((s) => s.kind)).toEqual(['proxmox']);
|
||||
});
|
||||
|
||||
it('falls back to the proxmox block for an unrecognised surfaceKey', () => {
|
||||
// 'availability' is not a member of the surfaceKey union, so it misses
|
||||
// every `if` and lands in the default (proxmox) return.
|
||||
const scoped = createSurfaceScopedRow(fullRow, 'availability' as unknown as SurfaceKey);
|
||||
expect(scoped.rowKey).toBe('host-1-proxmox-surface');
|
||||
expect(scoped.capabilities).toEqual(['proxmox']);
|
||||
expect(scoped.surfaces.map((s) => s.kind)).toEqual(['proxmox']);
|
||||
});
|
||||
|
||||
it('scopes to agent with a host-only row: capabilities collapse to just ["agent"]', () => {
|
||||
const scoped = createSurfaceScopedRow(
|
||||
makeRow({
|
||||
capabilities: ['agent'],
|
||||
surfaces: [surface('Host telemetry', 'agent')],
|
||||
}),
|
||||
'agent',
|
||||
);
|
||||
expect(scoped.rowKey).toBe('row-1-agent-surface');
|
||||
expect(scoped.capabilities).toEqual(['agent']);
|
||||
expect(scoped.surfaces.map((s) => s.kind)).toEqual(['agent']);
|
||||
});
|
||||
|
||||
it('scopes to agent with docker present: appends docker to the host-managed set', () => {
|
||||
const scoped = createSurfaceScopedRow(
|
||||
makeRow({
|
||||
capabilities: ['agent', 'docker'],
|
||||
surfaces: [
|
||||
surface('Host telemetry', 'agent'),
|
||||
surface('Docker runtime data', 'docker'),
|
||||
surface('PBS data', 'pbs'),
|
||||
],
|
||||
}),
|
||||
'agent',
|
||||
);
|
||||
expect(scoped.capabilities).toEqual(['agent', 'docker']);
|
||||
// The agent block surfaces filter keeps agent/docker/kubernetes only, so
|
||||
// the pbs surface is dropped even though it exists on the source row.
|
||||
expect(scoped.surfaces.map((s) => s.kind)).toEqual(['agent', 'docker']);
|
||||
});
|
||||
|
||||
it('scopes to agent with kubernetes present (no docker): appends kubernetes only', () => {
|
||||
const scoped = createSurfaceScopedRow(
|
||||
makeRow({
|
||||
capabilities: ['agent', 'kubernetes'],
|
||||
surfaces: [
|
||||
surface('Host telemetry', 'agent'),
|
||||
surface('Kubernetes cluster data', 'kubernetes'),
|
||||
],
|
||||
}),
|
||||
'agent',
|
||||
);
|
||||
expect(scoped.capabilities).toEqual(['agent', 'kubernetes']);
|
||||
expect(scoped.surfaces.map((s) => s.kind)).toEqual(['agent', 'kubernetes']);
|
||||
});
|
||||
|
||||
it('scopes to agent with docker + kubernetes: keeps all three host-managed kinds', () => {
|
||||
const scoped = createSurfaceScopedRow(fullRow, 'agent');
|
||||
expect(scoped.capabilities).toEqual(['agent', 'docker', 'kubernetes']);
|
||||
expect(scoped.dockerActionId).toBeUndefined();
|
||||
expect(scoped.kubernetesActionId).toBeUndefined();
|
||||
// Unlike the docker/kubernetes blocks, the agent block does NOT clear the
|
||||
// host action id or the linked node.
|
||||
expect(scoped.agentActionId).toBe('agent-act');
|
||||
expect(scoped.linkedNodeId).toBe('node-99');
|
||||
expect(scoped.surfaces.map((s) => s.kind)).toEqual(['agent', 'docker', 'kubernetes']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStopMonitoringSurfaces', () => {
|
||||
it('returns only the stop-monitoring surfaces when no agent surface is host-managed (false arm)', () => {
|
||||
// The docker surface has a stop-monitoring action but there is no agent
|
||||
// surface at all, so hostManagedStopApplies is false and the function
|
||||
// returns just the filtered stop-monitoring list instead of expanding.
|
||||
const row = makeRow({
|
||||
surfaces: [
|
||||
surface('Docker runtime data', 'docker', { action: 'stop-monitoring' }),
|
||||
surface('PBS data', 'pbs'),
|
||||
],
|
||||
});
|
||||
expect(getStopMonitoringSurfaces(row).map((s) => s.kind)).toEqual(['docker']);
|
||||
});
|
||||
|
||||
it('returns an empty list when no surface carries a stop-monitoring action', () => {
|
||||
const row = makeRow({
|
||||
surfaces: [
|
||||
surface('Host telemetry', 'agent', { action: 'allow-reconnect' }),
|
||||
surface('Docker runtime data', 'docker', { action: 'allow-reconnect' }),
|
||||
],
|
||||
});
|
||||
expect(getStopMonitoringSurfaces(row)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStopMonitoringScopeLabel', () => {
|
||||
it('falls back to the static copy when there are no stop-monitoring surfaces', () => {
|
||||
const row = makeRow({
|
||||
surfaces: [surface('Host telemetry', 'agent', { action: 'allow-reconnect' })],
|
||||
});
|
||||
expect(getStopMonitoringScopeLabel(row)).toBe('Reporting for this item');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasMachineInstallActions', () => {
|
||||
it('returns false when both ids are undefined', () => {
|
||||
expect(hasMachineInstallActions(makeRow({}))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when both ids are whitespace-only (trim() zeroes them out)', () => {
|
||||
expect(hasMachineInstallActions(makeRow({ agentActionId: ' ', agentId: '\t' }))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when agentActionId is whitespace but agentId is present', () => {
|
||||
expect(hasMachineInstallActions(makeRow({ agentActionId: ' ', agentId: 'agent-1' }))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns true when only agentActionId is present', () => {
|
||||
expect(hasMachineInstallActions(makeRow({ agentActionId: 'uninstall-1' }))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCapabilitySurfaceLabel', () => {
|
||||
it.each<[AgentCapability, string]>([
|
||||
['agent', 'Host telemetry'],
|
||||
['docker', 'Docker runtime data'],
|
||||
['kubernetes', 'Kubernetes cluster data'],
|
||||
['proxmox', 'Proxmox data'],
|
||||
['pbs', 'PBS data'],
|
||||
['pmg', 'PMG data'],
|
||||
['truenas', 'TrueNAS data'],
|
||||
])('returns the dedicated surface label for %s', (capability, label) => {
|
||||
expect(getCapabilitySurfaceLabel(capability)).toBe(label);
|
||||
});
|
||||
|
||||
it('falls through to getAgentCapabilityLabel for an unrecognised capability, yielding undefined', () => {
|
||||
// The default arm delegates to getAgentCapabilityLabel, which itself has no
|
||||
// default case and returns undefined for a value outside its switch.
|
||||
expect(getCapabilitySurfaceLabel('foobar' as unknown as AgentCapability)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlatformConnectionsViewForCapability', () => {
|
||||
it.each<[AgentCapability, 'proxmox' | 'truenas']>([
|
||||
['proxmox', 'proxmox'],
|
||||
['pbs', 'proxmox'],
|
||||
['pmg', 'proxmox'],
|
||||
['truenas', 'truenas'],
|
||||
])('routes %s to the %s connections view', (capability, view) => {
|
||||
expect(getPlatformConnectionsViewForCapability(capability)).toBe(view);
|
||||
});
|
||||
|
||||
it.each<AgentCapability>(['agent', 'docker', 'kubernetes'])(
|
||||
'returns null for the host-managed capability %s (default arm)',
|
||||
(capability) => {
|
||||
expect(getPlatformConnectionsViewForCapability(capability)).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('getCapabilityManagementPath', () => {
|
||||
it.each<AgentCapability>(['agent', 'docker', 'kubernetes'])(
|
||||
'returns null for non-platform host capabilities (%s)',
|
||||
(capability) => {
|
||||
expect(getCapabilityManagementPath(capability)).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('isPlatformConnectionsCapability', () => {
|
||||
it.each<AgentCapability>(['docker', 'kubernetes', 'agent'])(
|
||||
'returns false for the host capability %s',
|
||||
(capability) => {
|
||||
expect(isPlatformConnectionsCapability(capability)).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each<AgentCapability>(['proxmox', 'pbs', 'pmg', 'truenas'])(
|
||||
'returns true for the platform capability %s',
|
||||
(capability) => {
|
||||
expect(isPlatformConnectionsCapability(capability)).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('installFlagsForCapabilities (via rowFromConnectedInfrastructureItem)', () => {
|
||||
const scope = { label: 'Default', category: 'default' as const };
|
||||
|
||||
it('emits the docker host-disable flag pair for a docker-only item', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ surfaces: [connectedSurface('docker')] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.installFlags).toEqual(['--enable-docker', '--disable-host']);
|
||||
});
|
||||
|
||||
it('emits only --enable-kubernetes for a kubernetes-only item', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ surfaces: [connectedSurface('kubernetes')] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.installFlags).toEqual(['--enable-kubernetes']);
|
||||
});
|
||||
|
||||
it('emits the pve proxmox type for a proxmox-only item', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ surfaces: [connectedSurface('proxmox')] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.installFlags).toEqual(['--enable-proxmox', '--proxmox-type pve']);
|
||||
});
|
||||
|
||||
it('prefers proxmox-pve over pbs when both capabilities are present (else-if precedence)', () => {
|
||||
// installFlagsForCapabilities uses `else if (capabilities.includes('pbs'))`,
|
||||
// so a proxmox+pbs item pins to pve and never emits the pbs type.
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({
|
||||
surfaces: [connectedSurface('proxmox'), connectedSurface('pbs')],
|
||||
}),
|
||||
scope,
|
||||
);
|
||||
expect(row.installFlags).toEqual(['--enable-proxmox', '--proxmox-type pve']);
|
||||
});
|
||||
|
||||
it('emits no flags for an agent-only item', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ surfaces: [connectedSurface('agent')] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.installFlags).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rowFromConnectedInfrastructureItem', () => {
|
||||
const scope = { label: 'Default', category: 'default' as const };
|
||||
|
||||
describe('name fallback chain (name || displayName || hostname || id)', () => {
|
||||
it('falls back to displayName when name is empty', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ name: '', displayName: 'Display Name' }),
|
||||
scope,
|
||||
);
|
||||
expect(row.name).toBe('Display Name');
|
||||
});
|
||||
|
||||
it('falls back to hostname when name and displayName are empty', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ name: '', displayName: '', hostname: 'host.internal' }),
|
||||
scope,
|
||||
);
|
||||
expect(row.name).toBe('host.internal');
|
||||
});
|
||||
|
||||
it('falls back to the id when name, displayName and hostname are all empty', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ id: 'fallback-id', name: '', displayName: '', hostname: '' }),
|
||||
scope,
|
||||
);
|
||||
expect(row.name).toBe('fallback-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('rowKey derivation', () => {
|
||||
it('prefixes removed-docker for an ignored item whose first surface is docker', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({
|
||||
id: 'd-1',
|
||||
status: 'ignored',
|
||||
surfaces: [connectedSurface('docker')],
|
||||
}),
|
||||
scope,
|
||||
);
|
||||
expect(row.rowKey).toBe('removed-docker-d-1');
|
||||
});
|
||||
|
||||
it('prefixes removed-k8s for an ignored item whose first surface is kubernetes', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({
|
||||
id: 'k-1',
|
||||
status: 'ignored',
|
||||
surfaces: [connectedSurface('kubernetes')],
|
||||
}),
|
||||
scope,
|
||||
);
|
||||
expect(row.rowKey).toBe('removed-k8s-k-1');
|
||||
});
|
||||
|
||||
it('prefixes removed-host for an ignored item whose first surface is agent', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({
|
||||
id: 'h-1',
|
||||
status: 'ignored',
|
||||
surfaces: [connectedSurface('agent')],
|
||||
}),
|
||||
scope,
|
||||
);
|
||||
expect(row.rowKey).toBe('removed-host-h-1');
|
||||
});
|
||||
|
||||
it('prefixes removed-host for an ignored item with no surfaces', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ id: 'h-2', status: 'ignored', surfaces: [] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.rowKey).toBe('removed-host-h-2');
|
||||
});
|
||||
|
||||
it('uses the kubernetes controlId for a kubernetes-only active item', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({
|
||||
id: 'k-2',
|
||||
surfaces: [connectedSurface('kubernetes', { controlId: 'k8s-ctrl' })],
|
||||
}),
|
||||
scope,
|
||||
);
|
||||
expect(row.rowKey).toBe('k8s-k8s-ctrl');
|
||||
});
|
||||
|
||||
it('falls back to the item id when a kubernetes-only active item has no controlId', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({
|
||||
id: 'k-3',
|
||||
surfaces: [connectedSurface('kubernetes', { controlId: undefined })],
|
||||
}),
|
||||
scope,
|
||||
);
|
||||
expect(row.rowKey).toBe('k8s-k-3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('status mapping', () => {
|
||||
it('maps an ignored item status to the removed unified status', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ status: 'ignored', surfaces: [connectedSurface('agent')] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.status).toBe('removed');
|
||||
});
|
||||
|
||||
it('keeps an active item status as active', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ status: 'active', surfaces: [connectedSurface('agent')] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.status).toBe('active');
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgradePlatform fallback', () => {
|
||||
it('defaults to linux when upgradePlatform is absent', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ surfaces: [connectedSurface('agent')] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.upgradePlatform).toBe('linux');
|
||||
});
|
||||
|
||||
it('preserves an explicit freebsd upgrade platform', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ upgradePlatform: 'freebsd', surfaces: [connectedSurface('agent')] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.upgradePlatform).toBe('freebsd');
|
||||
});
|
||||
});
|
||||
|
||||
describe('agentActionId / agentId fallbacks', () => {
|
||||
it('prefers uninstallAgentId over the agent surface controlId for agentActionId', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({
|
||||
uninstallAgentId: 'uninstall-1',
|
||||
surfaces: [connectedSurface('agent', { controlId: 'agent-ctrl' })],
|
||||
}),
|
||||
scope,
|
||||
);
|
||||
expect(row.agentActionId).toBe('uninstall-1');
|
||||
});
|
||||
|
||||
it('uses the agent surface controlId when uninstallAgentId is absent', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({
|
||||
surfaces: [connectedSurface('agent', { controlId: 'agent-ctrl' })],
|
||||
}),
|
||||
scope,
|
||||
);
|
||||
expect(row.agentActionId).toBe('agent-ctrl');
|
||||
});
|
||||
|
||||
it('prefers scopeAgentId over uninstallAgentId for agentId', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({
|
||||
scopeAgentId: 'scope-1',
|
||||
uninstallAgentId: 'uninstall-1',
|
||||
surfaces: [connectedSurface('agent')],
|
||||
}),
|
||||
scope,
|
||||
);
|
||||
expect(row.agentId).toBe('scope-1');
|
||||
});
|
||||
|
||||
it('falls back to uninstallAgentId for agentId when scopeAgentId is absent', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({
|
||||
uninstallAgentId: 'uninstall-1',
|
||||
surfaces: [connectedSurface('agent')],
|
||||
}),
|
||||
scope,
|
||||
);
|
||||
expect(row.agentId).toBe('uninstall-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('kubernetesInfo', () => {
|
||||
it('builds a kubernetesInfo object when a kubernetes surface is present', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ surfaces: [connectedSurface('kubernetes')] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.kubernetesInfo).toEqual({
|
||||
server: undefined,
|
||||
context: undefined,
|
||||
tokenName: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('leaves kubernetesInfo undefined for a host-only item with no kubernetes', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ surfaces: [connectedSurface('agent')] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.kubernetesInfo).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('surfaceBreakdown label/detail fallbacks', () => {
|
||||
it('substitutes the capability surface label when the surface label is empty', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ surfaces: [connectedSurface('docker', { label: '' })] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.surfaces[0].label).toBe('Docker runtime data');
|
||||
});
|
||||
|
||||
it('defaults an absent detail to an empty string', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({
|
||||
surfaces: [connectedSurface('agent', { label: 'Host', detail: undefined })],
|
||||
}),
|
||||
scope,
|
||||
);
|
||||
expect(row.surfaces[0].detail).toBe('');
|
||||
});
|
||||
|
||||
it('routes an unrecognised surface kind through the agent default of agentCapabilityFromSurfaceKind', () => {
|
||||
// agentCapabilityFromSurfaceKind has a default -> 'agent' arm that is
|
||||
// only reachable when surface.kind is not one of the known union
|
||||
// members. Cast a bogus kind to exercise it; the capability collapses to
|
||||
// 'agent'.
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({
|
||||
surfaces: [
|
||||
connectedSurface('agent', {
|
||||
id: 'bogus',
|
||||
kind: 'not-a-real-kind' as unknown as ConnectedInfrastructureSurface['kind'],
|
||||
label: 'Bogus',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
scope,
|
||||
);
|
||||
expect(row.capabilities).toEqual(['agent']);
|
||||
expect(row.surfaces[0].kind).toBe('agent');
|
||||
});
|
||||
});
|
||||
});
|
||||
+514
@@ -0,0 +1,514 @@
|
||||
import { renderHook } from '@solidjs/testing-library';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
useSummaryPageInteractionState,
|
||||
useSummaryTableFocusBridge,
|
||||
} from '@/components/shared/summaryTableFocus';
|
||||
|
||||
// Branch-coverage tests for the UNCOVERED guard arms of summaryTableFocus.ts.
|
||||
// The existing spec (summaryTableFocus.test.tsx) exercises the happy paths;
|
||||
// this file targets the null/empty/early-return arms that never fire there:
|
||||
// - scheduleViewportRefresh (scroll/resize -> viewportVersion tick) — the
|
||||
// primary measured gap; never invoked by the existing suite.
|
||||
// - activeRow / focusedGroupRow guards: missing container ref, empty id,
|
||||
// querySelector miss, attribute-selector escaping.
|
||||
// - jumpToActiveRow: no-active-id no-op + frame-exhaustion arm.
|
||||
// - shouldShowJumpToActiveRow: empty summary collections + already-visible.
|
||||
// - Escape handler: defaultPrevented, every modifier arm, dialog target.
|
||||
// Every asserted value below is hand-computed against the source in
|
||||
// src/components/shared/summaryTableFocus.ts — no snapshots, no constant-
|
||||
// equals-itself tautologies.
|
||||
|
||||
const buildRect = (top: number, height = 32): DOMRect =>
|
||||
({
|
||||
x: 0,
|
||||
y: top,
|
||||
width: 240,
|
||||
height,
|
||||
top,
|
||||
bottom: top + height,
|
||||
left: 0,
|
||||
right: 240,
|
||||
toJSON: () => ({}),
|
||||
}) as DOMRect;
|
||||
|
||||
describe('summaryTableFocus.branchcov0723pm', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('innerHeight', 800);
|
||||
// Default synchronous rAF (matches the existing spec convention).
|
||||
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
||||
cb(0);
|
||||
return 1;
|
||||
});
|
||||
vi.stubGlobal('cancelAnimationFrame', () => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PRIMARY TARGET: scheduleViewportRefresh
|
||||
// (createEffect at summaryTableFocus.ts:226). The existing spec never
|
||||
// dispatches scroll/resize after mounting a table root, so the entire
|
||||
// effect body — listener registration, the rAF callback that ticks
|
||||
// viewportVersion, the dedup guard, and the cancel-on-cleanup arm — is
|
||||
// uncovered.
|
||||
// -------------------------------------------------------------------------
|
||||
describe('scheduleViewportRefresh', () => {
|
||||
it('ticks viewportVersion via the rAF callback after a scroll event, recomputing isActiveRowVisible', () => {
|
||||
// Deferred rAF: capture the callback so we control when viewportVersion
|
||||
// increments (the synchronous default stub would tick instantly and hide
|
||||
// the dedup arm tested below).
|
||||
const scheduled: FrameRequestCallback[] = [];
|
||||
let nextId = 7;
|
||||
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
||||
scheduled.push(cb);
|
||||
return nextId++;
|
||||
});
|
||||
|
||||
const root = document.createElement('div');
|
||||
const row = document.createElement('div');
|
||||
row.setAttribute('data-summary-series-id', 'workload-a');
|
||||
let rowTop = 1200; // below innerHeight(800) -> not visible
|
||||
Object.defineProperty(row, 'getBoundingClientRect', {
|
||||
configurable: true,
|
||||
value: () => buildRect(rowTop),
|
||||
});
|
||||
root.appendChild(row);
|
||||
document.body.appendChild(root);
|
||||
|
||||
const [hoveredSeriesId] = createSignal<string | null>('workload-a');
|
||||
const { result } = renderHook(() => useSummaryPageInteractionState({ hoveredSeriesId }));
|
||||
result.setTableRootRef(root);
|
||||
|
||||
// Row below the viewport -> jump affordance shown.
|
||||
expect(result.shouldShowJumpToActiveRow()).toBe(true);
|
||||
|
||||
// Slide the row into the viewport. The memo must NOT pick this up yet
|
||||
// because viewportVersion has not ticked (no rAF has fired).
|
||||
rowTop = 64;
|
||||
expect(result.shouldShowJumpToActiveRow()).toBe(true);
|
||||
|
||||
// scroll -> scheduleViewportRefresh schedules exactly one rAF.
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
expect(scheduled).toHaveLength(1);
|
||||
|
||||
// Fire the rAF -> viewportVersion++ -> isActiveRowVisible recomputes ->
|
||||
// row is now in-viewport -> affordance hidden.
|
||||
scheduled[0](0);
|
||||
expect(result.shouldShowJumpToActiveRow()).toBe(false);
|
||||
});
|
||||
|
||||
it('coalesces back-to-back scroll events into a single rAF (rafId !== undefined dedup arm)', () => {
|
||||
const scheduled: FrameRequestCallback[] = [];
|
||||
let nextId = 3;
|
||||
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
||||
scheduled.push(cb);
|
||||
return nextId++;
|
||||
});
|
||||
|
||||
const root = document.createElement('div');
|
||||
document.body.appendChild(root);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryTableFocusBridge({ activeSeriesId: () => null }),
|
||||
);
|
||||
result.setTableRootRef(root);
|
||||
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
|
||||
// Only the first scroll schedules an rAF; subsequent ones hit
|
||||
// `if (rafId !== undefined) return;` while the callback is pending.
|
||||
expect(scheduled).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('refreshes viewportVersion on window resize as well as scroll (resize listener arm)', () => {
|
||||
const scheduled: FrameRequestCallback[] = [];
|
||||
let nextId = 11;
|
||||
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
||||
scheduled.push(cb);
|
||||
return nextId++;
|
||||
});
|
||||
|
||||
const root = document.createElement('div');
|
||||
const row = document.createElement('div');
|
||||
row.setAttribute('data-summary-series-id', 'workload-a');
|
||||
let rowTop = 900;
|
||||
Object.defineProperty(row, 'getBoundingClientRect', {
|
||||
configurable: true,
|
||||
value: () => buildRect(rowTop),
|
||||
});
|
||||
root.appendChild(row);
|
||||
document.body.appendChild(root);
|
||||
|
||||
const [hoveredSeriesId] = createSignal<string | null>('workload-a');
|
||||
const { result } = renderHook(() => useSummaryPageInteractionState({ hoveredSeriesId }));
|
||||
result.setTableRootRef(root);
|
||||
expect(result.shouldShowJumpToActiveRow()).toBe(true);
|
||||
|
||||
rowTop = 50;
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
expect(scheduled).toHaveLength(1);
|
||||
scheduled[0](0);
|
||||
expect(result.shouldShowJumpToActiveRow()).toBe(false);
|
||||
});
|
||||
|
||||
it('cancels the pending rAF id when the owner is disposed (cancelAnimationFrame cleanup arm)', () => {
|
||||
const scheduled: FrameRequestCallback[] = [];
|
||||
const cancelSpy = vi.fn();
|
||||
const pendingId = 42;
|
||||
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
||||
scheduled.push(cb);
|
||||
return pendingId;
|
||||
});
|
||||
vi.stubGlobal('cancelAnimationFrame', cancelSpy);
|
||||
|
||||
const root = document.createElement('div');
|
||||
document.body.appendChild(root);
|
||||
|
||||
const { result, cleanup } = renderHook(() =>
|
||||
useSummaryTableFocusBridge({ activeSeriesId: () => null }),
|
||||
);
|
||||
result.setTableRootRef(root);
|
||||
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
expect(cancelSpy).not.toHaveBeenCalled();
|
||||
|
||||
cleanup();
|
||||
|
||||
// The rAF id returned by the deferred stub (42) is cancelled on dispose.
|
||||
expect(cancelSpy).toHaveBeenCalledWith(pendingId);
|
||||
});
|
||||
|
||||
it('does not attach scroll/resize listeners when tableRoot ref is missing (missing container ref arm)', () => {
|
||||
const rafSpy = vi.fn((cb: FrameRequestCallback) => {
|
||||
cb(0);
|
||||
return 1;
|
||||
});
|
||||
vi.stubGlobal('requestAnimationFrame', rafSpy);
|
||||
|
||||
renderHook(() => useSummaryTableFocusBridge({ activeSeriesId: () => null }));
|
||||
|
||||
// No setTableRootRef -> effect hits `if (!root ...) return;` -> no
|
||||
// listeners -> scroll/resize schedule no rAF.
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
expect(rafSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// activeRow guard arms: missing container ref, empty id, querySelector miss,
|
||||
// attribute-selector escaping.
|
||||
// -------------------------------------------------------------------------
|
||||
describe('activeRow guard arms', () => {
|
||||
it('returns null when the tableRoot ref has never been set (missing container ref)', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryTableFocusBridge({ activeSeriesId: () => 'workload-a' }),
|
||||
);
|
||||
expect(result.activeRow()).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when setTableRootRef receives undefined (setter `element ?? null` right arm)', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryTableFocusBridge({ activeSeriesId: () => 'workload-a' }),
|
||||
);
|
||||
result.setTableRootRef(undefined);
|
||||
expect(result.activeRow()).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for null / whitespace-only active ids and the row for a real id (normalizedActiveSeriesId guard)', () => {
|
||||
const root = document.createElement('div');
|
||||
const row = document.createElement('div');
|
||||
row.setAttribute('data-summary-series-id', 'workload-a');
|
||||
root.appendChild(row);
|
||||
document.body.appendChild(root);
|
||||
|
||||
const [activeSeriesId, setActiveSeriesId] = createSignal<string | null>(null);
|
||||
const { result } = renderHook(() => useSummaryTableFocusBridge({ activeSeriesId }));
|
||||
result.setTableRootRef(root);
|
||||
|
||||
expect(result.activeRow()).toBeNull();
|
||||
setActiveSeriesId(' ');
|
||||
expect(result.activeRow()).toBeNull();
|
||||
setActiveSeriesId('workload-a');
|
||||
expect(result.activeRow()).toBe(row);
|
||||
});
|
||||
|
||||
it('returns null when no descendant carries the matching data attribute (querySelector miss)', () => {
|
||||
const root = document.createElement('div');
|
||||
const decoy = document.createElement('div');
|
||||
decoy.setAttribute('data-summary-series-id', 'other');
|
||||
root.appendChild(decoy);
|
||||
document.body.appendChild(root);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryTableFocusBridge({ activeSeriesId: () => 'workload-a' }),
|
||||
);
|
||||
result.setTableRootRef(root);
|
||||
|
||||
expect(result.activeRow()).toBeNull();
|
||||
});
|
||||
|
||||
it('escapes backslashes and double quotes in the series id before injecting it into the attribute selector', () => {
|
||||
const root = document.createElement('div');
|
||||
const row = document.createElement('div');
|
||||
// Literal id containing both a double-quote and a backslash — the exact
|
||||
// characters escapeAttributeSelectorValue rewrites.
|
||||
row.setAttribute('data-summary-series-id', 'id"with\\special');
|
||||
root.appendChild(row);
|
||||
document.body.appendChild(root);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryTableFocusBridge({ activeSeriesId: () => 'id"with\\special' }),
|
||||
);
|
||||
result.setTableRootRef(root);
|
||||
|
||||
// If escaping were broken the selector would not match the literal id.
|
||||
expect(result.activeRow()).toBe(row);
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// jumpToActiveRow guard arms
|
||||
// -------------------------------------------------------------------------
|
||||
describe('jumpToActiveRow guard arms', () => {
|
||||
it('is a no-op when there is no active series id (normalizedActiveSeriesId falsy -> early return)', () => {
|
||||
const revealActiveSeries = vi.fn();
|
||||
const rafSpy = vi.fn((cb: FrameRequestCallback) => {
|
||||
cb(0);
|
||||
return 1;
|
||||
});
|
||||
vi.stubGlobal('requestAnimationFrame', rafSpy);
|
||||
|
||||
const root = document.createElement('div');
|
||||
document.body.appendChild(root);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryTableFocusBridge({
|
||||
activeSeriesId: () => null,
|
||||
revealActiveSeries,
|
||||
}),
|
||||
);
|
||||
result.setTableRootRef(root);
|
||||
|
||||
result.jumpToActiveRow();
|
||||
|
||||
expect(revealActiveSeries).not.toHaveBeenCalled();
|
||||
expect(rafSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('exhausts the 6-frame retry budget without scrolling when the active row never mounts (no matching row)', () => {
|
||||
const revealActiveSeries = vi.fn();
|
||||
const rafSpy = vi.fn((cb: FrameRequestCallback) => {
|
||||
cb(0);
|
||||
return 1;
|
||||
});
|
||||
vi.stubGlobal('requestAnimationFrame', rafSpy);
|
||||
|
||||
const root = document.createElement('div');
|
||||
document.body.appendChild(root);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryTableFocusBridge({
|
||||
activeSeriesId: () => 'workload-a',
|
||||
revealActiveSeries,
|
||||
}),
|
||||
);
|
||||
result.setTableRootRef(root);
|
||||
|
||||
result.jumpToActiveRow();
|
||||
|
||||
// revealActiveSeries fires once before the scroll retry loop.
|
||||
expect(revealActiveSeries).toHaveBeenCalledWith('workload-a');
|
||||
expect(revealActiveSeries).toHaveBeenCalledTimes(1);
|
||||
// attemptScroll(6) recurses once per synchronous rAF until remainingHits
|
||||
// 0 -> 6 rAF calls total, then the `remainingFrames <= 0` arm returns.
|
||||
expect(rafSpy).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// shouldShowJumpToActiveRow — empty summary collections + already-visible row
|
||||
// -------------------------------------------------------------------------
|
||||
describe('shouldShowJumpToActiveRow arms', () => {
|
||||
it('is false when no hover/focus/group signal resolves an active series (empty summary collections / page-default state)', () => {
|
||||
const root = document.createElement('div');
|
||||
document.body.appendChild(root);
|
||||
|
||||
const { result } = renderHook(() => useSummaryPageInteractionState({}));
|
||||
result.setTableRootRef(root);
|
||||
|
||||
// resolveSummaryScopeState returns { kind: 'page', seriesId: null }.
|
||||
expect(result.activeSeriesId()).toBeNull();
|
||||
expect(result.activeGroupScope()).toBeNull();
|
||||
// Boolean(null) && !isActiveRowVisible() -> false && ... -> false.
|
||||
expect(result.shouldShowJumpToActiveRow()).toBe(false);
|
||||
});
|
||||
|
||||
it('is false when the active row is already inside the viewport (already-focused / visible arm)', () => {
|
||||
const root = document.createElement('div');
|
||||
const row = document.createElement('div');
|
||||
row.setAttribute('data-summary-series-id', 'workload-a');
|
||||
// top=200, height=40, innerHeight=800 -> 0 < 200 and 240 < 800 -> visible.
|
||||
Object.defineProperty(row, 'getBoundingClientRect', {
|
||||
configurable: true,
|
||||
value: () => buildRect(200, 40),
|
||||
});
|
||||
root.appendChild(row);
|
||||
document.body.appendChild(root);
|
||||
|
||||
const [hoveredSeriesId] = createSignal<string | null>('workload-a');
|
||||
const { result } = renderHook(() => useSummaryPageInteractionState({ hoveredSeriesId }));
|
||||
result.setTableRootRef(root);
|
||||
|
||||
expect(result.activeSeriesId()).toBe('workload-a');
|
||||
// Boolean('workload-a') && !true -> false.
|
||||
expect(result.shouldShowJumpToActiveRow()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Focused-group reveal effect (summaryTableFocus.ts:350) — the
|
||||
// already-visible arm skips scrollIntoView; the out-of-viewport arm scrolls.
|
||||
// -------------------------------------------------------------------------
|
||||
describe('focused-group reveal arms', () => {
|
||||
it('does not call scrollIntoView when the focused group row is already visible (already-focused arm)', () => {
|
||||
const scrollIntoView = vi.fn();
|
||||
const root = document.createElement('div');
|
||||
const row = document.createElement('div');
|
||||
row.setAttribute('data-summary-group-id', 'group-a');
|
||||
Object.defineProperty(row, 'getBoundingClientRect', {
|
||||
configurable: true,
|
||||
value: () => buildRect(120, 40), // inside viewport
|
||||
});
|
||||
Object.defineProperty(row, 'scrollIntoView', {
|
||||
configurable: true,
|
||||
value: scrollIntoView,
|
||||
});
|
||||
root.appendChild(row);
|
||||
document.body.appendChild(root);
|
||||
|
||||
const [focusedGroupId] = createSignal<string | null>('group-a');
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryTableFocusBridge({ activeSeriesId: () => null, focusedGroupId }),
|
||||
);
|
||||
result.setTableRootRef(root);
|
||||
|
||||
// `!isElementVisibleWithinViewport(row) && ...` short-circuits on the
|
||||
// left operand when the row is already on-screen.
|
||||
expect(scrollIntoView).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls scrollIntoView with nearest-block when the focused group row sits below the viewport', () => {
|
||||
const scrollIntoView = vi.fn();
|
||||
const root = document.createElement('div');
|
||||
const row = document.createElement('div');
|
||||
row.setAttribute('data-summary-group-id', 'group-a');
|
||||
Object.defineProperty(row, 'getBoundingClientRect', {
|
||||
configurable: true,
|
||||
value: () => buildRect(1200, 40), // below innerHeight(800)
|
||||
});
|
||||
Object.defineProperty(row, 'scrollIntoView', {
|
||||
configurable: true,
|
||||
value: scrollIntoView,
|
||||
});
|
||||
root.appendChild(row);
|
||||
document.body.appendChild(root);
|
||||
|
||||
const [focusedGroupId] = createSignal<string | null>('group-a');
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryTableFocusBridge({ activeSeriesId: () => null, focusedGroupId }),
|
||||
);
|
||||
result.setTableRootRef(root);
|
||||
|
||||
expect(scrollIntoView).toHaveBeenCalledWith({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
});
|
||||
});
|
||||
|
||||
it('retries via rAF then stops when the focused group row never mounts (no matching group row -> remainingFrames exhausted)', () => {
|
||||
const rafSpy = vi.fn((cb: FrameRequestCallback) => {
|
||||
cb(0);
|
||||
return 1;
|
||||
});
|
||||
vi.stubGlobal('requestAnimationFrame', rafSpy);
|
||||
|
||||
const root = document.createElement('div');
|
||||
document.body.appendChild(root);
|
||||
|
||||
const [focusedGroupId] = createSignal<string | null>('group-a');
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryTableFocusBridge({ activeSeriesId: () => null, focusedGroupId }),
|
||||
);
|
||||
result.setTableRootRef(root);
|
||||
|
||||
// Initial rAF + one per decrement: remainingFrames 12->0 = 13 calls.
|
||||
expect(rafSpy).toHaveBeenCalledTimes(13);
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Escape handler guard arms (summaryTableFocus.ts:174). The existing spec
|
||||
// only covers the happy path; every short-circuit in the `||` chain and the
|
||||
// dialog-target guard are uncovered.
|
||||
// -------------------------------------------------------------------------
|
||||
describe('escape handler guard arms', () => {
|
||||
it('does not invoke onEscapeClear when the event default is already prevented', () => {
|
||||
const onEscapeClear = vi.fn();
|
||||
renderHook(() => useSummaryPageInteractionState({ onEscapeClear }));
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
event.preventDefault();
|
||||
document.dispatchEvent(event);
|
||||
|
||||
expect(onEscapeClear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['altKey', { altKey: true }],
|
||||
['ctrlKey', { ctrlKey: true }],
|
||||
['metaKey', { metaKey: true }],
|
||||
['shiftKey', { shiftKey: true }],
|
||||
] as const)(
|
||||
'ignores Escape pressed while %s is held (modifier short-circuit arm)',
|
||||
(_label, modifiers) => {
|
||||
const onEscapeClear = vi.fn();
|
||||
renderHook(() => useSummaryPageInteractionState({ onEscapeClear }));
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, ...modifiers }),
|
||||
);
|
||||
expect(onEscapeClear).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('ignores Escape dispatched from inside an open dialog (dialog-target guard)', () => {
|
||||
const onEscapeClear = vi.fn();
|
||||
renderHook(() => useSummaryPageInteractionState({ onEscapeClear }));
|
||||
|
||||
const dialog = document.createElement('div');
|
||||
dialog.setAttribute('role', 'dialog');
|
||||
document.body.appendChild(dialog);
|
||||
dialog.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
||||
|
||||
expect(onEscapeClear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores non-Escape keys (event.key !== "Escape" arm)', () => {
|
||||
const onEscapeClear = vi.fn();
|
||||
renderHook(() => useSummaryPageInteractionState({ onEscapeClear }));
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
||||
expect(onEscapeClear).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { Resource } from '@/types/resource';
|
||||
|
||||
import type { Override } from '../types';
|
||||
import {
|
||||
buildAgentHeaderMeta,
|
||||
dockerContainerOverrideIdCandidates,
|
||||
findOverrideByCandidates,
|
||||
hasThresholdDiff,
|
||||
} from '../thresholdsResourceModel';
|
||||
|
||||
/**
|
||||
* Build a minimal Resource fixture. `as unknown as Resource` keeps strict
|
||||
* TypeScript clean while letting us exercise the runtime guards with
|
||||
* deliberately partial / malformed payloads.
|
||||
*/
|
||||
const makeAgent = (overrides: Partial<Resource> = {}): Resource =>
|
||||
({
|
||||
id: 'agent-id',
|
||||
type: 'agent',
|
||||
name: 'agent-name',
|
||||
displayName: 'agent-display',
|
||||
platformId: 'platform-id',
|
||||
platformType: 'generic',
|
||||
sourceType: 'agent',
|
||||
status: 'online',
|
||||
lastSeen: 0,
|
||||
...overrides,
|
||||
}) as unknown as Resource;
|
||||
|
||||
const makeOverride = (id: string, thresholds: Override['thresholds'] = {}): Override =>
|
||||
({
|
||||
id,
|
||||
name: id,
|
||||
type: 'guest',
|
||||
thresholds,
|
||||
}) as Override;
|
||||
|
||||
describe('thresholdsResourceModel branch coverage 0723pm', () => {
|
||||
describe('hasThresholdDiff', () => {
|
||||
it('returns false when the override is undefined (?.thresholds short-circuit)', () => {
|
||||
expect(hasThresholdDiff(undefined, { cpu: 80 })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when every threshold equals its default', () => {
|
||||
const override = makeOverride('a', { cpu: 80, memory: 70 });
|
||||
expect(hasThresholdDiff(override, { cpu: 80, memory: 70 })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when one threshold diverges while a sibling matches', () => {
|
||||
const override = makeOverride('a', { cpu: 80, memory: 90 });
|
||||
expect(hasThresholdDiff(override, { cpu: 80, memory: 70 })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when the first keyed threshold already diverges (some early-exit)', () => {
|
||||
const override = makeOverride('a', { cpu: 99, memory: 99 });
|
||||
expect(hasThresholdDiff(override, { cpu: 80, memory: 70 })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for an empty thresholds object (some() over an empty key list)', () => {
|
||||
const override = makeOverride('a', {});
|
||||
expect(hasThresholdDiff(override, { cpu: 80 })).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores keys whose override value is undefined even when a default exists', () => {
|
||||
// The predicate's `!== undefined` guard must filter out absent values.
|
||||
const override = makeOverride('a', { cpu: undefined });
|
||||
expect(hasThresholdDiff(override, { cpu: 80 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dockerContainerOverrideIdCandidates', () => {
|
||||
it('prefixes every host candidate as docker:<hostId>/<shortId>, preserving order', () => {
|
||||
const host = makeAgent({
|
||||
id: 'runtime-1',
|
||||
discoveryTarget: {
|
||||
resourceType: 'app-container',
|
||||
resourceId: 'container-123',
|
||||
agentId: 'agent-7',
|
||||
},
|
||||
platformData: {
|
||||
docker: { hostSourceId: 'docker-platform' },
|
||||
hostSourceId: 'host-platform',
|
||||
},
|
||||
});
|
||||
expect(dockerContainerOverrideIdCandidates(host, 'a1b2c3d4e5f6')).toEqual([
|
||||
'docker:container-123/a1b2c3d4e5f6',
|
||||
'docker:docker-platform/a1b2c3d4e5f6',
|
||||
'docker:host-platform/a1b2c3d4e5f6',
|
||||
'docker:agent-7/a1b2c3d4e5f6',
|
||||
'docker:runtime-1/a1b2c3d4e5f6',
|
||||
]);
|
||||
});
|
||||
|
||||
it('produces a single prefixed entry when the host exposes only its resource id', () => {
|
||||
const host = makeAgent({ id: 'lone-host' });
|
||||
expect(dockerContainerOverrideIdCandidates(host, 'deadbeef')).toEqual([
|
||||
'docker:lone-host/deadbeef',
|
||||
]);
|
||||
});
|
||||
|
||||
it('falls back to platformData.docker.hostSourceId when discoveryTarget is absent', () => {
|
||||
const host = makeAgent({
|
||||
id: 'host-1',
|
||||
platformData: { docker: { hostSourceId: 'docker-src' } },
|
||||
});
|
||||
expect(dockerContainerOverrideIdCandidates(host, 'cid')).toEqual([
|
||||
'docker:docker-src/cid',
|
||||
'docker:host-1/cid',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns an empty array when the host has no usable id signal at all', () => {
|
||||
// resource.id is whitespace -> readString drops it -> uniqueIds yields [].
|
||||
const host = makeAgent({ id: ' ' });
|
||||
expect(dockerContainerOverrideIdCandidates(host, 'cid')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOverrideByCandidates', () => {
|
||||
it('returns the override held under the first candidate (early return)', () => {
|
||||
const first = makeOverride('first', { cpu: 1 });
|
||||
const map = new Map<string, Override>([
|
||||
['c1', first],
|
||||
['c2', makeOverride('second', { cpu: 2 })],
|
||||
]);
|
||||
expect(findOverrideByCandidates(map, ['c1', 'c2'])).toBe(first);
|
||||
});
|
||||
|
||||
it('skips missing candidates and returns the first hit further down the list', () => {
|
||||
const later = makeOverride('later', { cpu: 5 });
|
||||
const map = new Map<string, Override>([['c3', later]]);
|
||||
expect(findOverrideByCandidates(map, ['c1', 'c2', 'c3'])).toBe(later);
|
||||
});
|
||||
|
||||
it('returns undefined when no candidate is present in the map', () => {
|
||||
const map = new Map<string, Override>([['unrelated', makeOverride('x')]]);
|
||||
expect(findOverrideByCandidates(map, ['c1', 'c2'])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for an empty candidate list (loop body never executes)', () => {
|
||||
const map = new Map<string, Override>([['c1', makeOverride('x')]]);
|
||||
expect(findOverrideByCandidates(map, [])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('skips a candidate whose map value is explicitly undefined (truthy guard)', () => {
|
||||
// Map.get yields undefined for both missing keys and keys set to undefined;
|
||||
// the `if (override)` guard must treat them identically and keep scanning.
|
||||
const real = makeOverride('real', { cpu: 9 });
|
||||
const map = new Map<string, Override | undefined>([
|
||||
['hole', undefined],
|
||||
['real', real],
|
||||
]) as unknown as Map<string, Override>;
|
||||
expect(findOverrideByCandidates(map, ['hole', 'real'])).toBe(real);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildAgentHeaderMeta', () => {
|
||||
it('uses identity.hostname for rawName and collects distinct display/hostname/id keys', () => {
|
||||
const agent = makeAgent({
|
||||
id: 'agent-7',
|
||||
name: 'titan-host',
|
||||
displayName: 'Titan Server',
|
||||
status: 'warning',
|
||||
identity: { hostname: 'titan.example.com' },
|
||||
});
|
||||
const { headerMeta, keys } = buildAgentHeaderMeta(agent);
|
||||
expect(headerMeta.type).toBe('agent');
|
||||
expect(headerMeta.status).toBe('warning');
|
||||
expect(headerMeta.rawName).toBe('titan.example.com');
|
||||
expect(headerMeta.displayName).toBe('Titan Server');
|
||||
expect([...keys]).toEqual(['Titan Server', 'titan.example.com', 'agent-7']);
|
||||
});
|
||||
|
||||
it('falls back to the trimmed agent.name for rawName when no hostname signal is present', () => {
|
||||
const agent = makeAgent({
|
||||
id: 'agent-2',
|
||||
name: 'box-2',
|
||||
displayName: undefined,
|
||||
});
|
||||
const { headerMeta, keys } = buildAgentHeaderMeta(agent);
|
||||
// getPreferredResourceHostname bottoms out at asTrimmedString(resource.name).
|
||||
expect(headerMeta.rawName).toBe('box-2');
|
||||
// displayName collapses onto the same value, so keys dedupe to two entries.
|
||||
expect(headerMeta.displayName).toBe('box-2');
|
||||
expect([...keys]).toEqual(['box-2', 'agent-2']);
|
||||
});
|
||||
|
||||
it('falls back to agent.id for rawName when both hostname and name are absent', () => {
|
||||
const agent = makeAgent({
|
||||
id: 'agent-3',
|
||||
name: '',
|
||||
displayName: undefined,
|
||||
platformId: undefined,
|
||||
});
|
||||
const { headerMeta, keys } = buildAgentHeaderMeta(agent);
|
||||
expect(headerMeta.rawName).toBe('agent-3');
|
||||
expect(headerMeta.displayName).toBe('agent-3');
|
||||
expect([...keys]).toEqual(['agent-3']);
|
||||
});
|
||||
|
||||
it('uses a whitespace-only name for rawName but keeps it out of the keys set', () => {
|
||||
// asTrimmedString drops the whitespace name (and platformId is cleared),
|
||||
// so getPreferredResourceHostname is undefined and the `|| agent.name` arm
|
||||
// fires, making rawName the raw " " string. The keys loop then skips it
|
||||
// via its `value.trim()` guard.
|
||||
const agent = makeAgent({
|
||||
id: 'real-id',
|
||||
name: ' ',
|
||||
displayName: undefined,
|
||||
platformId: undefined,
|
||||
});
|
||||
const { headerMeta, keys } = buildAgentHeaderMeta(agent);
|
||||
expect(headerMeta.rawName).toBe(' ');
|
||||
expect([...keys]).toEqual(['real-id']);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user