use cortiq_engine::tokenizer::Tokenizer;
fn tok_with_template() -> Tokenizer {
let mut t = Tokenizer::byte_level();
t.chat_template = Some(include_str!("fixtures/qwen3_chat_template.jinja").to_string());
t
}
fn j(v: serde_json::Value) -> serde_json::Value {
v
}
#[test]
fn tools_render_into_the_system_block() {
let t = tok_with_template();
let tools = vec![j(serde_json::json!({
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}))];
let msgs = vec![j(serde_json::json!({"role": "user", "content": "Weather in Paris?"}))];
let text = t.render_chat_json(&msgs, Some(&tools), None).unwrap();
assert!(text.contains("<tools>"), "tool signatures section missing");
assert!(
text.contains("\"get_weather\""),
"the function declaration must reach the prompt"
);
assert!(
text.contains("<tool_call>"),
"the output-grammar instruction must reach the prompt"
);
let bare = t.render_chat_json(&msgs, None, None).unwrap();
assert!(!bare.contains("<tools>"), "tools block must be absent without tools");
}
#[test]
fn assistant_history_with_tool_calls_renders() {
let t = tok_with_template();
let msgs = vec![
j(serde_json::json!({"role": "user", "content": "Weather in Paris?"})),
j(serde_json::json!({
"role": "assistant",
"content": "",
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}
}]
})),
j(serde_json::json!({
"role": "tool",
"tool_call_id": "call_1",
"content": "{\"temp_c\": 18, \"sky\": \"clear\"}"
})),
];
let text = t.render_chat_json(&msgs, None, None).unwrap();
assert!(
text.contains("<tool_call>\n{\"name\": \"get_weather\", \"arguments\": {\"city\": \"Paris\"}}"),
"assistant tool_calls must render in the trained grammar, got:\n{text}"
);
assert!(
text.contains("<tool_response>\n{\"temp_c\": 18"),
"role:tool results must render as <tool_response>, got:\n{text}"
);
}
#[test]
fn consecutive_tool_results_share_one_user_turn() {
let t = tok_with_template();
let msgs = vec![
j(serde_json::json!({"role": "user", "content": "Two cities."})),
j(serde_json::json!({
"role": "assistant", "content": "",
"tool_calls": [
{"type": "function", "function": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}},
{"type": "function", "function": {"name": "get_weather", "arguments": "{\"city\": \"Oslo\"}"}}
]
})),
j(serde_json::json!({"role": "tool", "content": "18C"})),
j(serde_json::json!({"role": "tool", "content": "7C"})),
];
let text = t.render_chat_json(&msgs, None, None).unwrap();
let user_starts = text.matches("<|im_start|>user").count();
assert_eq!(user_starts, 2, "the two tool results must share one user turn:\n{text}");
assert_eq!(text.matches("<tool_response>").count(), 2);
}
fn nanbeige_tok() -> Tokenizer {
let mut t = Tokenizer::byte_level();
t.chat_template =
Some(include_str!("fixtures/nanbeige42_chat_template.jinja").to_string());
t
}
#[test]
fn nanbeige_tools_render_in_json_grammar() {
let t = nanbeige_tok();
let tools = vec![j(serde_json::json!({
"type": "function",
"function": {"name": "get_weather", "description": "d",
"parameters": {"type": "object", "properties": {}}}
}))];
let msgs = vec![j(serde_json::json!({"role": "user", "content": "Weather in Paris?"}))];
let text = t.render_chat_json(&msgs, Some(&tools), None).unwrap();
assert!(text.contains("<tools>"), "tools section missing:\n{}", &text[..text.len().min(400)]);
assert!(text.contains("\"get_weather\""));
assert!(
text.contains("\"arguments\": <args-json-object>"),
"the JSON grammar must be selected, not the XML default"
);
assert!(
!text.contains("<function=example_function_name>"),
"the XML branch must not be the one rendered"
);
}
#[test]
fn nanbeige_tool_history_round_trips() {
let t = nanbeige_tok();
let msgs = vec![
j(serde_json::json!({"role": "user", "content": "Weather?"})),
j(serde_json::json!({"role": "assistant", "content": "",
"tool_calls": [{"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}}]})),
j(serde_json::json!({"role": "tool", "content": "18C"})),
];
let text = t.render_chat_json(&msgs, None, None).unwrap();
assert!(text.contains("get_weather"), "call history lost:\n{text}");
assert!(text.contains("<tool_response>"), "tool result lost:\n{text}");
}