use car_inference::model_management::{ModelManagementError, ModelManagementStore};
use car_inference::schema::ModelSource;
use car_inference::{InferenceConfig, InferenceEngine};
static ENV_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
struct RestoredCarHome(Option<std::ffi::OsString>);
impl RestoredCarHome {
fn set(root: &std::path::Path) -> Self {
let previous = std::env::var_os(car_home::ENV_VAR);
unsafe { std::env::set_var(car_home::ENV_VAR, root) };
Self(previous)
}
}
impl Drop for RestoredCarHome {
fn drop(&mut self) {
unsafe {
match &self.0 {
Some(value) => std::env::set_var(car_home::ENV_VAR, value),
None => std::env::remove_var(car_home::ENV_VAR),
}
}
}
}
#[cfg(unix)]
use car_inference::model_management::ManagedArtifactKind;
#[cfg(unix)]
use car_inference::ModelCapability;
#[test]
fn management_paths_hash_exact_untrusted_utf8_ids_inside_private_roots() {
let root = tempfile::tempdir().unwrap();
let store = ModelManagementStore::new(root.path().join("state"), root.path().join("models"));
let fixtures = [
(
"org/model",
"c87c0c64d2fb215b38496b07952d86526559fee390c98b0f383fef0d230107f4.json",
),
(
"../escape",
"1ba7343c47dc442de7dec43a995deb9a7b62234ecca16d7c6f597b5155bd85b1.json",
),
(
"モデル/四",
"48102f4e7ca99fc00ae4d8942bd2f797779d456ee7b7ed34297c299e7c3a6529.json",
),
];
for (model_id, expected_filename) in fixtures {
let path = store.receipt_path(model_id);
assert_eq!(path.parent(), Some(store.receipts_root()));
assert_eq!(path.file_name().unwrap(), expected_filename);
}
assert_ne!(store.receipt_path("é"), store.receipt_path("é"));
assert_eq!(
store.tombstone_path("org/model").parent(),
Some(store.tombstones_root())
);
assert_eq!(
store.lease_path("org/model").parent(),
Some(store.leases_root())
);
}
#[test]
fn receipt_rejects_unknown_fields() {
let root = tempfile::tempdir().unwrap();
let store = ModelManagementStore::new(root.path().join("state"), root.path().join("models"));
std::fs::create_dir_all(store.receipts_root()).unwrap();
let payload = serde_json::json!({
"model_id": "org/model",
"managed_path": root.path().join("models/Model"),
"artifact_kind": "directory",
"source_model_id": "org/model",
"source_revision": null,
"creation_generation": 1,
"shared_cache_references": [],
"adopted": false,
"unexpected": true
});
std::fs::write(
store.receipt_path("org/model"),
serde_json::to_vec(&payload).unwrap(),
)
.unwrap();
assert!(matches!(
store.load_receipt("org/model"),
Err(ModelManagementError::InvalidState { .. })
));
}
#[test]
fn active_cross_process_lease_blocks_removal_until_final_drop() {
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
std::fs::create_dir_all(&models).unwrap();
let store = ModelManagementStore::new(root.path().join("state"), models);
let first = store.acquire_lease("org/model").unwrap();
let second = store.acquire_lease("org/model").unwrap();
assert!(store.model_in_use("org/model").unwrap());
drop(first);
assert!(store.model_in_use("org/model").unwrap());
drop(second);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
loop {
if !store.model_in_use("org/model").unwrap() {
break;
}
assert!(
std::time::Instant::now() < deadline,
"model must become removable once the final lease is dropped"
);
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
#[cfg(unix)]
#[tokio::test]
async fn unified_list_auto_adopts_safe_legacy_symlink_and_tombstone_blocks_local_use() {
let _environment = ENV_MUTEX.lock().await;
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().unwrap();
let state = root.path().join("state");
let models = root.path().join("models");
std::fs::create_dir_all(&models).unwrap();
let engine = InferenceEngine::new(InferenceConfig {
state_root: state,
models_dir: models.clone(),
..InferenceConfig::default()
});
let schema = engine
.unified_registry
.all()
.find(|schema| {
matches!(schema.source, ModelSource::Local { .. })
&& schema.has_capability(ModelCapability::Generate)
})
.cloned()
.expect("downloadable built-in local generation model");
let shared = root.path().join("shared/model");
std::fs::create_dir_all(&shared).unwrap();
std::fs::write(shared.join("sentinel"), b"preserve").unwrap();
std::fs::write(shared.join("model.gguf"), b"weights").unwrap();
std::fs::write(shared.join("tokenizer.json"), b"{}").unwrap();
let managed = models.join(&schema.name);
symlink(&shared, &managed).unwrap();
let listed = engine.list_models_unified();
let row = listed.iter().find(|row| row.id == schema.id).unwrap();
assert!(row.car_enabled);
assert!(row.can_remove);
assert!(!row.in_use);
assert_eq!(row.management_evidence.as_deref(), Some("install_receipt"));
let receipt = engine
.model_management_store()
.load_receipt(&schema.id)
.unwrap()
.expect("safe legacy symlink should be adopted during refresh");
assert!(receipt.adopted);
assert_eq!(receipt.managed_path, managed);
engine.remove_model_from_car(&schema.id).await.unwrap();
let row = engine
.list_models_unified()
.into_iter()
.find(|row| row.id == schema.id)
.unwrap();
assert!(!row.car_enabled);
assert!(!row.can_remove);
assert!(
!row.available,
"compatibility clients that only inspect available must not route a tombstoned model"
);
assert!(engine.local_model_preflight(&schema.id, 0).is_err());
assert!(shared.join("sentinel").exists());
}
#[tokio::test]
async fn pull_refuses_to_mint_ownership_over_a_preexisting_unreceipted_directory() {
let _environment = ENV_MUTEX.lock().await;
let root = tempfile::tempdir().unwrap();
let _home = RestoredCarHome::set(&root.path().join("car-home"));
let models = root.path().join("models");
std::fs::create_dir_all(&models).unwrap();
let models = models.canonicalize().unwrap();
let engine = InferenceEngine::new(InferenceConfig {
state_root: root.path().join("state"),
models_dir: models.clone(),
..InferenceConfig::default()
});
let schema = engine
.unified_registry
.all()
.find(|schema| matches!(schema.source, ModelSource::Local { .. }))
.cloned()
.expect("downloadable local model");
let hand_installed = models.join(&schema.name);
std::fs::create_dir_all(&hand_installed).unwrap();
std::fs::write(hand_installed.join("model.gguf"), b"weights").unwrap();
std::fs::write(hand_installed.join("tokenizer.json"), b"{}").unwrap();
std::fs::write(hand_installed.join("sentinel"), b"preserve").unwrap();
let error = engine.pull_model(&schema.id).await.unwrap_err();
assert!(
error.to_string().contains("pre-existing unreceipted"),
"unexpected pull error: {error}"
);
assert!(engine
.model_management_store()
.load_receipt(&schema.id)
.unwrap()
.is_none());
assert!(hand_installed.join("sentinel").exists());
}
#[test]
fn unified_list_drops_vanished_auto_discovery_despite_stale_coordination_records() {
let root = tempfile::tempdir().unwrap();
let state = root.path().join("car-home");
let models = root.path().join("models");
let model_name = "Stray-Unregistered-LLM";
let model_id = "local/stray-unregistered-llm";
let model_dir = models.join(model_name);
std::fs::create_dir_all(&model_dir).unwrap();
std::fs::write(model_dir.join("model.gguf"), b"weights").unwrap();
std::fs::write(model_dir.join("tokenizer.json"), b"{}").unwrap();
let engine = InferenceEngine::new(InferenceConfig {
state_root: state.clone(),
models_dir: models.clone(),
..InferenceConfig::default()
});
assert!(engine
.list_models_unified()
.iter()
.any(|row| row.id == model_id));
let management = engine.model_management_store();
assert!(management.lease_path(model_id).is_file());
assert!(management.mutation_lock_path(model_id).is_file());
std::fs::remove_dir_all(model_dir).unwrap();
assert!(
!engine
.list_models_unified()
.iter()
.any(|row| row.id == model_id),
"activity and mutation records must not keep a vanished auto-discovered model listed"
);
assert!(management.lease_path(model_id).is_file());
assert!(management.mutation_lock_path(model_id).is_file());
assert!(
!engine
.list_schemas()
.iter()
.any(|schema| schema.id == model_id),
"catalog snapshots must also drop the vanished row"
);
let restarted = InferenceEngine::new(InferenceConfig {
state_root: state,
models_dir: models,
..InferenceConfig::default()
});
assert!(
!restarted
.list_models_unified()
.iter()
.any(|row| row.id == model_id),
"coordination records for an unknown id must not create a registry row on restart"
);
}
#[test]
fn legacy_model_registry_removal_fails_closed_and_preserves_managed_bytes() {
let root = tempfile::tempdir().unwrap();
let registry = car_inference::ModelRegistry::new(root.path().to_path_buf());
let model = registry.list_models().into_iter().next().unwrap();
let model_dir = root.path().join(&model.name);
std::fs::create_dir_all(&model_dir).unwrap();
std::fs::write(model_dir.join("sentinel"), b"preserve").unwrap();
#[allow(deprecated)]
let result = registry.remove_model(&model.name);
assert!(result.is_err());
assert!(model_dir.join("sentinel").exists());
}
#[cfg(unix)]
#[tokio::test]
async fn engine_adopts_and_removes_only_managed_artifact_after_runtime_release() {
let _environment = ENV_MUTEX.lock().await;
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
std::fs::create_dir_all(&models).unwrap();
let engine = InferenceEngine::new(InferenceConfig {
state_root: root.path().join("state"),
models_dir: models.clone(),
..InferenceConfig::default()
});
let schema = engine
.unified_registry
.all()
.find(|schema| matches!(schema.source, ModelSource::Local { .. }))
.cloned()
.expect("downloadable local model");
let shared = root.path().join("hf/snapshot");
std::fs::create_dir_all(&shared).unwrap();
std::fs::write(shared.join("sentinel"), b"shared").unwrap();
std::fs::write(shared.join("model.gguf"), b"weights").unwrap();
std::fs::write(shared.join("tokenizer.json"), b"{}").unwrap();
symlink(&shared, models.join(&schema.name)).unwrap();
let receipt = engine.adopt_model_into_car(&schema.id).await.unwrap();
assert!(receipt.adopted);
assert_eq!(receipt.model_id, schema.id);
assert!(engine
.model_management_store()
.load_receipt(&schema.id)
.unwrap()
.is_some());
let removed = engine.remove_model_from_car(&schema.id).await.unwrap();
assert_eq!(removed.model_id, schema.id);
let retry_after_lost_response = engine.remove_model_from_car(&schema.id).await.unwrap();
assert_eq!(retry_after_lost_response, removed);
assert!(shared.join("sentinel").exists());
assert!(!engine
.model_management_store()
.car_enabled(&schema.id)
.unwrap());
let remote = engine
.unified_registry
.all()
.find(|schema| !schema.is_local())
.expect("remote catalog model");
assert!(engine.adopt_model_into_car(&remote.id).await.is_err());
assert!(engine.remove_model_from_car(&remote.id).await.is_err());
}
#[cfg(unix)]
#[tokio::test]
async fn explicit_adoption_never_claims_or_deletes_a_hand_installed_directory() {
let _environment = ENV_MUTEX.lock().await;
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
std::fs::create_dir_all(&models).unwrap();
let engine = InferenceEngine::new(InferenceConfig {
state_root: root.path().join("state"),
models_dir: models.clone(),
..InferenceConfig::default()
});
let schema = engine
.unified_registry
.all()
.find(|schema| matches!(schema.source, ModelSource::Local { .. }))
.cloned()
.expect("downloadable local model");
let hand_installed = models.join(&schema.name);
std::fs::create_dir_all(&hand_installed).unwrap();
std::fs::write(hand_installed.join("sentinel"), b"hand-installed").unwrap();
std::fs::write(hand_installed.join("model.gguf"), b"weights").unwrap();
std::fs::write(hand_installed.join("tokenizer.json"), b"{}").unwrap();
let receipt = engine.adopt_model_into_car(&schema.id).await.unwrap();
assert_eq!(receipt.artifact_kind, ManagedArtifactKind::Symlink);
assert_ne!(receipt.managed_path, hand_installed);
assert!(hand_installed.join("sentinel").exists());
engine.remove_model_from_car(&schema.id).await.unwrap();
assert!(hand_installed.join("sentinel").exists());
assert!(hand_installed.join("model.gguf").exists());
let row = engine
.list_models_unified()
.into_iter()
.find(|row| row.id == schema.id)
.unwrap();
assert!(!row.car_enabled);
assert!(
row.weights_ready,
"physical readiness remains diagnostic evidence"
);
assert!(
!row.available,
"legacy clients that only inspect available must not route tombstoned weights"
);
}