#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } <# FR-021, SC-007: Processed = Matched + Unclassified + EvaluationError at every summary, and a mismatch is reported as an engine defect. The forced-mismatch tests are the point. A reconciliation check that only ever sees correct data proves that the arithmetic works, not that the failure path does - and the failure path is the only part anyone will ever depend on. #> BeforeAll { $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent . (Join-Path $repoRoot 'tests/TestHelpers.ps1') foreach ($file in (Get-PersonaSourceFile -RepoRoot $repoRoot)) { . $file } $script:target = 'extension__' $script:population = @(New-TestPopulation -Count 15 -TargetAttribute $target) } Describe 'Reconciliation arithmetic (FR-021)' { It 'passes when the outcome buckets account for every processed user' { $counters = New-PersonaRunCounter -Rules @() $counters.Processed = 10 $counters.Matched = 7 $counters.Unclassified = 2 $counters.EvaluationError = 1 Test-PersonaReconciliation -Counters $counters | Should -BeTrue } It 'fails when a user was processed but landed in no bucket' { $counters = New-PersonaRunCounter -Rules @() $counters.Processed = 10 $counters.Matched = 7 $counters.Unclassified = 2 $counters.EvaluationError = 0 Test-PersonaReconciliation -Counters $counters | Should -BeFalse } It 'fails when a user was double-counted' { $counters = New-PersonaRunCounter -Rules @() $counters.Processed = 10 $counters.Matched = 8 $counters.Unclassified = 2 $counters.EvaluationError = 1 Test-PersonaReconciliation -Counters $counters | Should -BeFalse } It 'passes trivially on an empty run' { Test-PersonaReconciliation -Counters (New-PersonaRunCounter -Rules @()) | Should -BeTrue } It 'ignores the action buckets, which would otherwise mask a lost user' { # Skipped is a catch-all. If reconciliation checked the action buckets, a lost # user absorbed into Skipped would keep the sums balanced on a broken run. $counters = New-PersonaRunCounter -Rules @() $counters.Processed = 5 $counters.Matched = 5 $counters.Skipped = 99 Test-PersonaReconciliation -Counters $counters | Should -BeTrue } } Describe 'Reconciliation failure detail' { It 'reports the difference and its sign so a maintainer can tell loss from duplication' { $counters = New-PersonaRunCounter -Rules @() $counters.Processed = 10 $counters.Matched = 7 $counters.Unclassified = 2 $counters.EvaluationError = 0 $detail = Get-PersonaReconciliationDetail -Counters $counters $detail.processed | Should -Be 10 $detail.outcomeTotal | Should -Be 9 $detail.difference | Should -Be 1 $detail.severity | Should -Be 'Error' $detail.defect | Should -Be 'ReconciliationFailure' } It 'reports a negative difference when a user was double-counted' { $counters = New-PersonaRunCounter -Rules @() $counters.Processed = 10 $counters.Matched = 9 $counters.Unclassified = 2 (Get-PersonaReconciliationDetail -Counters $counters).difference | Should -Be -1 } } Describe 'Reconciliation over a real run (SC-007)' { BeforeEach { Mock Get-PersonaUsers { $script:population } Mock Write-Host { } } It 'reconciles at every interim summary and at completion' { $config = New-TestRuntimeConfiguration -TargetAttribute $target -SummaryInterval 5 $info = $null $null = Invoke-PersonaEngineRun -Configuration $config -TargetAttribute $target ` -Context (New-TestAuditContext) -AuditParameters @{ Destination = 'stream' } ` -InformationVariable info $summaries = Get-CapturedAuditRecord -Captured $info -RecordType 'Summary' # Three interim boundaries at 5, 10, 15 plus the final summary. $summaries.Count | Should -BeGreaterThan 1 foreach ($summary in $summaries) { $summary['reconciliationPassed'] | Should -BeTrue $summary['processed'] | Should -Be ($summary['matched'] + $summary['unclassified'] + $summary['evaluationError']) } } It 'returns exit code 5 and emits an EngineDefect when reconciliation fails' { # The failure is forced by making Add-PersonaRunResult drop the outcome tally # while still counting the user as processed - precisely the bookkeeping defect # FR-021 exists to catch. Mock Add-PersonaRunResult { $Counters.Processed++ } $config = New-TestRuntimeConfiguration -TargetAttribute $target -SummaryInterval 0 $info = $null $outcome = Invoke-PersonaEngineRun -Configuration $config -TargetAttribute $target ` -Context (New-TestAuditContext) -AuditParameters @{ Destination = 'stream' } ` -InformationVariable info $outcome.ExitCode | Should -Be 5 $defects = Get-CapturedAuditRecord -Captured $info -RecordType 'EngineDefect' $defects.Count | Should -BeGreaterThan 0 $defects[0]['defect'] | Should -Be 'ReconciliationFailure' $defects[0]['severity'] | Should -Be 'Error' } It 'marks the summary record itself as failed when reconciliation fails' { Mock Add-PersonaRunResult { $Counters.Processed++ } $config = New-TestRuntimeConfiguration -TargetAttribute $target -SummaryInterval 0 $info = $null $null = Invoke-PersonaEngineRun -Configuration $config -TargetAttribute $target ` -Context (New-TestAuditContext) -AuditParameters @{ Destination = 'stream' } ` -InformationVariable info (Get-CapturedAuditRecord -Captured $info -RecordType 'Summary')[0]['reconciliationPassed'] | Should -BeFalse } }