#![allow(clippy::unwrap_used, clippy::indexing_slicing)]
use entelix_core::ir::{
ContentPart, Message, ModelRequest, ModelResponse, ProviderEchoSnapshot, Role, StopReason,
Usage,
};
fn assistant_text(text: &str) -> ContentPart {
ContentPart::Text {
text: text.to_owned(),
cache_control: None,
provider_echoes: Vec::new(),
}
}
fn prior_response(content: Vec<ContentPart>, echoes: Vec<ProviderEchoSnapshot>) -> ModelResponse {
ModelResponse {
id: "resp-1".to_owned(),
model: "claude-opus-4-7".to_owned(),
stop_reason: StopReason::EndTurn,
content,
usage: Usage::default(),
rate_limit: None,
warnings: Vec::new(),
provider_echoes: echoes,
}
}
#[test]
fn appends_assistant_then_user_in_order() {
let req = ModelRequest {
model: "claude-opus-4-7".to_owned(),
messages: vec![Message::user("first user turn")],
..ModelRequest::default()
};
let prior = prior_response(vec![assistant_text("first model reply")], Vec::new());
let next = req.continue_turn(&prior, Message::user("second user turn"));
assert_eq!(next.messages.len(), 3, "user → assistant → user");
assert_eq!(next.messages[0].role, Role::User);
assert_eq!(next.messages[1].role, Role::Assistant);
assert_eq!(next.messages[2].role, Role::User);
match &next.messages[1].content[0] {
ContentPart::Text { text, .. } => assert_eq!(text, "first model reply"),
other => panic!("expected text in assistant turn, got {other:?}"),
}
match &next.messages[2].content[0] {
ContentPart::Text { text, .. } => assert_eq!(text, "second user turn"),
other => panic!("expected text in next user turn, got {other:?}"),
}
}
#[test]
fn chains_provider_echoes_from_response_to_request() {
let anthropic_sig =
ProviderEchoSnapshot::for_provider("anthropic-messages", "signature", "WaUjzkypQ2yIBQs=");
let req = ModelRequest::default();
let prior = prior_response(vec![assistant_text("ok")], vec![anthropic_sig.clone()]);
let next = req.continue_turn(&prior, Message::user("continue"));
assert_eq!(next.continued_from.len(), 1);
assert_eq!(next.continued_from[0], anthropic_sig);
}
#[test]
fn carries_multiple_provider_echoes_unchanged() {
let anthropic = ProviderEchoSnapshot::for_provider("anthropic-messages", "signature", "a");
let openai =
ProviderEchoSnapshot::for_provider("openai-responses", "previous_response_id", "resp_abc");
let prior = prior_response(
vec![assistant_text("multi-vendor")],
vec![anthropic.clone(), openai.clone()],
);
let next = ModelRequest::default().continue_turn(&prior, Message::user("more"));
assert_eq!(next.continued_from, vec![anthropic, openai]);
}
#[test]
fn replaces_existing_continued_from() {
let stale = ProviderEchoSnapshot::for_provider("anthropic-messages", "signature", "stale");
let fresh = ProviderEchoSnapshot::for_provider("anthropic-messages", "signature", "fresh");
let req = ModelRequest {
continued_from: vec![stale],
..ModelRequest::default()
};
let prior = prior_response(vec![assistant_text("turn")], vec![fresh.clone()]);
let next = req.continue_turn(&prior, Message::user("again"));
assert_eq!(next.continued_from, vec![fresh]);
}
#[test]
fn preserves_model_system_tools_and_sampling_knobs() {
use std::sync::Arc;
let req = ModelRequest {
model: "gpt-4o".to_owned(),
temperature: Some(0.7),
max_tokens: Some(1024),
top_p: Some(0.9),
stop_sequences: vec!["END".to_owned()],
tools: Arc::from(Vec::<entelix_core::ir::ToolSpec>::new()),
..ModelRequest::default()
};
let prior = prior_response(vec![assistant_text("ok")], Vec::new());
let next = req.continue_turn(&prior, Message::user("again"));
assert_eq!(next.model, "gpt-4o");
assert_eq!(next.temperature, Some(0.7));
assert_eq!(next.max_tokens, Some(1024));
assert_eq!(next.top_p, Some(0.9));
assert_eq!(next.stop_sequences, vec!["END".to_owned()]);
}
#[test]
fn empty_echoes_yields_empty_continued_from() {
let prior = prior_response(vec![assistant_text("trivial")], Vec::new());
let next = ModelRequest::default().continue_turn(&prior, Message::user("more"));
assert!(next.continued_from.is_empty());
}
#[test]
fn carries_non_text_assistant_content_into_next_turn() {
let prior_content = vec![
assistant_text("calling tool"),
ContentPart::ToolUse {
id: "call-1".to_owned(),
name: "calc".to_owned(),
input: serde_json::json!({"a": 1, "b": 2}),
provider_echoes: Vec::new(),
},
];
let prior = prior_response(prior_content, Vec::new());
let mut req = ModelRequest {
messages: vec![
Message::user("compute"),
],
..ModelRequest::default()
};
let result_msg = Message::new(
Role::Tool,
vec![ContentPart::ToolResult {
tool_use_id: "call-1".to_owned(),
name: "calc".to_owned(),
content: entelix_core::ir::ToolResultContent::Text("3".to_owned()),
is_error: false,
cache_control: None,
provider_echoes: Vec::new(),
}],
);
req = req.continue_turn(&prior, result_msg);
assert_eq!(req.messages.len(), 3);
assert!(matches!(
req.messages[1].content[1],
ContentPart::ToolUse { .. }
));
assert_eq!(req.messages[2].role, Role::Tool);
}