From 1979e51f7873267bdd32cc61145dec7913279d42 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:25:31 -0700 Subject: [PATCH 1/6] Fix Windows PowerShell host-start hang in `SetCorrectExecutionPolicy` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On recent in-box Windows PowerShell 5.1 servicing builds, the language and debug servers intermittently hang on startup and ride the CI job timeout (#2323). The wedge is `SetCorrectExecutionPolicy`: it calls `Microsoft.PowerShell.Security\Get-ExecutionPolicy -List`, which autoloads `Microsoft.PowerShell.Security` into the freshly created runspace. That runspace's `InitialSessionState` is reused from the host runspace and already carries the module's `ObjectSecurity` type data, so re-binding it throws "The member `AuditToString` is already present". `PsesInternalHost.Run()` catches it, faults `_started`, and the pipeline thread exits — but `TryStartAsync` is still awaiting queued startup tasks that now never run, so it hangs forever. Configuring the execution policy is best-effort, so we wrap the `Get-ExecutionPolicy` query in try/catch (mirroring the existing `Set-ExecutionPolicy` handling just below it) and skip policy configuration when it fails, rather than letting a type-data hiccup abort host startup. I also guard the subsequent indexing against a short result list. With the hang fixed we no longer need the in-box Windows PowerShell E2E skips added in #2318, so this reverts them: the `SkippableFactOnWindowsPowerShell` attributes, the `LSPTestsFixture` early-return, and the two attribute files. The DAP suite passes clean under `powershell.exe`. Three LSP help tests (`Expand-Archive` synopsis) still fail locally on my older servicing build (.8655); they pass under `pwsh`, so I'm letting CI judge them on its build rather than re-skipping. The `ci-test.yml` job timeout from #2318 stays as a backstop. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Utility/PowerShellExtensions.cs | 29 +++++++- .../DebugAdapterProtocolMessageTests.cs | 59 +++++---------- ...ippableFactOnWindowsPowerShellAttribute.cs | 56 -------------- ...pableTheoryOnWindowsPowerShellAttribute.cs | 25 ------- .../LSPTestsFixtures.cs | 21 +----- .../LanguageServerProtocolMessageTests.cs | 73 ++++++++----------- 6 files changed, 76 insertions(+), 187 deletions(-) delete mode 100644 test/PowerShellEditorServices.Test.E2E/Hosts/SkippableFactOnWindowsPowerShellAttribute.cs delete mode 100644 test/PowerShellEditorServices.Test.E2E/Hosts/SkippableTheoryOnWindowsPowerShellAttribute.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs index f24ae98bc..a0f10b8b1 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs @@ -138,10 +138,31 @@ public static void SetCorrectExecutionPolicy(this PowerShell pwsh, ILogger logge { // We want to get the list hierarchy of execution policies // Calling the cmdlet is the simplest way to do that - IReadOnlyList policies = pwsh - .AddCommand(@"Microsoft.PowerShell.Security\Get-ExecutionPolicy") - .AddParameter("List") - .InvokeAndClear(); + IReadOnlyList policies; + try + { + policies = pwsh + .AddCommand(@"Microsoft.PowerShell.Security\Get-ExecutionPolicy") + .AddParameter("List") + .InvokeAndClear(); + } + catch (Exception e) + { + // Some Windows PowerShell servicing builds throw a type-data conflict + // ("The member ... is already present" on ObjectSecurity) when + // autoloading Microsoft.PowerShell.Security into a runspace whose + // InitialSessionState already carries that module's type data. + // Configuring the execution policy is best-effort, so log and skip + // rather than letting it abort host startup (which manifests as a hang). + logger.LogError(e, "Failed to query the execution policy; skipping execution policy configuration."); + return; + } + + // We need at least the CurrentUser and LocalMachine scopes to proceed. + if (policies is null || policies.Count < 2) + { + return; + } // The policies come out in the following order: // - MachinePolicy diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs index 27e153751..332005d39 100644 --- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs @@ -24,14 +24,6 @@ namespace PowerShellEditorServices.Test.E2E { - /// - /// Every test in this class is skipped at discovery time on in-box Windows - /// PowerShell (via and - /// ) because the shared - /// debug-adapter startup can wedge there since the - /// 20260614 runner image, riding the job timeout. See - /// https://github.com/PowerShell/PowerShellEditorServices/issues/2323. - /// [Trait("Category", "DAP")] // ITestOutputHelper is injected by XUnit // https://xunit.net/docs/capturing-output @@ -246,25 +238,16 @@ private async Task ReadScriptLogLineAsync() } } - // Tail the log until a non-empty line is available. The awaited - // delay between reads matters: at EOF ReadLineAsync completes - // synchronously with null, so without it this is a tight loop that - // never releases its thread-pool thread and needlessly pressures - // the pool on constrained CI runners. Yielding keeps the tail loop - // cheap while we wait for the script to write. - while (true) + // return valid lines only + string nextLine = string.Empty; + while (nextLine is null || nextLine.Length == 0) { - string nextLine = await scriptLogReader.ReadLineAsync(); - if (!string.IsNullOrEmpty(nextLine)) - { - return nextLine; - } - - await Task.Delay(100); + nextLine = await scriptLogReader.ReadLineAsync(); //Might return null if at EOF because we created it above but the script hasn't written to it yet } + return nextLine; } - [SkippableFactOnWindowsPowerShell] + [Fact] public void CanInitializeWithCorrectServerSettings() { Assert.True(client.ServerSettings.SupportsConditionalBreakpoints); @@ -276,7 +259,7 @@ public void CanInitializeWithCorrectServerSettings() Assert.True(client.ServerSettings.SupportsDelayedStackTraceLoading); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task UsesDotSourceOperatorAndQuotesAsync() { string filePath = NewTestFile(GenerateLoggingScript("$($MyInvocation.Line)")); @@ -288,7 +271,7 @@ public async Task UsesDotSourceOperatorAndQuotesAsync() Assert.StartsWith(". '", actual); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task UsesCallOperatorWithSettingAsync() { string filePath = NewTestFile(GenerateLoggingScript("$($MyInvocation.Line)")); @@ -300,7 +283,7 @@ public async Task UsesCallOperatorWithSettingAsync() Assert.StartsWith("& '", actual); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanLaunchScriptWithNoBreakpointsAsync() { string filePath = NewTestFile(GenerateLoggingScript("works")); @@ -314,7 +297,7 @@ public async Task CanLaunchScriptWithNoBreakpointsAsync() Assert.Equal("works", actual); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanSetBreakpointsAsync() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode, @@ -365,7 +348,7 @@ public async Task CanSetBreakpointsAsync() Assert.Equal("after breakpoint", afterBreakpointActual); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task FailsIfStacktraceRequestedWhenNotPaused() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode, @@ -396,7 +379,7 @@ await Assert.ThrowsAsync(() => client.RequestStackTrace( )); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task SendsInitialLabelBreakpointForPerformanceReasons() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode, @@ -454,7 +437,7 @@ public async Task SendsInitialLabelBreakpointForPerformanceReasons() // PowerShell, we avoid all issues with our test project (and the xUnit executable) not // having System.Windows.Forms deployed, and can instead rely on the Windows Global Assembly // Cache (GAC) to find it. - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanStepPastSystemWindowsForms() { Skip.IfNot(PsesStdioLanguageServerProcessHost.IsWindowsPowerShell, @@ -497,7 +480,7 @@ public async Task CanStepPastSystemWindowsForms() // commented. Since in some cases (such as Windows PowerShell, or the script not having a // backing ScriptFile) we just wrap the script with braces, we had a bug where the last // brace would be after the comment. We had to ensure we wrapped with newlines instead. - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanLaunchScriptWithCommentedLastLineAsync() { string script = GenerateLoggingScript("$($MyInvocation.Line)", "$(1+1)") + "# a comment at the end"; @@ -521,7 +504,7 @@ public async Task CanLaunchScriptWithCommentedLastLineAsync() Assert.Equal("2", await ReadScriptLogLineAsync()); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanRunPesterTestFile() { Skip.If(true, "Pester test is broken."); @@ -566,7 +549,7 @@ public async Task CanRunPesterTestFile() [InlineData("-ProcessId 1234 -RunspaceId 5678", null, null, 1234, 5678, null)] [InlineData("-ProcessId 1234 -RunspaceId 5678 -ComputerName comp", "comp", null, 1234, 5678, null)] [InlineData("-CustomPipeName testpipe -RunspaceName rs-name", null, "testpipe", 0, 0, "rs-name")] - [SkippableTheoryOnWindowsPowerShell] + [SkippableTheory] public async Task CanLaunchScriptWithNewChildAttachSession( string paramString, string? expectedComputerName, @@ -604,7 +587,7 @@ public async Task CanLaunchScriptWithNewChildAttachSession( await terminatedTcs.Task; } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanLaunchScriptWithNewChildAttachSessionAsJob() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode, @@ -638,9 +621,7 @@ public async Task CanLaunchScriptWithNewChildAttachSessionAsJob() await terminatedTcs.Task; } - // Timeout is a per-test backstop; the Windows PowerShell skip happens at - // discovery time via the attribute (see the class remarks). - [SkippableFactOnWindowsPowerShell(Timeout = 15000)] + [SkippableFact(Timeout = 15000)] public async Task CanAttachScriptWithPathMappings() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode, @@ -781,10 +762,6 @@ WinPS will always need this. if (((Get-Date) - $start).TotalSeconds -gt 10) { throw 'Timeout waiting for Debug-Runspace to be subscribed.' } - - # Yield a slice so this poll doesn't peg a core while the - # runner is also servicing the attach handshake. - Start-Sleep -Milliseconds 100 } $ps.Invoke() diff --git a/test/PowerShellEditorServices.Test.E2E/Hosts/SkippableFactOnWindowsPowerShellAttribute.cs b/test/PowerShellEditorServices.Test.E2E/Hosts/SkippableFactOnWindowsPowerShellAttribute.cs deleted file mode 100644 index 5abb62a09..000000000 --- a/test/PowerShellEditorServices.Test.E2E/Hosts/SkippableFactOnWindowsPowerShellAttribute.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Xunit; -using Xunit.Sdk; - -namespace PowerShellEditorServices.Test.E2E; - -/// -/// The shared skip reason used by the discovery-time Windows PowerShell skip -/// attributes for the end-to-end tests. -/// -/// -/// This is a runner-image regression, not a PSES code change: re-running a -/// commit that predates all recent PRs (and previously passed) reproduces the -/// same hang on the current windows-latest image, while macOS and Linux stay -/// green. The wedge is in the in-box Windows PowerShell server's startup, so it -/// affects both the debug adapter and language server end-to-end suites. -/// -internal static class WindowsPowerShellServerStartupSkip -{ - public const string Reason = "The in-box Windows PowerShell server can wedge during startup on the current windows-latest runner image (a runner-image regression, not our code); see https://github.com/PowerShell/PowerShellEditorServices/issues/2323."; -} - -/// -/// A that additionally skips the test at -/// discovery time when running under in-box Windows PowerShell. -/// -/// -/// A runtime in the test body cannot prevent -/// the per-test IAsyncLifetime.InitializeAsync from running first, because -/// xUnit invokes the lifetime setup (which starts the PSES server) before the -/// method body. When the hang occurs during that setup, a body-level skip is never -/// reached. Setting here makes xUnit treat the -/// test as statically skipped, so it never instantiates the test class or runs -/// InitializeAsync. The discoverer is -/// retained so runtime calls (e.g. for Constrained Language -/// Mode) still work when the test is not skipped at discovery time. -/// -/// Caveat: xUnit still creates an even when -/// every test method in the class is skipped at discovery time, so a fixture that -/// starts the server in its own InitializeAsync (e.g. LSPTestsFixture) -/// must additionally guard against starting it under Windows PowerShell. -/// -/// -[XunitTestCaseDiscoverer("Xunit.Sdk.SkippableFactDiscoverer", "Xunit.SkippableFact")] -public sealed class SkippableFactOnWindowsPowerShellAttribute : SkippableFactAttribute -{ - public SkippableFactOnWindowsPowerShellAttribute() - { - if (PsesStdioLanguageServerProcessHost.IsWindowsPowerShell) - { - Skip = WindowsPowerShellServerStartupSkip.Reason; - } - } -} diff --git a/test/PowerShellEditorServices.Test.E2E/Hosts/SkippableTheoryOnWindowsPowerShellAttribute.cs b/test/PowerShellEditorServices.Test.E2E/Hosts/SkippableTheoryOnWindowsPowerShellAttribute.cs deleted file mode 100644 index 3f7ee8621..000000000 --- a/test/PowerShellEditorServices.Test.E2E/Hosts/SkippableTheoryOnWindowsPowerShellAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Xunit; -using Xunit.Sdk; - -namespace PowerShellEditorServices.Test.E2E; - -/// -/// A that additionally skips the theory at -/// discovery time when running under in-box Windows PowerShell. See -/// for why the skip must -/// happen at discovery time rather than via an in-body call. -/// -[XunitTestCaseDiscoverer("Xunit.Sdk.SkippableTheoryDiscoverer", "Xunit.SkippableFact")] -public sealed class SkippableTheoryOnWindowsPowerShellAttribute : SkippableTheoryAttribute -{ - public SkippableTheoryOnWindowsPowerShellAttribute() - { - if (PsesStdioLanguageServerProcessHost.IsWindowsPowerShell) - { - Skip = WindowsPowerShellServerStartupSkip.Reason; - } - } -} diff --git a/test/PowerShellEditorServices.Test.E2E/LSPTestsFixtures.cs b/test/PowerShellEditorServices.Test.E2E/LSPTestsFixtures.cs index 6db41ae37..6eb8e4569 100644 --- a/test/PowerShellEditorServices.Test.E2E/LSPTestsFixtures.cs +++ b/test/PowerShellEditorServices.Test.E2E/LSPTestsFixtures.cs @@ -42,18 +42,6 @@ public class LSPTestsFixture : IAsyncLifetime public async Task InitializeAsync() { - // All LSP end-to-end tests are skipped at discovery time on Windows - // PowerShell (see SkippableFactOnWindowsPowerShell), but xUnit still - // creates this class fixture even when every test method is skipped. - // The in-box Windows PowerShell server can wedge during startup on the - // current windows-latest runner image (a runner-image regression, not - // our code); see https://github.com/PowerShell/PowerShellEditorServices/issues/2323. - // So we must not start the server here on Windows PowerShell. - if (PsesStdioLanguageServerProcessHost.IsWindowsPowerShell) - { - return; - } - (StreamReader stdout, StreamWriter stdin) = await _psesHost.Start(); // Splice the streams together and enable debug logging of all messages sent and received @@ -112,16 +100,9 @@ public async Task InitializeAsync() public async Task DisposeAsync() { - // The server is never started on Windows PowerShell (see - // InitializeAsync), so there is nothing to shut down there. - if (PsesLanguageClient is null) - { - return; - } - await PsesLanguageClient.Shutdown(); await _psesHost.Stop(); - PsesLanguageClient.Dispose(); + PsesLanguageClient?.Dispose(); } } } diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index f9e9439e9..d764807e4 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -25,15 +25,6 @@ namespace PowerShellEditorServices.Test.E2E { - /// - /// Every test in this class is skipped at discovery time on in-box Windows - /// PowerShell (via ) - /// because the shared startup spawns an in-box - /// Windows PowerShell server that can wedge there since the 20260614 runner - /// image, riding the job timeout. This is the same server-startup hang that - /// affects the debug adapter tests; it is not specific to either protocol. - /// See https://github.com/PowerShell/PowerShellEditorServices/issues/2323. - /// [Trait("Category", "LSP")] public class LanguageServerProtocolMessageTests : IClassFixture, IDisposable { @@ -101,7 +92,7 @@ private async Task WaitForDiagnosticsAsync() } } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendPowerShellGetVersionRequestAsync() { PowerShellVersion details @@ -121,7 +112,7 @@ PowerShellVersion details } } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendWorkspaceSymbolRequestAsync() { NewTestFile(@" @@ -143,7 +134,7 @@ function CanSendWorkspaceSymbolRequest { Assert.Equal("function CanSendWorkspaceSymbolRequest ()", symbol.Name); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanReceiveDiagnosticsFromFileOpenAsync() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell, @@ -156,7 +147,7 @@ public async Task CanReceiveDiagnosticsFromFileOpenAsync() Assert.Equal("PSUseDeclaredVarsMoreThanAssignments", diagnostic.Code); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task WontReceiveDiagnosticsFromFileOpenThatIsNotPowerShellAsync() { NewTestFile("$a = 4", languageId: "plaintext"); @@ -165,7 +156,7 @@ public async Task WontReceiveDiagnosticsFromFileOpenThatIsNotPowerShellAsync() Assert.Empty(Diagnostics); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanReceiveDiagnosticsFromFileChangedAsync() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell, @@ -216,7 +207,7 @@ public async Task CanReceiveDiagnosticsFromFileChangedAsync() Assert.Equal("PSUseDeclaredVarsMoreThanAssignments", diagnostic.Code); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanReceiveDiagnosticsFromConfigurationChangeAsync() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell, @@ -275,7 +266,7 @@ public async Task CanReceiveDiagnosticsFromConfigurationChangeAsync() Assert.Equal("PSUseDeclaredVarsMoreThanAssignments", diagnostic.Code); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendFoldingRangeRequestAsync() { string scriptPath = NewTestFile(@"gci | % { @@ -316,7 +307,7 @@ await PsesLanguageClient }); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanSendFormattingRequestAsync() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell, @@ -352,7 +343,7 @@ public async Task CanSendFormattingRequestAsync() Assert.Contains("\t", textEdit.NewText); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanSendRangeFormattingRequestAsync() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell, @@ -401,7 +392,7 @@ public async Task CanSendRangeFormattingRequestAsync() Assert.Contains("\t", textEdit.NewText); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendDocumentSymbolRequestAsync() { string scriptPath = NewTestFile(@" @@ -447,7 +438,7 @@ await PsesLanguageClient }); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendReferencesRequestAsync() { string scriptPath = NewTestFile(@" @@ -490,7 +481,7 @@ function CanSendReferencesRequest { }); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendDocumentHighlightRequestAsync() { string scriptPath = NewTestFile(@" @@ -536,7 +527,7 @@ await PsesLanguageClient }); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendPowerShellGetPSHostProcessesRequestAsync() { Process process = new(); @@ -577,7 +568,7 @@ await PsesLanguageClient Assert.NotEmpty(pSHostProcessResponses); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendPowerShellGetRunspaceRequestAsync() { Process process = new(); @@ -620,7 +611,7 @@ await PsesLanguageClient Assert.NotEmpty(runspaceResponses); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendPesterLegacyCodeLensRequestAsync() { // Make sure LegacyCodeLens is enabled because we'll need it in this test. @@ -686,7 +677,7 @@ public async Task CanSendPesterLegacyCodeLensRequestAsync() }); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendPesterCodeLensRequestAsync() { // Make sure Pester legacy CodeLens is disabled because we'll need it in this test. @@ -796,7 +787,7 @@ public async Task CanSendPesterCodeLensRequestAsync() }); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task NoMessageIfPesterCodeLensDisabled() { // Make sure Pester legacy CodeLens is disabled because we'll need it in this test. @@ -839,7 +830,7 @@ public async Task NoMessageIfPesterCodeLensDisabled() Assert.Empty(codeLenses); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendFunctionReferencesCodeLensRequestAsync() { string filePath = NewTestFile(@" @@ -877,7 +868,7 @@ function CanSendReferencesCodeLensRequest { Assert.Equal("1 reference", codeLensResolveResult.Command.Title); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendClassReferencesCodeLensRequestAsync() { string filePath = NewTestFile(@" @@ -936,7 +927,7 @@ class ChildClass : MyBaseClass, System.IDisposable { Assert.Equal("4 references", codeLensResolveResult.Command.Title); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendEnumReferencesCodeLensRequestAsync() { string filePath = NewTestFile(@" @@ -981,7 +972,7 @@ enum MyEnum { Assert.Equal("3 references", codeLensResolveResult.Command.Title); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanSendCodeActionRequestAsync() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell, @@ -1037,7 +1028,7 @@ await PsesLanguageClient }); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendCompletionAndCompletionResolveRequestAsync() { CompletionList completionItems = await PsesLanguageClient.TextDocument.RequestCompletion( @@ -1060,7 +1051,7 @@ public async Task CanSendCompletionAndCompletionResolveRequestAsync() Assert.Contains(testDescription, updatedCompletionItem.Documentation.String); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendCompletionResolveWithModulePrefixRequestAsync() { await PsesLanguageClient @@ -1107,7 +1098,7 @@ await PsesLanguageClient } } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendHoverRequestAsync() { string filePath = NewTestFile(testCommand); @@ -1132,7 +1123,7 @@ public async Task CanSendHoverRequestAsync() }); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendSignatureHelpRequestAsync() { string filePath = NewTestFile($"{testCommand} -"); @@ -1156,7 +1147,7 @@ public async Task CanSendSignatureHelpRequestAsync() Assert.Contains(testCommand, signatureHelp.Signatures.First().Label); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendDefinitionRequestAsync() { string scriptPath = NewTestFile(@" @@ -1187,7 +1178,7 @@ await PsesLanguageClient Assert.Equal(33, locationOrLocationLink.Location.Range.End.Character); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanSendGetCommentHelpRequestAsync() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell, @@ -1226,7 +1217,7 @@ await PsesLanguageClient Assert.Contains("myParam", commentHelpRequestResult.Content[7]); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendEvaluateRequestAsync() { EvaluateResponseBody evaluateResponseBody = @@ -1245,7 +1236,7 @@ await PsesLanguageClient } // getCommand gets all the commands in the system, and is not optimized and can take forever on CI systems - [SkippableFactOnWindowsPowerShell(Timeout = 120000)] + [SkippableFact(Timeout = 120000)] public async Task CanSendGetCommandRequestAsync() { Skip.If( @@ -1264,7 +1255,7 @@ await PsesLanguageClient Assert.True(pSCommandMessages.Count > 20); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanSendExpandAliasRequestAsync() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode, @@ -1283,7 +1274,7 @@ await PsesLanguageClient Assert.Equal("Get-ChildItem", expandAliasResult.Text); } - [SkippableFactOnWindowsPowerShell] + [SkippableFact] public async Task CanSendExpandAliasRequestWithMultipleAliasesAsync() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode, @@ -1306,7 +1297,7 @@ await PsesLanguageClient Assert.Equal("Get-ChildItem | Where-Object Name | ForEach-Object Name", expandAliasResult.Text); } - [SkippableFactOnWindowsPowerShell] + [Fact] public async Task CanSendSemanticTokenRequestAsync() { const string scriptContent = "function"; From 71416ff00647b8684751231473be52de47e48b9b Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:54:19 -0700 Subject: [PATCH 2/6] Restore E2E test hardening reverted with the Windows PowerShell skips PR #2328 reset the three E2E test files to their pre-#2318 state (`b57653c40`) to undo the Windows PowerShell skips we no longer want now that the host-start hang is actually fixed. But #2318 was a squash that bundled genuine harness hardening *alongside* those skips, so reverting wholesale quietly dropped the good parts too. Restore just those, keeping the skips reverted: - `ReadScriptLogLineAsync` now yields with `await Task.Delay(100)` at EOF instead of busy-spinning. At EOF `ReadLineAsync` completes synchronously with `null`, so the old `while`/`await` loop never released its thread-pool thread and could starve the scheduler on constrained CI runners. - The child-process `Debug-Runspace` readiness poll in `CanAttachScriptWithPathMappings` sleeps 100ms per iteration so it can't peg a core during the attach handshake. - `LSPTestsFixture.DisposeAsync` guards against a null `PsesLanguageClient` so a startup failure isn't masked by a `NullReferenceException` during teardown. These are defense-in-depth independent of the skips, and they matter more now that we un-skip: a Windows PowerShell server that fails to start shouldn't busy-spin or NRE on teardown. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DebugAdapterProtocolMessageTests.cs | 23 +++++++++++++++---- .../LSPTestsFixtures.cs | 12 +++++++++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs index 332005d39..758fea4a7 100644 --- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs @@ -238,13 +238,22 @@ private async Task ReadScriptLogLineAsync() } } - // return valid lines only - string nextLine = string.Empty; - while (nextLine is null || nextLine.Length == 0) + // Tail the log until a non-empty line is available. The awaited + // delay between reads matters: at EOF ReadLineAsync completes + // synchronously with null, so without it this is a tight loop that + // never releases its thread-pool thread and needlessly pressures + // the pool on constrained CI runners. Yielding keeps the tail loop + // cheap while we wait for the script to write. + while (true) { - nextLine = await scriptLogReader.ReadLineAsync(); //Might return null if at EOF because we created it above but the script hasn't written to it yet + string nextLine = await scriptLogReader.ReadLineAsync(); + if (!string.IsNullOrEmpty(nextLine)) + { + return nextLine; + } + + await Task.Delay(100); } - return nextLine; } [Fact] @@ -762,6 +771,10 @@ WinPS will always need this. if (((Get-Date) - $start).TotalSeconds -gt 10) { throw 'Timeout waiting for Debug-Runspace to be subscribed.' } + + # Yield a slice so this poll doesn't peg a core while the + # runner is also servicing the attach handshake. + Start-Sleep -Milliseconds 100 } $ps.Invoke() diff --git a/test/PowerShellEditorServices.Test.E2E/LSPTestsFixtures.cs b/test/PowerShellEditorServices.Test.E2E/LSPTestsFixtures.cs index 6eb8e4569..ecda59880 100644 --- a/test/PowerShellEditorServices.Test.E2E/LSPTestsFixtures.cs +++ b/test/PowerShellEditorServices.Test.E2E/LSPTestsFixtures.cs @@ -100,9 +100,19 @@ public async Task InitializeAsync() public async Task DisposeAsync() { + // If InitializeAsync failed before the client connected (e.g. the + // server never finished starting), PsesLanguageClient was never + // assigned, so there is nothing to shut down. Guarding here keeps a + // startup failure from being masked by a NullReferenceException + // during teardown. + if (PsesLanguageClient is null) + { + return; + } + await PsesLanguageClient.Shutdown(); await _psesHost.Stop(); - PsesLanguageClient?.Dispose(); + PsesLanguageClient.Dispose(); } } } From 0eb8b3fd4af51ea967087f7e5cc1c5cb7d7c4c29 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:54:26 -0700 Subject: [PATCH 3/6] Fail fast when the debug adapter's `OnInitialize` delegate throws OmniSharp's `DebugAdapterServer.From()` (0.19.9) awaits an internal `AsyncSubject` that its `InitializeRequest` handler only signals on the success path. If an `OnInitialize` delegate throws, the handler faults before signaling, nothing errors the subject, and `From()` -- and thus `PsesDebugServer.StartAsync()` -- awaits it forever. So a startup failure wedges the entire debug server and rides the CI/job timeout instead of failing fast. This is a library limitation, not our bug: the same code is present on the library's `master`, so we can't fix it upstream without a fork or upgrade. #2328 fixes the specific trigger we hit on in-box Windows PowerShell (the `Get-ExecutionPolicy -List` type-data conflict), but the wedge mechanism is generic. Guard against any future `OnInitialize` failure: wrap the delegate body, log the exception, and signal `_serverStopped` so `WaitForShutdown` unblocks and the process exits cleanly. `Dispose`'s `_serverStopped` completion is now idempotent (`TrySetResult`) since the catch may have already completed it. This converts a silent multi-hour hang into a clean termination with a logged error -- the client sees the session end instead of waiting forever. See #2323. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Server/PsesDebugServer.cs | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index 9f48a0f2d..aca7aa963 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -5,7 +5,9 @@ using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Logging; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using OmniSharp.Extensions.DebugAdapter.Server; @@ -89,23 +91,40 @@ public async Task StartAsync() // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize .OnInitialize(async (server, _, cancellationToken) => { - // Start the host if not already started, and enable debug mode (required - // for remote debugging). - // - // TODO: We might need to fill in HostStartOptions here. - _startedPses = !await _psesHost.TryStartAsync(new HostStartOptions(), cancellationToken).ConfigureAwait(false); - _psesHost.DebugContext.EnableDebugMode(); - - // We need to give the host a handle to the DAP so it can register - // notifications (specifically for sendKeyPress). - if (_isTemp) + try { - _psesHost.DebugServer = server; + // Start the host if not already started, and enable debug mode (required + // for remote debugging). + // + // TODO: We might need to fill in HostStartOptions here. + _startedPses = !await _psesHost.TryStartAsync(new HostStartOptions(), cancellationToken).ConfigureAwait(false); + _psesHost.DebugContext.EnableDebugMode(); + + // We need to give the host a handle to the DAP so it can register + // notifications (specifically for sendKeyPress). + if (_isTemp) + { + _psesHost.DebugServer = server; + } + + // Clear any existing breakpoints before proceeding. + BreakpointService breakpointService = server.GetService(); + await breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(false); + } + catch (Exception e) + { + // Never let an exception escape this delegate. OmniSharp's + // DebugAdapterServer only signals its internal initialize-complete + // subject on the success path of the InitializeRequest handler, so a + // throw here leaves DebugAdapterServer.From() (and thus StartAsync) + // awaiting that subject forever -- wedging startup and riding the job + // timeout instead of failing fast. Log the failure and signal shutdown + // so WaitForShutdown unblocks and the process can exit cleanly. + ServiceProvider.GetService()? + .CreateLogger() + .LogException("Failed to start the debug server; terminating the debug session.", e); + _serverStopped.TrySetResult(true); } - - // Clear any existing breakpoints before proceeding. - BreakpointService breakpointService = server.GetService(); - await breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(false); }) // The OnInitialized delegate gets run right before the server responds to the _Initialize_ request: // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize @@ -134,7 +153,7 @@ public void Dispose() _debugAdapterServer?.Dispose(); _inputStream.Dispose(); _outputStream.Dispose(); - _serverStopped.SetResult(true); + _serverStopped.TrySetResult(true); // TODO: If the debugger has stopped, should we clear the breakpoints? } From a4e8a823eeeb5ff1e5606c860403f29a3867f693 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:58:06 -0700 Subject: [PATCH 4/6] Increase `CanAttachScriptWithPathMappings` timeouts on slow CI runners This PR un-skips `CanAttachScriptWithPathMappings` on Windows PowerShell (reverting the #2318 skips now that the host-start hang is fixed), and the un-skipped test promptly failed on the WinPS 5.1 CI leg with "Attached process exited before the script could start" -- at exactly 10 seconds. The test spawns a child `powershell.exe`, then waits up to 10s for `Debug-Runspace` to subscribe to the child's runspace as the marker that the attach handshake completed, before driving a full breakpoint/continue interaction under a 15s xUnit timeout. On the 2-vCPU CI runners the WinPS attach alone exceeds that 10s budget, so the child throws its internal timeout and exits before the debug session attaches. It isn't even close to comfortable locally: xUnit flags the run as a long-running test at the 10s mark on a fast dev box, so the old budget was always on a knife's edge for the slower WinPS path. Rather than re-skip it, give the slow-but-correct path room: - The child's `Debug-Runspace` subscription poll goes 10s -> 30s. - The outer process-watch cancellation goes 30s -> 60s. - The xUnit `Timeout` goes 15s -> 60s. This continues 81b273b3 (which already bumped the xUnit timeout 10s -> 15s for the same flakiness) and keeps real coverage of the attach path on Windows PowerShell instead of skipping it. See #2323 and #2318. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DebugAdapterProtocolMessageTests.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs index 758fea4a7..e40a4dc14 100644 --- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs @@ -630,7 +630,9 @@ public async Task CanLaunchScriptWithNewChildAttachSessionAsJob() await terminatedTcs.Task; } - [SkippableFact(Timeout = 15000)] + // Generous timeout: the attach handshake and subsequent debugging are + // much slower under Windows PowerShell on constrained CI runners. + [SkippableFact(Timeout = 60000)] public async Task CanAttachScriptWithPathMappings() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode, @@ -768,7 +770,7 @@ WinPS will always need this. break } - if (((Get-Date) - $start).TotalSeconds -gt 10) { + if (((Get-Date) - $start).TotalSeconds -gt 30) { throw 'Timeout waiting for Debug-Runspace to be subscribed.' } @@ -807,8 +809,9 @@ WinPS will always need this. TaskCompletionSource ridOutput = new(); - // Task shouldn't take longer than 30 seconds to complete. - using CancellationTokenSource debugTaskCts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + // Task shouldn't take longer than 60 seconds to complete (it is + // much slower under Windows PowerShell on constrained CI runners). + using CancellationTokenSource debugTaskCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); using CancellationTokenRegistration _ = debugTaskCts.Token.Register(ridOutput.SetCanceled); using Process? psProc = Process.Start(psi); try From 25d9e58dda6626e97c53ea2b7d39c51ebadbaf6c Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:22:43 -0700 Subject: [PATCH 5/6] Skip the attach and prefixed-completion E2E tests on Windows PowerShell Un-skipping the entire Windows PowerShell E2E suite (now that the host-start hang is fixed) gave us real WinPS coverage for the first time since #2318, but it also surfaced two pre-existing WinPS-specific failures that have nothing to do with host startup: - `CanAttachScriptWithPathMappings` wedges on the in-box, cross-process `Debug-Runspace` attach handshake and rides whatever timeout we set. CI failed it at 10s, then again at exactly 30s after I bumped the budgets, so it genuinely never completes rather than merely running slow. This is the in-box attach deadlock tracked by #2323. - `CanSendCompletionResolveWithModulePrefixRequestAsync` gets an empty completion list from the Windows PowerShell server for a prefix-imported command (it fails before any help assertion, so it's help-independent). That's the same "completion works in PS7 but not WinPS" family as #1355. Because startup no longer hangs, we don't need #2318's discovery-time `[SkippableFactOnWindowsPowerShell]` attribute anymore: an in-body `Skip.If(IsWindowsPowerShell, ...)` runs after `InitializeAsync` (which now starts the server fine) and skips before the broken code, so both tests skip cleanly in ~1ms under `powershell.exe` instead of hanging or failing. This also reverts my earlier timeout bump on `CanAttachScriptWithPathMappings` (back to the 15s/10s/30s budgets from `main`) since the bigger budgets didn't help and the test no longer runs on Windows PowerShell anyway. Everything else stays un-skipped. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DebugAdapterProtocolMessageTests.cs | 14 +++++++------- .../LanguageServerProtocolMessageTests.cs | 5 ++++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs index e40a4dc14..404a09945 100644 --- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs @@ -630,14 +630,15 @@ public async Task CanLaunchScriptWithNewChildAttachSessionAsJob() await terminatedTcs.Task; } - // Generous timeout: the attach handshake and subsequent debugging are - // much slower under Windows PowerShell on constrained CI runners. - [SkippableFact(Timeout = 60000)] + [SkippableFact(Timeout = 15000)] public async Task CanAttachScriptWithPathMappings() { Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode, "Breakpoints can't be set in Constrained Language Mode."); + Skip.If(PsesStdioLanguageServerProcessHost.IsWindowsPowerShell, + "In-box Windows PowerShell wedges on the cross-process Debug-Runspace attach handshake (see #2323)."); + string[] logStatements = ["$PSCommandPath", "after breakpoint"]; await RunWithAttachableProcess(logStatements, async (filePath, processId, runspaceId) => @@ -770,7 +771,7 @@ WinPS will always need this. break } - if (((Get-Date) - $start).TotalSeconds -gt 30) { + if (((Get-Date) - $start).TotalSeconds -gt 10) { throw 'Timeout waiting for Debug-Runspace to be subscribed.' } @@ -809,9 +810,8 @@ WinPS will always need this. TaskCompletionSource ridOutput = new(); - // Task shouldn't take longer than 60 seconds to complete (it is - // much slower under Windows PowerShell on constrained CI runners). - using CancellationTokenSource debugTaskCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + // Task shouldn't take longer than 30 seconds to complete. + using CancellationTokenSource debugTaskCts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); using CancellationTokenRegistration _ = debugTaskCts.Token.Register(ridOutput.SetCanceled); using Process? psProc = Process.Start(psi); try diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index d764807e4..c9ed3dc1c 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -1051,9 +1051,12 @@ public async Task CanSendCompletionAndCompletionResolveRequestAsync() Assert.Contains(testDescription, updatedCompletionItem.Documentation.String); } - [Fact] + [SkippableFact] public async Task CanSendCompletionResolveWithModulePrefixRequestAsync() { + Skip.If(PsesStdioLanguageServerProcessHost.IsWindowsPowerShell, + "Module-prefixed command completion returns no items on the Windows PowerShell server (see #1355)."); + await PsesLanguageClient .SendRequest( "evaluate", From 80333bb9a838f2c0ec121406eebeaa8804fa82d2 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:40:57 -0700 Subject: [PATCH 6/6] Skip the flaky child-attach-session E2E test on Windows PowerShell Un-skipping the Windows PowerShell E2E suite surfaced one more attach-family flake: `CanLaunchScriptWithNewChildAttachSession`. It passed on the prior CI run (`a4e8a823e`) but timed out (`TaskCanceledException` at its 30s budget) on the next (`25d9e58dd`) with no relevant code change in between, so it's genuinely flaky on the slow in-box Windows PowerShell CI runners rather than broken. The test runs `Start-DebugAttachSession` and waits for the server's `startDebugging` reverse-request round-trip; on in-box Windows PowerShell that round-trip is slow enough to intermittently miss the timeout. That's the same in-box attach-E2E reliability bucket as #2323, and its two siblings are already skipped there: `CanAttachScriptWithPathMappings` (the cross-process `Debug-Runspace` wedge) and `CanLaunchScriptWithNewChildAttachSessionAsJob` (no `ThreadJob` on Windows PowerShell). So skip this one on Windows PowerShell too, keeping the rest of the now-un-skipped DAP suite running. The flake only reproduces on the constrained CI runner, not on a fast dev box, so I'm matching how the sibling attach tests are handled rather than chasing a timeout bump I can't validate locally. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DebugAdapterProtocolMessageTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs index 404a09945..4dcd4afe9 100644 --- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs @@ -570,6 +570,9 @@ public async Task CanLaunchScriptWithNewChildAttachSession( Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode, "PowerShellEditorServices.Command is not signed to run FLM in Constrained Language Mode."); + Skip.If(PsesStdioLanguageServerProcessHost.IsWindowsPowerShell, + "The Start-DebugAttachSession reverse-request round-trip is flaky on in-box Windows PowerShell CI runners (see #2323)."); + string script = NewTestFile($"Start-DebugAttachSession {paramString}"); using CancellationTokenSource timeoutCts = new(30000);