use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Duration;
#[derive(Debug, Default)]
pub struct ConfigBuilder {
nats_url: Option<String>,
http_port: Option<u16>,
executor_work_root: Option<PathBuf>,
log_level: Option<String>,
environment: Option<ConfigEnvironment>,
}
#[derive(Debug, Clone, Copy)]
pub enum ConfigEnvironment {
Development,
Production,
Testing,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_nats_url(mut self, url: impl Into<String>) -> Self {
self.nats_url = Some(url.into());
self
}
pub fn with_http_port(mut self, port: u16) -> Self {
self.http_port = Some(port);
self
}
pub fn with_executor_work_root(mut self, path: impl Into<PathBuf>) -> Self {
self.executor_work_root = Some(path.into());
self
}
pub fn with_log_level(mut self, level: impl Into<String>) -> Self {
self.log_level = Some(level.into());
self
}
pub fn for_environment(mut self, env: ConfigEnvironment) -> Self {
self.environment = Some(env);
self
}
pub fn build(self) -> Config {
let mut config = match self.environment.unwrap_or(ConfigEnvironment::Development) {
ConfigEnvironment::Development => Config::development(),
ConfigEnvironment::Production => Config::production(),
ConfigEnvironment::Testing => Config::testing(),
};
if let Some(url) = self.nats_url {
config.nats.url = url;
}
if let Some(port) = self.http_port {
config.http.port = port;
}
if let Some(path) = self.executor_work_root {
config.executor.work_root = path;
}
if let Some(level) = self.log_level {
config.logging.level = level;
}
config
}
}
pub mod app;
pub mod behavior;
pub mod diff;
pub mod executor;
pub mod http;
pub mod manifest;
pub mod mcp;
pub mod nats;
pub mod nats_adapter;
pub mod observability;
pub mod shell;
pub use behavior::{BehaviorMode, BehaviorPack, BehaviorPackManager, EnabledCapabilities};
pub use diff::{BehaviorPackDiff, DiffSummary, RiskLevel};
pub use executor::{
CgroupLimits, ExecutorConfig, ExecutorNatsConfig, LandlockProfile, PolicyDerivations,
};
pub use http::HttpConfig;
pub use mcp::{McpConfig, McpServerConfig};
pub use nats::NatsConfig;
pub use nats_adapter::{AdapterConfig as NatsAdapterConfig, QueueConfig as NatsQueueConfig};
pub use observability::{
ClickHouseConfig, CollectorConfig, HyperDxConfig, ObservabilityConfig, PerformanceThresholds,
PhoenixConfig, RedactionLevel, SamplingStrategy,
};
pub use shell::{ShellConfig, ShellSpecificConfig};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
pub nats: NatsConfig,
pub nats_adapter: nats_adapter::AdapterConfig,
pub http: HttpConfig,
pub executor: ExecutorConfig,
pub shell: ShellConfig,
pub logging: LoggingConfig,
pub metrics: MetricsConfig,
pub behavior: BehaviorConfig,
pub monitoring: MonitoringConfig,
pub core: CoreConfig,
pub admission: AdmissionConfig,
pub attestation: AttestationConfig,
pub mcp: McpConfig,
pub observability: ObservabilityConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitoringConfig {
pub bind_addr: String,
pub port: u16,
pub chaos_enabled: bool,
pub sla_monitoring_enabled: bool,
pub health_check_interval: u64,
pub metrics_collection_interval: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoreConfig {
pub bind_addr: String,
pub port: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdmissionConfig {
pub bind_addr: String,
pub port: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AttestationConfig {
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String,
pub json_format: bool,
pub log_requests: bool,
pub log_performance: bool,
pub log_file: Option<PathBuf>,
pub nats: NatsLoggingConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NatsLoggingConfig {
pub enabled: bool,
pub buffer_size: usize,
pub max_retries: u32,
pub publish_timeout_ms: u64,
pub target_filters: Vec<String>,
pub level_filter: Option<String>,
pub rate_limit: u64,
pub batch_enabled: bool,
pub batch_size: usize,
pub batch_timeout_ms: u64,
pub include_spans: bool,
pub include_traces: bool,
pub fallback_to_console: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehaviorConfig {
pub config_dir: PathBuf,
pub default_pack: String,
pub poll_interval_seconds: u64,
pub enable_hot_reload: bool,
pub max_file_size_bytes: u64,
}
impl Default for BehaviorConfig {
fn default() -> Self {
Self {
config_dir: PathBuf::from("config/behavior"),
default_pack: "prod-stable".to_string(),
poll_interval_seconds: 5,
enable_hot_reload: true,
max_file_size_bytes: 1024 * 1024, }
}
}
impl Default for MonitoringConfig {
fn default() -> Self {
Self {
bind_addr: "0.0.0.0".to_string(),
port: 8082,
chaos_enabled: false,
sla_monitoring_enabled: true,
health_check_interval: 10,
metrics_collection_interval: 15,
}
}
}
impl Default for CoreConfig {
fn default() -> Self {
Self {
bind_addr: "0.0.0.0".to_string(),
port: 8083,
}
}
}
impl Default for AdmissionConfig {
fn default() -> Self {
Self {
bind_addr: "0.0.0.0".to_string(),
port: 8080,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsConfig {
pub enabled: bool,
pub prefix: String,
pub port: Option<u16>,
pub interval_seconds: u64,
pub labels: HashMap<String, String>,
}
impl Default for NatsLoggingConfig {
fn default() -> Self {
Self {
enabled: false,
buffer_size: 1000,
max_retries: 3,
publish_timeout_ms: 1000,
target_filters: Vec::new(),
level_filter: None,
rate_limit: 0, batch_enabled: true,
batch_size: 50,
batch_timeout_ms: 100,
include_spans: true,
include_traces: false,
fallback_to_console: true,
}
}
}
impl NatsLoggingConfig {
pub fn development() -> Self {
Self {
enabled: true,
buffer_size: 500, max_retries: 3,
publish_timeout_ms: 500,
target_filters: vec![
"smith".to_string(), "executor".to_string(),
"architect".to_string(),
],
level_filter: Some("debug".to_string()),
rate_limit: 0, batch_enabled: false, batch_size: 10,
batch_timeout_ms: 50,
include_spans: true,
include_traces: true, fallback_to_console: true,
}
}
pub fn production() -> Self {
Self {
enabled: true,
buffer_size: 2000, max_retries: 5,
publish_timeout_ms: 2000,
target_filters: vec![
"smith".to_string(),
"executor".to_string(),
"architect".to_string(),
],
level_filter: Some("info".to_string()),
rate_limit: 100, batch_enabled: true, batch_size: 100,
batch_timeout_ms: 200,
include_spans: true,
include_traces: false, fallback_to_console: true,
}
}
pub fn testing() -> Self {
Self {
enabled: false, buffer_size: 100,
max_retries: 1,
publish_timeout_ms: 100,
target_filters: Vec::new(),
level_filter: Some("warn".to_string()),
rate_limit: 0,
batch_enabled: false,
batch_size: 5,
batch_timeout_ms: 10,
include_spans: false,
include_traces: false,
fallback_to_console: true,
}
}
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
json_format: false,
log_requests: true,
log_performance: true,
log_file: None,
nats: NatsLoggingConfig::default(),
}
}
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
enabled: true,
prefix: "smith".to_string(),
port: Some(9090),
interval_seconds: 15,
labels: HashMap::new(),
}
}
}
impl Config {
pub fn builder() -> ConfigBuilder {
ConfigBuilder::new()
}
#[cfg(feature = "env")]
pub fn from_env() -> Result<Self> {
use figment::{
providers::{Env, Format, Serialized, Toml},
Figment,
};
let mut figment = Figment::from(Serialized::defaults(Config::development()));
if let Some(config_path) =
std::env::var_os("SMITH_CONFIG_FILE").or_else(|| std::env::var_os("SMITH_CONFIG_PATH"))
{
let path = PathBuf::from(&config_path);
if !path.exists() {
return Err(anyhow!(
"Configuration file specified by SMITH_CONFIG_FILE does not exist: {}",
path.display()
));
}
figment = figment.merge(Toml::file(path));
} else if Path::new("smith.toml").exists() {
figment = figment.merge(Toml::file("smith.toml")); }
figment = figment.merge(Env::prefixed("SMITH_").split("_"));
figment
.extract()
.context("Failed to load configuration from environment")
}
#[cfg(not(feature = "env"))]
pub fn from_env() -> Result<Self> {
let mut config = Self::default();
Self::apply_all_env_overrides(&mut config)?;
Ok(config)
}
#[allow(dead_code)]
fn apply_all_env_overrides(config: &mut Config) -> Result<()> {
config.apply_nats_env_overrides()?;
config.apply_nats_adapter_env_overrides()?;
config.apply_http_env_overrides()?;
config.apply_executor_env_overrides()?;
config.apply_logging_env_overrides()?;
config.apply_metrics_env_overrides()?;
config.apply_observability_env_overrides()?;
Ok(())
}
pub fn apply_env_overrides(&mut self) -> Result<()> {
Self::apply_all_env_overrides(self)
}
#[allow(dead_code)]
fn apply_nats_env_overrides(&mut self) -> Result<()> {
Self::apply_env_string("SMITH_NATS_URL", &mut self.nats.url);
Self::apply_env_string(
"SMITH_NATS_JETSTREAM_DOMAIN",
&mut self.nats.jetstream_domain,
);
Ok(())
}
fn apply_nats_adapter_env_overrides(&mut self) -> Result<()> {
let security = &mut self.nats_adapter.security;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_REQUIRE_AUTH",
&mut security.require_authentication,
)?;
if let Ok(token) = std::env::var("SMITH_NATS_ADAPTER_AUTH_TOKEN") {
security.auth_token = Some(token);
}
if let Ok(username) = std::env::var("SMITH_NATS_ADAPTER_USERNAME") {
security.username = Some(username);
}
if let Ok(password) = std::env::var("SMITH_NATS_ADAPTER_PASSWORD") {
security.password = Some(password);
}
if let Ok(jwt) = std::env::var("SMITH_NATS_ADAPTER_JWT") {
security.jwt_token = Some(jwt);
}
if let Ok(nkey_seed) = std::env::var("SMITH_NATS_ADAPTER_NKEY_SEED") {
security.nkey_seed = Some(nkey_seed);
}
Self::apply_env_parse("SMITH_NATS_ADAPTER_TLS_ENABLED", &mut security.tls.enabled)?;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_TLS_REQUIRED",
&mut security.tls.required,
)?;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_TLS_SKIP_VERIFY",
&mut security.tls.insecure_skip_verify,
)?;
if let Ok(server_name) = std::env::var("SMITH_NATS_ADAPTER_TLS_SERVER_NAME") {
security.tls.server_name = Some(server_name);
}
if let Ok(allowed_ips) = std::env::var("SMITH_NATS_ADAPTER_ALLOWED_IPS") {
security.allowed_ips = allowed_ips
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
let topics = &mut self.nats_adapter.topics;
if let Ok(prefix) = std::env::var("SMITH_NATS_ADAPTER_TOPIC_PREFIX") {
topics.prefix = prefix;
}
if let Ok(command) = std::env::var("SMITH_NATS_ADAPTER_COMMAND_SUBJECT") {
topics.command_subject = command;
}
if let Ok(event) = std::env::var("SMITH_NATS_ADAPTER_EVENT_SUBJECT") {
topics.event_subject = event;
}
if let Ok(patterns) = std::env::var("SMITH_NATS_ADAPTER_ALLOWED_PATTERNS") {
let values = patterns
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
topics.allowed_patterns = values;
}
let queues = &mut self.nats_adapter.queues;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_COMMAND_QUEUE_SIZE",
&mut queues.command_queue_size,
)?;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_EVENT_QUEUE_SIZE",
&mut queues.event_queue_size,
)?;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_PROCESSING_QUEUE_SIZE",
&mut queues.processing_queue_size,
)?;
let performance = &mut self.nats_adapter.performance;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_MAX_MESSAGES_PER_SECOND",
&mut performance.max_messages_per_second,
)?;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_TARGET_LATENCY_MS",
&mut performance.target_latency_ms,
)?;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_MAX_MESSAGE_SIZE",
&mut performance.max_message_size,
)?;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_CONNECTION_POOL_SIZE",
&mut performance.connection_pool_size,
)?;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_ENABLE_COMPRESSION",
&mut performance.enable_compression,
)?;
Self::apply_env_parse("SMITH_NATS_ADAPTER_BATCH_SIZE", &mut performance.batch_size)?;
if let Ok(flush_interval) = std::env::var("SMITH_NATS_ADAPTER_FLUSH_INTERVAL_MS") {
let millis: u64 = flush_interval
.parse()
.context("Invalid SMITH_NATS_ADAPTER_FLUSH_INTERVAL_MS value")?;
performance.flush_interval = Duration::from_millis(millis);
}
if let Ok(subject_allow) = std::env::var("SMITH_NATS_ADAPTER_SUBJECT_ALLOW") {
let values: Vec<String> = subject_allow
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if !values.is_empty() {
security.subject_permissions.publish_allow = values.clone().into_iter().collect();
security.subject_permissions.subscribe_allow = values.into_iter().collect();
}
}
let rate_limits = &mut security.rate_limits;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_RATE_MESSAGES_PER_SECOND",
&mut rate_limits.messages_per_second,
)?;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_RATE_BYTES_PER_SECOND",
&mut rate_limits.bytes_per_second,
)?;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_RATE_MAX_SUBSCRIPTIONS",
&mut rate_limits.max_subscriptions,
)?;
Self::apply_env_parse(
"SMITH_NATS_ADAPTER_RATE_MAX_PAYLOAD",
&mut rate_limits.max_payload_size,
)?;
Ok(())
}
#[allow(dead_code)]
fn apply_http_env_overrides(&mut self) -> Result<()> {
Self::apply_env_parse("SMITH_HTTP_PORT", &mut self.http.port)?;
Self::apply_env_string("SMITH_HTTP_BIND", &mut self.http.bind_address);
Ok(())
}
#[allow(dead_code)]
fn apply_executor_env_overrides(&mut self) -> Result<()> {
if let Ok(work_root) = std::env::var("SMITH_EXECUTOR_WORK_ROOT") {
self.executor.work_root = PathBuf::from(work_root);
}
Self::apply_env_string("SMITH_EXECUTOR_NODE_NAME", &mut self.executor.node_name);
Ok(())
}
#[allow(dead_code)]
fn apply_logging_env_overrides(&mut self) -> Result<()> {
Self::apply_env_string("SMITH_LOG_LEVEL", &mut self.logging.level);
Self::apply_env_parse("SMITH_LOG_JSON", &mut self.logging.json_format)?;
Self::apply_env_parse("SMITH_LOG_NATS_ENABLED", &mut self.logging.nats.enabled)?;
Self::apply_env_parse(
"SMITH_LOG_NATS_BUFFER_SIZE",
&mut self.logging.nats.buffer_size,
)?;
Self::apply_env_parse(
"SMITH_LOG_NATS_MAX_RETRIES",
&mut self.logging.nats.max_retries,
)?;
Self::apply_env_parse(
"SMITH_LOG_NATS_TIMEOUT",
&mut self.logging.nats.publish_timeout_ms,
)?;
Self::apply_env_parse(
"SMITH_LOG_NATS_RATE_LIMIT",
&mut self.logging.nats.rate_limit,
)?;
Self::apply_env_parse(
"SMITH_LOG_NATS_BATCH_ENABLED",
&mut self.logging.nats.batch_enabled,
)?;
Self::apply_env_parse(
"SMITH_LOG_NATS_BATCH_SIZE",
&mut self.logging.nats.batch_size,
)?;
Self::apply_env_parse(
"SMITH_LOG_NATS_INCLUDE_SPANS",
&mut self.logging.nats.include_spans,
)?;
Self::apply_env_parse(
"SMITH_LOG_NATS_INCLUDE_TRACES",
&mut self.logging.nats.include_traces,
)?;
Self::apply_env_parse(
"SMITH_LOG_NATS_FALLBACK_CONSOLE",
&mut self.logging.nats.fallback_to_console,
)?;
if let Ok(level_filter) = std::env::var("SMITH_LOG_NATS_LEVEL_FILTER") {
self.logging.nats.level_filter = Some(level_filter);
}
if let Ok(filters) = std::env::var("SMITH_LOG_NATS_TARGET_FILTERS") {
self.logging.nats.target_filters = filters
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
Ok(())
}
#[allow(dead_code)]
fn apply_metrics_env_overrides(&mut self) -> Result<()> {
Self::apply_env_parse("SMITH_METRICS_ENABLED", &mut self.metrics.enabled)?;
if let Ok(port_str) = std::env::var("SMITH_METRICS_PORT") {
let port: u16 = port_str
.parse()
.context("Invalid SMITH_METRICS_PORT value")?;
self.metrics.port = Some(port);
}
Ok(())
}
#[allow(dead_code)]
fn apply_observability_env_overrides(&mut self) -> Result<()> {
if let Ok(enabled) = std::env::var("OBSERVABILITY_ENABLED") {
self.observability.enabled = enabled
.parse()
.context("Invalid OBSERVABILITY_ENABLED value")?;
}
if let Ok(redaction) = std::env::var("OBS_REDACTION_LEVEL") {
self.observability.redaction_level = Self::parse_redaction_level(&redaction)?;
}
if let Ok(service_name) = std::env::var("SMITH_OBSERVABILITY_SERVICE_NAME") {
self.observability.service_name = service_name;
}
if let Ok(service_version) = std::env::var("SMITH_OBSERVABILITY_SERVICE_VERSION") {
self.observability.service_version = service_version;
}
if let Ok(env) = std::env::var("SMITH_OBSERVABILITY_ENVIRONMENT") {
self.observability.deployment_environment = env;
}
Ok(())
}
#[allow(dead_code)]
fn parse_redaction_level(value: &str) -> Result<RedactionLevel> {
match value {
"strict" => Ok(RedactionLevel::Strict),
"balanced" => Ok(RedactionLevel::Balanced),
"permissive" => Ok(RedactionLevel::Permissive),
_ => Err(anyhow::anyhow!(
"Invalid OBS_REDACTION_LEVEL: must be 'strict', 'balanced', or 'permissive'"
)),
}
}
#[allow(dead_code)]
fn apply_env_string(var_name: &str, target: &mut String) {
if let Ok(value) = std::env::var(var_name) {
*target = value;
}
}
#[allow(dead_code)]
fn apply_env_parse<T>(var_name: &str, target: &mut T) -> Result<()>
where
T: std::str::FromStr,
T::Err: std::fmt::Display + Send + Sync + std::error::Error + 'static,
{
if let Ok(value) = std::env::var(var_name) {
*target = value
.parse()
.with_context(|| format!("Invalid {} value", var_name))?;
}
Ok(())
}
fn validate_port(port: u16, service_name: &str) -> Result<()> {
if port < 1024 {
return Err(anyhow::anyhow!(
"Invalid {} port: {}. Must be between 1024 and 65535",
service_name,
port
));
}
Ok(())
}
fn development_bind_addr() -> String {
"127.0.0.1".to_string()
}
fn production_bind_addr() -> String {
"0.0.0.0".to_string()
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path.as_ref())
.with_context(|| format!("Failed to read config file: {}", path.as_ref().display()))?;
let config: Config = toml::from_str(&content)
.with_context(|| format!("Failed to parse TOML config: {}", path.as_ref().display()))?;
config.validate()?;
Ok(config)
}
pub fn validate(&self) -> Result<()> {
Self::validate_core_services(self)?;
Self::validate_platform_services(self)?;
Self::validate_system_configs(self)?;
Ok(())
}
fn validate_core_services(config: &Config) -> Result<()> {
config
.nats
.validate()
.context("NATS configuration validation failed")?;
config
.http
.validate()
.context("HTTP configuration validation failed")?;
config
.executor
.validate()
.context("Executor configuration validation failed")?;
config
.shell
.validate()
.context("Shell configuration validation failed")?;
Ok(())
}
fn validate_platform_services(config: &Config) -> Result<()> {
config
.nats_adapter
.validate()
.context("NATS adapter configuration validation failed")?;
config
.monitoring
.validate()
.context("Monitoring configuration validation failed")?;
config
.core
.validate()
.context("Core configuration validation failed")?;
config
.admission
.validate()
.context("Admission configuration validation failed")?;
config
.observability
.validate()
.context("Observability configuration validation failed")?;
Ok(())
}
fn validate_system_configs(config: &Config) -> Result<()> {
config
.logging
.validate()
.context("Logging configuration validation failed")?;
config
.metrics
.validate()
.context("Metrics configuration validation failed")?;
config
.behavior
.validate()
.context("Behavior configuration validation failed")?;
Ok(())
}
pub fn development() -> Self {
Self::create_environment_config(ConfigEnvironment::Development)
}
pub fn production() -> Self {
Self::create_environment_config(ConfigEnvironment::Production)
}
pub fn testing() -> Self {
Self::create_environment_config(ConfigEnvironment::Testing)
}
fn create_environment_config(env: ConfigEnvironment) -> Self {
match env {
ConfigEnvironment::Development => Self {
nats: NatsConfig::development(),
nats_adapter: nats_adapter::AdapterConfig::development(),
http: HttpConfig::development(),
executor: ExecutorConfig::development(),
shell: ShellConfig::development(),
logging: LoggingConfig::development(),
metrics: MetricsConfig::development(),
behavior: BehaviorConfig::development(),
monitoring: MonitoringConfig::development(),
core: CoreConfig::development(),
admission: AdmissionConfig::development(),
attestation: AttestationConfig::development(),
mcp: McpConfig::development(),
observability: ObservabilityConfig::development(),
},
ConfigEnvironment::Production => Self {
nats: NatsConfig::production(),
nats_adapter: nats_adapter::AdapterConfig::production(),
http: HttpConfig::production(),
executor: ExecutorConfig::production(),
shell: ShellConfig::production(),
logging: LoggingConfig::production(),
metrics: MetricsConfig::production(),
behavior: BehaviorConfig::production(),
monitoring: MonitoringConfig::production(),
core: CoreConfig::production(),
admission: AdmissionConfig::production(),
attestation: AttestationConfig::production(),
mcp: McpConfig::production(),
observability: ObservabilityConfig::production(),
},
ConfigEnvironment::Testing => Self {
nats: NatsConfig::testing(),
nats_adapter: nats_adapter::AdapterConfig::testing(),
http: HttpConfig::testing(),
executor: ExecutorConfig::testing(),
shell: ShellConfig::testing(),
logging: LoggingConfig::testing(),
metrics: MetricsConfig::testing(),
behavior: BehaviorConfig::testing(),
monitoring: MonitoringConfig::testing(),
core: CoreConfig::testing(),
admission: AdmissionConfig::testing(),
attestation: AttestationConfig::testing(),
mcp: McpConfig::default(), observability: ObservabilityConfig::testing(),
},
}
}
}
impl LoggingConfig {
pub fn validate(&self) -> Result<()> {
let valid_levels = ["error", "warn", "info", "debug", "trace"];
if !valid_levels.contains(&self.level.as_str()) {
return Err(anyhow::anyhow!(
"Invalid log level: {}. Must be one of: {}",
self.level,
valid_levels.join(", ")
));
}
if let Some(ref log_file) = self.log_file {
if let Some(parent) = log_file.parent() {
if !parent.exists() {
return Err(anyhow::anyhow!(
"Log file parent directory does not exist: {}",
parent.display()
));
}
}
}
Ok(())
}
pub fn development() -> Self {
Self {
level: "debug".to_string(),
json_format: false,
log_requests: true,
log_performance: true,
log_file: None,
nats: NatsLoggingConfig::development(),
}
}
pub fn production() -> Self {
Self {
level: "info".to_string(),
json_format: true,
log_requests: false, log_performance: true,
log_file: Some(PathBuf::from("/var/log/smith/smith.log")),
nats: NatsLoggingConfig::production(),
}
}
pub fn testing() -> Self {
Self {
level: "warn".to_string(), json_format: false,
log_requests: false,
log_performance: false,
log_file: None,
nats: NatsLoggingConfig::testing(),
}
}
}
impl MetricsConfig {
pub fn validate(&self) -> Result<()> {
if self.prefix.is_empty() {
return Err(anyhow::anyhow!("Metrics prefix cannot be empty"));
}
if let Some(port) = self.port {
Config::validate_port(port, "metrics")?;
}
if self.interval_seconds == 0 {
return Err(anyhow::anyhow!("Metrics interval cannot be zero"));
}
if self.interval_seconds > 300 {
tracing::warn!("Metrics interval > 5 minutes may not provide adequate observability");
}
Ok(())
}
pub fn development() -> Self {
Self {
enabled: true,
prefix: "smith_dev".to_string(),
port: Some(9090),
interval_seconds: 5, labels: [("env".to_string(), "development".to_string())]
.into_iter()
.collect(),
}
}
pub fn production() -> Self {
Self {
enabled: true,
prefix: "smith".to_string(),
port: Some(9090),
interval_seconds: 15,
labels: [("env".to_string(), "production".to_string())]
.into_iter()
.collect(),
}
}
pub fn testing() -> Self {
Self {
enabled: false, prefix: "smith_test".to_string(),
port: None,
interval_seconds: 60,
labels: HashMap::new(),
}
}
}
impl BehaviorConfig {
pub fn validate(&self) -> Result<()> {
if self.default_pack.is_empty() {
return Err(anyhow::anyhow!(
"Default behavior pack name cannot be empty"
));
}
if self.poll_interval_seconds == 0 {
return Err(anyhow::anyhow!("Poll interval cannot be zero"));
}
if self.poll_interval_seconds > 300 {
tracing::warn!("Poll interval > 5 minutes may cause slow behavior pack updates");
}
if self.max_file_size_bytes == 0 {
return Err(anyhow::anyhow!("Maximum file size cannot be zero"));
}
if self.max_file_size_bytes > 10 * 1024 * 1024 {
tracing::warn!(
"Large maximum file size ({}MB) for behavior packs",
self.max_file_size_bytes / (1024 * 1024)
);
}
Ok(())
}
pub fn development() -> Self {
Self {
config_dir: PathBuf::from("config/behavior"),
default_pack: "eng-alpha".to_string(), poll_interval_seconds: 2, enable_hot_reload: true,
max_file_size_bytes: 1024 * 1024,
}
}
pub fn production() -> Self {
Self {
config_dir: PathBuf::from("config/behavior"),
default_pack: "prod-stable".to_string(),
poll_interval_seconds: 30, enable_hot_reload: false, max_file_size_bytes: 512 * 1024, }
}
pub fn testing() -> Self {
Self {
config_dir: PathBuf::from("config/behavior"),
default_pack: "shadow-test".to_string(), poll_interval_seconds: 60, enable_hot_reload: false,
max_file_size_bytes: 256 * 1024,
}
}
}
impl MonitoringConfig {
pub fn validate(&self) -> Result<()> {
Config::validate_port(self.port, "monitoring")?;
if self.health_check_interval == 0 {
return Err(anyhow::anyhow!("Health check interval cannot be zero"));
}
if self.metrics_collection_interval == 0 {
return Err(anyhow::anyhow!(
"Metrics collection interval cannot be zero"
));
}
Ok(())
}
pub fn development() -> Self {
Self {
bind_addr: Config::development_bind_addr(),
port: 8082,
chaos_enabled: true, sla_monitoring_enabled: true,
health_check_interval: 5, metrics_collection_interval: 10,
}
}
pub fn production() -> Self {
Self {
bind_addr: Config::production_bind_addr(),
port: 8082,
chaos_enabled: false, sla_monitoring_enabled: true,
health_check_interval: 15,
metrics_collection_interval: 30,
}
}
pub fn testing() -> Self {
Self {
bind_addr: Config::development_bind_addr(), port: 8082,
chaos_enabled: false,
sla_monitoring_enabled: false, health_check_interval: 60,
metrics_collection_interval: 60,
}
}
}
impl CoreConfig {
pub fn validate(&self) -> Result<()> {
Config::validate_port(self.port, "core service")?;
Ok(())
}
pub fn development() -> Self {
Self {
bind_addr: Config::development_bind_addr(),
port: 8083,
}
}
pub fn production() -> Self {
Self {
bind_addr: Config::production_bind_addr(),
port: 8083,
}
}
pub fn testing() -> Self {
Self {
bind_addr: Config::development_bind_addr(), port: 8083,
}
}
}
impl AdmissionConfig {
pub fn validate(&self) -> Result<()> {
Config::validate_port(self.port, "admission service")?;
Ok(())
}
pub fn development() -> Self {
Self {
bind_addr: Config::development_bind_addr(),
port: 8080,
}
}
pub fn production() -> Self {
Self {
bind_addr: Config::production_bind_addr(),
port: 8080,
}
}
pub fn testing() -> Self {
Self {
bind_addr: Config::development_bind_addr(), port: 8080,
}
}
}
impl AttestationConfig {
pub fn validate(&self) -> Result<()> {
Ok(())
}
pub fn development() -> Self {
Self {
enabled: false, }
}
pub fn production() -> Self {
Self {
enabled: true, }
}
pub fn testing() -> Self {
Self {
enabled: false, }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::{tempdir, NamedTempFile};
#[test]
fn test_default_config() {
let _config = Config::default();
}
#[test]
fn test_environment_profiles() {
let dev_config = Config::development();
let prod_config = Config::production();
let test_config = Config::testing();
assert_eq!(dev_config.logging.level, "debug");
assert_eq!(prod_config.logging.level, "info");
assert_eq!(test_config.logging.level, "warn");
}
#[test]
fn test_logging_validation() {
let mut config = LoggingConfig::default();
assert!(config.validate().is_ok());
config.level = "invalid".to_string();
assert!(config.validate().is_err());
}
#[test]
fn test_metrics_validation() {
let mut config = MetricsConfig::default();
assert!(config.validate().is_ok());
config.prefix = "".to_string();
assert!(config.validate().is_err());
config.prefix = "smith".to_string();
config.port = Some(80); assert!(config.validate().is_err());
}
#[test]
fn test_config_builder() {
let config = Config::builder()
.with_nats_url("nats://test:4222")
.with_http_port(8080)
.with_log_level("debug")
.for_environment(ConfigEnvironment::Development)
.build();
assert_eq!(config.nats.url, "nats://test:4222");
assert_eq!(config.http.port, 8080);
assert_eq!(config.logging.level, "debug");
}
#[test]
fn test_port_validation() {
assert!(Config::validate_port(8080, "test").is_ok());
assert!(Config::validate_port(80, "test").is_err());
assert!(Config::validate_port(1023, "test").is_err());
}
#[test]
fn test_bind_address_helpers() {
assert_eq!(Config::development_bind_addr(), "127.0.0.1");
assert_eq!(Config::production_bind_addr(), "0.0.0.0");
}
#[test]
fn test_redaction_level_parsing() {
assert!(matches!(
Config::parse_redaction_level("strict"),
Ok(RedactionLevel::Strict)
));
assert!(matches!(
Config::parse_redaction_level("balanced"),
Ok(RedactionLevel::Balanced)
));
assert!(matches!(
Config::parse_redaction_level("permissive"),
Ok(RedactionLevel::Permissive)
));
assert!(Config::parse_redaction_level("invalid").is_err());
}
#[test]
fn test_structured_validation() {
let config = Config::development();
assert!(config.validate().is_ok() || config.validate().is_err()); }
#[test]
fn test_config_builder_comprehensive() {
let default_config = ConfigBuilder::new().build();
assert_eq!(default_config.logging.level, "debug");
let config = ConfigBuilder::new()
.with_nats_url("nats://custom:4222")
.with_http_port(9999)
.with_executor_work_root("/custom/path")
.with_log_level("trace")
.for_environment(ConfigEnvironment::Production)
.build();
assert_eq!(config.nats.url, "nats://custom:4222");
assert_eq!(config.http.port, 9999);
assert_eq!(config.executor.work_root, PathBuf::from("/custom/path"));
assert_eq!(config.logging.level, "trace");
let dev_config = ConfigBuilder::new()
.for_environment(ConfigEnvironment::Development)
.build();
assert_eq!(dev_config.logging.level, "debug");
let prod_config = ConfigBuilder::new()
.for_environment(ConfigEnvironment::Production)
.build();
assert_eq!(prod_config.logging.level, "info");
let test_config = ConfigBuilder::new()
.for_environment(ConfigEnvironment::Testing)
.build();
assert_eq!(test_config.logging.level, "warn");
}
#[test]
fn test_config_environment_variations() {
let environments = [
ConfigEnvironment::Development,
ConfigEnvironment::Production,
ConfigEnvironment::Testing,
];
for env in environments {
let config = Config::create_environment_config(env);
assert!(!config.nats.url.is_empty());
match env {
ConfigEnvironment::Testing => {
assert_eq!(
config.http.port, 0,
"Testing environment should use OS-assigned port"
);
}
_ => {
assert!(
config.http.port > 0,
"Port is {} for environment {:?}",
config.http.port,
env
);
}
}
assert!(!config.logging.level.is_empty());
match env {
ConfigEnvironment::Development => {
assert_eq!(config.logging.level, "debug");
assert!(!config.logging.json_format);
assert!(config.behavior.enable_hot_reload);
assert_eq!(config.behavior.poll_interval_seconds, 2);
}
ConfigEnvironment::Production => {
assert_eq!(config.logging.level, "info");
assert!(config.logging.json_format);
assert!(!config.behavior.enable_hot_reload);
assert_eq!(config.behavior.poll_interval_seconds, 30);
}
ConfigEnvironment::Testing => {
assert_eq!(config.logging.level, "warn");
assert!(!config.logging.json_format);
assert!(!config.behavior.enable_hot_reload);
assert_eq!(config.behavior.poll_interval_seconds, 60);
}
}
}
}
#[test]
fn test_logging_config_comprehensive() {
let dev_config = LoggingConfig::development();
assert_eq!(dev_config.level, "debug");
assert!(!dev_config.json_format);
assert!(dev_config.log_requests);
assert!(dev_config.log_performance);
assert!(dev_config.log_file.is_none());
let prod_config = LoggingConfig::production();
assert_eq!(prod_config.level, "info");
assert!(prod_config.json_format);
assert!(!prod_config.log_requests); assert!(prod_config.log_performance);
assert!(prod_config.log_file.is_some());
let test_config = LoggingConfig::testing();
assert_eq!(test_config.level, "warn");
assert!(!test_config.json_format);
assert!(!test_config.log_requests);
assert!(!test_config.log_performance);
assert!(test_config.log_file.is_none());
}
#[test]
fn test_logging_validation_comprehensive() {
let mut config = LoggingConfig::default();
let valid_levels = ["error", "warn", "info", "debug", "trace"];
for level in &valid_levels {
config.level = level.to_string();
assert!(config.validate().is_ok(), "Level {} should be valid", level);
}
let invalid_levels = ["INVALID", "warning", "ERROR", "DEBUG", "verbose", ""];
for level in &invalid_levels {
config.level = level.to_string();
assert!(
config.validate().is_err(),
"Level {} should be invalid",
level
);
}
config.level = "info".to_string();
config.log_file = Some(PathBuf::from("/nonexistent/directory/log.txt"));
assert!(config.validate().is_err());
let temp_dir = tempdir().unwrap();
let log_file = temp_dir.path().join("test.log");
config.log_file = Some(log_file);
assert!(config.validate().is_ok());
}
#[test]
fn test_nats_logging_config_comprehensive() {
let dev_config = NatsLoggingConfig::development();
assert!(dev_config.enabled);
assert_eq!(dev_config.buffer_size, 500);
assert_eq!(dev_config.level_filter, Some("debug".to_string()));
assert!(!dev_config.batch_enabled); assert!(dev_config.include_traces);
let prod_config = NatsLoggingConfig::production();
assert!(prod_config.enabled);
assert_eq!(prod_config.buffer_size, 2000);
assert_eq!(prod_config.level_filter, Some("info".to_string()));
assert_eq!(prod_config.rate_limit, 100);
assert!(prod_config.batch_enabled);
assert!(!prod_config.include_traces);
let test_config = NatsLoggingConfig::testing();
assert!(!test_config.enabled); assert_eq!(test_config.level_filter, Some("warn".to_string()));
assert!(!test_config.batch_enabled);
assert!(!test_config.include_spans);
assert!(!test_config.include_traces);
let default_config = NatsLoggingConfig::default();
assert!(!default_config.enabled);
assert_eq!(default_config.buffer_size, 1000);
assert_eq!(default_config.max_retries, 3);
assert_eq!(default_config.publish_timeout_ms, 1000);
assert!(default_config.target_filters.is_empty());
assert_eq!(default_config.level_filter, None);
assert_eq!(default_config.rate_limit, 0);
assert!(default_config.batch_enabled);
assert_eq!(default_config.batch_size, 50);
assert_eq!(default_config.batch_timeout_ms, 100);
assert!(default_config.include_spans);
assert!(!default_config.include_traces);
assert!(default_config.fallback_to_console);
}
#[test]
fn test_metrics_config_comprehensive() {
let dev_config = MetricsConfig::development();
assert!(dev_config.enabled);
assert_eq!(dev_config.prefix, "smith_dev");
assert_eq!(dev_config.port, Some(9090));
assert_eq!(dev_config.interval_seconds, 5);
assert_eq!(
dev_config.labels.get("env"),
Some(&"development".to_string())
);
let prod_config = MetricsConfig::production();
assert!(prod_config.enabled);
assert_eq!(prod_config.prefix, "smith");
assert_eq!(prod_config.port, Some(9090));
assert_eq!(prod_config.interval_seconds, 15);
assert_eq!(
prod_config.labels.get("env"),
Some(&"production".to_string())
);
let test_config = MetricsConfig::testing();
assert!(!test_config.enabled); assert_eq!(test_config.prefix, "smith_test");
assert_eq!(test_config.port, None);
assert_eq!(test_config.interval_seconds, 60);
assert!(test_config.labels.is_empty());
}
#[test]
fn test_metrics_validation_comprehensive() {
let mut config = MetricsConfig::default();
assert!(config.validate().is_ok());
config.prefix = "".to_string();
assert!(config.validate().is_err());
config.prefix = "valid_prefix".to_string();
assert!(config.validate().is_ok());
config.port = Some(0);
assert!(config.validate().is_err());
config.port = Some(1023);
assert!(config.validate().is_err());
config.port = Some(1024);
assert!(config.validate().is_ok());
config.port = Some(65535);
assert!(config.validate().is_ok());
config.port = None;
assert!(config.validate().is_ok());
config.interval_seconds = 0;
assert!(config.validate().is_err());
config.interval_seconds = 1;
assert!(config.validate().is_ok());
config.interval_seconds = 300;
assert!(config.validate().is_ok());
config.interval_seconds = 301;
assert!(config.validate().is_ok());
}
#[test]
fn test_behavior_config_comprehensive() {
let dev_config = BehaviorConfig::development();
assert_eq!(dev_config.default_pack, "eng-alpha");
assert_eq!(dev_config.poll_interval_seconds, 2);
assert!(dev_config.enable_hot_reload);
assert_eq!(dev_config.max_file_size_bytes, 1024 * 1024);
let prod_config = BehaviorConfig::production();
assert_eq!(prod_config.default_pack, "prod-stable");
assert_eq!(prod_config.poll_interval_seconds, 30);
assert!(!prod_config.enable_hot_reload);
assert_eq!(prod_config.max_file_size_bytes, 512 * 1024);
let test_config = BehaviorConfig::testing();
assert_eq!(test_config.default_pack, "shadow-test");
assert_eq!(test_config.poll_interval_seconds, 60);
assert!(!test_config.enable_hot_reload);
assert_eq!(test_config.max_file_size_bytes, 256 * 1024);
let default_config = BehaviorConfig::default();
assert_eq!(default_config.default_pack, "prod-stable");
assert_eq!(default_config.poll_interval_seconds, 5);
assert!(default_config.enable_hot_reload);
assert_eq!(default_config.max_file_size_bytes, 1024 * 1024);
}
#[test]
fn test_behavior_validation_comprehensive() {
let mut config = BehaviorConfig::default();
assert!(config.validate().is_ok());
config.default_pack = "".to_string();
assert!(config.validate().is_err());
config.default_pack = "valid-pack".to_string();
assert!(config.validate().is_ok());
config.poll_interval_seconds = 0;
assert!(config.validate().is_err());
config.poll_interval_seconds = 1;
assert!(config.validate().is_ok());
config.poll_interval_seconds = 300;
assert!(config.validate().is_ok());
config.poll_interval_seconds = 301;
assert!(config.validate().is_ok());
config.max_file_size_bytes = 0;
assert!(config.validate().is_err());
config.max_file_size_bytes = 1024;
assert!(config.validate().is_ok());
config.max_file_size_bytes = 10 * 1024 * 1024;
assert!(config.validate().is_ok());
config.max_file_size_bytes = 11 * 1024 * 1024;
assert!(config.validate().is_ok());
}
#[test]
fn test_monitoring_config_comprehensive() {
let dev_config = MonitoringConfig::development();
assert_eq!(dev_config.bind_addr, "127.0.0.1");
assert_eq!(dev_config.port, 8082);
assert!(dev_config.chaos_enabled);
assert!(dev_config.sla_monitoring_enabled);
assert_eq!(dev_config.health_check_interval, 5);
assert_eq!(dev_config.metrics_collection_interval, 10);
let prod_config = MonitoringConfig::production();
assert_eq!(prod_config.bind_addr, "0.0.0.0");
assert_eq!(prod_config.port, 8082);
assert!(!prod_config.chaos_enabled);
assert!(prod_config.sla_monitoring_enabled);
assert_eq!(prod_config.health_check_interval, 15);
assert_eq!(prod_config.metrics_collection_interval, 30);
let test_config = MonitoringConfig::testing();
assert_eq!(test_config.bind_addr, "127.0.0.1");
assert_eq!(test_config.port, 8082);
assert!(!test_config.chaos_enabled);
assert!(!test_config.sla_monitoring_enabled);
assert_eq!(test_config.health_check_interval, 60);
assert_eq!(test_config.metrics_collection_interval, 60);
}
#[test]
fn test_monitoring_validation_comprehensive() {
let mut config = MonitoringConfig::default();
assert!(config.validate().is_ok());
config.port = 1023;
assert!(config.validate().is_err());
config.port = 8082;
assert!(config.validate().is_ok());
config.health_check_interval = 0;
assert!(config.validate().is_err());
config.health_check_interval = 1;
assert!(config.validate().is_ok());
config.metrics_collection_interval = 0;
assert!(config.validate().is_err());
config.metrics_collection_interval = 1;
assert!(config.validate().is_ok());
}
#[test]
fn test_core_config_comprehensive() {
let dev_config = CoreConfig::development();
assert_eq!(dev_config.bind_addr, "127.0.0.1");
assert_eq!(dev_config.port, 8083);
let prod_config = CoreConfig::production();
assert_eq!(prod_config.bind_addr, "0.0.0.0");
assert_eq!(prod_config.port, 8083);
let test_config = CoreConfig::testing();
assert_eq!(test_config.bind_addr, "127.0.0.1");
assert_eq!(test_config.port, 8083);
assert!(dev_config.validate().is_ok());
assert!(prod_config.validate().is_ok());
assert!(test_config.validate().is_ok());
}
#[test]
fn test_admission_config_comprehensive() {
let dev_config = AdmissionConfig::development();
assert_eq!(dev_config.bind_addr, "127.0.0.1");
assert_eq!(dev_config.port, 8080);
let prod_config = AdmissionConfig::production();
assert_eq!(prod_config.bind_addr, "0.0.0.0");
assert_eq!(prod_config.port, 8080);
let test_config = AdmissionConfig::testing();
assert_eq!(test_config.bind_addr, "127.0.0.1");
assert_eq!(test_config.port, 8080);
assert!(dev_config.validate().is_ok());
assert!(prod_config.validate().is_ok());
assert!(test_config.validate().is_ok());
}
#[test]
fn test_attestation_config_comprehensive() {
let dev_config = AttestationConfig::development();
assert!(!dev_config.enabled);
let prod_config = AttestationConfig::production();
assert!(prod_config.enabled);
let test_config = AttestationConfig::testing();
assert!(!test_config.enabled);
assert!(dev_config.validate().is_ok());
assert!(prod_config.validate().is_ok());
assert!(test_config.validate().is_ok());
let default_config = AttestationConfig::default();
assert!(!default_config.enabled);
assert!(default_config.validate().is_ok());
}
#[test]
fn test_port_validation_comprehensive() {
assert!(Config::validate_port(1024, "test").is_ok()); assert!(Config::validate_port(65535, "test").is_ok());
assert!(Config::validate_port(0, "test").is_err());
assert!(Config::validate_port(1, "test").is_err());
assert!(Config::validate_port(80, "test").is_err());
assert!(Config::validate_port(443, "test").is_err());
assert!(Config::validate_port(1023, "test").is_err());
let valid_ports = [1024, 3000, 8080, 8443, 9090, 65535];
for port in &valid_ports {
assert!(Config::validate_port(*port, "test").is_ok());
}
}
#[test]
fn test_redaction_level_parsing_comprehensive() {
assert!(matches!(
Config::parse_redaction_level("strict"),
Ok(RedactionLevel::Strict)
));
assert!(matches!(
Config::parse_redaction_level("balanced"),
Ok(RedactionLevel::Balanced)
));
assert!(matches!(
Config::parse_redaction_level("permissive"),
Ok(RedactionLevel::Permissive)
));
assert!(Config::parse_redaction_level("Strict").is_err());
assert!(Config::parse_redaction_level("STRICT").is_err());
assert!(Config::parse_redaction_level("Balanced").is_err());
assert!(Config::parse_redaction_level("BALANCED").is_err());
assert!(Config::parse_redaction_level("Permissive").is_err());
assert!(Config::parse_redaction_level("PERMISSIVE").is_err());
let invalid_values = ["", "invalid", "none", "all", "normal"];
for value in &invalid_values {
assert!(Config::parse_redaction_level(value).is_err());
}
}
#[test]
fn test_config_serialization() {
let config = Config::development();
let json = serde_json::to_string(&config).unwrap();
assert!(!json.is_empty());
let deserialized: Config = serde_json::from_str(&json).unwrap();
assert_eq!(config.logging.level, deserialized.logging.level);
assert_eq!(config.http.port, deserialized.http.port);
assert_eq!(config.nats.url, deserialized.nats.url);
}
#[test]
fn test_config_toml_serialization() {
let config = Config::testing();
let toml_str = toml::to_string(&config).unwrap();
assert!(!toml_str.is_empty());
let deserialized: Config = toml::from_str(&toml_str).unwrap();
assert_eq!(config.logging.level, deserialized.logging.level);
assert_eq!(config.metrics.enabled, deserialized.metrics.enabled);
}
#[test]
fn test_config_from_file_error_handling() {
let result = Config::from_file("/nonexistent/file.toml");
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Failed to read config file"));
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "invalid toml content [unclosed section").unwrap();
let result = Config::from_file(temp_file.path());
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Failed to parse TOML config"));
}
#[test]
fn test_config_from_file_success() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(
temp_file,
r#"
[http]
port = 9999
bind_address = "127.0.0.1"
[logging]
level = "debug"
json_format = true
"#
)
.unwrap();
let result = Config::from_file(temp_file.path());
assert!(result.is_err());
let error = result.unwrap_err();
assert!(
error.to_string().contains("Failed to parse TOML config")
|| error.to_string().contains("missing field")
);
let default_config = Config::default();
let toml_content = toml::to_string(&default_config).unwrap();
let parsed_config: Config = toml::from_str(&toml_content).unwrap();
assert_eq!(parsed_config.http.port, default_config.http.port);
assert_eq!(parsed_config.logging.level, default_config.logging.level);
}
#[test]
fn test_config_validation_failure_cascade() {
let mut config = Config::default();
config.logging.level = "invalid_level".to_string();
config.metrics.prefix = "".to_string();
config.behavior.default_pack = "".to_string();
let result = config.validate();
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("configuration validation failed"));
}
#[test]
fn test_environment_config_consistency() {
let dev = Config::development();
let prod = Config::production();
let test = Config::testing();
assert!(!dev.nats.url.is_empty());
assert!(!prod.nats.url.is_empty());
assert!(!test.nats.url.is_empty());
assert!(dev.http.port > 0);
assert!(prod.http.port > 0);
assert_eq!(test.http.port, 0);
assert!(!dev.logging.level.is_empty());
assert!(!prod.logging.level.is_empty());
assert!(!test.logging.level.is_empty());
assert_ne!(dev.logging.level, prod.logging.level);
assert_ne!(prod.logging.level, test.logging.level);
}
#[test]
fn test_config_clone_and_default() {
let config = Config::development();
let cloned = config.clone();
assert_eq!(config.nats.url, cloned.nats.url);
assert_eq!(config.http.port, cloned.http.port);
assert_eq!(config.logging.level, cloned.logging.level);
let default_config = Config::default();
assert!(!default_config.nats.url.is_empty());
assert!(default_config.http.port > 0);
}
#[test]
fn test_config_debug_format() {
let config = Config::testing();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("Config"));
assert!(debug_str.contains("nats"));
assert!(debug_str.contains("http"));
assert!(debug_str.contains("logging"));
}
#[test]
fn test_config_builder_edge_cases() {
let minimal_config = ConfigBuilder::new().build();
assert_eq!(minimal_config.logging.level, "debug");
let config = ConfigBuilder::new()
.with_nats_url("")
.with_http_port(0)
.with_log_level("")
.build();
assert_eq!(config.nats.url, "");
assert_eq!(config.http.port, 0);
assert_eq!(config.logging.level, "");
assert!(config.validate().is_err());
}
#[test]
#[ignore] fn test_complex_config_scenarios() {
let mut config = Config::development();
config
.metrics
.labels
.insert("datacenter".to_string(), "us-west-2".to_string());
config
.metrics
.labels
.insert("version".to_string(), "v1.2.3".to_string());
assert_eq!(config.metrics.labels.len(), 3); assert!(config.validate().is_ok());
config.logging.nats.enabled = true;
config.logging.nats.target_filters = vec![
"smith".to_string(),
"executor".to_string(),
"custom_module".to_string(),
];
config.logging.nats.level_filter = Some("trace".to_string());
assert!(config.validate().is_ok());
}
}
#[cfg(test)]
mod simple_coverage_tests;
#[cfg(test)]
mod env_and_io_tests;
#[cfg(test)]
mod behavior_tests;
#[cfg(test)]
mod diff_tests;