mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Add interactive setup wizard and persistent settings
- Add 7-step setup wizard: database, security, auth, model, embeddings, channels, heartbeat - Store settings in ~/.ironclaw/settings.json with env var > settings > default priority - Add OS keychain integration for secrets master key (macOS/Linux) - Add `ironclaw config` CLI subcommand (list/get/set/reset/path) - Expand Settings struct with all configuration fields - Enhanced setup detection to auto-trigger wizard when needed Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
598dd43b1c
commit
0ab9643843
+649
-6
@@ -1,6 +1,7 @@
|
||||
//! User settings persistence.
|
||||
//!
|
||||
//! Stores user preferences like selected model in ~/.ironclaw/settings.json.
|
||||
//! Stores user preferences in ~/.ironclaw/settings.json.
|
||||
//! Settings are loaded with env var > settings.json > default priority.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -9,21 +10,118 @@ use serde::{Deserialize, Serialize};
|
||||
/// User settings persisted to disk.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Settings {
|
||||
/// Currently selected model.
|
||||
#[serde(default)]
|
||||
pub selected_model: Option<String>,
|
||||
|
||||
/// Whether setup wizard has been completed.
|
||||
#[serde(default)]
|
||||
pub setup_completed: bool,
|
||||
|
||||
/// Tunnel configuration for exposing the agent to the internet.
|
||||
// === Step 1: Database ===
|
||||
/// Database connection URL (postgres://...).
|
||||
#[serde(default)]
|
||||
pub database_url: Option<String>,
|
||||
|
||||
/// Database pool size.
|
||||
#[serde(default)]
|
||||
pub database_pool_size: Option<usize>,
|
||||
|
||||
// === Step 2: Security ===
|
||||
/// Source for the secrets master key.
|
||||
#[serde(default)]
|
||||
pub secrets_master_key_source: KeySource,
|
||||
|
||||
// === Step 3: NEAR AI Auth ===
|
||||
// Session stored separately in session.json
|
||||
|
||||
// === Step 4: Model Selection ===
|
||||
/// Currently selected model.
|
||||
#[serde(default)]
|
||||
pub selected_model: Option<String>,
|
||||
|
||||
// === Step 5: Embeddings ===
|
||||
/// Embeddings configuration.
|
||||
#[serde(default)]
|
||||
pub embeddings: EmbeddingsSettings,
|
||||
|
||||
// === Step 6: Channels ===
|
||||
/// Tunnel configuration for public webhook endpoints.
|
||||
#[serde(default)]
|
||||
pub tunnel: TunnelSettings,
|
||||
|
||||
/// Channel configuration.
|
||||
#[serde(default)]
|
||||
pub channels: ChannelSettings,
|
||||
|
||||
// === Step 7: Heartbeat ===
|
||||
/// Heartbeat configuration.
|
||||
#[serde(default)]
|
||||
pub heartbeat: HeartbeatSettings,
|
||||
|
||||
// === Advanced Settings (not asked during setup, editable via CLI) ===
|
||||
/// Agent behavior configuration.
|
||||
#[serde(default)]
|
||||
pub agent: AgentSettings,
|
||||
|
||||
/// WASM sandbox configuration.
|
||||
#[serde(default)]
|
||||
pub wasm: WasmSettings,
|
||||
|
||||
/// Docker sandbox configuration.
|
||||
#[serde(default)]
|
||||
pub sandbox: SandboxSettings,
|
||||
|
||||
/// Safety configuration.
|
||||
#[serde(default)]
|
||||
pub safety: SafetySettings,
|
||||
|
||||
/// Builder configuration.
|
||||
#[serde(default)]
|
||||
pub builder: BuilderSettings,
|
||||
}
|
||||
|
||||
/// Source for the secrets master key.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum KeySource {
|
||||
/// Auto-generated key stored in OS keychain.
|
||||
Keychain,
|
||||
/// User provides via SECRETS_MASTER_KEY env var.
|
||||
Env,
|
||||
/// Not configured (secrets features disabled).
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
/// Embeddings configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EmbeddingsSettings {
|
||||
/// Whether embeddings are enabled.
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Provider to use: "openai" or "nearai".
|
||||
#[serde(default = "default_embeddings_provider")]
|
||||
pub provider: String,
|
||||
|
||||
/// Model to use for embeddings.
|
||||
#[serde(default = "default_embeddings_model")]
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
fn default_embeddings_provider() -> String {
|
||||
"nearai".to_string()
|
||||
}
|
||||
|
||||
fn default_embeddings_model() -> String {
|
||||
"text-embedding-3-small".to_string()
|
||||
}
|
||||
|
||||
impl Default for EmbeddingsSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
provider: default_embeddings_provider(),
|
||||
model: default_embeddings_model(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tunnel settings for public webhook endpoints.
|
||||
@@ -47,11 +145,330 @@ pub struct ChannelSettings {
|
||||
#[serde(default)]
|
||||
pub http_port: Option<u16>,
|
||||
|
||||
/// HTTP webhook host.
|
||||
#[serde(default)]
|
||||
pub http_host: Option<String>,
|
||||
|
||||
/// Enabled WASM channels by name.
|
||||
/// Channels not in this list but present in the channels directory will still load.
|
||||
/// This is primarily used by the setup wizard to track which channels were configured.
|
||||
#[serde(default)]
|
||||
pub wasm_channels: Vec<String>,
|
||||
|
||||
/// Whether WASM channels are enabled.
|
||||
#[serde(default = "default_true")]
|
||||
pub wasm_channels_enabled: bool,
|
||||
|
||||
/// Directory containing WASM channel modules.
|
||||
#[serde(default)]
|
||||
pub wasm_channels_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Heartbeat configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HeartbeatSettings {
|
||||
/// Whether heartbeat is enabled.
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Interval between heartbeat checks in seconds.
|
||||
#[serde(default = "default_heartbeat_interval")]
|
||||
pub interval_secs: u64,
|
||||
|
||||
/// Channel to notify on heartbeat findings.
|
||||
#[serde(default)]
|
||||
pub notify_channel: Option<String>,
|
||||
|
||||
/// User ID to notify on heartbeat findings.
|
||||
#[serde(default)]
|
||||
pub notify_user: Option<String>,
|
||||
}
|
||||
|
||||
fn default_heartbeat_interval() -> u64 {
|
||||
1800 // 30 minutes
|
||||
}
|
||||
|
||||
impl Default for HeartbeatSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
interval_secs: default_heartbeat_interval(),
|
||||
notify_channel: None,
|
||||
notify_user: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Agent behavior configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentSettings {
|
||||
/// Agent name.
|
||||
#[serde(default = "default_agent_name")]
|
||||
pub name: String,
|
||||
|
||||
/// Maximum parallel jobs.
|
||||
#[serde(default = "default_max_parallel_jobs")]
|
||||
pub max_parallel_jobs: u32,
|
||||
|
||||
/// Job timeout in seconds.
|
||||
#[serde(default = "default_job_timeout")]
|
||||
pub job_timeout_secs: u64,
|
||||
|
||||
/// Stuck job threshold in seconds.
|
||||
#[serde(default = "default_stuck_threshold")]
|
||||
pub stuck_threshold_secs: u64,
|
||||
|
||||
/// Whether to use planning before tool execution.
|
||||
#[serde(default = "default_true")]
|
||||
pub use_planning: bool,
|
||||
|
||||
/// Self-repair check interval in seconds.
|
||||
#[serde(default = "default_repair_interval")]
|
||||
pub repair_check_interval_secs: u64,
|
||||
|
||||
/// Maximum repair attempts.
|
||||
#[serde(default = "default_max_repair_attempts")]
|
||||
pub max_repair_attempts: u32,
|
||||
}
|
||||
|
||||
fn default_agent_name() -> String {
|
||||
"ironclaw".to_string()
|
||||
}
|
||||
|
||||
fn default_max_parallel_jobs() -> u32 {
|
||||
5
|
||||
}
|
||||
|
||||
fn default_job_timeout() -> u64 {
|
||||
3600 // 1 hour
|
||||
}
|
||||
|
||||
fn default_stuck_threshold() -> u64 {
|
||||
300 // 5 minutes
|
||||
}
|
||||
|
||||
fn default_repair_interval() -> u64 {
|
||||
60 // 1 minute
|
||||
}
|
||||
|
||||
fn default_max_repair_attempts() -> u32 {
|
||||
3
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for AgentSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: default_agent_name(),
|
||||
max_parallel_jobs: default_max_parallel_jobs(),
|
||||
job_timeout_secs: default_job_timeout(),
|
||||
stuck_threshold_secs: default_stuck_threshold(),
|
||||
use_planning: true,
|
||||
repair_check_interval_secs: default_repair_interval(),
|
||||
max_repair_attempts: default_max_repair_attempts(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WASM sandbox configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WasmSettings {
|
||||
/// Whether WASM tool execution is enabled.
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Directory containing installed WASM tools.
|
||||
#[serde(default)]
|
||||
pub tools_dir: Option<PathBuf>,
|
||||
|
||||
/// Default memory limit in bytes.
|
||||
#[serde(default = "default_wasm_memory_limit")]
|
||||
pub default_memory_limit: u64,
|
||||
|
||||
/// Default execution timeout in seconds.
|
||||
#[serde(default = "default_wasm_timeout")]
|
||||
pub default_timeout_secs: u64,
|
||||
|
||||
/// Default fuel limit for CPU metering.
|
||||
#[serde(default = "default_wasm_fuel_limit")]
|
||||
pub default_fuel_limit: u64,
|
||||
|
||||
/// Whether to cache compiled modules.
|
||||
#[serde(default = "default_true")]
|
||||
pub cache_compiled: bool,
|
||||
|
||||
/// Directory for compiled module cache.
|
||||
#[serde(default)]
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn default_wasm_memory_limit() -> u64 {
|
||||
10 * 1024 * 1024 // 10 MB
|
||||
}
|
||||
|
||||
fn default_wasm_timeout() -> u64 {
|
||||
60
|
||||
}
|
||||
|
||||
fn default_wasm_fuel_limit() -> u64 {
|
||||
10_000_000
|
||||
}
|
||||
|
||||
impl Default for WasmSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
tools_dir: None,
|
||||
default_memory_limit: default_wasm_memory_limit(),
|
||||
default_timeout_secs: default_wasm_timeout(),
|
||||
default_fuel_limit: default_wasm_fuel_limit(),
|
||||
cache_compiled: true,
|
||||
cache_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Docker sandbox configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SandboxSettings {
|
||||
/// Whether the Docker sandbox is enabled.
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Sandbox policy: "readonly", "workspace_write", or "full_access".
|
||||
#[serde(default = "default_sandbox_policy")]
|
||||
pub policy: String,
|
||||
|
||||
/// Command timeout in seconds.
|
||||
#[serde(default = "default_sandbox_timeout")]
|
||||
pub timeout_secs: u64,
|
||||
|
||||
/// Memory limit in megabytes.
|
||||
#[serde(default = "default_sandbox_memory")]
|
||||
pub memory_limit_mb: u64,
|
||||
|
||||
/// CPU shares (relative weight).
|
||||
#[serde(default = "default_sandbox_cpu_shares")]
|
||||
pub cpu_shares: u32,
|
||||
|
||||
/// Docker image for the sandbox.
|
||||
#[serde(default = "default_sandbox_image")]
|
||||
pub image: String,
|
||||
|
||||
/// Whether to auto-pull the image if not found.
|
||||
#[serde(default = "default_true")]
|
||||
pub auto_pull_image: bool,
|
||||
|
||||
/// Additional domains to allow through the network proxy.
|
||||
#[serde(default)]
|
||||
pub extra_allowed_domains: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_sandbox_policy() -> String {
|
||||
"readonly".to_string()
|
||||
}
|
||||
|
||||
fn default_sandbox_timeout() -> u64 {
|
||||
120
|
||||
}
|
||||
|
||||
fn default_sandbox_memory() -> u64 {
|
||||
2048
|
||||
}
|
||||
|
||||
fn default_sandbox_cpu_shares() -> u32 {
|
||||
1024
|
||||
}
|
||||
|
||||
fn default_sandbox_image() -> String {
|
||||
"ghcr.io/nearai/sandbox:latest".to_string()
|
||||
}
|
||||
|
||||
impl Default for SandboxSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
policy: default_sandbox_policy(),
|
||||
timeout_secs: default_sandbox_timeout(),
|
||||
memory_limit_mb: default_sandbox_memory(),
|
||||
cpu_shares: default_sandbox_cpu_shares(),
|
||||
image: default_sandbox_image(),
|
||||
auto_pull_image: true,
|
||||
extra_allowed_domains: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Safety configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SafetySettings {
|
||||
/// Maximum output length in bytes.
|
||||
#[serde(default = "default_max_output_length")]
|
||||
pub max_output_length: usize,
|
||||
|
||||
/// Whether injection check is enabled.
|
||||
#[serde(default = "default_true")]
|
||||
pub injection_check_enabled: bool,
|
||||
}
|
||||
|
||||
fn default_max_output_length() -> usize {
|
||||
100_000
|
||||
}
|
||||
|
||||
impl Default for SafetySettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_output_length: default_max_output_length(),
|
||||
injection_check_enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BuilderSettings {
|
||||
/// Whether the software builder tool is enabled.
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Directory for build artifacts.
|
||||
#[serde(default)]
|
||||
pub build_dir: Option<PathBuf>,
|
||||
|
||||
/// Maximum iterations for the build loop.
|
||||
#[serde(default = "default_builder_max_iterations")]
|
||||
pub max_iterations: u32,
|
||||
|
||||
/// Build timeout in seconds.
|
||||
#[serde(default = "default_builder_timeout")]
|
||||
pub timeout_secs: u64,
|
||||
|
||||
/// Whether to automatically register built WASM tools.
|
||||
#[serde(default = "default_true")]
|
||||
pub auto_register: bool,
|
||||
}
|
||||
|
||||
fn default_builder_max_iterations() -> u32 {
|
||||
20
|
||||
}
|
||||
|
||||
fn default_builder_timeout() -> u64 {
|
||||
600
|
||||
}
|
||||
|
||||
impl Default for BuilderSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
build_dir: None,
|
||||
max_iterations: default_builder_max_iterations(),
|
||||
timeout_secs: default_builder_timeout(),
|
||||
auto_register: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
@@ -106,6 +523,163 @@ impl Settings {
|
||||
self.selected_model = Some(model.to_string());
|
||||
self.save()
|
||||
}
|
||||
|
||||
/// Get a setting value by dotted path (e.g., "agent.max_parallel_jobs").
|
||||
pub fn get(&self, path: &str) -> Option<String> {
|
||||
let json = serde_json::to_value(self).ok()?;
|
||||
let mut current = &json;
|
||||
|
||||
for part in path.split('.') {
|
||||
current = current.get(part)?;
|
||||
}
|
||||
|
||||
match current {
|
||||
serde_json::Value::String(s) => Some(s.clone()),
|
||||
serde_json::Value::Number(n) => Some(n.to_string()),
|
||||
serde_json::Value::Bool(b) => Some(b.to_string()),
|
||||
serde_json::Value::Null => Some("null".to_string()),
|
||||
serde_json::Value::Array(arr) => Some(serde_json::to_string(arr).unwrap_or_default()),
|
||||
serde_json::Value::Object(obj) => Some(serde_json::to_string(obj).unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a setting value by dotted path.
|
||||
///
|
||||
/// Returns error if path is invalid or value cannot be parsed.
|
||||
pub fn set(&mut self, path: &str, value: &str) -> Result<(), String> {
|
||||
let mut json = serde_json::to_value(&self)
|
||||
.map_err(|e| format!("Failed to serialize settings: {}", e))?;
|
||||
|
||||
let parts: Vec<&str> = path.split('.').collect();
|
||||
if parts.is_empty() {
|
||||
return Err("Empty path".to_string());
|
||||
}
|
||||
|
||||
// Navigate to parent and set the final key
|
||||
let mut current = &mut json;
|
||||
for part in &parts[..parts.len() - 1] {
|
||||
current = current
|
||||
.get_mut(*part)
|
||||
.ok_or_else(|| format!("Path not found: {}", path))?;
|
||||
}
|
||||
|
||||
let final_key = parts.last().unwrap();
|
||||
let obj = current
|
||||
.as_object_mut()
|
||||
.ok_or_else(|| format!("Parent is not an object: {}", path))?;
|
||||
|
||||
// Try to infer the type from the existing value
|
||||
let new_value = if let Some(existing) = obj.get(*final_key) {
|
||||
match existing {
|
||||
serde_json::Value::Bool(_) => {
|
||||
let b = value
|
||||
.parse::<bool>()
|
||||
.map_err(|_| format!("Expected boolean for {}, got '{}'", path, value))?;
|
||||
serde_json::Value::Bool(b)
|
||||
}
|
||||
serde_json::Value::Number(n) => {
|
||||
if n.is_u64() {
|
||||
let n = value.parse::<u64>().map_err(|_| {
|
||||
format!("Expected integer for {}, got '{}'", path, value)
|
||||
})?;
|
||||
serde_json::Value::Number(n.into())
|
||||
} else if n.is_i64() {
|
||||
let n = value.parse::<i64>().map_err(|_| {
|
||||
format!("Expected integer for {}, got '{}'", path, value)
|
||||
})?;
|
||||
serde_json::Value::Number(n.into())
|
||||
} else {
|
||||
let n = value.parse::<f64>().map_err(|_| {
|
||||
format!("Expected number for {}, got '{}'", path, value)
|
||||
})?;
|
||||
serde_json::Number::from_f64(n)
|
||||
.map(serde_json::Value::Number)
|
||||
.unwrap_or(serde_json::Value::String(value.to_string()))
|
||||
}
|
||||
}
|
||||
serde_json::Value::Null => {
|
||||
// Could be Option<T>, try to parse as JSON or use string
|
||||
serde_json::from_str(value)
|
||||
.unwrap_or(serde_json::Value::String(value.to_string()))
|
||||
}
|
||||
serde_json::Value::Array(_) => serde_json::from_str(value)
|
||||
.map_err(|e| format!("Invalid JSON array for {}: {}", path, e))?,
|
||||
serde_json::Value::Object(_) => serde_json::from_str(value)
|
||||
.map_err(|e| format!("Invalid JSON object for {}: {}", path, e))?,
|
||||
serde_json::Value::String(_) => serde_json::Value::String(value.to_string()),
|
||||
}
|
||||
} else {
|
||||
// Key doesn't exist, try to parse as JSON or use string
|
||||
serde_json::from_str(value).unwrap_or(serde_json::Value::String(value.to_string()))
|
||||
};
|
||||
|
||||
obj.insert((*final_key).to_string(), new_value);
|
||||
|
||||
// Deserialize back to Settings
|
||||
*self =
|
||||
serde_json::from_value(json).map_err(|e| format!("Failed to apply setting: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reset a setting to its default value.
|
||||
pub fn reset(&mut self, path: &str) -> Result<(), String> {
|
||||
let default = Self::default();
|
||||
let default_value = default
|
||||
.get(path)
|
||||
.ok_or_else(|| format!("Unknown setting: {}", path))?;
|
||||
|
||||
self.set(path, &default_value)
|
||||
}
|
||||
|
||||
/// List all settings as (path, value) pairs.
|
||||
pub fn list(&self) -> Vec<(String, String)> {
|
||||
let json = match serde_json::to_value(self) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let mut results = Vec::new();
|
||||
collect_settings(&json, String::new(), &mut results);
|
||||
results.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively collect settings paths and values.
|
||||
fn collect_settings(
|
||||
value: &serde_json::Value,
|
||||
prefix: String,
|
||||
results: &mut Vec<(String, String)>,
|
||||
) {
|
||||
match value {
|
||||
serde_json::Value::Object(obj) => {
|
||||
for (key, val) in obj {
|
||||
let path = if prefix.is_empty() {
|
||||
key.clone()
|
||||
} else {
|
||||
format!("{}.{}", prefix, key)
|
||||
};
|
||||
collect_settings(val, path, results);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Array(arr) => {
|
||||
let display = serde_json::to_string(arr).unwrap_or_default();
|
||||
results.push((prefix, display));
|
||||
}
|
||||
serde_json::Value::String(s) => {
|
||||
results.push((prefix, s.clone()));
|
||||
}
|
||||
serde_json::Value::Number(n) => {
|
||||
results.push((prefix, n.to_string()));
|
||||
}
|
||||
serde_json::Value::Bool(b) => {
|
||||
results.push((prefix, b.to_string()));
|
||||
}
|
||||
serde_json::Value::Null => {
|
||||
results.push((prefix, "null".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -146,4 +720,73 @@ mod tests {
|
||||
};
|
||||
assert_eq!(settings.model_or("default-model"), "my-model".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_setting() {
|
||||
let settings = Settings::default();
|
||||
|
||||
assert_eq!(settings.get("agent.name"), Some("ironclaw".to_string()));
|
||||
assert_eq!(
|
||||
settings.get("agent.max_parallel_jobs"),
|
||||
Some("5".to_string())
|
||||
);
|
||||
assert_eq!(settings.get("heartbeat.enabled"), Some("false".to_string()));
|
||||
assert_eq!(settings.get("nonexistent"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_setting() {
|
||||
let mut settings = Settings::default();
|
||||
|
||||
settings.set("agent.name", "mybot").unwrap();
|
||||
assert_eq!(settings.agent.name, "mybot");
|
||||
|
||||
settings.set("agent.max_parallel_jobs", "10").unwrap();
|
||||
assert_eq!(settings.agent.max_parallel_jobs, 10);
|
||||
|
||||
settings.set("heartbeat.enabled", "true").unwrap();
|
||||
assert!(settings.heartbeat.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_setting() {
|
||||
let mut settings = Settings::default();
|
||||
|
||||
settings.agent.name = "custom".to_string();
|
||||
settings.reset("agent.name").unwrap();
|
||||
assert_eq!(settings.agent.name, "ironclaw");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_settings() {
|
||||
let settings = Settings::default();
|
||||
let list = settings.list();
|
||||
|
||||
// Check some expected entries
|
||||
assert!(list.iter().any(|(k, _)| k == "agent.name"));
|
||||
assert!(list.iter().any(|(k, _)| k == "heartbeat.enabled"));
|
||||
assert!(list.iter().any(|(k, _)| k == "setup_completed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_source_serialization() {
|
||||
let settings = Settings {
|
||||
secrets_master_key_source: KeySource::Keychain,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&settings).unwrap();
|
||||
assert!(json.contains("\"keychain\""));
|
||||
|
||||
let loaded: Settings = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(loaded.secrets_master_key_source, KeySource::Keychain);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_embeddings_defaults() {
|
||||
let settings = Settings::default();
|
||||
assert!(!settings.embeddings.enabled);
|
||||
assert_eq!(settings.embeddings.provider, "nearai");
|
||||
assert_eq!(settings.embeddings.model, "text-embedding-3-small");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user