1use crate::{Harness, HarnessInfo};
11#[cfg(feature = "bob")]
12use crate::Bob;
13#[cfg(feature = "claude")]
14use crate::Claude;
15#[cfg(feature = "codex")]
16use crate::Codex;
17
18pub const DEFAULT_HARNESS_ID: &str = "bob";
22
23#[derive(Default)]
34pub struct Registry {
35 harnesses: Vec<Box<dyn Harness>>,
36}
37
38impl Registry {
39 pub fn new() -> Self {
41 Self::default()
42 }
43
44 pub fn register(mut self, harness: impl Harness + 'static) -> Self {
47 self.harnesses.push(Box::new(harness));
48 self
49 }
50
51 pub fn by_id(&self, id: &str) -> Option<&dyn Harness> {
53 self.harnesses
54 .iter()
55 .map(Box::as_ref)
56 .find(|h| h.info().id == id)
57 }
58
59 pub fn catalog(&self) -> Vec<HarnessInfo> {
61 self.harnesses.iter().map(|h| h.info()).collect()
62 }
63
64 pub fn ids(&self) -> Vec<String> {
66 self.harnesses.iter().map(|h| h.info().id).collect()
67 }
68}
69
70pub fn default_registry() -> Registry {
73 #[allow(unused_mut)]
74 let mut reg = Registry::new();
75 #[cfg(feature = "bob")]
76 {
77 reg = reg.register(Bob::new());
78 }
79 #[cfg(feature = "claude")]
80 {
81 reg = reg.register(Claude::new());
82 }
83 #[cfg(feature = "codex")]
84 {
85 reg = reg.register(Codex::new());
86 }
87 reg
88}
89
90pub fn harness_by_id(id: &str) -> Option<Box<dyn Harness>> {
93 let _ = id;
94 #[cfg(feature = "bob")]
95 {
96 if id == crate::BOB_HARNESS_ID {
97 return Some(Box::new(Bob::new()));
98 }
99 }
100 #[cfg(feature = "claude")]
101 {
102 if id == crate::CLAUDE_HARNESS_ID {
103 return Some(Box::new(Claude::new()));
104 }
105 }
106 #[cfg(feature = "codex")]
107 {
108 if id == crate::CODEX_HARNESS_ID {
109 return Some(Box::new(Codex::new()));
110 }
111 }
112 None
113}
114
115pub fn harness_catalog() -> Vec<HarnessInfo> {
117 default_registry().catalog()
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::{
124 CredentialSpec, HarnessCapabilities, HarnessReadiness, InstallCallback, RunCallback,
125 RunHandle, RunRequest,
126 };
127
128 #[test]
129 fn default_registry_lists_bob_claude_codex_in_order() {
130 assert_eq!(default_registry().ids(), vec!["bob", "claude", "codex"]);
131 assert_eq!(default_registry().catalog()[0].id, DEFAULT_HARNESS_ID);
132 }
133
134 #[test]
135 fn harness_by_id_resolves_builtins_and_rejects_unknown() {
136 assert!(harness_by_id("bob").is_some());
137 assert!(harness_by_id("claude").is_some());
138 assert!(harness_by_id("codex").is_some());
139 assert!(harness_by_id("nope").is_none());
140 }
141
142 #[test]
143 fn capabilities_match_each_adapter_and_back_credential_required() {
144 let caps = |id: &str| harness_by_id(id).unwrap().info().capabilities;
145
146 let bob = caps("bob");
147 assert!(bob.credential_required && bob.previews_edits);
148 assert!(bob.models.is_empty() && !bob.supports_effort && !bob.supports_max_turns);
149 assert_eq!(
150 bob.credential_required,
151 harness_by_id("bob").unwrap().credential().required
152 );
153
154 let claude = caps("claude");
155 assert!(!claude.credential_required && !claude.previews_edits);
156 assert!(!claude.models.is_empty() && !claude.allows_custom_model);
157 assert!(claude.supports_max_turns && !claude.supports_effort);
158
159 let codex = caps("codex");
160 assert!(!codex.credential_required && !codex.previews_edits);
161 assert!(codex.allows_custom_model && codex.supports_effort && !codex.supports_max_turns);
162
163 assert!(claude.supports_login && codex.supports_login && !bob.supports_login);
164 }
165
166 struct Acme;
169 impl Harness for Acme {
170 fn info(&self) -> HarnessInfo {
171 HarnessInfo {
172 id: "acme".to_owned(),
173 display_name: "Acme".to_owned(),
174 description: "A custom third-party harness.".to_owned(),
175 requires_install: false,
176 capabilities: HarnessCapabilities {
177 credential_required: false,
178 previews_edits: false,
179 models: Vec::new(),
180 allows_custom_model: true,
181 supports_effort: false,
182 supports_max_turns: false,
183 supports_login: false,
184 },
185 }
186 }
187 fn readiness(&self) -> HarnessReadiness {
188 HarnessReadiness {
189 harness_id: "acme".to_owned(),
190 ready: true,
191 installed: true,
192 version: None,
193 auth_configured: true,
194 error: None,
195 details: serde_json::Value::Null,
196 }
197 }
198 fn install(&self, _on_event: InstallCallback) -> Result<(), crate::HarnessError> {
199 Ok(())
200 }
201 fn run(
202 &self,
203 _req: RunRequest,
204 _on_event: RunCallback,
205 ) -> Result<RunHandle, crate::HarnessError> {
206 Err(crate::HarnessError::Other(
209 "acme: run not implemented in test".to_owned(),
210 ))
211 }
212 fn credential(&self) -> CredentialSpec {
213 CredentialSpec {
214 label: "Acme key".to_owned(),
215 keychain_service: "acme".to_owned(),
216 keychain_account: "ACME_API_KEY".to_owned(),
217 required: false,
218 }
219 }
220 }
221
222 #[test]
223 fn custom_harness_registers_and_resolves_alongside_builtins() {
224 let reg = Registry::new().register(Bob::new()).register(Acme);
225 assert!(reg.by_id("bob").is_some());
226 assert!(reg.by_id("acme").is_some(), "custom harness must resolve");
227 assert_eq!(reg.ids(), vec!["bob", "acme"]);
228 }
229}