diff --git a/frontend-modern/src/components/Settings/__tests__/availabilitySettingsModel.branchcov0724pm.test.ts b/frontend-modern/src/components/Settings/__tests__/availabilitySettingsModel.branchcov0724pm.test.ts new file mode 100644 index 0000000000..359d91352c --- /dev/null +++ b/frontend-modern/src/components/Settings/__tests__/availabilitySettingsModel.branchcov0724pm.test.ts @@ -0,0 +1,122 @@ +/** + * Branch-coverage tests for availabilitySettingsModel, targeting the arms left + * cold by availabilitySettingsModel.test.ts and availabilitySettingsModel.branchcov2.test.ts. + * + * Scoped coverage against those two existing specs leaves exactly five branches + * uncovered, all reachable from the public surface: + * - the 'udp' switch case in getAvailabilityTargetMethodLabel (both the + * with-port and without-port ternary arms inside it), + * - the `status.outcome === 'indeterminate'` consequent in + * getAvailabilityTargetStatusLabel ('Open or filtered'), + * - the `status.outcome === 'indeterminate'` consequent in + * getAvailabilityTargetStatusClass (amber badge), + * - the empty-targets consequent in getAvailabilityTargetsSummary + * ('No availability checks configured'), + * - the fall-through (no down, no indeterminate) to the final default return + * in getAvailabilityTargetsSummary ('N enabled · M total'). + * + * Fixture builders and import style mirror the sibling branchcov2 file. + */ +import { describe, expect, it } from 'vitest'; +import { + getAvailabilityTargetMethodLabel, + getAvailabilityTargetStatusClass, + getAvailabilityTargetStatusLabel, + getAvailabilityTargetsSummary, +} from '../availabilitySettingsModel'; +import type { AvailabilityProbeStatus, AvailabilityTarget } from '@/api/availabilityTargets'; + +// ---- Fixtures --------------------------------------------------------------- +// Mirrors the sibling test factories so the three files stay consistent. + +const target = (overrides: Partial = {}): AvailabilityTarget => ({ + id: 'mqtt-broker', + name: 'MQTT broker', + address: 'mqtt.local', + protocol: 'tcp', + port: 1883, + enabled: true, + ...overrides, +}); + +const status = (overrides: Partial = {}): AvailabilityProbeStatus => ({ + targetId: 'mqtt-broker', + name: 'MQTT broker', + address: 'mqtt.local', + protocol: 'tcp', + enabled: true, + available: true, + ...overrides, +}); + +// ---- getAvailabilityTargetMethodLabel -------------------------------------- +// The 'udp' switch case is the only protocol arm never entered by the sibling +// specs. Drive both ternary operands inside it (port present vs absent/0). + +describe('getAvailabilityTargetMethodLabel (udp arm)', () => { + it("includes the port for a udp target with one ('UDP ' ternary arm)", () => { + expect(getAvailabilityTargetMethodLabel(target({ protocol: 'udp', port: 27015 }))).toBe( + 'UDP 27015', + ); + }); + + it("returns 'UDP port' for a udp target without a port (port falsy ternary arm)", () => { + expect(getAvailabilityTargetMethodLabel(target({ protocol: 'udp', port: undefined }))).toBe( + 'UDP port', + ); + }); +}); + +// ---- getAvailabilityTargetStatusLabel -------------------------------------- +// The indeterminate-outcome consequent short-circuits before the available / +// lastError arms and was never hit. outcome takes priority over `available`, so +// confirm it wins even when `available` is false. + +describe('getAvailabilityTargetStatusLabel (indeterminate arm)', () => { + it("returns 'Open or filtered' when outcome is indeterminate (before the available branch)", () => { + expect( + getAvailabilityTargetStatusLabel( + target({ + status: status({ available: false, outcome: 'indeterminate' }), + }), + ), + ).toBe('Open or filtered'); + }); +}); + +// ---- getAvailabilityTargetStatusClass -------------------------------------- +// The indeterminate-outcome consequent returns the amber badge and was never +// hit. Like the label helper it is checked before the available branch. + +describe('getAvailabilityTargetStatusClass (indeterminate arm)', () => { + it('returns the amber badge classes for an indeterminate outcome', () => { + expect( + getAvailabilityTargetStatusClass( + target({ + status: status({ available: false, outcome: 'indeterminate' }), + }), + ), + ).toBe('bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300'); + }); +}); + +// ---- getAvailabilityTargetsSummary ----------------------------------------- +// Two cold arms live here: the empty-input early return, and the final default +// return reached only when no target is down and none are indeterminate. + +describe('getAvailabilityTargetsSummary (empty + all-healthy arms)', () => { + it("returns 'No availability checks configured' for an empty list (length === 0 arm)", () => { + expect(getAvailabilityTargetsSummary([])).toBe('No availability checks configured'); + }); + + it("returns the default ' enabled · total' when nothing is down or indeterminate (final fall-through arm)", () => { + // One online target and one paused target: enabled=1, down=0, indeterminate=0, + // total=2 -> falls through every guard to the default return. + expect( + getAvailabilityTargetsSummary([ + target({ id: 'online', status: status({ targetId: 'online', available: true }) }), + target({ id: 'paused', enabled: false }), + ]), + ).toBe('1 enabled · 2 total'); + }); +}); diff --git a/frontend-modern/src/components/Settings/__tests__/connectionsTableModel.branchcov0724pm.test.ts b/frontend-modern/src/components/Settings/__tests__/connectionsTableModel.branchcov0724pm.test.ts new file mode 100644 index 0000000000..a349fbc963 --- /dev/null +++ b/frontend-modern/src/components/Settings/__tests__/connectionsTableModel.branchcov0724pm.test.ts @@ -0,0 +1,255 @@ +import { describe, expect, it } from 'vitest'; +import type { + Connection, + ConnectionAgentModuleStatus, + ConnectionFleetGovernance, +} from '@/api/connections'; +import { + fleetGovernanceSignalsForConnection, + surfaceLabel, + visibleFleetGovernanceSignals, + type FleetGovernanceSignal, + type FleetGovernanceSignalKey, +} from '../connectionsTableModel'; + +// ---- Fixtures --------------------------------------------------------------- +// Mirrors the sibling coverage tests so the private signal builders below are +// driven through the single exported orchestrator (fleetGovernanceSignalsForConnection). + +const connectionFixture = (overrides: Partial = {}): Connection => ({ + id: 'agent:host-1', + type: 'agent', + name: 'host-1', + address: 'host-1', + state: 'active', + stateReason: '', + enabled: true, + surfaces: ['host'], + scope: { host: true }, + lastSeen: '2026-04-23T12:00:00Z', + lastError: null, + source: 'agent', + capabilities: { supportsPause: false, supportsScope: false, supportsTest: false }, + ...overrides, +}); + +const fleetFixture = ( + overrides: Partial = {}, +): ConnectionFleetGovernance => ({ + enrollmentState: 'enrolled', + livenessState: 'active', + versionDrift: 'current', + adapterHealth: 'healthy', + configRollout: 'reported', + credentialStatus: 'verified', + updateStatus: 'current', + remoteControl: 'disabled', + ...overrides, +}); + +const signalByKey = ( + connection: Connection, + key: FleetGovernanceSignalKey, +): FleetGovernanceSignal | undefined => + fleetGovernanceSignalsForConnection(connection).find((signal) => signal.key === key); + +const agentModule = ( + overrides: Partial = {}, +): ConnectionAgentModuleStatus => ({ + name: 'host', + enabled: true, + state: 'retrying', + updatedAt: '2026-07-09T12:00:00Z', + ...overrides, +}); + +// ---- surfaceLabel (private fallback arm) ------------------------------------ +// +// surfaceLabel returns SURFACE_LABELS[key] ?? key. The sibling suites exercise +// it only through the orchestrator paths with known keys; the cold arm is the +// `?? key` fallback for an unregistered surface. + +describe('surfaceLabel', () => { + it('falls back to the raw key when the surface is unregistered (?? key arm)', () => { + expect(surfaceLabel('totallyUnknown')).toBe('totallyUnknown'); + }); +}); + +// ---- commandPolicyFromFleet (private) — remoteControl "disconnected" -------- +// +// commandPolicyFromFleet switches on remoteControl when no explicit commandPolicy +// is present. The 'disconnected' arm was the only un-exercised case: it derives a +// blocked policy that commandPolicySignal then renders as 'Remote control blocked'. + +describe('commandPolicyFromFleet (via fleetGovernanceSignalsForConnection)', () => { + it('derives a blocked command policy from remoteControl disconnected', () => { + const signal = signalByKey( + connectionFixture({ fleet: fleetFixture({ remoteControl: 'disconnected' }) }), + 'command-policy', + ); + expect(signal).toMatchObject({ + key: 'command-policy', + label: 'Remote control blocked', + detail: 'The agent reports command execution enabled, but no command channel is connected.', + tone: 'critical', + }); + }); +}); + +// ---- commandPolicySignal (private) — explicit status "unknown" -------------- +// +// commandPolicySignal first branches on enforcement (drifted/pending), then +// switches on status. The derived path can never produce status 'unknown' with a +// non-pending enforcement, so the switch's 'unknown' arm is only reachable +// through an explicit fleet.commandPolicy override whose enforcement is neither +// drifted nor pending. + +describe('commandPolicySignal (via fleetGovernanceSignalsForConnection)', () => { + it('surfaces an explicit unknown command-policy status as a warning when enforcement is neutral', () => { + // enforcement is omitted (undefined) so neither the drifted nor pending + // short-circuit fires; the status switch reaches its 'unknown' arm. + const signal = signalByKey( + connectionFixture({ + fleet: fleetFixture({ + remoteControl: 'enabled', + commandPolicy: { status: 'unknown' }, + }), + }), + 'command-policy', + ); + expect(signal).toMatchObject({ + key: 'command-policy', + label: 'Remote control unknown', + detail: 'Pulse has not confirmed command-policy state yet.', + tone: 'warning', + }); + }); +}); + +// ---- updateSignal (private) — failed fallback detail ------------------------ +// +// updateSignal's 'failed' arm uses `update?.lastError || `. The +// sibling suite always supplies a populated agentUpdate.lastError for the failed +// case, so the `|| fallback` arm is cold. It fires when the agent has no update +// object at all, or when lastError is blank. + +describe('updateSignal (via fleetGovernanceSignalsForConnection)', () => { + it('uses the generic update-failed detail when no agentUpdate object is present', () => { + const signal = signalByKey( + connectionFixture({ fleet: fleetFixture({ updateStatus: 'failed' }) }), + 'updates', + ); + expect(signal).toMatchObject({ + key: 'updates', + label: 'Agent update failed', + detail: 'The agent could not complete its latest update check or update attempt.', + tone: 'critical', + }); + }); + + it('falls back to the generic detail when agentUpdate.lastError is an empty string', () => { + const signal = signalByKey( + connectionFixture({ + fleet: fleetFixture({ updateStatus: 'failed' }), + agentUpdate: { state: 'error', autoUpdate: false, lastError: '' }, + }), + 'updates', + ); + expect(signal?.detail).toBe( + 'The agent could not complete its latest update check or update attempt.', + ); + }); +}); + +// ---- moduleHealthSignal (private) — displayName ternary arms ----------------- +// +// moduleHealthSignal maps module.name to a display name via a nested ternary +// (docker/kubernetes/host/). The sibling suites cover the docker and +// kubernetes names; the remaining arms (host + the raw fallback) are cold. + +describe('moduleHealthSignal (via fleetGovernanceSignalsForConnection)', () => { + it('prettifies a failing host module to "Host" and uses the generic detail when lastError is absent', () => { + const signal = signalByKey( + connectionFixture({ + agentModules: [agentModule({ name: 'host', state: 'retrying' })], + fleet: fleetFixture(), + }), + 'module-health', + ); + expect(signal).toMatchObject({ + key: 'module-health', + label: 'Host module retrying', + tone: 'warning', + }); + expect(signal?.detail).toBe('Host monitoring is enabled but the module is not running yet.'); + }); + + it('uses the raw module name for a non-canonical module', () => { + const signal = signalByKey( + connectionFixture({ + agentModules: [agentModule({ name: 'backup-collector', state: 'starting' })], + fleet: fleetFixture(), + }), + 'module-health', + ); + expect(signal?.label).toBe('backup-collector module starting'); + expect(signal?.detail).toBe( + 'backup-collector monitoring is enabled but the module is not running yet.', + ); + }); +}); + +// ---- isPassiveAgentRolloutConfirmationFallbackSignal (private) -------------- +// +// This guard returns true for a rollout warning whose detail contains EITHER of +// two confirmation phrases, but only when a passive config-confirmation signal +// is also present. The sibling suites cover the first phrase +// ("staged rollout is waiting for confirmation"); the second phrase +// ("rollout state cannot be confirmed without comparable desired and applied +// agent config fingerprints") is cold. Driving it through +// visibleFleetGovernanceSignals asserts the observable effect: such a rollout +// signal is filtered out as a passive handshake. + +describe('isPassiveAgentRolloutConfirmationFallbackSignal (via visibleFleetGovernanceSignals)', () => { + it('hides a rollout signal whose detail carries the second confirmation phrase', () => { + const signals: FleetGovernanceSignal[] = [ + { + key: 'config-drift', + label: 'Config pending', + detail: 'Pulse has not received a comparable applied agent configuration fingerprint yet.', + tone: 'warning', + }, + { + key: 'rollout', + label: 'Rollout pending', + detail: + 'rollout state cannot be confirmed without comparable desired and applied agent config fingerprints', + tone: 'warning', + }, + ]; + // Both signals are passive handshakes, so neither surfaces as a chip. + expect(visibleFleetGovernanceSignals(signals)).toEqual([]); + }); + + it('keeps a rollout warning visible when its detail matches neither confirmation phrase', () => { + const signals: FleetGovernanceSignal[] = [ + { + key: 'config-drift', + label: 'Config pending', + detail: 'Pulse has not received a comparable applied agent configuration fingerprint yet.', + tone: 'warning', + }, + { + key: 'rollout', + label: 'Rollout pending', + detail: 'The canary batch for this rollout is stalled.', + tone: 'warning', + }, + ]; + // The config-drift handshake is hidden, but the rollout carries an + // actionable, non-handshake reason and must remain visible. + expect(visibleFleetGovernanceSignals(signals).map((signal) => signal.label)).toEqual([ + 'Rollout pending', + ]); + }); +}); diff --git a/frontend-modern/src/components/Storage/__tests__/diskResourceUtils.branchcov0724pm.test.ts b/frontend-modern/src/components/Storage/__tests__/diskResourceUtils.branchcov0724pm.test.ts new file mode 100644 index 0000000000..f1949b1eb6 --- /dev/null +++ b/frontend-modern/src/components/Storage/__tests__/diskResourceUtils.branchcov0724pm.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest'; +import type { Resource } from '@/types/resource'; +import { + getPhysicalDiskNodeIdentity, + matchesPhysicalDiskNode, + resolvePhysicalDiskMetricResourceId, +} from '@/components/Storage/diskResourceUtils'; + +const buildDisk = (overrides: Partial = {}): Resource => + ({ + id: 'disk-1', + type: 'physical_disk', + name: 'disk-1', + displayName: 'disk-1', + platformId: 'cluster-main', + platformType: 'proxmox-pve', + sourceType: 'api', + status: 'online', + lastSeen: Date.now(), + platformData: { + proxmox: { nodeName: 'tower', instance: 'cluster-main' }, + physicalDisk: { devPath: '/dev/sda' }, + }, + identity: { hostname: 'tower' }, + canonicalIdentity: { hostname: 'tower' }, + ...overrides, + }) as Resource; + +describe('diskResourceUtils branch coverage', () => { + describe('getPhysicalDiskNodeIdentity node fallback chain', () => { + // L24: `resource.platformData || {}` right-hand fallback — platformData falsy. + it('falls back to an empty platformData record and resolves the node from identity', () => { + const disk = buildDisk({ platformData: undefined }); + // No proxmox block survives the fallback, so identity.hostname is the + // only node evidence and the instance collapses to ''. + expect(getPhysicalDiskNodeIdentity(disk)).toEqual({ node: 'tower', instance: '' }); + }); + + // L28: proxmox.nodeName absent -> identity?.hostname arm. + it('derives the node from resource identity hostname when proxmox omits it', () => { + const disk = buildDisk({ + platformData: { physicalDisk: { devPath: '/dev/sda' } } as Resource['platformData'], + identity: { hostname: 'host-from-identity' }, + }); + expect(getPhysicalDiskNodeIdentity(disk)).toEqual({ + node: 'host-from-identity', + instance: '', + }); + }); + + // L28: proxmox.nodeName + identity absent -> canonicalIdentity?.hostname arm. + it('falls back to canonicalIdentity hostname when proxmox and identity are absent', () => { + const disk = buildDisk({ + platformData: { physicalDisk: { devPath: '/dev/sda' } } as Resource['platformData'], + identity: undefined, + canonicalIdentity: { hostname: 'canon-host' }, + }); + expect(getPhysicalDiskNodeIdentity(disk)).toEqual({ node: 'canon-host', instance: '' }); + }); + + // L28: every hostname source absent -> final `|| ''` arm. + it('returns an empty node when no hostname evidence exists anywhere', () => { + const disk = buildDisk({ + platformData: { physicalDisk: { devPath: '/dev/sda' } } as Resource['platformData'], + identity: undefined, + canonicalIdentity: undefined, + }); + expect(getPhysicalDiskNodeIdentity(disk)).toEqual({ node: '', instance: '' }); + }); + }); + + describe('matchesPhysicalDiskNode', () => { + // L40: `target.id && disk.parentId === target.id` -> early `return true`. + it('short-circuits to a match when the disk parentId equals the target id', () => { + // A name that would NOT match by node identity, proving the parentId + // early-return path is the one taken. + const disk = buildDisk({ parentId: 'node-1' }); + expect(matchesPhysicalDiskNode(disk, { id: 'node-1', name: 'unrelated-node-name' })).toBe( + true, + ); + }); + }); + + describe('resolvePhysicalDiskMetricResourceId', () => { + // L82 ternary false arm + L84 `!agentId` early return: no candidate node. + it('returns null when the node collection is empty and no agent can be linked', () => { + expect(resolvePhysicalDiskMetricResourceId(buildDisk(), [], '/dev/sda')).toBeNull(); + }); + }); +}); diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.branchcov0724pm.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.branchcov0724pm.test.ts new file mode 100644 index 0000000000..c0aa8267a5 --- /dev/null +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.branchcov0724pm.test.ts @@ -0,0 +1,135 @@ +import { describe, expect, it } from 'vitest'; +import type { Resource } from '@/types/resource'; + +import { + buildProxmoxPageModel, + getProxmoxProviderScope, + isProxmoxChildOfNode, +} from '../proxmoxPageModel'; + +// --------------------------------------------------------------------------- +// Fixture builder — mirrors the sibling proxmoxPageModel.test.ts factory so +// import style and default platform posture stay aligned. +// --------------------------------------------------------------------------- + +const makeResource = (resource: Partial & Pick): Resource => ({ + name: resource.id, + displayName: resource.id, + platformId: 'lab', + platformType: 'proxmox-pve', + sourceType: 'api', + status: 'online', + lastSeen: 1_700_000_000_000, + ...resource, +}); + +// =========================================================================== +// getProxmoxProviderScope — the `||` fallback chain. The happy-path specs +// only ever exercise resources whose `proxmox.instance` (or, transitively, +// `platformId`) short-circuits the chain. The cold arms are the later +// operands: clusterId, parentId, and the final id fall-through. +// =========================================================================== + +describe('getProxmoxProviderScope fallback chain', () => { + it('falls back to clusterId when proxmox.instance and platformId are absent', () => { + const resource = makeResource({ + id: 'r', + type: 'agent', + platformId: '', + clusterId: 'cluster-9', + }); + expect(getProxmoxProviderScope(resource)).toBe('cluster-9'); + }); + + it('falls back to parentId when instance, platformId, and clusterId are absent', () => { + const resource = makeResource({ + id: 'r', + type: 'agent', + platformId: '', + parentId: 'host-node', + }); + expect(getProxmoxProviderScope(resource)).toBe('host-node'); + }); + + it('returns resource.id as the final fallback when every scope field is absent', () => { + const resource = makeResource({ id: 'last-resort', type: 'agent', platformId: '' }); + expect(getProxmoxProviderScope(resource)).toBe('last-resort'); + }); +}); + +// =========================================================================== +// isProxmoxChildOfNode — the parentId early-return guard. Existing specs +// relate guests to nodes via nodeName/scope only, so the +// `child.parentId && child.parentId === node.id` arm is never taken. +// =========================================================================== + +describe('isProxmoxChildOfNode parentId early-return', () => { + it('returns true when child.parentId matches node.id without consulting node names', () => { + const node = makeResource({ + id: 'node-1', + type: 'agent', + proxmox: { nodeName: 'node-1' }, + }); + // Deliberately mismatched nodeName to prove the parentId arm wins outright. + const child = makeResource({ + id: 'vm-1', + type: 'vm', + parentId: 'node-1', + proxmox: { nodeName: 'unrelated-host', vmid: 101 }, + }); + expect(isProxmoxChildOfNode(child, node)).toBe(true); + }); + + it('does not early-return when child.parentId is set but differs from node.id', () => { + const node = makeResource({ + id: 'node-1', + type: 'agent', + proxmox: { nodeName: 'node-1' }, + }); + const child = makeResource({ + id: 'vm-2', + type: 'vm', + parentId: 'other-node', + proxmox: { nodeName: 'different-host', vmid: 102 }, + }); + expect(isProxmoxChildOfNode(child, node)).toBe(false); + }); +}); + +// =========================================================================== +// buildProxmoxPageModel — the cluster-group sort comparator. The happy-path +// specs always insert named clusters before the standalone bucket, so V8's +// sort only ever calls the comparator with (standalone, non-standalone) pairs +// and only the `1` (consequent) arm fires. Inserting a standalone node FIRST +// makes the comparator receive (non-standalone, standalone) pairs and return +// the `-1` (alternate) arm. +// =========================================================================== + +describe('buildProxmoxPageModel standalone-first sort comparator', () => { + it('exercises the -1 arm of the sort comparator when the standalone group is inserted first', () => { + // A node carrying no cluster hint resolves to 'Standalone'. Placed before a + // named-cluster node, the standalone bucket lands first in the insertion + // order, flipping the comparator argument direction. + const standaloneNode = makeResource({ + id: 'solo-node', + type: 'agent', + proxmox: { nodeName: 'solo-node' }, + }); + const namedNode = makeResource({ + id: 'named-node', + type: 'agent', + proxmox: { nodeName: 'named-node', clusterName: 'named' }, + }); + + const model = buildProxmoxPageModel([standaloneNode, namedNode]); + + // Standalone was inserted first; the sort still places named before standalone. + expect(model.clusterGroups.map((group) => group.label)).toEqual(['named', 'Standalone']); + expect(model.clusterGroups.map((group) => group.id)).toEqual([ + 'lab::named', + 'lab::__standalone__', + ]); + // The standalone bucket never counts as a real cluster. + expect(model.summary.clusterCount).toBe(1); + }); +}); diff --git a/frontend-modern/src/features/standalone/__tests__/standalonePageModel.branchcov0724pm.test.ts b/frontend-modern/src/features/standalone/__tests__/standalonePageModel.branchcov0724pm.test.ts new file mode 100644 index 0000000000..aa2dc54801 --- /dev/null +++ b/frontend-modern/src/features/standalone/__tests__/standalonePageModel.branchcov0724pm.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from 'vitest'; +import type { Resource } from '@/types/resource'; +import { + buildStandalonePostureSummary, + getStandaloneResourceStatusIndicator, +} from '../standalonePageModel'; + +const NOW_MS = Date.parse('2026-07-24T12:00:00Z'); + +const resource = (overrides: Partial): Resource => + ({ + id: overrides.id ?? 'agent-1', + name: overrides.name ?? overrides.id ?? 'agent-1', + displayName: overrides.displayName ?? overrides.name ?? overrides.id ?? 'agent-1', + type: overrides.type ?? 'agent', + platformId: overrides.platformId ?? 'agent-1', + platformType: overrides.platformType ?? 'agent', + sourceType: overrides.sourceType ?? 'agent', + status: overrides.status ?? 'online', + lastSeen: overrides.lastSeen ?? NOW_MS, + ...overrides, + }) as Resource; + +describe('standalonePageModel branch coverage', () => { + it('treats an ambiguous availability correlation with fresh evidence as an unresolved-identity warning', () => { + // Freshness is deliberately fresh (recent lastChecked + short poll + // interval keep it inside the freshness window) so the warning can only + // come from correlationState === 'ambiguous', and the label ternary falls + // through to 'Identity unresolved'. + const indicator = getStandaloneResourceStatusIndicator( + resource({ + id: 'availability:ambiguous', + type: 'network-endpoint', + platformType: 'availability', + status: 'online', + availability: { + targetId: 'ambiguous', + correlationState: 'ambiguous', + available: true, + lastChecked: '2026-07-24T11:59:00Z', + pollIntervalSeconds: 60, + }, + }), + NOW_MS, + ); + + expect(indicator).toEqual({ variant: 'warning', label: 'Identity unresolved' }); + }); + + it('treats an unresolved availability correlation with fresh evidence as an unresolved-identity warning', () => { + const indicator = getStandaloneResourceStatusIndicator( + resource({ + id: 'availability:unresolved', + type: 'network-endpoint', + platformType: 'availability', + status: 'online', + availability: { + targetId: 'unresolved', + correlationState: 'unresolved', + available: true, + lastChecked: '2026-07-24T11:59:00Z', + pollIntervalSeconds: 60, + }, + }), + NOW_MS, + ); + + expect(indicator).toEqual({ variant: 'warning', label: 'Identity unresolved' }); + }); + + it('treats an agent carrying the explicit stale flag as attention even when its last report is recent', () => { + // lastSeen is one minute before now, far inside the 5-minute stale + // window, so the time-based staleness expression cannot be the cause; + // only resource.agent?.stale === true forces the warning here. + const indicator = getStandaloneResourceStatusIndicator( + resource({ + id: 'agent-flag-stale', + type: 'agent', + status: 'online', + lastSeen: NOW_MS - 60_000, + agent: { stale: true }, + }), + NOW_MS, + ); + + expect(indicator).toEqual({ variant: 'warning', label: 'Stale' }); + }); + + it('counts a status outside online/degraded/offline buckets as an unknown posture row', () => { + // 'paused' is not in OFFLINE/DEGRADED/online sets, so the base indicator + // resolves to the muted variant and the summary falls into the unknown + // else arm. lastSeen is non-positive so latestUpdateAt must stay unset. + const summary = buildStandalonePostureSummary( + [ + resource({ + id: 'paused-endpoint', + type: 'network-endpoint', + status: 'paused', + lastSeen: 0, + }), + ], + NOW_MS, + ); + + expect(summary).toEqual({ + attention: 0, + critical: 0, + normal: 0, + total: 1, + unknown: 1, + warning: 0, + }); + expect(summary.latestUpdateAt).toBeUndefined(); + }); +});