//! Integration tests for loading actual service configurations from Nickel //! files These tests verify that real service configs can be loaded from NCL //! files use platform_config::ConfigLoader; #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] struct McpServerConfig { #[serde(default)] pub name: String, #[serde(default)] pub version: String, #[serde(default)] pub server: ServerConfig, #[serde(default)] pub logging: LoggingConfig, #[serde(default)] pub features: FeaturesConfig, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] struct ServerConfig { #[serde(default)] pub host: String, #[serde(default)] pub port: u16, #[serde(default)] pub workers: u16, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] struct LoggingConfig { #[serde(default)] pub level: String, #[serde(default)] pub format: String, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] struct FeaturesConfig { #[serde(default)] pub cache_enabled: bool, #[serde(default)] pub cache_ttl_seconds: u32, } impl ConfigLoader for McpServerConfig { fn service_name() -> &'static str { "mcp-server" } fn load_from_hierarchy() -> std::result::Result> { Ok(Self::default()) } fn apply_env_overrides( &mut self, ) -> std::result::Result<(), Box> { Ok(()) } fn from_path>( path: P, ) -> std::result::Result> { let path = path.as_ref(); let json_value = platform_config::format::load_config(path).map_err(|e| { let err: Box = Box::new(e); err })?; serde_json::from_value(json_value).map_err(|e| { let err_msg = format!("Failed to deserialize MCP config from {:?}: {}", path, e); Box::new(std::io::Error::new( std::io::ErrorKind::InvalidData, err_msg, )) as Box }) } } #[test] fn test_load_mcp_server_from_nickel() { let ncl_file = "/tmp/mcp-server.solo.ncl"; // Verify the file exists assert!( std::path::Path::new(ncl_file).exists(), "NCL file does not exist: {}", ncl_file ); let config = McpServerConfig::from_path(ncl_file) .expect("Failed to load MCP server config from Nickel file"); assert_eq!(config.name, "mcp-server"); assert_eq!(config.version, "0.1.0"); assert_eq!(config.server.host, "127.0.0.1"); assert_eq!(config.server.port, 8080); assert_eq!(config.server.workers, 4); assert_eq!(config.logging.level, "info"); assert_eq!(config.logging.format, "json"); assert!(config.features.cache_enabled); assert_eq!(config.features.cache_ttl_seconds, 300); } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] struct RagConfig { #[serde(default)] pub enabled: bool, #[serde(default)] pub embeddings: EmbeddingConfig, #[serde(default)] pub vector_db: VectorDbConfig, #[serde(default)] pub llm: LlmConfig, #[serde(default)] pub retrieval: RetrievalConfig, #[serde(default)] pub ingestion: IngestionConfig, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] struct EmbeddingConfig { #[serde(default)] pub provider: String, #[serde(default)] pub model: String, #[serde(default)] pub dimension: u16, #[serde(default)] pub batch_size: u16, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] struct VectorDbConfig { #[serde(default)] pub db_type: String, #[serde(default)] pub url: String, #[serde(default)] pub namespace: String, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] struct LlmConfig { #[serde(default)] pub provider: String, #[serde(default)] pub model: String, #[serde(default)] pub temperature: f64, #[serde(default)] pub max_tokens: u32, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] struct RetrievalConfig { #[serde(default)] pub top_k: u8, #[serde(default)] pub similarity_threshold: f64, #[serde(default)] pub use_reranking: bool, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] struct IngestionConfig { #[serde(default)] pub auto_ingest: bool, #[serde(default)] pub chunk_size: u16, #[serde(default)] pub chunk_overlap: u16, } impl ConfigLoader for RagConfig { fn service_name() -> &'static str { "rag" } fn load_from_hierarchy() -> std::result::Result> { Ok(Self::default()) } fn apply_env_overrides( &mut self, ) -> std::result::Result<(), Box> { Ok(()) } fn from_path>( path: P, ) -> std::result::Result> { let path = path.as_ref(); let json_value = platform_config::format::load_config(path).map_err(|e| { let err: Box = Box::new(e); err })?; serde_json::from_value(json_value).map_err(|e| { let err_msg = format!("Failed to deserialize RAG config from {:?}: {}", path, e); Box::new(std::io::Error::new( std::io::ErrorKind::InvalidData, err_msg, )) as Box }) } } #[test] fn test_load_rag_from_nickel() { let ncl_file = "/tmp/rag.solo.ncl"; assert!( std::path::Path::new(ncl_file).exists(), "NCL file does not exist: {}", ncl_file ); let config = RagConfig::from_path(ncl_file).expect("Failed to load RAG config from Nickel file"); assert!(config.enabled); assert_eq!(config.embeddings.provider, "openai"); assert_eq!(config.embeddings.model, "text-embedding-3-small"); assert_eq!(config.embeddings.dimension, 1536); assert_eq!(config.embeddings.batch_size, 32); assert_eq!(config.vector_db.db_type, "surrealdb"); assert_eq!(config.vector_db.url, "ws://127.0.0.1:8000"); assert_eq!(config.vector_db.namespace, "rag_solo"); assert_eq!(config.llm.provider, "openai"); assert_eq!(config.llm.model, "gpt-4-turbo"); assert!((config.llm.temperature - 0.7).abs() < 0.01); assert_eq!(config.llm.max_tokens, 2048); assert_eq!(config.retrieval.top_k, 5); assert!((config.retrieval.similarity_threshold - 0.7).abs() < 0.01); assert!(config.retrieval.use_reranking); assert!(config.ingestion.auto_ingest); assert_eq!(config.ingestion.chunk_size, 1024); assert_eq!(config.ingestion.chunk_overlap, 100); } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] struct ExtensionRegistryConfig { #[serde(default)] pub server: ExtRegServerConfig, #[serde(default)] pub cache: CacheConfig, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] struct ExtRegServerConfig { #[serde(default)] pub host: String, #[serde(default)] pub port: u16, #[serde(default)] pub workers: u16, #[serde(default)] pub enable_cors: bool, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] struct CacheConfig { #[serde(default)] pub capacity: usize, #[serde(default)] pub ttl_seconds: u64, #[serde(default)] pub enable_metadata_cache: bool, } impl ConfigLoader for ExtensionRegistryConfig { fn service_name() -> &'static str { "extension-registry" } fn load_from_hierarchy() -> std::result::Result> { Ok(Self::default()) } fn apply_env_overrides( &mut self, ) -> std::result::Result<(), Box> { Ok(()) } fn from_path>( path: P, ) -> std::result::Result> { let path = path.as_ref(); let json_value = platform_config::format::load_config(path).map_err(|e| { let err: Box = Box::new(e); err })?; serde_json::from_value(json_value).map_err(|e| { let err_msg = format!( "Failed to deserialize extension-registry config from {:?}: {}", path, e ); Box::new(std::io::Error::new( std::io::ErrorKind::InvalidData, err_msg, )) as Box }) } } #[test] fn test_load_extension_registry_from_nickel() { let ncl_file = "/tmp/extension-registry.solo.ncl"; assert!( std::path::Path::new(ncl_file).exists(), "NCL file does not exist: {}", ncl_file ); let config = ExtensionRegistryConfig::from_path(ncl_file) .expect("Failed to load extension-registry config from Nickel file"); assert_eq!(config.server.host, "0.0.0.0"); assert_eq!(config.server.port, 8082); assert_eq!(config.server.workers, 4); assert!(config.server.enable_cors); assert_eq!(config.cache.capacity, 1000); assert_eq!(config.cache.ttl_seconds, 300); assert!(config.cache.enable_metadata_cache); } #[test] fn test_load_control_center_from_nickel() { let ncl_file = "/tmp/control-center.solo.ncl"; assert!( std::path::Path::new(ncl_file).exists(), "NCL file does not exist: {}", ncl_file ); // Just verify it can be exported and parsed let output = std::process::Command::new("nickel") .arg("export") .arg("--format") .arg("json") .arg(ncl_file) .output() .expect("Failed to run nickel export"); assert!(output.status.success()); let json_str = String::from_utf8(output.stdout).expect("Failed to read JSON output"); let json_value: serde_json::Value = serde_json::from_str(&json_str).expect("Failed to parse JSON"); // Verify key fields exist and have correct values assert_eq!(json_value["server"]["host"].as_str(), Some("127.0.0.1")); assert_eq!(json_value["server"]["port"].as_u64(), Some(8080)); assert_eq!(json_value["auth"]["require_mfa"].as_bool(), Some(false)); assert_eq!(json_value["anomaly"]["enabled"].as_bool(), Some(true)); } #[test] fn test_nickel_vs_toml_equivalence() { // Create equivalent TOML file let toml_file = "/tmp/test-equiv.toml"; let toml_content = r#" name = "test-service" port = 8080 enabled = true "#; std::fs::write(toml_file, toml_content).expect("Failed to write TOML file"); // Create equivalent NCL file let ncl_file = "/tmp/test-equiv.ncl"; let ncl_content = r#" { name = "test-service", port = 8080, enabled = true, } "#; std::fs::write(ncl_file, ncl_content).expect("Failed to write NCL file"); // Export NCL to JSON let output = std::process::Command::new("nickel") .arg("export") .arg("--format") .arg("json") .arg(ncl_file) .output() .expect("Failed to run nickel export"); assert!(output.status.success()); let ncl_json_str = String::from_utf8(output.stdout).expect("Failed to read JSON output"); // Load TOML via platform_config let toml_json_value = platform_config::format::load_config(toml_file).expect("Failed to load TOML config"); // Parse NCL JSON let ncl_json_value: serde_json::Value = serde_json::from_str(&ncl_json_str).expect("Failed to parse NCL JSON"); // Verify equivalent values assert_eq!( ncl_json_value["name"].as_str(), toml_json_value["name"].as_str() ); assert_eq!(ncl_json_value["port"], toml_json_value["port"]); assert_eq!(ncl_json_value["enabled"], toml_json_value["enabled"]); }