use super::*;
use crate::entities::message::{MessageFinish, MessageMetadata};
use crate::entities::profile::LlmChange;
use crate::shared::config::ServerMode;
fn reply(model: Option<&str>, mode: ServerMode, finish: MessageFinish) -> Message {
let mut msg = Message::assistant("ответ");
msg.metadata = Some(MessageMetadata {
sampling: SamplingConfig::default(),
mode,
model: model.map(Into::into),
finish: Some(finish),
});
msg
}
fn land_turn(orch: &mut Orchestrator, chat_id: Uuid, messages: Vec<Message>) {
let gen_id = Uuid::new_v4();
orch.gen_state
.begin(gen_id, tokio_util::sync::CancellationToken::new());
orch.handle_done(super::super::generation::GenResult {
id: gen_id,
chat_id,
messages,
effects: vec![],
deleted: vec![],
usage: None,
continuation: None,
images_withheld: 0,
});
}
fn orch_with_chat() -> (tempfile::TempDir, Orchestrator, Uuid, Uuid) {
let (dir, mut orch, _rx) = bare_orch_rx();
let profile = Profile::new("P", "sys");
let pid = profile.id;
let mut chat = Chat::from_profile(&profile, "t");
chat.push_message(Message::user("привет"));
let chat_id = chat.id;
orch.profiles.push(profile);
orch.chats.push(chat);
orch.active_id = Some(chat_id);
(dir, orch, pid, chat_id)
}
#[tokio::test]
async fn a_completed_exchange_writes_the_baseline_and_then_only_changes() {
let (_d, mut orch, pid, chat_id) = orch_with_chat();
land_turn(
&mut orch,
chat_id,
vec![reply(
Some("gemma-4"),
ServerMode::Managed,
MessageFinish::Stop,
)],
);
land_turn(
&mut orch,
chat_id,
vec![reply(
Some("gemma-4"),
ServerMode::Managed,
MessageFinish::Stop,
)],
);
land_turn(
&mut orch,
chat_id,
vec![reply(
Some("qwen-3.6"),
ServerMode::Managed,
MessageFinish::Stop,
)],
);
land_turn(
&mut orch,
chat_id,
vec![reply(
Some("qwen-3.6"),
ServerMode::External,
MessageFinish::Stop,
)],
);
let history = orch.storage.db().llm_history(pid).unwrap();
assert_eq!(
history
.iter()
.map(|r| (r.model.as_str(), r.mode))
.collect::<Vec<_>>(),
[
("gemma-4", ServerMode::Managed),
("qwen-3.6", ServerMode::Managed),
("qwen-3.6", ServerMode::External),
]
);
}
#[tokio::test]
async fn an_engine_that_did_not_say_a_name_records_nothing() {
let (_d, mut orch, pid, chat_id) = orch_with_chat();
land_turn(
&mut orch,
chat_id,
vec![reply(None, ServerMode::External, MessageFinish::Stop)],
);
assert!(orch.storage.db().llm_history(pid).unwrap().is_empty());
}
#[tokio::test]
async fn a_cancelled_partial_reply_still_counts_as_the_models_work() {
let (_d, mut orch, pid, chat_id) = orch_with_chat();
land_turn(
&mut orch,
chat_id,
vec![reply(
Some("gemma-4"),
ServerMode::Managed,
MessageFinish::Cancelled,
)],
);
assert_eq!(orch.storage.db().llm_history(pid).unwrap().len(), 1);
}
#[tokio::test]
async fn the_newest_metadata_of_the_turn_names_the_record() {
let (_d, mut orch, pid, chat_id) = orch_with_chat();
let mut bare = Message::assistant("");
bare.metadata = None;
land_turn(
&mut orch,
chat_id,
vec![
reply(Some("gemma-4"), ServerMode::Managed, MessageFinish::Stop),
bare,
],
);
let history = orch.storage.db().llm_history(pid).unwrap();
assert_eq!(history.len(), 1);
assert_eq!(history[0].model, "gemma-4");
}
#[tokio::test]
async fn the_record_lands_under_the_chats_own_profile() {
let (_d, mut orch, pid_a, chat_a) = orch_with_chat();
let profile_b = Profile::new("B", "sys");
let pid_b = profile_b.id;
let mut chat = Chat::from_profile(&profile_b, "t2");
chat.push_message(Message::user("привет"));
let chat_b = chat.id;
orch.profiles.push(profile_b);
orch.chats.push(chat);
land_turn(
&mut orch,
chat_a,
vec![reply(
Some("gemma-4"),
ServerMode::Managed,
MessageFinish::Stop,
)],
);
orch.active_id = Some(chat_b);
land_turn(
&mut orch,
chat_b,
vec![reply(
Some("gpt-5.2"),
ServerMode::OpenAi,
MessageFinish::Stop,
)],
);
let a = orch.storage.db().llm_history(pid_a).unwrap();
let b = orch.storage.db().llm_history(pid_b).unwrap();
assert_eq!(
a.iter().map(|r| r.model.as_str()).collect::<Vec<_>>(),
["gemma-4"]
);
assert_eq!(
b.iter().map(|r| r.model.as_str()).collect::<Vec<_>>(),
["gpt-5.2"]
);
}
#[tokio::test]
async fn a_turn_for_a_vanished_chat_records_nothing_and_does_not_panic() {
let (_d, mut orch, pid, _chat_id) = orch_with_chat();
land_turn(
&mut orch,
Uuid::new_v4(),
vec![reply(
Some("gemma-4"),
ServerMode::Managed,
MessageFinish::Stop,
)],
);
assert!(orch.storage.db().llm_history(pid).unwrap().is_empty());
}
#[test]
fn llm_change_compares_by_name_and_mode() {
let at = chrono::Utc::now();
let base = LlmChange {
changed_at: at,
model: "m".into(),
mode: ServerMode::Managed,
};
assert_eq!(base, base.clone());
assert_ne!(
base,
LlmChange {
model: "other".into(),
..base.clone()
}
);
assert_ne!(
base,
LlmChange {
mode: ServerMode::External,
..base.clone()
}
);
}
fn dated_reply(ts: &str, model: &str, mode: ServerMode) -> Message {
let mut msg = reply(Some(model), mode, MessageFinish::Stop);
msg.timestamp = ts.parse().unwrap();
msg
}
fn stored_history(orch: &Orchestrator, pid: Uuid) -> Vec<(String, String)> {
orch.storage
.db()
.llm_history(pid)
.unwrap()
.into_iter()
.map(|r| (r.changed_at.format("%Y-%m-%d").to_string(), r.model))
.collect()
}
#[tokio::test]
async fn bootstrap_seeds_an_empty_history_from_what_the_chats_already_record() {
let (_d, mut orch, _rx) = bare_orch_rx();
let profile = Profile::new("P", "sys");
let pid = profile.id;
orch.storage.json().upsert_profile(&profile).unwrap();
let mut chat = Chat::from_profile(&profile, "t");
chat.push_message(Message::user("привет"));
chat.push_message(dated_reply(
"2026-01-01T10:00:00Z",
"gemma-4",
ServerMode::Managed,
));
chat.push_message(dated_reply(
"2026-02-01T10:00:00Z",
"qwen-3.6",
ServerMode::External,
));
let chat_id = chat.id;
orch.storage.json().save_chat(&chat).unwrap();
let mut hidden = Chat::from_profile(&profile, "удалённый");
hidden.is_hidden = true;
hidden.push_message(dated_reply(
"2026-01-15T10:00:00Z",
"hidden-chat-model",
ServerMode::Grok,
));
orch.storage.json().save_chat(&hidden).unwrap();
orch.bootstrap().unwrap();
assert_eq!(
stored_history(&orch, pid),
[
("2026-01-01".to_string(), "gemma-4".to_string()),
("2026-02-01".to_string(), "qwen-3.6".to_string()),
]
);
orch.bootstrap().unwrap();
assert_eq!(stored_history(&orch, pid).len(), 2);
land_turn(
&mut orch,
chat_id,
vec![reply(
Some("qwen-3.6"),
ServerMode::External,
MessageFinish::Stop,
)],
);
assert_eq!(stored_history(&orch, pid).len(), 2);
land_turn(
&mut orch,
chat_id,
vec![reply(
Some("gpt-5.2"),
ServerMode::OpenAi,
MessageFinish::Stop,
)],
);
assert_eq!(
stored_history(&orch, pid)
.iter()
.map(|(_, model)| model.as_str())
.collect::<Vec<_>>(),
["gemma-4", "qwen-3.6", "gpt-5.2"]
);
}
#[test]
fn bootstrap_leaves_a_history_that_has_records_alone() {
let (_d, mut orch, _rx) = bare_orch_rx();
let profile = Profile::new("P", "sys");
let pid = profile.id;
orch.storage.json().upsert_profile(&profile).unwrap();
let mut chat = Chat::from_profile(&profile, "t");
chat.push_message(dated_reply(
"2020-01-01T10:00:00Z",
"ancient",
ServerMode::Managed,
));
orch.storage.json().save_chat(&chat).unwrap();
orch.storage
.db()
.llm_history_note(
pid,
&LlmChange {
changed_at: "2026-08-30T10:00:00Z".parse().unwrap(),
model: "gemma-4".into(),
mode: ServerMode::Managed,
},
)
.unwrap();
orch.bootstrap().unwrap();
assert_eq!(
stored_history(&orch, pid),
[("2026-08-30".to_string(), "gemma-4".to_string())]
);
}
#[test]
fn bootstrap_seeds_nothing_from_chats_whose_replies_name_no_model() {
let (_d, mut orch, _rx) = bare_orch_rx();
let profile = Profile::new("P", "sys");
let pid = profile.id;
orch.storage.json().upsert_profile(&profile).unwrap();
let mut chat = Chat::from_profile(&profile, "t");
chat.push_message(Message::user("привет"));
chat.push_message(Message::assistant("ответ"));
orch.storage.json().save_chat(&chat).unwrap();
orch.bootstrap().unwrap();
assert!(orch.storage.db().llm_history(pid).unwrap().is_empty());
}