<# .SYNOPSIS Run the explicitly opted-in real Pi CLI / real LLM / real workbook E2E. .DESCRIPTION Loads only the LLM_* values needed by the product runner, points every APS persistence path at a fresh temporary artifact root, and invokes the opt-in pytest module. It never starts or connects to the live 8000/5173 services. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$SourcePath, [string]$ArtifactRoot, [string]$Python, [string]$PiCli, [string]$NodeBin, [int]$PiTimeoutSec = 300 ) $ErrorActionPreference = 'Stop' $RepoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $ArtifactRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("aps-pi-real-e2e-" + $stamp) } $ArtifactRoot = [System.IO.Path]::GetFullPath($ArtifactRoot) New-Item -ItemType Directory -Path $ArtifactRoot -Force | Out-Null $ResolvedSource = (Resolve-Path -LiteralPath $SourcePath).Path if (-not (Test-Path -LiteralPath $ResolvedSource -PathType Leaf)) { throw "SourcePath must be a file: $ResolvedSource" } if ([string]::IsNullOrWhiteSpace($Python)) { $venvPython = Join-Path $RepoRoot '.venv\Scripts\python.exe' if (Test-Path -LiteralPath $venvPython -PathType Leaf) { $Python = $venvPython } else { $Python = (Get-Command python -ErrorAction Stop).Source } } if (-not (Test-Path -LiteralPath $Python -PathType Leaf)) { throw "Python executable not found: $Python" } if ([string]::IsNullOrWhiteSpace($PiCli)) { $PiCli = Join-Path $RepoRoot 'poc\pi-fallback\runtime\node_modules\@mariozechner\pi-coding-agent\dist\cli.js' } if (-not (Test-Path -LiteralPath $PiCli -PathType Leaf)) { throw "Real Pi CLI not found: $PiCli" } if ([string]::IsNullOrWhiteSpace($NodeBin)) { if (-not [string]::IsNullOrWhiteSpace($env:APS_FALLBACK_NODE)) { $NodeBin = $env:APS_FALLBACK_NODE } else { $NodeBin = (Get-Command node -ErrorAction Stop).Source } } if (-not (Test-Path -LiteralPath $NodeBin -PathType Leaf)) { $resolvedNode = Get-Command $NodeBin -ErrorAction SilentlyContinue if ($null -eq $resolvedNode) { throw "Node executable not found: $NodeBin" } $NodeBin = $resolvedNode.Source } function Read-DotEnvValue { param([string]$Path, [string]$Name) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null } $pattern = '^\s*(?:export\s+)?' + [regex]::Escape($Name) + '\s*=\s*(.*)$' foreach ($line in [System.IO.File]::ReadAllLines($Path)) { if ($line -match $pattern) { $value = $Matches[1].Trim() if ($value.Length -ge 2) { $first = $value.Substring(0, 1) $last = $value.Substring($value.Length - 1, 1) if (($first -eq '"' -and $last -eq '"') -or ($first -eq "'" -and $last -eq "'")) { $value = $value.Substring(1, $value.Length - 2) } } return $value } } return $null } $DotEnv = Join-Path $RepoRoot '.env' foreach ($name in @('LLM_PROVIDER', 'LLM_API_KEY', 'LLM_BASE_URL', 'LLM_MODEL')) { if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) { $value = Read-DotEnvValue -Path $DotEnv -Name $name if (-not [string]::IsNullOrWhiteSpace($value)) { [Environment]::SetEnvironmentVariable($name, $value, 'Process') } } } if ([string]::IsNullOrWhiteSpace($env:LLM_BASE_URL) -or [string]::IsNullOrWhiteSpace($env:LLM_API_KEY)) { throw "LLM_BASE_URL and LLM_API_KEY are required; fill the whitelisted values in .env or the caller environment." } $env:ROUND87_SOURCE = $ResolvedSource $env:ROUND87_REQUIRED = '1' $env:APS_REAL_PI_E2E = '1' $env:APS_REAL_PI_E2E_ARTIFACT_DIR = $ArtifactRoot $env:APS_FALLBACK_PI_CLI = $PiCli $env:APS_FALLBACK_NODE = $NodeBin $env:APS_FALLBACK_TIMEOUT_SEC = [string]$PiTimeoutSec $env:APS_FALLBACK_EXEC_TIMEOUT_SEC = [string]$PiTimeoutSec $PytestArgs = @( '-m', 'pytest', 'tests\e2e\test_pi_real_workbook_e2e.py', '-q', '-s', '--tb=short', '-p', 'no:cacheprovider' ) $Reproduce = '.\scripts\run-pi-real-workbook-e2e.ps1 -SourcePath "$env:ROUND87_SOURCE" -ArtifactRoot "' + $ArtifactRoot + '"' Write-Host "[real-pi-e2e] repo: $RepoRoot" Write-Host "[real-pi-e2e] artifacts: $ArtifactRoot" Write-Host "[real-pi-e2e] source: $ResolvedSource" Write-Host "[real-pi-e2e] pi cli: $PiCli" Write-Host "[real-pi-e2e] llm configured: $([bool]$env:LLM_API_KEY) provider=$($env:LLM_PROVIDER) model=$($env:LLM_MODEL)" Write-Host "[real-pi-e2e] live services: not used; in-process TestClient only" Push-Location -LiteralPath $RepoRoot try { & $Python $PytestArgs $ExitCode = $LASTEXITCODE } finally { Pop-Location } if ($ExitCode -ne 0) { Write-Host "[real-pi-e2e] FAILED exit=$ExitCode" Write-Host "[real-pi-e2e] artifacts preserved: $ArtifactRoot" Write-Host "[real-pi-e2e] reproduce: $Reproduce" exit $ExitCode } Write-Host "[real-pi-e2e] PASSED" # 运行时目录(Pi 会话 / 临时工作目录)可能在断言结束后被回收: # 汇总查找必须是尽力而为,否则会把已通过的验收误报成脚本失败。 $Summary = $null try { $Summary = Get-ChildItem -LiteralPath $ArtifactRoot -Recurse -Filter 'latest-summary.json' -File ` -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 } catch { $Summary = $null } if ($null -ne $Summary) { Write-Host "[real-pi-e2e] evidence summary: $($Summary.FullName)" } else { Write-Host '[real-pi-e2e] evidence summary: ' }