use serde_json::json;
use super::tests::{Place, harness, read_card, say};
use super::*;
use crate::provider::fake::{FakeServer, calls, says, sse};
fn save_conversation(place: &Place, text: &str) {
let file = place.store().file().to_path_buf();
std::fs::create_dir_all(file.parent().expect("a parent")).expect("the conversations folder");
std::fs::write(file, text).expect("write the conversation");
}
fn beside(place: &Place) -> Vec<std::path::PathBuf> {
let file = place.store().file().to_path_buf();
let folder = file.parent().expect("a parent");
std::fs::read_dir(folder)
.expect("the conversations folder")
.map(|entry| entry.expect("an entry").path())
.filter(|path| path != &file)
.collect()
}
fn result_of(app: &QCli, id: &str) -> (String, bool) {
app.conversation()
.messages()
.iter()
.flat_map(conversation::blocks)
.find(|block| block["type"] == "tool_result" && block["tool_use_id"] == id)
.map(|block| (block["content"].as_str().unwrap_or_default().to_owned(), block["is_error"] == true))
.unwrap_or_else(|| panic!("no result for the call {id}"))
}
#[test]
fn a_conversation_file_that_cannot_be_read_is_kept_and_the_new_thread_saved_in_its_place() {
let place = Place::new("hostile-unreadable-thread");
let theirs = b"{\"messages\": [{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"yesterday\"}]}";
save_conversation(&place, &String::from_utf8_lossy(theirs));
let server = FakeServer::start(vec![(200, says("Here is the answer."))]);
let mut harness = harness(place.app(Some(&server.address)));
assert!(harness.app().conversation().is_empty(), "nothing of it can be shown");
assert!(harness.screen().contains("Ask anything about this folder"), "{}", harness.screen());
say(&mut harness, "hello again");
assert_eq!(harness.app().phase(), &Phase::Idle);
assert!(harness.screen().contains("Here is the answer."), "{}", harness.screen());
let kept = beside(&place);
assert_eq!(kept.len(), 1, "the file that could not be read is kept, not written over: {kept:?}");
assert_eq!(std::fs::read(&kept[0]).expect("read"), theirs, "the very same bytes are still there");
let saved = std::fs::read_to_string(place.store().file()).expect("the conversation is saved");
assert!(saved.contains("hello again") && saved.contains("Here is the answer."), "{saved}");
}
#[test]
fn a_conversation_of_a_hundred_thousand_characters_in_one_line_is_drawn_and_the_person_can_write() {
let place = Place::new("hostile-long-line");
let long = "z".repeat(100_000);
save_conversation(
&place,
&json!({"version": 1, "messages": [{"role": "user", "content": [{"type": "text", "text": long}]}]}).to_string(),
);
let server = FakeServer::start(vec![(200, says("Short enough."))]);
let mut harness = harness(place.app(Some(&server.address)));
assert!(
harness.screen().contains("zzzz"),
"the line is on screen, as much of it as fits: {}",
&harness.screen()[..200.min(harness.screen().len())]
);
assert!(harness.screen().contains("Write a message"), "the writing field is there: {}", harness.screen());
say(&mut harness, "and now?");
assert!(harness.screen().contains("Short enough."), "{}", harness.screen());
assert_eq!(harness.app().phase(), &Phase::Idle);
}
#[test]
fn a_diff_of_ten_thousand_lines_on_the_card_is_shown_and_the_person_can_still_say_no() {
let place = Place::new("hostile-big-diff");
let body: String = (1..=10_000).map(|number| format!("line {number}\n")).collect();
let file = place.work().join("long.txt");
std::fs::write(&file, &body).expect("write");
let server = FakeServer::start(vec![
(200, calls("d1", "edit_file", &json!({"path": "long.txt", "old_text": body, "new_text": "one line\n"}))),
(200, says("Left it alone.")),
]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "shorten the file");
assert!(matches!(harness.app().phase(), Phase::Asking { .. }), "the card waits for an answer");
assert!(harness.screen().contains("+ one line"), "the change is in view: {}", harness.screen());
assert!(harness.screen().contains("Allow") && harness.screen().contains("Decline"), "{}", harness.screen());
read_card(&mut harness);
harness.click_text("Decline");
assert_eq!(std::fs::read_to_string(&file).expect("read"), body, "the file is as it was");
assert!(harness.screen().contains("Left it alone."), "{}", harness.screen());
}
#[test]
fn a_path_of_five_thousand_characters_is_reported_whole_and_the_person_can_write_again() {
let place = Place::new("hostile-long-path");
let path = "p".repeat(5000);
let server = FakeServer::start(vec![
(200, calls("p1", "read_file", &json!({"path": path}))),
(200, says("That is not a file I can read.")),
]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "read a very long name");
let (result, failed) = result_of(harness.app(), "p1");
assert!(failed, "a path no file system will take is not read: {result}");
assert!(result.contains(&path), "the path it was given is named whole: {}…", &result[..80]);
assert!(harness.screen().contains("That is not a file I can read."), "{}", harness.screen());
assert_eq!(harness.app().phase(), &Phase::Idle);
}
#[test]
fn a_reply_of_five_hundred_calls_is_all_taken_and_every_call_is_answered() {
let place = Place::new("hostile-many-calls");
std::fs::write(place.work().join("notes.txt"), "the word is here\n").expect("write");
let events: Vec<_> = (0..500)
.flat_map(|number| {
[
json!({"type": "content_block_start", "index": number, "content_block": {"type": "tool_use", "id": format!("c{number}"), "name": "read_file", "input": {}}}),
json!({"type": "content_block_delta", "index": number, "delta": {"type": "input_json_delta", "partial_json": "{\"path\": \"notes.txt\"}"}}),
json!({"type": "content_block_stop", "index": number}),
]
})
.chain([json!({"type": "message_delta", "delta": {"stop_reason": "tool_use"}}), json!({"type": "message_stop"})])
.collect();
let server = FakeServer::start(vec![(200, sse(&events)), (200, says("All five hundred read."))]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "read it five hundred times");
assert!(harness.screen().contains("All five hundred read."), "{}", harness.screen());
let results: Vec<_> = harness
.app()
.conversation()
.messages()
.iter()
.flat_map(conversation::blocks)
.filter(|block| block["type"] == "tool_result")
.collect();
assert_eq!(results.len(), 500, "every call of the reply has a result");
assert_eq!(results[499]["tool_use_id"], "c499");
assert_eq!(results[0]["content"].as_str().map(|text| text.contains("the word is here")), Some(true));
}
#[test]
fn a_call_with_no_name_and_a_stop_reason_of_nothing_known_end_the_turn_with_a_sentence() {
let place = Place::new("hostile-odd-reply");
let events = vec![
json!({"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "x"}}),
json!({"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"path\": \"notes.txt\"}"}}),
json!({"type": "content_block_stop", "index": 0}),
json!({"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}),
json!({"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "I looked."}}),
json!({"type": "content_block_stop", "index": 1}),
json!({"type": "message_delta", "delta": {"stop_reason": "something_new"}}),
json!({"type": "message_stop"}),
];
let server = FakeServer::start(vec![(200, sse(&events)), (200, says("Fine."))]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "look around");
let (sentence, failed) = result_of(harness.app(), "x");
assert!(failed, "a call that names no tool is not done");
assert!(sentence.contains("no tool named"), "the model is told why: {sentence}");
assert!(harness.screen().contains("I looked."), "the rest of the reply is still shown: {}", harness.screen());
assert_eq!(harness.app().phase(), &Phase::Idle, "a stop reason of nothing known ends the turn");
say(&mut harness, "go on");
assert!(harness.screen().contains("Fine."), "{}", harness.screen());
assert_eq!(server.received().len(), 2, "the next request went out and was answered");
}
#[test]
fn a_text_delta_of_a_megabyte_arrives_whole_and_the_thread_goes_on() {
let place = Place::new("hostile-megabyte-reply");
let long = "w ".repeat(512 * 1024);
let answer = sse(&[
json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": long}}),
json!({"type": "content_block_stop", "index": 0}),
json!({"type": "message_delta", "delta": {"stop_reason": "end_turn"}}),
json!({"type": "message_stop"}),
]);
let server = FakeServer::start(vec![(200, answer), (200, says("And a little more."))]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "write a lot");
assert_eq!(harness.app().phase(), &Phase::Idle);
let said = harness.app().conversation().messages()[1]["content"][0]["text"].as_str().expect("the answer");
assert_eq!(said.len(), long.len(), "every character of the delta is kept");
assert!(harness.screen().contains("Write a message"), "the writing field is under it: {}", harness.screen());
say(&mut harness, "and a little?");
assert!(harness.screen().contains("And a little more."), "{}", harness.screen());
}
#[test]
fn a_reply_cut_in_the_middle_of_a_line_says_so_and_the_person_can_write_again() {
let place = Place::new("hostile-cut-reply");
let whole = sse(&[
json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Half an ans"}}),
]);
let server = FakeServer::start(vec![(200, format!("{whole}data: {{\"type\": \"mess")), (200, says("Back again."))]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "answer me");
assert!(
matches!(harness.app().notice(), Some(Notice::Provider(said)) if said.contains("ended before the message was complete")),
"{:?}",
harness.app().notice()
);
assert!(harness.screen().contains("The provider did not answer"), "{}", harness.screen());
assert_eq!(harness.app().phase(), &Phase::Idle);
say(&mut harness, "try again");
assert!(harness.screen().contains("Back again."), "{}", harness.screen());
}
#[test]
fn a_settings_file_that_is_a_folder_opens_the_form_and_writes_nothing() {
let place = Place::new("hostile-settings-folder");
let conf = place.base().join(format!("config/{}", config::APP));
std::fs::create_dir_all(&conf).expect("the settings file is a folder");
let mut harness = harness(place.app(None));
assert!(harness.screen().contains("Provider"), "{}", harness.screen());
assert!(harness.is_focused(form::FIRST));
harness.click_text("Cancel");
assert!(!harness.screen().contains("Provider"), "the form closes and the chat is there: {}", harness.screen());
assert!(conf.is_dir(), "the folder that stands where the settings file was is still a folder");
assert!(std::fs::read_dir(&conf).expect("read").next().is_none(), "and nothing was written into it");
}
#[test]
fn a_key_of_a_megabyte_in_one_line_is_refused_by_the_transport_and_the_person_is_told() {
let place = Place::new("hostile-megabyte-key");
let key = "k".repeat(1024 * 1024);
std::fs::write(place.base().join("key"), &key).expect("write the key file");
let server = FakeServer::start(vec![(200, says("Hello."))]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "hello");
assert!(matches!(harness.app().notice(), Some(Notice::Provider(_))), "{:?}", harness.app().notice());
assert!(harness.screen().contains("The provider did not answer"), "{}", harness.screen());
assert!(!harness.screen().contains("kkkk"), "and the key is nowhere on the screen");
assert_eq!(harness.app().phase(), &Phase::Idle);
assert!(server.received().is_empty(), "nothing reached the server, so nothing came back from it");
say(&mut harness, "are you there");
assert!(server.received().is_empty(), "the thread is still the person's to write on");
assert!(harness.screen().contains("The provider did not answer"), "{}", harness.screen());
}
#[test]
fn a_file_that_goes_between_the_card_and_the_allow_is_not_written() {
let place = Place::new("hostile-vanished");
let file = place.work().join("target.txt");
std::fs::write(&file, "before\n").expect("write");
let server = FakeServer::start(vec![
(200, calls("v1", "edit_file", &json!({"path": "target.txt", "old_text": "before", "new_text": "after"}))),
(200, says("Understood.")),
]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "change it");
assert!(matches!(harness.app().phase(), Phase::Asking { .. }), "the card is waiting");
std::fs::remove_file(&file).expect("the file is gone");
read_card(&mut harness);
harness.press("y");
assert!(!file.exists(), "an allowed edit does not make the file that went away");
let (sentence, failed) = result_of(harness.app(), "v1");
assert!(failed, "the model is told it was not done: {sentence}");
assert!(sentence.contains("does not exist"), "{sentence}");
assert!(harness.screen().contains("Understood."), "{}", harness.screen());
}
#[test]
fn a_terminal_of_one_column_and_a_resize_to_one_by_one_keep_the_chat_usable() {
let place = Place::new("hostile-tiny-terminal");
std::fs::write(place.work().join("target.txt"), "before\n").expect("write");
let server = FakeServer::start(vec![
(200, calls("t1", "edit_file", &json!({"path": "target.txt", "old_text": "before", "new_text": "after"}))),
(200, says("Done.")),
]);
let mut harness = harness(place.app(Some(&server.address)));
harness.resize(1, 40);
say(&mut harness, "change it");
assert!(matches!(harness.app().phase(), Phase::Asking { .. }), "the card waits in one column");
harness.resize(1, 1);
assert!(matches!(harness.app().phase(), Phase::Asking { .. }), "the card is still waiting");
harness.resize(100, 40);
read_card(&mut harness);
harness.press("y");
assert_eq!(std::fs::read_to_string(place.work().join("target.txt")).expect("read"), "after\n");
assert!(harness.screen().contains("Done."), "{}", harness.screen());
say(&mut harness, "and again");
assert_eq!(harness.app().phase(), &Phase::Idle);
}
#[test]
fn a_terminal_with_no_cells_at_all_is_not_a_crash() {
let place = Place::new("hostile-no-screen");
let mut harness = harness(place.app(None));
harness.resize(0, 0);
assert!(harness.screen().is_empty(), "no cells, no rows: {:?}", harness.screen());
harness.resize(1, 1);
assert_eq!(harness.screen().lines().count(), 1, "one cell, one row: {:?}", harness.screen());
harness.resize(100, 40);
assert!(harness.screen().contains("qcli"), "the chat is back: {}", harness.screen());
}
#[test]
fn a_working_folder_that_is_gone_while_the_app_is_open_answers_every_call_with_a_sentence() {
let place = Place::new("hostile-folder-gone");
std::fs::write(place.work().join("notes.txt"), "the word is here\n").expect("write");
let reply = sse(&[
json!({"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "g1", "name": "read_file", "input": {}}}),
json!({"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"path\": \"notes.txt\"}"}}),
json!({"type": "content_block_stop", "index": 0}),
json!({"type": "message_delta", "delta": {"stop_reason": "tool_use"}}),
json!({"type": "message_stop"}),
]);
let server = FakeServer::start(vec![(200, reply), (200, says("There is no folder to read."))]);
let mut harness = harness(place.app(Some(&server.address)));
let root = place.work();
std::fs::remove_dir_all(&root).expect("the working folder is gone");
say(&mut harness, "read the notes");
let (sentence, failed) = result_of(harness.app(), "g1");
assert!(failed, "nothing could be read out of a folder that is not there");
assert!(
sentence.contains("outside the working folder") || sentence.contains("does not exist"),
"the model is told the folder is not there: {sentence}"
);
assert!(harness.screen().contains("There is no folder to read."), "{}", harness.screen());
assert_eq!(harness.app().phase(), &Phase::Idle);
assert!(!root.exists(), "nothing was made where the folder was");
}
#[test]
fn a_file_changed_between_the_card_and_the_allow_is_not_written_over_what_was_shown() {
let place = Place::new("hostile-changed");
let file = place.work().join("target.txt");
std::fs::write(&file, "first\nhello world\nlast\n").expect("write");
let server = FakeServer::start(vec![
(200, calls("c1", "edit_file", &json!({"path": "target.txt", "old_text": "world", "new_text": "moon"}))),
(200, says("I will read it again.")),
]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "change it");
assert!(harness.screen().contains("last"), "the card shows the lines around the change: {}", harness.screen());
std::fs::write(&file, "first\nhello world\nsomebody else's line\n").expect("changed elsewhere");
read_card(&mut harness);
harness.press("y");
assert_eq!(
std::fs::read_to_string(&file).expect("read"),
"first\nhello world\nsomebody else's line\n",
"what the person allowed is not what would be written, so nothing is"
);
let (sentence, failed) = result_of(harness.app(), "c1");
assert!(failed && sentence.contains("changed after"), "the model is told why: {sentence}");
}