#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } <# FR-027: the interactive editor can add a new rule, and rejects a colliding id or priority before the rule is added. Add-PersonaConfigRule is exercised directly rather than through the interactive script - it is the pure function the [A] command calls, and pure functions are what this suite can test without driving Read-Host through a subprocess (see NonInteractive.Tests.ps1 for why that path is reserved for hang/prompt proof, not ordinary behaviour). #> BeforeAll { $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent . (Join-Path $repoRoot 'tests/TestHelpers.ps1') foreach ($file in (Get-PersonaSourceFile -RepoRoot $repoRoot)) { . $file } function New-TestRule { param( [string] $Id = 'RULE-0500-TEST', [int] $Priority = 500, [string] $Persona = 'Employee' ) [pscustomobject]@{ id = $Id name = 'Test rule' description = 'A rule added by a test.' enabled = $true priority = $Priority persona = $Persona match = [pscustomobject]@{ operator = 'all' conditions = @([pscustomobject]@{ type = 'property'; property = 'Department'; operator = 'isNotNull' }) } } } } Describe 'Add-PersonaConfigRule (FR-027)' { It 'appends the new rule to the collection' { $existing = @((New-TestRule -Id 'RULE-0010-GUEST' -Priority 10)) $new = New-TestRule -Id 'RULE-0500-TEST' -Priority 500 $result = Add-PersonaConfigRule -Rules $existing -Rule $new $result.Count | Should -Be 2 ($result | Where-Object { [string]$_.id -eq 'RULE-0500-TEST' }).Count | Should -Be 1 } It 'does not mutate the input collection' { $existing = @((New-TestRule -Id 'RULE-0010-GUEST' -Priority 10)) $new = New-TestRule -Id 'RULE-0500-TEST' -Priority 500 Add-PersonaConfigRule -Rules $existing -Rule $new | Out-Null $existing.Count | Should -Be 1 } It 'always returns an array, even when the result has exactly one rule' { # The single-element case is the one PowerShell silently collapses to a # scalar unless the function guards against it (see Add-PersonaConfigRule's # `, (...)` return) - assigning the call's result directly, with no extra # @() at this call site, is what proves the function's own guard is doing # the work rather than an @() here masking a function that does not. $new = New-TestRule -Id 'RULE-0500-TEST' -Priority 500 $result = Add-PersonaConfigRule -Rules @() -Rule $new , $result | Should -BeOfType [array] $result.Count | Should -Be 1 } It 'rejects a duplicate id before adding' { $existing = @((New-TestRule -Id 'RULE-0010-GUEST' -Priority 10)) $colliding = New-TestRule -Id 'RULE-0010-GUEST' -Priority 999 { Add-PersonaConfigRule -Rules $existing -Rule $colliding } | Should -Throw '*RULE-0010-GUEST*' } It 'rejects a duplicate priority before adding' { $existing = @((New-TestRule -Id 'RULE-0010-GUEST' -Priority 10)) $colliding = New-TestRule -Id 'RULE-0500-TEST' -Priority 10 { Add-PersonaConfigRule -Rules $existing -Rule $colliding } | Should -Throw '*10*' } It 'leaves the original collection unchanged when the add is rejected' { $existing = @((New-TestRule -Id 'RULE-0010-GUEST' -Priority 10)) $colliding = New-TestRule -Id 'RULE-0010-GUEST' -Priority 999 try { Add-PersonaConfigRule -Rules $existing -Rule $colliding } catch { } $existing.Count | Should -Be 1 } It 'rejects a blank id' { $existing = @((New-TestRule -Id 'RULE-0010-GUEST' -Priority 10)) $blank = New-TestRule -Id '' -Priority 999 { Add-PersonaConfigRule -Rules $existing -Rule $blank } | Should -Throw } It 'the added rule survives a full validation pass' { $document = New-TestConfigurationDocument $new = @{ id = 'RULE-0500-TEST' name = 'Test rule' description = 'A rule added by a test.' enabled = $true priority = 500 persona = 'Employee' match = @{ operator = 'all' conditions = @(@{ type = 'property'; property = 'Department'; operator = 'isNotNull' }) } } $document.rules = Add-PersonaConfigRule -Rules $document.rules -Rule $new $path = Save-TestConfiguration -Document $document -Directory ([System.IO.Path]::GetTempPath()) try { $result = Test-PersonaConfiguration -Path $path $result.IsValid | Should -BeTrue @($result.Document.rules).Count | Should -Be 3 } finally { Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue } } }