use super::*;
struct AskGate {
outcome: HumanQuestionOutcome,
asked: Vec<String>,
}
impl AskGate {
fn new(answer: Option<&str>) -> Self {
let outcome = answer.map_or(HumanQuestionOutcome::Unavailable, |a| {
HumanQuestionOutcome::Answer(a.to_string())
});
Self::with_outcome(outcome)
}
fn with_outcome(outcome: HumanQuestionOutcome) -> Self {
Self {
outcome,
asked: Vec::new(),
}
}
}
impl super::PermissionGate for AskGate {
fn ask(&mut self, _requests: &[super::PermissionRequest]) -> super::PermissionDecision {
super::PermissionDecision::Deny
}
fn ask_question(&mut self, question: &str) -> HumanQuestionOutcome {
self.asked.push(question.to_string());
self.outcome.clone()
}
}
#[test]
fn request_user_input_returns_the_human_answer() {
let mut gate = AskGate::new(Some("postgres"));
let out = execute_request_user_input(
&serde_json::json!({"question": "which database should I target?"}),
Some(&mut gate),
false,
20,
);
assert_eq!(out, "postgres");
assert_eq!(
gate.asked,
vec!["which database should I target?".to_string()]
);
}
#[test]
fn request_user_input_reaches_the_operator_even_when_permissions_are_denied() {
let mut gate = AskGate::new(Some("postgres"));
let out = execute_request_user_input(
&serde_json::json!({"question": "which database?"}),
Some(&mut gate),
false,
20,
);
assert_eq!(out, "postgres");
assert!(
!out.contains("headless"),
"a present operator is not headless: {out}"
);
}
#[test]
fn request_user_input_no_gate_reports_headless_never_hangs() {
let out = execute_request_user_input(
&serde_json::json!({"question": "are you sure?"}),
None,
false,
20,
);
assert_eq!(out, HEADLESS_NO_HUMAN);
assert!(out.contains("no human available"), "got: {out}");
}
#[test]
fn request_user_input_unavailable_reports_no_operator_not_headless() {
let mut gate = AskGate::with_outcome(HumanQuestionOutcome::Unavailable);
let out = execute_request_user_input(
&serde_json::json!({"question": "pick one"}),
Some(&mut gate),
false,
20,
);
assert_eq!(out, NO_OPERATOR_AVAILABLE);
assert!(
!out.contains("headless"),
"Unavailable must not say headless: {out}"
);
}
#[test]
fn request_user_input_cancelled_reports_cancel_not_headless() {
let mut gate = AskGate::with_outcome(HumanQuestionOutcome::Cancelled);
let out = execute_request_user_input(
&serde_json::json!({"question": "pick one"}),
Some(&mut gate),
false,
20,
);
assert_eq!(out, OPERATOR_CANCELLED);
assert!(!out.contains("headless"), "got: {out}");
assert!(!out.contains("no human available"), "got: {out}");
}
#[test]
fn request_user_input_exit_reports_exit_not_headless() {
let mut gate = AskGate::with_outcome(HumanQuestionOutcome::ExitRequested);
let out = execute_request_user_input(
&serde_json::json!({"question": "pick one"}),
Some(&mut gate),
false,
20,
);
assert_eq!(out, OPERATOR_EXIT_REQUESTED);
assert!(!out.contains("headless"), "got: {out}");
}
#[test]
fn request_user_input_eof_is_not_an_empty_answer() {
let mut gate = AskGate::with_outcome(HumanQuestionOutcome::InputClosed);
let out = execute_request_user_input(
&serde_json::json!({"question": "pick one"}),
Some(&mut gate),
false,
20,
);
assert_eq!(out, OPERATOR_INPUT_CLOSED);
assert!(!out.is_empty(), "EOF must not become an empty answer");
assert!(!out.contains("headless"), "got: {out}");
}
#[test]
fn request_user_input_failure_is_distinct_from_headless() {
let mut gate = AskGate::with_outcome(HumanQuestionOutcome::InputFailed);
let out = execute_request_user_input(
&serde_json::json!({"question": "pick one"}),
Some(&mut gate),
false,
20,
);
assert_eq!(out, OPERATOR_INPUT_FAILED);
assert_ne!(out, HEADLESS_NO_HUMAN);
assert!(!out.contains("headless"), "got: {out}");
}
#[test]
fn request_user_input_empty_answer_stays_an_empty_answer() {
let mut gate = AskGate::with_outcome(HumanQuestionOutcome::Answer(String::new()));
let out = execute_request_user_input(
&serde_json::json!({"question": "pick one"}),
Some(&mut gate),
false,
20,
);
assert_eq!(out, "");
}
#[test]
fn request_user_input_requires_a_question() {
let mut gate = AskGate::new(Some("unused"));
let out = execute_request_user_input(
&serde_json::json!({"question": " "}),
Some(&mut gate),
false,
20,
);
assert!(out.contains("'question' is required"), "got: {out}");
assert!(
gate.asked.is_empty(),
"gate not consulted for a blank question"
);
}
#[test]
fn request_user_input_is_a_real_tool_not_a_phantom() {
assert!(resolve_tool_alias("request_user_input").is_none());
assert!(ALL_TOOL_NAMES.contains(&"request_user_input"));
assert!(classify_phantom_reach(
"request_user_input",
&serde_json::json!({"question": "which db?"}),
"postgres",
true,
)
.is_none());
let defs = merged_tool_definitions(
&NoMcp,
false,
false,
false,
None,
false,
false,
false,
false,
false,
false,
false,
false,
Default::default(),
crate::agentic::shell_dialect_sentence(false, false),
);
let names: Vec<&str> = defs
.as_array()
.unwrap()
.iter()
.filter_map(|d| d["function"]["name"].as_str())
.collect();
assert!(names.contains(&"request_user_input"), "got: {names:?}");
}
#[test]
fn ask_verbs_rewrite_to_request_user_input() {
for verb in [
"ask_user",
"ask_human",
"prompt_user",
"get_user_input",
"ask_question",
"clarify",
"ask",
] {
match resolve_tool_alias(verb) {
Some(AliasOutcome::Rewrite(c)) => {
assert_eq!(c, "request_user_input", "verb: {verb}");
}
_ => panic!("expected Rewrite(request_user_input) for {verb}"),
}
}
}
#[tokio::test]
async fn request_user_input_dispatches_through_execute_tool() {
let ws = tempfile::TempDir::new().unwrap();
let caveats = Caveats::top();
let mut gate = AskGate::new(Some("the answer"));
let out = execute_tool(
"request_user_input",
&serde_json::json!({"question": "what now?"}),
&ws.path().to_string_lossy(),
false,
20,
&caveats,
&mut NoMcp,
None,
None,
None,
None, Some(&mut gate),
None,
None, None, None, None, None, None, None, )
.await;
assert_eq!(out, "the answer");
assert_eq!(gate.asked, vec!["what now?".to_string()]);
}
#[tokio::test]
async fn explain_request_user_input_keeps_the_interactive_question_gate() {
let ws = tempfile::TempDir::new().unwrap();
let caveats = Caveats::top();
let mut mcp = NoMcp;
let mut gate = AskGate::new(Some("send an Act request"));
let out = run_tool_with_disposition(
"request_user_input",
serde_json::json!({"question": "Please send this as an explicit action request."}),
ws.path(),
&caveats,
&mut mcp,
Some(&mut gate),
None,
PromptDisposition::Explain,
)
.await;
assert_eq!(out, "send an Act request");
assert_eq!(
gate.asked,
vec!["Please send this as an explicit action request.".to_string()]
);
}