diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs
index 9d9e4e4377..c963c785e3 100644
--- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs
+++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs
@@ -1036,6 +1036,87 @@ public void AnsiTerminal_UsesWindowWidthNotBufferWidth()
Assert.AreEqual(120, terminal.Width);
}
+ ///
+ /// Locks in the PR #8348 fix for issue #6753: when the progress state has not changed between
+ /// two refresh ticks (same ProgressId + ProgressVersion), the renderer must only
+ /// rewrite the duration cell instead of erasing and re-emitting the whole progress line.
+ ///
+ /// Before the fix the optimization was guarded by a && false leftover, which made every
+ /// 500 ms tick fall through to the full re-render branch (CSI K + counters + assembly name).
+ /// This test fails if that branch is ever disabled again.
+ ///
+ [TestMethod]
+ public void AnsiTerminal_ProgressFrame_OnlyUpdatesDuration_WhenProgressVersionUnchanged()
+ {
+ string targetFramework = "net8.0";
+ string architecture = "x64";
+ string assembly = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\work\assembly.dll" : "/mnt/work/assembly.dll";
+
+ var stringBuilderConsole = new StringBuilderConsole();
+ var stopwatchFactory = new StopwatchFactory();
+ var terminalReporter = new TerminalTestReporter(assembly, targetFramework, architecture, stringBuilderConsole, new CTRLPlusCCancellationTokenSource(), new TerminalTestReporterOptions
+ {
+ ShowPassedTests = () => true,
+ AnsiMode = AnsiMode.ForceAnsi,
+
+ // Intentionally do NOT enable ShowActiveTests: the optimization is per-line and we keep
+ // the rendered frame to a single line (the assembly progress) to make assertions simple.
+ ShowActiveTests = false,
+ ShowProgress = () => true,
+ })
+ {
+ CreateStopwatch = stopwatchFactory.CreateStopwatch,
+ };
+
+ // Gate the refresher thread so renders happen one at a time, on our cue, with deterministic
+ // duration values. Without this, the 500 ms timer would race with our assertions.
+ var renderGate = new AutoResetEvent(initialState: false);
+ var renderDone = new AutoResetEvent(initialState: false);
+ terminalReporter.OnProgressStartUpdate += (sender, args) => renderGate.WaitOne();
+ terminalReporter.OnProgressStopUpdate += (sender, args) => renderDone.Set();
+
+ terminalReporter.TestExecutionStarted(DateTimeOffset.MinValue, 1, isDiscovery: false);
+ terminalReporter.AssemblyRunStarted();
+
+ // Pick a starting elapsed value whose rendered form ("1s") has the same length as the value
+ // we will use for the second tick ("2s"). The duration-only path only fires when the rendered
+ // duration string has the same length as the one rendered in the previous frame.
+ stopwatchFactory.AddTime(TimeSpan.FromSeconds(1));
+
+ // First tick: nothing was rendered yet, so this is the full frame.
+ int beforeFirstRender = stringBuilderConsole.Output.Length;
+ renderGate.Set();
+ renderDone.WaitOne();
+ string firstRender = stringBuilderConsole.Output[beforeFirstRender..];
+
+ // Sanity: the first render is the full frame (counters + assembly name + duration).
+ Assert.Contains("assembly.dll", firstRender);
+ Assert.Contains("(1s)", firstRender);
+
+ // Advance the clock by 1 second without touching any progress state. The worker version is
+ // unchanged, so the next render should take the "same Id + Version → duration-only" path.
+ stopwatchFactory.AddTime(TimeSpan.FromSeconds(1));
+
+ int beforeSecondRender = stringBuilderConsole.Output.Length;
+ renderGate.Set();
+ renderDone.WaitOne();
+ string secondRender = stringBuilderConsole.Output[beforeSecondRender..];
+
+ // The duration-only path writes only the new duration with cursor positioning; it must not
+ // re-emit the counters, the assembly name, or a CSI K erase-in-line.
+ Assert.Contains("(2s)", secondRender);
+ Assert.DoesNotContain("assembly.dll", secondRender);
+ Assert.DoesNotContain("(1s)", secondRender);
+ Assert.DoesNotContain($"{AnsiCodes.CSI}{AnsiCodes.EraseInLine}", secondRender);
+ Assert.DoesNotContain("✓", secondRender);
+ Assert.Contains(AnsiCodes.SetCursorHorizontal(250), secondRender);
+
+ // Note: we deliberately do not stop the reporter here. The refresher thread is a background
+ // thread that is currently blocked in OnProgressStartUpdate; calling StopShowingProgress
+ // (which Joins the thread) would deadlock. The existing tests in this file follow the same
+ // pattern - the thread dies with the test process.
+ }
+
///
/// Reproduces the bug from issue #7240: when Console.BufferWidth > Console.WindowWidth,
/// the ANSI cursor positioning places timings off-screen because it was using BufferWidth