Modules/businessdev.ALbuild.OnPrem/Private/Resolve-BcAppFileIdentity.ps1

function Resolve-BcAppFileIdentity {
    <#
    .SYNOPSIS
        Reads identity and test-app facts out of a built .app file.
 
    .DESCRIPTION
        Used to decide what a per-tenant publish should actually send to a customer environment, and to
        log WHICH app is being published rather than just a file name. An app counts as a test app when
        its manifest carries Target="Test" or it depends on a Microsoft test/assert library - the same
        rule the Marketplace submission uses to keep test apps out of AppSource
        (Resolve-BcMarketplaceAppSet). Test apps must never reach a customer tenant: they carry no
        business value, pull in the test framework, and can fail to install where it is absent.
 
        A file whose manifest cannot be read is reported with Readable = $false rather than throwing, and
        falls back to the FILE NAME for the test check. Refusing to publish it would be worse than the
        bug being fixed: a quirk in one artifact would silently turn a release into a no-op. So an
        unreadable file is still published (with a warning) unless its name marks it as a test app.
 
    .PARAMETER Path
        The .app file to inspect.
 
    .OUTPUTS
        PSCustomObject: File, Name, Publisher, Version, Id, IsTest, IsRuntime, Readable, Error.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [string] $Path
    )

    $leaf = Split-Path -Path $Path -Leaf
    $facts = [PSCustomObject]@{
        File      = $Path
        Name      = $leaf
        Publisher = ''
        Version   = ''
        Id        = ''
        IsTest    = $false
        IsRuntime = ($leaf -like '*.runtime.app')
        Readable  = $false
        Error     = ''
    }

    try { $info = Expand-BcAppFile -Path $Path }
    catch {
        $facts.Error = $_.Exception.Message
        # Name-based fallback only. A built app is named '<publisher>_<name>_<version>.app', so the base
        # name ENDS in the version - matching '*.Tests' would miss the real thing (a live release shipped
        # 'KTC - ... GmbH_Bollfilter-Core Tests_1.0.22.696.app'). Match a 'Test'/'Tests' token anywhere,
        # on a separator boundary so 'Latest' and 'Contest' do not qualify.
        $bare = [System.IO.Path]::GetFileNameWithoutExtension($leaf)
        $facts.IsTest = ($bare -match '(?i)(^|[\s._-])tests?([\s._-]|$)')
        return $facts
    }

    $facts.Readable = $true
    $facts.Name = "$($info.Name)"
    $facts.Publisher = "$($info.Publisher)"
    $facts.Version = "$($info.Version)"
    $facts.Id = "$($info.Id)"

    # Manifest Target="Test" is the authoritative signal.
    $target = ''
    try {
        $appNode = $info.Manifest.SelectSingleNode("//*[local-name()='App']")
        if ($appNode) { $target = "$($appNode.GetAttribute('Target'))" }
    }
    catch { Write-Verbose "Could not read the App Target attribute from '$leaf': $($_.Exception.Message)" }
    if ($target -ieq 'Test') { $facts.IsTest = $true }

    # Fallback: a dependency on a Microsoft test/assert library (apps that omit Target).
    if (-not $facts.IsTest) {
        foreach ($dependency in @($info.Dependencies)) {
            if ("$($dependency.Publisher)" -eq 'Microsoft' -and
                ("$($dependency.Name)" -like '*Test*' -or "$($dependency.Name)" -like '*Assert*')) {
                $facts.IsTest = $true
                break
            }
        }
    }

    return $facts
}