From a99bab04f3e668a69a983ccdd5fcdbc8d4ba6506 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 25 Aug 2026 13:43:42 -0400 Subject: [PATCH 1/3] Fix flaky Debug-Runspace attach event test The test started a 'Wait-Event' pipeline with BeginInvoke on a PowerShell instance whose runspace was not yet opened, then immediately started Debug-Runspace against that runspace id. Nothing synchronized the two, so Debug-Runspace could run before the target runspace was open/executing, in which case the cmdlet errored out or skipped the null-conditional Events.GenerateEvent call and OnDebugAttach never fired. Open the target runspace explicitly, gate on the Wait-Event pipeline actually running before attaching, wait deterministically for detach, and dispose all instances in a finally block. Assertions are unchanged and no production code was modified. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2a30274b-8dba-4f6b-92b3-e715b633f8b3 --- .../Debug-Runspace.Tests.ps1 | 91 ++++++++++++++----- 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 index 2b517e4f301..704ce675ef2 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 @@ -34,30 +34,73 @@ Describe "Debug-Runspace" -Tag "CI" { It "Should write attach event and mark runspace as having a remote debugger attached" { $onAttachName = [System.Management.Automation.PSEngineEvent]::OnDebugAttach - - $debugTarget = [PowerShell]::Create() - $null = $debugTarget.AddCommand('Wait-Event').AddParameter('SourceIdentifier', $onAttachName) - $waitTask = $debugTarget.BeginInvoke() - - $debugTarget.Runspace.IsRemoteDebuggerAttached | Should -BeFalse - - $debugger = [PowerShell]::Create() - $null = $debugger.AddCommand('Debug-Runspace').AddParameter('Id', $debugTarget.Runspace.Id) - $debugTask = $debugger.BeginInvoke() - - $waitTask.AsyncWaitHandle.WaitOne(5000) | Should -BeTrue - $waitInfo = $debugTarget.EndInvoke($waitTask) - $waitInfo.SourceIdentifier | Should -Be $onAttachName - - $debugTarget.Runspace.IsRemoteDebuggerAttached | Should -BeTrue - - $debugger.Stop() - $exp = { - $debugger.EndInvoke($debugTask) - } | Should -Throw -PassThru - $exp.FullyQualifiedErrorId | Should -Be "PipelineStoppedException" - - $debugTarget.Runspace.IsRemoteDebuggerAttached | Should -BeFalse + + $targetRunspace = $null + $debugTarget = $null + $debugger = $null + $debugTask = $null + + try { + # Open the target runspace up front so that 'Debug-Runspace' can never observe it in a + # non-Opened state, and so that its event manager is guaranteed to exist when the + # OnDebugAttach event is generated. + $targetRunspace = [runspacefactory]::CreateRunspace() + $targetRunspace.Open() + + $debugTarget = [PowerShell]::Create() + $debugTarget.Runspace = $targetRunspace + $null = $debugTarget.AddCommand('Wait-Event').AddParameter('SourceIdentifier', $onAttachName) + $waitTask = $debugTarget.BeginInvoke() + + # 'BeginInvoke' only queues the work. Wait until the 'Wait-Event' pipeline is actually + # running in the target runspace before attaching the debugger, so the attach event is + # never generated against a runspace that has not started executing the waiter. + $ready = Wait-UntilTrue -IntervalInMilliseconds 20 -TimeoutInMilliseconds 30000 -sb { + $debugTarget.InvocationStateInfo.State -eq [System.Management.Automation.PSInvocationState]::Running -and + $targetRunspace.RunspaceAvailability -eq [System.Management.Automation.Runspaces.RunspaceAvailability]::Busy + } + $ready | Should -BeTrue -Because "the 'Wait-Event' pipeline should be running in the target runspace" + + $targetRunspace.IsRemoteDebuggerAttached | Should -BeFalse + + $debugger = [PowerShell]::Create() + $null = $debugger.AddCommand('Debug-Runspace').AddParameter('Id', $targetRunspace.Id) + $debugTask = $debugger.BeginInvoke() + + $waitTask.AsyncWaitHandle.WaitOne(30000) | Should -BeTrue + $waitInfo = $debugTarget.EndInvoke($waitTask) + $waitInfo.SourceIdentifier | Should -Be $onAttachName + + $targetRunspace.IsRemoteDebuggerAttached | Should -BeTrue + + $debugger.Stop() + $exp = { + $debugger.EndInvoke($debugTask) + } | Should -Throw -PassThru + $exp.FullyQualifiedErrorId | Should -Be "PipelineStoppedException" + + # 'IsRemoteDebuggerAttached' is reset by the cmdlet as it unwinds, which happens + # asynchronously with respect to 'Stop' completing. + $detached = Wait-UntilTrue -IntervalInMilliseconds 20 -TimeoutInMilliseconds 30000 -sb { + -not $targetRunspace.IsRemoteDebuggerAttached + } + $detached | Should -BeTrue + + $targetRunspace.IsRemoteDebuggerAttached | Should -BeFalse + } + finally { + if ($debugger) { + try { $debugger.Stop() } catch { } + $debugger.Dispose() + } + + if ($debugTarget) { + try { $debugTarget.Stop() } catch { } + $debugTarget.Dispose() + } + + if ($targetRunspace) { $targetRunspace.Dispose() } + } } } From 8879d915a31ae2ccca73cdcfa840b749e910533a Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 25 Aug 2026 15:24:55 -0400 Subject: [PATCH 2/3] Tighten Debug-Runspace test cleanup Keep the original five-second assertion timeout and avoid broad silent catches in cleanup while preserving deterministic readiness and teardown. --- .../Debug-Runspace.Tests.ps1 | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 index 704ce675ef2..f42d2f099ae 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 @@ -55,7 +55,7 @@ Describe "Debug-Runspace" -Tag "CI" { # 'BeginInvoke' only queues the work. Wait until the 'Wait-Event' pipeline is actually # running in the target runspace before attaching the debugger, so the attach event is # never generated against a runspace that has not started executing the waiter. - $ready = Wait-UntilTrue -IntervalInMilliseconds 20 -TimeoutInMilliseconds 30000 -sb { + $ready = Wait-UntilTrue -IntervalInMilliseconds 20 -TimeoutInMilliseconds 5000 -sb { $debugTarget.InvocationStateInfo.State -eq [System.Management.Automation.PSInvocationState]::Running -and $targetRunspace.RunspaceAvailability -eq [System.Management.Automation.Runspaces.RunspaceAvailability]::Busy } @@ -67,7 +67,7 @@ Describe "Debug-Runspace" -Tag "CI" { $null = $debugger.AddCommand('Debug-Runspace').AddParameter('Id', $targetRunspace.Id) $debugTask = $debugger.BeginInvoke() - $waitTask.AsyncWaitHandle.WaitOne(30000) | Should -BeTrue + $waitTask.AsyncWaitHandle.WaitOne(5000) | Should -BeTrue $waitInfo = $debugTarget.EndInvoke($waitTask) $waitInfo.SourceIdentifier | Should -Be $onAttachName @@ -81,7 +81,7 @@ Describe "Debug-Runspace" -Tag "CI" { # 'IsRemoteDebuggerAttached' is reset by the cmdlet as it unwinds, which happens # asynchronously with respect to 'Stop' completing. - $detached = Wait-UntilTrue -IntervalInMilliseconds 20 -TimeoutInMilliseconds 30000 -sb { + $detached = Wait-UntilTrue -IntervalInMilliseconds 20 -TimeoutInMilliseconds 5000 -sb { -not $targetRunspace.IsRemoteDebuggerAttached } $detached | Should -BeTrue @@ -90,12 +90,12 @@ Describe "Debug-Runspace" -Tag "CI" { } finally { if ($debugger) { - try { $debugger.Stop() } catch { } + $debugger.Stop() $debugger.Dispose() } if ($debugTarget) { - try { $debugTarget.Stop() } catch { } + $debugTarget.Stop() $debugTarget.Dispose() } @@ -103,4 +103,3 @@ Describe "Debug-Runspace" -Tag "CI" { } } } - From fa3013806aaccf9855b4d7dc7b79835093e05580 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 25 Aug 2026 15:26:24 -0400 Subject: [PATCH 3/3] Warn when Debug-Runspace cleanup fails Preserve best-effort cleanup while surfacing stop failures instead of suppressing them silently. --- .../Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 index f42d2f099ae..0f39c34f63d 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Debug-Runspace.Tests.ps1 @@ -90,12 +90,12 @@ Describe "Debug-Runspace" -Tag "CI" { } finally { if ($debugger) { - $debugger.Stop() + try { $debugger.Stop() } catch { Write-Warning "Failed to stop the debugger during cleanup: $_" } $debugger.Dispose() } if ($debugTarget) { - $debugTarget.Stop() + try { $debugTarget.Stop() } catch { Write-Warning "Failed to stop the debug target during cleanup: $_" } $debugTarget.Dispose() }