param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) $ErrorActionPreference = 'Stop' $assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) function Assert-True($Actual, [string]$Message) { if (-not $Actual) { throw $Message } } function Assert-False($Actual, [string]$Message) { if ($Actual) { throw $Message } } function Assert-Equal($Expected, $Actual, [string]$Message) { if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } } function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) { if ([Math]::Abs($Expected - $Actual) -gt 0.000000001) { throw "$Message Expected=$Expected Actual=$Actual" } } function Get-RequiredType([string]$Name) { return $assembly.GetType($Name, $true) } function Assert-AttemptedStrengths($Snapshot, [double[]]$Expected, [string]$Message) { Assert-Equal $Expected.Length $Snapshot.AttemptedStrengths.Count ($Message + ' count') for ($index = 0; $index -lt $Expected.Length; $index++) { Assert-Near $Expected[$index] $Snapshot.AttemptedStrengths[$index] ($Message + " index=$index") } } $root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' $algorithms = $root + 'Algorithms.' $runnerType = Get-RequiredType ($algorithms + 'SmoothingAlgorithmRunner') $smootherType = Get-RequiredType ($algorithms + 'IPathSmoother') Assert-False $smootherType.IsPublic 'IPathSmoother must remain internal to the algorithm assembly.' $hooksType = $runnerType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic') Assert-True ($null -ne $hooksType) 'SmoothingAlgorithmRunner must expose its narrowly scoped nested TestHooks helper.' $executeMethod = $hooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static') Assert-True ($null -ne $executeMethod) 'TestHooks must expose deterministic scenario execution for reflection tests.' function Invoke-Scenario([string]$Scenario) { return $executeMethod.Invoke($null, @($Scenario)) } $allRejected = Invoke-Scenario 'RejectAll' Assert-Equal 'Infeasible' $allRejected.Status 'All rejected finite candidates must produce an Infeasible runner result.' Assert-AttemptedStrengths $allRejected @(1.00, 0.75, 0.50, 0.25) 'Rejected candidates must use the finite retry schedule exactly.' Assert-Equal 0 $allRejected.AcceptedPathPointCount 'An infeasible runner result must not retain a rejected candidate as an accepted path.' Assert-True ($allRejected.RejectedComparisonCandidatePointCount -gt 0) 'Only comparison diagnostics may retain the last rejected candidate geometry.' Assert-Equal 4 $allRejected.FailureCount 'Every rejected validation attempt must retain its failure reason.' $accepted = Invoke-Scenario 'AcceptFirst' Assert-Equal 'Success' $accepted.Status 'The first safe candidate must be accepted.' Assert-AttemptedStrengths $accepted @(1.00) 'The runner must stop immediately after the first accepted candidate.' Assert-True ($accepted.AcceptedPathPointCount -gt 0) 'A successful runner result must publish the validated path internally.' Assert-Equal 0 $accepted.RejectedComparisonCandidatePointCount 'An accepted candidate must not create rejected comparison geometry.' $numericalFailure = Invoke-Scenario 'NumericalFailure' Assert-Equal 'Failed' $numericalFailure.Status 'A numerical candidate failure must stop the runner as Failed.' Assert-AttemptedStrengths $numericalFailure @(1.00) 'Numerical candidate failure must not retry at lower strength.' Assert-Equal 0 $numericalFailure.RejectedComparisonCandidatePointCount 'A non-candidate numerical failure must not retain comparison geometry.' $cancelled = Invoke-Scenario 'CancelBeforeNextAttempt' Assert-True $cancelled.CancellationPropagated 'Cancellation between attempts must propagate out of the runner.' Assert-AttemptedStrengths $cancelled @(1.00) 'Cancellation before the next attempt must prevent another smoother call.' Assert-Equal 0 $cancelled.AcceptedPathPointCount 'A cancelled run must not publish a partial path.' Write-Output 'Path smoothing retry runner checks passed.'