param( [string]$BaseUrl = "http://127.0.0.1:17317", [int]$Limit = 2, [ValidateSet("artifacts", "weapons", "characters", "materials")] [string]$Category = "artifacts", [int]$PollIntervalSeconds = 1, [int]$TimeoutSeconds = 180, [string]$OutputRoot = (Join-Path (Resolve-Path -LiteralPath ".").Path "outputs\native-live-smoke"), [switch]$SkipProbe, [switch]$Persist ) $ErrorActionPreference = "Stop" function ConvertTo-SafeFilePart([string]$Value) { $safe = $Value -replace "[^A-Za-z0-9._-]+", "-" $safe = $safe.Trim("-") if ($safe.Length -eq 0) { return "item" } if ($safe.Length -gt 80) { return $safe.Substring(0, 80) } return $safe } function Invoke-DevJson([string]$Path) { $uri = if ($Path.StartsWith("http")) { $Path } else { "$BaseUrl$Path" } try { Invoke-RestMethod -Method Get -Uri $uri -TimeoutSec 90 } catch { $response = $_.Exception.Response if ($response) { $stream = $response.GetResponseStream() if ($stream) { $reader = New-Object System.IO.StreamReader($stream) $body = $reader.ReadToEnd() if (-not [string]::IsNullOrWhiteSpace($body)) { try { return $body | ConvertFrom-Json } catch { throw "Dev endpoint $uri returned HTTP error with non-JSON body: $body" } } } } throw } } function Save-Json([string]$Name, [object]$Payload) { $path = Join-Path $RunDir "$(ConvertTo-SafeFilePart $Name).json" $Payload | ConvertTo-Json -Depth 40 | Set-Content -LiteralPath $path -Encoding UTF8 return $path } function Test-ProbeSucceeded([object]$ProbePayload) { if ($ProbePayload.ok) { return $true } if ($ProbePayload.changed) { return $true } if ($ProbePayload.click -and $ProbePayload.click.clicked -and $ProbePayload.click.moved -and -not $ProbePayload.click.inputBlocked) { return $true } return $false } function Get-NativeScanner([object]$StatusPayload) { if ($StatusPayload.nativeScanner) { return $StatusPayload.nativeScanner } if ($StatusPayload.scanner) { return $StatusPayload.scanner } return $null } function Get-ExpectedAppSignature() { $mainPath = Join-Path (Resolve-Path -LiteralPath ".").Path "electron\main.ts" $mainSource = Get-Content -LiteralPath $mainPath -Raw $match = [regex]::Match($mainSource, 'APP_RUNTIME_SIGNATURE\s*=\s*"([^"]+)"') if (-not $match.Success) { throw "APP_RUNTIME_SIGNATURE not found in electron/main.ts." } return $match.Groups[1].Value } function UriEscape([string]$Value) { return [System.Uri]::EscapeDataString($Value) } $safeLimit = [Math]::Max(1, [Math]::Min(100, $Limit)) $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $RunDir = Join-Path $OutputRoot $timestamp New-Item -ItemType Directory -Force -Path $RunDir | Out-Null $summary = [ordered]@{ ok = $false createdAt = (Get-Date).ToString("o") limit = $safeLimit category = $Category expectedSignature = "" outputDir = $RunDir persistRequested = [bool]$Persist health = $null data = $null preflight = $null probe = $null finalStatus = $null process = $null results = $null errors = @() } try { $expectedSignature = Get-ExpectedAppSignature $summary.expectedSignature = $expectedSignature $health = Invoke-DevJson "/health" $summary.health = @{ ok = [bool]$health.ok signature = if ($health.appBuild) { [string]$health.appBuild.signature } else { "" } } Save-Json "01-health" $health | Out-Null if (-not $health.appBuild -or [string]$health.appBuild.signature -ne $expectedSignature) { throw "Dev endpoint is stale: /health signature '$($summary.health.signature)' does not match source '$expectedSignature'. Restart the elevated app." } $data = Invoke-DevJson "/scanner/native/data" $summary.data = @{ ok = [bool]$data.ok version = [string]$data.status.version totalEntries = [int]$data.status.totalEntries valid = [bool]$data.status.valid } Save-Json "02-native-data" $data | Out-Null if (-not $data.ok) { throw "Native IK data check failed." } $preflight = Invoke-DevJson "/scanner/native/preflight?category=$Category" $summary.preflight = @{ ok = [bool]$preflight.ok ready = [bool]$preflight.status.ready category = [string]$preflight.status.category categoryReady = [bool]$preflight.status.categoryReady genshinFound = [bool]$preflight.status.genshinFound isSixteenNine = [bool]$preflight.status.isSixteenNine gridCount = [int]$preflight.status.grid.count visualReady = if ($preflight.status.visual) { [bool]$preflight.status.visual.ready } else { $null } visualWhitePct = if ($preflight.status.visual) { [double]$preflight.status.visual.whitePct } else { $null } visualDarkPct = if ($preflight.status.visual) { [double]$preflight.status.visual.darkPct } else { $null } visualColorPct = if ($preflight.status.visual) { [double]$preflight.status.visual.colorPct } else { $null } visualLumaStdDev = if ($preflight.status.visual) { [double]$preflight.status.visual.lumaStdDev } else { $null } blockReason = [string]$preflight.status.blockReason } Save-Json "03-native-preflight" $preflight | Out-Null if (-not $preflight.ok) { $preflightReason = if ($preflight.status.blockReason) { [string]$preflight.status.blockReason } else { "unknown preflight block reason" } throw "Native scanner preflight is not ready for category '$Category': $preflightReason" } if (-not $SkipProbe) { $probe = Invoke-DevJson "/automation/probe-click?index=1" $summary.probe = @{ ok = [bool](Test-ProbeSucceeded $probe) endpointOk = [bool]$probe.ok changed = [bool]$probe.changed } Save-Json "04-probe-click" $probe | Out-Null if (-not (Test-ProbeSucceeded $probe)) { throw "Probe click did not prove safe input delivery." } } $start = Invoke-DevJson "/scanner/start?limit=$safeLimit&category=$Category" Save-Json "05-native-start" $start | Out-Null $startedScanner = Get-NativeScanner $start if ($null -eq $startedScanner) { throw "Native scanner start returned no scanner payload." } $deadline = (Get-Date).AddSeconds($TimeoutSeconds) $polls = @() do { Start-Sleep -Seconds $PollIntervalSeconds $status = Invoke-DevJson "/scanner/status" $scanner = Get-NativeScanner $status $polls += [pscustomobject]@{ at = (Get-Date).ToString("o") status = if ($scanner) { [string]$scanner.status } else { "missing" } running = if ($scanner) { [bool]$scanner.running } else { $false } captured = if ($scanner) { [int]$scanner.captured } else { 0 } activeMs = if ($scanner) { [int]$scanner.activeMs } else { 0 } message = if ($scanner) { [string]$scanner.message } else { "missing native scanner payload" } } if ($null -ne $scanner -and -not $scanner.running) { break } } while ((Get-Date) -lt $deadline) Save-Json "06-native-polls" $polls | Out-Null $final = Invoke-DevJson "/scanner/status" Save-Json "07-native-final-status" $final | Out-Null $finalScanner = Get-NativeScanner $final if ($null -eq $finalScanner) { throw "Native scanner final status returned no scanner payload." } $summary.finalStatus = @{ status = [string]$finalScanner.status running = [bool]$finalScanner.running runDir = [string]$finalScanner.runDir captured = [int]$finalScanner.captured clicked = [int]$finalScanner.clicked pages = [int]$finalScanner.pages activeMs = [int]$finalScanner.activeMs message = [string]$finalScanner.message } if ($finalScanner.running) { throw "Native scanner did not finish before timeout." } if ([string]$finalScanner.status -ne "done") { throw "Native scanner finished with status '$($finalScanner.status)': $($finalScanner.message)" } if ([int]$finalScanner.captured -lt $safeLimit) { throw "Native scanner captured $($finalScanner.captured), expected at least $safeLimit." } $runDirParam = UriEscape([string]$finalScanner.runDir) $persistParam = if ($Persist) { "1" } else { "0" } $process = Invoke-DevJson "/scanner/native/process?runDir=$runDirParam&limit=$safeLimit&persist=$persistParam" Save-Json "08-native-process" $process | Out-Null $summary.process = @{ ok = [bool]$process.ok processed = [int]$process.status.processed parsed = [int]$process.status.parsed review = [int]$process.status.review errors = [int]$process.status.errors stored = [int]$process.status.stored persisted = [bool]$process.status.persisted queueConcurrency = [int]$process.status.queueConcurrency elapsedMs = [int]$process.status.elapsedMs } if (-not $process.ok) { throw "Native post-capture processing failed." } $results = Invoke-DevJson "/scanner/native/results?runDir=$runDirParam&limit=$safeLimit" Save-Json "09-native-results" $results | Out-Null $summary.results = @{ ok = [bool]$results.ok total = [int]$results.status.total loaded = [int]$results.status.results.Count } if (-not $results.ok) { throw "Native scan results could not be loaded." } $summary.ok = $true } catch { $summary.errors += [string]$_.Exception.Message throw } finally { $summaryPath = Save-Json "native-live-smoke-summary" ([pscustomobject]$summary) Write-Host "Native live smoke summary: $summaryPath" if ($summary.ok) { Write-Host ("ok limit={0} captured={1} parsed={2} review={3} errors={4} stored={5} persisted={6}" -f ` $summary.limit, $summary.finalStatus.captured, $summary.process.parsed, $summary.process.review, $summary.process.errors, $summary.process.stored, $summary.process.persisted) } }