use car_inference::{
Device, HardwareInfo, InferenceConfig, InferenceError, ModelRegistry, ModelRole, ModelRouter,
TaskComplexity,
};
use serde_json::json;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn device_auto_detection() {
let device = Device::auto();
#[cfg(all(
any(target_os = "linux", target_os = "windows"),
target_arch = "x86_64",
not(car_skip_cuda)
))]
assert!(matches!(device, Device::Cuda(0)));
#[cfg(all(target_os = "macos", feature = "metal"))]
assert_eq!(device, Device::Metal);
#[cfg(not(any(
all(target_os = "macos", feature = "metal"),
all(
any(target_os = "linux", target_os = "windows"),
target_arch = "x86_64",
not(car_skip_cuda)
)
)))]
assert_eq!(device, Device::Cpu);
}
#[test]
fn default_config() {
let config = InferenceConfig::default();
assert!(config.models_dir.ends_with(".car/models"));
assert_eq!(config.embedding_model, "Qwen3-Embedding-0.6B");
assert_eq!(config.classification_model, "Qwen3-0.6B");
assert!(config.device.is_none()); }
#[test]
fn model_registry_lists_catalog() {
let dir = PathBuf::from("/tmp/car-test-models");
let registry = ModelRegistry::new(dir);
let models = registry.list_models();
assert_eq!(models.len(), 6);
let emb = models
.iter()
.find(|m| m.name == "Qwen3-Embedding-0.6B")
.unwrap();
assert_eq!(emb.role, ModelRole::Embedding);
let small = models.iter().find(|m| m.name == "Qwen3-0.6B").unwrap();
assert_eq!(small.role, ModelRole::Small);
assert_eq!(small.param_count, "0.6B");
assert!(!small.downloaded);
let moe = models.iter().find(|m| m.name == "Qwen3-30B-A3B").unwrap();
assert_eq!(moe.role, ModelRole::Expert);
assert_eq!(moe.param_count, "30B (3B active)");
}
#[test]
fn model_registry_not_found() {
let dir = PathBuf::from("/tmp/car-test-models");
let registry = ModelRegistry::new(dir);
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(registry.ensure_model("NonExistentModel-999B"));
assert!(matches!(result, Err(InferenceError::ModelNotFound(_))));
}
#[test]
fn legacy_model_registry_remove_is_disabled_even_when_absent() {
let dir = PathBuf::from("/tmp/car-test-models-remove");
let registry = ModelRegistry::new(dir);
#[allow(deprecated)]
let result = registry.remove_model("Qwen3-0.6B");
assert!(result.is_err());
}
#[test]
fn unified_registry_reports_curated_model_upgrades() {
let tmp = TempDir::new().unwrap();
let models_dir = tmp.path().join("models");
std::fs::create_dir_all(models_dir.join("Qwen3-30B-A3B-MLX")).unwrap();
std::fs::write(
models_dir.join("Qwen3-30B-A3B-MLX").join("config.json"),
"{}",
)
.unwrap();
let registry =
car_inference::UnifiedRegistry::new_with_state_root(tmp.path().to_path_buf(), models_dir);
let upgrades = registry.available_upgrades();
#[cfg(all(target_os = "macos", target_arch = "aarch64", not(car_skip_mlx)))]
{
assert_eq!(upgrades.len(), 1);
let upgrade = &upgrades[0];
assert_eq!(upgrade.from_id, "mlx/qwen3-30b-a3b:4bit");
assert_eq!(upgrade.to_id, "vllm-mlx/qwen3.6-35b-a3b:4bit");
assert!(!upgrade.target_pullable);
assert!(upgrade.remove_old_supported);
}
#[cfg(not(all(target_os = "macos", target_arch = "aarch64", not(car_skip_mlx))))]
{
assert!(
!upgrades
.iter()
.any(|u| u.from_id == "mlx/qwen3-30b-a3b:4bit"),
"mlx/qwen3-30b-a3b:4bit upgrade should not be reported on a non-MLX target — \
the source model can't execute here so it can't be the basis for an upgrade. \
Found: {:?}",
upgrades.iter().map(|u| &u.from_id).collect::<Vec<_>>()
);
}
}
#[test]
fn unified_registry_includes_qwen36_vllm_target() {
let tmp = TempDir::new().unwrap();
let registry = car_inference::UnifiedRegistry::new_with_state_root(
tmp.path().to_path_buf(),
tmp.path().join("models"),
);
let qwen36 = registry
.get("vllm-mlx/qwen3.6-35b-a3b:4bit")
.expect("qwen3.6 target");
assert_eq!(qwen36.family, "qwen3.6");
assert_eq!(qwen36.context_length, 262_144);
assert!(matches!(
qwen36.source,
car_inference::ModelSource::ManagedVllmMlx { .. }
));
assert!(qwen36.has_capability(car_inference::ModelCapability::Code));
assert!(qwen36.has_capability(car_inference::ModelCapability::Vision));
}
#[test]
fn service_schemas_defined() {
let schemas = car_inference::service::all_schemas();
assert_eq!(schemas.len(), 13);
let names: Vec<&str> = schemas.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"infer"));
assert!(names.contains(&"infer.grounded"));
assert!(names.contains(&"embed"));
assert!(names.contains(&"classify"));
assert!(names.contains(&"generate_image"));
assert!(names.contains(&"generate_video"));
assert!(names.contains(&"transcribe"));
assert!(names.contains(&"synthesize"));
assert!(names.contains(&"search"));
assert!(names.contains(&"web_fetch"));
let infer = schemas.iter().find(|s| s.name == "infer").unwrap();
let required = infer.parameters.get("required").unwrap();
assert!(required.as_array().unwrap().iter().any(|v| v == "prompt"));
let embed = schemas.iter().find(|s| s.name == "embed").unwrap();
assert!(embed.idempotent);
assert!(embed.cache_ttl_secs.is_some());
let classify = schemas.iter().find(|s| s.name == "classify").unwrap();
assert!(classify.idempotent);
}
#[test]
fn generate_params_defaults() {
let params = car_inference::GenerateParams::default();
assert!((params.temperature - 0.7).abs() < f64::EPSILON);
assert!((params.top_p - 0.9).abs() < f64::EPSILON);
assert_eq!(params.top_k, 0);
assert_eq!(params.max_tokens, 4096);
assert!(params.stop.is_empty());
assert_eq!(params.estimated_cache_read_input_tokens, 0);
assert_eq!(params.estimated_cache_write_input_tokens, 0);
}
#[test]
fn generate_params_serde() {
let json = r#"{"temperature": 0.0, "max_tokens": 100}"#;
let params: car_inference::GenerateParams = serde_json::from_str(json).unwrap();
assert_eq!(params.temperature, 0.0);
assert_eq!(params.max_tokens, 100);
assert!((params.top_p - 0.9).abs() < f64::EPSILON);
assert_eq!(params.estimated_cache_read_input_tokens, 0);
assert_eq!(params.estimated_cache_write_input_tokens, 0);
}
#[test]
fn inference_engine_creation() {
let config = InferenceConfig {
models_dir: PathBuf::from("/tmp/car-test-engine"),
state_root: PathBuf::from("/tmp/car-test-engine"),
device: Some(Device::Cpu),
generation_model: "Qwen3-0.6B".to_string(),
preferred_generation_model: None,
embedding_model: "Qwen3-0.6B".to_string(),
preferred_embedding_model: None,
classification_model: "Qwen3-0.6B".to_string(),
preferred_classification_model: None,
};
let engine = car_inference::InferenceEngine::new(config);
let models = engine.list_models();
assert_eq!(models.len(), 6); }
#[test]
fn generate_request_with_context() {
let req = car_inference::GenerateRequest {
prompt: "What should I do?".into(),
model: None,
params: Default::default(),
context: Some("## Facts\n- Project uses Rust\n- Deadline is Friday".into()),
context_stable_prefix: None,
tools: None,
images: None,
messages: None,
cache_control: false,
response_format: None,
intent: None,
client_ref: None,
expected_row_digest: None,
expected_catalog_revision: None,
caller: None,
};
assert!(req.context.is_some());
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("context"));
assert!(json.contains("Deadline is Friday"));
}
#[test]
fn generate_request_without_context_backward_compat() {
let json = r#"{"prompt":"hello","params":{"temperature":0.7}}"#;
let req: car_inference::GenerateRequest = serde_json::from_str(json).unwrap();
assert!(req.context.is_none());
}
#[test]
fn generate_request_with_context_roundtrip() {
let req = car_inference::GenerateRequest {
prompt: "test prompt".into(),
model: Some("Qwen3-0.6B".into()),
params: Default::default(),
context: Some("some context".into()),
context_stable_prefix: None,
tools: None,
images: None,
messages: None,
cache_control: false,
response_format: None,
intent: None,
client_ref: None,
expected_row_digest: None,
expected_catalog_revision: None,
caller: None,
};
let json = serde_json::to_string(&req).unwrap();
let deserialized: car_inference::GenerateRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.prompt, "test prompt");
assert_eq!(deserialized.context.as_deref(), Some("some context"));
assert_eq!(deserialized.model.as_deref(), Some("Qwen3-0.6B"));
}
#[test]
fn generate_request_with_null_context() {
let json = r#"{"prompt":"hello","context":null}"#;
let req: car_inference::GenerateRequest = serde_json::from_str(json).unwrap();
assert!(req.context.is_none());
}
#[test]
fn infer_grounded_schema_defined() {
let schemas = car_inference::service::all_schemas();
assert_eq!(schemas.len(), 13);
let names: Vec<&str> = schemas.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"infer.grounded"));
let grounded = schemas.iter().find(|s| s.name == "infer.grounded").unwrap();
let required = grounded.parameters.get("required").unwrap();
assert!(required.as_array().unwrap().iter().any(|v| v == "prompt"));
assert!(!grounded.idempotent);
}
#[test]
fn routing_simple_question() {
let complexity = TaskComplexity::assess("What is the capital of France?");
assert_eq!(complexity, TaskComplexity::Simple);
}
#[test]
fn routing_code_task() {
let complexity = TaskComplexity::assess("Write a function to sort a list");
assert_eq!(complexity, TaskComplexity::Code);
}
#[test]
fn routing_complex_reasoning() {
let complexity = TaskComplexity::assess(
"Analyze the trade-offs between microservices and monolithic architecture for our use case",
);
assert_eq!(complexity, TaskComplexity::Complex);
}
#[test]
fn routing_medium_default() {
let complexity = TaskComplexity::assess(
"Tell me about the history of computing and how it has evolved over the decades from early mechanical calculation devices through transistors to the modern digital systems we use today in everyday life",
);
assert_eq!(complexity, TaskComplexity::Medium);
}
#[test]
fn router_respects_hardware() {
let mut hw = HardwareInfo::detect();
hw.max_model_mb = 1000; hw.recommended_model = "Qwen3-0.6B".to_string();
let router = ModelRouter::new(hw);
let registry = ModelRegistry::new(PathBuf::from("/tmp/car-test-routing"));
let decision = router.route_generate(
"Analyze complex architecture decisions step by step",
®istry,
);
assert_eq!(decision.model, "Qwen3-0.6B");
assert_eq!(decision.complexity, TaskComplexity::Complex);
}
#[test]
fn routing_repair_is_code() {
let complexity = TaskComplexity::assess("Debug this failing test case");
assert_eq!(complexity, TaskComplexity::Code);
}
#[test]
fn routing_backtick_code() {
let complexity =
TaskComplexity::assess("Here is my code:\n```rust\nfn main() {}\n```\nWhat does it do?");
assert_eq!(complexity, TaskComplexity::Code);
}
#[test]
fn hardware_detect_returns_reasonable_values() {
let hw = HardwareInfo::detect();
assert!(
hw.os == "macos" || hw.os == "linux" || hw.os == "windows",
"unexpected os: {}",
hw.os
);
assert!(!hw.arch.is_empty());
assert!(hw.cpu_cores >= 1);
assert!(
hw.total_ram_mb >= 1024,
"unexpected RAM: {} MB",
hw.total_ram_mb
);
let known_models = [
"Qwen3-0.6B",
"Qwen3-1.7B",
"Qwen3-4B",
"Qwen3-8B",
"Qwen3-30B-A3B",
"Qwen3-0.6B-MLX",
"Qwen3-1.7B-MLX",
"Qwen3-4B-MLX",
"Qwen3-8B-MLX",
"Qwen3-30B-A3B-MLX",
];
assert!(
known_models.contains(&hw.recommended_model.as_str()),
"unexpected recommended model: {}",
hw.recommended_model
);
assert!(hw.recommended_context >= 2048);
assert!(hw.recommended_context <= 131072);
assert!(hw.max_model_mb > 0);
}
#[test]
fn generate_video_request_defaults_to_t2v() {
let req = car_inference::GenerateVideoRequest {
prompt: "a dog running".into(),
..car_inference::GenerateVideoRequest::default()
};
assert_eq!(req.effective_mode(), car_inference::VideoMode::T2v);
}
#[test]
fn generate_video_request_infers_i2v_from_image_path() {
let req = car_inference::GenerateVideoRequest {
prompt: "the dog jumps".into(),
image_path: Some("/tmp/ref.png".into()),
..car_inference::GenerateVideoRequest::default()
};
assert_eq!(req.effective_mode(), car_inference::VideoMode::I2v);
}
#[test]
fn generate_video_request_round_trips_i2v_through_json() {
let payload = json!({
"prompt": "cat walking",
"image_path": "/tmp/cat.jpg",
});
let req: car_inference::GenerateVideoRequest =
serde_json::from_value(payload).expect("deserialize");
assert_eq!(req.prompt, "cat walking");
assert_eq!(req.image_path.as_deref(), Some("/tmp/cat.jpg"));
assert_eq!(req.effective_mode(), car_inference::VideoMode::I2v);
let back = serde_json::to_value(&req).expect("serialize");
assert_eq!(
back.get("image_path").and_then(|v| v.as_str()),
Some("/tmp/cat.jpg")
);
}
#[test]
fn generate_video_schema_exposes_i2v_fields() {
let schemas = car_inference::service::all_schemas();
let video = schemas
.iter()
.find(|s| s.name == "generate_video")
.expect("generate_video schema");
let props = video
.parameters
.get("properties")
.and_then(|v| v.as_object())
.expect("properties object");
assert!(
props.contains_key("image_path"),
"i2v field surfaced in tool schema"
);
assert!(
props.contains_key("audio_path"),
"audio reference field surfaced in tool schema"
);
assert!(
props.contains_key("mode"),
"mode selector surfaced in tool schema"
);
let mode = props.get("mode").and_then(|v| v.as_object()).unwrap();
let enum_vals: Vec<&str> = mode
.get("enum")
.and_then(|v| v.as_array())
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert!(enum_vals.contains(&"t2v") && enum_vals.contains(&"i2v"));
assert!(
enum_vals.contains(&"audio_video"),
"audio_video exposed in schema"
);
assert!(
enum_vals.contains(&"audio_ref_video"),
"audio_ref_video exposed in schema"
);
}
#[test]
fn generate_video_request_explicit_audio_video_mode() {
let payload = json!({
"prompt": "thunderstorm over the plains",
"mode": "audio_video",
});
let req: car_inference::GenerateVideoRequest =
serde_json::from_value(payload).expect("deserialize");
assert_eq!(req.effective_mode(), car_inference::VideoMode::AudioVideo);
assert!(req.validate().is_ok());
}
#[test]
fn generate_video_request_rejects_audio_video_with_image() {
let payload = json!({
"prompt": "x",
"mode": "audio_video",
"image_path": "/tmp/x.png",
});
let req: car_inference::GenerateVideoRequest =
serde_json::from_value(payload).expect("deserialize");
let err = req
.validate()
.expect_err("audio_video + image_path should be rejected");
assert!(err.contains("audio_video"), "error mentions mode: {}", err);
}
#[test]
fn generate_video_request_rejects_t2v_with_image() {
let payload = json!({
"prompt": "x",
"mode": "t2v",
"image_path": "/tmp/x.png",
});
let req: car_inference::GenerateVideoRequest =
serde_json::from_value(payload).expect("deserialize");
assert!(req.validate().is_err());
}
#[test]
fn generate_video_request_audio_video_round_trip() {
let req = car_inference::GenerateVideoRequest {
prompt: "waves crashing".into(),
mode: Some(car_inference::VideoMode::AudioVideo),
..car_inference::GenerateVideoRequest::default()
};
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v.get("mode").and_then(|m| m.as_str()), Some("audio_video"));
let back: car_inference::GenerateVideoRequest = serde_json::from_value(v).unwrap();
assert_eq!(back.effective_mode(), car_inference::VideoMode::AudioVideo);
}
#[test]
fn generate_video_request_audio_ref_round_trip() {
let req = car_inference::GenerateVideoRequest {
prompt: "chorus explodes into color".into(),
audio_path: Some("/tmp/chorus.wav".into()),
audio_passthrough: false,
..car_inference::GenerateVideoRequest::default()
};
assert_eq!(
req.effective_mode(),
car_inference::VideoMode::AudioRefVideo
);
req.validate().unwrap();
let v = serde_json::to_value(&req).unwrap();
assert_eq!(
v.get("audio_path").and_then(|m| m.as_str()),
Some("/tmp/chorus.wav")
);
let back: car_inference::GenerateVideoRequest = serde_json::from_value(v).unwrap();
assert_eq!(
back.effective_mode(),
car_inference::VideoMode::AudioRefVideo
);
}