use theway_transport::config::{ModelDefault, ThinkingSummarySettings};
use theway_transport::triggers::DEFAULT_DYNAMIC_TRIGGER_POLL_INTERVAL_SECS;
use theway_transport::wire::WireDaemonConfig;
#[derive(Clone, Debug, PartialEq)]
pub struct StartupConfig {
pub model_default: Option<ModelDefault>,
pub builtin_skills: Vec<String>,
pub trigger_poll_secs: u64,
pub tui_max_feed_lines: Option<u64>,
pub thinking_summary: Option<ThinkingSummarySettings>,
pub load_local_sources: bool,
pub storage_service_addr: Option<String>,
}
impl Default for StartupConfig {
fn default() -> Self {
Self {
model_default: None,
builtin_skills: Vec::new(),
trigger_poll_secs: DEFAULT_DYNAMIC_TRIGGER_POLL_INTERVAL_SECS,
tui_max_feed_lines: None,
thinking_summary: None,
load_local_sources: true,
storage_service_addr: None,
}
}
}
impl StartupConfig {
pub fn from_wire(payload: &WireDaemonConfig) -> Self {
let mut config = Self::default();
config.apply_wire(payload);
config
}
pub fn apply_wire(&mut self, patch: &WireDaemonConfig) -> usize {
let mut touched = 0;
if let (Some(provider), Some(model)) = (patch.provider.as_deref(), patch.model.as_deref()) {
self.model_default = Some(ModelDefault {
provider: provider.to_string(),
model: model.to_string(),
});
touched += 1;
}
if !patch.builtin_skills.is_empty() {
self.builtin_skills = patch.builtin_skills.clone();
touched += 1;
}
if let Some(secs) = patch.trigger_poll_secs {
self.trigger_poll_secs = secs;
touched += 1;
}
if let Some(lines) = patch.tui_max_feed_lines {
self.tui_max_feed_lines = Some(lines);
touched += 1;
}
touched
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_match_the_pre_issue_73_builtin_defaults() {
let config = StartupConfig::default();
assert!(config.model_default.is_none());
assert!(config.builtin_skills.is_empty());
assert_eq!(
config.trigger_poll_secs,
DEFAULT_DYNAMIC_TRIGGER_POLL_INTERVAL_SECS
);
assert_eq!(config.trigger_poll_secs, 600);
assert!(config.tui_max_feed_lines.is_none());
assert!(config.thinking_summary.is_none());
assert!(config.load_local_sources, "local scans stay on by default");
assert!(config.storage_service_addr.is_none());
}
#[test]
fn empty_payload_leaves_defaults_untouched() {
let config = StartupConfig::from_wire(&WireDaemonConfig::default());
assert_eq!(config, StartupConfig::default());
}
#[test]
fn payload_replaces_defaults_field_by_field() {
let payload = WireDaemonConfig {
provider: Some("anthropic".into()),
model: Some("claude-x".into()),
builtin_skills: vec!["debugging".into()],
trigger_poll_secs: Some(30),
tui_max_feed_lines: Some(8000),
..Default::default()
};
let config = StartupConfig::from_wire(&payload);
assert_eq!(
config.model_default.as_ref().map(|d| d.provider.as_str()),
Some("anthropic")
);
assert_eq!(
config.model_default.as_ref().map(|d| d.model.as_str()),
Some("claude-x")
);
assert_eq!(config.builtin_skills, vec!["debugging".to_string()]);
assert_eq!(config.trigger_poll_secs, 30);
assert_eq!(config.tui_max_feed_lines, Some(8000));
}
#[test]
fn half_configured_model_pair_is_ignored() {
let payload = WireDaemonConfig {
provider: Some("anthropic".into()),
..Default::default()
};
let mut config = StartupConfig::default();
assert_eq!(config.apply_wire(&payload), 0);
assert!(config.model_default.is_none());
}
#[test]
fn apply_wire_counts_touched_areas_and_merges_incrementally() {
let mut config = StartupConfig::default();
let first = WireDaemonConfig {
trigger_poll_secs: Some(60),
..Default::default()
};
assert_eq!(config.apply_wire(&first), 1);
assert_eq!(config.trigger_poll_secs, 60);
let second = WireDaemonConfig {
provider: Some("openai".into()),
model: Some("gpt-x".into()),
trigger_poll_secs: Some(15),
..Default::default()
};
assert_eq!(config.apply_wire(&second), 2);
assert_eq!(config.trigger_poll_secs, 15);
assert_eq!(
config.model_default.as_ref().map(|d| d.model.as_str()),
Some("gpt-x")
);
}
}