//! Infrastructure tests for local model-server adapters. use std::path::PathBuf; use std::sync::Arc; use domain::model_server::{ ExecutablePath, HfModelRef, LlamaCppOptions, LocalModelRef, LocalModelServerConfig, LocalModelServerKind, ModelPath, ModelServerEndpoint, ModelSource, StopPolicy, }; use domain::ports::{ FileSystem, ModelArtifactCancel, ModelArtifactDownloader, ModelServerRegistry, ModelServerRuntime, RemotePath, }; use domain::LocalModelServerId; use infrastructure::{ FsModelServerRegistry, HfModelArtifactDownloader, LlamaCppRuntime, LocalFileSystem, }; use uuid::Uuid; struct TempDir(PathBuf); impl TempDir { fn new() -> Self { let path = std::env::temp_dir().join(format!("idea-model-server-{}", Uuid::new_v4())); std::fs::create_dir_all(&path).unwrap(); Self(path) } fn app_data_dir(&self) -> String { self.0.to_string_lossy().into_owned() } fn child(&self, name: &str) -> RemotePath { RemotePath::new(self.0.join(name).to_string_lossy().into_owned()) } fn path(&self) -> &std::path::Path { &self.0 } } impl Drop for TempDir { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } fn sid(n: u128) -> LocalModelServerId { LocalModelServerId::from_uuid(Uuid::from_u128(n)) } fn config(id: LocalModelServerId, port: u16) -> LocalModelServerConfig { let binary = std::env::current_exe() .unwrap() .to_string_lossy() .into_owned(); LocalModelServerConfig::new( id, LocalModelServerKind::LlamaCpp, "Local Qwen", ModelServerEndpoint::new(format!("http://localhost:{port}/v1"), port).unwrap(), LocalModelRef::new( "qwen", "Qwen", Some(ModelSource::LocalPath { path: ModelPath::new("/models/qwen.gguf").unwrap(), }), "qwen3-coder-30b", ) .unwrap(), Some(ExecutablePath::new(binary).unwrap()), LlamaCppOptions::new("127.0.0.1", Some(35), Some(8192), true).unwrap(), vec!["--threads".to_owned(), "8".to_owned()], true, StopPolicy::StopOnAppExit, ) .unwrap() } #[test] fn llamacpp_runtime_builds_structured_argv() { let argv = LlamaCppRuntime::new() .build_argv(&config(sid(1), 8080)) .unwrap(); assert_eq!( argv.command, std::env::current_exe().unwrap().to_string_lossy() ); assert_eq!( argv.args, vec![ "--model", "/models/qwen.gguf", "--port", "8080", "--host", "127.0.0.1", "-ngl", "35", "-c", "8192", "--jinja", "--threads", "8" ] ); } #[test] fn llamacpp_runtime_builds_hf_argv() { let mut config = config(sid(3), 8083); config.model.source = Some(ModelSource::HuggingFace { repo: HfModelRef::new("Qwen/Qwen3-Coder:Q4_K_M").unwrap(), }); config.options = LlamaCppOptions::default(); config.args = Vec::new(); let spec = LlamaCppRuntime::new().build_spawn_spec(&config).unwrap(); assert_eq!( spec.args, vec![ "-hf", "Qwen/Qwen3-Coder:Q4_K_M", "--port", "8083", "--host", "127.0.0.1" ] ); assert!(spec.context_plan.is_none()); } #[tokio::test] async fn fs_model_server_registry_roundtrips_global_json() { let tmp = TempDir::new(); let fs: Arc = Arc::new(LocalFileSystem::new()); let registry = FsModelServerRegistry::new(Arc::clone(&fs), tmp.app_data_dir()); let first = config(sid(2), 8081) .with_warmup_deadline_secs(Some(900)) .unwrap(); registry.save(first.clone()).await.unwrap(); assert_eq!(registry.get(&first.id).await.unwrap(), Some(first.clone())); assert_eq!(registry.list().await.unwrap(), vec![first.clone()]); assert!( fs.exists(&tmp.child("model-servers.json")).await.unwrap(), "registry must live in global app-data dir" ); let updated = config(sid(2), 8082); registry.save(updated.clone()).await.unwrap(); assert_eq!(registry.list().await.unwrap(), vec![updated]); } #[tokio::test] async fn fs_model_server_registry_migrates_v1_json_to_v2() { let tmp = TempDir::new(); let fs: Arc = Arc::new(LocalFileSystem::new()); fs.create_dir_all(&RemotePath::new(tmp.app_data_dir())) .await .unwrap(); fs.write( &tmp.child("model-servers.json"), br#"{ "version": 1, "servers": [{ "id": "00000000-0000-0000-0000-000000000004", "kind": "llamaCpp", "name": "Legacy Qwen", "endpoint": { "baseURL": "http://localhost:8084/v1", "port": 8084 }, "model": { "id": "qwen", "label": "Qwen", "path": "/models/qwen.gguf", "servedName": "qwen3-coder" }, "binary": "/usr/bin/llama-server", "args": ["--ctx-size", "4096"], "autoStart": true, "stopPolicy": "stopOnAppExit" }] }"#, ) .await .unwrap(); let registry = FsModelServerRegistry::new(Arc::clone(&fs), tmp.app_data_dir()); let servers = registry.list().await.unwrap(); assert_eq!(servers.len(), 1); assert_eq!(servers[0].options, LlamaCppOptions::default()); assert_eq!(servers[0].warmup_deadline_secs, None); assert_eq!(servers[0].args, vec!["--ctx-size", "4096"]); assert_eq!( servers[0].model.source, Some(ModelSource::LocalPath { path: ModelPath::new("/models/qwen.gguf").unwrap() }) ); } #[tokio::test] async fn hf_model_artifact_downloader_resolves_deterministic_local_cache_hit_without_network() { let tmp = TempDir::new(); let downloader = HfModelArtifactDownloader::new(tmp.path()); let repo = HfModelRef::new("Qwen/Qwen3-Coder:Q4_K_M").unwrap(); let path = downloader.cache_path_for(&repo); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(&path, b"gguf").unwrap(); let resolution = downloader .resolve_hf_model( &repo, Arc::new(|_| panic!("cache hit must not report progress")), ModelArtifactCancel::new(), ) .await .unwrap(); assert!(resolution.cache_hit); assert_eq!(resolution.path.as_str(), path.to_string_lossy()); assert_eq!( path.strip_prefix(tmp.path()).unwrap(), std::path::Path::new("Qwen--Qwen3-Coder").join("Q4_K_M.gguf") ); }