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