prvng_platform/crates/platform-config/tests/service_nickel_tests.rs
Jesús Pérez 09a97ac8f5
chore: update platform submodule to monorepo crates structure
Platform restructured into crates/, added AI service and detector,
       migrated control-center-ui to Leptos 0.8
2026-01-08 21:32:59 +00:00

422 lines
12 KiB
Rust

//! 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<Self, Box<dyn std::error::Error + Send + Sync>>
{
Ok(Self::default())
}
fn apply_env_overrides(
&mut self,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(())
}
fn from_path<P: AsRef<std::path::Path>>(
path: P,
) -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let path = path.as_ref();
let json_value = platform_config::format::load_config(path).map_err(|e| {
let err: Box<dyn std::error::Error + Send + Sync> = 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<dyn std::error::Error + Send + Sync>
})
}
}
#[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<Self, Box<dyn std::error::Error + Send + Sync>>
{
Ok(Self::default())
}
fn apply_env_overrides(
&mut self,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(())
}
fn from_path<P: AsRef<std::path::Path>>(
path: P,
) -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let path = path.as_ref();
let json_value = platform_config::format::load_config(path).map_err(|e| {
let err: Box<dyn std::error::Error + Send + Sync> = 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<dyn std::error::Error + Send + Sync>
})
}
}
#[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<Self, Box<dyn std::error::Error + Send + Sync>>
{
Ok(Self::default())
}
fn apply_env_overrides(
&mut self,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(())
}
fn from_path<P: AsRef<std::path::Path>>(
path: P,
) -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let path = path.as_ref();
let json_value = platform_config::format::load_config(path).map_err(|e| {
let err: Box<dyn std::error::Error + Send + Sync> = 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<dyn std::error::Error + Send + Sync>
})
}
}
#[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"]);
}