diff --git a/tests/FSharp.Test.Utilities/TestFramework.fs b/tests/FSharp.Test.Utilities/TestFramework.fs
index dcb64909ad2..edfd00f9b6f 100644
--- a/tests/FSharp.Test.Utilities/TestFramework.fs
+++ b/tests/FSharp.Test.Utilities/TestFramework.fs
@@ -515,33 +515,19 @@ module Command =
 
     let exec dir envVars (redirect:RedirectInfo) path args =
 
-#if !NETCOREAPP
-        let ensureConsole () =
-            // Set UTF-8 encoding for console input/output to ensure FSI receives UTF-8 data.
-            // This is needed because on net472 ProcessStartInfo.StandardInputEncoding is unavailable,
-            // so the spawned process inherits the console's encoding settings.
-            Console.InputEncoding <- Text.UTF8Encoding(false)
-            Console.OutputEncoding <- Text.UTF8Encoding(false)
-#else
-        let ensureConsole () = ()
-#endif
-
         let inputWriter sources (writer: StreamWriter) =
             let pipeFile name = async {
                 let path = Commands.getfullpath dir name
-                
-                // Read file content as text using UTF-8 (the standard encoding for F# source files)
-                let! content = async {
-                    use reader = new StreamReader(path, Text.Encoding.UTF8, detectEncodingFromByteOrderMarks = true)
-                    return! reader.ReadToEndAsync() |> Async.AwaitTask
-                }
-                
-                // Write using the StreamWriter which now uses UTF-8 encoding (set in ensureConsole).
+                use reader = File.OpenRead (path)
+                use ms = new MemoryStream()
+                do! reader.CopyToAsync (ms) |> (Async.AwaitIAsyncResult >> Async.Ignore)
+                ms.Position <- 0L
                 try
-                    do! writer.WriteAsync(content) |> Async.AwaitTask
+                    do! ms.CopyToAsync(writer.BaseStream) |> (Async.AwaitIAsyncResult >> Async.Ignore)
                     do! writer.FlushAsync() |> (Async.AwaitIAsyncResult >> Async.Ignore)
                 with
-                | :? System.IO.IOException -> ()
+                | :? System.IO.IOException -> //input closed is ok if process is closed
+                    ()
                 }
             sources |> pipeFile |> Async.RunSynchronously
 
@@ -585,8 +571,6 @@ module Command =
 
         let exec cmdArgs =
             printfn "%s" (logExec dir path args redirect)
-            if cmdArgs.RedirectInput.IsSome then
-                ensureConsole()
             Process.exec cmdArgs dir envVars path args
 
         { RedirectOutput = None; RedirectError = None; RedirectInput = None }
diff --git a/tests/FSharp.Test.Utilities/XunitHelpers.fs b/tests/FSharp.Test.Utilities/XunitHelpers.fs
index e74e8c38f71..91f230277a2 100644
--- a/tests/FSharp.Test.Utilities/XunitHelpers.fs
+++ b/tests/FSharp.Test.Utilities/XunitHelpers.fs
@@ -18,6 +18,7 @@ open FSharp.Compiler.Diagnostics
 open OpenTelemetry.Resources
 open OpenTelemetry.Trace
 open OpenTelemetry.Metrics
+open System.Text
 
 /// Disables custom internal parallelization added with XUNIT_EXTRAS.
 /// Execute test cases in a class or a module one by one instead of all at once. Allow other collections to run simultaneously.
@@ -189,35 +190,38 @@ module OneTimeSetup =
         // Ensure that the initialization is done only once per test run.
         init.Force()
 
