#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } <# SC-001: every processed user lands in exactly one outcome bucket. Exclusivity is what makes reconciliation meaningful. If a user could be both Matched and EvaluationError, the sum would still be checkable but would no longer mean anything, and FR-021 would be verifying arithmetic rather than correctness. #> 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__' # A rule set that needs membership data, so a failed lookup produces # EvaluationError and all three buckets are populated in one run. $script:rules = @( (New-TestMembershipRule) [pscustomobject]@{ id = 'RULE-0900-EMPLOYEE'; name = 'Employees'; enabled = $true; priority = 900; persona = 'Employee' match = [pscustomobject]@{ operator = 'all' conditions = @([pscustomobject]@{ type = 'property'; property = 'Department'; operator = 'isNotNull' }) } } ) $script:config = New-TestRuntimeConfiguration -TargetAttribute $target -Rules $script:rules } Describe 'Exactly one outcome per user (SC-001)' { BeforeEach { Mock Write-Host { } Mock Set-UserPersonaAttribute { [pscustomobject]@{ Succeeded = $true } } Mock Get-PersonaUsers { New-TestPopulation -Count 24 -TargetAttribute $script:target } # Every fourth account fails its membership lookup, producing a genuine mix # of all three outcomes rather than a run where only one bucket is exercised. $script:lookup = 0 Mock Get-PersonaGroupMembership { $script:lookup++ ($script:lookup % 4) -eq 0 ` ? (New-PersonaMembershipRecord -DirectFailureReason 'Graph 503 after 5 attempts') ` : (New-PersonaMembershipRecord -AllRetrieved) } } It 'accounts for every processed user across the three outcome buckets' { $outcome = Invoke-PersonaEngineRun -Configuration $config -TargetAttribute $target ` -Context (New-TestAuditContext) -ShouldProcessGate { param($t, $d) $false } $sum = $outcome.Counters.Matched + $outcome.Counters.Unclassified + $outcome.Counters.EvaluationError $sum | Should -Be $outcome.Counters.Processed } It 'populates all three buckets, so the sum is not trivially satisfied' { $outcome = Invoke-PersonaEngineRun -Configuration $config -TargetAttribute $target ` -Context (New-TestAuditContext) -ShouldProcessGate { param($t, $d) $false } $outcome.Counters.Matched | Should -BeGreaterThan 0 $outcome.Counters.Unclassified | Should -BeGreaterThan 0 $outcome.Counters.EvaluationError | Should -BeGreaterThan 0 } It 'accounts for every processed user across the action buckets too' { $outcome = Invoke-PersonaEngineRun -Configuration $config -TargetAttribute $target ` -Context (New-TestAuditContext) -ShouldProcessGate { param($t, $d) $false } $actions = $outcome.Counters.Unchanged + $outcome.Counters.WouldUpdate + $outcome.Counters.Updated + $outcome.Counters.UpdateFailed + $outcome.Counters.Skipped $actions | Should -Be $outcome.Counters.Processed } It 'assigns a single outcome value to each decision result' { $record = New-PersonaUserRecord -AccountObjectId '00000000-0000-0000-0000-000000000101' ` -UserPrincipalName 'a@example.invalid' -Properties @{ Department = 'Finance' } ` -Membership (New-PersonaMembershipRecord -AllRetrieved) $result = Resolve-UserPersona -UserRecord $record -Rules $script:rules @($result.Outcome).Count | Should -Be 1 $result.Outcome | Should -BeIn @('Matched', 'Unclassified', 'EvaluationError') } It 'never reports an EvaluationError user as Matched' { # The specific confusion FR-013 exists to prevent: an unevaluable account # must never carry a persona, or the preserved value and the calculated value # would both look authoritative. $record = New-PersonaUserRecord -AccountObjectId '00000000-0000-0000-0000-000000000102' ` -UserPrincipalName 'b@example.invalid' -Properties @{ Department = 'Finance' } ` -StoredPersona 'Employee' ` -Membership (New-PersonaMembershipRecord -DirectFailureReason 'lookup failed') $result = Resolve-UserPersona -UserRecord $record -Rules $script:rules $result.Outcome | Should -Be 'EvaluationError' $result.MatchedRuleId | Should -BeNullOrEmpty $result.CalculatedPersona | Should -BeNullOrEmpty $result.StoredPersona | Should -Be 'Employee' } }