Skip to main content

luft_core/scheduler/
registry.rs

1//! Backend registry (§2.2): backend id → implementation. Registered before a
2//! run, read-only during it.
3
4use super::error::SchedulerError;
5use crate::contract::backend::AgentBackend;
6use std::collections::HashMap;
7use std::sync::Arc;
8
9#[derive(Clone, Default)]
10pub struct BackendRegistry {
11    backends: HashMap<&'static str, Arc<dyn AgentBackend>>,
12}
13
14impl std::fmt::Debug for BackendRegistry {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        f.debug_struct("BackendRegistry")
17            .field("backend_ids", &self.backends.keys().collect::<Vec<_>>())
18            .finish()
19    }
20}
21
22impl BackendRegistry {
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    pub fn register(&mut self, backend: Arc<dyn AgentBackend>) {
28        let id = backend.id();
29        tracing::debug!(id, "registering backend");
30        self.backends.insert(id, backend);
31    }
32
33    /// Builder-style registration.
34    pub fn with(mut self, backend: Arc<dyn AgentBackend>) -> Self {
35        self.register(backend);
36        self
37    }
38
39    pub fn get(&self, id: &str) -> Result<Arc<dyn AgentBackend>, SchedulerError> {
40        self.backends.get(id).cloned().ok_or_else(|| {
41            tracing::error!(id, "backend not found in registry");
42            SchedulerError::UnknownBackend(id.to_owned())
43        })
44    }
45
46    /// First registered backend (v0.1 single-backend default routing).
47    pub fn default_backend(&self) -> Result<Arc<dyn AgentBackend>, SchedulerError> {
48        self.backends.values().next().cloned().ok_or_else(|| {
49            tracing::error!("no backend registered");
50            SchedulerError::NoBackendRegistered
51        })
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::contract::*;
59    use async_trait::async_trait;
60
61    /// Minimal stub backend for registry tests.
62    struct TestBackend {
63        id: &'static str,
64    }
65
66    #[async_trait]
67    impl AgentBackend for TestBackend {
68        fn id(&self) -> &'static str {
69            self.id
70        }
71        fn capabilities(&self) -> AgentCapabilities {
72            AgentCapabilities::default()
73        }
74        fn as_any(&self) -> &dyn std::any::Any {
75            self
76        }
77        async fn run(
78            &self,
79            _task: AgentTask,
80            _ctx: RunContext,
81        ) -> Result<AgentResult, BackendError> {
82            unimplemented!("registry tests never invoke run()")
83        }
84    }
85
86    fn make_backend(id: &'static str) -> Arc<dyn AgentBackend> {
87        Arc::new(TestBackend { id })
88    }
89
90    // ── construction ────────────────────────────────────────────
91
92    #[test]
93    fn test_new_is_empty() {
94        let reg = BackendRegistry::new();
95        assert!(matches!(
96            reg.default_backend(),
97            Err(SchedulerError::NoBackendRegistered)
98        ));
99    }
100
101    #[test]
102    fn test_default_is_empty() {
103        let reg = BackendRegistry::default();
104        assert!(matches!(
105            reg.default_backend(),
106            Err(SchedulerError::NoBackendRegistered)
107        ));
108    }
109
110    // ── register / with ────────────────────────────────────────
111
112    #[test]
113    fn test_register_adds_backend() {
114        let mut reg = BackendRegistry::new();
115        reg.register(make_backend("alpha"));
116        assert_eq!(reg.get("alpha").unwrap().id(), "alpha");
117    }
118
119    #[test]
120    fn test_with_builder_pattern() {
121        let reg = BackendRegistry::new()
122            .with(make_backend("a"))
123            .with(make_backend("b"))
124            .with(make_backend("c"));
125        assert_eq!(reg.get("a").unwrap().id(), "a");
126        assert_eq!(reg.get("b").unwrap().id(), "b");
127        assert_eq!(reg.get("c").unwrap().id(), "c");
128    }
129
130    #[test]
131    fn test_register_overwrites_existing_id() {
132        let mut reg = BackendRegistry::new();
133        reg.register(make_backend("dup"));
134        reg.register(make_backend("dup"));
135        assert_eq!(reg.get("dup").unwrap().id(), "dup");
136        // Still only one entry.
137        assert!(reg.get("other").is_err());
138    }
139
140    // ── get ────────────────────────────────────────────────────
141
142    #[test]
143    fn test_get_unknown_backend() {
144        let reg = BackendRegistry::new();
145        assert!(matches!(
146            reg.get("nonexistent"),
147            Err(SchedulerError::UnknownBackend(_))
148        ));
149    }
150
151    #[test]
152    fn test_get_after_register() {
153        let mut reg = BackendRegistry::new();
154        reg.register(make_backend("x"));
155        assert_eq!(reg.get("x").unwrap().id(), "x");
156    }
157
158    #[test]
159    fn test_get_multiple_backends() {
160        let mut reg = BackendRegistry::new();
161        reg.register(make_backend("first"));
162        reg.register(make_backend("second"));
163        reg.register(make_backend("third"));
164        assert_eq!(reg.get("first").unwrap().id(), "first");
165        assert_eq!(reg.get("second").unwrap().id(), "second");
166        assert_eq!(reg.get("third").unwrap().id(), "third");
167        assert!(reg.get("fourth").is_err());
168    }
169
170    // ── default_backend ────────────────────────────────────────
171
172    #[test]
173    fn test_default_backend_empty() {
174        let reg = BackendRegistry::new();
175        assert!(matches!(
176            reg.default_backend(),
177            Err(SchedulerError::NoBackendRegistered)
178        ));
179    }
180
181    #[test]
182    fn test_default_backend_returns_one_of_registered() {
183        let mut reg = BackendRegistry::new();
184        reg.register(make_backend("alpha"));
185        reg.register(make_backend("beta"));
186        let id = reg.default_backend().unwrap().id();
187        // HashMap order is non-deterministic; either is acceptable.
188        assert!(id == "alpha" || id == "beta", "unexpected id: {id}");
189    }
190
191    #[test]
192    fn test_default_backend_single_entry() {
193        let mut reg = BackendRegistry::new();
194        reg.register(make_backend("sole"));
195        assert_eq!(reg.default_backend().unwrap().id(), "sole");
196    }
197
198    // ── Clone ──────────────────────────────────────────────────
199
200    #[test]
201    fn test_clone_empty() {
202        let reg = BackendRegistry::new();
203        let cloned = reg.clone();
204        assert!(matches!(
205            cloned.default_backend(),
206            Err(SchedulerError::NoBackendRegistered)
207        ));
208    }
209
210    #[test]
211    fn test_clone_is_independent() {
212        let mut reg = BackendRegistry::new();
213        reg.register(make_backend("original"));
214        let mut cloned = reg.clone();
215        cloned.register(make_backend("new-only"));
216        // Original must not see the new backend.
217        assert!(reg.get("new-only").is_err());
218        assert_eq!(cloned.get("new-only").unwrap().id(), "new-only");
219        // Both still have "original".
220        assert_eq!(reg.get("original").unwrap().id(), "original");
221        assert_eq!(cloned.get("original").unwrap().id(), "original");
222    }
223
224    // ── Debug ──────────────────────────────────────────────────
225
226    #[test]
227    fn test_debug_empty() {
228        let reg = BackendRegistry::new();
229        let s = format!("{:?}", reg);
230        assert!(s.starts_with("BackendRegistry"));
231    }
232
233    #[test]
234    fn test_debug_with_backends() {
235        let mut reg = BackendRegistry::new();
236        reg.register(make_backend("dbg-a"));
237        reg.register(make_backend("dbg-b"));
238        let s = format!("{:?}", reg);
239        assert!(s.starts_with("BackendRegistry"));
240        assert!(s.contains("dbg-a") || s.contains("dbg-b"));
241    }
242}