226 lines
8.9 KiB
PowerShell
226 lines
8.9 KiB
PowerShell
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' }
|
|
|
|
<#
|
|
FR-030 / acceptance scenarios 9-11 (spec.md User Story 5): the [A]/[E]/[D]
|
|
commands are actually wired into Edit-PersonaEngineConfig.ps1's interactive loop,
|
|
structural edits stay in memory until Save, and a depth-limit violation is
|
|
rejected at edit time using the real validator's own finding.
|
|
|
|
The pure functions behind these commands (Add-PersonaConfigRule,
|
|
Get/Add/Remove-PersonaConditionNode, Set-PersonaConditionLeaf) are covered
|
|
directly in EditorAddRule/EditorEditRule/EditorDeleteRule.Tests.ps1. This suite
|
|
exists to prove the wiring itself - the prompts, the menu, and the
|
|
candidate-edit-or-revert behaviour - which only exists inside the script, not in
|
|
a dot-sourceable function.
|
|
|
|
Same technique as NonInteractive.Tests.ps1: a child pwsh with stdin redirected
|
|
from a file. There, an EMPTY file proves the non-interactive path never reads it.
|
|
Here, a SCRIPTED file drives the interactive menu exactly as a human typing
|
|
answers would, which is the only way to exercise Read-Host-based prompts from
|
|
a top-level script that is not a dot-sourceable module.
|
|
#>
|
|
|
|
BeforeAll {
|
|
$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
. (Join-Path $repoRoot 'tests/TestHelpers.ps1')
|
|
|
|
$script:editor = Join-Path $repoRoot 'Edit-PersonaEngineConfig.ps1'
|
|
$script:scratch = Join-Path ([System.IO.Path]::GetTempPath()) ("pe-structural-{0}" -f [guid]::NewGuid().ToString('N'))
|
|
$null = New-Item -ItemType Directory -Path $script:scratch -Force
|
|
|
|
function Invoke-EditorWithScriptedInput {
|
|
<#
|
|
Runs the editor interactively (no -NonInteractive) with stdin fed from a
|
|
sequence of scripted answers, one per line, and a hard timeout so a
|
|
prompt sequence that runs dry (and therefore blocks on Read-Host) fails
|
|
the test instead of hanging the suite.
|
|
#>
|
|
param(
|
|
[string[]] $ArgumentList,
|
|
[string[]] $Answers,
|
|
[int] $TimeoutSeconds = 60
|
|
)
|
|
|
|
$stdinFile = Join-Path $script:scratch ("in-{0}.txt" -f [guid]::NewGuid().ToString('N'))
|
|
Set-Content -LiteralPath $stdinFile -Value ($Answers -join "`n") -NoNewline -Encoding utf8NoBOM
|
|
|
|
$stdout = Join-Path $script:scratch ("out-{0}.txt" -f [guid]::NewGuid().ToString('N'))
|
|
$stderr = Join-Path $script:scratch ("err-{0}.txt" -f [guid]::NewGuid().ToString('N'))
|
|
|
|
$process = Start-Process -FilePath (Get-Process -Id $PID).Path `
|
|
-ArgumentList (@('-NoProfile', '-File', $script:editor) + $ArgumentList) `
|
|
-RedirectStandardInput $stdinFile `
|
|
-RedirectStandardOutput $stdout `
|
|
-RedirectStandardError $stderr `
|
|
-PassThru -WindowStyle Hidden
|
|
|
|
if (-not $process.WaitForExit($TimeoutSeconds * 1000)) {
|
|
$process.Kill($true)
|
|
return [pscustomobject]@{ ExitCode = -1; Output = 'TIMED OUT'; Error = ''; TimedOut = $true }
|
|
}
|
|
|
|
[pscustomobject]@{
|
|
ExitCode = $process.ExitCode
|
|
Output = (Get-Content -LiteralPath $stdout -Raw -ErrorAction SilentlyContinue)
|
|
Error = (Get-Content -LiteralPath $stderr -Raw -ErrorAction SilentlyContinue)
|
|
TimedOut = $false
|
|
}
|
|
}
|
|
|
|
function New-ScratchConfigPath {
|
|
param([hashtable] $Document = (New-TestConfigurationDocument))
|
|
Save-TestConfiguration -Document $Document -Directory $script:scratch
|
|
}
|
|
}
|
|
|
|
AfterAll {
|
|
Remove-Item -LiteralPath $script:scratch -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Describe 'Interactive rule add/edit/delete (FR-027 - FR-030)' {
|
|
|
|
It 'adds a rule and persists it only after Save' {
|
|
$path = New-ScratchConfigPath
|
|
|
|
$answers = @(
|
|
'A'
|
|
'RULE-0500-TEST'; 'Test rule'; 'A rule added by a test'; '500'; 'Employee'; 'Y'
|
|
'all'
|
|
'C'; 'P'; 'isNotNull'; 'JobTitle'
|
|
'N'
|
|
'S'
|
|
'Q'
|
|
)
|
|
|
|
$run = Invoke-EditorWithScriptedInput -ArgumentList @('-ConfigPath', $path) -Answers $answers
|
|
|
|
$run.TimedOut | Should -BeFalse
|
|
$run.ExitCode | Should -Be 0
|
|
$run.Output | Should -Match 'Rule RULE-0500-TEST added'
|
|
$run.Output | Should -Match 'Saved:'
|
|
|
|
$saved = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json -Depth 32
|
|
@($saved.rules | Where-Object { $_.id -eq 'RULE-0500-TEST' }).Count | Should -Be 1
|
|
}
|
|
|
|
It 'leaves the file untouched when an add is not saved' {
|
|
$path = New-ScratchConfigPath
|
|
$before = Get-Content -LiteralPath $path -Raw
|
|
|
|
$answers = @(
|
|
'A'
|
|
'RULE-0500-TEST'; 'Test rule'; 'A rule added by a test'; '500'; 'Employee'; 'Y'
|
|
'all'
|
|
'C'; 'P'; 'isNotNull'; 'JobTitle'
|
|
'N'
|
|
'Q'; 'y'
|
|
)
|
|
|
|
$run = Invoke-EditorWithScriptedInput -ArgumentList @('-ConfigPath', $path) -Answers $answers
|
|
|
|
$run.TimedOut | Should -BeFalse
|
|
$run.ExitCode | Should -Be 0
|
|
(Get-Content -LiteralPath $path -Raw) | Should -Be $before
|
|
}
|
|
|
|
It 'rejects a duplicate rule id with a message naming the conflict, and does not save it' {
|
|
$path = New-ScratchConfigPath
|
|
|
|
$answers = @(
|
|
'A'
|
|
'RULE-0010-GUEST'; 'Duplicate'; 'Collides with an existing id'; '999'; 'Employee'; 'Y'
|
|
'all'
|
|
'C'; 'P'; 'isNotNull'; 'JobTitle'
|
|
'N'
|
|
'Q'
|
|
)
|
|
|
|
$run = Invoke-EditorWithScriptedInput -ArgumentList @('-ConfigPath', $path) -Answers $answers
|
|
|
|
$run.TimedOut | Should -BeFalse
|
|
$run.Output | Should -Match 'RULE-0010-GUEST'
|
|
$run.Output | Should -Match 'Add cancelled'
|
|
|
|
$saved = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json -Depth 32
|
|
@($saved.rules).Count | Should -Be 2
|
|
}
|
|
|
|
It 'deletes a rule after confirmation and persists it only after Save' {
|
|
$path = New-ScratchConfigPath
|
|
|
|
$answers = @('D', 'RULE-0010-GUEST', 'y', 'S', 'Q')
|
|
|
|
$run = Invoke-EditorWithScriptedInput -ArgumentList @('-ConfigPath', $path) -Answers $answers
|
|
|
|
$run.TimedOut | Should -BeFalse
|
|
$run.Output | Should -Match 'Rule RULE-0010-GUEST deleted'
|
|
|
|
$saved = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json -Depth 32
|
|
@($saved.rules | Where-Object { $_.id -eq 'RULE-0010-GUEST' }).Count | Should -Be 0
|
|
}
|
|
|
|
It 'declining the delete confirmation leaves the rule in place' {
|
|
$path = New-ScratchConfigPath
|
|
|
|
$answers = @('D', 'RULE-0010-GUEST', 'n', 'Q')
|
|
|
|
$run = Invoke-EditorWithScriptedInput -ArgumentList @('-ConfigPath', $path) -Answers $answers
|
|
|
|
$run.TimedOut | Should -BeFalse
|
|
$run.Output | Should -Match 'Delete cancelled'
|
|
|
|
$saved = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json -Depth 32
|
|
@($saved.rules | Where-Object { $_.id -eq 'RULE-0010-GUEST' }).Count | Should -Be 1
|
|
}
|
|
|
|
It 'rejects a condition-tree edit that would exceed the configured nesting depth, using the real validator finding' {
|
|
# maxConditionDepth: 2 means root(1) -> leaf(2) is the deepest a rule may go.
|
|
# Nesting a group under the root puts that group's own children at depth 3,
|
|
# which PE-SEM-012 catches - the same code the runtime validator and the [V]
|
|
# command produce (see Test-PersonaConfigurationSemantic.ps1).
|
|
$document = New-TestConfigurationDocument
|
|
$document.engine.maxConditionDepth = 2
|
|
$path = New-ScratchConfigPath -Document $document
|
|
$before = Get-Content -LiteralPath $path -Raw
|
|
|
|
$answers = @(
|
|
'E'; 'RULE-0900-EMPLOYEE'
|
|
'C'; ''
|
|
'G'; 'any'
|
|
'C'; 'P'; 'isNotNull'; 'JobTitle'
|
|
'N'
|
|
'Q'
|
|
)
|
|
|
|
$run = Invoke-EditorWithScriptedInput -ArgumentList @('-ConfigPath', $path) -Answers $answers
|
|
|
|
$run.TimedOut | Should -BeFalse
|
|
$run.Output | Should -Match 'Edit rejected'
|
|
$run.Output | Should -Match 'PE-SEM-012'
|
|
|
|
(Get-Content -LiteralPath $path -Raw) | Should -Be $before
|
|
}
|
|
|
|
It 'an accepted condition-tree edit is held in memory and appears after Save' {
|
|
$document = New-TestConfigurationDocument
|
|
$path = New-ScratchConfigPath -Document $document
|
|
|
|
$answers = @(
|
|
'E'; 'RULE-0900-EMPLOYEE'
|
|
'C'; ''
|
|
'C'; 'P'; 'isNotNull'; 'JobTitle'
|
|
'S'; 'Q'
|
|
)
|
|
|
|
$run = Invoke-EditorWithScriptedInput -ArgumentList @('-ConfigPath', $path) -Answers $answers
|
|
|
|
$run.TimedOut | Should -BeFalse
|
|
$run.Output | Should -Match 'Rule RULE-0900-EMPLOYEE updated'
|
|
$run.Output | Should -Match 'Saved:'
|
|
|
|
$saved = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json -Depth 32
|
|
$rule = $saved.rules | Where-Object { $_.id -eq 'RULE-0900-EMPLOYEE' }
|
|
@($rule.match.conditions).Count | Should -Be 2
|
|
}
|
|
}
|