//! L5 integration tests for the LOT C2 embedder-config adapters: //! - [`FsEmbedderProfileStore`] driven through the [`EmbedderProfileStore`] port //! against a real [`LocalFileSystem`] + temp dir (round-trip, `embedder.json` //! persistence, delete-absent ⇒ `NotFound`), //! - [`EmbedderEnvProbe::inspect`] without the HTTP feature (`ollama_detected` //! always `false`) and reflecting a prepared ONNX cache. //! //! No network is required. The temp-dir convention mirrors `tests/project_store.rs` //! (`std::env::temp_dir()` + a UUID, self-cleaning on drop) — no new dependency. use std::path::PathBuf; use std::sync::Arc; use domain::ports::{ EmbedderEnvInspector, EmbedderProfileStore, FileSystem, RemotePath, StoreError, }; use domain::profile::{EmbedderProfile, EmbedderStrategy}; use infrastructure::{ EmbedderEnvProbe, FsEmbedderProfileStore, LocalFileSystem, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED, }; use uuid::Uuid; /// A unique scratch directory under the OS temp dir, cleaned up on drop. struct TempDir(PathBuf); impl TempDir { fn new() -> Self { let p = std::env::temp_dir().join(format!("idea-l5-embedder-{}", Uuid::new_v4())); std::fs::create_dir_all(&p).unwrap(); Self(p) } fn app_data_dir(&self) -> String { self.0.to_string_lossy().into_owned() } fn path(&self) -> &std::path::Path { &self.0 } fn child(&self, name: &str) -> RemotePath { RemotePath::new(self.0.join(name).to_string_lossy().into_owned()) } } impl Drop for TempDir { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } fn store(tmp: &TempDir) -> FsEmbedderProfileStore { let fs: Arc = Arc::new(LocalFileSystem::new()); FsEmbedderProfileStore::new(fs, tmp.app_data_dir()) } fn sample(id: &str, name: &str, dimension: usize) -> EmbedderProfile { EmbedderProfile::new( id, name, EmbedderStrategy::LocalOnnx, Some("multilingual-e5-small".to_owned()), None, None, dimension, ) .unwrap() } // --------------------------------------------------------------------------- // FsEmbedderProfileStore via the EmbedderProfileStore port // --------------------------------------------------------------------------- #[tokio::test] async fn save_then_list_roundtrips() { let tmp = TempDir::new(); let store: &dyn EmbedderProfileStore = &store(&tmp); assert!( store.list().await.unwrap().is_empty(), "no embedder.json yet ⇒ empty list" ); let p = sample("local-onnx", "Local ONNX", 384); store.save(&p).await.unwrap(); assert_eq!(store.list().await.unwrap(), vec![p]); } #[tokio::test] async fn save_upserts_by_id_without_duplicating() { let tmp = TempDir::new(); let store: &dyn EmbedderProfileStore = &store(&tmp); store.save(&sample("e", "before", 384)).await.unwrap(); let updated = sample("e", "after", 768); store.save(&updated).await.unwrap(); let listed = store.list().await.unwrap(); assert_eq!(listed.len(), 1, "upsert must not duplicate by id"); assert_eq!(listed[0], updated); } #[tokio::test] async fn delete_removes_profile() { let tmp = TempDir::new(); let store: &dyn EmbedderProfileStore = &store(&tmp); store.save(&sample("a", "A", 384)).await.unwrap(); store.save(&sample("b", "B", 384)).await.unwrap(); store.delete("a").await.unwrap(); let listed = store.list().await.unwrap(); assert_eq!(listed.len(), 1); assert_eq!(listed[0].id, "b"); } #[tokio::test] async fn delete_unknown_is_not_found() { let tmp = TempDir::new(); let store: &dyn EmbedderProfileStore = &store(&tmp); store.save(&sample("a", "A", 384)).await.unwrap(); let err = store .delete("ghost") .await .expect_err("deleting unknown id fails"); assert!(matches!(err, StoreError::NotFound), "got {err:?}"); } #[tokio::test] async fn embedder_file_is_camelcase_versioned() { let tmp = TempDir::new(); let store: &dyn EmbedderProfileStore = &store(&tmp); store .save( &EmbedderProfile::new( "api-openai", "OpenAI", EmbedderStrategy::Api, Some("text-embedding-3-small".to_owned()), Some("https://api.openai.com/v1/embeddings".to_owned()), Some("OPENAI_API_KEY".to_owned()), 1536, ) .unwrap(), ) .await .unwrap(); let fs = LocalFileSystem::new(); let bytes = fs.read(&tmp.child("embedder.json")).await.unwrap(); let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); assert_eq!(json["version"], 1); let profiles = json .get("profiles") .and_then(|v| v.as_array()) .expect("top-level `profiles` array"); assert_eq!(profiles.len(), 1); let entry = &profiles[0]; assert_eq!(entry["id"], "api-openai"); assert_eq!(entry["strategy"], "api"); // camelCase field, never the secret itself. assert_eq!(entry["apiKeyEnv"], "OPENAI_API_KEY"); assert!(entry.get("api_key_env").is_none(), "no snake_case leak"); assert_eq!(entry["dimension"], 1536); } // --------------------------------------------------------------------------- // EmbedderEnvProbe::inspect — pure-FS cache scan, no network // --------------------------------------------------------------------------- #[tokio::test] async fn probe_empty_cache_reports_nothing() { let tmp = TempDir::new(); // An ONNX cache subdir that exists but is empty ⇒ no cached models. let cache = tmp.path().join(ONNX_CACHE_SUBDIR); std::fs::create_dir_all(&cache).unwrap(); let probe = EmbedderEnvProbe::new(cache, "http://127.0.0.1:1"); // unreachable host let report = probe.inspect().await; assert!(report.onnx_cached_models.is_empty(), "empty cache ⇒ none"); assert!( !report.ollama_detected, "without vector-http (or with an unreachable host) ⇒ never detected" ); } #[tokio::test] async fn probe_missing_cache_dir_is_best_effort_empty() { // A cache dir that does not exist must not panic — best-effort ⇒ empty. let cache = std::env::temp_dir().join(format!("idea-no-such-cache-{}", Uuid::new_v4())); assert!(!cache.exists()); let probe = EmbedderEnvProbe::new(cache, "http://127.0.0.1:1"); let report = probe.inspect().await; assert!(report.onnx_cached_models.is_empty()); assert!(!report.ollama_detected); } #[tokio::test] async fn probe_detects_prepared_onnx_cache() { let tmp = TempDir::new(); let cache = tmp.path().join(ONNX_CACHE_SUBDIR); std::fs::create_dir_all(&cache).unwrap(); // Recreate the hf-hub-style cache layout for the recommended model: // a non-empty subdirectory whose name contains the model token // (`onnx_model_is_cached`: needle = id with `_`/`/`→`-`, lowercased). let model = RECOMMENDED_ONNX_MODELS .iter() .find(|m| m.recommended) .expect("a recommended model exists"); let model_dir = cache.join(format!("models--intfloat--{}", model.id)); std::fs::create_dir_all(&model_dir).unwrap(); // Non-empty: the probe treats an empty dir as "not present". std::fs::write(model_dir.join("model.onnx"), b"stub").unwrap(); let probe = EmbedderEnvProbe::new(cache, "http://127.0.0.1:1"); let report = probe.inspect().await; assert_eq!( report.onnx_cached_models, vec![model.id.to_owned()], "the prepared cached model must be detected" ); assert!(!report.ollama_detected, "no real Ollama required"); } #[test] fn compiled_capability_flags_match_features() { // The consts must mirror the build's features exactly (honest reporting to the UI). assert_eq!(VECTOR_HTTP_ENABLED, cfg!(feature = "vector-http")); assert_eq!(VECTOR_ONNX_ENABLED, cfg!(feature = "vector-onnx")); }