Cross-Platform Test #31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Cross-Platform Test | |
| on: | |
| workflow_dispatch: # Manual trigger | |
| push: | |
| branches: [ main ] | |
| paths: | |
| - 'PowerShell.MCP/**' | |
| - 'PowerShell.MCP.Proxy/**' | |
| - 'Staging/**' | |
| permissions: | |
| contents: read | |
| jobs: | |
| test: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-latest | |
| rid: win-x64 | |
| exe: PowerShell.MCP.Proxy.exe | |
| - os: ubuntu-latest | |
| rid: linux-x64 | |
| exe: PowerShell.MCP.Proxy | |
| - os: macos-14 | |
| rid: osx-arm64 | |
| exe: PowerShell.MCP.Proxy | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '9.0.x' | |
| - name: Install PowerShell 7.5 (Windows) | |
| if: runner.os == 'Windows' | |
| run: | | |
| # GitHub Actions has PowerShell 7.4 which uses .NET 8 | |
| # PowerShell.MCP requires .NET 9, so we need PowerShell 7.5+ | |
| # Use ZIP package to avoid MSI installation issues | |
| $ErrorActionPreference = "Stop" | |
| $url = "https://github.com/PowerShell/PowerShell/releases/download/v7.5.4/PowerShell-7.5.4-win-x64.zip" | |
| $zipPath = "$env:TEMP\PowerShell-7.5.4-win-x64.zip" | |
| $installPath = "C:\pwsh75" | |
| Write-Host "Downloading PowerShell 7.5.4 ZIP..." | |
| Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing | |
| Write-Host "Downloaded: $((Get-Item $zipPath).Length) bytes" | |
| Write-Host "Extracting to $installPath..." | |
| Expand-Archive -Path $zipPath -DestinationPath $installPath -Force | |
| Write-Host "Verifying installation..." | |
| & "$installPath\pwsh.exe" --version | |
| # Add to PATH for subsequent steps | |
| echo "$installPath" | Out-File -FilePath $env:GITHUB_PATH -Append | |
| shell: pwsh | |
| - name: Install PowerShell (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| brew install powershell/tap/powershell | |
| - name: Install PowerShell (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| # Install PowerShell on Ubuntu | |
| sudo apt-get update | |
| sudo apt-get install -y wget apt-transport-https software-properties-common | |
| source /etc/os-release | |
| wget -q https://packages.microsoft.com/config/ubuntu/$VERSION_ID/packages-microsoft-prod.deb | |
| sudo dpkg -i packages-microsoft-prod.deb | |
| rm packages-microsoft-prod.deb | |
| sudo apt-get update | |
| sudo apt-get install -y powershell | |
| - name: Verify PowerShell | |
| shell: pwsh | |
| run: | | |
| Write-Host "PowerShell Version: $($PSVersionTable.PSVersion)" | |
| Write-Host "PowerShell Path: $((Get-Process -Id $PID).Path)" | |
| Write-Host "OS: $($PSVersionTable.OS)" | |
| # On Windows, verify we're using 7.5+ | |
| if ($IsWindows -and $PSVersionTable.PSVersion.Major -eq 7 -and $PSVersionTable.PSVersion.Minor -lt 5) { | |
| throw "Expected PowerShell 7.5+, got $($PSVersionTable.PSVersion)" | |
| } | |
| - name: Build PowerShell.MCP module | |
| run: | | |
| dotnet build PowerShell.MCP -c Release --no-incremental | |
| - name: Build Proxy | |
| run: | | |
| dotnet publish PowerShell.MCP.Proxy -c Release -r ${{ matrix.rid }} --self-contained | |
| - name: Setup module directory (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $modulePath = "$env:USERPROFILE\Documents\PowerShell\Modules\PowerShell.MCP" | |
| New-Item -Path "$modulePath\bin\${{ matrix.rid }}" -ItemType Directory -Force | Out-Null | |
| Copy-Item "PowerShell.MCP\bin\Release\net9.0\PowerShell.MCP.dll" -Destination $modulePath | |
| Copy-Item "PowerShell.MCP\bin\Release\net9.0\Ude.NetStandard.dll" -Destination $modulePath | |
| Copy-Item "Staging\PowerShell.MCP.psd1" -Destination $modulePath | |
| Copy-Item "Staging\PowerShell.MCP.psm1" -Destination $modulePath | |
| Copy-Item "PowerShell.MCP.Proxy\bin\Release\net9.0\${{ matrix.rid }}\publish\${{ matrix.exe }}" -Destination "$modulePath\bin\${{ matrix.rid }}" | |
| Write-Host "Module files:" | |
| Get-ChildItem $modulePath -Recurse | Select-Object FullName | |
| - name: Setup module directory (Linux/macOS) | |
| if: runner.os != 'Windows' | |
| run: | | |
| MODULE_PATH="$HOME/.local/share/powershell/Modules/PowerShell.MCP" | |
| mkdir -p "$MODULE_PATH/bin/${{ matrix.rid }}" | |
| cp PowerShell.MCP/bin/Release/net9.0/PowerShell.MCP.dll "$MODULE_PATH/" | |
| cp PowerShell.MCP/bin/Release/net9.0/Ude.NetStandard.dll "$MODULE_PATH/" | |
| cp Staging/PowerShell.MCP.psd1 "$MODULE_PATH/" | |
| cp Staging/PowerShell.MCP.psm1 "$MODULE_PATH/" | |
| cp "PowerShell.MCP.Proxy/bin/Release/net9.0/${{ matrix.rid }}/publish/${{ matrix.exe }}" "$MODULE_PATH/bin/${{ matrix.rid }}/" | |
| chmod +x "$MODULE_PATH/bin/${{ matrix.rid }}/${{ matrix.exe }}" | |
| echo "Module files:" | |
| ls -laR "$MODULE_PATH/" | |
| - name: Test module import | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| Write-Host "=== Importing module ===" -ForegroundColor Cyan | |
| Import-Module PowerShell.MCP -Verbose | |
| Write-Host "`n=== Module info ===" -ForegroundColor Cyan | |
| Get-Module PowerShell.MCP | Format-List Name, Version, ModuleBase | |
| Write-Host "`n=== Get-MCPProxyPath ===" -ForegroundColor Cyan | |
| $proxyPath = Get-MCPProxyPath | |
| Write-Host "Proxy path: $proxyPath" | |
| if (-not (Test-Path $proxyPath)) { throw "Proxy not found at $proxyPath" } | |
| Write-Host "`n=== Get-MCPProxyPath -Escape ===" -ForegroundColor Cyan | |
| $escapedPath = Get-MCPProxyPath -Escape | |
| Write-Host "Escaped path: $escapedPath" | |
| Write-Host "`n=== PSReadLine status ===" -ForegroundColor Cyan | |
| $psrl = Get-Module PSReadLine | |
| if ($IsWindows) { | |
| if ($psrl) { | |
| Write-Host "PSReadLine is loaded (expected on Windows)" -ForegroundColor Green | |
| } else { | |
| Write-Host "PSReadLine is NOT loaded (unexpected on Windows)" -ForegroundColor Yellow | |
| } | |
| } else { | |
| if ($psrl) { | |
| Write-Host "PSReadLine is loaded (unexpected on Linux/macOS)" -ForegroundColor Yellow | |
| } else { | |
| Write-Host "PSReadLine is NOT loaded (expected on Linux/macOS)" -ForegroundColor Green | |
| } | |
| } | |
| Write-Host "`n=== All tests passed ===" -ForegroundColor Green | |
| - name: Test Proxy JSON-RPC communication | |
| shell: pwsh | |
| timeout-minutes: 2 | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| Write-Host "=== Testing Proxy JSON-RPC ===" -ForegroundColor Cyan | |
| $proxyPath = Get-MCPProxyPath | |
| Write-Host "Proxy path: $proxyPath" | |
| # Start Proxy process with redirected stdin/stdout | |
| $psi = [System.Diagnostics.ProcessStartInfo]::new() | |
| $psi.FileName = $proxyPath | |
| $psi.RedirectStandardInput = $true | |
| $psi.RedirectStandardOutput = $true | |
| $psi.RedirectStandardError = $true | |
| $psi.UseShellExecute = $false | |
| $psi.CreateNoWindow = $true | |
| $process = [System.Diagnostics.Process]::Start($psi) | |
| Write-Host "Proxy started with PID: $($process.Id)" | |
| # Use async reading with timeout | |
| function Send-JsonRpc { | |
| param([string]$Json, [int]$TimeoutMs = 5000) | |
| Write-Host "Sending: $Json" | |
| $process.StandardInput.WriteLine($Json) | |
| $process.StandardInput.Flush() | |
| # Read response line with timeout | |
| $task = $process.StandardOutput.ReadLineAsync() | |
| if ($task.Wait($TimeoutMs)) { | |
| return $task.Result | |
| } else { | |
| throw "Timeout waiting for response" | |
| } | |
| } | |
| try { | |
| # Test 1: Initialize | |
| Write-Host "`n=== Test 1: Initialize ===" -ForegroundColor Yellow | |
| $initRequest = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | |
| $response = Send-JsonRpc $initRequest | |
| Write-Host "Response: $response" | |
| if ($response -match '"protocolVersion"') { | |
| Write-Host "Initialize: PASSED" -ForegroundColor Green | |
| } else { | |
| throw "Initialize failed - no protocolVersion in response" | |
| } | |
| # Send initialized notification (no response expected) | |
| Write-Host "`nSending initialized notification..." | |
| $process.StandardInput.WriteLine('{"jsonrpc":"2.0","method":"notifications/initialized"}') | |
| $process.StandardInput.Flush() | |
| Start-Sleep -Milliseconds 200 | |
| # Test 2: List tools | |
| Write-Host "`n=== Test 2: List Tools ===" -ForegroundColor Yellow | |
| $listRequest = '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | |
| $response = Send-JsonRpc $listRequest | |
| Write-Host "Response: $response" | |
| if ($response -match '"get_current_location"' -and $response -match '"invoke_expression"') { | |
| Write-Host "List Tools: PASSED" -ForegroundColor Green | |
| } else { | |
| throw "List Tools failed - expected tools not found" | |
| } | |
| Write-Host "`n=== All JSON-RPC tests passed ===" -ForegroundColor Green | |
| } finally { | |
| if (-not $process.HasExited) { $process.Kill() } | |
| $process.Dispose() | |
| } | |
| - name: Test Named Pipe communication | |
| shell: pwsh | |
| timeout-minutes: 2 | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| Write-Host "=== Testing Named Pipe Communication ===" -ForegroundColor Cyan | |
| # Clean up any existing pwsh processes that might have Named Pipe servers | |
| Write-Host "Cleaning up existing pwsh processes..." | |
| $currentPid = $PID | |
| $pwshProcs = Get-Process -Name pwsh -ErrorAction SilentlyContinue | Where-Object { $_.Id -ne $currentPid } | |
| if ($pwshProcs) { | |
| Write-Host "Found $($pwshProcs.Count) other pwsh process(es), terminating..." | |
| $pwshProcs | ForEach-Object { | |
| Write-Host " Killing PID $($_.Id)" | |
| $_ | Stop-Process -Force -ErrorAction SilentlyContinue | |
| } | |
| Start-Sleep -Seconds 2 | |
| } else { | |
| Write-Host "No other pwsh processes found" | |
| } | |
| # Clean up any existing Named Pipe socket files (Linux/macOS) | |
| if (-not $IsWindows) { | |
| $pipeFile = "/tmp/CoreFxPipe_PowerShell.MCP.Communication" | |
| if (Test-Path $pipeFile) { | |
| Write-Host "Removing existing Named Pipe socket: $pipeFile" | |
| Remove-Item $pipeFile -Force -ErrorAction SilentlyContinue | |
| Start-Sleep -Seconds 1 | |
| } | |
| } | |
| # Verify Named Pipe is gone | |
| $pipeName = "PowerShell.MCP.Communication" | |
| if ($IsWindows) { | |
| $pipeExists = Test-Path "\\.\pipe\$pipeName" | |
| } else { | |
| $pipeExists = Test-Path "/tmp/CoreFxPipe_$pipeName" | |
| } | |
| Write-Host "Named Pipe exists before starting background pwsh: $pipeExists" | |
| # Start a background pwsh process with PowerShell.MCP imported | |
| Write-Host "Starting background pwsh with PowerShell.MCP..." | |
| $bgPsi = [System.Diagnostics.ProcessStartInfo]::new() | |
| $bgPsi.FileName = "pwsh" | |
| $bgPsi.Arguments = "-NoProfile -NoExit -Command `"Import-Module PowerShell.MCP -Verbose`"" | |
| $bgPsi.UseShellExecute = $false | |
| $bgPsi.CreateNoWindow = $true | |
| $bgPsi.RedirectStandardInput = $true | |
| $bgPsi.RedirectStandardOutput = $true | |
| $bgPsi.RedirectStandardError = $true | |
| $bgProcess = [System.Diagnostics.Process]::Start($bgPsi) | |
| Write-Host "Background pwsh PID: $($bgProcess.Id)" | |
| # Start async stderr reading to capture debug output | |
| $bgStderrBuilder = [System.Text.StringBuilder]::new() | |
| $bgStderrTask = $bgProcess.StandardError.ReadToEndAsync() | |
| # Wait for Named Pipe server to be ready | |
| Write-Host "Waiting for Named Pipe server..." | |
| $pipeName = "PowerShell.MCP.Communication" | |
| $pipeReady = $false | |
| for ($i = 0; $i -lt 30; $i++) { | |
| Start-Sleep -Milliseconds 500 | |
| # Check if process is still running | |
| if ($bgProcess.HasExited) { | |
| Write-Host "ERROR: Background pwsh exited with code $($bgProcess.ExitCode)" -ForegroundColor Red | |
| Write-Host "=== stdout ===" -ForegroundColor Yellow | |
| Write-Host $bgProcess.StandardOutput.ReadToEnd() | |
| Write-Host "=== stderr ===" -ForegroundColor Yellow | |
| if ($bgStderrTask.IsCompleted) { Write-Host $bgStderrTask.Result } | |
| throw "Background pwsh process exited unexpectedly" | |
| } | |
| if ($IsWindows) { | |
| $pipeReady = Test-Path "\\.\pipe\$pipeName" | |
| } else { | |
| # Check both /tmp and $TMPDIR on macOS/Linux | |
| $pipeReady = (Test-Path "/tmp/CoreFxPipe_$pipeName") -or | |
| ($env:TMPDIR -and (Test-Path "$env:TMPDIR/CoreFxPipe_$pipeName")) | |
| } | |
| if ($pipeReady) { | |
| Write-Host "Named Pipe ready after $($i * 500)ms" | |
| # Debug: Show pipe details | |
| if ($IsWindows) { | |
| Write-Host "=== Windows Named Pipes matching pattern ===" -ForegroundColor Cyan | |
| Get-ChildItem "\\.\pipe\" -ErrorAction SilentlyContinue | Where-Object { $_.Name -match "PowerShell|MCP|Communication" } | ForEach-Object { Write-Host " $($_.Name)" } | |
| Write-Host "=== All pipes with our name ===" -ForegroundColor Cyan | |
| $pipes = Get-ChildItem "\\.\pipe\" -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $pipeName } | |
| Write-Host " Found $($pipes.Count) pipe(s) named '$pipeName'" | |
| } else { | |
| Write-Host "=== macOS/Linux: TMPDIR and pipe locations ===" -ForegroundColor Cyan | |
| Write-Host " TMPDIR: $env:TMPDIR" | |
| Write-Host " Checking /tmp..." | |
| bash -c "ls -la /tmp/CoreFxPipe_* 2>/dev/null || echo ' No CoreFxPipe files in /tmp'" | |
| if ($env:TMPDIR) { | |
| Write-Host " Checking TMPDIR..." | |
| bash -c "ls -la `$TMPDIR/CoreFxPipe_* 2>/dev/null || echo ' No CoreFxPipe files in TMPDIR'" | |
| } | |
| Write-Host "=== lsof for background pwsh ===" -ForegroundColor Cyan | |
| bash -c "lsof -p $($bgProcess.Id) 2>/dev/null | grep -E 'unix|PIPE|sock' | head -10 || echo ' No matching descriptors'" | |
| } | |
| break | |
| } | |
| # Show progress every 5 seconds | |
| if ($i % 10 -eq 9) { | |
| Write-Host "Still waiting... ($($i * 500)ms elapsed)" | |
| } | |
| } | |
| if (-not $pipeReady) { | |
| Write-Host "=== Named Pipe not found, checking process state ===" -ForegroundColor Red | |
| Write-Host "Process running: $(-not $bgProcess.HasExited)" | |
| Write-Host "Background pwsh PID: $($bgProcess.Id)" | |
| # Debug: List pipe-related files and sockets | |
| if (-not $IsWindows) { | |
| Write-Host "=== Environment ===" -ForegroundColor Yellow | |
| Write-Host " TMPDIR: $env:TMPDIR" | |
| Write-Host "=== /tmp pipe files (PowerShell) ===" -ForegroundColor Yellow | |
| Get-ChildItem /tmp -Filter "CoreFxPipe_*" -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_.FullName } | |
| Write-Host "=== /tmp (native ls) ===" -ForegroundColor Yellow | |
| bash -c "ls -la /tmp/ | grep -i 'corefx\|pipe\|mcp\|communication' || echo 'No matching files'" | |
| bash -c "ls -la /tmp/ | head -20" | |
| if ($env:TMPDIR) { | |
| Write-Host "=== TMPDIR contents ===" -ForegroundColor Yellow | |
| bash -c "ls -la $env:TMPDIR/ 2>/dev/null | grep -i 'corefx\|pipe\|mcp' || echo 'No matching files in TMPDIR'" | |
| bash -c "ls -la $env:TMPDIR/ 2>/dev/null | head -20" | |
| } | |
| Write-Host "=== lsof for background pwsh ===" -ForegroundColor Yellow | |
| bash -c "lsof -p $($bgProcess.Id) 2>/dev/null | grep -iE 'unix|pipe|socket|stream' | head -20 || echo 'No sockets found'" | |
| Write-Host "=== All CoreFx sockets (find) ===" -ForegroundColor Yellow | |
| bash -c "find /tmp \$TMPDIR /var/folders -name '*CoreFx*' -o -name '*Communication*' 2>/dev/null | head -20 || echo 'None found'" | |
| } | |
| # Kill process first, then read outputs | |
| if (-not $bgProcess.HasExited) { | |
| Write-Host "Killing background process..." | |
| $bgProcess.Kill() | |
| $bgProcess.WaitForExit(5000) | |
| } | |
| # Now read stdout (sync, process is dead) | |
| Write-Host "=== stdout ===" -ForegroundColor Yellow | |
| $stdoutTask = $bgProcess.StandardOutput.ReadToEndAsync() | |
| if ($stdoutTask.Wait(3000)) { | |
| Write-Host $stdoutTask.Result | |
| } else { | |
| Write-Host "(stdout read timeout)" | |
| } | |
| # Wait for stderr task and display | |
| Write-Host "=== stderr (debug output) ===" -ForegroundColor Yellow | |
| if ($bgStderrTask.Wait(3000)) { | |
| Write-Host $bgStderrTask.Result | |
| } else { | |
| Write-Host "(stderr read timeout)" | |
| } | |
| throw "Named Pipe server did not start within 15 seconds" | |
| } | |
| # Start Proxy (don't call Get-MCPProxyPath to avoid importing module in test script) | |
| Write-Host "Starting Proxy..." | |
| # Build proxy path directly to avoid loading PowerShell.MCP in test script | |
| if ($IsWindows) { | |
| $proxyPath = "$env:USERPROFILE\Documents\PowerShell\Modules\PowerShell.MCP\bin\win-x64\PowerShell.MCP.Proxy.exe" | |
| } elseif ($IsMacOS) { | |
| $proxyPath = "$HOME/.local/share/powershell/Modules/PowerShell.MCP/bin/osx-arm64/PowerShell.MCP.Proxy" | |
| } else { | |
| $proxyPath = "$HOME/.local/share/powershell/Modules/PowerShell.MCP/bin/linux-x64/PowerShell.MCP.Proxy" | |
| } | |
| Write-Host "Proxy path: $proxyPath" | |
| $psi = [System.Diagnostics.ProcessStartInfo]::new() | |
| $psi.FileName = $proxyPath | |
| $psi.RedirectStandardInput = $true | |
| $psi.RedirectStandardOutput = $true | |
| $psi.RedirectStandardError = $true | |
| $psi.UseShellExecute = $false | |
| $psi.CreateNoWindow = $true | |
| $process = [System.Diagnostics.Process]::Start($psi) | |
| # Use async reading with timeout | |
| function Send-JsonRpc { | |
| param([string]$Json, [int]$TimeoutMs = 5000) | |
| Write-Host "Sending: $Json" | |
| $process.StandardInput.WriteLine($Json) | |
| $process.StandardInput.Flush() | |
| $task = $process.StandardOutput.ReadLineAsync() | |
| if ($task.Wait($TimeoutMs)) { | |
| return $task.Result | |
| } else { | |
| # On timeout, dump stderr for debugging | |
| Write-Host "=== Proxy stderr (timeout debug) ===" -ForegroundColor Red | |
| while ($process.StandardError.Peek() -ge 0) { | |
| Write-Host ([char]$process.StandardError.Read()) -NoNewline | |
| } | |
| Write-Host "" | |
| throw "Timeout waiting for response" | |
| } | |
| } | |
| try { | |
| # Initialize | |
| $initRequest = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | |
| $response = Send-JsonRpc $initRequest | |
| Write-Host "Init response: $response" | |
| $process.StandardInput.WriteLine('{"jsonrpc":"2.0","method":"notifications/initialized"}') | |
| $process.StandardInput.Flush() | |
| Start-Sleep -Milliseconds 200 | |
| # Test: invoke_expression with Get-Date | |
| Write-Host "`n=== Test: invoke_expression (Get-Date) ===" -ForegroundColor Yellow | |
| $invokeRequest = '{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Get-Date -Format yyyy-MM-dd"}}}' | |
| $response = Send-JsonRpc $invokeRequest 10000 | |
| Write-Host "Response: $response" | |
| $today = Get-Date -Format "yyyy-MM-dd" | |
| if ($response -match $today) { | |
| Write-Host "invoke_expression (Get-Date): PASSED" -ForegroundColor Green | |
| } else { | |
| throw "invoke_expression failed - expected date $today not found" | |
| } | |
| Write-Host "`n=== All Named Pipe tests passed ===" -ForegroundColor Green | |
| } finally { | |
| # Dump bgProcess stderr for debugging before cleanup | |
| Write-Host "=== Background pwsh stderr ===" -ForegroundColor Cyan | |
| if (-not $bgProcess.HasExited) { | |
| $bgProcess.Kill() | |
| $bgProcess.WaitForExit(5000) | |
| } | |
| if ($bgStderrTask.Wait(3000)) { | |
| Write-Host $bgStderrTask.Result | |
| } | |
| if (-not $process.HasExited) { $process.Kill() } | |
| $process.Dispose() | |
| $bgProcess.Dispose() | |
| } | |