use std::io::Write as _;
use std::sync::Arc;
use std::time::Duration;
use tests_pty::Pty;
use super::live_output::ToolSpinner;
use crate::agentic::{LiveToolOutput, ToolOutputStream};
const HEADER: &str = "⚙ run_command: sleep";
const RESULT: &str = "▒ (exit 0)";
const LIVE_MARK: &str = "LIVEBYTES:";
const DWELL: Duration = Duration::from_millis(300);
fn frames(s: &str) -> Vec<char> {
s.chars()
.filter(|c| ('\u{2800}'..='\u{28FF}').contains(c))
.collect()
}
fn flush() {
let _ = std::io::stdout().flush();
}
struct MarkerSink;
impl LiveToolOutput for MarkerSink {
fn start(&self, _generation: u64) {}
fn write(&self, _generation: u64, _stream: ToolOutputStream, chunk: &[u8]) {
println!("{LIVE_MARK}{}", String::from_utf8_lossy(chunk));
flush();
}
fn finish(&self, _generation: u64) {}
fn abandon(&self, _generation: u64) {}
}
#[test]
#[ignore = "child process of the tool-spinner PTY test"]
fn silent_tool_child() {
if std::env::var_os("NEWT_TOOL_SPINNER_PTY_CHILD").as_deref() != Some("silent".as_ref()) {
return;
}
println!("{HEADER} 1");
flush();
{
let spinner = ToolSpinner::start("run_command", true);
assert!(
spinner.is_live(),
"a pty child with TERM set must be able to own the line"
);
std::thread::sleep(DWELL); } println!("{RESULT}");
flush();
}
#[test]
#[ignore = "child process of the tool-spinner PTY test"]
fn delayed_first_output_child() {
if std::env::var_os("NEWT_TOOL_SPINNER_PTY_CHILD").as_deref() != Some("delayed".as_ref()) {
return;
}
println!("{HEADER} 2");
flush();
{
let spinner = ToolSpinner::start("run_command", true);
let sink = spinner
.wrap(Some(Arc::new(MarkerSink) as Arc<dyn LiveToolOutput>))
.expect("a live sink is wrapped");
std::thread::sleep(DWELL); sink.start(1);
std::thread::sleep(DWELL);
sink.write(1, ToolOutputStream::Stdout, b"first output");
std::thread::sleep(DWELL);
}
println!("{RESULT}");
flush();
}
fn run_scenario(scenario: &str, child_test: &str) -> String {
let pty = Pty::open();
let mut child = std::process::Command::new(
std::env::current_exe().expect("the test binary re-invokes itself"),
)
.args(["--exact", child_test, "--ignored", "--nocapture"])
.env("NEWT_TOOL_SPINNER_PTY_CHILD", scenario)
.env("TERM", "xterm-256color")
.stdin(pty.slave_stdio())
.stdout(pty.slave_stdio())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn the pty child");
let status = child.wait().expect("wait for the pty child");
let screen = pty.screen_to_eof();
assert!(
status.success(),
"the {scenario} scenario child failed.\n\nscreen:\n{screen:?}"
);
screen
}
fn locate(screen: &str, needle: &str, what: &str) -> usize {
screen
.find(needle)
.unwrap_or_else(|| panic!("{what} never reached the terminal.\n\nscreen:\n{screen:?}"))
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn a_silent_tool_call_is_never_a_blank_row() {
let screen = run_scenario(
"silent",
"agentic::tools::tool_spinner_pty_test::silent_tool_child",
);
let header_end = locate(&screen, HEADER, "the tool header") + HEADER.len();
let result_at = locate(&screen, RESULT, "the tool result");
assert!(
!frames(&screen[header_end..result_at]).is_empty(),
"no spinner between the header and the result — this IS #1727.\n\nbetween:\n{:?}",
&screen[header_end..result_at]
);
assert!(
screen[header_end..result_at].contains("run_command…"),
"the spinner did not carry the tool's label.\n\nbetween:\n{:?}",
&screen[header_end..result_at]
);
assert!(
frames(&screen[result_at..]).is_empty(),
"a spinner frame survived past the result.\n\nafter:\n{:?}",
&screen[result_at..]
);
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn the_first_live_byte_takes_the_row_and_the_spinner_never_returns() {
let screen = run_scenario(
"delayed",
"agentic::tools::tool_spinner_pty_test::delayed_first_output_child",
);
let header_end = locate(&screen, HEADER, "the tool header") + HEADER.len();
let live_at = locate(&screen, LIVE_MARK, "the live output");
let result_at = locate(&screen, RESULT, "the tool result");
assert!(
!frames(&screen[header_end..live_at]).is_empty(),
"no spinner covered the child before its first byte.\n\nbefore:\n{:?}",
&screen[header_end..live_at]
);
assert!(
frames(&screen[live_at..]).is_empty(),
"a spinner frame landed at or after the live output — stale chrome on \
the viewport's row.\n\nafter the handover:\n{:?}",
&screen[live_at..]
);
assert!(
live_at < result_at,
"the live output must precede the canonical result"
);
}