Modules/businessdev.ALbuild.Containers/Private/Test-BcArtifactFolderIntact.ps1

function Test-BcArtifactFolderIntact {
    <#
    .SYNOPSIS
        Decides whether a cached BC artifact folder can be trusted, and says why not.
 
    .DESCRIPTION
        Two very different kinds of cached artifact exist side by side on a build agent:
 
        1. Folders ALbuild downloaded itself. Those carry a '.albuild-complete' marker with the file
           count and total size recorded at publish time, so this can re-count and detect ANY later
           loss - a missing folder, a truncated file, a partial manual cleanup.
        2. Folders somebody else produced - the BC container image downloading into the mounted cache,
           BcContainerHelper, a copied folder. Those have no marker, so completeness can only be judged
           structurally.
 
        The structural check deliberately goes beyond "manifest.json parses", which is what the previous
        validation did and why a real failure slipped through: the broken cache HAD a valid manifest and
        the database file - what it was missing were the Applications.<country> and Extensions folders.
        The container then started fine and only failed minutes later, inside the test-toolkit install,
        with AL compiler errors that named none of this.
 
        A legacy marker (the old bare-timestamp format) is treated as trustworthy-but-unfingerprinted:
        it is NOT invalidated, because doing so would throw away every already-good cache on the estate
        the moment this ships. It is reported so the caller can adopt it by writing a real fingerprint.
 
    .PARAMETER Path
        The extracted artifact folder to check.
 
    .PARAMETER IsPlatform
        The folder is the platform artifact, which has a different expected shape than a country one
        (no Applications.<country>, and older platform artifacts ship no manifest.json at all).
 
    .OUTPUTS
        PSCustomObject: IsIntact (bool), Reason (string), MarkerState ('None'|'Legacy'|'Fingerprinted').
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path,
        [switch] $IsPlatform
    )

    $result = { param($intact, $reason, $marker) [PSCustomObject]@{ IsIntact = $intact; Reason = $reason; MarkerState = $marker } }

    if (-not (Test-Path -LiteralPath $Path)) { return & $result $false 'the folder does not exist' 'None' }

    # --- 1. ALbuild-owned folders: re-count against the recorded fingerprint --------------------------
    $markerPath = Join-Path $Path '.albuild-complete'
    if (Test-Path -LiteralPath $markerPath) {
        $recorded = $null
        try { $recorded = Get-Content -LiteralPath $markerPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop }
        catch { $recorded = $null }   # legacy bare-timestamp marker

        $hasFingerprint = $recorded -and
            ($recorded.PSObject.Properties.Name -contains 'files') -and
            ($recorded.PSObject.Properties.Name -contains 'bytes')

        if ($hasFingerprint) {
            $now = Get-BcArtifactInventory -Path $Path
            if ([int] $recorded.files -eq $now.Files -and [long] $recorded.bytes -eq $now.Bytes) {
                return & $result $true '' 'Fingerprinted'
            }
            return & $result $false (
                "content changed since it was cached (recorded $($recorded.files) file(s) / $($recorded.bytes) bytes, " +
                "found $($now.Files) / $($now.Bytes))") 'Fingerprinted'
        }
        # Legacy marker: fall through to the structural check, but report it so the caller can adopt it.
        $legacy = $true
    }
    else { $legacy = $false }

    # --- 2. Structural check for anything without a usable fingerprint --------------------------------
    $markerState = if ($legacy) { 'Legacy' } else { 'None' }

    $manifestPath = Join-Path $Path 'manifest.json'
    $manifest = $null
    if (Test-Path -LiteralPath $manifestPath) {
        try { $manifest = Get-Content -LiteralPath $manifestPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop }
        catch { return & $result $false "manifest.json is present but not parseable ($($_.Exception.Message))" $markerState }
    }

    if ($IsPlatform) {
        # Deliberately unchanged from the long-standing rule: a parseable manifest.json, or (older BC
        # platform artifacts ship none) a 'lastused' marker plus real content. Demanding specific folders
        # here would be guessing - the platform layout differs across BC versions and artifact types, and
        # a wrong guess deletes valid multi-GB caches across the estate. Loss of platform content is
        # caught by the fingerprint instead, which needs no assumptions about the layout.
        if ($manifest) { return & $result $true '' $markerState }
        if (Test-Path -LiteralPath (Join-Path $Path 'lastused')) {
            $hasContent = @(Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue |
                    Where-Object { $_.Name -ne 'lastused' -and $_.Name -ne '.albuild-complete' }).Count -gt 0
            if ($hasContent) { return & $result $true '' $markerState }
            return & $result $false 'only a lastused marker is present, with no content' $markerState
        }
        return & $result $false 'neither manifest.json nor a lastused marker is present' $markerState
    }

    # Country artifact: the image reads manifest.json, so it must be there and parseable.
    if (-not $manifest) { return & $result $false 'manifest.json is missing' $markerState }

    # The manifest names its own database and license file - verify exactly those rather than guessing.
    foreach ($field in 'database', 'licenseFile') {
        if ($manifest.PSObject.Properties.Name -notcontains $field) { continue }
        $named = "$($manifest.$field)"
        if (-not $named) { continue }
        if (-not (Test-Path -LiteralPath (Join-Path $Path $named))) {
            return & $result $false "the file '$named' named by manifest.json ($field) is missing" $markerState
        }
    }

    # The one folder requirement that is EVIDENCE-BASED rather than assumed: a localised artifact carries
    # its compiled country apps in 'Applications.<country>', and its absence is precisely the defect that
    # took three product pipelines down - the container silently falls back to the source packages under
    # C:\Applications, which do not compile for a localised build. w1 legitimately has no such folder, so
    # this is required only when the manifest names a country other than W1. Nothing else about the
    # country layout is asserted; the fingerprint covers the rest without guessing.
    $country = if ($manifest.PSObject.Properties.Name -contains 'country') { "$($manifest.country)" } else { '' }
    if ($country -and $country -notmatch '^(?i)w1$') {
        $appsFolder = Join-Path $Path "Applications.$country"
        if (-not (Test-Path -LiteralPath $appsFolder)) {
            return & $result $false ("the 'Applications.$country' folder is missing - the container would fall back to the " +
                'source packages under C:\Applications, which are recompiled on publish and fail for a localised artifact') $markerState
        }
        if (-not (Get-ChildItem -LiteralPath $appsFolder -File -Force -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1)) {
            return & $result $false "the 'Applications.$country' folder is empty" $markerState
        }
    }

    return & $result $true '' $markerState
}