2424 - name : Install PowerShell
2525 run : brew install powershell/tap/powershell
2626
27- - name : Grant AppleScript access to Terminal.app
28- run : |
29- # Pre-authorize osascript to control Terminal.app via TCC database
30- # GitHub Actions runner has sudo access
31- sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db \
32- "INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version, indirect_object_identifier_type, indirect_object_identifier, flags, last_modified) VALUES ('kTCCServiceAppleEvents', '/usr/bin/osascript', 1, 2, 0, 1, 0, 'com.apple.Terminal', 0, strftime('%s','now'));" 2>&1 || true
33- # Also allow the runner agent
34- RUNNER_PATH=$(which Runner.Listener 2>/dev/null || echo "/Users/runner/runners/*/bin/Runner.Listener")
35- sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db \
36- "INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version, indirect_object_identifier_type, indirect_object_identifier, flags, last_modified) VALUES ('kTCCServiceAppleEvents', '/usr/bin/osascript', 1, 2, 0, 1, 1, 'com.apple.Terminal', 0, strftime('%s','now'));" 2>&1 || true
37- echo "TCC database updated"
38-
3927 - name : Build and setup module
4028 run : |
4129 dotnet build PowerShell.MCP -c Release --no-incremental
@@ -53,125 +41,131 @@ jobs:
5341 echo "Module files:"
5442 ls -laR "$MODULE_PATH/"
5543
56- - name : Test Terminal.app launch and invoke_expression (Issue # 38)
44+ - name : Launch pwsh in Terminal.app manually (bypass AppleScript TCC)
45+ run : |
46+ # AppleScript requires TCC approval which can't be granted in CI.
47+ # Instead, use 'open' command to launch Terminal.app, then run pwsh via .zshrc trick.
48+ # Create a launcher script that Terminal.app will execute on open.
49+ MODULE_PATH="$HOME/.local/share/powershell/Modules/PowerShell.MCP"
50+ PROXY_PID=$$
51+
52+ cat > /tmp/launch-pwsh.sh << LAUNCHER
53+ #!/bin/zsh
54+ export PATH="/opt/homebrew/bin:/usr/local/bin:\$PATH"
55+ exec pwsh -NoExit -Command "\\\$global:PowerShellMCPProxyPid = ${PROXY_PID}; \\\$global:PowerShellMCPAgentId = 'default'; Import-Module PowerShell.MCP -Force; Remove-Module PSReadLine -ErrorAction SilentlyContinue"
56+ LAUNCHER
57+ chmod +x /tmp/launch-pwsh.sh
58+
59+ # Open Terminal.app running our script
60+ open -a Terminal /tmp/launch-pwsh.sh
61+
62+ # Wait for Named Pipe to appear
63+ echo "Waiting for Named Pipe..."
64+ for i in $(seq 1 60); do
65+ PIPE=$(find /tmp -name "CoreFxPipe_PowerShell.MCP.*" 2>/dev/null | head -1)
66+ if [ -n "$PIPE" ]; then
67+ echo "Named Pipe found: $PIPE (after ${i}s)"
68+ break
69+ fi
70+ sleep 1
71+ done
72+
73+ if [ -z "$PIPE" ]; then
74+ echo "ERROR: Named Pipe not found after 60s"
75+ screencapture -x /tmp/screenshot-error.png 2>/dev/null
76+ exit 1
77+ fi
78+
79+ screencapture -x /tmp/screenshot-after-start.png 2>/dev/null
80+ echo "Terminal.app with pwsh is running"
81+
82+ - name : Test invoke_expression via Named Pipe (Issue # 38)
5783 shell : pwsh
58- timeout-minutes : 5
84+ timeout-minutes : 3
5985 run : |
6086 $ErrorActionPreference = "Stop"
6187
62- $proxyPath = Get-MCPProxyPath
63- Write-Host "Proxy path: $proxyPath"
64-
65- # Start Proxy process
66- $psi = [System.Diagnostics.ProcessStartInfo]::new()
67- $psi.FileName = $proxyPath
68- $psi.RedirectStandardInput = $true
69- $psi.RedirectStandardOutput = $true
70- $psi.RedirectStandardError = $true
71- $psi.UseShellExecute = $false
72- $psi.CreateNoWindow = $true
73-
74- $process = [System.Diagnostics.Process]::Start($psi)
75- Write-Host "Proxy started with PID: $($process.Id)"
76-
77- function Send-JsonRpc {
78- param([string]$Json, [int]$TimeoutMs = 30000)
79- Write-Host "Sending: $($Json.Substring(0, [Math]::Min(120, $Json.Length)))..."
80- $process.StandardInput.WriteLine($Json)
81- $process.StandardInput.Flush()
82- $task = $process.StandardOutput.ReadLineAsync()
83- if ($task.Wait($TimeoutMs)) {
84- return $task.Result
85- } else {
86- throw "Timeout waiting for response after ${TimeoutMs}ms"
88+ # Find the Named Pipe
89+ $pipes = Get-ChildItem /tmp/CoreFxPipe_PowerShell.MCP.* -ErrorAction SilentlyContinue
90+ if (-not $pipes) {
91+ # Also check TMPDIR
92+ $tmpdir = $env:TMPDIR
93+ if ($tmpdir) {
94+ $pipes = Get-ChildItem "$tmpdir/CoreFxPipe_PowerShell.MCP.*" -ErrorAction SilentlyContinue
8795 }
8896 }
97+ if (-not $pipes) { throw "No Named Pipe found" }
8998
90- try {
91- # 1. Initialize
92- Write-Host "`n=== Step 1: Initialize ===" -ForegroundColor Cyan
93- $response = Send-JsonRpc '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
94- Write-Host "OK: $($response.Substring(0, [Math]::Min(100, $response.Length)))..."
95-
96- $process.StandardInput.WriteLine('{"jsonrpc":"2.0","method":"notifications/initialized"}')
97- $process.StandardInput.Flush()
98- Start-Sleep -Seconds 1
99-
100- # 2. Start console via Terminal.app
101- Write-Host "`n=== Step 2: start_powershell_console (Terminal.app) ===" -ForegroundColor Cyan
102- $response = Send-JsonRpc '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"start_powershell_console","arguments":{"reason":"issue38 test","banner":"Issue #38 E2E Test"}}}' 60000
103- Write-Host "Response: $($response.Substring(0, [Math]::Min(200, $response.Length)))..."
104-
105- if ($response -match '"error"' -or $response -match 'Failed to start') {
106- Write-Host "ERROR in start_powershell_console response:" -ForegroundColor Red
107- Write-Host $response
108- screencapture -x /tmp/screenshot-error.png 2>$null
109- throw "start_powershell_console failed"
110- }
111- Write-Host "Terminal.app console started" -ForegroundColor Green
112-
113- # Take screenshot after console start
114- screencapture -x /tmp/screenshot-after-start.png 2>$null
115- Write-Host "Screenshot saved: /tmp/screenshot-after-start.png"
99+ $pipeName = $pipes[0].Name -replace '^CoreFxPipe_', ''
100+ Write-Host "Using pipe: $pipeName"
116101
117- # 3. Quick command - should execute without manual Enter
118- Write-Host "`n=== Step 3: invoke_expression (quick) ===" -ForegroundColor Cyan
119- $response = Send-JsonRpc '{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Write-Host TEST-QUICK -ForegroundColor Green"}}}' 30000
120- Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
102+ # Import module for NamedPipeClient
103+ Import-Module PowerShell.MCP
121104
122- if ($response -match 'TEST-QUICK') {
123- Write-Host "PASS: Quick command executed without manual Enter" -ForegroundColor Green
124- } else {
125- Write-Host "WARN: TEST-QUICK not in response (may be first-call redirect)" -ForegroundColor Yellow
126- }
105+ # Helper to send command via Named Pipe
106+ function Send-PipeCommand {
107+ param([string]$Pipeline, [int]$TimeoutSec = 30)
108+ $client = [System.IO.Pipes.NamedPipeClientStream]::new('.', $pipeName)
109+ $client.Connect($TimeoutSec * 1000)
110+ $writer = [System.IO.StreamWriter]::new($client)
111+ $reader = [System.IO.StreamReader]::new($client)
127112
128- # 4. Delayed command - the main #38 scenario
129- Write-Host "`n=== Step 4: invoke_expression after 5s delay ===" -ForegroundColor Cyan
130- Start-Sleep -Seconds 5
131- $response = Send-JsonRpc '{"jsonrpc":"2.0","id":20,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Get-Date -Format yyyy-MM-dd"}}}' 30000
132- Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
133-
134- $today = Get-Date -Format "yyyy-MM-dd"
135- if ($response -match $today) {
136- Write-Host "PASS: Delayed command returned correct date ($today)" -ForegroundColor Green
137- } else {
138- Write-Host "FAIL: Expected date $today not found in response" -ForegroundColor Red
139- throw "Issue #38 regression: delayed command did not execute automatically"
140- }
113+ $json = @{ name = "invoke_expression"; pipeline = $Pipeline } | ConvertTo-Json -Compress
114+ $writer.WriteLine($json)
115+ $writer.Flush()
141116
142- # 5. Long-running command
143- Write-Host "`n=== Step 5: Long-running command (3s sleep) ===" -ForegroundColor Cyan
144- $response = Send-JsonRpc '{"jsonrpc":"2.0","id":30,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Start-Sleep -Seconds 3; Write-Host LONG-DONE"}}}' 60000
145- Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
117+ $response = $reader.ReadLine()
118+ $client.Dispose()
119+ return $response
120+ }
146121
147- if ($response -match 'LONG-DONE') {
148- Write-Host "PASS: Long-running command completed" -ForegroundColor Green
149- } else {
150- throw "Long-running command did not return expected output"
151- }
122+ # Test 1: Quick command
123+ Write-Host "`n=== Test 1: Quick command ===" -ForegroundColor Cyan
124+ $response = Send-PipeCommand "Write-Host TEST-QUICK -ForegroundColor Green"
125+ Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
126+ if ($response -match 'TEST-QUICK') {
127+ Write-Host "PASS: Quick command executed" -ForegroundColor Green
128+ } else {
129+ throw "Quick command failed"
130+ }
152131
153- # 6. Command after long-running
154- Write-Host "`n=== Step 6: Command immediately after long-running ===" -ForegroundColor Cyan
155- $response = Send-JsonRpc '{"jsonrpc":"2.0","id":40,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Write-Host AFTER-LONG"}}}' 30000
156- Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
132+ # Test 2: Delayed command (main #38 scenario)
133+ Write-Host "`n=== Test 2: Command after 5s delay ===" -ForegroundColor Cyan
134+ Start-Sleep -Seconds 5
135+ $response = Send-PipeCommand "Get-Date -Format yyyy-MM-dd"
136+ Write-Host "Response: $response"
137+ $today = Get-Date -Format "yyyy-MM-dd"
138+ if ($response -match $today) {
139+ Write-Host "PASS: Delayed command returned correct date" -ForegroundColor Green
140+ } else {
141+ throw "Issue #38 regression: delayed command did not execute"
142+ }
157143
158- if ($response -match 'AFTER-LONG') {
159- Write-Host "PASS: Post-long command executed" -ForegroundColor Green
160- } else {
161- throw "Post-long command did not execute"
162- }
144+ # Test 3: Long-running command
145+ Write-Host "`n=== Test 3: Long-running command (3s) ===" -ForegroundColor Cyan
146+ $response = Send-PipeCommand "Start-Sleep -Seconds 3; Write-Host LONG-DONE" 60
147+ Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
148+ if ($response -match 'LONG-DONE') {
149+ Write-Host "PASS: Long-running command completed" -ForegroundColor Green
150+ } else {
151+ throw "Long-running command failed"
152+ }
163153
164- # Take final screenshot
165- screencapture -x /tmp/screenshot-final.png 2>$null
154+ # Test 4: Command immediately after long-running
155+ Write-Host "`n=== Test 4: Command after long-running ===" -ForegroundColor Cyan
156+ $response = Send-PipeCommand "Write-Host AFTER-LONG"
157+ Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
158+ if ($response -match 'AFTER-LONG') {
159+ Write-Host "PASS: Post-long command executed" -ForegroundColor Green
160+ } else {
161+ throw "Post-long command failed"
162+ }
166163
167- Write-Host "`n========================================" -ForegroundColor Green
168- Write-Host "ALL TESTS PASSED - Issue #38 not reproduced" -ForegroundColor Green
169- Write-Host "========================================" -ForegroundColor Green
164+ screencapture -x /tmp/screenshot-final.png 2>$null
170165
171- } finally {
172- if (-not $process.HasExited) { $process.Kill() }
173- $process.Dispose()
174- }
166+ Write-Host "`n========================================" -ForegroundColor Green
167+ Write-Host "ALL TESTS PASSED - Issue #38 not reproduced" -ForegroundColor Green
168+ Write-Host "========================================" -ForegroundColor Green
175169
176170 - name : Upload screenshots
177171 if : always()
0 commit comments