pub mod legacy_adapter;
pub mod rust_adapter;
use crate::core::types::ExecutorKind;
use crate::orchestrator::apply::ApplyExecutor;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AdapterError {
AdapterNotFound { component_id: String },
ExecutionFailed {
component_id: String,
exit_code: i32,
stderr: String,
},
InvocationError {
component_id: String,
reason: String,
},
OutputParityError {
component_id: String,
expected: String,
actual: String,
},
}
impl fmt::Display for AdapterError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AdapterError::AdapterNotFound { component_id } => {
write!(f, "no adapter registered for component '{component_id}'")
}
AdapterError::ExecutionFailed {
component_id,
exit_code,
stderr,
} => {
write!(
f,
"execution failed for '{component_id}': exit code {exit_code}, stderr: {stderr}"
)
}
AdapterError::InvocationError {
component_id,
reason,
} => {
write!(f, "invocation error for '{component_id}': {reason}")
}
AdapterError::OutputParityError {
component_id,
expected,
actual,
} => {
write!(
f,
"output parity error for '{component_id}': expected '{expected}', got '{actual}'"
)
}
}
}
}
impl std::error::Error for AdapterError {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AdapterOutput {
pub component_id: String,
pub executor_kind: ExecutorKind,
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
pub duration_ms: u64,
}
impl AdapterOutput {
pub fn is_success(&self) -> bool {
self.exit_code == 0
}
}
pub trait Adapter: Send + Sync {
fn execute(&self, component_id: &str, version: &str) -> Result<AdapterOutput, AdapterError>;
fn executor_kind(&self) -> ExecutorKind;
}
pub struct AdapterRegistry {
rust_adapters: HashMap<String, Box<dyn Adapter>>,
legacy_adapter: Option<Box<dyn Adapter>>,
known_legacy_ids: HashMap<String, String>,
}
impl AdapterRegistry {
pub fn new() -> Self {
Self {
rust_adapters: HashMap::new(),
legacy_adapter: None,
known_legacy_ids: HashMap::new(),
}
}
pub fn with_legacy(scripts_dir: impl Into<PathBuf>) -> Self {
let scripts_dir = scripts_dir.into();
let mut registry = Self::new();
registry.legacy_adapter = Some(Box::new(legacy_adapter::LegacyAdapter::new(
scripts_dir.clone(),
)));
registry.register_known_legacy_ids(&scripts_dir);
registry
}
pub fn register_rust(&mut self, component_id: impl Into<String>, adapter: Box<dyn Adapter>) {
self.rust_adapters.insert(component_id.into(), adapter);
}
fn register_known_legacy_ids(&mut self, _scripts_dir: &Path) {
let known = crate::platform::registry::known_components();
for info in known {
if !info.installer_script.is_empty() {
self.known_legacy_ids
.insert(info.id.clone(), info.installer_script.clone());
}
}
}
pub fn get_adapter(&self, component_id: &str) -> Result<AdapterRef, AdapterError> {
if self.rust_adapters.contains_key(component_id) {
return Ok(AdapterRef {
executor_kind: ExecutorKind::Rust,
component_id: component_id.to_string(),
});
}
if self.legacy_adapter.is_some() {
if self.known_legacy_ids.contains_key(component_id) {
return Ok(AdapterRef {
executor_kind: ExecutorKind::LegacyScript,
component_id: component_id.to_string(),
});
}
}
Err(AdapterError::AdapterNotFound {
component_id: component_id.to_string(),
})
}
pub fn execute(
&self,
component_id: &str,
version: &str,
) -> Result<AdapterOutput, AdapterError> {
if let Some(rust_adapter) = self.rust_adapters.get(component_id) {
return rust_adapter.execute(component_id, version);
}
if let Some(legacy) = &self.legacy_adapter {
if self.known_legacy_ids.contains_key(component_id) {
return legacy.execute(component_id, version);
}
}
Err(AdapterError::AdapterNotFound {
component_id: component_id.to_string(),
})
}
pub fn has_rust_adapter(&self, component_id: &str) -> bool {
self.rust_adapters.contains_key(component_id)
}
pub fn has_legacy_adapter(&self, component_id: &str) -> bool {
self.known_legacy_ids.contains_key(component_id)
}
pub fn executor_kind(&self, component_id: &str) -> Result<ExecutorKind, AdapterError> {
let adapter_ref = self.get_adapter(component_id)?;
Ok(adapter_ref.executor_kind)
}
pub fn rust_adapter_count(&self) -> usize {
self.rust_adapters.len()
}
pub fn legacy_adapter_count(&self) -> usize {
self.known_legacy_ids.len()
}
pub fn rust_component_ids(&self) -> Vec<String> {
self.rust_adapters.keys().cloned().collect()
}
pub fn legacy_component_ids(&self) -> Vec<String> {
self.known_legacy_ids.keys().cloned().collect()
}
}
impl Default for AdapterRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdapterRef {
pub executor_kind: ExecutorKind,
pub component_id: String,
}
pub struct RegistryExecutor {
registry: AdapterRegistry,
}
impl RegistryExecutor {
pub fn new(registry: AdapterRegistry) -> Self {
Self { registry }
}
}
impl ApplyExecutor for RegistryExecutor {
fn apply_component(&self, component_id: &str, proposed_version: &str) -> Result<(), String> {
match self.registry.execute(component_id, proposed_version) {
Ok(output) if output.is_success() => Ok(()),
Ok(output) => Err(format!(
"adapter failed for '{component_id}': exit code {}, stderr: {}",
output.exit_code, output.stderr
)),
Err(e) => Err(format!("adapter error for '{component_id}': {e}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockRustAdapter {
output_prefix: String,
}
impl MockRustAdapter {
fn new(prefix: &str) -> Self {
Self {
output_prefix: prefix.to_string(),
}
}
}
impl Adapter for MockRustAdapter {
fn execute(
&self,
component_id: &str,
version: &str,
) -> Result<AdapterOutput, AdapterError> {
Ok(AdapterOutput {
component_id: component_id.to_string(),
executor_kind: ExecutorKind::Rust,
stdout: format!("{}: {version} installed", self.output_prefix),
stderr: String::new(),
exit_code: 0,
duration_ms: 10,
})
}
fn executor_kind(&self) -> ExecutorKind {
ExecutorKind::Rust
}
}
struct FailingRustAdapter;
impl Adapter for FailingRustAdapter {
fn execute(
&self,
component_id: &str,
_version: &str,
) -> Result<AdapterOutput, AdapterError> {
Ok(AdapterOutput {
component_id: component_id.to_string(),
executor_kind: ExecutorKind::Rust,
stdout: String::new(),
stderr: "installation failed".to_string(),
exit_code: 1,
duration_ms: 100,
})
}
fn executor_kind(&self) -> ExecutorKind {
ExecutorKind::Rust
}
}
struct PartialFailureRustAdapter;
impl Adapter for PartialFailureRustAdapter {
fn execute(
&self,
component_id: &str,
_version: &str,
) -> Result<AdapterOutput, AdapterError> {
Ok(AdapterOutput {
component_id: component_id.to_string(),
executor_kind: ExecutorKind::Rust,
stdout: "partially completed".to_string(),
stderr: "some steps failed".to_string(),
exit_code: 2,
duration_ms: 50,
})
}
fn executor_kind(&self) -> ExecutorKind {
ExecutorKind::Rust
}
}
fn empty_registry() -> AdapterRegistry {
AdapterRegistry::new()
}
fn registry_with_rust_adapter() -> AdapterRegistry {
let mut registry = AdapterRegistry::new();
registry.register_rust(
"test-component",
Box::new(MockRustAdapter::new("rust-impl")),
);
registry
}
fn registry_with_both() -> AdapterRegistry {
let mut registry = AdapterRegistry::new();
registry.register_rust("pytorch", Box::new(MockRustAdapter::new("rust-pytorch")));
registry.legacy_adapter = Some(Box::new(legacy_adapter::LegacyAdapter::new(
PathBuf::from("/nonexistent/scripts"),
)));
let known = crate::platform::registry::known_components();
for info in known {
if !info.installer_script.is_empty() {
registry
.known_legacy_ids
.insert(info.id.clone(), info.installer_script.clone());
}
}
registry
}
#[test]
fn test_rust_adapter_dispatches_to_rust_impl() {
let registry = registry_with_rust_adapter();
let result = registry.execute("test-component", "1.0.0").unwrap();
assert_eq!(result.executor_kind, ExecutorKind::Rust);
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("rust-impl"));
assert!(result.stdout.contains("1.0.0 installed"));
}
#[test]
fn test_rust_adapter_no_shell_subprocess() {
let registry = registry_with_rust_adapter();
let result = registry.execute("test-component", "1.0.0").unwrap();
assert!(!result.stdout.contains("bash"));
assert!(!result.stdout.contains("sh"));
assert_eq!(result.executor_kind, ExecutorKind::Rust);
}
#[test]
fn test_rust_adapter_takes_priority_over_legacy() {
let registry = registry_with_both();
let result = registry.execute("pytorch", "2.5.0").unwrap();
assert_eq!(result.executor_kind, ExecutorKind::Rust);
assert!(result.stdout.contains("rust-pytorch"));
}
#[test]
fn test_legacy_fallback_when_no_rust_adapter() {
let registry = registry_with_both();
let adapter_ref = registry.get_adapter("triton").unwrap();
assert_eq!(adapter_ref.executor_kind, ExecutorKind::LegacyScript);
}
#[test]
fn test_legacy_adapter_has_correct_script_mapping() {
let registry = registry_with_both();
assert_eq!(
registry.known_legacy_ids.get("pytorch").map(|s| s.as_str()),
Some("install_pytorch_rocm.sh")
);
assert_eq!(
registry.known_legacy_ids.get("triton").map(|s| s.as_str()),
Some("install_triton_multi.sh")
);
assert_eq!(
registry
.known_legacy_ids
.get("flash-attn")
.map(|s| s.as_str()),
Some("install_flash_attention_ck.sh")
);
assert_eq!(
registry
.known_legacy_ids
.get("deepspeed")
.map(|s| s.as_str()),
Some("install_deepspeed.sh")
);
}
#[test]
fn test_all_17_known_components_have_legacy_mapping() {
let registry = registry_with_both();
let legacy_count = registry.known_legacy_ids.len();
assert!(
legacy_count >= 15,
"Expected at least 15 legacy mappings, got {legacy_count}"
);
}
#[test]
fn test_unknown_component_returns_adapter_not_found() {
let registry = empty_registry();
let result = registry.execute("nonexistent-component", "1.0.0");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, AdapterError::AdapterNotFound { .. }));
}
#[test]
fn test_unknown_component_error_contains_id() {
let registry = empty_registry();
let result = registry.execute("fake-component-xyz", "1.0.0");
let err = result.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("fake-component-xyz"));
}
#[test]
fn test_multiple_unknown_ids_return_not_found() {
let registry = empty_registry();
let unknown_ids = [
"does-not-exist",
"also-fake",
"not-a-component",
"imaginary-tool",
"xyz-123",
];
for id in &unknown_ids {
let result = registry.execute(id, "1.0.0");
assert!(
matches!(result, Err(AdapterError::AdapterNotFound { .. })),
"Expected AdapterNotFound for '{id}'"
);
}
}
#[test]
fn test_unknown_with_rust_registry_still_not_found() {
let registry = registry_with_rust_adapter();
let result = registry.execute("other-component", "1.0.0");
assert!(matches!(result, Err(AdapterError::AdapterNotFound { .. })));
}
#[test]
fn test_rust_adapter_output_format_matches_shell_convention() {
let registry = registry_with_rust_adapter();
let result = registry.execute("test-component", "2.0.0").unwrap();
assert!(result.stdout.contains("installed"));
assert!(result.stdout.contains("2.0.0"));
}
#[test]
fn test_adapter_output_has_all_required_fields() {
let registry = registry_with_rust_adapter();
let result = registry.execute("test-component", "1.0.0").unwrap();
assert!(!result.component_id.is_empty());
assert_eq!(result.component_id, "test-component");
assert!(result.duration_ms > 0);
assert_eq!(result.exit_code, 0);
}
#[test]
fn test_exit_code_0_on_success() {
let registry = registry_with_rust_adapter();
let result = registry.execute("test-component", "1.0.0").unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.is_success());
}
#[test]
fn test_exit_code_1_on_failure() {
let mut registry = AdapterRegistry::new();
registry.register_rust("fail-comp", Box::new(FailingRustAdapter));
let result = registry.execute("fail-comp", "1.0.0").unwrap();
assert_eq!(result.exit_code, 1);
assert!(!result.is_success());
assert!(!result.stderr.is_empty());
}
#[test]
fn test_exit_code_2_on_partial_failure() {
let mut registry = AdapterRegistry::new();
registry.register_rust("partial-comp", Box::new(PartialFailureRustAdapter));
let result = registry.execute("partial-comp", "1.0.0").unwrap();
assert_eq!(result.exit_code, 2);
assert!(!result.is_success());
assert!(!result.stdout.is_empty());
assert!(!result.stderr.is_empty());
}
#[test]
fn test_adapter_error_display() {
let err = AdapterError::AdapterNotFound {
component_id: "test".to_string(),
};
assert!(err.to_string().contains("test"));
assert!(err.to_string().contains("no adapter registered"));
let err = AdapterError::ExecutionFailed {
component_id: "pytorch".to_string(),
exit_code: 1,
stderr: "error msg".to_string(),
};
assert!(err.to_string().contains("exit code 1"));
assert!(err.to_string().contains("error msg"));
let err = AdapterError::InvocationError {
component_id: "rocm".to_string(),
reason: "script not found".to_string(),
};
assert!(err.to_string().contains("script not found"));
}
#[test]
fn test_adapter_error_serde_roundtrip() {
let errors = vec![
AdapterError::AdapterNotFound {
component_id: "test".to_string(),
},
AdapterError::ExecutionFailed {
component_id: "pytorch".to_string(),
exit_code: 1,
stderr: "failed".to_string(),
},
AdapterError::InvocationError {
component_id: "rocm".to_string(),
reason: "not found".to_string(),
},
AdapterError::OutputParityError {
component_id: "triton".to_string(),
expected: "ok".to_string(),
actual: "err".to_string(),
},
];
for err in &errors {
let json = serde_json::to_string(err).unwrap();
let back: AdapterError = serde_json::from_str(&json).unwrap();
assert_eq!(err, &back);
}
}
#[test]
fn test_adapter_output_serde_roundtrip() {
let output = AdapterOutput {
component_id: "pytorch".to_string(),
executor_kind: ExecutorKind::Rust,
stdout: "installed".to_string(),
stderr: String::new(),
exit_code: 0,
duration_ms: 150,
};
let json = serde_json::to_string(&output).unwrap();
let back: AdapterOutput = serde_json::from_str(&json).unwrap();
assert_eq!(output, back);
}
#[test]
fn test_adapter_output_is_success() {
let success = AdapterOutput {
component_id: "test".to_string(),
executor_kind: ExecutorKind::Rust,
stdout: String::new(),
stderr: String::new(),
exit_code: 0,
duration_ms: 10,
};
assert!(success.is_success());
let failure = AdapterOutput {
component_id: "test".to_string(),
executor_kind: ExecutorKind::Rust,
stdout: String::new(),
stderr: "error".to_string(),
exit_code: 1,
duration_ms: 10,
};
assert!(!failure.is_success());
}
#[test]
fn test_has_rust_adapter() {
let registry = registry_with_rust_adapter();
assert!(registry.has_rust_adapter("test-component"));
assert!(!registry.has_rust_adapter("other-component"));
}
#[test]
fn test_has_legacy_adapter() {
let registry = registry_with_both();
assert!(registry.has_legacy_adapter("pytorch"));
assert!(registry.has_legacy_adapter("triton"));
assert!(!registry.has_legacy_adapter("nonexistent"));
}
#[test]
fn test_executor_kind_rust() {
let registry = registry_with_rust_adapter();
let kind = registry.executor_kind("test-component").unwrap();
assert_eq!(kind, ExecutorKind::Rust);
}
#[test]
fn test_executor_kind_unknown() {
let registry = empty_registry();
let result = registry.executor_kind("nonexistent");
assert!(matches!(result, Err(AdapterError::AdapterNotFound { .. })));
}
#[test]
fn test_rust_component_ids() {
let registry = registry_with_rust_adapter();
let ids = registry.rust_component_ids();
assert_eq!(ids, vec!["test-component"]);
}
#[test]
fn test_legacy_component_ids() {
let registry = registry_with_both();
let ids = registry.legacy_component_ids();
assert!(!ids.is_empty());
assert!(ids.contains(&"pytorch".to_string()));
assert!(ids.contains(&"triton".to_string()));
}
#[test]
fn test_registry_executor_success() {
let mut inner = AdapterRegistry::new();
inner.register_rust("test", Box::new(MockRustAdapter::new("ok")));
let executor = RegistryExecutor::new(inner);
let result = executor.apply_component("test", "1.0.0");
assert!(result.is_ok());
}
#[test]
fn test_registry_executor_failure() {
let mut inner = AdapterRegistry::new();
inner.register_rust("fail", Box::new(FailingRustAdapter));
let executor = RegistryExecutor::new(inner);
let result = executor.apply_component("fail", "1.0.0");
assert!(result.is_err());
assert!(result.unwrap_err().contains("exit code 1"));
}
#[test]
fn test_registry_executor_not_found() {
let inner = AdapterRegistry::new();
let executor = RegistryExecutor::new(inner);
let result = executor.apply_component("nonexistent", "1.0.0");
assert!(result.is_err());
assert!(result.unwrap_err().contains("no adapter registered"));
}
#[test]
fn test_adapter_registry_default() {
let registry = AdapterRegistry::default();
assert!(registry.rust_adapters.is_empty());
assert!(registry.legacy_adapter.is_none());
assert!(registry.known_legacy_ids.is_empty());
}
}