Private/Get-AzLocalClusterReadinessStatus.ps1

function Get-AzLocalClusterReadinessStatus {
    ########################################
    <#
    .SYNOPSIS
        Classifies a single Get-AzLocalClusterUpdateReadiness row into exactly
        one readiness-status bucket using the canonical priority cascade.
    .DESCRIPTION
        v0.8.74 single source of truth for the "what state is this cluster in"
        classification shared by Step.5 (Export-AzLocalClusterUpdateReadinessReport),
        Step.7 (Export-AzLocalClusterReadinessGateReport) and Step.9
        (Export-AzLocalFleetUpdateStatusReport).
 
        Prior to this helper each step re-implemented the bucket logic inline,
        and the "Up to Date" definition drifted: Step.9's rendered Primary Status
        table used a priority cascade (correct), while Step.5 and the JSON exports
        used a stricter test that also required AllAvailableUpdates to be empty.
        That strict test silently returned zero "Up to Date" clusters in
        production because a cluster that has applied all updates still carries
        the already-Installed update names in AllAvailableUpdates - so up-to-date
        clusters were mis-bucketed as "Not Ready" (Step.5) or shown with a
        no-entry icon (Step.7), implying failure.
 
        Every consumer now calls this helper so the classification is identical
        across all three steps.
 
        Priority cascade (first match wins, cluster counted exactly once):
            UpdateFailed > ActionRequired > HealthFailure > SbeBlocked >
            InProgress > ReadyForUpdate > UpToDate > NeedsInvestigation
    .PARAMETER ReadinessRow
        A single PSCustomObject row as emitted by Get-AzLocalClusterUpdateReadiness.
        Optional properties are read defensively for Set-StrictMode -Version Latest.
    .OUTPUTS
        [string] one of:
            UpdateFailed, ActionRequired, HealthFailure, SbeBlocked,
            InProgress, ReadyForUpdate, UpToDate, NeedsInvestigation
    .NOTES
        Author : AzLocal.UpdateManagement
        Version : 0.8.74
    #>

    ########################################
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$ReadinessRow
    )

    Set-StrictMode -Version Latest

    # Strict-mode-safe optional property reads. Different code paths in
    # Get-AzLocalClusterUpdateReadiness emit rows with the same shape, but
    # external callers (and the error/catch row) may not, so guard every read.
    $updateState = if ($ReadinessRow.PSObject.Properties['UpdateState'] -and $ReadinessRow.UpdateState) { [string]$ReadinessRow.UpdateState } else { '' }
    $healthState = if ($ReadinessRow.PSObject.Properties['HealthState'] -and $ReadinessRow.HealthState) { [string]$ReadinessRow.HealthState } else { '' }
    $hasPrereq   = if ($ReadinessRow.PSObject.Properties['HasPrerequisiteUpdates'] -and $ReadinessRow.HasPrerequisiteUpdates) { [string]$ReadinessRow.HasPrerequisiteUpdates } else { '' }
    $readyForUpdate = if ($ReadinessRow.PSObject.Properties['ReadyForUpdate']) { [bool]$ReadinessRow.ReadyForUpdate } else { $false }

    if ($updateState -in @('Failed', 'UpdateFailed', 'NeedsAttention'))       { return 'UpdateFailed' }
    elseif ($updateState -eq 'PreparationFailed')                            { return 'ActionRequired' }
    elseif ($healthState -eq 'Failure')                                      { return 'HealthFailure' }
    elseif ($hasPrereq)                                                      { return 'SbeBlocked' }
    elseif ($updateState -in @('UpdateInProgress', 'PreparationInProgress')) { return 'InProgress' }
    elseif ($readyForUpdate -eq $true)                                       { return 'ReadyForUpdate' }
    elseif ($updateState -in @('UpToDate', 'AppliedSuccessfully'))           { return 'UpToDate' }
    else                                                                     { return 'NeedsInvestigation' }
}