use std::time::Duration;
use tests_pty::Pty;
use crate::danger;
use crate::permissions::{
permission_prompt_text, prompt_permission_choice, PermissionPromptState, PromptPermissionGate,
};
use newt_core::caveats::{Caveats, CountBound, Scope};
use newt_core::tty::{LineCaps, Sink, Spinner};
use newt_core::{DenialKind, PermissionGate as _, PermissionRequest};
const CHILD_TEST: &str = "prompt_visibility_test::prompt_scenario_child";
const HUMAN_THINKING_TIME: Duration = Duration::from_millis(600);
fn no_net_caveats() -> Caveats {
Caveats {
fs_read: Scope::only(["/ws".to_string()]),
fs_write: Scope::only(["/ws".to_string()]),
exec: Scope::only(["cargo".to_string()]),
net: Scope::none(),
max_calls: CountBound::Unlimited,
valid_for_generation: Scope::All,
}
}
fn web_fetch_request(host: &str) -> PermissionRequest {
PermissionRequest {
tool: "web_fetch".to_string(),
kind: DenialKind::Net,
target: host.to_string(),
reason: format!("net does not permit '{host}'"),
}
}
#[test]
#[ignore = "child process of the prompt-visibility regression test"]
fn prompt_scenario_child() {
if std::env::var_os("NEWT_PROMPT_VISIBILITY_CHILD").is_none() {
return;
}
let spinner = Spinner::start_with_caps(LineCaps::Own, "thinking…", Sink::Stdout, true)
.expect("the pty is a real terminal, so the spinner takes the line");
std::thread::sleep(Duration::from_millis(250));
let request = web_fetch_request("example.com");
let mut state = PermissionPromptState::default();
{
let mut gate = PromptPermissionGate {
state: &mut state,
base: no_net_caveats(),
key_path: None,
conversation_id: "conv-prompt-visibility".to_string(),
log_path: None,
denials_path: None,
config_path: None,
preset_clamp: None,
danger: danger::DangerTable::builtin(),
color: true,
verbose: false,
web_decision_timeout: std::time::Duration::from_secs(2),
ask_human: prompt_permission_choice,
};
let _decision = gate.ask(std::slice::from_ref(&request));
}
drop(spinner);
}
#[serial_test::serial(prompt_stdin)]
#[test]
fn a_permission_prompt_is_visible_and_survives_a_live_spinner() {
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_PROMPT_VISIBILITY_CHILD", "1")
.stdin(pty.slave_stdio())
.stdout(pty.slave_stdio())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn the pty child");
std::thread::sleep(HUMAN_THINKING_TIME);
pty.type_in("d\n");
let status = child.wait().expect("wait for the pty child");
let screen = pty.screen();
assert!(
status.success(),
"the scenario child failed.\n\nscreen:\n{screen:?}"
);
let prompt_start = screen.find('⊘').unwrap_or_else(|| {
panic!("the question was never written to the terminal at all.\n\nscreen:\n{screen:?}")
});
let echo_rel = screen[prompt_start..]
.find("d\r\n")
.or_else(|| screen[prompt_start..].find("d\n"))
.unwrap_or_else(|| {
panic!(
"never saw the operator's keystroke echoed — the prompt did not \
reach a blocking read.\n\nscreen:\n{screen:?}"
)
});
let window = &screen[prompt_start..prompt_start + echo_rel];
let expected_prompt = permission_prompt_text(
&web_fetch_request("example.com"),
&danger::DangerTable::builtin(),
);
let window_lf = window.replace("\r\n", "\n");
assert!(
window_lf.contains(expected_prompt.trim_end()),
"the question did not survive intact.\n\n expected:\n{expected_prompt:?}\n\n \
on screen between the question and the keystroke:\n{window:?}"
);
let intruders: Vec<char> = window
.chars()
.filter(|c| ('\u{2800}'..='\u{28FF}').contains(c))
.collect();
assert!(
intruders.is_empty(),
"an ephemeral writer painted {} spinner frame(s) {:?} OVER the question \
while the operator was reading it — this is the reported hang: newt is \
correctly blocked on a question that has been scribbled out.\n\n \
on screen:\n{window:?}",
intruders.len(),
intruders,
);
let visible_tail = window.trim_end_matches(|c: char| c.is_whitespace());
assert!(
visible_tail.ends_with('>'),
"the choice menu was not the last thing on screen when the read \
blocked.\n\n on screen:\n{window:?}"
);
}