Repository: denoland/deno
Pull request: fix(test): user input clearing test runner output

I noticed that we don't sanitise output in the test runner when printing it. This allows clearing the output from the test runner itself through "destructive" ansi codes. Ran into this on the Fresh code base.

Before:
<img width="586" height="235" alt="Screenshot 2026-02-06 at 14 01 28" src="https://github.com/user-attachments/assets/0db8b8cd-cff8-4379-b824-ce1601bcf812" />

After:

<img width="593" height="200" alt="Screenshot 2026-02-06 at 14 00 59" src="https://github.com/user-attachments/assets/ce7d55fb-c7ab-4e9f-b0ab-b27f5a4b4e6e" />


Changed files:
filename: cli/tools/test/reporters/pretty.rs
patch: @@ -1,8 +1,11 @@
 // Copyright 2018-2026 the Deno authors. MIT license.
 
+use std::io::IsTerminal;
+
 use super::common;
 use super::fmt::to_relative_path_or_remote_url;
 use super::*;
+use crate::util::console::filter_destructive_ansi;
 
 pub struct PrettyTestReporter {
   parallel: bool,
@@ -104,6 +107,14 @@ impl PrettyTestReporter {
     self.write_output_end();
     if self.in_new_line || self.scope_test_id != Some(description.id) {
       self.force_report_step_wait(description);
+    } else if std::io::stdout().is_terminal() {
+      write!(
+        &mut self.writer,
+        "\r{}{} ...",
+        "  ".repeat(description.level),
+        description.name
+      )
+      .ok();
     }
 
     if !self.parallel {
@@ -241,7 +252,8 @@ impl TestReporter for PrettyTestReporter {
 
     // output everything to stdout in order to prevent
     // stdout and stderr racing
-    std::io::stdout().write_all(output).ok();
+    let filtered = filter_destructive_ansi(output);
+    std::io::stdout().write_all(&filtered).ok();
   }
 
   fn report_result(
@@ -276,6 +288,26 @@ impl TestReporter for PrettyTestReporter {
     self.write_output_end();
     if self.in_new_line || self.scope_test_id != Some(description.id) {
       self.force_report_wait(description);
+    } else if std::io::stdout().is_terminal() {
+      // We believe the cursor is right after "test name ...", but external
+      // output (e.g. from native addons writing directly to fd 1) may have
+      // moved it. Use \r to return to column 0 and re-write the test name
+      // so the result line is always intact. For normal tests this harmlessly
+      // overwrites the same bytes. Only do this on a real terminal — on pipes
+      // \r is a literal byte that would produce doubled output.
+      write!(&mut self.writer, "\r").ok();
+      if self.parallel {
+        write!(
+          &mut self.writer,
+          "{}",
+          colors::gray(format!(
+            "{} => ",
+            to_relative_path_or_remote_url(&self.cwd, &description.origin)
+          ))
+        )
+        .ok();
+      }
+      write!(&mut self.writer, "{} ...", description.name).ok();
     }
 
     let status = match result {

filename: cli/util/console.rs
patch: @@ -1,5 +1,6 @@
 // Copyright 2018-2026 the Deno authors. MIT license.
 
+use std::borrow::Cow;
 use std::io;
 use std::sync::Arc;
 
@@ -33,6 +34,101 @@ pub fn new_console_static_text() -> ConsoleStaticText {
   })
 }
 
+/// Strips destructive ANSI escape sequences from user output while preserving
+/// SGR (color/style) sequences. Returns `Cow::Borrowed` when no filtering needed.
+pub fn filter_destructive_ansi(input: &[u8]) -> Cow<'_, [u8]> {
+  if !input
+    .iter()
+    .any(|&b| b == 0x1b || b == 0x07 || b == 0x08 || b == b'\r')
+  {
+    return Cow::Borrowed(input);
+  }
+
+  let mut out = Vec::with_capacity(input.len());
+  let mut i = 0;
+
+  while i < input.len() {
+    match input[i] {
+      0x07 | 0x08 => i += 1,
+      // Strip standalone \r (line-overwrite), keep \r\n
+      b'\r' if i + 1 < input.len() && input[i + 1] == b'\n' => {
+        out.extend_from_slice(b"\r\n");
+        i += 2;
+      }
+      b'\r' => i += 1,
+      0x1b if i + 1 >= input.len() => i += 1,
+      0x1b => {
+        match input[i + 1] {
+          b'[' => {
+            let seq_end = skip_csi(&input[i..]);
+            // Keep SGR sequences (final byte 'm', no private marker '?'/'>'/'<')
+            let final_byte = input.get(i + seq_end - 1);
+            let has_private = input
+              .get(i + 2)
+              .is_some_and(|&b| matches!(b, b'?' | b'>' | b'<'));
+            if final_byte == Some(&b'm') && !has_private {
+              out.extend_from_slice(&input[i..i + seq_end]);
+            }
+            i += seq_end;
+          }
+          // OSC/DCS/PM/APC: string sequences terminated by BEL/ST
+          b']' | b'P' | b'^' | b'_' => i += skip_str_seq(&input[i..]),
+          // Two-byte ESC sequences (Fe/Fp/Fs)
+          0x30..=0x7E => i += 2,
+          // nF: ESC + intermediate bytes (0x20..=0x2F) + final byte
+          0x20..=0x2F => {
+            i += 2;
+            while i < input.len() && (0x20..=0x2F).contains(&input[i]) {
+              i += 1;
+            }
+            if i < input.len() && (0x30..=0x7E).contains(&input[i]) {
+              i += 1;
+            }
+          }
+          _ => i += 1,
+        }
+      }
+      b => {
+        out.push(b);
+        i += 1;
+      }
+    }
+  }
+
+  Cow::Owned(out)
+}
+
+/// Returns the length of a CSI sequence (`ESC [` params final-byte).
+fn skip_csi(data: &[u8]) -> usize {
+  let mut j = 2;
+  if j < data.len() && matches!(data[j], b'?' | b'>' | b'<') {
+    j += 1;
+  }
+  while j < data.len() && (0x30..=0x3F).contains(&data[j]) {
+    j += 1;
+  }
+  while j < data.len() && (0x20..=0x2F).contains(&data[j]) {
+    j += 1;
+  }
+  if j < data.len() && (0x40..=0x7E).contains(&data[j]) {
+    j += 1;
+  }
+  j
+}
+
+/// Skips an OSC/DCS/PM/APC string sequence terminated by BEL, ST (ESC \), or 0x9c.
+fn skip_str_seq(data: &[u8]) -> usize {
+  let mut j = 2;
+  while j < data.len() {
+    match data[j] {
+      0x07 | 0x9c => return j + 1,
+      0x1b if data.get(j + 1) == Some(&b'\\') => return j + 2,
+      _ => j += 1,
+    }
+  }
+  j
+}
+
 pub struct RawMode {
   needs_disable: bool,
 }
@@ -170,3 +266,163 @@ pub fn confirm(options: ConfirmOptions) -> Option<bool> {
 
   None
 }
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn filter_destructive_ansi_plain_text() {
+    let input = b"hello world";
+    let result = filter_destructive_ansi(input);
+    assert!(matches!(result, Cow::Borrowed(_)));
+    assert_eq!(&*result, b"hello world");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_preserves_sgr_color() {
+    let input = b"\x1b[31mred\x1b[0m";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"\x1b[31mred\x1b[0m");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_preserves_complex_sgr() {
+    let input = b"\x1b[1;38;2;255;0;0mbold red\x1b[0m";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"\x1b[1;38;2;255;0;0mbold red\x1b[0m");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_strips_clear_screen() {
+    let input = b"before\x1b[2Jafter";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"beforeafter");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_strips_cursor_up() {
+    let input = b"line\x1b[2Aup";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"lineup");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_strips_erase_line() {
+    let input = b"text\x1b[Kmore";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"textmore");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_strips_cursor_position() {
+    let input = b"start\x1b[10;20Hend";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"startend");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_strips_terminal_reset() {
+    let input = b"before\x1bcafter";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"beforeafter");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_strips_private_mode() {
+    // Hide cursor
+    let input = b"text\x1b[?25lmore";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"textmore");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_strips_osc_title() {
+    let input = b"before\x1b]0;evil title\x07after";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"beforeafter");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_strips_osc_with_st() {
+    let input = b"before\x1b]0;title\x1b\\after";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"beforeafter");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_mixed_sequences() {
+    // SGR red + clear screen + text + SGR reset
+    let input = b"\x1b[31mred\x1b[2Jtext\x1b[0m";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"\x1b[31mredtext\x1b[0m");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_bel_and_bs_stripped() {
+    let input = b"hello\x07world\x08!";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"helloworld!");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_preserves_whitespace() {
+    let input = b"line1\nline2\ttab";
+    let result = filter_destructive_ansi(input);
+    assert!(matches!(result, Cow::Borrowed(_)));
+    assert_eq!(&*result, b"line1\nline2\ttab");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_strips_standalone_cr() {
+    // Standalone \r (used by progress bars to overwrite lines) is stripped
+    let input = b"progress\roverwrite";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"progressoverwrite");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_preserves_crlf() {
+    // \r\n line endings are preserved
+    let input = b"line1\r\nline2\r\n";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"line1\r\nline2\r\n");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_strips_alt_screen() {
+    // Alt screen on and off
+    let input = b"\x1b[?1049hcontent\x1b[?1049l";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"content");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_strips_dcs_sequence() {
+    let input = b"before\x1bPsome data\x1b\\after";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"beforeafter");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_strips_scroll_up() {
+    let input = b"text\x1b[3Smore";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"textmore");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_trailing_esc() {
+    let input = b"text\x1b";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"text");
+  }
+
+  #[test]
+  fn filter_destructive_ansi_sgr_reset_bare() {
+    // Bare ESC[m is equivalent to ESC[0m
+    let input = b"\x1b[mtext";
+    let result = filter_destructive_ansi(input);
+    assert_eq!(&*result, b"\x1b[mtext");
+  }
+}

filename: tests/specs/test/ansi_escape_in_test/__test__.jsonc
patch: @@ -0,0 +1,5 @@
+{
+  "args": "test main.ts",
+  "output": "main.out",
+  "exitCode": 0
+}

filename: tests/specs/test/ansi_escape_in_test/main.out
patch: @@ -0,0 +1,12 @@
+Check [WILDCARD]main.ts
+running 1 test from ./main.ts
+ansi escape filtering ...
+------- output -------
+[WILDCARD]red text[WILDCARD]
+[WILDCARD]bold green[WILDCARD]
+plain output
+----- output end -----
+ansi escape filtering ... ok ([WILDCARD])
+
+ok | 1 passed | 0 failed ([WILDCARD])
+

filename: tests/specs/test/ansi_escape_in_test/main.ts
patch: @@ -0,0 +1,18 @@
+Deno.test("ansi escape filtering", () => {
+  // Destructive sequences that should be stripped
+  console.log("\x1b[2J"); // clear screen
+  console.log("\x1b[H"); // cursor home
+  console.log("\x1b[10;20H"); // cursor position
+  console.log("\x1b[2A"); // cursor up
+  console.log("\x1b[K"); // erase line
+  console.log("\x1b[?25l"); // hide cursor
+  console.log("\x1b[?1049h"); // alt screen
+  console.log("\x1bc"); // terminal reset
+
+  // SGR color sequences should pass through
+  console.log("\x1b[31mred text\x1b[0m");
+  console.log("\x1b[1;32mbold green\x1b[0m");
+
+  // Plain text should pass through
+  console.log("plain output");
+});