tests/RepoHerd.Integration.Tests.ps1
|
#Requires -Version 7.6 #Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0' } <# .SYNOPSIS Integration tests for RepoHerd - runs the script against all test configs .DESCRIPTION Executes RepoHerd.ps1 against each of the 16 test JSON configs and asserts the expected exit code and structured JSON output. Tests perform actual git clones with recursive dependency processing to exercise the full checkout flow including API compatibility checks. Requires network access to GitHub test repos. Run with: Invoke-Pester ./tests/RepoHerd.Integration.Tests.ps1 -Output Detailed Skip with: Use -ExcludeTag 'Integration' to skip when no network is available #> BeforeDiscovery { $script:ScriptRoot = Split-Path $PSScriptRoot -Parent $script:TestConfigDir = Join-Path $script:ScriptRoot 'tests' # Test matrix: config filename -> expected exit code and repository count # All test configs are named dependencies.json in subdirectories so the filename # propagates correctly through all recursive depth levels. # ExpectedTags: expected final tag per repo URL suffix (e.g., 'RootA' -> 'v3.0.0'). # Repo URL suffixes used: RootA, RootB, TestA, TestB, TestC $script:TestCases = @( # SemVer mode tests @{ Config = 'semver-basic/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'SemVer'; HasRootScript = $false ExpectedTags = @{ RootA = 'v3.0.0'; RootB = 'v3.0.0'; TestA = 'v3.0.0'; TestB = 'v3.0.0'; TestC = 'v3.0.0' } Label = 'SemVer basic' } @{ Config = 'semver-floating-versions/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'SemVer'; HasRootScript = $false ExpectedTags = @{ RootA = 'v3.0.1'; RootB = 'v3.0.0'; TestA = 'v3.0.0'; TestB = 'v3.0.1'; TestC = 'v3.0.2' } Label = 'SemVer floating versions' } @{ Config = 'semver-floating-versions-2/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'SemVer'; HasRootScript = $false ExpectedTags = @{ RootA = 'v3.0.2'; RootB = 'v3.0.0'; TestA = 'v3.0.0'; TestB = 'v3.0.2'; TestC = 'v3.1.0' } Label = 'SemVer floating versions 2' } @{ Config = 'semver-custom-dep-path-1/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'SemVer'; HasRootScript = $false ExpectedTags = @{ RootA = 'v3.0.0'; RootB = 'v3.0.2'; TestA = 'v3.0.0'; TestB = 'v3.0.0'; TestC = 'v3.0.0' } Label = 'SemVer custom dep path 1' } @{ Config = 'semver-custom-dep-path-2/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'SemVer'; HasRootScript = $false ExpectedTags = @{ RootA = 'v3.0.0'; RootB = 'v3.0.3'; TestA = 'v3.0.0'; TestB = 'v3.0.0'; TestC = 'v3.0.0' } Label = 'SemVer custom dep path 2' } @{ Config = 'semver-post-checkout-scripts/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'SemVer'; HasRootScript = $false ExpectedTags = @{ RootA = 'v3.0.0'; RootB = 'v3.0.4'; TestA = 'v3.0.0'; TestB = 'v3.0.0'; TestC = 'v3.0.0' } Label = 'SemVer post-checkout scripts' } @{ Config = 'semver-post-checkout-scripts-2/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'SemVer'; HasRootScript = $false ExpectedTags = @{ RootA = 'v3.0.0'; RootB = 'v3.0.5'; TestA = 'v3.0.0'; TestB = 'v3.0.0'; TestC = 'v3.0.0' } Label = 'SemVer post-checkout scripts 2' } @{ Config = 'semver-post-checkout-scripts-depth-0/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'SemVer'; HasRootScript = $true ExpectedTags = @{ RootA = 'v3.0.0'; RootB = 'v3.0.5'; TestA = 'v3.0.0'; TestB = 'v3.0.0'; TestC = 'v3.0.0' } Label = 'SemVer post-checkout depth 0' } # Agnostic mode tests @{ Config = 'agnostic-recursive/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'Agnostic'; HasRootScript = $false ExpectedTags = @{ RootA = 'v1.0.0'; RootB = 'v1.0.1'; TestA = 'v1.0.3'; TestB = 'v1.0.3'; TestC = 'v1.0.3' } Label = 'Agnostic recursive' } @{ Config = 'agnostic-custom-dep-path/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'Agnostic'; HasRootScript = $false ExpectedTags = @{ RootA = 'v1.0.0'; RootB = 'v1.0.2'; TestA = 'v1.0.3'; TestB = 'v1.0.3'; TestC = 'v1.0.3' } Label = 'Agnostic custom dep path' } @{ Config = 'agnostic-partial-api-overlap/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'Agnostic'; HasRootScript = $false ExpectedTags = @{ RootA = 'v1.0.0'; RootB = 'v1.0.1'; TestA = 'v1.0.3'; TestB = 'v1.0.3'; TestC = 'v1.0.3' } Label = 'Agnostic partial API overlap' } @{ Config = 'agnostic-post-checkout-scripts/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'Agnostic'; HasRootScript = $false ExpectedTags = @{ RootA = 'v1.0.0'; RootB = 'v1.0.4'; TestA = 'v1.0.3'; TestB = 'v1.0.3'; TestC = 'v1.0.3' } Label = 'Agnostic post-checkout scripts' } @{ Config = 'agnostic-post-checkout-scripts-2/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'Agnostic'; HasRootScript = $false ExpectedTags = @{ RootA = 'v1.0.0'; RootB = 'v1.0.5'; TestA = 'v1.0.3'; TestB = 'v1.0.3'; TestC = 'v1.0.3' } Label = 'Agnostic post-checkout scripts 2' } @{ Config = 'agnostic-post-checkout-scripts-depth-0/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'Agnostic'; HasRootScript = $true ExpectedTags = @{ RootA = 'v1.0.0'; RootB = 'v1.0.4'; TestA = 'v1.0.3'; TestB = 'v1.0.3'; TestC = 'v1.0.3' } Label = 'Agnostic post-checkout depth 0' } # API incompatibility tests @{ Config = 'api-incompatibility-agnostic/dependencies.json'; ExpectedExit = 0; ExpectedRepos = 5; Mode = 'Agnostic'; HasRootScript = $false; ApiMode = 'Permissive' ExpectedTags = @{ RootA = 'v1.0.0'; RootB = 'v1.1.0'; TestA = 'v1.0.3'; TestB = 'v1.0.3'; TestC = 'v1.0.3' } Label = 'Agnostic API incompatibility (Permissive)' } @{ Config = 'api-incompatibility-agnostic/dependencies.json'; ExpectedExit = 1; ExpectedRepos = 5; Mode = 'Agnostic'; HasRootScript = $false; ApiMode = 'Strict' ExpectedTags = @{ RootA = 'v1.0.0'; RootB = 'v1.1.0'; TestA = 'v1.0.3'; TestB = 'v1.0.3'; TestC = 'v2.0.0' } Label = 'Agnostic API incompatibility (Strict)' } @{ Config = 'api-incompatibility-semver/dependencies.json'; ExpectedExit = 1; ExpectedRepos = 5; Mode = 'SemVer'; HasRootScript = $false; ApiMode = 'Permissive' ExpectedTags = @{ RootA = 'v3.0.0'; RootB = 'v3.1.0'; TestA = 'v3.0.0'; TestB = 'v3.0.0'; TestC = 'v4.0.0' } Label = 'SemVer API incompatibility' } ) } Describe 'RepoHerd Integration Tests' -Tag 'Integration' { BeforeAll { $script:ScriptRoot = Split-Path $PSScriptRoot -Parent $script:ScriptPath = Join-Path $script:ScriptRoot 'RepoHerd.ps1' $script:TestConfigDir = Join-Path $script:ScriptRoot 'tests' # Verify the script exists $script:ScriptPath | Should -Exist } BeforeEach { # Clean up cloned test repositories to ensure each test starts fresh # Repos are cloned inside each test's subdirectory $cleanupPatterns = @('test-root-a', 'test-root-b', 'libs') $testSubDirs = Get-ChildItem -Path $script:TestConfigDir -Directory | Where-Object { Test-Path (Join-Path $_.FullName 'dependencies.json') } foreach ($subDir in $testSubDirs) { foreach ($pattern in $cleanupPatterns) { $dir = Join-Path $subDir.FullName $pattern if (Test-Path $dir) { Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue } } } } It '<Label> (<Config>) exits with code <ExpectedExit>' -TestCases $script:TestCases { param($Config, $ExpectedExit, $ExpectedRepos, $Mode, $HasRootScript, $ApiMode, $ExpectedTags, $Label) $configPath = Join-Path $script:TestConfigDir $Config $configPath | Should -Exist # Write JSON output to Pester's auto-cleaned temp directory $outputJson = Join-Path $TestDrive ("result_{0}.json" -f ($Config -replace '[^a-zA-Z0-9]', '_')) # Run the script as a child process — no DryRun so recursive checkout is exercised $scriptArgs = @( '-NoProfile', '-NonInteractive', '-File', $script:ScriptPath, '-InputFile', $configPath, '-OutputFile', $outputJson, '-DisablePostCheckoutScripts' ) if ($ApiMode) { $scriptArgs += @('-ApiCompatibility', $ApiMode) } $output = & pwsh @scriptArgs 2>&1 $actualExit = $LASTEXITCODE # Validate exit code $actualExit | Should -Be $ExpectedExit -Because ( "Config '$Config' should exit $ExpectedExit but got $actualExit.`n" + "Output (last 20 lines):`n$($output | Select-Object -Last 20 | Out-String)" ) # Validate structured JSON output $outputJson | Should -Exist -Because "JSON output file should be written" $result = Get-Content $outputJson -Raw | ConvertFrom-Json # Schema version $result.schemaVersion | Should -Be '1.0.0' # Metadata $result.metadata.toolVersion | Should -Be '9.1.0' $result.metadata.recursiveMode | Should -Be $true $result.metadata.apiCompatibility | Should -BeIn @('Strict', 'Permissive') $result.metadata.powershellVersion | Should -Not -BeNullOrEmpty # Summary $result.summary.success | Should -Be ($ExpectedExit -eq 0) -Because "summary.success should match exit code" $result.summary.totalRepositories | Should -Be $ExpectedRepos -Because "should have exactly $ExpectedRepos unique repositories" # Repositories array $result.repositories | Should -Not -BeNullOrEmpty -Because "should have repository entries" foreach ($repo in $result.repositories) { $repo.url | Should -Not -BeNullOrEmpty $repo.path | Should -Not -BeNullOrEmpty $repo.dependencyResolution | Should -BeIn @('SemVer', 'Agnostic') $repo.status | Should -BeIn @('success', 'failed', 'skipped') $repo.requestedBy | Should -Not -BeNullOrEmpty -Because "every repo should have a requestedBy" } # Mode-specific validations if ($Mode -eq 'SemVer') { $semVerRepos = $result.repositories | Where-Object { $_.dependencyResolution -eq 'SemVer' } $semVerRepos | Should -Not -BeNullOrEmpty -Because "SemVer test should have SemVer repositories" foreach ($repo in $semVerRepos) { $repo.tag | Should -Not -BeNullOrEmpty -Because "SemVer repo should have a selected tag" $repo.selectedVersion | Should -Not -BeNullOrEmpty -Because "SemVer repo should have a selected version" } } # Validate expected tags per repository if ($ExpectedTags) { # Map repo URL suffixes to full URLs $urlMap = @{ RootA = 'https://github.com/LS-Instruments/LsiCheckOutTestRootA.git' RootB = 'https://github.com/LS-Instruments/LsiCheckOutTestRootB.git' TestA = 'https://github.com/LS-Instruments/LsiCheckOutTestA.git' TestB = 'https://github.com/LS-Instruments/LsiCheckOutTestB.git' TestC = 'https://github.com/LS-Instruments/LsiCheckOutTestC.git' } foreach ($entry in $ExpectedTags.GetEnumerator()) { $fullUrl = $urlMap[$entry.Key] $repo = $result.repositories | Where-Object { $_.url -eq $fullUrl } $repo | Should -Not -BeNullOrEmpty -Because "repo $($entry.Key) ($fullUrl) should be in results" $repo.tag | Should -Be $entry.Value -Because "$($entry.Key) should be checked out at tag $($entry.Value)" } } # Processed dependency files $result.processedDependencyFiles | Should -Not -BeNullOrEmpty -Because "at least one dependency file should be processed" # Post-checkout script tracking $result.summary.postCheckoutScripts.enabled | Should -Be $false -Because "tests run with -DisablePostCheckoutScripts" if ($HasRootScript) { $result.rootPostCheckoutScripts | Should -Not -BeNullOrEmpty -Because "depth-0 config declares a post-checkout script" foreach ($pcs in $result.rootPostCheckoutScripts) { $pcs.configured | Should -Be $true $pcs.status | Should -Be 'skipped' $pcs.reason | Should -Be 'Disabled globally via -DisablePostCheckoutScripts' } } # Errors array should be empty for successful tests if ($ExpectedExit -eq 0) { $result.errors.Count | Should -Be 0 -Because "successful run should have no errors" } } } Describe 'SemVer with -DisableRecursion (regression #16)' -Tag 'Integration' { BeforeAll { $script:ScriptRoot = Split-Path $PSScriptRoot -Parent $script:ScriptPath = Join-Path $script:ScriptRoot 'RepoHerd.ps1' $script:TestConfigDir = Join-Path $script:ScriptRoot 'tests' } BeforeEach { # Clean up cloned repos from semver-basic test directory $testDir = Join-Path $script:TestConfigDir 'semver-basic' foreach ($pattern in @('test-root-a', 'test-root-b', 'libs')) { $dir = Join-Path $testDir $pattern if (Test-Path $dir) { Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue } } } It 'resolves SemVer tags correctly without recursive mode' { $configPath = Join-Path $script:TestConfigDir 'semver-basic/dependencies.json' $outputJson = Join-Path $TestDrive 'result_nonrecursive.json' $output = & pwsh -NoProfile -NonInteractive -File $script:ScriptPath ` -InputFile $configPath ` -OutputFile $outputJson ` -DisableRecursion ` -DisablePostCheckoutScripts 2>&1 $actualExit = $LASTEXITCODE $actualExit | Should -Be 0 -Because ( "SemVer with -DisableRecursion should succeed.`n" + "Output (last 20 lines):`n$($output | Select-Object -Last 20 | Out-String)" ) $outputJson | Should -Exist $result = Get-Content $outputJson -Raw | ConvertFrom-Json # Should only have the 2 root repos (no recursive deps) $result.summary.totalRepositories | Should -Be 2 $result.metadata.recursiveMode | Should -Be $false # Both repos should have resolved tags (not empty) foreach ($repo in $result.repositories) { $repo.tag | Should -Not -BeNullOrEmpty -Because "SemVer tag must be resolved even with -DisableRecursion (regression #16)" $repo.selectedVersion | Should -Not -BeNullOrEmpty $repo.status | Should -Be 'success' } # Validate specific tags $rootA = $result.repositories | Where-Object { $_.url -like '*RootA*' } $rootA.tag | Should -Be 'v3.0.0' $rootB = $result.repositories | Where-Object { $_.url -like '*RootB*' } $rootB.tag | Should -Be 'v3.0.0' } } |