use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Config {
pub watchers: Vec<String>,
pub auto_link: bool,
pub auto_link_threshold: f64,
pub commit_footer: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub machine_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub machine_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cloud_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub encryption_salt: Option<String>,
#[serde(default)]
pub use_keychain: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary_provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary_api_key_anthropic: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary_api_key_openai: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary_api_key_openrouter: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary_model_anthropic: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary_model_openai: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary_model_openrouter: Option<String>,
#[serde(default)]
pub summary_auto: bool,
#[serde(default = "default_summary_auto_threshold")]
pub summary_auto_threshold: usize,
}
impl Default for Config {
fn default() -> Self {
Self {
watchers: vec!["claude-code".to_string()],
auto_link: false,
auto_link_threshold: 0.7,
commit_footer: false,
machine_id: None,
machine_name: None,
cloud_url: None,
encryption_salt: None,
use_keychain: false,
summary_provider: None,
summary_api_key_anthropic: None,
summary_api_key_openai: None,
summary_api_key_openrouter: None,
summary_model_anthropic: None,
summary_model_openai: None,
summary_model_openrouter: None,
summary_auto: false,
summary_auto_threshold: 4,
}
}
}
impl Config {
pub fn load() -> Result<Self> {
let path = Self::config_path()?;
Self::load_from_path(&path)
}
pub fn save(&self) -> Result<()> {
let path = Self::config_path()?;
self.save_to_path(&path)
}
pub fn load_from_path(path: &Path) -> Result<Self> {
if !path.exists() {
return Ok(Self::default());
}
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {}", path.display()))?;
if content.trim().is_empty() {
return Ok(Self::default());
}
let config: Config = serde_saphyr::from_str(&content)
.with_context(|| format!("Failed to parse config file: {}", path.display()))?;
Ok(config)
}
pub fn save_to_path(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).with_context(|| {
format!("Failed to create config directory: {}", parent.display())
})?;
}
let content = serde_saphyr::to_string(self).context("Failed to serialize config")?;
fs::write(path, content)
.with_context(|| format!("Failed to write config file: {}", path.display()))?;
Ok(())
}
pub fn get_or_create_machine_id(&mut self) -> Result<String> {
if let Some(ref id) = self.machine_id {
return Ok(id.clone());
}
let id = Uuid::new_v4().to_string();
self.machine_id = Some(id.clone());
self.save()?;
Ok(id)
}
pub fn get_machine_name(&self) -> String {
if let Some(ref name) = self.machine_name {
return name.clone();
}
hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_else(|| "unknown".to_string())
}
pub fn set_machine_name(&mut self, name: &str) -> Result<()> {
self.machine_name = Some(name.to_string());
self.save()
}
pub fn get_cloud_url(&self) -> String {
self.cloud_url
.clone()
.unwrap_or_else(|| "https://app.lore.varalys.com".to_string())
}
#[allow(dead_code)]
pub fn set_cloud_url(&mut self, url: &str) -> Result<()> {
self.cloud_url = Some(url.to_string());
self.save()
}
pub fn get_or_create_encryption_salt(&mut self) -> Result<String> {
if let Some(ref salt) = self.encryption_salt {
return Ok(salt.clone());
}
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use rand::RngCore;
let mut salt_bytes = [0u8; 16];
rand::thread_rng().fill_bytes(&mut salt_bytes);
let salt_b64 = BASE64.encode(salt_bytes);
self.encryption_salt = Some(salt_b64.clone());
self.save()?;
Ok(salt_b64)
}
pub fn get(&self, key: &str) -> Option<String> {
match key {
"watchers" => Some(self.watchers.join(",")),
"auto_link" => Some(self.auto_link.to_string()),
"auto_link_threshold" => Some(self.auto_link_threshold.to_string()),
"commit_footer" => Some(self.commit_footer.to_string()),
"machine_id" => self.machine_id.clone(),
"machine_name" => Some(self.get_machine_name()),
"cloud_url" => Some(self.get_cloud_url()),
"encryption_salt" => self.encryption_salt.clone(),
"use_keychain" => Some(self.use_keychain.to_string()),
"summary_provider" => self.summary_provider.clone(),
"summary_api_key_anthropic" => self.summary_api_key_anthropic.clone(),
"summary_api_key_openai" => self.summary_api_key_openai.clone(),
"summary_api_key_openrouter" => self.summary_api_key_openrouter.clone(),
"summary_model_anthropic" => self.summary_model_anthropic.clone(),
"summary_model_openai" => self.summary_model_openai.clone(),
"summary_model_openrouter" => self.summary_model_openrouter.clone(),
"summary_auto" => Some(self.summary_auto.to_string()),
"summary_auto_threshold" => Some(self.summary_auto_threshold.to_string()),
_ => None,
}
}
pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
match key {
"watchers" => {
self.watchers = value
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
"auto_link" => {
self.auto_link = parse_bool(value)
.with_context(|| format!("Invalid value for auto_link: '{value}'"))?;
}
"auto_link_threshold" => {
let threshold: f64 = value
.parse()
.with_context(|| format!("Invalid value for auto_link_threshold: '{value}'"))?;
if !(0.0..=1.0).contains(&threshold) {
bail!("auto_link_threshold must be between 0.0 and 1.0, got {threshold}");
}
self.auto_link_threshold = threshold;
}
"commit_footer" => {
self.commit_footer = parse_bool(value)
.with_context(|| format!("Invalid value for commit_footer: '{value}'"))?;
}
"machine_name" => {
self.machine_name = Some(value.to_string());
}
"cloud_url" => {
self.cloud_url = Some(value.to_string());
}
"machine_id" => {
bail!("machine_id cannot be set manually; it is auto-generated");
}
"encryption_salt" => {
bail!("encryption_salt cannot be set manually; it is auto-generated");
}
"use_keychain" => {
self.use_keychain = parse_bool(value).with_context(|| {
format!("Invalid boolean value for use_keychain: '{value}'")
})?;
}
"summary_provider" => {
let lower = value.to_lowercase();
match lower.as_str() {
"anthropic" | "openai" | "openrouter" => {
self.summary_provider = Some(lower);
}
_ => {
bail!(
"Invalid summary_provider: '{value}'. \
Must be one of: anthropic, openai, openrouter"
);
}
}
}
"summary_api_key_anthropic" => {
self.summary_api_key_anthropic = Some(value.to_string());
}
"summary_api_key_openai" => {
self.summary_api_key_openai = Some(value.to_string());
}
"summary_api_key_openrouter" => {
self.summary_api_key_openrouter = Some(value.to_string());
}
"summary_model_anthropic" => {
self.summary_model_anthropic = Some(value.to_string());
}
"summary_model_openai" => {
self.summary_model_openai = Some(value.to_string());
}
"summary_model_openrouter" => {
self.summary_model_openrouter = Some(value.to_string());
}
"summary_auto" => {
self.summary_auto = parse_bool(value)
.with_context(|| format!("Invalid value for summary_auto: '{value}'"))?;
}
"summary_auto_threshold" => {
let threshold: usize = value.parse().with_context(|| {
format!("Invalid value for summary_auto_threshold: '{value}'")
})?;
if threshold == 0 {
bail!("summary_auto_threshold must be greater than 0, got {threshold}");
}
self.summary_auto_threshold = threshold;
}
_ => {
bail!("Unknown configuration key: '{key}'");
}
}
Ok(())
}
pub fn config_path() -> Result<PathBuf> {
let config_dir = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?
.join(".lore");
Ok(config_dir.join("config.yaml"))
}
pub fn valid_keys() -> &'static [&'static str] {
&[
"watchers",
"auto_link",
"auto_link_threshold",
"commit_footer",
"machine_id",
"machine_name",
"cloud_url",
"encryption_salt",
"use_keychain",
"summary_provider",
"summary_api_key_anthropic",
"summary_api_key_openai",
"summary_api_key_openrouter",
"summary_model_anthropic",
"summary_model_openai",
"summary_model_openrouter",
"summary_auto",
"summary_auto_threshold",
]
}
pub fn summary_api_key_for_provider(&self, provider: &str) -> Option<String> {
match provider {
"anthropic" => self.summary_api_key_anthropic.clone(),
"openai" => self.summary_api_key_openai.clone(),
"openrouter" => self.summary_api_key_openrouter.clone(),
_ => None,
}
}
pub fn summary_model_for_provider(&self, provider: &str) -> Option<String> {
match provider {
"anthropic" => self.summary_model_anthropic.clone(),
"openai" => self.summary_model_openai.clone(),
"openrouter" => self.summary_model_openrouter.clone(),
_ => None,
}
}
pub fn is_use_keychain_configured() -> Result<bool> {
let path = Self::config_path()?;
if !path.exists() {
return Ok(false);
}
let content = fs::read_to_string(&path)
.with_context(|| format!("Failed to read config file: {}", path.display()))?;
if content.trim().is_empty() {
return Ok(false);
}
Ok(content.lines().any(|line| {
let trimmed = line.trim();
trimmed.starts_with("use_keychain:")
}))
}
}
fn default_summary_auto_threshold() -> usize {
4
}
fn parse_bool(value: &str) -> Result<bool> {
match value.to_lowercase().as_str() {
"true" | "1" | "yes" => Ok(true),
"false" | "0" | "no" => Ok(false),
_ => bail!("Expected 'true' or 'false', got '{value}'"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.watchers, vec!["claude-code".to_string()]);
assert!(!config.auto_link);
assert!((config.auto_link_threshold - 0.7).abs() < f64::EPSILON);
assert!(!config.commit_footer);
assert!(config.machine_id.is_none());
assert!(config.machine_name.is_none());
}
#[test]
fn test_save_and_load_roundtrip() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("config.yaml");
let config = Config {
auto_link: true,
auto_link_threshold: 0.8,
watchers: vec!["claude-code".to_string(), "cursor".to_string()],
machine_id: Some("test-uuid".to_string()),
machine_name: Some("test-name".to_string()),
..Default::default()
};
config.save_to_path(&path).unwrap();
let loaded = Config::load_from_path(&path).unwrap();
assert_eq!(loaded, config);
}
#[test]
fn test_save_creates_parent_directories() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir
.path()
.join("nested")
.join("dir")
.join("config.yaml");
let config = Config::default();
config.save_to_path(&path).unwrap();
assert!(path.exists());
}
#[test]
fn test_load_returns_default_for_missing_or_empty_file() {
let temp_dir = TempDir::new().unwrap();
let nonexistent = temp_dir.path().join("nonexistent.yaml");
let config = Config::load_from_path(&nonexistent).unwrap();
assert_eq!(config, Config::default());
let empty = temp_dir.path().join("empty.yaml");
fs::write(&empty, "").unwrap();
let config = Config::load_from_path(&empty).unwrap();
assert_eq!(config, Config::default());
}
#[test]
fn test_get_returns_expected_values() {
let config = Config {
watchers: vec!["claude-code".to_string(), "cursor".to_string()],
auto_link: true,
auto_link_threshold: 0.85,
commit_footer: true,
machine_id: Some("test-uuid".to_string()),
machine_name: Some("test-machine".to_string()),
cloud_url: None,
encryption_salt: None,
use_keychain: false,
..Default::default()
};
assert_eq!(
config.get("watchers"),
Some("claude-code,cursor".to_string())
);
assert_eq!(config.get("auto_link"), Some("true".to_string()));
assert_eq!(config.get("auto_link_threshold"), Some("0.85".to_string()));
assert_eq!(config.get("commit_footer"), Some("true".to_string()));
assert_eq!(config.get("machine_id"), Some("test-uuid".to_string()));
assert_eq!(config.get("machine_name"), Some("test-machine".to_string()));
assert_eq!(config.get("use_keychain"), Some("false".to_string()));
assert_eq!(config.get("unknown_key"), None);
}
#[test]
fn test_set_updates_values() {
let mut config = Config::default();
config
.set("watchers", "claude-code, cursor, copilot")
.unwrap();
assert_eq!(
config.watchers,
vec![
"claude-code".to_string(),
"cursor".to_string(),
"copilot".to_string()
]
);
config.set("auto_link", "true").unwrap();
assert!(config.auto_link);
config.set("auto_link", "no").unwrap();
assert!(!config.auto_link);
config.set("commit_footer", "yes").unwrap();
assert!(config.commit_footer);
config.set("auto_link_threshold", "0.5").unwrap();
assert!((config.auto_link_threshold - 0.5).abs() < f64::EPSILON);
config.set("machine_name", "dev-workstation").unwrap();
assert_eq!(config.machine_name, Some("dev-workstation".to_string()));
}
#[test]
fn test_set_validates_threshold_range() {
let mut config = Config::default();
config.set("auto_link_threshold", "0.0").unwrap();
assert!((config.auto_link_threshold - 0.0).abs() < f64::EPSILON);
config.set("auto_link_threshold", "1.0").unwrap();
assert!((config.auto_link_threshold - 1.0).abs() < f64::EPSILON);
assert!(config.set("auto_link_threshold", "-0.1").is_err());
assert!(config.set("auto_link_threshold", "1.1").is_err());
assert!(config.set("auto_link_threshold", "not_a_number").is_err());
}
#[test]
fn test_set_rejects_invalid_input() {
let mut config = Config::default();
assert!(config.set("unknown_key", "value").is_err());
assert!(config.set("auto_link", "maybe").is_err());
let result = config.set("machine_id", "some-uuid");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("cannot be set manually"));
}
#[test]
fn test_parse_bool_accepts_multiple_formats() {
assert!(parse_bool("true").unwrap());
assert!(parse_bool("TRUE").unwrap());
assert!(parse_bool("1").unwrap());
assert!(parse_bool("yes").unwrap());
assert!(parse_bool("YES").unwrap());
assert!(!parse_bool("false").unwrap());
assert!(!parse_bool("FALSE").unwrap());
assert!(!parse_bool("0").unwrap());
assert!(!parse_bool("no").unwrap());
assert!(parse_bool("invalid").is_err());
}
#[test]
fn test_machine_name_fallback_to_hostname() {
let config = Config::default();
let name = config.get_machine_name();
assert!(!name.is_empty());
}
#[test]
fn test_machine_identity_yaml_serialization() {
let config = Config::default();
let yaml = serde_saphyr::to_string(&config).unwrap();
assert!(!yaml.contains("machine_id"));
assert!(!yaml.contains("machine_name"));
let config = Config {
machine_id: Some("uuid-1234".to_string()),
machine_name: Some("my-machine".to_string()),
..Default::default()
};
let yaml = serde_saphyr::to_string(&config).unwrap();
assert!(yaml.contains("machine_id"));
assert!(yaml.contains("machine_name"));
}
#[test]
fn test_is_use_keychain_configured_with_default_config() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.yaml");
let config = Config::default();
config.save_to_path(&config_path).unwrap();
let content = fs::read_to_string(&config_path).unwrap();
let has_use_keychain = content.lines().any(|line| {
let trimmed = line.trim();
trimmed.starts_with("use_keychain:")
});
assert!(has_use_keychain);
}
#[test]
fn test_is_use_keychain_configured_detects_explicit_setting() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.yaml");
let config = Config {
use_keychain: true,
..Default::default()
};
config.save_to_path(&config_path).unwrap();
let content = fs::read_to_string(&config_path).unwrap();
let has_use_keychain = content.lines().any(|line| {
let trimmed = line.trim();
trimmed.starts_with("use_keychain:")
});
assert!(has_use_keychain);
}
#[test]
fn test_is_use_keychain_configured_returns_false_for_empty_file() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.yaml");
fs::write(&config_path, "").unwrap();
let content = fs::read_to_string(&config_path).unwrap();
let has_use_keychain = content.lines().any(|line| {
let trimmed = line.trim();
trimmed.starts_with("use_keychain:")
});
assert!(!has_use_keychain);
}
#[test]
fn test_default_config_summary_fields() {
let config = Config::default();
assert!(config.summary_provider.is_none());
assert!(config.summary_api_key_anthropic.is_none());
assert!(config.summary_api_key_openai.is_none());
assert!(config.summary_api_key_openrouter.is_none());
assert!(config.summary_model_anthropic.is_none());
assert!(config.summary_model_openai.is_none());
assert!(config.summary_model_openrouter.is_none());
assert!(!config.summary_auto);
assert_eq!(config.summary_auto_threshold, 4);
}
#[test]
fn test_get_set_summary_provider() {
let mut config = Config::default();
assert_eq!(config.get("summary_provider"), None);
config.set("summary_provider", "anthropic").unwrap();
assert_eq!(
config.get("summary_provider"),
Some("anthropic".to_string())
);
config.set("summary_provider", "OpenAI").unwrap();
assert_eq!(config.get("summary_provider"), Some("openai".to_string()));
config.set("summary_provider", "OPENROUTER").unwrap();
assert_eq!(
config.get("summary_provider"),
Some("openrouter".to_string())
);
}
#[test]
fn test_set_summary_provider_validates() {
let mut config = Config::default();
let result = config.set("summary_provider", "invalid-provider");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Invalid summary_provider"));
assert!(err_msg.contains("invalid-provider"));
}
#[test]
fn test_get_set_summary_api_keys_per_provider() {
let mut config = Config::default();
assert_eq!(config.get("summary_api_key_anthropic"), None);
assert_eq!(config.get("summary_api_key_openai"), None);
assert_eq!(config.get("summary_api_key_openrouter"), None);
config
.set("summary_api_key_anthropic", "sk-ant-123")
.unwrap();
config.set("summary_api_key_openai", "sk-oai-456").unwrap();
config
.set("summary_api_key_openrouter", "sk-or-789")
.unwrap();
assert_eq!(
config.get("summary_api_key_anthropic"),
Some("sk-ant-123".to_string())
);
assert_eq!(
config.get("summary_api_key_openai"),
Some("sk-oai-456".to_string())
);
assert_eq!(
config.get("summary_api_key_openrouter"),
Some("sk-or-789".to_string())
);
assert_eq!(
config.summary_api_key_for_provider("anthropic"),
Some("sk-ant-123".to_string())
);
assert_eq!(
config.summary_api_key_for_provider("openai"),
Some("sk-oai-456".to_string())
);
assert_eq!(
config.summary_api_key_for_provider("openrouter"),
Some("sk-or-789".to_string())
);
assert_eq!(config.summary_api_key_for_provider("unknown"), None);
}
#[test]
fn test_get_set_summary_models_per_provider() {
let mut config = Config::default();
assert_eq!(config.get("summary_model_anthropic"), None);
assert_eq!(config.get("summary_model_openai"), None);
assert_eq!(config.get("summary_model_openrouter"), None);
config
.set("summary_model_anthropic", "claude-sonnet-4-20250514")
.unwrap();
config.set("summary_model_openai", "gpt-4o").unwrap();
config
.set(
"summary_model_openrouter",
"meta-llama/llama-3.1-8b-instruct:free",
)
.unwrap();
assert_eq!(
config.get("summary_model_anthropic"),
Some("claude-sonnet-4-20250514".to_string())
);
assert_eq!(
config.get("summary_model_openai"),
Some("gpt-4o".to_string())
);
assert_eq!(
config.get("summary_model_openrouter"),
Some("meta-llama/llama-3.1-8b-instruct:free".to_string())
);
assert_eq!(
config.summary_model_for_provider("anthropic"),
Some("claude-sonnet-4-20250514".to_string())
);
assert_eq!(
config.summary_model_for_provider("openai"),
Some("gpt-4o".to_string())
);
assert_eq!(config.summary_model_for_provider("unknown"), None);
}
#[test]
fn test_get_set_summary_auto() {
let mut config = Config::default();
assert_eq!(config.get("summary_auto"), Some("false".to_string()));
config.set("summary_auto", "true").unwrap();
assert!(config.summary_auto);
assert_eq!(config.get("summary_auto"), Some("true".to_string()));
config.set("summary_auto", "false").unwrap();
assert!(!config.summary_auto);
assert_eq!(config.get("summary_auto"), Some("false".to_string()));
assert!(config.set("summary_auto", "maybe").is_err());
}
#[test]
fn test_get_set_summary_auto_threshold() {
let mut config = Config::default();
assert_eq!(config.get("summary_auto_threshold"), Some("4".to_string()));
config.set("summary_auto_threshold", "10").unwrap();
assert_eq!(config.summary_auto_threshold, 10);
assert_eq!(config.get("summary_auto_threshold"), Some("10".to_string()));
let result = config.set("summary_auto_threshold", "0");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("must be greater than 0"));
assert!(config.set("summary_auto_threshold", "-1").is_err());
assert!(config.set("summary_auto_threshold", "abc").is_err());
}
#[test]
fn test_summary_fields_yaml_serialization() {
let config = Config::default();
let yaml = serde_saphyr::to_string(&config).unwrap();
assert!(!yaml.contains("summary_provider"));
assert!(!yaml.contains("summary_api_key"));
assert!(!yaml.contains("summary_model"));
let config = Config {
summary_provider: Some("anthropic".to_string()),
summary_api_key_anthropic: Some("sk-ant-test".to_string()),
summary_api_key_openai: Some("sk-oai-test".to_string()),
summary_model_anthropic: Some("claude-sonnet-4-20250514".to_string()),
summary_auto: true,
summary_auto_threshold: 8,
..Default::default()
};
let yaml = serde_saphyr::to_string(&config).unwrap();
assert!(yaml.contains("summary_provider"));
assert!(yaml.contains("anthropic"));
assert!(yaml.contains("summary_api_key_anthropic"));
assert!(yaml.contains("sk-ant-test"));
assert!(yaml.contains("summary_api_key_openai"));
assert!(yaml.contains("sk-oai-test"));
assert!(yaml.contains("summary_model_anthropic"));
assert!(yaml.contains("claude-sonnet-4-20250514"));
assert!(yaml.contains("summary_auto"));
assert!(yaml.contains("summary_auto_threshold"));
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("config.yaml");
config.save_to_path(&path).unwrap();
let loaded = Config::load_from_path(&path).unwrap();
assert_eq!(loaded.summary_provider, Some("anthropic".to_string()));
assert_eq!(
loaded.summary_api_key_anthropic,
Some("sk-ant-test".to_string())
);
assert_eq!(
loaded.summary_api_key_openai,
Some("sk-oai-test".to_string())
);
assert_eq!(
loaded.summary_model_anthropic,
Some("claude-sonnet-4-20250514".to_string())
);
assert!(loaded.summary_auto);
assert_eq!(loaded.summary_auto_threshold, 8);
}
}