param( [string]$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot ".." )).Path, [string]$EnvFile = ".env", [string]$SampleImageUrl = "https://files.skinbase.org/img/aa/bb/cc/md.webp", [switch]$SkipAnalyze ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" function Write-Info([string]$Message) { Write-Host "[INFO] $Message" -ForegroundColor Cyan } function Write-Ok([string]$Message) { Write-Host "[OK] $Message" -ForegroundColor Green } function Write-Fail([string]$Message) { Write-Host "[FAIL] $Message" -ForegroundColor Red } function Get-EnvMap([string]$Root, [string]$RelativeEnvFile) { $map = @{} $envPath = Join-Path $Root $RelativeEnvFile if (-not (Test-Path $envPath)) { $fallback = Join-Path $Root ".env.example" if (Test-Path $fallback) { Write-Info "Env file '$RelativeEnvFile' not found. Falling back to .env.example." $envPath = $fallback } else { throw "Neither '$RelativeEnvFile' nor '.env.example' was found in $Root" } } Get-Content -Path $envPath | ForEach-Object { $line = $_.Trim() if ($line -eq "" -or $line.StartsWith("#")) { return } $idx = $line.IndexOf("=") if ($idx -lt 1) { return } $key = $line.Substring(0, $idx).Trim() $val = $line.Substring($idx + 1).Trim() if ($val.StartsWith('"') -and $val.EndsWith('"') -and $val.Length -ge 2) { $val = $val.Substring(1, $val.Length - 2) } if (-not $map.ContainsKey($key)) { $map[$key] = $val } } return $map } function Get-Setting([hashtable]$Map, [string]$Key, [string]$Default = "") { $fromProcess = [Environment]::GetEnvironmentVariable($Key) if (-not [string]::IsNullOrWhiteSpace($fromProcess)) { return $fromProcess.Trim() } if ($Map.ContainsKey($Key)) { return [string]$Map[$Key] } return $Default } function Test-Truthy([string]$Value, [bool]$Default = $false) { if ([string]::IsNullOrWhiteSpace($Value)) { return $Default } switch ($Value.Trim().ToLowerInvariant()) { "1" { return $true } "true" { return $true } "yes" { return $true } "on" { return $true } "0" { return $false } "false" { return $false } "no" { return $false } "off" { return $false } default { return $Default } } } function Join-Url([string]$Base, [string]$Path) { $left = $Base.TrimEnd('/') $right = $Path.TrimStart('/') return "$left/$right" } function Invoke-Health([string]$Name, [string]$BaseUrl) { if ([string]::IsNullOrWhiteSpace($BaseUrl)) { throw "$Name base URL is empty" } $url = Join-Url $BaseUrl "/health" Write-Info "Checking $Name health: $url" try { $response = Invoke-WebRequest -Uri $url -Method GET -TimeoutSec 10 -UseBasicParsing } catch { throw "$Name health request failed: $($_.Exception.Message)" } if ($response.StatusCode -lt 200 -or $response.StatusCode -ge 300) { throw "$Name health returned status $($response.StatusCode)" } Write-Ok "$Name health check passed" } function Invoke-Analyze([string]$ClipBaseUrl, [string]$AnalyzeEndpoint, [string]$ImageUrl) { if ([string]::IsNullOrWhiteSpace($ClipBaseUrl)) { throw "CLIP base URL is empty" } $url = Join-Url $ClipBaseUrl $AnalyzeEndpoint Write-Info "Running sample CLIP analyze call: $url" $payload = @{ image_url = $ImageUrl } | ConvertTo-Json -Depth 4 try { $response = Invoke-WebRequest -Uri $url -Method POST -ContentType "application/json" -Body $payload -TimeoutSec 15 -UseBasicParsing } catch { throw "CLIP analyze request failed: $($_.Exception.Message)" } if ($response.StatusCode -lt 200 -or $response.StatusCode -ge 300) { throw "CLIP analyze returned status $($response.StatusCode)" } $json = $null try { $json = $response.Content | ConvertFrom-Json } catch { throw "CLIP analyze response is not valid JSON" } $hasTags = $false if ($json -is [System.Collections.IEnumerable] -and -not ($json -is [string])) { $hasTags = $true } if ($null -ne $json.tags) { $hasTags = $true } if ($null -ne $json.data) { $hasTags = $true } if (-not $hasTags) { throw "CLIP analyze response does not contain expected tags/data payload" } Write-Ok "Sample CLIP analyze call passed" } function Test-NonBlockingPublish([string]$Root) { Write-Info "Validating non-blocking publish path (code assertions)" $uploadController = Join-Path $Root "app/Http/Controllers/Api/UploadController.php" $derivativesJob = Join-Path $Root "app/Jobs/GenerateDerivativesJob.php" if (-not (Test-Path $uploadController)) { throw "Missing file: $uploadController" } if (-not (Test-Path $derivativesJob)) { throw "Missing file: $derivativesJob" } $uploadText = Get-Content -Raw -Path $uploadController $jobText = Get-Content -Raw -Path $derivativesJob if ($uploadText -notmatch "AutoTagArtworkJob::dispatch\(") { throw "UploadController does not dispatch AutoTagArtworkJob" } if ($jobText -notmatch "AutoTagArtworkJob::dispatch\(") { throw "GenerateDerivativesJob does not dispatch AutoTagArtworkJob" } if ($uploadText -match "dispatchSync\(" -or $jobText -match "dispatchSync\(") { throw "Found dispatchSync in publish path; auto-tagging must remain async" } if ($uploadText -match "Illuminate\\Support\\Facades\\Http" -or $uploadText -match "Http::") { throw "UploadController appears to call external vision HTTP directly" } Write-Ok "Non-blocking publish path validation passed" } $failed = $false try { Write-Info "Vision smoke test starting" Write-Info "Project root: $ProjectRoot" $envMap = Get-EnvMap -Root $ProjectRoot -RelativeEnvFile $EnvFile $visionEnabled = Test-Truthy (Get-Setting -Map $envMap -Key "VISION_ENABLED" -Default "true") $true if (-not $visionEnabled) { throw "VISION_ENABLED=false. Vision integration is disabled; smoke check cannot continue." } $clipBaseUrl = Get-Setting -Map $envMap -Key "CLIP_BASE_URL" $clipAnalyzeEndpoint = Get-Setting -Map $envMap -Key "CLIP_ANALYZE_ENDPOINT" -Default "/analyze" $yoloEnabled = Test-Truthy (Get-Setting -Map $envMap -Key "YOLO_ENABLED" -Default "true") $true $yoloBaseUrl = Get-Setting -Map $envMap -Key "YOLO_BASE_URL" Invoke-Health -Name "CLIP" -BaseUrl $clipBaseUrl if ($yoloEnabled) { if ([string]::IsNullOrWhiteSpace($yoloBaseUrl)) { Write-Info "YOLO is enabled but YOLO_BASE_URL is empty; skipping YOLO /health check." } else { Invoke-Health -Name "YOLO" -BaseUrl $yoloBaseUrl } } else { Write-Info "YOLO is disabled; skipping YOLO /health check." } if ($SkipAnalyze) { Write-Info "Skipping sample analyze call (SkipAnalyze set)." } else { Invoke-Analyze -ClipBaseUrl $clipBaseUrl -AnalyzeEndpoint $clipAnalyzeEndpoint -ImageUrl $SampleImageUrl } Test-NonBlockingPublish -Root $ProjectRoot Write-Ok "Vision smoke test completed successfully" } catch { $failed = $true Write-Fail $_.Exception.Message } if ($failed) { exit 1 } exit 0