diff --git a/tests/Local-Runbook.Tests.ps1 b/tests/Local-Runbook.Tests.ps1 new file mode 100644 index 000000000..be09cdabe --- /dev/null +++ b/tests/Local-Runbook.Tests.ps1 @@ -0,0 +1,66 @@ +# Requires -Version 5.1 +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Describe 'Local-Runbook topology summary' -Tag 'Unit' { + It 'auto-stages a runbook report for loop runs and prints loop execution topology' { + $repoRoot = Join-Path $TestDrive 'repo' + $toolsDir = Join-Path $repoRoot 'tools' + $scriptsDir = Join-Path $repoRoot 'scripts' + New-Item -ItemType Directory -Path $toolsDir -Force | Out-Null + New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null + + Copy-Item -LiteralPath (Join-Path $PSScriptRoot '..' 'tools' 'Local-Runbook.ps1') -Destination (Join-Path $toolsDir 'Local-Runbook.ps1') -Force + + $stub = @" +param( + [switch]`$All, + [string[]]`$Phases, + [string]`$JsonReport, + [switch]`$FailOnDiff, + [switch]`$PassThru +) +if ([string]::IsNullOrWhiteSpace(`$JsonReport)) { + throw 'Expected JsonReport path' +} +`$payload = [ordered]@{ + schema = 'integration-runbook-v1' + generated = '2026-03-24T13:00:00Z' + overallStatus = 'Passed' + phases = @( + [ordered]@{ + name = 'Loop' + status = 'Passed' + details = [ordered]@{ + executionTopology = [ordered]@{ + runtimeSurface = 'windows-native-teststand' + processModelClass = 'parallel-process-model' + executionCellLeaseId = 'lease-hooke-loop-01' + harnessInstanceLeaseId = 'harness-lease-hooke-loop-01' + harnessInstanceId = 'ts-hooke-loop-01' + } + } + } + ) +} +(`$payload | ConvertTo-Json -Depth 8) | Set-Content -LiteralPath `$JsonReport -Encoding UTF8 +exit 0 +"@ + Set-Content -LiteralPath (Join-Path $scriptsDir 'Invoke-IntegrationRunbook.ps1') -Value $stub -Encoding UTF8 + + Push-Location $repoRoot + try { + $output = & pwsh -NoLogo -NoProfile -File (Join-Path $toolsDir 'Local-Runbook.ps1') -Profile loop 2>&1 | Out-String + $LASTEXITCODE | Should -Be 0 + } finally { + Pop-Location + } + + $output | Should -Match 'Loop Execution Topology:' + $output | Should -Match 'runtimeSurface: windows-native-teststand' + $output | Should -Match 'processModelClass: parallel-process-model' + $output | Should -Match 'executionCellLeaseId: lease-hooke-loop-01' + $output | Should -Match 'harnessInstanceLeaseId: harness-lease-hooke-loop-01' + $output | Should -Match 'harnessInstanceId: ts-hooke-loop-01' + } +} diff --git a/tools/Local-Runbook.ps1 b/tools/Local-Runbook.ps1 index c21a2a5e5..fc86620f5 100644 --- a/tools/Local-Runbook.ps1 +++ b/tools/Local-Runbook.ps1 @@ -14,6 +14,61 @@ $ErrorActionPreference = 'Stop' $scriptRoot = Split-Path -Parent $PSCommandPath $repoRoot = Resolve-Path (Join-Path $scriptRoot '..') | Select-Object -ExpandProperty Path +function Get-EffectiveJsonReportPath { + param( + [AllowNull()][string]$RequestedPath, + [string[]]$SelectedPhases, + [switch]$RunAll, + [string]$RepoRoot + ) + + if (-not [string]::IsNullOrWhiteSpace($RequestedPath)) { + return $RequestedPath + } + + if ($RunAll -or ($SelectedPhases -contains 'Loop')) { + $outDir = Join-Path $RepoRoot 'tests' 'results' '_agent' 'local-runbook' + if (-not (Test-Path -LiteralPath $outDir)) { + New-Item -ItemType Directory -Path $outDir -Force | Out-Null + } + return (Join-Path $outDir 'local-runbook-report.json') + } + + return $null +} + +function Write-LoopExecutionTopologySummary { + param([AllowNull()][string]$JsonReportPath) + + if ([string]::IsNullOrWhiteSpace($JsonReportPath)) { + return + } + if (-not (Test-Path -LiteralPath $JsonReportPath -PathType Leaf)) { + return + } + + try { + $report = Get-Content -LiteralPath $JsonReportPath -Raw | ConvertFrom-Json -ErrorAction Stop + $loopPhase = @($report.phases) | Where-Object { $_.name -eq 'Loop' } | Select-Object -First 1 + if (-not $loopPhase) { + return + } + $executionTopology = if ($loopPhase.details.PSObject.Properties.Name -contains 'executionTopology') { $loopPhase.details.executionTopology } else { $null } + if (-not $executionTopology) { + return + } + + Write-Output 'Loop Execution Topology:' + if ($executionTopology.runtimeSurface) { Write-Output (" runtimeSurface: {0}" -f $executionTopology.runtimeSurface) } + if ($executionTopology.processModelClass) { Write-Output (" processModelClass: {0}" -f $executionTopology.processModelClass) } + if ($executionTopology.executionCellLeaseId) { Write-Output (" executionCellLeaseId: {0}" -f $executionTopology.executionCellLeaseId) } + if ($executionTopology.harnessInstanceLeaseId) { Write-Output (" harnessInstanceLeaseId: {0}" -f $executionTopology.harnessInstanceLeaseId) } + if ($executionTopology.harnessInstanceId) { Write-Output (" harnessInstanceId: {0}" -f $executionTopology.harnessInstanceId) } + } catch { + Write-Warning ("Failed to read loop execution topology from JSON report: {0}" -f $_.Exception.Message) + } +} + Push-Location $repoRoot try { Write-Host "=== Local Runbook ===" -ForegroundColor Cyan @@ -50,6 +105,7 @@ try { $env:RUNBOOK_LOOP_ITERATIONS = '1' $env:RUNBOOK_LOOP_QUICK = '1' if ($FailOnDiff) { $env:RUNBOOK_LOOP_FAIL_ON_DIFF = '1' } else { Remove-Item Env:RUNBOOK_LOOP_FAIL_ON_DIFF -ErrorAction SilentlyContinue } + $effectiveJsonReport = Get-EffectiveJsonReportPath -RequestedPath $JsonReport -SelectedPhases $selectedPhases -RunAll:$All -RepoRoot $repoRoot $runbookArgs = @() if ($All) { $runbookArgs += '-All' } @@ -57,7 +113,7 @@ try { $runbookArgs += @('-Phases', ($selectedPhases -join ',')) } if ($FailOnDiff) { $runbookArgs += '-FailOnDiff' } - if ($JsonReport) { $runbookArgs += @('-JsonReport', $JsonReport) } + if ($effectiveJsonReport) { $runbookArgs += @('-JsonReport', $effectiveJsonReport) } if ($PassThru) { $runbookArgs += '-PassThru' } Write-Host "Invoking Invoke-IntegrationRunbook.ps1 with arguments:" -ForegroundColor Gray @@ -66,8 +122,10 @@ try { $runbookArgs | ForEach-Object { Write-Host (" {0}" -f $_) -ForegroundColor DarkGray } } - & "$repoRoot/scripts/Invoke-IntegrationRunbook.ps1" @runbookArgs - exit $LASTEXITCODE + & pwsh -NoLogo -NoProfile -File "$repoRoot/scripts/Invoke-IntegrationRunbook.ps1" @runbookArgs + $runbookExitCode = $LASTEXITCODE + Write-LoopExecutionTopologySummary -JsonReportPath $effectiveJsonReport + exit $runbookExitCode } finally { Pop-Location