param( [Parameter(Mandatory)][string]$TestRoot, [Parameter(Mandatory)][string]$VenvPython, [string]$EvidenceRoot = 'artifacts\evidence\readme-verification', [switch]$CliOnly ) $ErrorActionPreference = 'Stop' $repository = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path $testRootPath = (Resolve-Path -LiteralPath $TestRoot).Path $venvPythonPath = (Resolve-Path -LiteralPath $VenvPython).Path $evidence = [IO.Path]::GetFullPath((Join-Path $repository $EvidenceRoot)) if (-not $evidence.StartsWith($repository + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw 'Evidence output must remain inside the repository.' } $pwshPath = (Get-Command pwsh.exe -ErrorAction Stop).Source $cmdPath = $env:ComSpec $venvRoot = Split-Path (Split-Path $venvPythonPath) $pythonRuntime = (((Get-Content -LiteralPath (Join-Path $venvRoot 'pyvenv.cfg')) | Where-Object { $_ -match '^home\s*=' }) -split '=', 2)[1].Trim() $basePythonPath = Join-Path $pythonRuntime 'python.exe' if (-not (Test-Path -LiteralPath $basePythonPath -PathType Leaf)) { throw "Base Python does not exist: $basePythonPath" } New-Item -ItemType Directory -Force -Path $evidence | Out-Null $runRoot = Join-Path $testRootPath ('agent-mcp-readme-' + [Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $runRoot | Out-Null $agent = Join-Path $repository 'build\Release\Agent.App\net10.0-windows10.0.26100.0\agent-mcp.exe' $windowsTests = Join-Path $repository 'build\Release\Agent.Windows.Tests\net10.0-windows10.0.26100.0\Agent.Windows.Tests.exe' $configPath = Join-Path $runRoot 'agent.yaml' $logPath = Join-Path $evidence 'verification.log' $server = $null $failed = $false function Invoke-Step([string]$Name, [scriptblock]$Action) { "START $Name $(Get-Date -Format o)" | Tee-Object -FilePath $logPath -Append try { $global:LASTEXITCODE = 0 & $Action *>&1 | Tee-Object -FilePath $logPath -Append if ($LASTEXITCODE -ne 0) { throw "$Name exited with $LASTEXITCODE" } "PASS $Name" | Tee-Object -FilePath $logPath -Append } catch { $script:failed = $true; "FAIL $Name $($_.Exception.Message)" | Tee-Object -FilePath $logPath -Append } } function Invoke-ExpectedFailure([string]$Name, [string[]]$Arguments) { & $agent @Arguments *>&1 | Tee-Object -FilePath $logPath -Append if ($LASTEXITCODE -ne 2) { $script:failed = $true; "FAIL $Name expected=2 actual=$LASTEXITCODE" | Tee-Object -FilePath $logPath -Append } else { "PASS $Name" | Tee-Object -FilePath $logPath -Append } } try { $listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0); $listener.Start() $httpPort = ([Net.IPEndPoint]$listener.LocalEndpoint).Port; $listener.Stop() $forwardRoot = $testRootPath.Replace('\','/') $forwardPrompt = (Join-Path $repository 'prompts\user-system.md').Replace('\','/') $forwardSkills = (Join-Path $repository 'skills').Replace('\','/') $forwardLocales = (Join-Path $repository 'config\locales').Replace('\','/') $forwardLogs = (Join-Path $runRoot 'logs').Replace('\','/') $pythonYaml = $basePythonPath.Replace('\','/'); $runtimeYaml = $pythonRuntime.Replace('\','/') $pwshYaml = $pwshPath.Replace('\','/'); $cmdYaml = $cmdPath.Replace('\','/') @" schema_version: 2 application: { language: en } mcp: transport: http http: { host: 127.0.0.1, port: $httpPort } agent: { user_system_prompt: $forwardPrompt, max_iterations: 8, same_failure_limit: 2 } ollama: { endpoint: 'http://127.0.0.1:11434', model: 'deepseek-v4-flash:cloud', think: true } planner: { max_tasks: 8 } sandbox: profile: development workspaces: [$forwardRoot] mounts: [] execution: entries: - { alias: python, path: $pythonYaml, runtime_root: $runtimeYaml } - { alias: powershell, path: '$pwshYaml' } - { alias: cmd, path: $cmdYaml } network: { access: none, local_endpoints: [], restricted_endpoints: [] } resources: { profile: constrained } localization: { directory: $forwardLocales, fallback: en } skills: { directory: $forwardSkills } logging: { directory: $forwardLogs, level: information, max_file_bytes: 10485760, retained_files: 5 } "@ | Set-Content -LiteralPath $configPath -Encoding utf8NoBOM $normalizedConfig = [IO.Path]::GetFullPath($configPath).ToUpperInvariant() $hash = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($normalizedConfig))) $appliedStorePath = Join-Path $env:LOCALAPPDATA "AgentSandboxMcp\applied-configurations\$hash.json" Invoke-Step 'cli-show-before-apply' { & $agent show $configPath } Invoke-Step 'cli-apply-yes' { & $agent apply $configPath --yes } Invoke-Step 'cli-show-applied' { & $agent show $configPath } Invoke-ExpectedFailure 'removed-serve' @('serve') Invoke-ExpectedFailure 'removed-check' @('check') Invoke-ExpectedFailure 'removed-config-option' @('--config', $configPath) Invoke-ExpectedFailure 'removed-language-option' @('--lang', 'ja') Invoke-ExpectedFailure 'removed-transport-option' @('--transport', 'http') Invoke-ExpectedFailure 'removed-executable-command' @('executable', 'list') Invoke-ExpectedFailure 'removed-sandbox-command' @('sandbox', 'show') if (-not $CliOnly) { Invoke-Step 'implicit-http-start' { $script:server = Start-Process -FilePath $agent -ArgumentList @($configPath) -WorkingDirectory (Split-Path $agent) -WindowStyle Hidden -PassThru $endpoint = "http://127.0.0.1:$httpPort/mcp" for ($attempt=0; $attempt -lt 100; $attempt++) { if ($script:server.HasExited) { throw "Server exited: $($script:server.ExitCode)" } try { $response = Invoke-WebRequest -Uri $endpoint -Method Get -SkipHttpErrorCheck; if ($response.StatusCode -eq 405) { break } } catch { } Start-Sleep -Milliseconds 100 } if ($attempt -eq 100) { throw 'Server did not become ready.' } $script:server.Kill($true); $script:server.WaitForExit(); $script:server = $null } $env:AGENT_MCP_MONKEY_ROOT = $testRootPath $env:AGENT_MCP_TEST_VENV_PYTHON = $venvPythonPath $env:AGENT_MCP_TEST_POWERSHELL = $pwshPath $env:AGENT_MCP_TEST_POWERSHELL_RUNTIME = Split-Path $pwshPath $env:AGENT_MCP_TEST_CMD = $cmdPath Invoke-Step 'readme-operational-windows-tests' { & $windowsTests -class Agent.Windows.Tests.ReadmeOperationalTests -parallel none -failSkips } } } finally { if ($server -and -not $server.HasExited) { $server.Kill($true); $server.WaitForExit() } if ($appliedStorePath -and (Test-Path -LiteralPath $appliedStorePath)) { Remove-Item -LiteralPath $appliedStorePath -Force } if (Test-Path -LiteralPath $runRoot) { $resolved = (Resolve-Path -LiteralPath $runRoot).Path if (-not $resolved.StartsWith($testRootPath.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase)) { throw 'Refusing to clean outside TestRoot.' } Remove-Item -LiteralPath $resolved -Recurse -Force } Remove-Item Env:AGENT_MCP_MONKEY_ROOT,Env:AGENT_MCP_TEST_VENV_PYTHON,Env:AGENT_MCP_TEST_POWERSHELL,Env:AGENT_MCP_TEST_POWERSHELL_RUNTIME,Env:AGENT_MCP_TEST_CMD -ErrorAction SilentlyContinue } if ($failed) { throw "README verification failed. Evidence: $evidence" } Write-Output "README verification PASS. Evidence: $evidence"