mockforge_plugin_loader/
runtime_adapter.rs

1//! Runtime adapter for supporting multiple plugin runtimes
2//!
3//! This module provides an abstraction layer that allows MockForge to load and execute
4//! plugins written in different languages and compiled with different WASM runtimes.
5//!
6//! Supported runtimes:
7//! - Rust (native, via wasmtime)
8//! - TinyGo (Go compiled to WASM)
9//! - AssemblyScript (TypeScript-like, compiled to WASM)
10//! - Remote (HTTP/gRPC-based plugins in any language)
11
12use async_trait::async_trait;
13use mockforge_plugin_core::{
14    AuthRequest, AuthResponse, DataQuery, DataResult, PluginContext, PluginError, PluginId,
15    ResolutionContext, ResponseData, ResponseRequest,
16};
17use std::collections::HashMap;
18use std::sync::{Arc, Mutex};
19use wasmtime::{Engine, Instance, Linker, Module, Store};
20use wasmtime_wasi::{WasiCtx, WasiCtxBuilder};
21
22/// Enum representing different plugin runtime types
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum RuntimeType {
25    /// Native Rust plugin compiled to WASM
26    Rust,
27    /// TinyGo compiled plugin
28    TinyGo,
29    /// AssemblyScript compiled plugin
30    AssemblyScript,
31    /// Remote plugin accessed via HTTP/gRPC
32    Remote(RemoteRuntimeConfig),
33}
34
35/// Configuration for remote plugin runtime
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct RemoteRuntimeConfig {
38    /// Protocol to use (http or grpc)
39    pub protocol: RemoteProtocol,
40    /// Endpoint URL
41    pub endpoint: String,
42    /// Request timeout in milliseconds
43    pub timeout_ms: u64,
44    /// Maximum number of retries
45    pub max_retries: u32,
46    /// Authentication configuration
47    pub auth: Option<RemoteAuthConfig>,
48}
49
50/// Protocol for remote plugin communication
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum RemoteProtocol {
53    /// HTTP/REST protocol
54    Http,
55    /// gRPC protocol
56    Grpc,
57}
58
59/// Authentication configuration for remote plugin runtime
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct RemoteAuthConfig {
62    /// Authentication type (bearer, api_key, etc.)
63    pub auth_type: String,
64    /// Authentication value
65    pub value: String,
66}
67
68/// Trait that all runtime adapters must implement
69///
70/// This allows different WASM runtimes and remote plugins to be used interchangeably
71#[async_trait]
72pub trait RuntimeAdapter: Send + Sync {
73    /// Get the runtime type
74    fn runtime_type(&self) -> RuntimeType;
75
76    /// Initialize the runtime (called once during plugin load)
77    async fn initialize(&mut self) -> Result<(), PluginError>;
78
79    /// Call authentication plugin
80    async fn call_auth(
81        &self,
82        context: &PluginContext,
83        request: &AuthRequest,
84    ) -> Result<AuthResponse, PluginError>;
85
86    /// Call template function plugin
87    async fn call_template_function(
88        &self,
89        function_name: &str,
90        args: &[serde_json::Value],
91        context: &ResolutionContext,
92    ) -> Result<serde_json::Value, PluginError>;
93
94    /// Call response generator plugin
95    async fn call_response_generator(
96        &self,
97        context: &PluginContext,
98        request: &ResponseRequest,
99    ) -> Result<ResponseData, PluginError>;
100
101    /// Call data source query plugin
102    async fn call_datasource_query(
103        &self,
104        query: &DataQuery,
105        context: &PluginContext,
106    ) -> Result<DataResult, PluginError>;
107
108    /// Health check - returns true if the plugin is healthy
109    async fn health_check(&self) -> Result<bool, PluginError>;
110
111    /// Cleanup resources (called during plugin unload)
112    async fn cleanup(&mut self) -> Result<(), PluginError>;
113
114    /// Get runtime-specific metrics
115    fn get_metrics(&self) -> HashMap<String, serde_json::Value> {
116        HashMap::new()
117    }
118}
119
120/// Detect runtime type from WASM binary
121pub fn detect_runtime_type(wasm_bytes: &[u8]) -> Result<RuntimeType, PluginError> {
122    // Parse WASM module to detect runtime type
123    // This is a simplified version - real implementation would parse WASM sections
124
125    // Check for TinyGo signatures
126    if has_tinygo_signature(wasm_bytes) {
127        return Ok(RuntimeType::TinyGo);
128    }
129
130    // Check for AssemblyScript signatures
131    if has_assemblyscript_signature(wasm_bytes) {
132        return Ok(RuntimeType::AssemblyScript);
133    }
134
135    // Default to Rust
136    Ok(RuntimeType::Rust)
137}
138
139/// Check if WASM binary has TinyGo signature
140fn has_tinygo_signature(wasm_bytes: &[u8]) -> bool {
141    // Look for TinyGo-specific exports or custom sections
142    // TinyGo typically exports functions like "resume" and "getsp"
143    // This is a placeholder - real implementation would use wasmparser
144
145    // Simple heuristic: check for "tinygo" in custom sections
146    String::from_utf8_lossy(wasm_bytes).contains("tinygo")
147}
148
149/// Check if WASM binary has AssemblyScript signature
150fn has_assemblyscript_signature(wasm_bytes: &[u8]) -> bool {
151    // Look for AssemblyScript-specific exports or custom sections
152    // AS typically has "__new", "__pin", "__unpin" exports
153
154    // Simple heuristic: check for "assemblyscript" in custom sections
155    String::from_utf8_lossy(wasm_bytes).contains("assemblyscript")
156}
157
158/// Factory for creating runtime adapters
159pub struct RuntimeAdapterFactory;
160
161impl RuntimeAdapterFactory {
162    /// Create a runtime adapter for the given runtime type
163    pub fn create(
164        runtime_type: RuntimeType,
165        plugin_id: PluginId,
166        wasm_bytes: Vec<u8>,
167    ) -> Result<Box<dyn RuntimeAdapter>, PluginError> {
168        match runtime_type {
169            RuntimeType::Rust => Ok(Box::new(RustAdapter::new(plugin_id, wasm_bytes)?)),
170            RuntimeType::TinyGo => Ok(Box::new(TinyGoAdapter::new(plugin_id, wasm_bytes)?)),
171            RuntimeType::AssemblyScript => {
172                Ok(Box::new(AssemblyScriptAdapter::new(plugin_id, wasm_bytes)?))
173            }
174            RuntimeType::Remote(config) => Ok(Box::new(RemoteAdapter::new(plugin_id, config)?)),
175        }
176    }
177}
178
179// ============================================================================
180// Rust Runtime Adapter (Existing Implementation)
181// ============================================================================
182
183/// WASM runtime adapter for plugins compiled from Rust
184pub struct RustAdapter {
185    plugin_id: PluginId,
186    engine: Arc<Engine>,
187    module: Module,
188    // Store and Instance need to be behind a Mutex since they're not Send/Sync
189    runtime: Mutex<Option<WasmRuntime>>,
190}
191
192struct WasmRuntime {
193    store: Store<WasiCtx>,
194    instance: Instance,
195}
196
197impl RustAdapter {
198    /// Create a new Rust runtime adapter
199    ///
200    /// # Arguments
201    /// * `plugin_id` - Unique identifier for the plugin
202    /// * `wasm_bytes` - WebAssembly binary bytes
203    ///
204    /// # Returns
205    /// `Ok(RustAdapter)` on success, `Err(PluginError)` if WASM module cannot be loaded
206    pub fn new(plugin_id: PluginId, wasm_bytes: Vec<u8>) -> Result<Self, PluginError> {
207        let engine = Arc::new(Engine::default());
208        let module = Module::from_binary(&engine, &wasm_bytes)
209            .map_err(|e| PluginError::execution(format!("Failed to load WASM module: {}", e)))?;
210
211        Ok(Self {
212            plugin_id,
213            engine,
214            module,
215            runtime: Mutex::new(None),
216        })
217    }
218
219    /// Helper to call a WASM function with JSON input/output
220    fn call_wasm_json(
221        &self,
222        function_name: &str,
223        input_data: serde_json::Value,
224    ) -> Result<serde_json::Value, PluginError> {
225        let mut runtime_guard =
226            self.runtime.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
227        let runtime = runtime_guard.as_mut().ok_or_else(|| {
228            PluginError::execution("Runtime not initialized. Call initialize() first.".to_string())
229        })?;
230
231        let input_json = serde_json::to_string(&input_data)
232            .map_err(|e| PluginError::execution(format!("Failed to serialize input: {}", e)))?;
233
234        let input_bytes = input_json.as_bytes();
235        let input_len = input_bytes.len() as i32;
236
237        // Get memory and alloc function
238        let memory =
239            runtime.instance.get_memory(&mut runtime.store, "memory").ok_or_else(|| {
240                PluginError::execution("WASM module must export 'memory'".to_string())
241            })?;
242
243        let alloc_func = runtime
244            .instance
245            .get_typed_func::<i32, i32>(&mut runtime.store, "alloc")
246            .map_err(|e| {
247            PluginError::execution(format!("Failed to get alloc function: {}", e))
248        })?;
249
250        // Allocate memory for input
251        let input_ptr = alloc_func
252            .call(&mut runtime.store, input_len)
253            .map_err(|e| PluginError::execution(format!("Failed to allocate memory: {}", e)))?;
254
255        // Write input to WASM memory
256        memory
257            .write(&mut runtime.store, input_ptr as usize, input_bytes)
258            .map_err(|e| PluginError::execution(format!("Failed to write input: {}", e)))?;
259
260        // Call the plugin function
261        let plugin_func = runtime
262            .instance
263            .get_typed_func::<(i32, i32), (i32, i32)>(&mut runtime.store, function_name)
264            .map_err(|e| {
265                PluginError::execution(format!("Function '{}' not found: {}", function_name, e))
266            })?;
267
268        let (output_ptr, output_len) =
269            plugin_func.call(&mut runtime.store, (input_ptr, input_len)).map_err(|e| {
270                PluginError::execution(format!(
271                    "Failed to call function '{}': {}",
272                    function_name, e
273                ))
274            })?;
275
276        // Read output from WASM memory
277        let mut output_bytes = vec![0u8; output_len as usize];
278        memory
279            .read(&runtime.store, output_ptr as usize, &mut output_bytes)
280            .map_err(|e| PluginError::execution(format!("Failed to read output: {}", e)))?;
281
282        // Deallocate memory if dealloc function exists
283        if let Ok(dealloc_func) =
284            runtime.instance.get_typed_func::<(i32, i32), ()>(&mut runtime.store, "dealloc")
285        {
286            let _ = dealloc_func.call(&mut runtime.store, (input_ptr, input_len));
287            let _ = dealloc_func.call(&mut runtime.store, (output_ptr, output_len));
288        }
289
290        // Parse output as JSON
291        let output_str = String::from_utf8(output_bytes)
292            .map_err(|e| PluginError::execution(format!("Failed to decode output: {}", e)))?;
293
294        serde_json::from_str(&output_str)
295            .map_err(|e| PluginError::execution(format!("Failed to parse output JSON: {}", e)))
296    }
297}
298
299#[async_trait]
300impl RuntimeAdapter for RustAdapter {
301    fn runtime_type(&self) -> RuntimeType {
302        RuntimeType::Rust
303    }
304
305    async fn initialize(&mut self) -> Result<(), PluginError> {
306        // Initialize wasmtime instance with the Rust WASM module
307        tracing::info!("Initializing Rust plugin: {}", self.plugin_id);
308
309        // Create WASI context
310        let wasi_ctx = WasiCtxBuilder::new().inherit_stderr().inherit_stdout().build();
311
312        // Create store
313        let mut store = Store::new(&self.engine, wasi_ctx);
314
315        // Create linker
316        let linker = Linker::new(&self.engine);
317
318        // Instantiate the module
319        let instance = linker
320            .instantiate(&mut store, &self.module)
321            .map_err(|e| PluginError::execution(format!("Failed to instantiate module: {}", e)))?;
322
323        // Store the runtime
324        let mut runtime_guard =
325            self.runtime.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
326        *runtime_guard = Some(WasmRuntime { store, instance });
327
328        tracing::info!("Successfully initialized Rust plugin: {}", self.plugin_id);
329        Ok(())
330    }
331
332    async fn call_auth(
333        &self,
334        context: &PluginContext,
335        request: &AuthRequest,
336    ) -> Result<AuthResponse, PluginError> {
337        // Call Rust WASM plugin's auth function
338        // Note: AuthRequest contains axum::http types which might not serialize well
339        // So we create a simplified version for WASM
340        let input = serde_json::json!({
341            "context": context,
342            "method": request.method.to_string(),
343            "uri": request.uri.to_string(),
344            "query_params": request.query_params,
345            "client_ip": request.client_ip,
346            "user_agent": request.user_agent,
347        });
348
349        let result = self.call_wasm_json("authenticate", input)?;
350        serde_json::from_value(result)
351            .map_err(|e| PluginError::execution(format!("Failed to parse AuthResponse: {}", e)))
352    }
353
354    async fn call_template_function(
355        &self,
356        function_name: &str,
357        args: &[serde_json::Value],
358        context: &ResolutionContext,
359    ) -> Result<serde_json::Value, PluginError> {
360        let input = serde_json::json!({
361            "function_name": function_name,
362            "args": args,
363            "context": context,
364        });
365
366        self.call_wasm_json("template_function", input)
367    }
368
369    async fn call_response_generator(
370        &self,
371        context: &PluginContext,
372        request: &ResponseRequest,
373    ) -> Result<ResponseData, PluginError> {
374        // Create simplified request for WASM (avoid non-serializable types)
375        let input = serde_json::json!({
376            "context": context,
377            "method": request.method.to_string(),
378            "uri": request.uri,
379            "path": request.path,
380            "query_params": request.query_params,
381            "path_params": request.path_params,
382            "client_ip": request.client_ip,
383            "user_agent": request.user_agent,
384        });
385
386        let result = self.call_wasm_json("generate_response", input)?;
387        serde_json::from_value(result)
388            .map_err(|e| PluginError::execution(format!("Failed to parse ResponseData: {}", e)))
389    }
390
391    async fn call_datasource_query(
392        &self,
393        query: &DataQuery,
394        context: &PluginContext,
395    ) -> Result<DataResult, PluginError> {
396        // DataQuery should be serializable, but create simplified version to be safe
397        let input = serde_json::json!({
398            "query_type": format!("{:?}", query.query_type),
399            "query": query.query,
400            "parameters": query.parameters,
401            "limit": query.limit,
402            "offset": query.offset,
403            "context": context,
404        });
405
406        let result = self.call_wasm_json("query_datasource", input)?;
407        serde_json::from_value(result)
408            .map_err(|e| PluginError::execution(format!("Failed to parse DataResult: {}", e)))
409    }
410
411    async fn health_check(&self) -> Result<bool, PluginError> {
412        Ok(true)
413    }
414
415    async fn cleanup(&mut self) -> Result<(), PluginError> {
416        Ok(())
417    }
418}
419
420// ============================================================================
421// TinyGo Runtime Adapter
422// ============================================================================
423
424/// WASM runtime adapter for plugins compiled from TinyGo
425pub struct TinyGoAdapter {
426    plugin_id: PluginId,
427    engine: Arc<Engine>,
428    module: Module,
429    runtime: Mutex<Option<WasmRuntime>>,
430}
431
432impl TinyGoAdapter {
433    /// Create a new TinyGo runtime adapter
434    ///
435    /// # Arguments
436    /// * `plugin_id` - Unique identifier for the plugin
437    /// * `wasm_bytes` - WebAssembly binary bytes from TinyGo compilation
438    ///
439    /// # Returns
440    /// `Ok(TinyGoAdapter)` on success, `Err(PluginError)` if WASM module cannot be loaded
441    pub fn new(plugin_id: PluginId, wasm_bytes: Vec<u8>) -> Result<Self, PluginError> {
442        let engine = Arc::new(Engine::default());
443        let module = Module::from_binary(&engine, &wasm_bytes).map_err(|e| {
444            PluginError::execution(format!("Failed to load TinyGo WASM module: {}", e))
445        })?;
446
447        Ok(Self {
448            plugin_id,
449            engine,
450            module,
451            runtime: Mutex::new(None),
452        })
453    }
454
455    /// Helper to call a TinyGo WASM function with JSON input/output
456    /// TinyGo uses specific memory management and calling conventions
457    fn call_wasm_json(
458        &self,
459        function_name: &str,
460        input_data: serde_json::Value,
461    ) -> Result<serde_json::Value, PluginError> {
462        let mut runtime_guard =
463            self.runtime.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
464        let runtime = runtime_guard.as_mut().ok_or_else(|| {
465            PluginError::execution("Runtime not initialized. Call initialize() first.".to_string())
466        })?;
467
468        let input_json = serde_json::to_string(&input_data)
469            .map_err(|e| PluginError::execution(format!("Failed to serialize input: {}", e)))?;
470
471        let input_bytes = input_json.as_bytes();
472        let input_len = input_bytes.len() as i32;
473
474        // Get memory (TinyGo always exports memory)
475        let memory =
476            runtime.instance.get_memory(&mut runtime.store, "memory").ok_or_else(|| {
477                PluginError::execution("TinyGo WASM module must export 'memory'".to_string())
478            })?;
479
480        // TinyGo uses malloc instead of alloc
481        let malloc_func = runtime
482            .instance
483            .get_typed_func::<i32, i32>(&mut runtime.store, "malloc")
484            .map_err(|e| {
485                PluginError::execution(format!(
486                    "Failed to get malloc function (TinyGo specific): {}",
487                    e
488                ))
489            })?;
490
491        // Allocate memory for input
492        let input_ptr = malloc_func
493            .call(&mut runtime.store, input_len)
494            .map_err(|e| PluginError::execution(format!("Failed to allocate memory: {}", e)))?;
495
496        // Write input to WASM memory
497        memory
498            .write(&mut runtime.store, input_ptr as usize, input_bytes)
499            .map_err(|e| PluginError::execution(format!("Failed to write input: {}", e)))?;
500
501        // Call the plugin function
502        let plugin_func = runtime
503            .instance
504            .get_typed_func::<(i32, i32), (i32, i32)>(&mut runtime.store, function_name)
505            .map_err(|e| {
506                PluginError::execution(format!("Function '{}' not found: {}", function_name, e))
507            })?;
508
509        let (output_ptr, output_len) =
510            plugin_func.call(&mut runtime.store, (input_ptr, input_len)).map_err(|e| {
511                PluginError::execution(format!(
512                    "Failed to call function '{}': {}",
513                    function_name, e
514                ))
515            })?;
516
517        // Read output from WASM memory
518        let mut output_bytes = vec![0u8; output_len as usize];
519        memory
520            .read(&runtime.store, output_ptr as usize, &mut output_bytes)
521            .map_err(|e| PluginError::execution(format!("Failed to read output: {}", e)))?;
522
523        // TinyGo uses free instead of dealloc
524        if let Ok(free_func) =
525            runtime.instance.get_typed_func::<i32, ()>(&mut runtime.store, "free")
526        {
527            let _ = free_func.call(&mut runtime.store, input_ptr);
528            let _ = free_func.call(&mut runtime.store, output_ptr);
529        }
530
531        // Parse output as JSON
532        let output_str = String::from_utf8(output_bytes)
533            .map_err(|e| PluginError::execution(format!("Failed to decode output: {}", e)))?;
534
535        serde_json::from_str(&output_str)
536            .map_err(|e| PluginError::execution(format!("Failed to parse output JSON: {}", e)))
537    }
538}
539
540#[async_trait]
541impl RuntimeAdapter for TinyGoAdapter {
542    fn runtime_type(&self) -> RuntimeType {
543        RuntimeType::TinyGo
544    }
545
546    async fn initialize(&mut self) -> Result<(), PluginError> {
547        // Initialize wasmtime instance with TinyGo-specific configuration
548        // TinyGo requires special memory management and import handling
549        tracing::info!("Initializing TinyGo plugin: {}", self.plugin_id);
550
551        // Create WASI context (TinyGo supports WASI)
552        let wasi_ctx = WasiCtxBuilder::new().inherit_stderr().inherit_stdout().build();
553
554        // Create store
555        let mut store = Store::new(&self.engine, wasi_ctx);
556
557        // Create linker with TinyGo-specific imports
558        let linker = Linker::new(&self.engine);
559
560        // TinyGo may require additional imports like syscall/js
561        // For now, we'll use the basic linker
562
563        // Instantiate the module
564        let instance = linker.instantiate(&mut store, &self.module).map_err(|e| {
565            PluginError::execution(format!("Failed to instantiate TinyGo module: {}", e))
566        })?;
567
568        // Store the runtime
569        let mut runtime_guard =
570            self.runtime.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
571        *runtime_guard = Some(WasmRuntime { store, instance });
572
573        tracing::info!("Successfully initialized TinyGo plugin: {}", self.plugin_id);
574        Ok(())
575    }
576
577    async fn call_auth(
578        &self,
579        context: &PluginContext,
580        request: &AuthRequest,
581    ) -> Result<AuthResponse, PluginError> {
582        // Call TinyGo WASM plugin's auth function
583        let input = serde_json::json!({
584            "context": context,
585            "method": request.method.to_string(),
586            "uri": request.uri.to_string(),
587            "query_params": request.query_params,
588            "client_ip": request.client_ip,
589            "user_agent": request.user_agent,
590        });
591
592        let result = self.call_wasm_json("authenticate", input)?;
593        serde_json::from_value(result)
594            .map_err(|e| PluginError::execution(format!("Failed to parse AuthResponse: {}", e)))
595    }
596
597    async fn call_template_function(
598        &self,
599        function_name: &str,
600        args: &[serde_json::Value],
601        context: &ResolutionContext,
602    ) -> Result<serde_json::Value, PluginError> {
603        let input = serde_json::json!({
604            "function_name": function_name,
605            "args": args,
606            "context": context,
607        });
608
609        self.call_wasm_json("template_function", input)
610    }
611
612    async fn call_response_generator(
613        &self,
614        context: &PluginContext,
615        request: &ResponseRequest,
616    ) -> Result<ResponseData, PluginError> {
617        // Create simplified request for WASM (avoid non-serializable types)
618        let input = serde_json::json!({
619            "context": context,
620            "method": request.method.to_string(),
621            "uri": request.uri,
622            "path": request.path,
623            "query_params": request.query_params,
624            "path_params": request.path_params,
625            "client_ip": request.client_ip,
626            "user_agent": request.user_agent,
627        });
628
629        let result = self.call_wasm_json("generate_response", input)?;
630        serde_json::from_value(result)
631            .map_err(|e| PluginError::execution(format!("Failed to parse ResponseData: {}", e)))
632    }
633
634    async fn call_datasource_query(
635        &self,
636        query: &DataQuery,
637        context: &PluginContext,
638    ) -> Result<DataResult, PluginError> {
639        // DataQuery should be serializable, but create simplified version to be safe
640        let input = serde_json::json!({
641            "query_type": format!("{:?}", query.query_type),
642            "query": query.query,
643            "parameters": query.parameters,
644            "limit": query.limit,
645            "offset": query.offset,
646            "context": context,
647        });
648
649        let result = self.call_wasm_json("query_datasource", input)?;
650        serde_json::from_value(result)
651            .map_err(|e| PluginError::execution(format!("Failed to parse DataResult: {}", e)))
652    }
653
654    async fn health_check(&self) -> Result<bool, PluginError> {
655        Ok(true)
656    }
657
658    async fn cleanup(&mut self) -> Result<(), PluginError> {
659        Ok(())
660    }
661}
662
663// ============================================================================
664// AssemblyScript Runtime Adapter
665// ============================================================================
666
667/// WASM runtime adapter for plugins compiled from AssemblyScript
668pub struct AssemblyScriptAdapter {
669    plugin_id: PluginId,
670    engine: Arc<Engine>,
671    module: Module,
672    runtime: Mutex<Option<WasmRuntime>>,
673}
674
675impl AssemblyScriptAdapter {
676    /// Create a new AssemblyScript runtime adapter
677    ///
678    /// # Arguments
679    /// * `plugin_id` - Unique identifier for the plugin
680    /// * `wasm_bytes` - WebAssembly binary bytes from AssemblyScript compilation
681    ///
682    /// # Returns
683    /// `Ok(AssemblyScriptAdapter)` on success, `Err(PluginError)` if WASM module cannot be loaded
684    pub fn new(plugin_id: PluginId, wasm_bytes: Vec<u8>) -> Result<Self, PluginError> {
685        let engine = Arc::new(Engine::default());
686        let module = Module::from_binary(&engine, &wasm_bytes).map_err(|e| {
687            PluginError::execution(format!("Failed to load AssemblyScript WASM module: {}", e))
688        })?;
689
690        Ok(Self {
691            plugin_id,
692            engine,
693            module,
694            runtime: Mutex::new(None),
695        })
696    }
697
698    /// Helper to call an AssemblyScript WASM function with JSON input/output
699    /// AssemblyScript uses __new, __pin, __unpin for memory management
700    fn call_wasm_json(
701        &self,
702        function_name: &str,
703        input_data: serde_json::Value,
704    ) -> Result<serde_json::Value, PluginError> {
705        let mut runtime_guard =
706            self.runtime.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
707        let runtime = runtime_guard.as_mut().ok_or_else(|| {
708            PluginError::execution("Runtime not initialized. Call initialize() first.".to_string())
709        })?;
710
711        let input_json = serde_json::to_string(&input_data)
712            .map_err(|e| PluginError::execution(format!("Failed to serialize input: {}", e)))?;
713
714        let input_bytes = input_json.as_bytes();
715        let input_len = input_bytes.len() as i32;
716
717        // Get memory
718        let memory =
719            runtime.instance.get_memory(&mut runtime.store, "memory").ok_or_else(|| {
720                PluginError::execution(
721                    "AssemblyScript WASM module must export 'memory'".to_string(),
722                )
723            })?;
724
725        // AssemblyScript uses __new for allocation
726        // Signature: __new(size: usize, id: u32) -> usize
727        // For strings, id is typically 1
728        let new_func = runtime
729            .instance
730            .get_typed_func::<(i32, i32), i32>(&mut runtime.store, "__new")
731            .map_err(|e| {
732                PluginError::execution(format!(
733                    "Failed to get __new function (AssemblyScript specific): {}",
734                    e
735                ))
736            })?;
737
738        // Allocate memory for input (id=1 for string type)
739        let input_ptr = new_func
740            .call(&mut runtime.store, (input_len, 1))
741            .map_err(|e| PluginError::execution(format!("Failed to allocate memory: {}", e)))?;
742
743        // Pin the allocated memory to prevent GC
744        if let Ok(pin_func) =
745            runtime.instance.get_typed_func::<i32, i32>(&mut runtime.store, "__pin")
746        {
747            let _ = pin_func.call(&mut runtime.store, input_ptr);
748        }
749
750        // Write input to WASM memory
751        memory
752            .write(&mut runtime.store, input_ptr as usize, input_bytes)
753            .map_err(|e| PluginError::execution(format!("Failed to write input: {}", e)))?;
754
755        // Call the plugin function
756        let plugin_func = runtime
757            .instance
758            .get_typed_func::<(i32, i32), (i32, i32)>(&mut runtime.store, function_name)
759            .map_err(|e| {
760                PluginError::execution(format!("Function '{}' not found: {}", function_name, e))
761            })?;
762
763        let (output_ptr, output_len) =
764            plugin_func.call(&mut runtime.store, (input_ptr, input_len)).map_err(|e| {
765                PluginError::execution(format!(
766                    "Failed to call function '{}': {}",
767                    function_name, e
768                ))
769            })?;
770
771        // Read output from WASM memory
772        let mut output_bytes = vec![0u8; output_len as usize];
773        memory
774            .read(&runtime.store, output_ptr as usize, &mut output_bytes)
775            .map_err(|e| PluginError::execution(format!("Failed to read output: {}", e)))?;
776
777        // Unpin the allocated memory
778        if let Ok(unpin_func) =
779            runtime.instance.get_typed_func::<i32, ()>(&mut runtime.store, "__unpin")
780        {
781            let _ = unpin_func.call(&mut runtime.store, input_ptr);
782            let _ = unpin_func.call(&mut runtime.store, output_ptr);
783        }
784
785        // Parse output as JSON
786        let output_str = String::from_utf8(output_bytes)
787            .map_err(|e| PluginError::execution(format!("Failed to decode output: {}", e)))?;
788
789        serde_json::from_str(&output_str)
790            .map_err(|e| PluginError::execution(format!("Failed to parse output JSON: {}", e)))
791    }
792}
793
794#[async_trait]
795impl RuntimeAdapter for AssemblyScriptAdapter {
796    fn runtime_type(&self) -> RuntimeType {
797        RuntimeType::AssemblyScript
798    }
799
800    async fn initialize(&mut self) -> Result<(), PluginError> {
801        tracing::info!("Initializing AssemblyScript plugin: {}", self.plugin_id);
802
803        // Create WASI context (AssemblyScript may use WASI features)
804        let wasi_ctx = WasiCtxBuilder::new().inherit_stderr().inherit_stdout().build();
805
806        // Create store
807        let mut store = Store::new(&self.engine, wasi_ctx);
808
809        // Create linker
810        let linker = Linker::new(&self.engine);
811
812        // AssemblyScript modules typically don't require special imports
813        // They use standard WASM with memory management functions
814
815        // Instantiate the module
816        let instance = linker.instantiate(&mut store, &self.module).map_err(|e| {
817            PluginError::execution(format!("Failed to instantiate AssemblyScript module: {}", e))
818        })?;
819
820        // Store the runtime
821        let mut runtime_guard =
822            self.runtime.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
823        *runtime_guard = Some(WasmRuntime { store, instance });
824
825        tracing::info!("Successfully initialized AssemblyScript plugin: {}", self.plugin_id);
826        Ok(())
827    }
828
829    async fn call_auth(
830        &self,
831        context: &PluginContext,
832        request: &AuthRequest,
833    ) -> Result<AuthResponse, PluginError> {
834        let input = serde_json::json!({
835            "context": context,
836            "method": request.method.to_string(),
837            "uri": request.uri.to_string(),
838            "query_params": request.query_params,
839            "client_ip": request.client_ip,
840            "user_agent": request.user_agent,
841        });
842
843        let result = self.call_wasm_json("authenticate", input)?;
844        serde_json::from_value(result)
845            .map_err(|e| PluginError::execution(format!("Failed to parse AuthResponse: {}", e)))
846    }
847
848    async fn call_template_function(
849        &self,
850        function_name: &str,
851        args: &[serde_json::Value],
852        context: &ResolutionContext,
853    ) -> Result<serde_json::Value, PluginError> {
854        let input = serde_json::json!({
855            "function_name": function_name,
856            "args": args,
857            "context": context,
858        });
859
860        self.call_wasm_json("template_function", input)
861    }
862
863    async fn call_response_generator(
864        &self,
865        context: &PluginContext,
866        request: &ResponseRequest,
867    ) -> Result<ResponseData, PluginError> {
868        // Create simplified request for WASM (avoid non-serializable types)
869        let input = serde_json::json!({
870            "context": context,
871            "method": request.method.to_string(),
872            "uri": request.uri,
873            "path": request.path,
874            "query_params": request.query_params,
875            "path_params": request.path_params,
876            "client_ip": request.client_ip,
877            "user_agent": request.user_agent,
878        });
879
880        let result = self.call_wasm_json("generate_response", input)?;
881        serde_json::from_value(result)
882            .map_err(|e| PluginError::execution(format!("Failed to parse ResponseData: {}", e)))
883    }
884
885    async fn call_datasource_query(
886        &self,
887        query: &DataQuery,
888        context: &PluginContext,
889    ) -> Result<DataResult, PluginError> {
890        // DataQuery should be serializable, but create simplified version to be safe
891        let input = serde_json::json!({
892            "query_type": format!("{:?}", query.query_type),
893            "query": query.query,
894            "parameters": query.parameters,
895            "limit": query.limit,
896            "offset": query.offset,
897            "context": context,
898        });
899
900        let result = self.call_wasm_json("query_datasource", input)?;
901        serde_json::from_value(result)
902            .map_err(|e| PluginError::execution(format!("Failed to parse DataResult: {}", e)))
903    }
904
905    async fn health_check(&self) -> Result<bool, PluginError> {
906        Ok(true)
907    }
908
909    async fn cleanup(&mut self) -> Result<(), PluginError> {
910        Ok(())
911    }
912}
913
914// ============================================================================
915// Remote Runtime Adapter (HTTP/gRPC)
916// ============================================================================
917
918/// Runtime adapter for plugins running on remote servers (HTTP or gRPC)
919pub struct RemoteAdapter {
920    plugin_id: PluginId,
921    config: RemoteRuntimeConfig,
922    client: reqwest::Client,
923}
924
925impl RemoteAdapter {
926    /// Create a new remote runtime adapter
927    ///
928    /// # Arguments
929    /// * `plugin_id` - Unique identifier for the plugin
930    /// * `config` - Remote runtime configuration (URL, protocol, auth, etc.)
931    ///
932    /// # Returns
933    /// `Ok(RemoteAdapter)` on success, `Err(PluginError)` if HTTP client cannot be created
934    pub fn new(plugin_id: PluginId, config: RemoteRuntimeConfig) -> Result<Self, PluginError> {
935        let client = reqwest::Client::builder()
936            .timeout(std::time::Duration::from_millis(config.timeout_ms))
937            .build()
938            .map_err(|e| PluginError::execution(format!("Failed to create HTTP client: {}", e)))?;
939
940        Ok(Self {
941            plugin_id,
942            config,
943            client,
944        })
945    }
946
947    async fn call_remote_plugin(
948        &self,
949        endpoint: &str,
950        body: serde_json::Value,
951    ) -> Result<serde_json::Value, PluginError> {
952        let url = format!("{}{}", self.config.endpoint, endpoint);
953
954        let mut request = self.client.post(&url).json(&body);
955
956        // Add authentication if configured
957        if let Some(auth) = &self.config.auth {
958            request = match auth.auth_type.as_str() {
959                "bearer" => request.bearer_auth(&auth.value),
960                "api_key" => request.header("X-API-Key", &auth.value),
961                _ => request,
962            };
963        }
964
965        let response = request
966            .send()
967            .await
968            .map_err(|e| PluginError::execution(format!("Remote plugin call failed: {}", e)))?;
969
970        if !response.status().is_success() {
971            return Err(PluginError::execution(format!(
972                "Remote plugin returned error status: {}",
973                response.status()
974            )));
975        }
976
977        let result: serde_json::Value = response
978            .json()
979            .await
980            .map_err(|e| PluginError::execution(format!("Failed to parse response: {}", e)))?;
981
982        Ok(result)
983    }
984}
985
986#[async_trait]
987impl RuntimeAdapter for RemoteAdapter {
988    fn runtime_type(&self) -> RuntimeType {
989        RuntimeType::Remote(self.config.clone())
990    }
991
992    async fn initialize(&mut self) -> Result<(), PluginError> {
993        tracing::info!("Initializing remote plugin: {}", self.plugin_id);
994
995        // Perform health check during initialization
996        self.health_check().await?;
997
998        Ok(())
999    }
1000
1001    async fn call_auth(
1002        &self,
1003        context: &PluginContext,
1004        request: &AuthRequest,
1005    ) -> Result<AuthResponse, PluginError> {
1006        let body = serde_json::json!({
1007            "context": context,
1008            "method": request.method.to_string(),
1009            "uri": request.uri.to_string(),
1010            "query_params": request.query_params,
1011            "client_ip": request.client_ip,
1012            "user_agent": request.user_agent,
1013        });
1014
1015        let result = self.call_remote_plugin("/plugin/authenticate", body).await?;
1016
1017        // Parse the AuthResponse from the response
1018        serde_json::from_value(result)
1019            .map_err(|e| PluginError::execution(format!("Failed to parse AuthResponse: {}", e)))
1020    }
1021
1022    async fn call_template_function(
1023        &self,
1024        function_name: &str,
1025        args: &[serde_json::Value],
1026        context: &ResolutionContext,
1027    ) -> Result<serde_json::Value, PluginError> {
1028        let body = serde_json::json!({
1029            "function_name": function_name,
1030            "args": args,
1031            "context": context,
1032        });
1033
1034        self.call_remote_plugin("/plugin/template/execute", body).await
1035    }
1036
1037    async fn call_response_generator(
1038        &self,
1039        context: &PluginContext,
1040        request: &ResponseRequest,
1041    ) -> Result<ResponseData, PluginError> {
1042        // Create simplified request
1043        let body = serde_json::json!({
1044            "context": context,
1045            "method": request.method.to_string(),
1046            "uri": request.uri,
1047            "path": request.path,
1048            "query_params": request.query_params,
1049            "path_params": request.path_params,
1050            "client_ip": request.client_ip,
1051            "user_agent": request.user_agent,
1052        });
1053
1054        let result = self.call_remote_plugin("/plugin/response/generate", body).await?;
1055
1056        serde_json::from_value(result)
1057            .map_err(|e| PluginError::execution(format!("Failed to parse ResponseData: {}", e)))
1058    }
1059
1060    async fn call_datasource_query(
1061        &self,
1062        query: &DataQuery,
1063        context: &PluginContext,
1064    ) -> Result<DataResult, PluginError> {
1065        let body = serde_json::json!({
1066            "query_type": format!("{:?}", query.query_type),
1067            "query": query.query,
1068            "parameters": query.parameters,
1069            "limit": query.limit,
1070            "offset": query.offset,
1071            "context": context,
1072        });
1073
1074        let result = self.call_remote_plugin("/plugin/datasource/query", body).await?;
1075
1076        serde_json::from_value(result)
1077            .map_err(|e| PluginError::execution(format!("Failed to parse DataResult: {}", e)))
1078    }
1079
1080    async fn health_check(&self) -> Result<bool, PluginError> {
1081        // Try to call health endpoint
1082        let url = format!("{}/health", self.config.endpoint);
1083
1084        match self.client.get(&url).send().await {
1085            Ok(response) => Ok(response.status().is_success()),
1086            Err(_) => Ok(false),
1087        }
1088    }
1089
1090    async fn cleanup(&mut self) -> Result<(), PluginError> {
1091        tracing::info!("Cleaning up remote plugin: {}", self.plugin_id);
1092        Ok(())
1093    }
1094
1095    fn get_metrics(&self) -> HashMap<String, serde_json::Value> {
1096        let mut metrics = HashMap::new();
1097        metrics.insert("plugin_id".to_string(), serde_json::json!(self.plugin_id.as_str()));
1098        metrics.insert("endpoint".to_string(), serde_json::json!(self.config.endpoint));
1099        metrics.insert(
1100            "protocol".to_string(),
1101            serde_json::json!(format!("{:?}", self.config.protocol)),
1102        );
1103        metrics
1104    }
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109    use super::*;
1110
1111    // ===== RuntimeType Tests =====
1112
1113    #[test]
1114    fn test_runtime_type_equality() {
1115        assert_eq!(RuntimeType::Rust, RuntimeType::Rust);
1116        assert_eq!(RuntimeType::TinyGo, RuntimeType::TinyGo);
1117        assert_eq!(RuntimeType::AssemblyScript, RuntimeType::AssemblyScript);
1118
1119        assert_ne!(RuntimeType::Rust, RuntimeType::TinyGo);
1120    }
1121
1122    #[test]
1123    fn test_runtime_type_clone() {
1124        let rt = RuntimeType::Rust;
1125        let cloned = rt.clone();
1126        assert_eq!(rt, cloned);
1127    }
1128
1129    #[test]
1130    fn test_runtime_type_detection() {
1131        // Test with empty bytes (should default to Rust)
1132        let empty_bytes = vec![];
1133        let runtime = detect_runtime_type(&empty_bytes).unwrap();
1134        assert_eq!(runtime, RuntimeType::Rust);
1135    }
1136
1137    #[test]
1138    fn test_runtime_type_detection_tinygo() {
1139        // Test with bytes containing "tinygo"
1140        let tinygo_bytes = b"wasm module with tinygo runtime".to_vec();
1141        let runtime = detect_runtime_type(&tinygo_bytes).unwrap();
1142        assert_eq!(runtime, RuntimeType::TinyGo);
1143    }
1144
1145    #[test]
1146    fn test_runtime_type_detection_assemblyscript() {
1147        // Test with bytes containing "assemblyscript"
1148        let as_bytes = b"wasm module with assemblyscript runtime".to_vec();
1149        let runtime = detect_runtime_type(&as_bytes).unwrap();
1150        assert_eq!(runtime, RuntimeType::AssemblyScript);
1151    }
1152
1153    #[test]
1154    fn test_has_tinygo_signature() {
1155        assert!(has_tinygo_signature(b"this contains tinygo"));
1156        assert!(!has_tinygo_signature(b"this does not contain it"));
1157    }
1158
1159    #[test]
1160    fn test_has_assemblyscript_signature() {
1161        assert!(has_assemblyscript_signature(b"this contains assemblyscript"));
1162        assert!(!has_assemblyscript_signature(b"this does not contain it"));
1163    }
1164
1165    // ===== RemoteRuntimeConfig Tests =====
1166
1167    #[test]
1168    fn test_remote_runtime_config() {
1169        let config = RemoteRuntimeConfig {
1170            protocol: RemoteProtocol::Http,
1171            endpoint: "http://localhost:8080".to_string(),
1172            timeout_ms: 5000,
1173            max_retries: 3,
1174            auth: Some(RemoteAuthConfig {
1175                auth_type: "bearer".to_string(),
1176                value: "secret-token".to_string(),
1177            }),
1178        };
1179
1180        assert_eq!(config.endpoint, "http://localhost:8080");
1181        assert_eq!(config.timeout_ms, 5000);
1182        assert_eq!(config.max_retries, 3);
1183        assert!(config.auth.is_some());
1184    }
1185
1186    #[test]
1187    fn test_remote_runtime_config_without_auth() {
1188        let config = RemoteRuntimeConfig {
1189            protocol: RemoteProtocol::Grpc,
1190            endpoint: "grpc://localhost:9090".to_string(),
1191            timeout_ms: 10000,
1192            max_retries: 5,
1193            auth: None,
1194        };
1195
1196        assert_eq!(config.protocol, RemoteProtocol::Grpc);
1197        assert!(config.auth.is_none());
1198    }
1199
1200    #[test]
1201    fn test_remote_runtime_config_clone() {
1202        let config = RemoteRuntimeConfig {
1203            protocol: RemoteProtocol::Http,
1204            endpoint: "http://localhost:8080".to_string(),
1205            timeout_ms: 5000,
1206            max_retries: 3,
1207            auth: None,
1208        };
1209
1210        let cloned = config.clone();
1211        assert_eq!(config.endpoint, cloned.endpoint);
1212        assert_eq!(config.timeout_ms, cloned.timeout_ms);
1213    }
1214
1215    #[test]
1216    fn test_remote_runtime_config_equality() {
1217        let config1 = RemoteRuntimeConfig {
1218            protocol: RemoteProtocol::Http,
1219            endpoint: "http://localhost:8080".to_string(),
1220            timeout_ms: 5000,
1221            max_retries: 3,
1222            auth: None,
1223        };
1224
1225        let config2 = RemoteRuntimeConfig {
1226            protocol: RemoteProtocol::Http,
1227            endpoint: "http://localhost:8080".to_string(),
1228            timeout_ms: 5000,
1229            max_retries: 3,
1230            auth: None,
1231        };
1232
1233        assert_eq!(config1, config2);
1234    }
1235
1236    // ===== RemoteProtocol Tests =====
1237
1238    #[test]
1239    fn test_remote_protocol_equality() {
1240        assert_eq!(RemoteProtocol::Http, RemoteProtocol::Http);
1241        assert_eq!(RemoteProtocol::Grpc, RemoteProtocol::Grpc);
1242        assert_ne!(RemoteProtocol::Http, RemoteProtocol::Grpc);
1243    }
1244
1245    #[test]
1246    fn test_remote_protocol_clone() {
1247        let proto = RemoteProtocol::Http;
1248        let cloned = proto.clone();
1249        assert_eq!(proto, cloned);
1250    }
1251
1252    // ===== RemoteAuthConfig Tests =====
1253
1254    #[test]
1255    fn test_remote_auth_config() {
1256        let auth = RemoteAuthConfig {
1257            auth_type: "bearer".to_string(),
1258            value: "secret-token".to_string(),
1259        };
1260
1261        assert_eq!(auth.auth_type, "bearer");
1262        assert_eq!(auth.value, "secret-token");
1263    }
1264
1265    #[test]
1266    fn test_remote_auth_config_api_key() {
1267        let auth = RemoteAuthConfig {
1268            auth_type: "api_key".to_string(),
1269            value: "my-api-key".to_string(),
1270        };
1271
1272        assert_eq!(auth.auth_type, "api_key");
1273    }
1274
1275    #[test]
1276    fn test_remote_auth_config_clone() {
1277        let auth = RemoteAuthConfig {
1278            auth_type: "bearer".to_string(),
1279            value: "token".to_string(),
1280        };
1281
1282        let cloned = auth.clone();
1283        assert_eq!(auth.auth_type, cloned.auth_type);
1284        assert_eq!(auth.value, cloned.value);
1285    }
1286
1287    #[test]
1288    fn test_remote_auth_config_equality() {
1289        let auth1 = RemoteAuthConfig {
1290            auth_type: "bearer".to_string(),
1291            value: "token".to_string(),
1292        };
1293
1294        let auth2 = RemoteAuthConfig {
1295            auth_type: "bearer".to_string(),
1296            value: "token".to_string(),
1297        };
1298
1299        assert_eq!(auth1, auth2);
1300    }
1301
1302    // ===== RuntimeAdapterFactory Tests =====
1303
1304    #[test]
1305    fn test_factory_create_rust_adapter() {
1306        let plugin_id = PluginId::new("test-plugin");
1307        // Create minimal valid WASM module (magic bytes + version)
1308        let wasm_bytes = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
1309
1310        let result = RuntimeAdapterFactory::create(RuntimeType::Rust, plugin_id, wasm_bytes);
1311
1312        assert!(result.is_ok());
1313        let adapter = result.unwrap();
1314        assert_eq!(adapter.runtime_type(), RuntimeType::Rust);
1315    }
1316
1317    #[test]
1318    fn test_factory_create_tinygo_adapter() {
1319        let plugin_id = PluginId::new("test-plugin");
1320        let wasm_bytes = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
1321
1322        let result = RuntimeAdapterFactory::create(RuntimeType::TinyGo, plugin_id, wasm_bytes);
1323
1324        assert!(result.is_ok());
1325        let adapter = result.unwrap();
1326        assert_eq!(adapter.runtime_type(), RuntimeType::TinyGo);
1327    }
1328
1329    #[test]
1330    fn test_factory_create_assemblyscript_adapter() {
1331        let plugin_id = PluginId::new("test-plugin");
1332        let wasm_bytes = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
1333
1334        let result =
1335            RuntimeAdapterFactory::create(RuntimeType::AssemblyScript, plugin_id, wasm_bytes);
1336
1337        assert!(result.is_ok());
1338        let adapter = result.unwrap();
1339        assert_eq!(adapter.runtime_type(), RuntimeType::AssemblyScript);
1340    }
1341
1342    #[test]
1343    fn test_factory_create_remote_adapter() {
1344        let plugin_id = PluginId::new("test-plugin");
1345        let config = RemoteRuntimeConfig {
1346            protocol: RemoteProtocol::Http,
1347            endpoint: "http://localhost:8080".to_string(),
1348            timeout_ms: 5000,
1349            max_retries: 3,
1350            auth: None,
1351        };
1352
1353        let result =
1354            RuntimeAdapterFactory::create(RuntimeType::Remote(config.clone()), plugin_id, vec![]);
1355
1356        assert!(result.is_ok());
1357        let adapter = result.unwrap();
1358        assert_eq!(adapter.runtime_type(), RuntimeType::Remote(config));
1359    }
1360
1361    #[test]
1362    fn test_factory_create_with_invalid_wasm() {
1363        let plugin_id = PluginId::new("test-plugin");
1364        // Invalid WASM bytes
1365        let wasm_bytes = vec![0x00, 0x00, 0x00, 0x00];
1366
1367        let result = RuntimeAdapterFactory::create(RuntimeType::Rust, plugin_id, wasm_bytes);
1368
1369        assert!(result.is_err());
1370    }
1371
1372    // ===== RemoteAdapter Tests =====
1373
1374    #[test]
1375    fn test_remote_adapter_creation() {
1376        let plugin_id = PluginId::new("test-plugin");
1377        let config = RemoteRuntimeConfig {
1378            protocol: RemoteProtocol::Http,
1379            endpoint: "http://localhost:8080".to_string(),
1380            timeout_ms: 5000,
1381            max_retries: 3,
1382            auth: None,
1383        };
1384
1385        let result = RemoteAdapter::new(plugin_id, config.clone());
1386        assert!(result.is_ok());
1387
1388        let adapter = result.unwrap();
1389        assert_eq!(adapter.runtime_type(), RuntimeType::Remote(config));
1390    }
1391
1392    #[test]
1393    fn test_remote_adapter_get_metrics() {
1394        let plugin_id = PluginId::new("test-plugin");
1395        let config = RemoteRuntimeConfig {
1396            protocol: RemoteProtocol::Http,
1397            endpoint: "http://localhost:8080".to_string(),
1398            timeout_ms: 5000,
1399            max_retries: 3,
1400            auth: None,
1401        };
1402
1403        let adapter = RemoteAdapter::new(plugin_id.clone(), config).unwrap();
1404        let metrics = adapter.get_metrics();
1405
1406        assert!(metrics.contains_key("plugin_id"));
1407        assert!(metrics.contains_key("endpoint"));
1408        assert!(metrics.contains_key("protocol"));
1409        assert_eq!(metrics.get("plugin_id").unwrap(), &serde_json::json!(plugin_id.as_str()));
1410    }
1411
1412    // ===== Edge Cases and Error Handling =====
1413
1414    #[test]
1415    fn test_runtime_type_with_mixed_signatures() {
1416        // Test with bytes containing both signatures (tinygo should win as it's checked first)
1417        let mixed_bytes = b"tinygo and assemblyscript".to_vec();
1418        let runtime = detect_runtime_type(&mixed_bytes).unwrap();
1419        assert_eq!(runtime, RuntimeType::TinyGo);
1420    }
1421
1422    #[test]
1423    fn test_empty_wasm_bytes() {
1424        let plugin_id = PluginId::new("test");
1425        let result = RustAdapter::new(plugin_id, vec![]);
1426        assert!(result.is_err());
1427    }
1428
1429    #[test]
1430    fn test_runtime_type_debug() {
1431        let rt = RuntimeType::Rust;
1432        let debug_str = format!("{:?}", rt);
1433        assert!(debug_str.contains("Rust"));
1434    }
1435
1436    #[test]
1437    fn test_remote_protocol_debug() {
1438        let proto = RemoteProtocol::Http;
1439        let debug_str = format!("{:?}", proto);
1440        assert!(debug_str.contains("Http"));
1441    }
1442}