Platform restructured into crates/, added AI service and detector,
migrated control-center-ui to Leptos 0.8
422 lines
12 KiB
Rust
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"]);
|
|
}
|