test: add llamacpp model-slot consistency and content-null tests

Cover the properties that prevent mid-turn model swaps in llama-swap
exclusive mode: vision_model defaults to primary, cloned local client
mirrors the user-selected model, embeddings stay on their own slot.
Also test the content:null serialization for tool-calling messages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cameron Cordes
2026-05-24 19:29:51 -04:00
parent 208344ad98
commit 9dba659d1e

View File

@@ -985,4 +985,91 @@ mod tests {
assert!(vision.has_vision);
assert!(other.has_vision);
}
#[test]
fn vision_model_defaults_to_primary() {
let c = LlamaCppClient::new(None, Some("qwen3:32b".into()));
assert_eq!(c.primary_model, "qwen3:32b");
assert_eq!(c.vision_model, "qwen3:32b");
}
#[test]
fn vision_model_explicit_override_diverges_from_primary() {
let mut c = LlamaCppClient::new(None, Some("qwen3:32b".into()));
c.set_vision_model("minicpm-v".into());
assert_eq!(c.primary_model, "qwen3:32b");
assert_eq!(c.vision_model, "minicpm-v");
}
#[test]
fn cloned_local_with_model_override_keeps_all_slots_consistent() {
// Simulates what resolve_backend does for the `local` client:
// clone the configured client, then override primary + vision
// to match the user-selected chat model. This prevents mid-turn
// model swaps in llama-swap exclusive mode.
let mut base = LlamaCppClient::new(None, Some("chat".into()));
base.set_vision_model("vision".into());
base.set_embedding_model("embed".into());
let mut local = base.clone();
let user_selected = "qwen3:32b";
local.primary_model = user_selected.to_string();
local.set_vision_model(user_selected.to_string());
// Chat generation (rerank) routes through primary_model.
assert_eq!(local.primary_model, user_selected);
// describe_image routes through vision_model.
assert_eq!(local.vision_model, user_selected);
// Embeddings stay on the dedicated slot — separate endpoint,
// no model swap conflict.
assert_eq!(local.embedding_model, "embed");
}
#[test]
fn assistant_tool_calls_emit_null_content() {
let msg = ChatMessage {
role: "assistant".into(),
content: String::new(),
tool_calls: Some(vec![ToolCall {
id: Some("call_1".into()),
function: ToolCallFunction {
name: "search".into(),
arguments: json!({}),
},
}]),
images: None,
};
let wire = LlamaCppClient::messages_to_openai(&[msg]);
assert!(wire[0]["content"].is_null(), "empty content + tool_calls should emit null");
}
#[test]
fn assistant_with_content_and_tool_calls_preserves_content() {
let msg = ChatMessage {
role: "assistant".into(),
content: "Let me search for that.".into(),
tool_calls: Some(vec![ToolCall {
id: Some("call_1".into()),
function: ToolCallFunction {
name: "search".into(),
arguments: json!({}),
},
}]),
images: None,
};
let wire = LlamaCppClient::messages_to_openai(&[msg]);
assert_eq!(wire[0]["content"], "Let me search for that.");
}
#[test]
fn assistant_without_tool_calls_keeps_empty_string_content() {
let msg = ChatMessage {
role: "assistant".into(),
content: String::new(),
tool_calls: None,
images: None,
};
let wire = LlamaCppClient::messages_to_openai(&[msg]);
assert_eq!(wire[0]["content"], "");
}
}