use rig_harness::prelude::*;
use super::*;
use crate::tests::{press, type_text, view_app};
use rig_harness::ask::Choice;
use rig_harness::pick::{Chosen, Picked, Picker};
#[derive(Resource, Default)]
struct Reported(Vec<Option<Chosen>>);
fn report(picked: On<Picked>, mut reported: ResMut<Reported>) {
reported.0.push(picked.chosen.clone());
}
#[derive(Resource, Default)]
struct Ran(Vec<String>);
fn shown(app: &mut App) -> Option<String> {
let world = app.world_mut();
let entity = world
.query_filtered::<Entity, With<PickerInput>>()
.single(world)
.ok()?;
let title = world
.get::<Picker>(entity)
.map(|picker| picker.title.clone());
title.or_else(|| world.get::<Ask>(entity).map(|ask| ask.title.clone()))
}
#[test]
fn pickers_wait_their_turn_and_say_what_was_picked() {
let (mut app, agent) = view_app();
app.init_resource::<Reported>()
.init_resource::<Ran>()
.add_observer(report)
.add_command(
"note",
"Notes its arguments",
|In(args): In<CommandArgs>, mut ran: ResMut<Ran>| {
ran.0.push(args.args);
},
);
let world = app.world_mut();
let first = world
.spawn(
Picker::new("Size")
.item("small", "note small")
.item("large", "note large"),
)
.id();
world.spawn(Picker::new("Branch").item("main", "").free_text());
world.spawn(
Picker::new("Model")
.for_agent(agent)
.item("fast", "note fast"),
);
app.update();
assert_eq!(shown(&mut app).as_deref(), Some("Size"));
press(&mut app, "down enter");
assert!(app.world().get_entity(first).is_err());
assert_eq!(app.world().resource::<Ran>().0, ["large"]);
assert_eq!(shown(&mut app).as_deref(), Some("Branch"));
type_text(&mut app, "feature");
press(&mut app, "enter");
assert_eq!(shown(&mut app).as_deref(), Some("Model"));
press(&mut app, "esc");
assert_eq!(shown(&mut app), None);
assert_eq!(
app.world().resource::<Reported>().0,
[Some(Chosen::item(1)), Some(Chosen::text("feature")), None]
);
assert_eq!(crate::tests::input(&app).text(), "");
}
#[test]
fn tab_ticks_several_items_and_enter_picks_them_in_list_order() {
let (mut app, agent) = view_app();
app.init_resource::<Reported>()
.init_resource::<Ran>()
.init_resource::<Answered>()
.add_observer(report)
.add_observer(record_answers)
.add_command(
"note",
"Notes its arguments",
|In(args): In<CommandArgs>, mut ran: ResMut<Ran>| {
ran.0.push(args.args);
},
);
let picker = Picker::new("Delete which files?")
.multiple()
.item("a.tmp", "note a")
.item("b.log", "note b")
.checked(true)
.item("c.tmp", "note c");
app.world_mut().spawn(picker);
app.update();
press(&mut app, "down tab tab up up tab");
assert_eq!(shown(&mut app).as_deref(), Some("Delete which files?"));
press(&mut app, "enter");
assert_eq!(
app.world().resource::<Reported>().0,
[Some(Chosen::items([0, 2]))]
);
assert_eq!(app.world().resource::<Ran>().0, ["a", "c"]);
let checks = Ask::new("Which checks?")
.choice("fmt", "fmt")
.choice("test", "test")
.multiple();
app.world_mut().spawn((checks, ForAgent(agent)));
app.update();
press(&mut app, "down enter");
let answers = &app.world().resource::<Answered>().0;
let test = Answer::choice("test");
assert_eq!(answers, &[("Which checks?".to_owned(), Some(test))]);
}
#[test]
fn a_picker_filled_in_batches_keeps_what_is_typed_and_selected() {
use rig_harness::pick::Highlighted;
let (mut app, _) = view_app();
app.init_resource::<Reported>().add_observer(report);
let batch = |items: &[&str]| {
let picker = Picker::new("Files");
items.iter().fold(picker, |picker, item| {
picker.item(*item, format!("open {item}"))
})
};
let picker = app.world_mut().spawn(batch(&["a.rs", "b.rs", "c.md"])).id();
app.update();
type_text(&mut app, "rs");
press(&mut app, "down");
let highlighted = |app: &App| app.world().get::<Highlighted>(picker).copied();
assert_eq!(highlighted(&app), Some(Highlighted(1)));
let more = batch(&["a.rs", "aa.rs", "b.rs", "c.md", "d.rs"]);
app.world_mut().entity_mut(picker).insert(more);
app.update();
let state = app.world().get::<PickerInput>(picker);
assert_eq!(state.map(PickerInput::filter), Some("rs"));
assert_eq!(highlighted(&app), Some(Highlighted(2)));
press(&mut app, "enter");
assert_eq!(
app.world().resource::<Reported>().0,
[Some(Chosen::item(2))]
);
let picker = app
.world_mut()
.spawn(Picker::new("Gone").item("x", ""))
.id();
app.update();
app.world_mut().despawn(picker);
app.update();
assert_eq!(shown(&mut app), None);
type_text(&mut app, "hi");
assert_eq!(crate::tests::input(&app).text(), "hi");
}
#[derive(Resource, Default)]
struct Answered(Vec<(String, Option<Answer>)>);
fn record_answers(
answered: On<Insert<Outcome>>,
asks: Query<(&Ask, &Outcome)>,
mut log: ResMut<Answered>,
) {
if let Ok((ask, outcome)) = asks.get(answered.entity) {
log.0.push((ask.title.clone(), Answer::of(outcome)));
}
}
#[test]
fn the_view_answers_the_questions_of_every_agent() {
let (mut app, agent) = view_app();
app.init_resource::<Answered>().add_observer(record_answers);
let can_answer = app
.world_mut()
.run_system_cached_with(
|In(agent): In<Entity>, answerers: Answerers| answerers.can_answer(Some(agent)),
agent,
)
.unwrap_or(false);
assert!(can_answer);
let mut ask = |question: Ask| {
app.world_mut().spawn((question, ForAgent(agent)));
};
ask(Ask::yes_no("Run it?"));
ask(Ask::yes_no("Why not?").free_text());
ask(Ask::yes_no("Really?"));
app.update();
assert_eq!(shown(&mut app).as_deref(), Some("Run it?"));
press(&mut app, "down enter");
type_text(&mut app, "too slow");
press(&mut app, "enter esc");
let answered = |title: &str, answer: Option<Answer>| (title.to_owned(), answer);
assert_eq!(
app.world().resource::<Answered>().0,
[
answered("Run it?", Some(Answer::choice("no"))),
answered("Why not?", Some(Answer::text("too slow"))),
answered("Really?", None),
]
);
let elsewhere = app
.world_mut()
.spawn((Ask::yes_no("Again?"), ForAgent(agent)))
.id();
app.update();
assert_eq!(shown(&mut app).as_deref(), Some("Again?"));
app.world_mut()
.entity_mut(elsewhere)
.insert(Answer::choice("yes").outcome());
app.update();
assert_eq!(shown(&mut app), None);
}
#[test]
fn a_ticked_choice_goes_with_the_note_typed() {
let (mut app, agent) = view_app();
app.init_resource::<Answered>().add_observer(record_answers);
let ask = Ask::new("Allow `rm`?")
.choice("deny", "Deny")
.choice("allow", "Allow")
.free_text();
app.world_mut().spawn((ask, ForAgent(agent)));
app.update();
press(&mut app, "tab tab");
type_text(&mut app, "just once");
press(&mut app, "enter");
let noted = Answer::choice("allow").with_text("just once");
assert_eq!(
app.world().resource::<Answered>().0,
[("Allow `rm`?".to_owned(), Some(noted))]
);
}
#[test]
fn a_long_detail_leaves_the_label_half_the_row() {
use unicode_width::UnicodeWidthStr;
let item = Item {
id: "",
label: "rig_basics::subagents::SubagentsPlugin",
detail: Some(
"(rig-basics) Subagents: the task, message and wait tools, with which the agent starts other agents",
),
checked: false,
more: None,
};
let row = item.line(60).to_string();
assert_eq!(row.width(), 60, "{row}");
assert!(
row.starts_with("rig_basics::subagents::SubagentsPlugin "),
"{row}"
);
assert!(row.ends_with('…'), "{row}");
}
#[test]
fn a_long_label_is_shortened_beside_its_whole_detail() {
use unicode_width::UnicodeWidthStr;
let item = Item {
id: "",
label: "fix the flaky picker test in the terminal view · ~/code/rigs",
detail: Some("$0.42 · 3h ago"),
checked: false,
more: None,
};
let row = item.line(40).to_string();
assert_eq!(row.width(), 40, "{row}");
assert!(row.ends_with(" $0.42 · 3h ago"), "{row}");
assert!(row.starts_with("fix the flaky picker te… "), "{row}");
let short = Item {
id: "",
label: "main",
detail: None,
checked: false,
more: None,
};
assert_eq!(short.line(40).to_string(), "main");
}
#[test]
fn a_question_with_initial_text_is_answered_in_the_input_and_the_draft_comes_back() {
let (mut app, agent) = view_app();
app.init_resource::<Answered>().add_observer(record_answers);
type_text(&mut app, "half a thought");
let title = Ask::new("PR title").initial("Fix the parser");
app.world_mut().spawn((title, ForAgent(agent)));
let body = Ask::new("PR body").initial("It loops.").multiline();
app.world_mut().spawn((body, ForAgent(agent)));
app.update();
assert_eq!(crate::tests::typed(&app), "Fix the parser");
press(
&mut app,
"backspace backspace backspace backspace backspace backspace",
);
type_text(&mut app, "lexer");
press(&mut app, "enter");
assert_eq!(crate::tests::typed(&app), "It loops.");
press(&mut app, "shift+enter");
type_text(&mut app, "Fixed.");
press(&mut app, "esc");
assert_eq!(crate::tests::typed(&app), "half a thought");
let answered = |title: &str, answer: Option<Answer>| (title.to_owned(), answer);
assert_eq!(
app.world().resource::<Answered>().0,
[
answered("PR title", Some(Answer::text("Fix the lexer"))),
answered("PR body", None),
]
);
assert!(
app.world()
.resource::<crate::tests::Delivered>()
.0
.is_empty()
);
}
#[test]
fn a_question_beside_the_work_leaves_the_keys_to_the_input() {
let (mut app, agent) = view_app();
app.init_resource::<Answered>().add_observer(record_answers);
let offer = Ask::new("Save rule “tabs”?")
.choice("save", "Save")
.choice("no", "Dismiss")
.free_text()
.inline();
app.world_mut().spawn((offer.clone(), ForAgent(agent)));
app.update();
assert_eq!(shown(&mut app), None);
type_text(&mut app, "go on");
assert_eq!(crate::tests::typed(&app), "go on");
press(&mut app, "alt+1");
let answered = |answer: &str| {
(
"Save rule “tabs”?".to_owned(),
Some(Answer::choice(answer.to_owned())),
)
};
assert_eq!(app.world().resource::<Answered>().0, [answered("save")]);
press(&mut app, "alt+1");
assert_eq!(app.world().resource::<Answered>().0.len(), 1);
app.world_mut().spawn((offer, ForAgent(agent)));
app.update();
press(&mut app, "alt+3");
assert_eq!(crate::tests::typed(&app), "");
type_text(&mut app, "only in Go");
press(&mut app, "enter");
assert_eq!(crate::tests::typed(&app), "go on");
let last = app.world().resource::<Answered>().0.last().cloned();
let text = Some(Answer::text("only in Go"));
assert_eq!(last, Some(("Save rule “tabs”?".to_owned(), text)));
}
#[test]
fn the_item_highlighted_is_known_by_its_place_among_all_the_items() {
use crate::InputContext;
let (mut app, _) = view_app();
let picker = Picker::new("Branch")
.item("main", "")
.item("jam", "")
.item("jet", "");
app.world_mut().spawn(picker);
app.update();
app.update();
let selection = |app: &App| app.world().resource::<InputContext>().selection();
assert_eq!(selection(&app), Some(0));
press(&mut app, "down");
assert_eq!(selection(&app), Some(1));
type_text(&mut app, "je");
assert_eq!(selection(&app), Some(2));
}
#[test]
fn a_questions_own_keys_tick_and_enter_answers_exactly_the_ticked() {
let (mut app, agent) = view_app();
app.init_resource::<Answered>().add_observer(record_answers);
let keys = ChoiceKeys(vec![
(KeyCode::Char(' '), ChoiceAction::Tick),
(KeyCode::Char('a'), ChoiceAction::TickAll),
(KeyCode::Char('n'), ChoiceAction::TickNone),
]);
let ask = || {
Ask::new("Approve these calls?")
.multiple()
.free_text()
.with(Choice::new("0", "read a.rs").checked())
.with(
Choice::new("1", "write b.rs")
.checked()
.more("{\"path\":\"b.rs\"}"),
)
};
app.world_mut()
.spawn((ask(), keys.clone(), ForAgent(agent)));
app.update();
press(&mut app, "space enter");
app.world_mut()
.spawn((ask(), keys.clone(), ForAgent(agent)));
app.update();
press(&mut app, "n enter");
app.world_mut().spawn((ask(), keys, ForAgent(agent)));
app.update();
press(&mut app, "down down");
type_text(&mut app, "a");
press(&mut app, "down enter");
let answered = |answer: Answer| ("Approve these calls?".to_owned(), Some(answer));
let none: [&str; 0] = [];
assert_eq!(
app.world().resource::<Answered>().0,
[
answered(Answer::choice("1")),
answered(Answer::choices(none)),
answered(Answer::choices(["0", "1"]).with_text("a")),
]
);
}
#[test]
fn a_superseded_question_keeps_the_users_ticks() {
use rig_core::effect::Disposition;
use rig_ecs::stage::{EffectRecorded, SupersededBy};
#[derive(Resource, Default)]
struct Dispositions(Vec<Disposition>);
let (mut app, agent) = view_app();
app.init_resource::<Answered>()
.init_resource::<Dispositions>()
.add_observer(record_answers)
.add_systems(
Last,
|mut recorded: MessageReader<EffectRecorded>, mut seen: ResMut<Dispositions>| {
let records = recorded.read().map(|record| record.0.disposition);
seen.0.extend(records);
},
);
let ask = Ask::new("Approve?")
.multiple()
.with(Choice::new("0", "read a.rs").checked())
.with(Choice::new("1", "read b.rs").checked());
let old = app.world_mut().spawn((ask.clone(), ForAgent(agent))).id();
app.update();
press(&mut app, "tab");
let joined = ask.with(Choice::new("2", "write c.rs").checked());
let new = app.world_mut().spawn((joined, ForAgent(agent))).id();
app.world_mut().entity_mut(old).insert(SupersededBy(new));
app.update();
let world = app.world_mut();
let shown = world
.query_filtered::<Entity, With<PickerInput>>()
.single(world);
assert_eq!(shown.ok(), Some(new));
press(&mut app, "enter");
assert_eq!(
app.world().resource::<Answered>().0,
[("Approve?".to_owned(), Some(Answer::choices(["1", "2"])))]
);
let seen = &app.world().resource::<Dispositions>().0;
assert!(seen.contains(&Disposition::Superseded), "{seen:?}");
}
#[test]
fn a_pickers_preview_is_drawn_beside_its_list() {
use rig_core::completion::Message;
use rig_harness::pick::PickPreview;
use crate::TuiTestScreen;
let (mut app, _) = view_app();
app.insert_resource(TuiTestScreen::new(100, 30));
let picker = Picker::new("Sessions").item("fix the parser", "resume a");
let mut conversation = rig_core::conversation::Conversation::default();
conversation.push(Message::user("why does the parser drop tabs?"));
let preview = PickPreview::Conversation {
conversation,
at: None,
};
let picker = app.world_mut().spawn((picker, preview)).id();
app.update();
app.update();
let screen = |app: &App| app.world().resource::<TuiTestScreen>().rows().join("\n");
let drawn = screen(&app);
let row = drawn
.lines()
.find(|row| row.contains("why does the parser drop tabs?"));
let column = row.and_then(|row| row.chars().position(|c| c == '›'));
assert!(column.is_some_and(|column| column >= 50), "{drawn}");
assert!(drawn.contains("fix the parser"), "{drawn}");
app.world_mut()
.entity_mut(picker)
.insert(PickPreview::Text("a saved session".into()));
app.update();
app.update();
let drawn = screen(&app);
assert!(drawn.contains("a saved session"), "{drawn}");
assert!(!drawn.contains("why does the parser"), "{drawn}");
}