| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- //! In-memory registries for Team and Cron lifecycle management.
- //!
- //! Provides TeamCreate/Delete and CronCreate/Delete/List runtime backing
- //! to replace the stub implementations in the tools crate.
- use std::collections::HashMap;
- use std::sync::{Arc, Mutex};
- use std::time::{SystemTime, UNIX_EPOCH};
- use serde::{Deserialize, Serialize};
- fn now_secs() -> u64 {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap_or_default()
- .as_secs()
- }
- // ─────────────────────────────────────────────
- // Team registry
- // ─────────────────────────────────────────────
- /// A team groups multiple tasks for parallel execution.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct Team {
- pub team_id: String,
- pub name: String,
- pub task_ids: Vec<String>,
- pub status: TeamStatus,
- pub created_at: u64,
- pub updated_at: u64,
- }
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
- #[serde(rename_all = "snake_case")]
- pub enum TeamStatus {
- Created,
- Running,
- Completed,
- Deleted,
- }
- impl std::fmt::Display for TeamStatus {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Created => write!(f, "created"),
- Self::Running => write!(f, "running"),
- Self::Completed => write!(f, "completed"),
- Self::Deleted => write!(f, "deleted"),
- }
- }
- }
- /// Thread-safe team registry.
- #[derive(Debug, Clone, Default)]
- pub struct TeamRegistry {
- inner: Arc<Mutex<TeamInner>>,
- }
- #[derive(Debug, Default)]
- struct TeamInner {
- teams: HashMap<String, Team>,
- counter: u64,
- }
- impl TeamRegistry {
- #[must_use]
- pub fn new() -> Self {
- Self::default()
- }
- /// Create a new team with the given name and task IDs.
- pub fn create(&self, name: &str, task_ids: Vec<String>) -> Team {
- let mut inner = self.inner.lock().expect("team registry lock poisoned");
- inner.counter += 1;
- let ts = now_secs();
- let team_id = format!("team_{:08x}_{}", ts, inner.counter);
- let team = Team {
- team_id: team_id.clone(),
- name: name.to_owned(),
- task_ids,
- status: TeamStatus::Created,
- created_at: ts,
- updated_at: ts,
- };
- inner.teams.insert(team_id, team.clone());
- team
- }
- /// Get a team by ID.
- pub fn get(&self, team_id: &str) -> Option<Team> {
- let inner = self.inner.lock().expect("team registry lock poisoned");
- inner.teams.get(team_id).cloned()
- }
- /// List all teams.
- pub fn list(&self) -> Vec<Team> {
- let inner = self.inner.lock().expect("team registry lock poisoned");
- inner.teams.values().cloned().collect()
- }
- /// Delete a team.
- pub fn delete(&self, team_id: &str) -> Result<Team, String> {
- let mut inner = self.inner.lock().expect("team registry lock poisoned");
- let team = inner
- .teams
- .get_mut(team_id)
- .ok_or_else(|| format!("team not found: {team_id}"))?;
- team.status = TeamStatus::Deleted;
- team.updated_at = now_secs();
- Ok(team.clone())
- }
- /// Remove a team entirely from the registry.
- pub fn remove(&self, team_id: &str) -> Option<Team> {
- let mut inner = self.inner.lock().expect("team registry lock poisoned");
- inner.teams.remove(team_id)
- }
- #[must_use]
- pub fn len(&self) -> usize {
- let inner = self.inner.lock().expect("team registry lock poisoned");
- inner.teams.len()
- }
- #[must_use]
- pub fn is_empty(&self) -> bool {
- self.len() == 0
- }
- }
- // ─────────────────────────────────────────────
- // Cron registry
- // ─────────────────────────────────────────────
- /// A cron entry schedules a prompt to run on a recurring schedule.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct CronEntry {
- pub cron_id: String,
- pub schedule: String,
- pub prompt: String,
- pub description: Option<String>,
- pub enabled: bool,
- pub created_at: u64,
- pub updated_at: u64,
- pub last_run_at: Option<u64>,
- pub run_count: u64,
- }
- /// Thread-safe cron registry.
- #[derive(Debug, Clone, Default)]
- pub struct CronRegistry {
- inner: Arc<Mutex<CronInner>>,
- }
- #[derive(Debug, Default)]
- struct CronInner {
- entries: HashMap<String, CronEntry>,
- counter: u64,
- }
- impl CronRegistry {
- #[must_use]
- pub fn new() -> Self {
- Self::default()
- }
- /// Create a new cron entry.
- pub fn create(&self, schedule: &str, prompt: &str, description: Option<&str>) -> CronEntry {
- let mut inner = self.inner.lock().expect("cron registry lock poisoned");
- inner.counter += 1;
- let ts = now_secs();
- let cron_id = format!("cron_{:08x}_{}", ts, inner.counter);
- let entry = CronEntry {
- cron_id: cron_id.clone(),
- schedule: schedule.to_owned(),
- prompt: prompt.to_owned(),
- description: description.map(str::to_owned),
- enabled: true,
- created_at: ts,
- updated_at: ts,
- last_run_at: None,
- run_count: 0,
- };
- inner.entries.insert(cron_id, entry.clone());
- entry
- }
- /// Get a cron entry by ID.
- pub fn get(&self, cron_id: &str) -> Option<CronEntry> {
- let inner = self.inner.lock().expect("cron registry lock poisoned");
- inner.entries.get(cron_id).cloned()
- }
- /// List all cron entries, optionally filtered to enabled only.
- pub fn list(&self, enabled_only: bool) -> Vec<CronEntry> {
- let inner = self.inner.lock().expect("cron registry lock poisoned");
- inner
- .entries
- .values()
- .filter(|e| !enabled_only || e.enabled)
- .cloned()
- .collect()
- }
- /// Delete (remove) a cron entry.
- pub fn delete(&self, cron_id: &str) -> Result<CronEntry, String> {
- let mut inner = self.inner.lock().expect("cron registry lock poisoned");
- inner
- .entries
- .remove(cron_id)
- .ok_or_else(|| format!("cron not found: {cron_id}"))
- }
- /// Disable a cron entry without removing it.
- pub fn disable(&self, cron_id: &str) -> Result<(), String> {
- let mut inner = self.inner.lock().expect("cron registry lock poisoned");
- let entry = inner
- .entries
- .get_mut(cron_id)
- .ok_or_else(|| format!("cron not found: {cron_id}"))?;
- entry.enabled = false;
- entry.updated_at = now_secs();
- Ok(())
- }
- /// Record a cron run.
- pub fn record_run(&self, cron_id: &str) -> Result<(), String> {
- let mut inner = self.inner.lock().expect("cron registry lock poisoned");
- let entry = inner
- .entries
- .get_mut(cron_id)
- .ok_or_else(|| format!("cron not found: {cron_id}"))?;
- entry.last_run_at = Some(now_secs());
- entry.run_count += 1;
- entry.updated_at = now_secs();
- Ok(())
- }
- #[must_use]
- pub fn len(&self) -> usize {
- let inner = self.inner.lock().expect("cron registry lock poisoned");
- inner.entries.len()
- }
- #[must_use]
- pub fn is_empty(&self) -> bool {
- self.len() == 0
- }
- }
- #[cfg(test)]
- mod tests {
- use super::*;
- // ── Team tests ──────────────────────────────────────
- #[test]
- fn creates_and_retrieves_team() {
- let registry = TeamRegistry::new();
- let team = registry.create("Alpha Squad", vec!["task_001".into(), "task_002".into()]);
- assert_eq!(team.name, "Alpha Squad");
- assert_eq!(team.task_ids.len(), 2);
- assert_eq!(team.status, TeamStatus::Created);
- let fetched = registry.get(&team.team_id).expect("team should exist");
- assert_eq!(fetched.team_id, team.team_id);
- }
- #[test]
- fn lists_and_deletes_teams() {
- let registry = TeamRegistry::new();
- let t1 = registry.create("Team A", vec![]);
- let t2 = registry.create("Team B", vec![]);
- let all = registry.list();
- assert_eq!(all.len(), 2);
- let deleted = registry.delete(&t1.team_id).expect("delete should succeed");
- assert_eq!(deleted.status, TeamStatus::Deleted);
- // Team is still listable (soft delete)
- let still_there = registry.get(&t1.team_id).unwrap();
- assert_eq!(still_there.status, TeamStatus::Deleted);
- // Hard remove
- registry.remove(&t2.team_id);
- assert_eq!(registry.len(), 1);
- }
- #[test]
- fn rejects_missing_team_operations() {
- let registry = TeamRegistry::new();
- assert!(registry.delete("nonexistent").is_err());
- assert!(registry.get("nonexistent").is_none());
- }
- // ── Cron tests ──────────────────────────────────────
- #[test]
- fn creates_and_retrieves_cron() {
- let registry = CronRegistry::new();
- let entry = registry.create("0 * * * *", "Check status", Some("hourly check"));
- assert_eq!(entry.schedule, "0 * * * *");
- assert_eq!(entry.prompt, "Check status");
- assert!(entry.enabled);
- assert_eq!(entry.run_count, 0);
- assert!(entry.last_run_at.is_none());
- let fetched = registry.get(&entry.cron_id).expect("cron should exist");
- assert_eq!(fetched.cron_id, entry.cron_id);
- }
- #[test]
- fn lists_with_enabled_filter() {
- let registry = CronRegistry::new();
- let c1 = registry.create("* * * * *", "Task 1", None);
- let c2 = registry.create("0 * * * *", "Task 2", None);
- registry
- .disable(&c1.cron_id)
- .expect("disable should succeed");
- let all = registry.list(false);
- assert_eq!(all.len(), 2);
- let enabled_only = registry.list(true);
- assert_eq!(enabled_only.len(), 1);
- assert_eq!(enabled_only[0].cron_id, c2.cron_id);
- }
- #[test]
- fn deletes_cron_entry() {
- let registry = CronRegistry::new();
- let entry = registry.create("* * * * *", "To delete", None);
- let deleted = registry
- .delete(&entry.cron_id)
- .expect("delete should succeed");
- assert_eq!(deleted.cron_id, entry.cron_id);
- assert!(registry.get(&entry.cron_id).is_none());
- assert!(registry.is_empty());
- }
- #[test]
- fn records_cron_runs() {
- let registry = CronRegistry::new();
- let entry = registry.create("*/5 * * * *", "Recurring", None);
- registry.record_run(&entry.cron_id).unwrap();
- registry.record_run(&entry.cron_id).unwrap();
- let fetched = registry.get(&entry.cron_id).unwrap();
- assert_eq!(fetched.run_count, 2);
- assert!(fetched.last_run_at.is_some());
- }
- #[test]
- fn rejects_missing_cron_operations() {
- let registry = CronRegistry::new();
- assert!(registry.delete("nonexistent").is_err());
- assert!(registry.disable("nonexistent").is_err());
- assert!(registry.record_run("nonexistent").is_err());
- assert!(registry.get("nonexistent").is_none());
- }
- }
|