Skip to main content

tuitbot_core/config/
defaults.rs

1//! Default values for all configuration sections.
2//!
3//! These defaults match the values specified in the CLI interface contract.
4//! Users only need to supply credentials and business profile.
5
6use super::{
7    AuthConfig, IntervalsConfig, LimitsConfig, McpPolicyConfig, ScoringConfig, StorageConfig,
8};
9
10impl Default for AuthConfig {
11    fn default() -> Self {
12        Self {
13            mode: "manual".to_string(),
14            callback_host: "127.0.0.1".to_string(),
15            callback_port: 8080,
16        }
17    }
18}
19
20impl Default for ScoringConfig {
21    fn default() -> Self {
22        Self {
23            threshold: 60,
24            keyword_relevance_max: 25.0,
25            follower_count_max: 15.0,
26            recency_max: 10.0,
27            engagement_rate_max: 15.0,
28            reply_count_max: 15.0,
29            content_type_max: 10.0,
30        }
31    }
32}
33
34impl Default for LimitsConfig {
35    fn default() -> Self {
36        Self {
37            max_replies_per_day: 5,
38            max_tweets_per_day: 6,
39            max_threads_per_week: 1,
40            min_action_delay_seconds: 45,
41            max_action_delay_seconds: 180,
42            max_replies_per_author_per_day: 1,
43            banned_phrases: vec![
44                "check out".to_string(),
45                "you should try".to_string(),
46                "I recommend".to_string(),
47                "link in bio".to_string(),
48            ],
49            product_mention_ratio: 0.2,
50        }
51    }
52}
53
54impl Default for IntervalsConfig {
55    fn default() -> Self {
56        Self {
57            mentions_check_seconds: 300,
58            discovery_search_seconds: 900,
59            content_post_window_seconds: 10800,
60            thread_interval_seconds: 604800,
61        }
62    }
63}
64
65impl Default for StorageConfig {
66    fn default() -> Self {
67        Self {
68            db_path: "~/.tuitbot/tuitbot.db".to_string(),
69            retention_days: 90,
70        }
71    }
72}
73
74impl Default for McpPolicyConfig {
75    fn default() -> Self {
76        Self {
77            enforce_for_mutations: true,
78            require_approval_for: vec![
79                "post_tweet".to_string(),
80                "reply_to_tweet".to_string(),
81                "follow_user".to_string(),
82                "like_tweet".to_string(),
83            ],
84            blocked_tools: Vec::new(),
85            dry_run_mutations: false,
86            max_mutations_per_hour: 20,
87            template: None,
88            rules: Vec::new(),
89            rate_limits: Vec::new(),
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn auth_config_defaults() {
100        let config = AuthConfig::default();
101        assert_eq!(config.mode, "manual");
102        assert_eq!(config.callback_host, "127.0.0.1");
103        assert_eq!(config.callback_port, 8080);
104    }
105
106    #[test]
107    fn scoring_config_defaults() {
108        let config = ScoringConfig::default();
109        assert_eq!(config.threshold, 60);
110        assert!((config.keyword_relevance_max - 25.0).abs() < f32::EPSILON);
111        assert!((config.follower_count_max - 15.0).abs() < f32::EPSILON);
112        assert!((config.recency_max - 10.0).abs() < f32::EPSILON);
113        assert!((config.engagement_rate_max - 15.0).abs() < f32::EPSILON);
114        assert!((config.reply_count_max - 15.0).abs() < f32::EPSILON);
115        assert!((config.content_type_max - 10.0).abs() < f32::EPSILON);
116    }
117
118    #[test]
119    fn scoring_config_weights_sum_to_90() {
120        let config = ScoringConfig::default();
121        let sum = config.keyword_relevance_max
122            + config.follower_count_max
123            + config.recency_max
124            + config.engagement_rate_max
125            + config.reply_count_max
126            + config.content_type_max;
127        assert!((sum - 90.0).abs() < f32::EPSILON);
128    }
129
130    #[test]
131    fn limits_config_defaults() {
132        let config = LimitsConfig::default();
133        assert_eq!(config.max_replies_per_day, 5);
134        assert_eq!(config.max_tweets_per_day, 6);
135        assert_eq!(config.max_threads_per_week, 1);
136        assert_eq!(config.min_action_delay_seconds, 45);
137        assert_eq!(config.max_action_delay_seconds, 180);
138        assert_eq!(config.max_replies_per_author_per_day, 1);
139        assert_eq!(config.banned_phrases.len(), 4);
140        assert!(config.banned_phrases.contains(&"check out".to_string()));
141        assert!(config
142            .banned_phrases
143            .contains(&"you should try".to_string()));
144        assert!(config.banned_phrases.contains(&"I recommend".to_string()));
145        assert!(config.banned_phrases.contains(&"link in bio".to_string()));
146        assert!((config.product_mention_ratio - 0.2).abs() < f32::EPSILON);
147    }
148
149    #[test]
150    fn limits_config_min_delay_less_than_max() {
151        let config = LimitsConfig::default();
152        assert!(config.min_action_delay_seconds < config.max_action_delay_seconds);
153    }
154
155    #[test]
156    fn intervals_config_defaults() {
157        let config = IntervalsConfig::default();
158        assert_eq!(config.mentions_check_seconds, 300);
159        assert_eq!(config.discovery_search_seconds, 900);
160        assert_eq!(config.content_post_window_seconds, 10800);
161        assert_eq!(config.thread_interval_seconds, 604800);
162    }
163
164    #[test]
165    fn intervals_config_reasonable_ranges() {
166        let config = IntervalsConfig::default();
167        // Mentions check: 5 minutes
168        assert_eq!(config.mentions_check_seconds, 5 * 60);
169        // Discovery search: 15 minutes
170        assert_eq!(config.discovery_search_seconds, 15 * 60);
171        // Content post window: 3 hours
172        assert_eq!(config.content_post_window_seconds, 3 * 60 * 60);
173        // Thread interval: 7 days
174        assert_eq!(config.thread_interval_seconds, 7 * 24 * 60 * 60);
175    }
176
177    #[test]
178    fn storage_config_defaults() {
179        let config = StorageConfig::default();
180        assert_eq!(config.db_path, "~/.tuitbot/tuitbot.db");
181        assert_eq!(config.retention_days, 90);
182    }
183
184    #[test]
185    fn mcp_policy_config_defaults() {
186        let config = McpPolicyConfig::default();
187        assert!(config.enforce_for_mutations);
188        assert!(!config.dry_run_mutations);
189        assert_eq!(config.max_mutations_per_hour, 20);
190        assert_eq!(config.require_approval_for.len(), 4);
191        assert!(config
192            .require_approval_for
193            .contains(&"post_tweet".to_string()));
194        assert!(config
195            .require_approval_for
196            .contains(&"reply_to_tweet".to_string()));
197        assert!(config
198            .require_approval_for
199            .contains(&"follow_user".to_string()));
200        assert!(config
201            .require_approval_for
202            .contains(&"like_tweet".to_string()));
203        assert!(config.blocked_tools.is_empty());
204        assert!(config.template.is_none());
205        assert!(config.rules.is_empty());
206        assert!(config.rate_limits.is_empty());
207    }
208}