Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions src/IdLE.Core/Private/Assert-IdleConditionPathsResolvable.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ function Assert-IdleConditionPathsResolvable {

[Parameter()]
[AllowNull()]
[object] $WarningSink
[object] $WarningSink,

# When set, skips validation of paths used by the Exists operator.
# Exists semantics intentionally allow missing paths (returns $false if absent),
# so strict execution-time path validation should exclude those paths.
[Parameter()]
[switch] $ExcludeExistsOperatorPaths
)

function Add-IdlePathIfPresent {
Expand Down Expand Up @@ -61,13 +67,16 @@ function Assert-IdleConditionPathsResolvable {

[Parameter(Mandatory)]
[AllowEmptyCollection()]
[System.Collections.Generic.List[string]] $PathList
[System.Collections.Generic.List[string]] $PathList,

[Parameter()]
[switch] $ExcludeExistsPaths
)

if ($Node.Contains('All')) {
foreach ($child in @($Node.All)) {
if ($child -is [System.Collections.IDictionary]) {
Get-IdleConditionPaths -Node $child -PathList $PathList
Get-IdleConditionPaths -Node $child -PathList $PathList -ExcludeExistsPaths:$ExcludeExistsPaths
}
}
return
Expand All @@ -76,7 +85,7 @@ function Assert-IdleConditionPathsResolvable {
if ($Node.Contains('Any')) {
foreach ($child in @($Node.Any)) {
if ($child -is [System.Collections.IDictionary]) {
Get-IdleConditionPaths -Node $child -PathList $PathList
Get-IdleConditionPaths -Node $child -PathList $PathList -ExcludeExistsPaths:$ExcludeExistsPaths
}
}
return
Expand All @@ -85,7 +94,7 @@ function Assert-IdleConditionPathsResolvable {
if ($Node.Contains('None')) {
foreach ($child in @($Node.None)) {
if ($child -is [System.Collections.IDictionary]) {
Get-IdleConditionPaths -Node $child -PathList $PathList
Get-IdleConditionPaths -Node $child -PathList $PathList -ExcludeExistsPaths:$ExcludeExistsPaths
}
}
return
Expand All @@ -102,12 +111,17 @@ function Assert-IdleConditionPathsResolvable {
}

if ($Node.Contains('Exists')) {
$existsVal = $Node.Exists
if ($existsVal -is [string]) {
Add-IdlePathIfPresent -PathList $PathList -PathCandidate $existsVal
}
elseif ($existsVal -is [System.Collections.IDictionary]) {
Add-IdlePathIfPresent -PathList $PathList -PathCandidate $existsVal.Path
# Exists operator semantics: checking for the presence of a path is intentional.
# When -ExcludeExistsPaths is set (e.g. strict execution-time validation), skip these
# so that Exists can still return $false without causing a path-not-found error.
if (-not $ExcludeExistsPaths) {
$existsVal = $Node.Exists
if ($existsVal -is [string]) {
Add-IdlePathIfPresent -PathList $PathList -PathCandidate $existsVal
}
elseif ($existsVal -is [System.Collections.IDictionary]) {
Add-IdlePathIfPresent -PathList $PathList -PathCandidate $existsVal.Path
}
}
return
}
Expand All @@ -119,7 +133,7 @@ function Assert-IdleConditionPathsResolvable {
}

$paths = [System.Collections.Generic.List[string]]::new()
Get-IdleConditionPaths -Node $Condition -PathList $paths
Get-IdleConditionPaths -Node $Condition -PathList $paths -ExcludeExistsPaths:$ExcludeExistsOperatorPaths

$uniquePaths = @($paths | Select-Object -Unique)
if ($uniquePaths.Count -eq 0) {
Expand Down
17 changes: 13 additions & 4 deletions src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,13 @@ function Invoke-IdlePlanObject {
# Fail closed: a malformed or unexpected node type is treated as a failed precondition.
$preconditionPassed = $false
}
elseif (-not (Test-IdleCondition -Condition ([hashtable]$stepPrecondition) -Context $preconditionContext)) {
$preconditionPassed = $false
else {
# Validate that all non-Exists paths exist at execution time.
# Exists operator paths are excluded because Exists semantics intentionally allow missing paths.
Assert-IdleConditionPathsResolvable -Condition ([hashtable]$stepPrecondition) -Context $preconditionContext -StepName $stepName -Source 'Precondition' -ExcludeExistsOperatorPaths
if (-not (Test-IdleCondition -Condition ([hashtable]$stepPrecondition) -Context $preconditionContext)) {
$preconditionPassed = $false
}
}

if (-not $preconditionPassed) {
Expand Down Expand Up @@ -626,8 +631,12 @@ function Invoke-IdlePlanObject {
if ($ofPrecondition -isnot [System.Collections.IDictionary]) {
$ofPreconditionPassed = $false
}
elseif (-not (Test-IdleCondition -Condition ([hashtable]$ofPrecondition) -Context $preconditionContext)) {
$ofPreconditionPassed = $false
else {
# Validate that all non-Exists paths exist at execution time.
Assert-IdleConditionPathsResolvable -Condition ([hashtable]$ofPrecondition) -Context $preconditionContext -StepName $ofName -Source 'Precondition' -ExcludeExistsOperatorPaths
if (-not (Test-IdleCondition -Condition ([hashtable]$ofPrecondition) -Context $preconditionContext)) {
$ofPreconditionPassed = $false
}
}

if (-not $ofPreconditionPassed) {
Expand Down
19 changes: 19 additions & 0 deletions tests/Core/Invoke-IdlePlan.Preconditions.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -374,5 +374,24 @@ Describe 'Invoke-IdlePlan - Runtime Preconditions' {
@($result.Events | Where-Object Type -eq 'OnFailureRan').Count | Should -Be 0
}
}

Context 'Unresolvable precondition path at execution time' {
It 'throws when a non-Exists precondition path is missing from the request context at invoke time' {
$wfPath = Join-Path -Path $script:FixturesPath -ChildPath 'missing-context-at-invoke.psd1'
$req = New-IdleRequest -LifecycleEvent 'Leaver'
$providers = @{
StepRegistry = @{ 'IdLE.Step.MissingContextAtInvoke' = 'Invoke-IdlePreconditionTestNoopStep' }
StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.MissingContextAtInvoke')
}

# Planning succeeds with a soft warning for the missing Request.Context path.
$plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers
$plan | Should -Not -BeNullOrEmpty
@($plan.Warnings).Count | Should -BeGreaterThan 0

# Execution must throw because the path is still missing at runtime.
{ Invoke-IdlePlan -Plan $plan -Providers $providers } | Should -Throw '*unresolved condition path*'
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@{
Name = 'Missing Context Path At Invoke'
LifecycleEvent = 'Leaver'
Steps = @(
@{
Name = 'Step1'
Type = 'IdLE.Step.MissingContextAtInvoke'
Precondition = @{
All = @(
@{ In = @{ Path = 'Request.Context.NA'; Values = @('EU', 'DE') } }
)
}
}
)
}