1pub mod legacy_adapter;
31pub mod rust_adapter;
32
33use crate::core::types::ExecutorKind;
34use crate::orchestrator::apply::ApplyExecutor;
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37use std::fmt;
38use std::path::{Path, PathBuf};
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub enum AdapterError {
47 AdapterNotFound { component_id: String },
49 ExecutionFailed {
51 component_id: String,
52 exit_code: i32,
53 stderr: String,
54 },
55 InvocationError {
57 component_id: String,
58 reason: String,
59 },
60 OutputParityError {
62 component_id: String,
63 expected: String,
64 actual: String,
65 },
66}
67
68impl fmt::Display for AdapterError {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 AdapterError::AdapterNotFound { component_id } => {
72 write!(f, "no adapter registered for component '{component_id}'")
73 }
74 AdapterError::ExecutionFailed {
75 component_id,
76 exit_code,
77 stderr,
78 } => {
79 write!(
80 f,
81 "execution failed for '{component_id}': exit code {exit_code}, stderr: {stderr}"
82 )
83 }
84 AdapterError::InvocationError {
85 component_id,
86 reason,
87 } => {
88 write!(f, "invocation error for '{component_id}': {reason}")
89 }
90 AdapterError::OutputParityError {
91 component_id,
92 expected,
93 actual,
94 } => {
95 write!(
96 f,
97 "output parity error for '{component_id}': expected '{expected}', got '{actual}'"
98 )
99 }
100 }
101 }
102}
103
104impl std::error::Error for AdapterError {}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
112pub struct AdapterOutput {
113 pub component_id: String,
115 pub executor_kind: ExecutorKind,
117 pub stdout: String,
119 pub stderr: String,
121 pub exit_code: i32,
123 pub duration_ms: u64,
125}
126
127impl AdapterOutput {
128 pub fn is_success(&self) -> bool {
130 self.exit_code == 0
131 }
132}
133
134pub trait Adapter: Send + Sync {
143 fn execute(&self, component_id: &str, version: &str) -> Result<AdapterOutput, AdapterError>;
147
148 fn executor_kind(&self) -> ExecutorKind;
150}
151
152pub struct AdapterRegistry {
166 rust_adapters: HashMap<String, Box<dyn Adapter>>,
168 legacy_adapter: Option<Box<dyn Adapter>>,
170 known_legacy_ids: HashMap<String, String>,
172}
173
174impl AdapterRegistry {
175 pub fn new() -> Self {
177 Self {
178 rust_adapters: HashMap::new(),
179 legacy_adapter: None,
180 known_legacy_ids: HashMap::new(),
181 }
182 }
183
184 pub fn with_legacy(scripts_dir: impl Into<PathBuf>) -> Self {
189 let scripts_dir = scripts_dir.into();
190 let mut registry = Self::new();
191 registry.legacy_adapter = Some(Box::new(legacy_adapter::LegacyAdapter::new(
192 scripts_dir.clone(),
193 )));
194 registry.register_known_legacy_ids(&scripts_dir);
195 registry
196 }
197
198 pub fn register_rust(&mut self, component_id: impl Into<String>, adapter: Box<dyn Adapter>) {
202 self.rust_adapters.insert(component_id.into(), adapter);
203 }
204
205 fn register_known_legacy_ids(&mut self, _scripts_dir: &Path) {
207 let known = crate::platform::registry::known_components();
208 for info in known {
209 if !info.installer_script.is_empty() {
210 self.known_legacy_ids
211 .insert(info.id.clone(), info.installer_script.clone());
212 }
213 }
214 }
215
216 pub fn get_adapter(&self, component_id: &str) -> Result<AdapterRef, AdapterError> {
226 if self.rust_adapters.contains_key(component_id) {
228 return Ok(AdapterRef {
229 executor_kind: ExecutorKind::Rust,
230 component_id: component_id.to_string(),
231 });
232 }
233
234 if self.legacy_adapter.is_some() {
236 if self.known_legacy_ids.contains_key(component_id) {
238 return Ok(AdapterRef {
239 executor_kind: ExecutorKind::LegacyScript,
240 component_id: component_id.to_string(),
241 });
242 }
243 }
244
245 Err(AdapterError::AdapterNotFound {
247 component_id: component_id.to_string(),
248 })
249 }
250
251 pub fn execute(
255 &self,
256 component_id: &str,
257 version: &str,
258 ) -> Result<AdapterOutput, AdapterError> {
259 if let Some(rust_adapter) = self.rust_adapters.get(component_id) {
261 return rust_adapter.execute(component_id, version);
262 }
263
264 if let Some(legacy) = &self.legacy_adapter {
266 if self.known_legacy_ids.contains_key(component_id) {
267 return legacy.execute(component_id, version);
268 }
269 }
270
271 Err(AdapterError::AdapterNotFound {
273 component_id: component_id.to_string(),
274 })
275 }
276
277 pub fn has_rust_adapter(&self, component_id: &str) -> bool {
279 self.rust_adapters.contains_key(component_id)
280 }
281
282 pub fn has_legacy_adapter(&self, component_id: &str) -> bool {
284 self.known_legacy_ids.contains_key(component_id)
285 }
286
287 pub fn executor_kind(&self, component_id: &str) -> Result<ExecutorKind, AdapterError> {
289 let adapter_ref = self.get_adapter(component_id)?;
290 Ok(adapter_ref.executor_kind)
291 }
292
293 pub fn rust_adapter_count(&self) -> usize {
295 self.rust_adapters.len()
296 }
297
298 pub fn legacy_adapter_count(&self) -> usize {
300 self.known_legacy_ids.len()
301 }
302
303 pub fn rust_component_ids(&self) -> Vec<String> {
305 self.rust_adapters.keys().cloned().collect()
306 }
307
308 pub fn legacy_component_ids(&self) -> Vec<String> {
310 self.known_legacy_ids.keys().cloned().collect()
311 }
312}
313
314impl Default for AdapterRegistry {
315 fn default() -> Self {
316 Self::new()
317 }
318}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
325pub struct AdapterRef {
326 pub executor_kind: ExecutorKind,
328 pub component_id: String,
330}
331
332pub struct RegistryExecutor {
341 registry: AdapterRegistry,
342}
343
344impl RegistryExecutor {
345 pub fn new(registry: AdapterRegistry) -> Self {
347 Self { registry }
348 }
349}
350
351impl ApplyExecutor for RegistryExecutor {
352 fn apply_component(&self, component_id: &str, proposed_version: &str) -> Result<(), String> {
353 match self.registry.execute(component_id, proposed_version) {
354 Ok(output) if output.is_success() => Ok(()),
355 Ok(output) => Err(format!(
356 "adapter failed for '{component_id}': exit code {}, stderr: {}",
357 output.exit_code, output.stderr
358 )),
359 Err(e) => Err(format!("adapter error for '{component_id}': {e}")),
360 }
361 }
362}
363
364#[cfg(test)]
369mod tests {
370 use super::*;
371
372 struct MockRustAdapter {
378 output_prefix: String,
379 }
380
381 impl MockRustAdapter {
382 fn new(prefix: &str) -> Self {
383 Self {
384 output_prefix: prefix.to_string(),
385 }
386 }
387 }
388
389 impl Adapter for MockRustAdapter {
390 fn execute(
391 &self,
392 component_id: &str,
393 version: &str,
394 ) -> Result<AdapterOutput, AdapterError> {
395 Ok(AdapterOutput {
396 component_id: component_id.to_string(),
397 executor_kind: ExecutorKind::Rust,
398 stdout: format!("{}: {version} installed", self.output_prefix),
399 stderr: String::new(),
400 exit_code: 0,
401 duration_ms: 10,
402 })
403 }
404
405 fn executor_kind(&self) -> ExecutorKind {
406 ExecutorKind::Rust
407 }
408 }
409
410 struct FailingRustAdapter;
412
413 impl Adapter for FailingRustAdapter {
414 fn execute(
415 &self,
416 component_id: &str,
417 _version: &str,
418 ) -> Result<AdapterOutput, AdapterError> {
419 Ok(AdapterOutput {
420 component_id: component_id.to_string(),
421 executor_kind: ExecutorKind::Rust,
422 stdout: String::new(),
423 stderr: "installation failed".to_string(),
424 exit_code: 1,
425 duration_ms: 100,
426 })
427 }
428
429 fn executor_kind(&self) -> ExecutorKind {
430 ExecutorKind::Rust
431 }
432 }
433
434 struct PartialFailureRustAdapter;
436
437 impl Adapter for PartialFailureRustAdapter {
438 fn execute(
439 &self,
440 component_id: &str,
441 _version: &str,
442 ) -> Result<AdapterOutput, AdapterError> {
443 Ok(AdapterOutput {
444 component_id: component_id.to_string(),
445 executor_kind: ExecutorKind::Rust,
446 stdout: "partially completed".to_string(),
447 stderr: "some steps failed".to_string(),
448 exit_code: 2,
449 duration_ms: 50,
450 })
451 }
452
453 fn executor_kind(&self) -> ExecutorKind {
454 ExecutorKind::Rust
455 }
456 }
457
458 fn empty_registry() -> AdapterRegistry {
460 AdapterRegistry::new()
461 }
462
463 fn registry_with_rust_adapter() -> AdapterRegistry {
465 let mut registry = AdapterRegistry::new();
466 registry.register_rust(
467 "test-component",
468 Box::new(MockRustAdapter::new("rust-impl")),
469 );
470 registry
471 }
472
473 fn registry_with_both() -> AdapterRegistry {
475 let mut registry = AdapterRegistry::new();
476 registry.register_rust("pytorch", Box::new(MockRustAdapter::new("rust-pytorch")));
478 registry.legacy_adapter = Some(Box::new(legacy_adapter::LegacyAdapter::new(
480 PathBuf::from("/nonexistent/scripts"),
481 )));
482 let known = crate::platform::registry::known_components();
484 for info in known {
485 if !info.installer_script.is_empty() {
486 registry
487 .known_legacy_ids
488 .insert(info.id.clone(), info.installer_script.clone());
489 }
490 }
491 registry
492 }
493
494 #[test]
499 fn test_rust_adapter_dispatches_to_rust_impl() {
500 let registry = registry_with_rust_adapter();
501 let result = registry.execute("test-component", "1.0.0").unwrap();
502
503 assert_eq!(result.executor_kind, ExecutorKind::Rust);
504 assert_eq!(result.exit_code, 0);
505 assert!(result.stdout.contains("rust-impl"));
506 assert!(result.stdout.contains("1.0.0 installed"));
507 }
508
509 #[test]
510 fn test_rust_adapter_no_shell_subprocess() {
511 let registry = registry_with_rust_adapter();
512 let result = registry.execute("test-component", "1.0.0").unwrap();
513
514 assert!(!result.stdout.contains("bash"));
516 assert!(!result.stdout.contains("sh"));
517 assert_eq!(result.executor_kind, ExecutorKind::Rust);
518 }
519
520 #[test]
521 fn test_rust_adapter_takes_priority_over_legacy() {
522 let registry = registry_with_both();
523
524 let result = registry.execute("pytorch", "2.5.0").unwrap();
526 assert_eq!(result.executor_kind, ExecutorKind::Rust);
527 assert!(result.stdout.contains("rust-pytorch"));
528 }
529
530 #[test]
535 fn test_legacy_fallback_when_no_rust_adapter() {
536 let registry = registry_with_both();
537
538 let adapter_ref = registry.get_adapter("triton").unwrap();
540 assert_eq!(adapter_ref.executor_kind, ExecutorKind::LegacyScript);
541 }
542
543 #[test]
544 fn test_legacy_adapter_has_correct_script_mapping() {
545 let registry = registry_with_both();
546
547 assert_eq!(
549 registry.known_legacy_ids.get("pytorch").map(|s| s.as_str()),
550 Some("install_pytorch_rocm.sh")
551 );
552 assert_eq!(
553 registry.known_legacy_ids.get("triton").map(|s| s.as_str()),
554 Some("install_triton_multi.sh")
555 );
556 assert_eq!(
557 registry
558 .known_legacy_ids
559 .get("flash-attn")
560 .map(|s| s.as_str()),
561 Some("install_flash_attention_ck.sh")
562 );
563 assert_eq!(
564 registry
565 .known_legacy_ids
566 .get("deepspeed")
567 .map(|s| s.as_str()),
568 Some("install_deepspeed.sh")
569 );
570 }
571
572 #[test]
573 fn test_all_17_known_components_have_legacy_mapping() {
574 let registry = registry_with_both();
575
576 let legacy_count = registry.known_legacy_ids.len();
579 assert!(
580 legacy_count >= 15,
581 "Expected at least 15 legacy mappings, got {legacy_count}"
582 );
583 }
584
585 #[test]
590 fn test_unknown_component_returns_adapter_not_found() {
591 let registry = empty_registry();
592 let result = registry.execute("nonexistent-component", "1.0.0");
593
594 assert!(result.is_err());
595 let err = result.unwrap_err();
596 assert!(matches!(err, AdapterError::AdapterNotFound { .. }));
597 }
598
599 #[test]
600 fn test_unknown_component_error_contains_id() {
601 let registry = empty_registry();
602 let result = registry.execute("fake-component-xyz", "1.0.0");
603
604 let err = result.unwrap_err();
605 let msg = err.to_string();
606 assert!(msg.contains("fake-component-xyz"));
607 }
608
609 #[test]
610 fn test_multiple_unknown_ids_return_not_found() {
611 let registry = empty_registry();
612 let unknown_ids = [
613 "does-not-exist",
614 "also-fake",
615 "not-a-component",
616 "imaginary-tool",
617 "xyz-123",
618 ];
619
620 for id in &unknown_ids {
621 let result = registry.execute(id, "1.0.0");
622 assert!(
623 matches!(result, Err(AdapterError::AdapterNotFound { .. })),
624 "Expected AdapterNotFound for '{id}'"
625 );
626 }
627 }
628
629 #[test]
630 fn test_unknown_with_rust_registry_still_not_found() {
631 let registry = registry_with_rust_adapter();
632 let result = registry.execute("other-component", "1.0.0");
634 assert!(matches!(result, Err(AdapterError::AdapterNotFound { .. })));
635 }
636
637 #[test]
642 fn test_rust_adapter_output_format_matches_shell_convention() {
643 let registry = registry_with_rust_adapter();
644 let result = registry.execute("test-component", "2.0.0").unwrap();
645
646 assert!(result.stdout.contains("installed"));
649 assert!(result.stdout.contains("2.0.0"));
650 }
651
652 #[test]
653 fn test_adapter_output_has_all_required_fields() {
654 let registry = registry_with_rust_adapter();
655 let result = registry.execute("test-component", "1.0.0").unwrap();
656
657 assert!(!result.component_id.is_empty());
659 assert_eq!(result.component_id, "test-component");
660 assert!(result.duration_ms > 0);
661 assert_eq!(result.exit_code, 0);
662 }
663
664 #[test]
669 fn test_exit_code_0_on_success() {
670 let registry = registry_with_rust_adapter();
671 let result = registry.execute("test-component", "1.0.0").unwrap();
672 assert_eq!(result.exit_code, 0);
673 assert!(result.is_success());
674 }
675
676 #[test]
677 fn test_exit_code_1_on_failure() {
678 let mut registry = AdapterRegistry::new();
679 registry.register_rust("fail-comp", Box::new(FailingRustAdapter));
680
681 let result = registry.execute("fail-comp", "1.0.0").unwrap();
682 assert_eq!(result.exit_code, 1);
683 assert!(!result.is_success());
684 assert!(!result.stderr.is_empty());
685 }
686
687 #[test]
688 fn test_exit_code_2_on_partial_failure() {
689 let mut registry = AdapterRegistry::new();
690 registry.register_rust("partial-comp", Box::new(PartialFailureRustAdapter));
691
692 let result = registry.execute("partial-comp", "1.0.0").unwrap();
693 assert_eq!(result.exit_code, 2);
694 assert!(!result.is_success());
695 assert!(!result.stdout.is_empty());
696 assert!(!result.stderr.is_empty());
697 }
698
699 #[test]
704 fn test_adapter_error_display() {
705 let err = AdapterError::AdapterNotFound {
706 component_id: "test".to_string(),
707 };
708 assert!(err.to_string().contains("test"));
709 assert!(err.to_string().contains("no adapter registered"));
710
711 let err = AdapterError::ExecutionFailed {
712 component_id: "pytorch".to_string(),
713 exit_code: 1,
714 stderr: "error msg".to_string(),
715 };
716 assert!(err.to_string().contains("exit code 1"));
717 assert!(err.to_string().contains("error msg"));
718
719 let err = AdapterError::InvocationError {
720 component_id: "rocm".to_string(),
721 reason: "script not found".to_string(),
722 };
723 assert!(err.to_string().contains("script not found"));
724 }
725
726 #[test]
727 fn test_adapter_error_serde_roundtrip() {
728 let errors = vec![
729 AdapterError::AdapterNotFound {
730 component_id: "test".to_string(),
731 },
732 AdapterError::ExecutionFailed {
733 component_id: "pytorch".to_string(),
734 exit_code: 1,
735 stderr: "failed".to_string(),
736 },
737 AdapterError::InvocationError {
738 component_id: "rocm".to_string(),
739 reason: "not found".to_string(),
740 },
741 AdapterError::OutputParityError {
742 component_id: "triton".to_string(),
743 expected: "ok".to_string(),
744 actual: "err".to_string(),
745 },
746 ];
747
748 for err in &errors {
749 let json = serde_json::to_string(err).unwrap();
750 let back: AdapterError = serde_json::from_str(&json).unwrap();
751 assert_eq!(err, &back);
752 }
753 }
754
755 #[test]
760 fn test_adapter_output_serde_roundtrip() {
761 let output = AdapterOutput {
762 component_id: "pytorch".to_string(),
763 executor_kind: ExecutorKind::Rust,
764 stdout: "installed".to_string(),
765 stderr: String::new(),
766 exit_code: 0,
767 duration_ms: 150,
768 };
769 let json = serde_json::to_string(&output).unwrap();
770 let back: AdapterOutput = serde_json::from_str(&json).unwrap();
771 assert_eq!(output, back);
772 }
773
774 #[test]
775 fn test_adapter_output_is_success() {
776 let success = AdapterOutput {
777 component_id: "test".to_string(),
778 executor_kind: ExecutorKind::Rust,
779 stdout: String::new(),
780 stderr: String::new(),
781 exit_code: 0,
782 duration_ms: 10,
783 };
784 assert!(success.is_success());
785
786 let failure = AdapterOutput {
787 component_id: "test".to_string(),
788 executor_kind: ExecutorKind::Rust,
789 stdout: String::new(),
790 stderr: "error".to_string(),
791 exit_code: 1,
792 duration_ms: 10,
793 };
794 assert!(!failure.is_success());
795 }
796
797 #[test]
802 fn test_has_rust_adapter() {
803 let registry = registry_with_rust_adapter();
804 assert!(registry.has_rust_adapter("test-component"));
805 assert!(!registry.has_rust_adapter("other-component"));
806 }
807
808 #[test]
809 fn test_has_legacy_adapter() {
810 let registry = registry_with_both();
811 assert!(registry.has_legacy_adapter("pytorch"));
812 assert!(registry.has_legacy_adapter("triton"));
813 assert!(!registry.has_legacy_adapter("nonexistent"));
814 }
815
816 #[test]
817 fn test_executor_kind_rust() {
818 let registry = registry_with_rust_adapter();
819 let kind = registry.executor_kind("test-component").unwrap();
820 assert_eq!(kind, ExecutorKind::Rust);
821 }
822
823 #[test]
824 fn test_executor_kind_unknown() {
825 let registry = empty_registry();
826 let result = registry.executor_kind("nonexistent");
827 assert!(matches!(result, Err(AdapterError::AdapterNotFound { .. })));
828 }
829
830 #[test]
831 fn test_rust_component_ids() {
832 let registry = registry_with_rust_adapter();
833 let ids = registry.rust_component_ids();
834 assert_eq!(ids, vec!["test-component"]);
835 }
836
837 #[test]
838 fn test_legacy_component_ids() {
839 let registry = registry_with_both();
840 let ids = registry.legacy_component_ids();
841 assert!(!ids.is_empty());
842 assert!(ids.contains(&"pytorch".to_string()));
843 assert!(ids.contains(&"triton".to_string()));
844 }
845
846 #[test]
851 fn test_registry_executor_success() {
852 let mut inner = AdapterRegistry::new();
853 inner.register_rust("test", Box::new(MockRustAdapter::new("ok")));
854
855 let executor = RegistryExecutor::new(inner);
856 let result = executor.apply_component("test", "1.0.0");
857 assert!(result.is_ok());
858 }
859
860 #[test]
861 fn test_registry_executor_failure() {
862 let mut inner = AdapterRegistry::new();
863 inner.register_rust("fail", Box::new(FailingRustAdapter));
864
865 let executor = RegistryExecutor::new(inner);
866 let result = executor.apply_component("fail", "1.0.0");
867 assert!(result.is_err());
868 assert!(result.unwrap_err().contains("exit code 1"));
869 }
870
871 #[test]
872 fn test_registry_executor_not_found() {
873 let inner = AdapterRegistry::new();
874 let executor = RegistryExecutor::new(inner);
875 let result = executor.apply_component("nonexistent", "1.0.0");
876 assert!(result.is_err());
877 assert!(result.unwrap_err().contains("no adapter registered"));
878 }
879
880 #[test]
885 fn test_adapter_registry_default() {
886 let registry = AdapterRegistry::default();
887 assert!(registry.rust_adapters.is_empty());
888 assert!(registry.legacy_adapter.is_none());
889 assert!(registry.known_legacy_ids.is_empty());
890 }
891}