use crate::errors::CoreError;
use crate::urls::backend_url_base;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum BackendAuth {
XApiKey,
Bearer,
}
impl Default for BackendAuth {
fn default() -> Self {
BackendAuth::XApiKey
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoreConfig {
pub backend_base_url: String,
pub api_key: Option<String>,
pub user_agent: String,
pub timeout_ms: u64,
pub retries: u32,
pub auth: BackendAuth,
}
impl Default for CoreConfig {
fn default() -> Self {
let backend_base_url = std::env::var("SYNTH_BACKEND_URL")
.ok()
.filter(|v| !v.trim().is_empty())
.unwrap_or_else(backend_url_base);
let api_key = std::env::var("SYNTH_API_KEY").ok();
CoreConfig {
backend_base_url,
api_key,
user_agent: "synth-core/0.1".to_string(),
timeout_ms: 30_000,
retries: 3,
auth: BackendAuth::default(),
}
}
}
impl CoreConfig {
pub fn with_backend(mut self, backend_base_url: String) -> Self {
self.backend_base_url = backend_base_url;
self
}
}
pub fn load_toml(path: &Path) -> Result<Value, CoreError> {
let content = fs::read_to_string(path)
.map_err(|e| CoreError::Config(format!("failed to read TOML file: {}", e)))?;
parse_toml(&content)
}
pub fn parse_toml(content: &str) -> Result<Value, CoreError> {
let toml_value: toml::Value = toml::from_str(content)
.map_err(|e| CoreError::Config(format!("failed to parse TOML: {}", e)))?;
toml_to_json(toml_value)
}
fn toml_to_json(toml: toml::Value) -> Result<Value, CoreError> {
match toml {
toml::Value::String(s) => Ok(Value::String(s)),
toml::Value::Integer(i) => Ok(Value::Number(i.into())),
toml::Value::Float(f) => serde_json::Number::from_f64(f)
.map(Value::Number)
.ok_or_else(|| CoreError::Config("invalid float value".to_string())),
toml::Value::Boolean(b) => Ok(Value::Bool(b)),
toml::Value::Datetime(dt) => Ok(Value::String(dt.to_string())),
toml::Value::Array(arr) => {
let json_arr: Result<Vec<Value>, CoreError> =
arr.into_iter().map(toml_to_json).collect();
Ok(Value::Array(json_arr?))
}
toml::Value::Table(table) => {
let mut map = serde_json::Map::new();
for (k, v) in table {
map.insert(k, toml_to_json(v)?);
}
Ok(Value::Object(map))
}
}
}
pub fn deep_merge(base: &mut Value, overrides: &Value) {
match (base, overrides) {
(Value::Object(base_map), Value::Object(override_map)) => {
for (key, override_val) in override_map {
if let Some(base_val) = base_map.get_mut(key) {
deep_merge(base_val, override_val);
} else {
base_map.insert(key.clone(), override_val.clone());
}
}
}
(base, overrides) => {
*base = overrides.clone();
}
}
}
pub fn deep_update(base: &mut Value, overrides: &Value) {
if !overrides.is_object() {
return;
}
if !base.is_object() {
*base = Value::Object(Map::new());
}
let base_map = base.as_object_mut().expect("base is object");
let override_map = overrides.as_object().expect("overrides is object");
for (key, override_val) in override_map {
if key.contains('.') {
apply_dot_update(base_map, key, override_val);
} else {
match base_map.get_mut(key) {
Some(existing) => {
if existing.is_object() && override_val.is_object() {
deep_update(existing, override_val);
} else {
*existing = override_val.clone();
}
}
None => {
base_map.insert(key.clone(), override_val.clone());
}
}
}
}
}
fn apply_dot_update(base_map: &mut Map<String, Value>, key: &str, override_val: &Value) {
let mut current = base_map;
let mut parts = key.split('.').peekable();
while let Some(part) = parts.next() {
if parts.peek().is_none() {
match current.get_mut(part) {
Some(existing) => {
if existing.is_object() && override_val.is_object() {
deep_update(existing, override_val);
} else {
*existing = override_val.clone();
}
}
None => {
current.insert(part.to_string(), override_val.clone());
}
}
} else {
let entry = current
.entry(part.to_string())
.or_insert_with(|| Value::Object(Map::new()));
if !entry.is_object() {
*entry = Value::Object(Map::new());
}
current = entry.as_object_mut().expect("entry is object");
}
}
}
pub fn validate_overrides(base: &Value, overrides: &Value, path: &str) -> Result<(), CoreError> {
match (base, overrides) {
(Value::Object(base_map), Value::Object(override_map)) => {
for (key, override_val) in override_map {
let key_path = if path.is_empty() {
key.clone()
} else {
format!("{}.{}", path, key)
};
if let Some(base_val) = base_map.get(key) {
validate_overrides(base_val, override_val, &key_path)?;
} else {
return Err(CoreError::Config(format!(
"unknown config key: {}",
key_path
)));
}
}
Ok(())
}
_ => Ok(()),
}
}
#[derive(Debug, Clone)]
pub struct ResolvedConfigValue {
pub value: Option<String>,
pub cli_value: Option<String>,
pub config_value: Option<String>,
pub cli_overrides_config: bool,
}
fn clean_opt(value: Option<&str>) -> Option<String> {
match value {
Some(raw) => {
let trimmed = raw.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
None => None,
}
}
pub fn resolve_config_value(
cli_value: Option<&str>,
env_value: Option<&str>,
config_value: Option<&str>,
default_value: Option<&str>,
) -> ResolvedConfigValue {
let cli_clean = clean_opt(cli_value);
let env_clean = clean_opt(env_value);
let config_clean = clean_opt(config_value);
let default_clean = clean_opt(default_value);
let cli_overrides_config = match (&cli_clean, &config_clean) {
(Some(cli), Some(config)) => cli != config,
_ => false,
};
let resolved = cli_clean
.clone()
.or(env_clean)
.or(config_clean.clone())
.or(default_clean);
ResolvedConfigValue {
value: resolved,
cli_value: cli_clean,
config_value: config_clean,
cli_overrides_config,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptimizationDefaults {
pub population_size: usize,
pub generations: usize,
pub mutation_rate: f64,
pub crossover_rate: f64,
pub elite_count: usize,
pub train_ratio: f64,
pub max_rollouts: usize,
pub rollout_timeout_secs: u64,
pub enable_caching: bool,
}
impl Default for OptimizationDefaults {
fn default() -> Self {
Self {
population_size: 10,
generations: 5,
mutation_rate: 0.1,
crossover_rate: 0.7,
elite_count: 2,
train_ratio: 0.7,
max_rollouts: 100,
rollout_timeout_secs: 60,
enable_caching: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpansionDefaultsV1 {
pub version: String,
pub train_ratio: f64,
pub rollout_budget: i64,
pub rollout_max_concurrent: i64,
pub mutation_rate: f64,
pub pop_size_min: i64,
pub pop_size_max: i64,
pub pop_size_divisor: i64,
pub num_generations: i64,
pub children_divisor: i64,
pub crossover_rate: f64,
pub selection_pressure: f64,
pub archive_multiplier: i64,
pub pareto_eps: f64,
pub feedback_fraction: f64,
pub eval_max_concurrent: i64,
pub eval_timeout: f64,
}
impl ExpansionDefaultsV1 {
pub fn v1() -> Self {
Self {
version: "v1".to_string(),
train_ratio: 0.7,
rollout_budget: 100_000_000,
rollout_max_concurrent: 20,
mutation_rate: 0.3,
pop_size_min: 10,
pop_size_max: 30,
pop_size_divisor: 10,
num_generations: 10,
children_divisor: 4,
crossover_rate: 0.5,
selection_pressure: 1.0,
archive_multiplier: 2,
pareto_eps: 1e-6,
feedback_fraction: 0.5,
eval_max_concurrent: 20,
eval_timeout: 600.0,
}
}
}
pub fn expansion_defaults(version: Option<&str>) -> Result<ExpansionDefaultsV1, CoreError> {
match version.unwrap_or("v1") {
"v1" => Ok(ExpansionDefaultsV1::v1()),
other => Err(CoreError::Config(format!(
"unknown defaults version: {}",
other
))),
}
}
pub fn resolve_seeds(seeds: &Value) -> Result<Vec<String>, CoreError> {
match seeds {
Value::Array(arr) => {
let mut result = Vec::new();
for item in arr {
match item {
Value::String(s) => result.push(s.clone()),
Value::Object(obj) => {
if let Some(Value::String(id)) = obj.get("id") {
result.push(id.clone());
}
}
_ => {}
}
}
Ok(result)
}
Value::Object(obj) => Ok(obj.keys().cloned().collect()),
Value::String(s) => Ok(vec![s.clone()]),
_ => Ok(Vec::new()),
}
}
pub fn split_train_validation(seeds: &[String], train_ratio: f64) -> (Vec<String>, Vec<String>) {
let ratio = train_ratio.clamp(0.0, 1.0);
let train_count = ((seeds.len() as f64) * ratio).round() as usize;
let train_count = train_count.max(1).min(seeds.len());
let train = seeds[..train_count].to_vec();
let validation = seeds[train_count..].to_vec();
(train, validation)
}
pub fn expand_config(minimal: &Value, defaults: &OptimizationDefaults) -> Result<Value, CoreError> {
let defaults_json = serde_json::to_value(defaults)
.map_err(|e| CoreError::Config(format!("failed to serialize defaults: {}", e)))?;
let mut expanded = defaults_json;
deep_merge(&mut expanded, minimal);
if minimal.get("population_size").is_none() {
if let Some(seeds) = minimal.get("seeds") {
let seed_count = resolve_seeds(seeds)?.len();
if let Value::Object(ref mut map) = expanded {
map.insert(
"population_size".to_string(),
Value::Number((seed_count.max(10)).into()),
);
}
}
}
Ok(expanded)
}
pub fn resolve_seed_spec(seeds_spec: &Value) -> Result<Vec<i64>, CoreError> {
match seeds_spec {
Value::Null => Ok(Vec::new()),
Value::Array(arr) => {
let mut out = Vec::with_capacity(arr.len());
for item in arr {
if let Some(n) = item.as_i64() {
out.push(n);
} else {
return Err(CoreError::Validation(
"seed array must contain integers".to_string(),
));
}
}
Ok(out)
}
Value::Object(map) => {
let start = map.get("start").and_then(|v| v.as_i64()).ok_or_else(|| {
CoreError::Validation("range dict must include integer 'start'".to_string())
})?;
let end = map.get("end").and_then(|v| v.as_i64()).ok_or_else(|| {
CoreError::Validation("range dict must include integer 'end'".to_string())
})?;
let step = map.get("step").and_then(|v| v.as_i64()).unwrap_or(1);
if step <= 0 {
return Err(CoreError::Validation(
"range dict 'step' must be positive".to_string(),
));
}
Ok((start..end).step_by(step as usize).collect())
}
_ => Err(CoreError::Validation(
"invalid seeds spec: expected list or range dict".to_string(),
)),
}
}
pub fn expand_eval_config(minimal: &Value) -> Result<Value, CoreError> {
let map = minimal
.as_object()
.ok_or_else(|| CoreError::Validation("config must be an object".to_string()))?;
let container_url = map
.get("container_url")
.and_then(|v| v.as_str())
.ok_or_else(|| CoreError::Validation("container_url is required".to_string()))?;
let seeds_value = map
.get("seeds")
.ok_or_else(|| CoreError::Validation("seeds is required".to_string()))?;
let defaults = expansion_defaults(map.get("defaults_version").and_then(|v| v.as_str()))?;
let seeds = resolve_seed_spec(seeds_value)?;
let seeds_len = seeds.len() as i64;
let env_name = map
.get("env_name")
.and_then(|v| v.as_str())
.or_else(|| map.get("app_id").and_then(|v| v.as_str()))
.unwrap_or("default");
let policy = map
.get("policy")
.cloned()
.unwrap_or_else(|| Value::Object(Map::new()));
let mut out = Map::new();
out.insert(
"container_url".to_string(),
Value::String(container_url.to_string()),
);
out.insert("env_name".to_string(), Value::String(env_name.to_string()));
if let Some(app_id) = map.get("app_id") {
out.insert("app_id".to_string(), app_id.clone());
}
out.insert(
"seeds".to_string(),
Value::Array(seeds.into_iter().map(Value::from).collect()),
);
out.insert(
"max_concurrent".to_string(),
Value::Number((defaults.eval_max_concurrent.min(seeds_len)).into()),
);
out.insert(
"timeout".to_string(),
Value::Number(
serde_json::Number::from_f64(defaults.eval_timeout)
.ok_or_else(|| CoreError::Validation("invalid eval_timeout".to_string()))?,
),
);
out.insert("policy".to_string(), policy);
out.insert(
"_defaults_version".to_string(),
Value::String(defaults.version),
);
Ok(Value::Object(out))
}
fn build_termination_config(minimal: &Map<String, Value>) -> Option<Value> {
let has_constraint = ["max_cost_usd", "max_rollouts", "max_seconds", "max_trials"]
.iter()
.any(|k| minimal.contains_key(*k));
if !has_constraint {
return None;
}
let mut map = Map::new();
map.insert(
"max_cost_usd".to_string(),
minimal
.get("max_cost_usd")
.cloned()
.unwrap_or_else(|| Value::Number(serde_json::Number::from_f64(1000.0).unwrap())),
);
map.insert(
"max_trials".to_string(),
minimal
.get("max_trials")
.cloned()
.unwrap_or_else(|| Value::Number(100000.into())),
);
if let Some(v) = minimal.get("max_rollouts") {
map.insert("max_rollouts".to_string(), v.clone());
}
if let Some(v) = minimal.get("max_seconds") {
map.insert("max_seconds".to_string(), v.clone());
}
Some(Value::Object(map))
}
pub fn expand_gepa_config(minimal: &Value) -> Result<Value, CoreError> {
let map = minimal
.as_object()
.ok_or_else(|| CoreError::Validation("config must be an object".to_string()))?;
let container_url = map
.get("container_url")
.and_then(|v| v.as_str())
.ok_or_else(|| CoreError::Validation("container_url is required".to_string()))?;
for key in [
"proposer_effort",
"proposer_output_tokens",
"num_generations",
"children_per_generation",
] {
if !map.contains_key(key) {
return Err(CoreError::Validation(format!("{} is required", key)));
}
}
let defaults = expansion_defaults(map.get("defaults_version").and_then(|v| v.as_str()))?;
let (train_seeds, val_seeds) =
if let Some(total) = map.get("total_seeds").and_then(|v| v.as_i64()) {
let split = (total as f64 * defaults.train_ratio) as i64;
let train: Vec<i64> = (0..split).collect();
let val: Vec<i64> = (split..total).collect();
(train, val)
} else if map.contains_key("train_seeds")
|| map.contains_key("validation_seeds")
|| map.contains_key("val_seeds")
{
let train_value = map.get("train_seeds").cloned().unwrap_or(Value::Null);
let val_value = map
.get("validation_seeds")
.cloned()
.or_else(|| map.get("val_seeds").cloned())
.unwrap_or(Value::Null);
(
resolve_seed_spec(&train_value)?,
resolve_seed_spec(&val_value)?,
)
} else {
return Err(CoreError::Validation(
"Either total_seeds or (train_seeds + validation_seeds) is required".to_string(),
));
};
if train_seeds.is_empty() {
return Err(CoreError::Validation(
"train_seeds cannot be empty".to_string(),
));
}
if val_seeds.is_empty() {
return Err(CoreError::Validation(
"validation_seeds cannot be empty".to_string(),
));
}
let n_train = train_seeds.len() as i64;
let computed = n_train / defaults.pop_size_divisor.max(1);
let mut pop_size = map
.get("population_size")
.and_then(|v| v.as_i64())
.unwrap_or(computed);
if pop_size < defaults.pop_size_min {
pop_size = defaults.pop_size_min;
}
if pop_size > defaults.pop_size_max {
pop_size = defaults.pop_size_max;
}
let mut gepa = Map::new();
let env_name = map
.get("env_name")
.and_then(|v| v.as_str())
.unwrap_or("default");
gepa.insert("env_name".to_string(), Value::String(env_name.to_string()));
gepa.insert(
"proposer_effort".to_string(),
map.get("proposer_effort").cloned().unwrap_or(Value::Null),
);
gepa.insert(
"proposer_output_tokens".to_string(),
map.get("proposer_output_tokens")
.cloned()
.unwrap_or(Value::Null),
);
let mut evaluation = Map::new();
evaluation.insert(
"train_seeds".to_string(),
Value::Array(train_seeds.into_iter().map(Value::from).collect()),
);
evaluation.insert(
"validation_seeds".to_string(),
Value::Array(val_seeds.into_iter().map(Value::from).collect()),
);
gepa.insert("evaluation".to_string(), Value::Object(evaluation));
let mut rollout = Map::new();
rollout.insert(
"budget".to_string(),
Value::Number(defaults.rollout_budget.into()),
);
rollout.insert(
"max_concurrent".to_string(),
Value::Number(defaults.rollout_max_concurrent.into()),
);
gepa.insert("rollout".to_string(), Value::Object(rollout));
let mut mutation = Map::new();
mutation.insert(
"rate".to_string(),
serde_json::Number::from_f64(defaults.mutation_rate)
.map(Value::Number)
.ok_or_else(|| CoreError::Validation("invalid mutation_rate".to_string()))?,
);
gepa.insert("mutation".to_string(), Value::Object(mutation));
let mut population = Map::new();
population.insert("initial_size".to_string(), Value::Number(pop_size.into()));
population.insert(
"num_generations".to_string(),
map.get("num_generations").cloned().unwrap_or(Value::Null),
);
population.insert(
"children_per_generation".to_string(),
map.get("children_per_generation")
.cloned()
.unwrap_or(Value::Null),
);
population.insert(
"crossover_rate".to_string(),
serde_json::Number::from_f64(defaults.crossover_rate)
.map(Value::Number)
.ok_or_else(|| CoreError::Validation("invalid crossover_rate".to_string()))?,
);
population.insert(
"selection_pressure".to_string(),
serde_json::Number::from_f64(defaults.selection_pressure)
.map(Value::Number)
.ok_or_else(|| CoreError::Validation("invalid selection_pressure".to_string()))?,
);
gepa.insert("population".to_string(), Value::Object(population));
let mut archive = Map::new();
let archive_size = pop_size * defaults.archive_multiplier;
archive.insert("size".to_string(), Value::Number(archive_size.into()));
archive.insert(
"pareto_set_size".to_string(),
Value::Number(archive_size.into()),
);
archive.insert(
"pareto_eps".to_string(),
serde_json::Number::from_f64(defaults.pareto_eps)
.map(Value::Number)
.ok_or_else(|| CoreError::Validation("invalid pareto_eps".to_string()))?,
);
archive.insert(
"feedback_fraction".to_string(),
serde_json::Number::from_f64(defaults.feedback_fraction)
.map(Value::Number)
.ok_or_else(|| CoreError::Validation("invalid feedback_fraction".to_string()))?,
);
gepa.insert("archive".to_string(), Value::Object(archive));
let mut out = Map::new();
out.insert("algorithm".to_string(), Value::String("gepa".to_string()));
out.insert(
"container_url".to_string(),
Value::String(container_url.to_string()),
);
if let Some(container_id) = map.get("container_id") {
out.insert("container_id".to_string(), container_id.clone());
}
for key in [
"policy",
"env_config",
"verifier",
"proxy_models",
"initial_prompt",
"auto_discover_patterns",
"use_byok",
] {
if let Some(value) = map.get(key) {
if !value.is_null() {
out.insert(key.to_string(), value.clone());
}
}
}
out.insert("gepa".to_string(), Value::Object(gepa));
if let Some(term) = build_termination_config(map) {
out.insert("termination_config".to_string(), term);
}
out.insert(
"_defaults_version".to_string(),
Value::String(defaults.version),
);
Ok(Value::Object(out))
}
pub fn gepa_candidate_to_initial_prompt(seed_candidate: &Value) -> Result<Value, CoreError> {
let map = seed_candidate
.as_object()
.ok_or_else(|| CoreError::Validation("seed_candidate must be an object".to_string()))?;
if map.is_empty() {
return Err(CoreError::Validation(
"seed_candidate must include at least one prompt component".to_string(),
));
}
let extract_prompt = |key: &str| -> Option<String> {
map.get(key)
.and_then(|v| v.as_str())
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.map(|v| v.to_string())
};
let mut messages: Vec<Value> = Vec::new();
let mut order = 0_i64;
let push_message = |messages: &mut Vec<Value>, order: &mut i64, role: &str, prompt: String| {
let mut msg = Map::new();
msg.insert("role".to_string(), Value::String(role.to_string()));
msg.insert("pattern".to_string(), Value::String(prompt));
msg.insert("order".to_string(), Value::Number((*order).into()));
*order += 1;
messages.push(Value::Object(msg));
};
if let Some(system_prompt) = extract_prompt("system_prompt")
.or_else(|| extract_prompt("instruction"))
.or_else(|| extract_prompt("prompt"))
.or_else(|| extract_prompt("system"))
{
push_message(&mut messages, &mut order, "system", system_prompt);
}
if let Some(user_prompt) = extract_prompt("user_prompt")
.or_else(|| extract_prompt("user_message"))
.or_else(|| extract_prompt("user"))
{
push_message(&mut messages, &mut order, "user", user_prompt);
}
if let Some(assistant_prompt) = extract_prompt("assistant_prompt")
.or_else(|| extract_prompt("assistant_message"))
.or_else(|| extract_prompt("assistant"))
{
push_message(&mut messages, &mut order, "assistant", assistant_prompt);
}
if messages.is_empty() && map.len() == 1 {
if let Some(value) = map.values().next().and_then(|v| v.as_str()) {
let trimmed = value.trim();
if !trimmed.is_empty() {
push_message(&mut messages, &mut order, "system", trimmed.to_string());
}
}
}
if messages.is_empty() {
return Err(CoreError::Validation(
"seed_candidate must include a system prompt or a single prompt string".to_string(),
));
}
let mut output = Map::new();
output.insert("messages".to_string(), Value::Array(messages));
output.insert("wildcards".to_string(), Value::Object(Map::new()));
Ok(Value::Object(output))
}
pub fn is_minimal_config(config: &Value) -> bool {
let map = match config.as_object() {
Some(map) => map,
None => return false,
};
let has_minimal = map.contains_key("total_seeds") || map.contains_key("defaults_version");
let has_full = map.contains_key("gepa") || map.contains_key("mipro");
has_minimal && !has_full
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_deep_merge_objects() {
let mut base = json!({
"a": 1,
"b": {
"c": 2,
"d": 3
}
});
let overrides = json!({
"b": {
"c": 99
},
"e": 4
});
deep_merge(&mut base, &overrides);
assert_eq!(base["a"], 1);
assert_eq!(base["b"]["c"], 99);
assert_eq!(base["b"]["d"], 3);
assert_eq!(base["e"], 4);
}
#[test]
fn test_deep_merge_replace() {
let mut base = json!({ "a": [1, 2, 3] });
let overrides = json!({ "a": [4, 5] });
deep_merge(&mut base, &overrides);
assert_eq!(base["a"], json!([4, 5]));
}
#[test]
fn test_deep_update_dot_keys() {
let mut base = json!({
"prompt_learning": {
"policy": { "model": "a" }
}
});
let overrides = json!({
"prompt_learning.policy.model": "b",
"prompt_learning.gepa.rollout.budget": 10
});
deep_update(&mut base, &overrides);
assert_eq!(base["prompt_learning"]["policy"]["model"], "b");
assert_eq!(base["prompt_learning"]["gepa"]["rollout"]["budget"], 10);
}
#[test]
fn test_deep_update_nested_merge() {
let mut base = json!({
"a": { "b": 1, "c": 2 }
});
let overrides = json!({
"a": { "b": 3 }
});
deep_update(&mut base, &overrides);
assert_eq!(base["a"]["b"], 3);
assert_eq!(base["a"]["c"], 2);
}
#[test]
fn test_resolve_seeds_array() {
let seeds = json!(["seed1", "seed2", "seed3"]);
let result = resolve_seeds(&seeds).unwrap();
assert_eq!(result, vec!["seed1", "seed2", "seed3"]);
}
#[test]
fn test_resolve_seeds_object() {
let seeds = json!({
"seed1": {"data": 1},
"seed2": {"data": 2}
});
let mut result = resolve_seeds(&seeds).unwrap();
result.sort();
assert_eq!(result, vec!["seed1", "seed2"]);
}
#[test]
fn test_split_train_validation() {
let seeds: Vec<String> = (1..=10).map(|i| format!("seed{}", i)).collect();
let (train, val) = split_train_validation(&seeds, 0.7);
assert_eq!(train.len(), 7);
assert_eq!(val.len(), 3);
let (train, val) = split_train_validation(&seeds, 0.5);
assert_eq!(train.len(), 5);
assert_eq!(val.len(), 5);
}
#[test]
fn test_parse_toml() {
let toml = r#"
[optimization]
generations = 10
mutation_rate = 0.2
[optimization.nested]
value = "test"
"#;
let result = parse_toml(toml).unwrap();
assert_eq!(result["optimization"]["generations"], 10);
assert_eq!(result["optimization"]["mutation_rate"], 0.2);
assert_eq!(result["optimization"]["nested"]["value"], "test");
}
#[test]
fn test_validate_overrides() {
let base = json!({
"a": 1,
"b": {
"c": 2
}
});
let valid = json!({
"a": 99,
"b": {
"c": 100
}
});
assert!(validate_overrides(&base, &valid, "").is_ok());
let invalid = json!({
"unknown_key": 1
});
assert!(validate_overrides(&base, &invalid, "").is_err());
}
}