scripts/internal/test-publish-harness.ps1
|
# test-publish-harness.ps1 # Trace: T003, T004, T005, FR-003, FR-004, FR-012, SC-001 # # Pre-publish E2E validation harness for Specrew module candidates. # This script runs inside a Docker container with a baseline Specrew v0.27.6 install # and validates that the packaged candidate is publication-ready. # # Validations performed: # 1. FileList integrity: Every Specrew.psd1 FileList entry exists on disk # 2. Version pin drift: .specrew/config.yml specrew_version matches Specrew.psd1 ModuleVersion (Prop 134) # 3. Clean initialization: `specrew init` succeeds in a fresh project # 4. Clean update: `specrew update` transitions succeed without corruption [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$CandidatePath ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # ----------------------------------------------------------------------------- # Helper Functions # ----------------------------------------------------------------------------- function Write-Pass { param([string]$Message) Write-Host "PASS: $Message" -ForegroundColor Green } function Write-Fail { param([string]$Message, [string]$Detail = '') Write-Host "FAIL: $Message" -ForegroundColor Red if ($Detail) { Write-Host " $Detail" -ForegroundColor Yellow } } function Write-Section { param([string]$Title) Write-Host "" Write-Host "========================================" -ForegroundColor Cyan Write-Host " $Title" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan } # ----------------------------------------------------------------------------- # Phase 1: Validate Candidate Structure # ----------------------------------------------------------------------------- Write-Section "Phase 1: Validate Candidate Structure" # Resolve candidate path $CandidatePath = (Resolve-Path -LiteralPath $CandidatePath).Path Write-Host "Candidate path: $CandidatePath" # Locate manifest $manifestPath = Join-Path -Path $CandidatePath -ChildPath 'Specrew.psd1' if (-not (Test-Path -LiteralPath $manifestPath)) { Write-Fail "Specrew.psd1 not found in candidate root." "Expected: $manifestPath" exit 1 } Write-Pass "Found Specrew.psd1 at $manifestPath" # Import manifest try { $manifest = Import-PowerShellDataFile -Path $manifestPath } catch { Write-Fail "Failed to parse Specrew.psd1." $_.Exception.Message exit 1 } Write-Pass "Successfully parsed Specrew.psd1" $candidateVersion = $manifest.ModuleVersion Write-Host "Candidate version: $candidateVersion" -ForegroundColor Cyan # ----------------------------------------------------------------------------- # Phase 2: FileList Integrity Check (FR-003) # ----------------------------------------------------------------------------- Write-Section "Phase 2: FileList Integrity Check" $fileList = @($manifest.FileList | ForEach-Object { [string]$_ }) Write-Host "FileList declares $($fileList.Count) entries." $missingFiles = @() $presentFiles = @() foreach ($relativePath in $fileList) { # Normalize path separators for cross-platform compatibility $normalizedPath = $relativePath -replace '\\', '/' $fullPath = Join-Path -Path $CandidatePath -ChildPath $normalizedPath if (Test-Path -LiteralPath $fullPath) { $presentFiles += $relativePath } else { $missingFiles += $relativePath } } if ($missingFiles.Count -gt 0) { Write-Fail "FileList integrity check FAILED. Missing $($missingFiles.Count) file(s):" foreach ($file in $missingFiles) { Write-Host " ❌ $file" -ForegroundColor Red } exit 1 } Write-Pass "FileList integrity check PASSED. All $($fileList.Count) files exist on disk." # ----------------------------------------------------------------------------- # Phase 3: Version Pin Drift Detection (FR-012, Prop 134) # ----------------------------------------------------------------------------- Write-Section "Phase 3: Version Pin Drift Detection (Prop 134)" $configPath = Join-Path -Path $CandidatePath -ChildPath '.specrew/config.yml' if (-not (Test-Path -LiteralPath $configPath)) { Write-Fail ".specrew/config.yml not found in candidate." "Expected: $configPath" exit 1 } Write-Pass "Found .specrew/config.yml" $configContent = Get-Content -LiteralPath $configPath -Raw -Encoding UTF8 # Parse specrew_version from config.yml if ($configContent -match 'specrew_version:\s*["'']?([0-9]+\.[0-9]+\.[0-9]+)["'']?') { $configVersion = $Matches[1] } else { Write-Fail "Could not parse specrew_version from config.yml" exit 1 } Write-Host "Config specrew_version: $configVersion" -ForegroundColor Cyan Write-Host "Manifest ModuleVersion: $candidateVersion" -ForegroundColor Cyan if ($configVersion -ne $candidateVersion) { Write-Fail "Version pin DRIFT detected!" "Config declares $configVersion but manifest declares $candidateVersion" Write-Host "Prop 134 requires these versions to be synchronized." -ForegroundColor Yellow exit 1 } Write-Pass "Version pin check PASSED. Config and manifest are synchronized at version $candidateVersion" # ----------------------------------------------------------------------------- # Phase 4: Test Project Initialization (FR-003) # ----------------------------------------------------------------------------- Write-Section "Phase 4: Test Project Initialization" # Create a clean test project directory $testProjectPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "specrew-test-$(Get-Random)" New-Item -ItemType Directory -Path $testProjectPath -Force | Out-Null Write-Host "Created test project at: $testProjectPath" # Initialize git repo (required by specrew init) Push-Location $testProjectPath try { git init 2>&1 | Out-Null git config user.email "test@example.com" 2>&1 | Out-Null git config user.name "Test User" 2>&1 | Out-Null Write-Pass "Initialized git repository" # Run specrew init with baseline version (v0.27.6 from PSGallery). # Note: init is non-interactive by default; the recognized flag is the CLI-form # '--force' (specrew-init.ps1 parses '^--force$'). '-NonInteractive' is not a real # option in any version, and '-Force' (PowerShell-style) is not recognized by the # init CLI parser — both yield "Unknown option". Write-Host "Running specrew init with baseline v0.27.6..." try { Initialize-Specrew --force Write-Pass "specrew init succeeded with baseline version" } catch { Write-Fail "specrew init failed with baseline version" $_.Exception.Message exit 1 } # Verify baseline layout $baselineConfigPath = Join-Path -Path $testProjectPath -ChildPath '.specrew/config.yml' if (-not (Test-Path -LiteralPath $baselineConfigPath)) { Write-Fail "Baseline .specrew/config.yml not created by init" exit 1 } Write-Pass "Baseline project structure validated" } finally { Pop-Location } # ----------------------------------------------------------------------------- # Phase 5: specrew update Transition Validation (FR-004) # ----------------------------------------------------------------------------- Write-Section "Phase 5: specrew update Transition Validation" Push-Location $testProjectPath try { # Stage and commit baseline state to enable update git add -A 2>&1 | Out-Null git commit -m "Initial baseline commit" 2>&1 | Out-Null Write-Pass "Committed baseline state" # Unload baseline module and load candidate module Write-Host "Switching to candidate module..." Remove-Module Specrew -Force -ErrorAction SilentlyContinue # Import candidate module explicitly $candidateModulePath = Join-Path -Path $CandidatePath -ChildPath 'Specrew.psm1' Import-Module $candidateModulePath -Force -Global Write-Pass "Loaded candidate module from $candidateModulePath" # Verify candidate module is active $loadedModule = Get-Module Specrew if (-not $loadedModule) { Write-Fail "Candidate module not loaded" exit 1 } Write-Host "Active module version: $($loadedModule.Version)" -ForegroundColor Cyan # Run specrew update to transition from baseline to candidate. specrew-update.ps1 # uses a [switch] param block: -Specrew updates the Specrew-managed assets from the # loaded candidate module; -SkipUpdateCheck avoids the PSGallery latest-version probe # (the candidate 0.28.0 isn't published yet — that's what this harness gates). There # is no -Force / -NonInteractive switch on update. Write-Host "Running specrew update to apply candidate version..." try { Update-Specrew -Specrew -SkipUpdateCheck Write-Pass "specrew update succeeded" } catch { Write-Fail "specrew update failed during baseline->candidate transition" $_.Exception.Message exit 1 } # Verify updated config reflects candidate version $updatedConfigPath = Join-Path -Path $testProjectPath -ChildPath '.specrew/config.yml' $updatedConfigContent = Get-Content -LiteralPath $updatedConfigPath -Raw -Encoding UTF8 if ($updatedConfigContent -match 'specrew_version:\s*["'']?([0-9]+\.[0-9]+\.[0-9]+)["'']?') { $updatedVersion = $Matches[1] if ($updatedVersion -eq $candidateVersion) { Write-Pass "Config updated to candidate version: $updatedVersion" } else { Write-Fail "Config version mismatch after update" "Expected $candidateVersion, got $updatedVersion" exit 1 } } else { Write-Fail "Could not parse specrew_version from updated config.yml" exit 1 } # Verify FileList integrity in updated project Write-Host "Verifying FileList integrity in updated project..." $updateMissingFiles = @() foreach ($relativePath in $fileList) { # Normalize path separators $normalizedPath = $relativePath -replace '\\', '/' # Check in candidate source first (not all files deploy to projects) $candidateFullPath = Join-Path -Path $CandidatePath -ChildPath $normalizedPath # For project-relevant files, check if they should exist in the project # Most module files stay in the module install location, not the project # We're primarily validating the candidate package integrity here if (-not (Test-Path -LiteralPath $candidateFullPath)) { $updateMissingFiles += $relativePath } } if ($updateMissingFiles.Count -gt 0) { Write-Fail "FileList integrity check FAILED after update. Missing $($updateMissingFiles.Count) file(s):" foreach ($file in $updateMissingFiles) { Write-Host " ❌ $file" -ForegroundColor Red } exit 1 } Write-Pass "FileList integrity validated after update transition" # Verify no duplicate Squad entries (FR-013 regression check) $teamPath = Join-Path -Path $testProjectPath -ChildPath '.squad/team.md' if (Test-Path -LiteralPath $teamPath) { $teamContent = Get-Content -LiteralPath $teamPath -Raw -Encoding UTF8 $teamLines = $teamContent -split "`n" | Where-Object { $_.Trim() -match '^\|' -and $_ -notmatch '^\|\s*-' } # Group by line content and find duplicates. @() guards StrictMode: when there # are no duplicates the pipeline yields $null, and $null.Count throws under # Set-StrictMode -Version Latest. Wrapping normalizes to a 0-length array. $duplicates = @($teamLines | Group-Object | Where-Object { $_.Count -gt 1 }) if ($duplicates.Count -gt 0) { Write-Fail "Duplicate Squad team entries detected (FR-013 regression):" foreach ($dup in $duplicates) { Write-Host " ❌ $($dup.Name) (appears $($dup.Count) times)" -ForegroundColor Red } exit 1 } Write-Pass "No duplicate Squad entries detected" } } finally { Pop-Location # Cleanup test project if (Test-Path -LiteralPath $testProjectPath) { Remove-Item -Path $testProjectPath -Recurse -Force -ErrorAction SilentlyContinue } } # ----------------------------------------------------------------------------- # Final Summary # ----------------------------------------------------------------------------- Write-Section "Pre-Publish Validation Summary" Write-Host "✅ All validation checks PASSED" -ForegroundColor Green Write-Host "" Write-Host "Validated candidate: Specrew v$candidateVersion" -ForegroundColor Cyan Write-Host " ✓ FileList integrity ($($fileList.Count) files)" -ForegroundColor Green Write-Host " ✓ Version pin synchronization (Prop 134)" -ForegroundColor Green Write-Host " ✓ Clean project initialization" -ForegroundColor Green Write-Host " ✓ Clean update transition (v0.27.6 → v$candidateVersion)" -ForegroundColor Green Write-Host " ✓ No duplicate Squad entries (FR-013)" -ForegroundColor Green Write-Host "" Write-Host "Candidate is READY for PSGallery publication." -ForegroundColor Green exit 0 |