Public/Test-GovernanceAccess.ps1

function Test-GovernanceAccess {
    <#
        .SYNOPSIS
        Verifies the PAT referenced by the program manifest carries every scope
        the engine needs, using side-effect-free probes (reads, plus writes with
        intentionally invalid payloads that ADO rejects AFTER the scope check —
        nothing is ever created or changed).

        Prints one line per scope family and returns a non-terminating error
        when any scope needed is missing, so CI exits non-zero. `unknown`
        verdicts (network failure, unexpected status) are reported but do not
        fail the run.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ProgramPath,
        [string]$Org
    )

    $source    = Import-GovernanceSource -ProgramPath $ProgramPath
    $manifest  = $source.Manifest
    $targetOrg = if ($Org) { $Org } else { $manifest.org }
    $orgUrl      = ConvertTo-AdoOrgUrl -Org $targetOrg
    $authMode    = Initialize-AdoAuth -Manifest $manifest -OrgUrl $orgUrl
    $projectName = if ($manifest.project -and $manifest.project.name) { $manifest.project.name } else { $manifest.program }

    $identity = if ($authMode -eq 'entra') {
        $who = az account show --query user.name --output tsv 2>$null
        "Entra ($who)"
    } else { 'PAT' }
    Write-Host "Checking access for '$projectName' in $orgUrl [auth: $identity]" -ForegroundColor Cyan
    Write-Host "(read probes + intentionally invalid writes - no changes are made)" -ForegroundColor DarkGray

    $results = @(Test-AdoAuthScope -OrgUrl $orgUrl -Project $projectName)
    foreach ($r in $results) {
        switch ($r.Verdict) {
            'ok'      { Write-Host (" [OK] {0,-28} {1,-28} needed for: {2}" -f $r.Family, $r.Scope, $r.NeededFor) -ForegroundColor Green }
            'missing' { Write-Host (" [MISSING] {0,-28} {1,-28} needed for: {2}" -f $r.Family, $r.Scope, $r.NeededFor) -ForegroundColor Red }
            default   { Write-Host (" [?] {0,-28} {1,-28} probe inconclusive (HTTP {3}); needed for: {2}" -f $r.Family, $r.Scope, $r.NeededFor, $r.StatusCode) -ForegroundColor Yellow }
        }
        if ($r.Note) { Write-Host (" note: {0}" -f $r.Note) -ForegroundColor Yellow }
    }

    # A missing scope is a finding, not a malfunction — report it as normal
    # output and return $false so the caller can set the exit code. Throwing
    # here would dress a successful diagnosis up as a crash.
    $missing = @($results | Where-Object Verdict -eq 'missing')
    if ($missing.Count -gt 0) {
        Write-Host ""
        Write-Host ("Missing {0} capability(ies): {1}" -f $missing.Count,
            (($missing | ForEach-Object { $_.Scope }) -join ', ')) -ForegroundColor Red
        Write-Host "Fix: add the scope to the PAT where selectable; for security writes (not PAT-selectable) sign in with 'az login' using an account with access to this org - see the note on the [MISSING] row." -ForegroundColor Red
        return $false
    }
    Write-Host "All scope probes passed." -ForegroundColor Green
    return $true
}