@@ -177,8 +177,54 @@ jobs:
177177 }
178178 }
179179
180+ Write-Host "`n=== PSConsoleHostReadLine fix (non-Windows) ===" -ForegroundColor Cyan
181+ if (-not $IsWindows) {
182+ # PSReadLine defines PSConsoleHostReadLine when loaded.
183+ # After Remove-Module PSReadLine, our psm1 defines a custom polling version.
184+ $fn = Get-Command PSConsoleHostReadLine -ErrorAction SilentlyContinue
185+ if (-not $fn) {
186+ throw "PSConsoleHostReadLine not defined - fix not applied"
187+ }
188+ Write-Host "PSConsoleHostReadLine: DEFINED (custom polling implementation)" -ForegroundColor Green
189+ Write-Host " CommandType : $($fn.CommandType)"
190+ Write-Host " Definition : $($fn.Definition.Substring(0, [Math]::Min(80, $fn.Definition.Length)))..."
191+ }
192+
180193 Write-Host "`n=== All tests passed ===" -ForegroundColor Green
181194
195+ - name : Test timer event polling mechanism (non-Windows)
196+ if : runner.os != 'Windows'
197+ shell : pwsh
198+ run : |
199+ $ErrorActionPreference = "Stop"
200+ Import-Module PowerShell.MCP
201+
202+ # Verify that Register-ObjectEvent action blocks fire during Start-Sleep polling.
203+ # This is the core mechanism of the PSConsoleHostReadLine fix:
204+ # the custom ReadLine polls Console.KeyAvailable + Start-Sleep 50ms in a loop,
205+ # and Start-Sleep must yield the runspace long enough for the timer action to run.
206+ Write-Host "=== Timer event fires during Start-Sleep polling ===" -ForegroundColor Cyan
207+
208+ $global:timerFired = $false
209+ $timer = New-Object System.Timers.Timer 150
210+ $timer.AutoReset = $false
211+ Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {
212+ $global:timerFired = $true
213+ } | Out-Null
214+ $timer.Start()
215+
216+ # Simulate PSConsoleHostReadLine's inner loop: poll every 50ms for up to 2s
217+ $deadline = [DateTime]::UtcNow.AddMilliseconds(2000)
218+ while (-not $global:timerFired -and [DateTime]::UtcNow -lt $deadline) {
219+ Start-Sleep -Milliseconds 50
220+ }
221+
222+ if (-not $global:timerFired) {
223+ throw "FAIL: Timer event did NOT fire during Start-Sleep polling. The PSConsoleHostReadLine fix will not work on this platform."
224+ }
225+ Write-Host "PASS: Timer event fired during Start-Sleep polling." -ForegroundColor Green
226+ Write-Host "The PSConsoleHostReadLine fix is effective on this platform." -ForegroundColor Green
227+
182228 - name : Test Proxy JSON-RPC communication
183229 shell : pwsh
184230 timeout-minutes : 2
@@ -402,43 +448,39 @@ jobs:
402448 $process.StandardInput.Flush()
403449 Start-Sleep -Milliseconds 200
404450
405- # invoke_expression test requires timer event processing (Register-ObjectEvent -Action).
406- # On macOS, timer events don't fire in non-interactive mode (redirected stdin),
407- # even with Start-Sleep or Wait-Event pumping. This is a CI-specific limitation;
408- # production macOS usage launches Terminal.app (interactive) where events work normally.
409- if ($IsMacOS) {
410- Write-Host "`n=== Skipping invoke_expression test on macOS (timer events require interactive session) ===" -ForegroundColor Yellow
411- Write-Host "Named Pipe server detection and Proxy JSON-RPC communication verified successfully."
412- Write-Host "`n=== Named Pipe tests passed (macOS: basic connectivity) ===" -ForegroundColor Green
451+ # invoke_expression: PSConsoleHostReadLine fix enables timer event processing
452+ # via Start-Sleep polling. This is verified by the preceding
453+ # "Test timer event polling mechanism" step. Both CI (via the stdin event-pump
454+ # loop already sent above) and interactive (via PSConsoleHostReadLine) use the
455+ # same Start-Sleep yield mechanism.
456+
457+ # Test: invoke_expression with Get-Date (first call - should return get_current_location)
458+ Write-Host "`n=== Test: invoke_expression (Get-Date) - First call ===" -ForegroundColor Yellow
459+ $invokeRequest = '{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Get-Date -Format yyyy-MM-dd"}}}'
460+ $response = Send-JsonRpc $invokeRequest 60000
461+ Write-Host "Response (truncated): $($response.Substring(0, [Math]::Min(200, $response.Length)))..."
462+
463+ if ($response -match "Switched to console" -and $response -match "Pipeline NOT executed") {
464+ Write-Host "First invoke_expression: PASSED (expected behavior - command not executed)" -ForegroundColor Green
413465 } else {
414- # Test: invoke_expression with Get-Date (first call - should return get_current_location)
415- Write-Host "`n=== Test: invoke_expression (Get-Date) - First call ===" -ForegroundColor Yellow
416- $invokeRequest = '{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Get-Date -Format yyyy-MM-dd"}}}'
417- $response = Send-JsonRpc $invokeRequest 60000
418- Write-Host "Response (truncated): $($response.Substring(0, [Math]::Min(200, $response.Length)))..."
419-
420- if ($response -match "Switched to console" -and $response -match "Pipeline NOT executed") {
421- Write-Host "First invoke_expression: PASSED (expected behavior - command not executed)" -ForegroundColor Green
422- } else {
423- throw "First invoke_expression did not return expected ''Switched to console'' message"
424- }
466+ throw "First invoke_expression did not return expected ''Switched to console'' message"
467+ }
425468
426- # Test: invoke_expression with Get-Date (second call - should execute)
427- Write-Host "`n=== Test: invoke_expression (Get-Date) - Second call ===" -ForegroundColor Yellow
428- $invokeRequest = '{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Get-Date -Format yyyy-MM-dd"}}}'
429- $response = Send-JsonRpc $invokeRequest 60000
430- Write-Host "Response: $response"
431-
432- $today = Get-Date -Format "yyyy-MM-dd"
433- if ($response -match $today) {
434- Write-Host "Second invoke_expression (Get-Date): PASSED" -ForegroundColor Green
435- } else {
436- throw "Second invoke_expression failed - expected date $today not found"
437- }
469+ # Test: invoke_expression with Get-Date (second call - should execute)
470+ Write-Host "`n=== Test: invoke_expression (Get-Date) - Second call ===" -ForegroundColor Yellow
471+ $invokeRequest = '{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Get-Date -Format yyyy-MM-dd"}}}'
472+ $response = Send-JsonRpc $invokeRequest 60000
473+ Write-Host "Response: $response"
438474
439- Write-Host "`n=== All Named Pipe tests passed ===" -ForegroundColor Green
475+ $today = Get-Date -Format "yyyy-MM-dd"
476+ if ($response -match $today) {
477+ Write-Host "Second invoke_expression (Get-Date): PASSED" -ForegroundColor Green
478+ } else {
479+ throw "Second invoke_expression failed - expected date $today not found"
440480 }
441481
482+ Write-Host "`n=== All Named Pipe tests passed ===" -ForegroundColor Green
483+
442484 } finally {
443485 if (-not $process.HasExited) { $process.Kill() }
444486 $process.Dispose()
0 commit comments