-/// `XunitTestFramework` providing parallel console support and conditionally enabling optional xUnit customizations.
-/// NOTE: Temporarily disabled due to xUnit3 API incompatibilities
-/// TODO: Reimplement for xUnit3 if OneTimeSetup, OpenTelemetry, or cleanup functionality is needed
-(*
-type FSharpXunitFramework(sink: IMessageSink) =
-    inherit XunitTestFramework(sink)
+type FSharpXunitFramework() =
+    inherit XunitTestFramework()
 
     do OneTimeSetup.EnsureInitialized()
             
-    override this.CreateExecutor (assemblyName) =
-        { new XunitTestFrameworkExecutor(assemblyName, this.SourceInformationProvider, this.DiagnosticMessageSink) with
+    override this.CreateExecutor (assembly) =
+        { new XunitTestFrameworkExecutor(new XunitTestAssembly(assembly)) with
             
-            // Because xUnit v2 lacks assembly fixture, this is a good place to ensure things get called right at the start of the test run.
-            override x.RunTestCases(testCases, executionMessageSink, executionOptions) =
-
-                let testRunName = $"RunTests_{assemblyName.Name} {Runtime.InteropServices.RuntimeInformation.FrameworkDescription}"
+            // TODO: Consider moving most of that to ITestPipelineStartup implementation.
+            override x.RunTestCases(testCases, executionMessageSink, executionOptions, cancellationToken) =
+                // When running in Azure DevOps, we end up with codepage 65001 (UTF-8).
+                // So, the process where the tests are running has UTF-8 with BOM, by default.
+                // The child fsi.exe process is started with CreateNoWindow, causing it to not inherit the same encoding.
+                // So, when we attempt to write to its standard input, we right with BOM, but the child process isn't expecting the BOM character.
+                // To workaround this, we set the console input encoding to UTF-8 without BOM, explicitly.
+                // This is probably a bug in .NET Framework Process API when CreateNoWindow is set to true.
+                // It looks like the following logic assumes that the child process always inherits the same input encoding:
+                // https://github.com/microsoft/referencesource/blob/ec9fa9ae770d522a5b5f0607898044b7478574a3/System/services/monitoring/system/diagnosticts/Process.cs#L2153-L2156
+                Console.InputEncoding <- UTF8Encoding(false)
+
+                let testRunName = $"RunTests_{assembly.GetName().Name} {Runtime.InteropServices.RuntimeInformation.FrameworkDescription}"
 
                 use _ = new OpenTelemetryExport(testRunName, Environment.GetEnvironmentVariable("FSHARP_OTEL_EXPORT") <> null)                 
   
                 begin
                     use _ = Activity.startNoTags testRunName
-                    // We can't just call base.RunTestCases here, because it's implementation is async void.
-                    use runner = new XunitTestAssemblyRunner (x.TestAssembly, testCases, x.DiagnosticMessageSink, executionMessageSink, executionOptions)
-                    runner.RunAsync().Wait()
+                    base.RunTestCases(testCases, executionMessageSink, executionOptions, cancellationToken).AsTask().Wait()
                 end
 
                 cleanUpTemporaryDirectoryOfThisTestRun ()
+                new ValueTask()
         }
-*)
 
 #if XUNIT_EXTRAS
     // Rewrites discovered test cases to support extra parallelization and batch trait injection.
diff --git a/tests/FSharp.Test.Utilities/XunitSetup.fs b/tests/FSharp.Test.Utilities/XunitSetup.fs
index 93d2b09121e..7f00c00d363 100644
--- a/tests/FSharp.Test.Utilities/XunitSetup.fs
+++ b/tests/FSharp.Test.Utilities/XunitSetup.fs
@@ -24,17 +24,6 @@ type NotThreadSafeResourceCollection() = class end
 
 module XUnitSetup =
 
-    // NOTE: Custom TestFramework temporarily disabled due to xUnit3 API incompatibilities
-    // TODO: Reimplement FSharpXunitFramework for xUnit3 if needed
-    // [<assembly: TestFramework("FSharp.Test.FSharpXunitFramework", "FSharp.Test.Utilities")>]
-    
-    // NOTE: CaptureTrace is disabled because it conflicts with TestConsole.ExecutionCapture
-    // which is used by FSI tests to capture console output. xUnit3's trace capture intercepts
-    // console output before it can reach TestConsole's redirectors.
-    // [<assembly: CaptureTrace>]
-    
-    /// Call this to ensure TestConsole is installed. Safe to call multiple times.
-    let initialize() = XUnitInit.initialize()
-    
-    // Force initialization when module is loaded
-    do initialize()
+    [<assembly: TestFramework(typeof<FSharpXunitFramework>)>]
+    do ()
+
