# Persona Engine - validation stage # # Everything here runs with no tenant, no credentials, and no network (SC-008). That # is the point: a pipeline that needs a directory connection to tell you a rule file # is wrong cannot run on every pull request, and the check that only runs sometimes is # the one that stops catching things. # # Gate order is deliberate, cheapest and most categorical first. Sanitization runs # before anything else because a leaked identifier in a branch is a problem whether or # not the code compiles, and every later stage prints file contents into build logs. trigger: branches: include: - main paths: include: - src/* - tests/* - config/* - pipelines/* - Invoke-PersonaEngine.ps1 - Edit-PersonaEngineConfig.ps1 - PersonaEngine.psd1 - PersonaEngine.psm1 pr: branches: include: - main pool: vmImage: windows-latest variables: # Pinned rather than latest. A validation stage that changes behaviour when an # upstream module publishes is not a gate, it is a coin flip. pesterVersion: '5.6.1' analyzerVersion: '1.22.0' steps: - checkout: self fetchDepth: 0 - task: PowerShell@2 displayName: 'Gate 1 - Sanitization (SC-013)' inputs: pwsh: true filePath: tests/Test-Sanitization.ps1 failOnStderr: false - task: PowerShell@2 displayName: 'Install pinned analysis modules' inputs: pwsh: true targetType: inline script: | Set-PSRepository -Name PSGallery -InstallationPolicy Trusted Install-Module Pester -RequiredVersion $(pesterVersion) -Force -SkipPublisherCheck -Scope CurrentUser Install-Module PSScriptAnalyzer -RequiredVersion $(analyzerVersion) -Force -Scope CurrentUser - task: PowerShell@2 displayName: 'Gate 2 - PSScriptAnalyzer' inputs: pwsh: true targetType: inline script: | $ErrorActionPreference = 'Stop' $findings = Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 if ($findings) { $findings | Format-Table -AutoSize | Out-String | Write-Host } $blocking = @($findings | Where-Object Severity -in 'Error', 'Warning') if ($blocking.Count -gt 0) { throw "PSScriptAnalyzer reported $($blocking.Count) blocking finding(s)." } - task: PowerShell@2 displayName: 'Gate 3 - Engine purity (Principle IV)' inputs: pwsh: true filePath: tests/Test-EnginePurity.ps1 - task: PowerShell@2 displayName: 'Gate 4 - Shipped schema is valid JSON Schema' inputs: pwsh: true targetType: inline script: | $ErrorActionPreference = 'Stop' # V-5a: Test-Json returns $true when the schema itself cannot be parsed, so a # broken schema would let every later check pass while validating nothing. # This stage exists solely to catch that. $errors = $null $null = '{}' | Test-Json -SchemaFile ./config/persona-engine.schema.json -ErrorAction SilentlyContinue -ErrorVariable errors $unusable = @($errors | Where-Object { $_.Exception.Message -match 'Cannot parse the JSON schema' }) if ($unusable.Count -gt 0) { throw 'config/persona-engine.schema.json is not valid JSON Schema. No configuration can be schema-validated until it is repaired.' } # The contract copy and the shipped copy must stay identical, or a rule author # reading the contract validates against a different schema than the engine. $shipped = (Get-FileHash ./config/persona-engine.schema.json -Algorithm SHA256).Hash $contract = (Get-FileHash ./specs/001-persona-engine/contracts/persona-engine.schema.json -Algorithm SHA256).Hash if ($shipped -ne $contract) { throw 'config/persona-engine.schema.json and the contract copy have diverged.' } - task: PowerShell@2 displayName: 'Gate 5 - Example configuration passes all four layers' inputs: pwsh: true targetType: inline script: | $ErrorActionPreference = 'Stop' ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.example.json -ValidateOnly -NonInteractive if ($LASTEXITCODE -ne 0) { throw "The shipped example configuration failed validation with exit code $LASTEXITCODE." } - task: PowerShell@2 displayName: 'Gate 6 - Offline Pester suite (SC-008)' inputs: pwsh: true targetType: inline script: | $ErrorActionPreference = 'Stop' $config = & ./tests/PesterConfiguration.ps1 -Suite Offline $config.Run.Exit = $false $config.Run.PassThru = $true $config.TestResult.Enabled = $true $config.TestResult.OutputPath = './testResults.offline.xml' $config.Output.Verbosity = 'Normal' $result = Invoke-Pester -Configuration $config # Asserted, not assumed. A configuration change that silently filtered every # test out would otherwise report a green pipeline over zero coverage. if ($result.TotalCount -eq 0) { throw 'The offline suite ran no tests.' } if ($result.FailedCount -gt 0) { throw "$($result.FailedCount) offline test(s) failed." } Write-Host "Offline suite: $($result.PassedCount) passed, $($result.FailedCount) failed." - task: PublishTestResults@2 displayName: 'Publish offline test results' condition: succeededOrFailed() inputs: testResultsFormat: NUnit testResultsFiles: './testResults.offline.xml' testRunTitle: 'Persona Engine - offline suite' - task: PowerShell@2 displayName: 'Gate 7 - No Graph module was loaded (SC-008)' inputs: pwsh: true targetType: inline script: | # The proof that the offline suite is genuinely offline. If a test ever # imports the Graph SDK, the "runs with no tenant" claim quietly stops being # true and nobody notices until an air-gapped build fails. $config = & ./tests/PesterConfiguration.ps1 -Suite Offline $config.Run.PassThru = $true $config.Output.Verbosity = 'None' $null = Invoke-Pester -Configuration $config $graph = Get-Module | Where-Object Name -like 'Microsoft.Graph*' if ($graph) { throw "A Graph module was loaded during the offline suite: $($graph.Name -join ', ')" } Write-Host 'No Graph module was loaded. SC-008 holds.'