1pub mod llm;
8pub mod monitoring;
9pub mod plugin;
10pub mod tool;
11
12use serde::{Deserialize, Serialize};
13use std::fmt;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(tag = "status", content = "message")]
18pub enum HealthStatus {
19 Healthy,
20 Degraded(String),
21 Down(String),
22}
23
24impl fmt::Display for HealthStatus {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 Self::Healthy => write!(f, "healthy"),
28 Self::Degraded(msg) => write!(f, "degraded: {msg}"),
29 Self::Down(msg) => write!(f, "down: {msg}"),
30 }
31 }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct PluginMeta {
37 pub name: String,
38 pub version: String,
39 pub description: String,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct SetupPrompt {
48 pub key: String,
50 pub question: String,
52 pub default: Option<String>,
54 pub required: bool,
56 pub secret: bool,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ScheduledTask {
63 pub id: String,
64 pub name: String,
65 pub cron: String,
67 pub channel: String,
69 pub prompt: String,
71 #[serde(default)]
73 pub output_routing: OutputRouting,
74 #[serde(default = "default_true")]
76 pub enabled: bool,
77 #[serde(default)]
79 pub created_by: TaskCreator,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub evaluator: Option<String>,
85}
86
87fn default_true() -> bool {
88 true
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
93#[serde(rename_all = "lowercase")]
94pub enum OutputRouting {
95 #[default]
96 Silent,
97 Share,
98 Call,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
103#[serde(rename_all = "lowercase")]
104pub enum TaskCreator {
105 #[default]
106 System,
107 Entity,
108 User,
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn health_status_display() {
117 assert_eq!(HealthStatus::Healthy.to_string(), "healthy");
118 assert_eq!(
119 HealthStatus::Degraded("high latency".into()).to_string(),
120 "degraded: high latency"
121 );
122 assert_eq!(
123 HealthStatus::Down("connection refused".into()).to_string(),
124 "down: connection refused"
125 );
126 }
127
128 #[test]
129 fn health_status_serializes() {
130 let json = serde_json::to_string(&HealthStatus::Healthy).unwrap();
131 assert!(json.contains("Healthy"));
132
133 let json = serde_json::to_string(&HealthStatus::Degraded("slow".into())).unwrap();
134 assert!(json.contains("Degraded"));
135 assert!(json.contains("slow"));
136 }
137
138 #[test]
139 fn plugin_meta_roundtrip() {
140 let meta = PluginMeta {
141 name: "voice-echo".into(),
142 version: "0.7.0".into(),
143 description: "Phone calls via Twilio".into(),
144 };
145 let json = serde_json::to_string(&meta).unwrap();
146 let back: PluginMeta = serde_json::from_str(&json).unwrap();
147 assert_eq!(back.name, "voice-echo");
148 assert_eq!(back.version, "0.7.0");
149 }
150
151 #[test]
152 fn setup_prompt_with_defaults() {
153 let prompt = SetupPrompt {
154 key: "host".into(),
155 question: "Bind address:".into(),
156 default: Some("0.0.0.0".into()),
157 required: false,
158 secret: false,
159 };
160 assert_eq!(prompt.default.as_deref(), Some("0.0.0.0"));
161 assert!(!prompt.secret);
162 }
163
164 #[test]
165 fn setup_prompt_secret() {
166 let prompt = SetupPrompt {
167 key: "api_key".into(),
168 question: "API Key:".into(),
169 default: None,
170 required: true,
171 secret: true,
172 };
173 assert!(prompt.secret);
174 assert!(prompt.required);
175 assert!(prompt.default.is_none());
176 }
177
178 #[test]
179 fn scheduled_task_defaults() {
180 let json = r#"{
181 "id": "morning",
182 "name": "Morning check",
183 "cron": "0 0 8 * * *",
184 "channel": "reflection",
185 "prompt": "Good morning."
186 }"#;
187 let task: ScheduledTask = serde_json::from_str(json).unwrap();
188 assert_eq!(task.output_routing, OutputRouting::Silent);
189 assert!(task.enabled);
190 assert_eq!(task.created_by, TaskCreator::System);
191 assert!(task.evaluator.is_none());
192 }
193
194 #[test]
195 fn scheduled_task_with_evaluator() {
196 let json = r#"{
197 "id": "reflection",
198 "name": "Reflection Window",
199 "cron": "0 30 4 * * *",
200 "channel": "system",
201 "prompt": "Reflect.",
202 "evaluator": "pipeline"
203 }"#;
204 let task: ScheduledTask = serde_json::from_str(json).unwrap();
205 assert_eq!(task.evaluator.as_deref(), Some("pipeline"));
206 }
207
208 #[test]
209 fn output_routing_variants() {
210 let json = r#""share""#;
211 let routing: OutputRouting = serde_json::from_str(json).unwrap();
212 assert_eq!(routing, OutputRouting::Share);
213 }
214}