1use serde::{Deserialize, Serialize};
9
10fn default_true() -> bool {
11 true
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct TitleConfig {
25 #[serde(default = "default_true")]
27 pub enabled: bool,
28
29 pub provider: Option<String>,
32
33 pub model: Option<String>,
38}
39
40impl Default for TitleConfig {
41 fn default() -> Self {
42 Self {
43 enabled: true,
44 provider: None,
45 model: None,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
65pub struct ObservabilityConfig {
66 #[serde(default)]
68 pub enabled: bool,
69
70 #[serde(default)]
72 pub exporter: TelemetryExporterKind,
73
74 pub endpoint: Option<String>,
79
80 pub service_name: Option<String>,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum TelemetryExporterKind {
89 #[default]
91 Otlp,
92 Stdout,
94 None,
96}
97
98impl TelemetryExporterKind {
99 pub fn is_none(self) -> bool {
101 matches!(self, Self::None)
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct PromptHints {
116 pub batch_tool: bool,
118 pub shell: bool,
120}
121
122impl Default for PromptHints {
123 fn default() -> Self {
125 Self {
126 batch_tool: true,
127 shell: true,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
135pub struct PromptHintOverrides {
136 pub batch_tool: Option<bool>,
138 pub shell: Option<bool>,
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn prompt_hints_default_on_and_overrides_default_inherit() {
148 let hints = PromptHints::default();
152 assert!(hints.batch_tool);
153 assert!(hints.shell);
154 assert_eq!(
155 hints,
156 PromptHints {
157 batch_tool: true,
158 shell: true
159 }
160 );
161 assert_ne!(
162 hints,
163 PromptHints {
164 batch_tool: true,
165 shell: false
166 }
167 );
168
169 let overrides = PromptHintOverrides::default();
171 assert_eq!(overrides.batch_tool, None);
172 assert_eq!(overrides.shell, None);
173 assert_eq!(
174 overrides,
175 PromptHintOverrides {
176 batch_tool: None,
177 shell: None
178 }
179 );
180 assert!(format!("{hints:?} {overrides:?}").contains("batch_tool"));
181 }
182
183 #[test]
184 fn test_title_config_default() {
185 let cfg = TitleConfig::default();
186 assert!(cfg.enabled);
187 assert!(cfg.provider.is_none());
188 assert!(cfg.model.is_none());
189 }
190
191 #[test]
192 fn test_title_config_fields() {
193 let cfg = TitleConfig {
194 enabled: false,
195 provider: Some("anthropic".to_string()),
196 model: Some("claude-haiku-4-5-20251001".to_string()),
197 };
198 assert!(!cfg.enabled);
199 assert_eq!(cfg.provider.as_deref(), Some("anthropic"));
200 assert_eq!(cfg.model.as_deref(), Some("claude-haiku-4-5-20251001"));
201 }
202
203 #[test]
204 fn test_title_config_clone_and_debug() {
205 let cfg = TitleConfig::default();
206 let cloned = cfg.clone();
207 assert_eq!(cloned.enabled, cfg.enabled);
208 assert!(format!("{:?}", cfg).contains("TitleConfig"));
210 }
211
212 #[test]
213 fn test_title_config_serde_roundtrip() {
214 let cfg = TitleConfig {
215 enabled: true,
216 provider: Some("openai".to_string()),
217 model: Some("gpt-4o-mini".to_string()),
218 };
219 let json = serde_json::to_string(&cfg).unwrap();
220 let back: TitleConfig = serde_json::from_str(&json).unwrap();
221 assert_eq!(back.enabled, cfg.enabled);
222 assert_eq!(back.provider, cfg.provider);
223 assert_eq!(back.model, cfg.model);
224 }
225
226 #[test]
227 fn test_title_config_deserialize_defaults_enabled_true() {
228 let toml_str = r#"
230provider = "anthropic"
231model = "claude-haiku-4-5-20251001"
232"#;
233 let cfg: TitleConfig = toml::from_str(toml_str).unwrap();
234 assert!(cfg.enabled);
235 assert_eq!(cfg.provider.as_deref(), Some("anthropic"));
236 }
237
238 #[test]
239 fn test_title_config_deserialize_explicit_disabled() {
240 let toml_str = r#"
241enabled = false
242"#;
243 let cfg: TitleConfig = toml::from_str(toml_str).unwrap();
244 assert!(!cfg.enabled);
245 assert!(cfg.provider.is_none());
246 assert!(cfg.model.is_none());
247 }
248
249 #[test]
250 fn observability_config_defaults_to_disabled_otlp() {
251 let cfg = ObservabilityConfig::default();
252 assert!(!cfg.enabled);
253 assert_eq!(cfg.exporter, TelemetryExporterKind::Otlp);
254 assert!(cfg.endpoint.is_none());
255 assert!(cfg.service_name.is_none());
256 let parsed: ObservabilityConfig = toml::from_str("").unwrap();
258 assert_eq!(parsed, cfg);
259 }
260
261 #[test]
262 fn observability_config_full_roundtrip() {
263 let toml_str = r#"
264enabled = true
265exporter = "stdout"
266endpoint = "http://collector:4318"
267service_name = "leviath-prod"
268"#;
269 let cfg: ObservabilityConfig = toml::from_str(toml_str).unwrap();
270 assert!(cfg.enabled);
271 assert_eq!(cfg.exporter, TelemetryExporterKind::Stdout);
272 assert_eq!(cfg.endpoint.as_deref(), Some("http://collector:4318"));
273 assert_eq!(cfg.service_name.as_deref(), Some("leviath-prod"));
274 let serialized = toml::to_string(&cfg).unwrap();
275 let back: ObservabilityConfig = toml::from_str(&serialized).unwrap();
276 assert_eq!(back, cfg);
277 assert!(format!("{cfg:?}").contains("ObservabilityConfig"));
278 }
279
280 #[test]
281 fn telemetry_exporter_kind_round_trips_through_config_syntax() {
282 for (kind, text) in [
283 (TelemetryExporterKind::Otlp, "otlp"),
284 (TelemetryExporterKind::Stdout, "stdout"),
285 (TelemetryExporterKind::None, "none"),
286 ] {
287 let toml_str = format!("exporter = \"{text}\"");
288 let cfg: ObservabilityConfig = toml::from_str(&toml_str).unwrap();
289 assert_eq!(cfg.exporter, kind);
290 let serialized = toml::to_string(&cfg).unwrap();
291 assert!(serialized.contains(text), "{serialized} missing {text}");
292 }
293 }
294
295 #[test]
296 fn telemetry_exporter_kind_is_none_helper() {
297 assert!(TelemetryExporterKind::None.is_none());
298 assert!(!TelemetryExporterKind::Otlp.is_none());
299 assert!(!TelemetryExporterKind::Stdout.is_none());
300 }
301
302 #[test]
303 fn telemetry_exporter_kind_rejects_unknown_value() {
304 let err = toml::from_str::<ObservabilityConfig>("exporter = \"grpc\"");
305 assert!(err.is_err());
306 }
307}