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)]
107#[serde(rename_all = "lowercase")]
108pub enum TaskCreator {
109 #[default]
111 System,
112 #[serde(alias = "entity")]
114 Pulse,
115 User,
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn health_status_display() {
125 assert_eq!(HealthStatus::Healthy.to_string(), "healthy");
126 assert_eq!(
127 HealthStatus::Degraded("high latency".into()).to_string(),
128 "degraded: high latency"
129 );
130 assert_eq!(
131 HealthStatus::Down("connection refused".into()).to_string(),
132 "down: connection refused"
133 );
134 }
135
136 #[test]
137 fn health_status_serializes() {
138 let json = serde_json::to_string(&HealthStatus::Healthy).unwrap();
139 assert!(json.contains("Healthy"));
140
141 let json = serde_json::to_string(&HealthStatus::Degraded("slow".into())).unwrap();
142 assert!(json.contains("Degraded"));
143 assert!(json.contains("slow"));
144 }
145
146 #[test]
147 fn plugin_meta_roundtrip() {
148 let meta = PluginMeta {
149 name: "voice-echo".into(),
150 version: "0.7.0".into(),
151 description: "Phone calls via Twilio".into(),
152 };
153 let json = serde_json::to_string(&meta).unwrap();
154 let back: PluginMeta = serde_json::from_str(&json).unwrap();
155 assert_eq!(back.name, "voice-echo");
156 assert_eq!(back.version, "0.7.0");
157 }
158
159 #[test]
160 fn setup_prompt_with_defaults() {
161 let prompt = SetupPrompt {
162 key: "host".into(),
163 question: "Bind address:".into(),
164 default: Some("0.0.0.0".into()),
165 required: false,
166 secret: false,
167 };
168 assert_eq!(prompt.default.as_deref(), Some("0.0.0.0"));
169 assert!(!prompt.secret);
170 }
171
172 #[test]
173 fn setup_prompt_secret() {
174 let prompt = SetupPrompt {
175 key: "api_key".into(),
176 question: "API Key:".into(),
177 default: None,
178 required: true,
179 secret: true,
180 };
181 assert!(prompt.secret);
182 assert!(prompt.required);
183 assert!(prompt.default.is_none());
184 }
185
186 #[test]
187 fn scheduled_task_defaults() {
188 let json = r#"{
189 "id": "morning",
190 "name": "Morning check",
191 "cron": "0 0 8 * * *",
192 "channel": "reflection",
193 "prompt": "Good morning."
194 }"#;
195 let task: ScheduledTask = serde_json::from_str(json).unwrap();
196 assert_eq!(task.output_routing, OutputRouting::Silent);
197 assert!(task.enabled);
198 assert_eq!(task.created_by, TaskCreator::System);
199 assert!(task.evaluator.is_none());
200 }
201
202 #[test]
203 fn scheduled_task_with_evaluator() {
204 let json = r#"{
205 "id": "reflection",
206 "name": "Reflection Window",
207 "cron": "0 30 4 * * *",
208 "channel": "system",
209 "prompt": "Reflect.",
210 "evaluator": "pipeline"
211 }"#;
212 let task: ScheduledTask = serde_json::from_str(json).unwrap();
213 assert_eq!(task.evaluator.as_deref(), Some("pipeline"));
214 }
215
216 #[test]
217 fn task_creator_pulse_serializes_as_pulse() {
218 let json = serde_json::to_string(&TaskCreator::Pulse).unwrap();
219 assert_eq!(json, r#""pulse""#);
220 let back: TaskCreator = serde_json::from_str(&json).unwrap();
221 assert_eq!(back, TaskCreator::Pulse);
222 }
223
224 #[test]
225 fn task_creator_legacy_entity_deserializes_as_pulse() {
226 let creator: TaskCreator = serde_json::from_str(r#""entity""#).unwrap();
227 assert_eq!(creator, TaskCreator::Pulse);
228 }
229
230 #[test]
231 fn task_creator_rejects_unknown_value() {
232 assert!(serde_json::from_str::<TaskCreator>(r#""robot""#).is_err());
233 }
234
235 #[test]
236 fn scheduled_task_legacy_entity_creator_loads_and_rewrites_as_pulse() {
237 let legacy = r#"{
238 "id": "self-made",
239 "name": "Self-made task",
240 "cron": "0 0 9 * * *",
241 "channel": "reflection",
242 "prompt": "Check in.",
243 "created_by": "entity"
244 }"#;
245 let task: ScheduledTask = serde_json::from_str(legacy).unwrap();
246 assert_eq!(task.created_by, TaskCreator::Pulse);
247
248 let rewritten: serde_json::Value = serde_json::to_value(&task).unwrap();
249 assert_eq!(rewritten["created_by"], "pulse");
250
251 let back: ScheduledTask = serde_json::from_value(rewritten).unwrap();
252 assert_eq!(back.created_by, TaskCreator::Pulse);
253 }
254
255 #[test]
256 fn output_routing_variants() {
257 let json = r#""share""#;
258 let routing: OutputRouting = serde_json::from_str(json).unwrap();
259 assert_eq!(routing, OutputRouting::Share);
260 }
261}