pub mod llm;
pub mod monitoring;
pub mod plugin;
pub mod tool;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", content = "message")]
pub enum HealthStatus {
Healthy,
Degraded(String),
Down(String),
}
impl fmt::Display for HealthStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Healthy => write!(f, "healthy"),
Self::Degraded(msg) => write!(f, "degraded: {msg}"),
Self::Down(msg) => write!(f, "down: {msg}"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginMeta {
pub name: String,
pub version: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetupPrompt {
pub key: String,
pub question: String,
pub default: Option<String>,
pub required: bool,
pub secret: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScheduledTask {
pub id: String,
pub name: String,
pub cron: String,
pub channel: String,
pub prompt: String,
#[serde(default)]
pub output_routing: OutputRouting,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub created_by: TaskCreator,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evaluator: Option<String>,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum OutputRouting {
#[default]
Silent,
Share,
Call,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum TaskCreator {
#[default]
System,
#[serde(alias = "entity")]
Pulse,
User,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn health_status_display() {
assert_eq!(HealthStatus::Healthy.to_string(), "healthy");
assert_eq!(
HealthStatus::Degraded("high latency".into()).to_string(),
"degraded: high latency"
);
assert_eq!(
HealthStatus::Down("connection refused".into()).to_string(),
"down: connection refused"
);
}
#[test]
fn health_status_serializes() {
let json = serde_json::to_string(&HealthStatus::Healthy).unwrap();
assert!(json.contains("Healthy"));
let json = serde_json::to_string(&HealthStatus::Degraded("slow".into())).unwrap();
assert!(json.contains("Degraded"));
assert!(json.contains("slow"));
}
#[test]
fn plugin_meta_roundtrip() {
let meta = PluginMeta {
name: "voice-echo".into(),
version: "0.7.0".into(),
description: "Phone calls via Twilio".into(),
};
let json = serde_json::to_string(&meta).unwrap();
let back: PluginMeta = serde_json::from_str(&json).unwrap();
assert_eq!(back.name, "voice-echo");
assert_eq!(back.version, "0.7.0");
}
#[test]
fn setup_prompt_with_defaults() {
let prompt = SetupPrompt {
key: "host".into(),
question: "Bind address:".into(),
default: Some("0.0.0.0".into()),
required: false,
secret: false,
};
assert_eq!(prompt.default.as_deref(), Some("0.0.0.0"));
assert!(!prompt.secret);
}
#[test]
fn setup_prompt_secret() {
let prompt = SetupPrompt {
key: "api_key".into(),
question: "API Key:".into(),
default: None,
required: true,
secret: true,
};
assert!(prompt.secret);
assert!(prompt.required);
assert!(prompt.default.is_none());
}
#[test]
fn scheduled_task_defaults() {
let json = r#"{
"id": "morning",
"name": "Morning check",
"cron": "0 0 8 * * *",
"channel": "reflection",
"prompt": "Good morning."
}"#;
let task: ScheduledTask = serde_json::from_str(json).unwrap();
assert_eq!(task.output_routing, OutputRouting::Silent);
assert!(task.enabled);
assert_eq!(task.created_by, TaskCreator::System);
assert!(task.evaluator.is_none());
}
#[test]
fn scheduled_task_with_evaluator() {
let json = r#"{
"id": "reflection",
"name": "Reflection Window",
"cron": "0 30 4 * * *",
"channel": "system",
"prompt": "Reflect.",
"evaluator": "pipeline"
}"#;
let task: ScheduledTask = serde_json::from_str(json).unwrap();
assert_eq!(task.evaluator.as_deref(), Some("pipeline"));
}
#[test]
fn task_creator_pulse_serializes_as_pulse() {
let json = serde_json::to_string(&TaskCreator::Pulse).unwrap();
assert_eq!(json, r#""pulse""#);
let back: TaskCreator = serde_json::from_str(&json).unwrap();
assert_eq!(back, TaskCreator::Pulse);
}
#[test]
fn task_creator_legacy_entity_deserializes_as_pulse() {
let creator: TaskCreator = serde_json::from_str(r#""entity""#).unwrap();
assert_eq!(creator, TaskCreator::Pulse);
}
#[test]
fn task_creator_rejects_unknown_value() {
assert!(serde_json::from_str::<TaskCreator>(r#""robot""#).is_err());
}
#[test]
fn scheduled_task_legacy_entity_creator_loads_and_rewrites_as_pulse() {
let legacy = r#"{
"id": "self-made",
"name": "Self-made task",
"cron": "0 0 9 * * *",
"channel": "reflection",
"prompt": "Check in.",
"created_by": "entity"
}"#;
let task: ScheduledTask = serde_json::from_str(legacy).unwrap();
assert_eq!(task.created_by, TaskCreator::Pulse);
let rewritten: serde_json::Value = serde_json::to_value(&task).unwrap();
assert_eq!(rewritten["created_by"], "pulse");
let back: ScheduledTask = serde_json::from_value(rewritten).unwrap();
assert_eq!(back.created_by, TaskCreator::Pulse);
}
#[test]
fn output_routing_variants() {
let json = r#""share""#;
let routing: OutputRouting = serde_json::from_str(json).unwrap();
assert_eq!(routing, OutputRouting::Share);
}
}