use anyhow::{Context, Result, anyhow};
use sqry_core::config::{
graph_config_persistence::{ConfigPersistence, LoadReport},
graph_config_schema::{AliasEntry, GraphConfigFile},
graph_config_store::GraphConfigStore,
};
use std::io::{self, BufRead};
use std::path::Path;
const KB_BYTES: u64 = 1024;
const MB_BYTES: u64 = KB_BYTES * 1024;
const GB_BYTES: u64 = MB_BYTES * 1024;
const KB_BYTES_F64: f64 = 1024.0;
const MB_BYTES_F64: f64 = 1024.0 * 1024.0;
const GB_BYTES_F64: f64 = 1024.0 * 1024.0 * 1024.0;
pub fn run_config_init(path: Option<&str>, force: bool) -> Result<()> {
let project_root = Path::new(path.unwrap_or("."));
let store = GraphConfigStore::new(project_root).context("Failed to create config store")?;
if store.is_initialized() && !force {
anyhow::bail!(
"Config already initialized at {}. Use --force to overwrite.",
store.paths().config_file().display()
);
}
store
.validate(false)
.context("Filesystem validation failed")?;
let persistence = ConfigPersistence::new(&store);
let config = persistence
.init(5000, "cli")
.context("Failed to initialize config")?;
println!(
"✓ Config initialized at {}",
store.paths().config_file().display()
);
println!(" Schema version: {}", config.schema_version);
println!(" Created at: {}", config.metadata.created_at);
Ok(())
}
pub fn run_config_show(path: Option<&str>, json: bool, key: Option<&str>) -> Result<()> {
let project_root = Path::new(path.unwrap_or("."));
let store = GraphConfigStore::new(project_root).context("Failed to create config store")?;
if !store.is_initialized() {
anyhow::bail!("Config not initialized. Run 'sqry config init' first.");
}
let persistence = ConfigPersistence::new(&store);
let (config, report) = persistence.load().context("Failed to load config")?;
print_config_diagnostics(&report);
if let Some(key_path) = key {
return show_config_key(&config, key_path, json);
}
if json {
let json_str =
serde_json::to_string_pretty(&config).context("Failed to serialize config")?;
println!("{json_str}");
} else {
print_config_human(&store, &config, &report);
}
Ok(())
}
fn print_config_diagnostics(report: &LoadReport) {
for warning in &report.warnings {
eprintln!("Warning: {warning}");
}
for action in &report.recovery_actions {
eprintln!("Recovery: {action}");
}
}
#[allow(clippy::too_many_lines)] fn print_config_human(store: &GraphConfigStore, config: &GraphConfigFile, report: &LoadReport) {
println!("Config file: {}", store.paths().config_file().display());
println!("Schema version: {}", config.schema_version);
println!("Integrity: {:?}", report.integrity_status);
println!();
println!("=== Metadata ===");
println!("Created at: {}", config.metadata.created_at);
println!("Updated at: {}", config.metadata.updated_at);
println!("sqry version: {}", config.metadata.written_by.sqry_version);
println!();
println!("=== Limits ===");
println!(
"max_results: {}",
if config.config.limits.max_results == 0 {
"unlimited".to_string()
} else {
config.config.limits.max_results.to_string()
}
);
println!(
"max_depth: {}",
if config.config.limits.max_depth == 0 {
"unlimited".to_string()
} else {
config.config.limits.max_depth.to_string()
}
);
println!(
"max_bytes_per_file: {}",
if config.config.limits.max_bytes_per_file == 0 {
"unlimited".to_string()
} else {
format_bytes(config.config.limits.max_bytes_per_file)
}
);
println!(
"max_files: {}",
if config.config.limits.max_files == 0 {
"unlimited".to_string()
} else {
config.config.limits.max_files.to_string()
}
);
println!();
println!("=== Analysis ===");
println!(
"analysis_label_budget_per_kind: {}",
if config.config.limits.analysis_label_budget_per_kind == 0 {
"unlimited".to_string()
} else {
config
.config
.limits
.analysis_label_budget_per_kind
.to_string()
}
);
println!(
"analysis_density_gate_threshold: {}",
if config.config.limits.analysis_density_gate_threshold == 0 {
"disabled".to_string()
} else {
config
.config
.limits
.analysis_density_gate_threshold
.to_string()
}
);
println!(
"analysis_budget_exceeded_policy: {}",
config.config.limits.analysis_budget_exceeded_policy
);
println!();
println!("=== Locking ===");
println!(
"write_lock_timeout_ms: {}",
config.config.locking.write_lock_timeout_ms
);
println!(
"stale_lock_timeout_ms: {}",
config.config.locking.stale_lock_timeout_ms
);
println!(
"stale_takeover_policy: {}",
config.config.locking.stale_takeover_policy
);
println!();
println!("=== Output ===");
println!(
"default_pagination: {}",
config.config.output.default_pagination
);
println!("page_size: {}", config.config.output.page_size);
println!(
"max_preview_bytes: {}",
format_bytes(config.config.output.max_preview_bytes)
);
println!();
println!("=== Parallelism ===");
println!(
"max_threads: {}",
if config.config.parallelism.max_threads == 0 {
"auto-detect".to_string()
} else {
config.config.parallelism.max_threads.to_string()
}
);
println!();
println!("=== Aliases ({}) ===", config.config.aliases.len());
for (name, alias) in &config.config.aliases {
println!(" {}: {}", name, alias.query);
if let Some(desc) = &alias.description {
println!(" Description: {desc}");
}
}
}
fn show_config_key(config: &GraphConfigFile, key_path: &str, json: bool) -> Result<()> {
let parts: Vec<&str> = key_path.split('.').collect();
if parts.is_empty() {
anyhow::bail!("Invalid key path: {key_path}");
}
let value = match parts[0] {
"limits" => match parts.get(1) {
Some(&"max_results") => serde_json::to_value(config.config.limits.max_results)?,
Some(&"max_depth") => serde_json::to_value(config.config.limits.max_depth)?,
Some(&"max_bytes_per_file") => {
serde_json::to_value(config.config.limits.max_bytes_per_file)?
}
Some(&"max_files") => serde_json::to_value(config.config.limits.max_files)?,
Some(&"analysis_label_budget_per_kind") => {
serde_json::to_value(config.config.limits.analysis_label_budget_per_kind)?
}
Some(&"analysis_density_gate_threshold") => {
serde_json::to_value(config.config.limits.analysis_density_gate_threshold)?
}
Some(&"analysis_budget_exceeded_policy") => {
serde_json::to_value(&config.config.limits.analysis_budget_exceeded_policy)?
}
_ => anyhow::bail!("Unknown limits key: {:?}", parts.get(1)),
},
"locking" => match parts.get(1) {
Some(&"write_lock_timeout_ms") => {
serde_json::to_value(config.config.locking.write_lock_timeout_ms)?
}
Some(&"stale_lock_timeout_ms") => {
serde_json::to_value(config.config.locking.stale_lock_timeout_ms)?
}
Some(&"stale_takeover_policy") => {
serde_json::to_value(&config.config.locking.stale_takeover_policy)?
}
_ => anyhow::bail!("Unknown locking key: {:?}", parts.get(1)),
},
"output" => match parts.get(1) {
Some(&"default_pagination") => {
serde_json::to_value(config.config.output.default_pagination)?
}
Some(&"page_size") => serde_json::to_value(config.config.output.page_size)?,
Some(&"max_preview_bytes") => {
serde_json::to_value(config.config.output.max_preview_bytes)?
}
_ => anyhow::bail!("Unknown output key: {:?}", parts.get(1)),
},
"parallelism" => match parts.get(1) {
Some(&"max_threads") => serde_json::to_value(config.config.parallelism.max_threads)?,
_ => anyhow::bail!("Unknown parallelism key: {:?}", parts.get(1)),
},
_ => anyhow::bail!("Unknown config section: {}", parts[0]),
};
if json {
let json_str = serde_json::to_string_pretty(&value)?;
println!("{json_str}");
} else {
println!("{value}");
}
Ok(())
}
pub fn run_config_set(path: Option<&str>, key: &str, value: &str, yes: bool) -> Result<()> {
let project_root = Path::new(path.unwrap_or("."));
let store = GraphConfigStore::new(project_root).context("Failed to create config store")?;
if !store.is_initialized() {
anyhow::bail!("Config not initialized. Run 'sqry config init' first.");
}
let persistence = ConfigPersistence::new(&store);
let (mut config, _report) = persistence.load().context("Failed to load config")?;
let old_value = get_config_value(&config, key)?;
set_config_value(&mut config, key, value)?;
config
.validate()
.context("Config validation failed after update")?;
if !yes {
println!("Config change:");
println!(" {key}: {old_value} → {value}");
println!();
print!("Apply this change? [y/N] ");
let stdin = io::stdin();
let mut line = String::new();
stdin.lock().read_line(&mut line)?;
if !line.trim().eq_ignore_ascii_case("y") {
println!("Cancelled.");
return Ok(());
}
}
persistence
.save(&mut config, 5000, "cli")
.context("Failed to save config")?;
println!("✓ Config updated: {key} = {value}");
Ok(())
}
fn get_config_value(config: &GraphConfigFile, key: &str) -> Result<String> {
let parts: Vec<&str> = key.split('.').collect();
let value = match parts[0] {
"limits" => match parts.get(1) {
Some(&"max_results") => config.config.limits.max_results.to_string(),
Some(&"max_depth") => config.config.limits.max_depth.to_string(),
Some(&"max_bytes_per_file") => config.config.limits.max_bytes_per_file.to_string(),
Some(&"max_files") => config.config.limits.max_files.to_string(),
Some(&"analysis_label_budget_per_kind") => config
.config
.limits
.analysis_label_budget_per_kind
.to_string(),
Some(&"analysis_density_gate_threshold") => config
.config
.limits
.analysis_density_gate_threshold
.to_string(),
Some(&"analysis_budget_exceeded_policy") => {
config.config.limits.analysis_budget_exceeded_policy.clone()
}
_ => anyhow::bail!("Unknown limits key: {:?}", parts.get(1)),
},
"locking" => match parts.get(1) {
Some(&"write_lock_timeout_ms") => {
config.config.locking.write_lock_timeout_ms.to_string()
}
Some(&"stale_lock_timeout_ms") => {
config.config.locking.stale_lock_timeout_ms.to_string()
}
Some(&"stale_takeover_policy") => config.config.locking.stale_takeover_policy.clone(),
_ => anyhow::bail!("Unknown locking key: {:?}", parts.get(1)),
},
"output" => match parts.get(1) {
Some(&"default_pagination") => config.config.output.default_pagination.to_string(),
Some(&"page_size") => config.config.output.page_size.to_string(),
Some(&"max_preview_bytes") => config.config.output.max_preview_bytes.to_string(),
_ => anyhow::bail!("Unknown output key: {:?}", parts.get(1)),
},
"parallelism" => match parts.get(1) {
Some(&"max_threads") => config.config.parallelism.max_threads.to_string(),
_ => anyhow::bail!("Unknown parallelism key: {:?}", parts.get(1)),
},
_ => anyhow::bail!("Unknown config section: {}", parts[0]),
};
Ok(value)
}
fn set_config_value(config: &mut GraphConfigFile, key: &str, value: &str) -> Result<()> {
let parts: Vec<&str> = key.split('.').collect();
let subsection = parts.get(1).copied();
match parts[0] {
"limits" => set_limits_config_value(config, subsection, value)?,
"locking" => set_locking_config_value(config, subsection, value)?,
"output" => set_output_config_value(config, subsection, value)?,
"parallelism" => set_parallelism_config_value(config, subsection, value)?,
_ => anyhow::bail!("Unknown config section: {}", parts[0]),
}
Ok(())
}
fn parse_u64_config_value(value: &str, context_message: &'static str) -> Result<u64> {
value.parse().context(context_message)
}
fn parse_bool_config_value(value: &str, context_message: &'static str) -> Result<bool> {
value.parse().context(context_message)
}
fn validate_enum_config_value(
key: &str,
value: &str,
allowed_values: &[&str],
expected_values: &str,
) -> Result<()> {
if allowed_values.contains(&value) {
Ok(())
} else {
anyhow::bail!("Invalid {key} (expected: {expected_values})");
}
}
fn set_limits_config_value(
config: &mut GraphConfigFile,
subsection: Option<&str>,
value: &str,
) -> Result<()> {
match subsection {
Some("max_results") => {
config.config.limits.max_results =
parse_u64_config_value(value, "Invalid value for max_results (expected u64)")?;
}
Some("max_depth") => {
config.config.limits.max_depth =
parse_u64_config_value(value, "Invalid value for max_depth (expected u64)")?;
}
Some("max_bytes_per_file") => {
config.config.limits.max_bytes_per_file = parse_u64_config_value(
value,
"Invalid value for max_bytes_per_file (expected u64)",
)?;
}
Some("max_files") => {
config.config.limits.max_files =
parse_u64_config_value(value, "Invalid value for max_files (expected u64)")?;
}
Some("analysis_label_budget_per_kind") => {
config.config.limits.analysis_label_budget_per_kind = parse_u64_config_value(
value,
"Invalid value for analysis_label_budget_per_kind (expected u64)",
)?;
}
Some("analysis_density_gate_threshold") => {
config.config.limits.analysis_density_gate_threshold = parse_u64_config_value(
value,
"Invalid value for analysis_density_gate_threshold (expected u64)",
)?;
}
Some("analysis_budget_exceeded_policy") => {
validate_enum_config_value(
"analysis_budget_exceeded_policy",
value,
&["degrade", "fail"],
"degrade or fail",
)?;
config.config.limits.analysis_budget_exceeded_policy = value.to_string();
}
_ => anyhow::bail!("Unknown limits key: {subsection:?}"),
}
Ok(())
}
fn set_locking_config_value(
config: &mut GraphConfigFile,
subsection: Option<&str>,
value: &str,
) -> Result<()> {
match subsection {
Some("write_lock_timeout_ms") => {
config.config.locking.write_lock_timeout_ms = parse_u64_config_value(
value,
"Invalid value for write_lock_timeout_ms (expected u64)",
)?;
}
Some("stale_lock_timeout_ms") => {
config.config.locking.stale_lock_timeout_ms = parse_u64_config_value(
value,
"Invalid value for stale_lock_timeout_ms (expected u64)",
)?;
}
Some("stale_takeover_policy") => {
validate_enum_config_value(
"stale_takeover_policy",
value,
&["deny", "warn", "allow"],
"deny, warn, or allow",
)?;
config.config.locking.stale_takeover_policy = value.to_string();
}
_ => anyhow::bail!("Unknown locking key: {subsection:?}"),
}
Ok(())
}
fn set_output_config_value(
config: &mut GraphConfigFile,
subsection: Option<&str>,
value: &str,
) -> Result<()> {
match subsection {
Some("default_pagination") => {
config.config.output.default_pagination = parse_bool_config_value(
value,
"Invalid value for default_pagination (expected bool)",
)?;
}
Some("page_size") => {
let page_size =
parse_u64_config_value(value, "Invalid value for page_size (expected u64)")?;
if page_size == 0 {
anyhow::bail!("page_size must be greater than 0");
}
config.config.output.page_size = page_size;
}
Some("max_preview_bytes") => {
config.config.output.max_preview_bytes = parse_u64_config_value(
value,
"Invalid value for max_preview_bytes (expected u64)",
)?;
}
_ => anyhow::bail!("Unknown output key: {subsection:?}"),
}
Ok(())
}
fn set_parallelism_config_value(
config: &mut GraphConfigFile,
subsection: Option<&str>,
value: &str,
) -> Result<()> {
match subsection {
Some("max_threads") => {
config.config.parallelism.max_threads =
parse_u64_config_value(value, "Invalid value for max_threads (expected u64)")?;
}
_ => anyhow::bail!("Unknown parallelism key: {subsection:?}"),
}
Ok(())
}
pub fn run_config_get(path: Option<&str>, key: &str) -> Result<()> {
let project_root = Path::new(path.unwrap_or("."));
let store = GraphConfigStore::new(project_root).context("Failed to create config store")?;
if !store.is_initialized() {
anyhow::bail!("Config not initialized. Run 'sqry config init' first.");
}
let persistence = ConfigPersistence::new(&store);
let (config, _report) = persistence.load().context("Failed to load config")?;
let value = get_config_value(&config, key)?;
println!("{value}");
Ok(())
}
pub fn run_config_validate(path: Option<&str>) -> Result<()> {
let project_root = Path::new(path.unwrap_or("."));
let store = GraphConfigStore::new(project_root).context("Failed to create config store")?;
if !store.is_initialized() {
anyhow::bail!("Config not initialized. Run 'sqry config init' first.");
}
let persistence = ConfigPersistence::new(&store);
match persistence.load() {
Ok((config, report)) => {
if !report.warnings.is_empty() {
println!("âš Warnings:");
for warning in &report.warnings {
println!(" - {warning}");
}
println!();
}
match config.validate() {
Ok(()) => {
println!("✓ Config is valid");
println!(" Schema version: {}", config.schema_version);
println!(" Integrity: {:?}", report.integrity_status);
Ok(())
}
Err(e) => {
eprintln!("✗ Config validation failed: {e}");
Err(anyhow!("Validation failed"))
}
}
}
Err(e) => {
eprintln!("✗ Failed to load config: {e}");
Err(anyhow!("Load failed"))
}
}
}
pub fn run_config_alias_set(
path: Option<&str>,
name: &str,
query: &str,
description: Option<&str>,
) -> Result<()> {
let project_root = Path::new(path.unwrap_or("."));
let store = GraphConfigStore::new(project_root).context("Failed to create config store")?;
if !store.is_initialized() {
anyhow::bail!("Config not initialized. Run 'sqry config init' first.");
}
let persistence = ConfigPersistence::new(&store);
let (mut config, _report) = persistence.load().context("Failed to load config")?;
let is_update = config.config.aliases.contains_key(name);
let alias_entry = AliasEntry::new(query, description.map(String::from));
config.config.aliases.insert(name.to_string(), alias_entry);
persistence
.save(&mut config, 5000, "cli")
.context("Failed to save config")?;
if is_update {
println!("✓ Alias '{name}' updated");
} else {
println!("✓ Alias '{name}' created");
}
println!(" Query: {query}");
if let Some(desc) = description {
println!(" Description: {desc}");
}
Ok(())
}
pub fn run_config_alias_list(path: Option<&str>, json: bool) -> Result<()> {
let project_root = Path::new(path.unwrap_or("."));
let store = GraphConfigStore::new(project_root).context("Failed to create config store")?;
if !store.is_initialized() {
anyhow::bail!("Config not initialized. Run 'sqry config init' first.");
}
let persistence = ConfigPersistence::new(&store);
let (config, _report) = persistence.load().context("Failed to load config")?;
if config.config.aliases.is_empty() {
println!("No aliases defined.");
return Ok(());
}
if json {
let json_str = serde_json::to_string_pretty(&config.config.aliases)
.context("Failed to serialize aliases")?;
println!("{json_str}");
} else {
println!("Aliases ({}):", config.config.aliases.len());
for (name, alias) in &config.config.aliases {
println!();
println!(" {name}");
println!(" Query: {}", alias.query);
if let Some(desc) = &alias.description {
println!(" Description: {desc}");
}
println!(" Created: {}", alias.created_at);
println!(" Updated: {}", alias.updated_at);
}
}
Ok(())
}
pub fn run_config_alias_remove(path: Option<&str>, name: &str) -> Result<()> {
let project_root = Path::new(path.unwrap_or("."));
let store = GraphConfigStore::new(project_root).context("Failed to create config store")?;
if !store.is_initialized() {
anyhow::bail!("Config not initialized. Run 'sqry config init' first.");
}
let persistence = ConfigPersistence::new(&store);
let (mut config, _report) = persistence.load().context("Failed to load config")?;
if !config.config.aliases.contains_key(name) {
anyhow::bail!("Alias '{name}' not found");
}
config.config.aliases.remove(name);
persistence
.save(&mut config, 5000, "cli")
.context("Failed to save config")?;
println!("✓ Alias '{name}' removed");
Ok(())
}
fn format_bytes(bytes: u64) -> String {
if bytes == 0 {
return "unlimited".to_string();
}
if bytes >= GB_BYTES {
format!("{:.2} GB", u64_to_f64_lossy(bytes) / GB_BYTES_F64)
} else if bytes >= MB_BYTES {
format!("{:.2} MB", u64_to_f64_lossy(bytes) / MB_BYTES_F64)
} else if bytes >= KB_BYTES {
format!("{:.2} KB", u64_to_f64_lossy(bytes) / KB_BYTES_F64)
} else {
format!("{bytes} bytes")
}
}
fn u64_to_f64_lossy(value: u64) -> f64 {
let narrowed = u32::try_from(value).unwrap_or(u32::MAX);
f64::from(narrowed)
}
#[cfg(test)]
mod tests {
use super::set_config_value;
use sqry_core::config::graph_config_schema::GraphConfigFile;
#[test]
fn set_config_value_updates_each_supported_section() {
let mut config = GraphConfigFile::default();
set_config_value(&mut config, "limits.max_results", "9000").unwrap();
set_config_value(&mut config, "locking.stale_takeover_policy", "warn").unwrap();
set_config_value(&mut config, "output.page_size", "25").unwrap();
set_config_value(&mut config, "parallelism.max_threads", "6").unwrap();
assert_eq!(config.config.limits.max_results, 9000);
assert_eq!(config.config.locking.stale_takeover_policy, "warn");
assert_eq!(config.config.output.page_size, 25);
assert_eq!(config.config.parallelism.max_threads, 6);
}
#[test]
fn set_config_value_rejects_invalid_limits_enum() {
let mut config = GraphConfigFile::default();
let error = set_config_value(
&mut config,
"limits.analysis_budget_exceeded_policy",
"panic",
)
.unwrap_err();
assert!(
error
.to_string()
.contains("Invalid analysis_budget_exceeded_policy")
);
}
#[test]
fn set_config_value_rejects_zero_page_size() {
let mut config = GraphConfigFile::default();
let error = set_config_value(&mut config, "output.page_size", "0").unwrap_err();
assert!(
error
.to_string()
.contains("page_size must be greater than 0")
);
}
#[test]
fn set_config_value_rejects_unknown_section() {
let mut config = GraphConfigFile::default();
let error = set_config_value(&mut config, "unknown.key", "1").unwrap_err();
assert!(error.to_string().contains("Unknown config section"));
}
#[test]
fn set_limits_all_u64_keys() {
let mut config = GraphConfigFile::default();
set_config_value(&mut config, "limits.max_depth", "42").unwrap();
set_config_value(&mut config, "limits.max_bytes_per_file", "8192").unwrap();
set_config_value(&mut config, "limits.max_files", "500").unwrap();
set_config_value(
&mut config,
"limits.analysis_label_budget_per_kind",
"100000",
)
.unwrap();
set_config_value(&mut config, "limits.analysis_density_gate_threshold", "75").unwrap();
assert_eq!(config.config.limits.max_depth, 42);
assert_eq!(config.config.limits.max_bytes_per_file, 8192);
assert_eq!(config.config.limits.max_files, 500);
assert_eq!(config.config.limits.analysis_label_budget_per_kind, 100_000);
assert_eq!(config.config.limits.analysis_density_gate_threshold, 75);
}
#[test]
fn set_limits_budget_policy_valid_values() {
let mut config = GraphConfigFile::default();
set_config_value(
&mut config,
"limits.analysis_budget_exceeded_policy",
"fail",
)
.unwrap();
assert_eq!(config.config.limits.analysis_budget_exceeded_policy, "fail");
set_config_value(
&mut config,
"limits.analysis_budget_exceeded_policy",
"degrade",
)
.unwrap();
assert_eq!(
config.config.limits.analysis_budget_exceeded_policy,
"degrade"
);
}
#[test]
fn set_limits_rejects_unknown_key() {
let mut config = GraphConfigFile::default();
let err = set_config_value(&mut config, "limits.nonexistent", "1").unwrap_err();
assert!(err.to_string().contains("Unknown limits key"));
}
#[test]
fn set_limits_rejects_non_numeric() {
let mut config = GraphConfigFile::default();
let err = set_config_value(&mut config, "limits.max_results", "abc").unwrap_err();
assert!(err.to_string().contains("Invalid"));
}
#[test]
fn set_locking_all_keys() {
let mut config = GraphConfigFile::default();
set_config_value(&mut config, "locking.write_lock_timeout_ms", "5000").unwrap();
set_config_value(&mut config, "locking.stale_lock_timeout_ms", "30000").unwrap();
assert_eq!(config.config.locking.write_lock_timeout_ms, 5000);
assert_eq!(config.config.locking.stale_lock_timeout_ms, 30000);
}
#[test]
fn set_locking_stale_takeover_policy_valid_values() {
let mut config = GraphConfigFile::default();
for policy in &["deny", "warn", "allow"] {
set_config_value(&mut config, "locking.stale_takeover_policy", policy).unwrap();
assert_eq!(config.config.locking.stale_takeover_policy, *policy);
}
}
#[test]
fn set_locking_rejects_invalid_policy() {
let mut config = GraphConfigFile::default();
let err =
set_config_value(&mut config, "locking.stale_takeover_policy", "yolo").unwrap_err();
assert!(err.to_string().contains("Invalid stale_takeover_policy"));
}
#[test]
fn set_locking_rejects_unknown_key() {
let mut config = GraphConfigFile::default();
let err = set_config_value(&mut config, "locking.nonexistent", "1").unwrap_err();
assert!(err.to_string().contains("Unknown locking key"));
}
#[test]
fn set_output_all_keys() {
let mut config = GraphConfigFile::default();
set_config_value(&mut config, "output.default_pagination", "true").unwrap();
set_config_value(&mut config, "output.max_preview_bytes", "4096").unwrap();
assert!(config.config.output.default_pagination);
assert_eq!(config.config.output.max_preview_bytes, 4096);
}
#[test]
fn set_output_rejects_invalid_bool() {
let mut config = GraphConfigFile::default();
let err = set_config_value(&mut config, "output.default_pagination", "maybe").unwrap_err();
assert!(err.to_string().contains("Invalid"));
}
#[test]
fn set_output_rejects_unknown_key() {
let mut config = GraphConfigFile::default();
let err = set_config_value(&mut config, "output.nonexistent", "1").unwrap_err();
assert!(err.to_string().contains("Unknown output key"));
}
#[test]
fn set_parallelism_rejects_unknown_key() {
let mut config = GraphConfigFile::default();
let err = set_config_value(&mut config, "parallelism.nonexistent", "1").unwrap_err();
assert!(err.to_string().contains("Unknown parallelism key"));
}
}