changed user choice for TODO stale check
user can ignore, mark stale or quit.
This commit is contained in:
@@ -98,4 +98,11 @@ impl UiWriter for MachineUiWriter {
|
|||||||
println!("PROMPT_USER_YES_NO: {}", message);
|
println!("PROMPT_USER_YES_NO: {}", message);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize {
|
||||||
|
println!("PROMPT_USER_CHOICE: {}", message);
|
||||||
|
println!("OPTIONS: {:?}", options);
|
||||||
|
// Default to first option (index 0) for automation
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -356,5 +356,27 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize {
|
||||||
|
println!("{} ", message);
|
||||||
|
for (i, option) in options.iter().enumerate() {
|
||||||
|
println!(" [{}] {}", i + 1, option);
|
||||||
|
}
|
||||||
|
print!("Select an option (1-{}): ", options.len());
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut input = String::new();
|
||||||
|
if io::stdin().read_line(&mut input).is_ok() {
|
||||||
|
if let Ok(choice) = input.trim().parse::<usize>() {
|
||||||
|
if choice > 0 && choice <= options.len() {
|
||||||
|
return choice - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print!("Invalid choice. Please select (1-{}): ", options.len());
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4276,8 +4276,25 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
print!("\x07\x07\x07\x07\x07\x07");
|
print!("\x07\x07\x07\x07\x07\x07");
|
||||||
let _ = std::io::stdout().flush();
|
let _ = std::io::stdout().flush();
|
||||||
|
|
||||||
if !self.ui_writer.prompt_user_yes_no("Requirements have changed! Continue?") {
|
let options = ["Ignore and Continue", "Mark as Stale", "Quit Application"];
|
||||||
return Ok("❌ User aborted due to stale TODO list.".to_string());
|
let choice = self.ui_writer.prompt_user_choice("Requirements have changed! What would you like to do?", &options);
|
||||||
|
|
||||||
|
match choice {
|
||||||
|
0 => {
|
||||||
|
// Ignore and Continue
|
||||||
|
self.ui_writer.print_context_status("⚠️ Ignoring staleness warning.");
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
// Mark as Stale
|
||||||
|
// We return a message to the agent so it knows to regenerate/fix it.
|
||||||
|
return Ok("⚠️ TODO list is stale (requirements changed). Please regenerate the TODO list to match the new requirements.".to_string());
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
// Quit Application
|
||||||
|
self.ui_writer.print_context_status("❌ Quitting application as requested.");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ pub trait UiWriter: Send + Sync {
|
|||||||
|
|
||||||
/// Prompt the user for a yes/no confirmation
|
/// Prompt the user for a yes/no confirmation
|
||||||
fn prompt_user_yes_no(&self, message: &str) -> bool;
|
fn prompt_user_yes_no(&self, message: &str) -> bool;
|
||||||
|
|
||||||
|
/// Prompt the user to choose from a list of options
|
||||||
|
/// Returns the index of the selected option
|
||||||
|
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A no-op implementation for when UI output is not needed
|
/// A no-op implementation for when UI output is not needed
|
||||||
@@ -84,4 +88,5 @@ impl UiWriter for NullUiWriter {
|
|||||||
fn flush(&self) {}
|
fn flush(&self) {}
|
||||||
fn wants_full_output(&self) -> bool { false }
|
fn wants_full_output(&self) -> bool { false }
|
||||||
fn prompt_user_yes_no(&self, _message: &str) -> bool { true }
|
fn prompt_user_yes_no(&self, _message: &str) -> bool { true }
|
||||||
|
fn prompt_user_choice(&self, _message: &str, _options: &[&str]) -> usize { 0 }
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ use serial_test::serial;
|
|||||||
struct MockUiWriter {
|
struct MockUiWriter {
|
||||||
output: Arc<Mutex<Vec<String>>>,
|
output: Arc<Mutex<Vec<String>>>,
|
||||||
prompt_responses: Arc<Mutex<Vec<bool>>>,
|
prompt_responses: Arc<Mutex<Vec<bool>>>,
|
||||||
|
choice_responses: Arc<Mutex<Vec<usize>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockUiWriter {
|
impl MockUiWriter {
|
||||||
@@ -17,6 +18,7 @@ impl MockUiWriter {
|
|||||||
Self {
|
Self {
|
||||||
output: Arc::new(Mutex::new(Vec::new())),
|
output: Arc::new(Mutex::new(Vec::new())),
|
||||||
prompt_responses: Arc::new(Mutex::new(Vec::new())),
|
prompt_responses: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
choice_responses: Arc::new(Mutex::new(Vec::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,6 +26,10 @@ impl MockUiWriter {
|
|||||||
self.prompt_responses.lock().unwrap().push(response);
|
self.prompt_responses.lock().unwrap().push(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_choice_response(&self, response: usize) {
|
||||||
|
self.choice_responses.lock().unwrap().push(response);
|
||||||
|
}
|
||||||
|
|
||||||
fn get_output(&self) -> Vec<String> {
|
fn get_output(&self) -> Vec<String> {
|
||||||
self.output.lock().unwrap().clone()
|
self.output.lock().unwrap().clone()
|
||||||
}
|
}
|
||||||
@@ -60,6 +66,10 @@ impl UiWriter for MockUiWriter {
|
|||||||
self.output.lock().unwrap().push(format!("PROMPT: {}", message));
|
self.output.lock().unwrap().push(format!("PROMPT: {}", message));
|
||||||
self.prompt_responses.lock().unwrap().pop().unwrap_or(true)
|
self.prompt_responses.lock().unwrap().pop().unwrap_or(true)
|
||||||
}
|
}
|
||||||
|
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize {
|
||||||
|
self.output.lock().unwrap().push(format!("CHOICE: {} Options: {:?}", message, options));
|
||||||
|
self.choice_responses.lock().unwrap().pop().unwrap_or(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -92,7 +102,7 @@ async fn test_todo_staleness_check_matching_sha() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_todo_staleness_check_mismatch_sha_abort() {
|
async fn test_todo_staleness_check_mismatch_sha_ignore() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let todo_path = temp_dir.path().join("todo.g3.md");
|
let todo_path = temp_dir.path().join("todo.g3.md");
|
||||||
std::env::set_current_dir(&temp_dir).unwrap();
|
std::env::set_current_dir(&temp_dir).unwrap();
|
||||||
@@ -106,38 +116,7 @@ async fn test_todo_staleness_check_mismatch_sha_abort() {
|
|||||||
config.agent.check_todo_staleness = true;
|
config.agent.check_todo_staleness = true;
|
||||||
|
|
||||||
let ui_writer = MockUiWriter::new();
|
let ui_writer = MockUiWriter::new();
|
||||||
ui_writer.set_prompt_response(false); // Abort
|
ui_writer.set_choice_response(0); // Ignore
|
||||||
|
|
||||||
let mut agent = Agent::new_autonomous(config, ui_writer).await.unwrap();
|
|
||||||
agent.set_requirements_sha(sha_req.to_string());
|
|
||||||
|
|
||||||
let tool_call = ToolCall {
|
|
||||||
tool: "todo_read".to_string(),
|
|
||||||
args: serde_json::json!({}),
|
|
||||||
};
|
|
||||||
let result = agent.execute_tool(&tool_call).await.unwrap();
|
|
||||||
|
|
||||||
assert!(result.contains("❌ User aborted due to stale TODO list."));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_todo_staleness_check_mismatch_sha_continue() {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let todo_path = temp_dir.path().join("todo.g3.md");
|
|
||||||
std::env::set_current_dir(&temp_dir).unwrap();
|
|
||||||
|
|
||||||
let sha_file = "old_sha";
|
|
||||||
let sha_req = "new_sha";
|
|
||||||
let content = format!("{{{{Based on the requirements file with SHA256: {}}}}}\n- [ ] Task 1", sha_file);
|
|
||||||
std::fs::write(&todo_path, content).unwrap();
|
|
||||||
|
|
||||||
let mut config = Config::default();
|
|
||||||
config.agent.check_todo_staleness = true;
|
|
||||||
|
|
||||||
let ui_writer = MockUiWriter::new();
|
|
||||||
ui_writer.set_prompt_response(true); // Continue
|
|
||||||
let output_handle = ui_writer.clone(); // Clone to keep handle
|
|
||||||
|
|
||||||
let mut agent = Agent::new_autonomous(config, ui_writer).await.unwrap();
|
let mut agent = Agent::new_autonomous(config, ui_writer).await.unwrap();
|
||||||
agent.set_requirements_sha(sha_req.to_string());
|
agent.set_requirements_sha(sha_req.to_string());
|
||||||
@@ -149,12 +128,42 @@ async fn test_todo_staleness_check_mismatch_sha_continue() {
|
|||||||
let result = agent.execute_tool(&tool_call).await.unwrap();
|
let result = agent.execute_tool(&tool_call).await.unwrap();
|
||||||
|
|
||||||
assert!(result.contains("📝 TODO list:"));
|
assert!(result.contains("📝 TODO list:"));
|
||||||
|
|
||||||
let output = output_handle.get_output();
|
|
||||||
let has_warning = output.iter().any(|s| s.contains("⚠️ TODO list is stale"));
|
|
||||||
assert!(has_warning, "Should have printed warning to UI");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_todo_staleness_check_mismatch_sha_mark_stale() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let todo_path = temp_dir.path().join("todo.g3.md");
|
||||||
|
std::env::set_current_dir(&temp_dir).unwrap();
|
||||||
|
|
||||||
|
let sha_file = "old_sha";
|
||||||
|
let sha_req = "new_sha";
|
||||||
|
let content = format!("{{{{Based on the requirements file with SHA256: {}}}}}\n- [ ] Task 1", sha_file);
|
||||||
|
std::fs::write(&todo_path, content).unwrap();
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.agent.check_todo_staleness = true;
|
||||||
|
|
||||||
|
let ui_writer = MockUiWriter::new();
|
||||||
|
ui_writer.set_choice_response(1); // Mark as Stale
|
||||||
|
|
||||||
|
let mut agent = Agent::new_autonomous(config, ui_writer).await.unwrap();
|
||||||
|
agent.set_requirements_sha(sha_req.to_string());
|
||||||
|
|
||||||
|
let tool_call = ToolCall {
|
||||||
|
tool: "todo_read".to_string(),
|
||||||
|
args: serde_json::json!({}),
|
||||||
|
};
|
||||||
|
let result = agent.execute_tool(&tool_call).await.unwrap();
|
||||||
|
|
||||||
|
assert!(result.contains("⚠️ TODO list is stale"));
|
||||||
|
assert!(result.contains("Please regenerate"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We cannot easily test "Quit" (index 2) because it calls std::process::exit(0)
|
||||||
|
// which would kill the test runner. We skip that test case here.
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_todo_staleness_check_disabled() {
|
async fn test_todo_staleness_check_disabled() {
|
||||||
|
|||||||
Reference in New Issue
Block a user