use serde_json::{Value, json};
use super::tests::{Place, harness, read_card, say};
use super::*;
use crate::provider::fake::{FakeServer, Usage, calls, calls_using, says, says_using, sse};
fn assert_accepted(body: &Value) {
let messages = body["messages"].as_array().expect("the messages");
for (n, message) in messages.iter().enumerate() {
let calls: Vec<&Value> = conversation::blocks(message).filter(|block| block["type"] == "tool_use").collect();
if calls.is_empty() {
continue;
}
let next = messages.get(n + 1).unwrap_or_else(|| panic!("message {n} has calls and nothing answers them"));
assert_eq!(next["role"], "user", "the message after the calls of message {n} is the person's");
let answered: Vec<&str> = conversation::blocks(next)
.take_while(|block| block["type"] == "tool_result")
.filter_map(|block| block["tool_use_id"].as_str())
.collect();
for call in calls {
assert!(call["input"].is_object(), "the input of a call is an object: {call}");
let id = call["id"].as_str().expect("an id");
assert!(answered.contains(&id), "the call {id} of message {n} is answered first thing: {next}");
}
}
}
fn result_in(body: &Value, id: &str) -> (String, bool) {
body["messages"]
.as_array()
.into_iter()
.flatten()
.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 {id} in {body}"))
}
fn cut_call(id: &str, partial: &str, reason: &str) -> String {
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": "Reading it."}}),
json!({"type": "content_block_stop", "index": 0}),
json!({"type": "content_block_start", "index": 1, "content_block": {"type": "tool_use", "id": id, "name": "read_file", "input": {}}}),
json!({"type": "content_block_delta", "index": 1, "delta": {"type": "input_json_delta", "partial_json": partial}}),
json!({"type": "message_delta", "delta": {"stop_reason": reason}}),
json!({"type": "message_stop"}),
])
}
#[test]
fn a_reply_cut_by_its_length_in_the_middle_of_a_call_leaves_a_thread_that_goes_on() {
let place = Place::new("turn-cut-call");
let server =
FakeServer::start(vec![(200, cut_call("half", "{\"path\": \"src/ma", "max_tokens")), (200, says("Going on."))]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "read main");
assert_eq!(harness.app().notice(), Some(&Notice::Cut));
assert!(harness.screen().contains("reached its length limit"), "the person is told: {}", harness.screen());
say(&mut harness, "go on");
assert_eq!(server.received().len(), 2, "the next request went out");
let body = &server.received()[1].body;
assert_accepted(body);
let (_, failed) = result_in(body, "half");
assert!(failed, "a call cut in half is not done");
assert!(harness.screen().contains("Going on."), "and it was answered: {}", harness.screen());
}
#[test]
fn a_reply_that_ends_its_turn_with_a_call_in_it_tells_the_model_the_call_was_not_taken() {
let place = Place::new("turn-end-with-call");
let server = FakeServer::start(vec![
(200, cut_call("whole", "{\"path\": \"notes.txt\"}", "end_turn")),
(200, says("Fine.")),
]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "look");
assert_eq!(harness.app().phase(), &Phase::Idle, "the turn is over");
say(&mut harness, "and?");
let body = &server.received()[1].body;
assert_accepted(body);
assert_eq!(result_in(body, "whole"), (TURN_ENDED.to_owned(), true), "the model hears the turn ended first");
}
#[test]
fn a_person_who_stops_the_turn_is_named_as_the_one_who_stopped_it() {
let place = Place::new("turn-person-stopped");
let server = FakeServer::start(vec![
(200, calls("r1", "run_command", &json!({"command": "printf a"}))),
(200, says("Fine.")),
]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "run it");
harness.press("esc");
say(&mut harness, "never mind");
let body = &server.received()[1].body;
assert_accepted(body);
assert_eq!(result_in(body, "r1"), (PERSON_STOPPED.to_owned(), true), "the model hears the person stopped it");
}
#[test]
fn a_call_whose_input_is_not_json_goes_back_as_an_object_that_still_holds_what_was_sent() {
let place = Place::new("turn-bad-input");
let server = FakeServer::start(vec![
(200, cut_call("odd", "{\"path\": notes.txt}", "tool_use")),
(200, says("Let me fix that.")),
]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "read the notes");
assert_eq!(server.received().len(), 2, "the call is answered and the turn goes on");
let body = &server.received()[1].body;
assert_accepted(body);
let call = body["messages"]
.as_array()
.into_iter()
.flatten()
.flat_map(conversation::blocks)
.find(|block| block["type"] == "tool_use")
.expect("the call");
assert!(call["input"].to_string().contains("notes.txt"), "the model still sees what it sent: {}", call["input"]);
assert!(result_in(body, "odd").1, "and hears it could not be done");
}
#[test]
fn a_thread_saved_broken_is_mended_when_the_folder_is_opened() {
let place = Place::new("turn-saved-broken");
let broken = json!({"version": 1, "messages": [
{"role": "user", "content": [{"type": "text", "text": "read it"}]},
{"role": "assistant", "content": [
{"type": "text", "text": "Reading."},
{"type": "tool_use", "id": "old", "name": "read_file", "input": "{\"path\": \"a"},
]},
{"role": "user", "content": [{"type": "text", "text": "hello?"}]},
]});
let file = place.store().file().to_path_buf();
std::fs::create_dir_all(file.parent().expect("a folder")).expect("the conversations folder");
std::fs::write(&file, broken.to_string()).expect("the saved thread");
let server = FakeServer::start(vec![(200, says("Here again."))]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "are you there?");
let body = &server.received()[0].body;
assert_accepted(body);
assert!(result_in(body, "old").1, "the old call is answered as not done");
assert!(harness.screen().contains("Here again."), "{}", harness.screen());
}
#[test]
fn a_turn_stops_after_its_last_allowed_request_and_says_so() {
let place = Place::new("turn-rounds");
std::fs::write(place.work().join("a.txt"), "a\n").expect("a file");
let answers: Vec<(u16, String)> =
(0..=MAX_ROUNDS).map(|n| (200, calls(&format!("c{n}"), "read_file", &json!({"path": "a.txt"})))).collect();
let server = FakeServer::start(answers);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "keep reading");
assert_eq!(server.received().len(), MAX_ROUNDS, "no request past the last one allowed");
assert_eq!(harness.app().notice(), Some(&Notice::Rounds));
assert!(
harness.screen().contains(&format!("stopped after {MAX_ROUNDS} requests")),
"the person is told: {}",
harness.screen()
);
assert_eq!(harness.app().phase(), &Phase::Idle, "and can write again");
}
#[test]
fn what_a_stopped_round_still_sends_back_is_not_taken() {
let place = Place::new("turn-stale");
let server = FakeServer::start(vec![
(200, calls("r1", "run_command", &json!({"command": "printf a"}))),
(200, says("Fine.")),
]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "run it");
read_card(&mut harness);
let stopped_round = harness.app().round;
harness.press("esc");
let kept = harness.app().conversation().clone();
let outcome = crate::actions::Outcome { text: "late".to_owned(), is_error: false };
harness.send(Msg::Done(stopped_round, "r1".to_owned(), outcome));
harness.send(Msg::Replied(
stopped_round,
Ok(crate::provider::Reply {
content: vec![json!({"type": "text", "text": "late reply"})],
stop: crate::provider::Stop::EndTurn,
input_tokens: None,
output_tokens: None,
}),
));
assert_eq!(harness.app().conversation(), &kept, "nothing of the stopped round reaches the thread");
assert!(!harness.screen().contains("late reply"));
assert_eq!(harness.app().phase(), &Phase::Idle);
}
#[test]
fn an_action_stopped_while_it_is_being_done_is_not_told_as_not_done() {
let place = Place::new("turn-stopped-while-doing");
let server = FakeServer::start(vec![(200, says("I will check."))]);
let mut app = place.app(Some(&server.address));
let call = conversation::Call {
id: "e1".to_owned(),
name: "edit_file".to_owned(),
input: json!({"path": "a.txt", "old_text": "", "new_text": "x"}),
};
app.conversation.say("write it");
app.conversation.reply(vec![json!({"type": "tool_use", "id": "e1", "name": "edit_file", "input": call.input})]);
let action = Action::from_call(&call.name, &call.input).expect("an edit");
app.phase = Phase::Acting { call, action };
let mut harness = harness(app);
harness.press("esc");
let (sentence, _) = result_in(&harness.app().conversation().to_json(), "e1");
assert_eq!(sentence, STOPPED_WHILE_DOING, "the model is told it may have been done");
}
#[test]
fn a_turn_says_how_many_tokens_its_replies_read_and_wrote() {
let place = Place::new("turn-tokens");
std::fs::write(place.work().join("notes.txt"), "the secret word is apricot\n").expect("a file to read");
let server = FakeServer::start(vec![
(200, calls_using("c1", "read_file", &json!({"path": "notes.txt"}), Usage::new(1000, 20))),
(200, says_using("The word is apricot.", Usage::new(1500, 30))),
]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "what is the word?");
assert_eq!(server.received().len(), 2, "reading asks nothing, so the turn was two requests");
let screen = harness.screen();
assert!(screen.contains("This turn: 2500 tokens read, 50 written"), "both replies counted: {screen}");
let file = place.store().file().to_path_buf();
let kept = std::fs::read_to_string(&file).expect("the conversation is saved");
assert!(!kept.contains("2500") && !kept.contains("This turn"), "the totals are not kept: {kept}");
}
#[test]
fn a_new_conversation_forgets_a_turns_totals_and_the_next_turn_counts_only_its_own() {
let place = Place::new("turn-tokens-fresh");
let server = FakeServer::start(vec![
(200, says_using("First answer.", Usage::new(1000, 20))),
(200, says_using("Second answer.", Usage::new(300, 5))),
]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "first question");
assert!(harness.screen().contains("This turn: 1000 tokens read, 20 written"), "{}", harness.screen());
harness.press("ctrl+n");
assert!(
!harness.screen().contains("This turn:"),
"a new turn's line is gone with the old one: {}",
harness.screen()
);
say(&mut harness, "second question");
let screen = harness.screen();
assert!(screen.contains("This turn: 300 tokens read, 5 written"), "only what this turn used: {screen}");
assert!(!screen.contains("1300"), "the first turn's numbers are not added in: {screen}");
}
#[test]
fn a_turn_whose_replies_count_nothing_says_no_totals() {
let place = Place::new("turn-tokens-silent");
let server = FakeServer::start(vec![(200, says_using("Nothing to count here.", Usage::SILENT))]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "hello");
let screen = harness.screen();
assert!(screen.contains("Nothing to count here."), "the answer is on the screen: {screen}");
assert!(!screen.contains("This turn:"), "and a turn that used no token says nothing of it: {screen}");
}
#[test]
fn the_next_message_counts_its_turn_from_nothing() {
let place = Place::new("turn-tokens-next");
let server = FakeServer::start(vec![
(200, says_using("First answer.", Usage::new(1000, 20))),
(200, says_using("Second answer.", Usage::new(300, 5))),
]);
let mut harness = harness(place.app(Some(&server.address)));
say(&mut harness, "first question");
say(&mut harness, "second question");
let screen = harness.screen();
assert!(screen.contains("This turn: 300 tokens read, 5 written"), "only this turn: {screen}");
assert!(!screen.contains("1300"), "the first turn is not added in: {screen}");
}