openzeppelin_relayer/repositories/plugin/
plugin_in_memory.rs

1//! This module provides an in-memory implementation of plugins.
2//!
3//! The `InMemoryPluginRepository` struct is used to store and retrieve plugins
4//! script paths for further execution. Also provides compiled code caching.
5use crate::{
6    models::{PaginationQuery, PluginModel},
7    repositories::{PaginatedResult, PluginRepositoryTrait, RepositoryError},
8};
9
10use async_trait::async_trait;
11
12use std::collections::HashMap;
13use tokio::sync::{Mutex, MutexGuard};
14
15/// Compiled plugin code entry
16#[derive(Debug, Clone)]
17struct CompiledCodeEntry {
18    code: String,
19    source_hash: Option<String>,
20}
21
22#[derive(Debug)]
23pub struct InMemoryPluginRepository {
24    store: Mutex<HashMap<String, PluginModel>>,
25    compiled_cache: Mutex<HashMap<String, CompiledCodeEntry>>,
26}
27
28impl Clone for InMemoryPluginRepository {
29    fn clone(&self) -> Self {
30        // Try to get the current data, or use empty HashMap if lock fails
31        let data = self
32            .store
33            .try_lock()
34            .map(|guard| guard.clone())
35            .unwrap_or_else(|_| HashMap::new());
36
37        let compiled = self
38            .compiled_cache
39            .try_lock()
40            .map(|guard| guard.clone())
41            .unwrap_or_else(|_| HashMap::new());
42
43        Self {
44            store: Mutex::new(data),
45            compiled_cache: Mutex::new(compiled),
46        }
47    }
48}
49
50impl InMemoryPluginRepository {
51    pub fn new() -> Self {
52        Self {
53            store: Mutex::new(HashMap::new()),
54            compiled_cache: Mutex::new(HashMap::new()),
55        }
56    }
57
58    pub async fn get_by_id(&self, id: &str) -> Result<Option<PluginModel>, RepositoryError> {
59        let store = Self::acquire_lock(&self.store).await?;
60        Ok(store.get(id).cloned())
61    }
62
63    async fn acquire_lock<T>(lock: &Mutex<T>) -> Result<MutexGuard<T>, RepositoryError> {
64        Ok(lock.lock().await)
65    }
66}
67
68impl Default for InMemoryPluginRepository {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74#[async_trait]
75impl PluginRepositoryTrait for InMemoryPluginRepository {
76    async fn get_by_id(&self, id: &str) -> Result<Option<PluginModel>, RepositoryError> {
77        let store = Self::acquire_lock(&self.store).await?;
78        Ok(store.get(id).cloned())
79    }
80
81    async fn add(&self, plugin: PluginModel) -> Result<(), RepositoryError> {
82        let mut store = Self::acquire_lock(&self.store).await?;
83        store.insert(plugin.id.clone(), plugin);
84        Ok(())
85    }
86
87    async fn update(&self, plugin: PluginModel) -> Result<PluginModel, RepositoryError> {
88        let mut store = Self::acquire_lock(&self.store).await?;
89        if !store.contains_key(&plugin.id) {
90            return Err(RepositoryError::NotFound(format!(
91                "Plugin with id {} not found",
92                plugin.id
93            )));
94        }
95        store.insert(plugin.id.clone(), plugin.clone());
96        Ok(plugin)
97    }
98
99    async fn list_paginated(
100        &self,
101        query: PaginationQuery,
102    ) -> Result<PaginatedResult<PluginModel>, RepositoryError> {
103        let total = self.count().await?;
104        let start = ((query.page - 1) * query.per_page) as usize;
105
106        let items = self
107            .store
108            .lock()
109            .await
110            .values()
111            .skip(start)
112            .take(query.per_page as usize)
113            .cloned()
114            .collect();
115
116        Ok(PaginatedResult {
117            items,
118            total: total as u64,
119            page: query.page,
120            per_page: query.per_page,
121        })
122    }
123
124    async fn count(&self) -> Result<usize, RepositoryError> {
125        let store = self.store.lock().await;
126        Ok(store.len())
127    }
128
129    async fn has_entries(&self) -> Result<bool, RepositoryError> {
130        let store = Self::acquire_lock(&self.store).await?;
131        Ok(!store.is_empty())
132    }
133
134    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
135        let mut store = Self::acquire_lock(&self.store).await?;
136        store.clear();
137        Ok(())
138    }
139
140    // Compiled code cache methods
141
142    async fn get_compiled_code(&self, plugin_id: &str) -> Result<Option<String>, RepositoryError> {
143        let cache = Self::acquire_lock(&self.compiled_cache).await?;
144        Ok(cache.get(plugin_id).map(|e| e.code.clone()))
145    }
146
147    async fn store_compiled_code(
148        &self,
149        plugin_id: &str,
150        compiled_code: &str,
151        source_hash: Option<&str>,
152    ) -> Result<(), RepositoryError> {
153        let mut cache = Self::acquire_lock(&self.compiled_cache).await?;
154        cache.insert(
155            plugin_id.to_string(),
156            CompiledCodeEntry {
157                code: compiled_code.to_string(),
158                source_hash: source_hash.map(|s| s.to_string()),
159            },
160        );
161        Ok(())
162    }
163
164    async fn invalidate_compiled_code(&self, plugin_id: &str) -> Result<(), RepositoryError> {
165        let mut cache = Self::acquire_lock(&self.compiled_cache).await?;
166        cache.remove(plugin_id);
167        Ok(())
168    }
169
170    async fn invalidate_all_compiled_code(&self) -> Result<(), RepositoryError> {
171        let mut cache = Self::acquire_lock(&self.compiled_cache).await?;
172        cache.clear();
173        Ok(())
174    }
175
176    async fn has_compiled_code(&self, plugin_id: &str) -> Result<bool, RepositoryError> {
177        let cache = Self::acquire_lock(&self.compiled_cache).await?;
178        Ok(cache.contains_key(plugin_id))
179    }
180
181    async fn get_source_hash(&self, plugin_id: &str) -> Result<Option<String>, RepositoryError> {
182        let cache = Self::acquire_lock(&self.compiled_cache).await?;
183        Ok(cache.get(plugin_id).and_then(|e| e.source_hash.clone()))
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use crate::{config::PluginFileConfig, constants::DEFAULT_PLUGIN_TIMEOUT_SECONDS};
190
191    use super::*;
192    use std::{sync::Arc, time::Duration};
193
194    // ============================================
195    // Helper functions
196    // ============================================
197
198    fn create_test_plugin(id: &str) -> PluginModel {
199        PluginModel {
200            id: id.to_string(),
201            path: format!("path/{id}"),
202            timeout: Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS),
203            emit_logs: false,
204            emit_traces: false,
205            raw_response: false,
206            allow_get_invocation: false,
207            config: None,
208            forward_logs: false,
209        }
210    }
211
212    fn create_test_plugin_with_options(
213        id: &str,
214        emit_logs: bool,
215        emit_traces: bool,
216        raw_response: bool,
217    ) -> PluginModel {
218        PluginModel {
219            id: id.to_string(),
220            path: format!("path/{id}"),
221            timeout: Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS),
222            emit_logs,
223            emit_traces,
224            raw_response,
225            allow_get_invocation: false,
226            config: None,
227            forward_logs: false,
228        }
229    }
230
231    // ============================================
232    // Basic repository tests
233    // ============================================
234
235    #[tokio::test]
236    async fn test_new_creates_empty_repository() {
237        let repo = InMemoryPluginRepository::new();
238
239        assert_eq!(repo.count().await.unwrap(), 0);
240        assert!(!repo.has_entries().await.unwrap());
241    }
242
243    #[tokio::test]
244    async fn test_default_creates_empty_repository() {
245        let repo = InMemoryPluginRepository::default();
246
247        assert_eq!(repo.count().await.unwrap(), 0);
248        assert!(!repo.has_entries().await.unwrap());
249    }
250
251    #[tokio::test]
252    async fn test_add_and_get_by_id() {
253        let repo = Arc::new(InMemoryPluginRepository::new());
254
255        let plugin = create_test_plugin("test-plugin");
256        repo.add(plugin.clone()).await.unwrap();
257
258        let retrieved = repo.get_by_id("test-plugin").await.unwrap();
259        assert_eq!(retrieved, Some(plugin));
260    }
261
262    #[tokio::test]
263    async fn test_get_nonexistent_plugin() {
264        let repo = Arc::new(InMemoryPluginRepository::new());
265
266        let result = repo.get_by_id("nonexistent").await;
267        assert!(matches!(result, Ok(None)));
268    }
269
270    #[tokio::test]
271    async fn test_add_multiple_plugins() {
272        let repo = Arc::new(InMemoryPluginRepository::new());
273
274        for i in 1..=5 {
275            let plugin = create_test_plugin(&format!("plugin-{i}"));
276            repo.add(plugin).await.unwrap();
277        }
278
279        assert_eq!(repo.count().await.unwrap(), 5);
280
281        for i in 1..=5 {
282            let result = repo.get_by_id(&format!("plugin-{i}")).await.unwrap();
283            assert!(result.is_some());
284        }
285    }
286
287    #[tokio::test]
288    async fn test_add_overwrites_existing() {
289        let repo = Arc::new(InMemoryPluginRepository::new());
290
291        let plugin1 = create_test_plugin_with_options("test-plugin", false, false, false);
292        repo.add(plugin1).await.unwrap();
293
294        let plugin2 = create_test_plugin_with_options("test-plugin", true, true, true);
295        repo.add(plugin2.clone()).await.unwrap();
296
297        // Should have overwritten
298        let retrieved = repo.get_by_id("test-plugin").await.unwrap().unwrap();
299        assert!(retrieved.emit_logs);
300        assert!(retrieved.emit_traces);
301        assert!(retrieved.raw_response);
302
303        // Count should still be 1
304        assert_eq!(repo.count().await.unwrap(), 1);
305    }
306
307    // ============================================
308    // Update tests
309    // ============================================
310
311    #[tokio::test]
312    async fn test_update_existing_plugin() {
313        let repo = Arc::new(InMemoryPluginRepository::new());
314
315        let plugin = create_test_plugin("test-plugin");
316        repo.add(plugin).await.unwrap();
317
318        let updated_plugin = create_test_plugin_with_options("test-plugin", true, true, true);
319        let result = repo.update(updated_plugin.clone()).await;
320
321        assert!(result.is_ok());
322        let returned = result.unwrap();
323        assert_eq!(returned.id, "test-plugin");
324        assert!(returned.emit_logs);
325        assert!(returned.emit_traces);
326
327        // Verify persisted
328        let retrieved = repo.get_by_id("test-plugin").await.unwrap().unwrap();
329        assert!(retrieved.emit_logs);
330    }
331
332    #[tokio::test]
333    async fn test_update_nonexistent_plugin_returns_error() {
334        let repo = Arc::new(InMemoryPluginRepository::new());
335
336        let plugin = create_test_plugin("nonexistent");
337        let result = repo.update(plugin).await;
338
339        assert!(result.is_err());
340        match result {
341            Err(RepositoryError::NotFound(msg)) => {
342                assert!(msg.contains("nonexistent"));
343            }
344            _ => panic!("Expected NotFound error"),
345        }
346    }
347
348    #[tokio::test]
349    async fn test_update_preserves_other_plugins() {
350        let repo = Arc::new(InMemoryPluginRepository::new());
351
352        repo.add(create_test_plugin("plugin-1")).await.unwrap();
353        repo.add(create_test_plugin("plugin-2")).await.unwrap();
354        repo.add(create_test_plugin("plugin-3")).await.unwrap();
355
356        let updated = create_test_plugin_with_options("plugin-2", true, false, false);
357        repo.update(updated).await.unwrap();
358
359        // Check other plugins unchanged
360        let p1 = repo.get_by_id("plugin-1").await.unwrap().unwrap();
361        assert!(!p1.emit_logs);
362
363        let p3 = repo.get_by_id("plugin-3").await.unwrap().unwrap();
364        assert!(!p3.emit_logs);
365
366        // Check updated plugin changed
367        let p2 = repo.get_by_id("plugin-2").await.unwrap().unwrap();
368        assert!(p2.emit_logs);
369    }
370
371    // ============================================
372    // Count tests
373    // ============================================
374
375    #[tokio::test]
376    async fn test_count_empty_repository() {
377        let repo = InMemoryPluginRepository::new();
378        assert_eq!(repo.count().await.unwrap(), 0);
379    }
380
381    #[tokio::test]
382    async fn test_count_with_entries() {
383        let repo = Arc::new(InMemoryPluginRepository::new());
384
385        for i in 1..=10 {
386            repo.add(create_test_plugin(&format!("plugin-{i}")))
387                .await
388                .unwrap();
389        }
390
391        assert_eq!(repo.count().await.unwrap(), 10);
392    }
393
394    #[tokio::test]
395    async fn test_count_after_drop_all() {
396        let repo = Arc::new(InMemoryPluginRepository::new());
397
398        for i in 1..=5 {
399            repo.add(create_test_plugin(&format!("plugin-{i}")))
400                .await
401                .unwrap();
402        }
403
404        assert_eq!(repo.count().await.unwrap(), 5);
405
406        repo.drop_all_entries().await.unwrap();
407
408        assert_eq!(repo.count().await.unwrap(), 0);
409    }
410
411    // ============================================
412    // Pagination tests
413    // ============================================
414
415    #[tokio::test]
416    async fn test_list_paginated_first_page() {
417        let repo = Arc::new(InMemoryPluginRepository::new());
418
419        for i in 1..=10 {
420            repo.add(create_test_plugin(&format!("plugin-{i:02}")))
421                .await
422                .unwrap();
423        }
424
425        let query = PaginationQuery {
426            page: 1,
427            per_page: 3,
428        };
429
430        let result = repo.list_paginated(query).await.unwrap();
431
432        assert_eq!(result.items.len(), 3);
433        assert_eq!(result.total, 10);
434        assert_eq!(result.page, 1);
435        assert_eq!(result.per_page, 3);
436    }
437
438    #[tokio::test]
439    async fn test_list_paginated_middle_page() {
440        let repo = Arc::new(InMemoryPluginRepository::new());
441
442        for i in 1..=10 {
443            repo.add(create_test_plugin(&format!("plugin-{i:02}")))
444                .await
445                .unwrap();
446        }
447
448        let query = PaginationQuery {
449            page: 2,
450            per_page: 3,
451        };
452
453        let result = repo.list_paginated(query).await.unwrap();
454
455        assert_eq!(result.items.len(), 3);
456        assert_eq!(result.total, 10);
457        assert_eq!(result.page, 2);
458    }
459
460    #[tokio::test]
461    async fn test_list_paginated_last_partial_page() {
462        let repo = Arc::new(InMemoryPluginRepository::new());
463
464        for i in 1..=10 {
465            repo.add(create_test_plugin(&format!("plugin-{i:02}")))
466                .await
467                .unwrap();
468        }
469
470        let query = PaginationQuery {
471            page: 4,
472            per_page: 3,
473        };
474
475        let result = repo.list_paginated(query).await.unwrap();
476
477        // 10 items, 3 per page: page 4 has only 1 item
478        assert_eq!(result.items.len(), 1);
479        assert_eq!(result.total, 10);
480    }
481
482    #[tokio::test]
483    async fn test_list_paginated_empty_repository() {
484        let repo = Arc::new(InMemoryPluginRepository::new());
485
486        let query = PaginationQuery {
487            page: 1,
488            per_page: 10,
489        };
490
491        let result = repo.list_paginated(query).await.unwrap();
492
493        assert_eq!(result.items.len(), 0);
494        assert_eq!(result.total, 0);
495    }
496
497    #[tokio::test]
498    async fn test_list_paginated_page_beyond_data() {
499        let repo = Arc::new(InMemoryPluginRepository::new());
500
501        for i in 1..=5 {
502            repo.add(create_test_plugin(&format!("plugin-{i}")))
503                .await
504                .unwrap();
505        }
506
507        let query = PaginationQuery {
508            page: 10, // Way beyond available data
509            per_page: 2,
510        };
511
512        let result = repo.list_paginated(query).await.unwrap();
513
514        assert_eq!(result.items.len(), 0);
515        assert_eq!(result.total, 5);
516    }
517
518    #[tokio::test]
519    async fn test_list_paginated_large_per_page() {
520        let repo = Arc::new(InMemoryPluginRepository::new());
521
522        for i in 1..=5 {
523            repo.add(create_test_plugin(&format!("plugin-{i}")))
524                .await
525                .unwrap();
526        }
527
528        let query = PaginationQuery {
529            page: 1,
530            per_page: 100, // More than available
531        };
532
533        let result = repo.list_paginated(query).await.unwrap();
534
535        assert_eq!(result.items.len(), 5);
536        assert_eq!(result.total, 5);
537    }
538
539    // ============================================
540    // has_entries and drop_all tests
541    // ============================================
542
543    #[tokio::test]
544    async fn test_has_entries_empty() {
545        let repo = InMemoryPluginRepository::new();
546        assert!(!repo.has_entries().await.unwrap());
547    }
548
549    #[tokio::test]
550    async fn test_has_entries_with_data() {
551        let repo = Arc::new(InMemoryPluginRepository::new());
552        repo.add(create_test_plugin("test")).await.unwrap();
553        assert!(repo.has_entries().await.unwrap());
554    }
555
556    #[tokio::test]
557    async fn test_drop_all_entries_clears_store() {
558        let repo = Arc::new(InMemoryPluginRepository::new());
559
560        for i in 1..=5 {
561            repo.add(create_test_plugin(&format!("plugin-{i}")))
562                .await
563                .unwrap();
564        }
565
566        assert!(repo.has_entries().await.unwrap());
567        assert_eq!(repo.count().await.unwrap(), 5);
568
569        repo.drop_all_entries().await.unwrap();
570
571        assert!(!repo.has_entries().await.unwrap());
572        assert_eq!(repo.count().await.unwrap(), 0);
573    }
574
575    #[tokio::test]
576    async fn test_drop_all_entries_on_empty_repo() {
577        let repo = InMemoryPluginRepository::new();
578
579        // Should not fail on empty repo
580        let result = repo.drop_all_entries().await;
581        assert!(result.is_ok());
582    }
583
584    // ============================================
585    // Compiled code cache tests
586    // ============================================
587
588    #[tokio::test]
589    async fn test_store_and_get_compiled_code() {
590        let repo = InMemoryPluginRepository::new();
591
592        repo.store_compiled_code("plugin-1", "compiled code here", None)
593            .await
594            .unwrap();
595
596        let result = repo.get_compiled_code("plugin-1").await.unwrap();
597        assert_eq!(result, Some("compiled code here".to_string()));
598    }
599
600    #[tokio::test]
601    async fn test_get_compiled_code_nonexistent() {
602        let repo = InMemoryPluginRepository::new();
603
604        let result = repo.get_compiled_code("nonexistent").await.unwrap();
605        assert_eq!(result, None);
606    }
607
608    #[tokio::test]
609    async fn test_store_compiled_code_with_source_hash() {
610        let repo = InMemoryPluginRepository::new();
611
612        repo.store_compiled_code("plugin-1", "code", Some("abc123hash"))
613            .await
614            .unwrap();
615
616        let code = repo.get_compiled_code("plugin-1").await.unwrap();
617        assert_eq!(code, Some("code".to_string()));
618
619        let hash = repo.get_source_hash("plugin-1").await.unwrap();
620        assert_eq!(hash, Some("abc123hash".to_string()));
621    }
622
623    #[tokio::test]
624    async fn test_store_compiled_code_overwrites_existing() {
625        let repo = InMemoryPluginRepository::new();
626
627        repo.store_compiled_code("plugin-1", "old code", Some("oldhash"))
628            .await
629            .unwrap();
630
631        repo.store_compiled_code("plugin-1", "new code", Some("newhash"))
632            .await
633            .unwrap();
634
635        let code = repo.get_compiled_code("plugin-1").await.unwrap();
636        assert_eq!(code, Some("new code".to_string()));
637
638        let hash = repo.get_source_hash("plugin-1").await.unwrap();
639        assert_eq!(hash, Some("newhash".to_string()));
640    }
641
642    #[tokio::test]
643    async fn test_has_compiled_code() {
644        let repo = InMemoryPluginRepository::new();
645
646        assert!(!repo.has_compiled_code("plugin-1").await.unwrap());
647
648        repo.store_compiled_code("plugin-1", "code", None)
649            .await
650            .unwrap();
651
652        assert!(repo.has_compiled_code("plugin-1").await.unwrap());
653        assert!(!repo.has_compiled_code("plugin-2").await.unwrap());
654    }
655
656    #[tokio::test]
657    async fn test_invalidate_compiled_code() {
658        let repo = InMemoryPluginRepository::new();
659
660        repo.store_compiled_code("plugin-1", "code1", None)
661            .await
662            .unwrap();
663        repo.store_compiled_code("plugin-2", "code2", None)
664            .await
665            .unwrap();
666
667        assert!(repo.has_compiled_code("plugin-1").await.unwrap());
668        assert!(repo.has_compiled_code("plugin-2").await.unwrap());
669
670        repo.invalidate_compiled_code("plugin-1").await.unwrap();
671
672        assert!(!repo.has_compiled_code("plugin-1").await.unwrap());
673        assert!(repo.has_compiled_code("plugin-2").await.unwrap());
674    }
675
676    #[tokio::test]
677    async fn test_invalidate_compiled_code_nonexistent() {
678        let repo = InMemoryPluginRepository::new();
679
680        // Should not fail on nonexistent
681        let result = repo.invalidate_compiled_code("nonexistent").await;
682        assert!(result.is_ok());
683    }
684
685    #[tokio::test]
686    async fn test_invalidate_all_compiled_code() {
687        let repo = InMemoryPluginRepository::new();
688
689        for i in 1..=5 {
690            repo.store_compiled_code(&format!("plugin-{i}"), &format!("code-{i}"), None)
691                .await
692                .unwrap();
693        }
694
695        for i in 1..=5 {
696            assert!(repo
697                .has_compiled_code(&format!("plugin-{i}"))
698                .await
699                .unwrap());
700        }
701
702        repo.invalidate_all_compiled_code().await.unwrap();
703
704        for i in 1..=5 {
705            assert!(!repo
706                .has_compiled_code(&format!("plugin-{i}"))
707                .await
708                .unwrap());
709        }
710    }
711
712    #[tokio::test]
713    async fn test_invalidate_all_compiled_code_empty() {
714        let repo = InMemoryPluginRepository::new();
715
716        // Should not fail on empty cache
717        let result = repo.invalidate_all_compiled_code().await;
718        assert!(result.is_ok());
719    }
720
721    #[tokio::test]
722    async fn test_get_source_hash() {
723        let repo = InMemoryPluginRepository::new();
724
725        // No hash stored
726        repo.store_compiled_code("plugin-1", "code", None)
727            .await
728            .unwrap();
729        let hash = repo.get_source_hash("plugin-1").await.unwrap();
730        assert_eq!(hash, None);
731
732        // Hash stored
733        repo.store_compiled_code("plugin-2", "code", Some("sha256:abc"))
734            .await
735            .unwrap();
736        let hash = repo.get_source_hash("plugin-2").await.unwrap();
737        assert_eq!(hash, Some("sha256:abc".to_string()));
738    }
739
740    #[tokio::test]
741    async fn test_get_source_hash_nonexistent() {
742        let repo = InMemoryPluginRepository::new();
743
744        let hash = repo.get_source_hash("nonexistent").await.unwrap();
745        assert_eq!(hash, None);
746    }
747
748    // ============================================
749    // Clone tests
750    // ============================================
751
752    #[tokio::test]
753    async fn test_clone_copies_store_data() {
754        let repo = InMemoryPluginRepository::new();
755        repo.add(create_test_plugin("plugin-1")).await.unwrap();
756        repo.add(create_test_plugin("plugin-2")).await.unwrap();
757
758        let cloned = repo.clone();
759
760        // Cloned should have same data
761        assert_eq!(cloned.count().await.unwrap(), 2);
762        assert!(cloned.get_by_id("plugin-1").await.unwrap().is_some());
763        assert!(cloned.get_by_id("plugin-2").await.unwrap().is_some());
764    }
765
766    #[tokio::test]
767    async fn test_clone_copies_compiled_cache() {
768        let repo = InMemoryPluginRepository::new();
769        repo.store_compiled_code("plugin-1", "code1", Some("hash1"))
770            .await
771            .unwrap();
772
773        let cloned = repo.clone();
774
775        // Cloned should have same compiled cache
776        assert!(cloned.has_compiled_code("plugin-1").await.unwrap());
777        assert_eq!(
778            cloned.get_compiled_code("plugin-1").await.unwrap(),
779            Some("code1".to_string())
780        );
781        assert_eq!(
782            cloned.get_source_hash("plugin-1").await.unwrap(),
783            Some("hash1".to_string())
784        );
785    }
786
787    #[tokio::test]
788    async fn test_clone_is_independent() {
789        let repo = InMemoryPluginRepository::new();
790        repo.add(create_test_plugin("plugin-1")).await.unwrap();
791
792        let cloned = repo.clone();
793
794        // Modify original
795        repo.add(create_test_plugin("plugin-2")).await.unwrap();
796
797        // Clone should not have the new plugin (independent copy)
798        // Note: This tests the independence after clone
799        assert_eq!(repo.count().await.unwrap(), 2);
800        // cloned was made before plugin-2 was added, so it only has 1
801        assert_eq!(cloned.count().await.unwrap(), 1);
802    }
803
804    // ============================================
805    // PluginModel conversion tests
806    // ============================================
807
808    #[tokio::test]
809    async fn test_plugin_model_try_from_config() {
810        let config = PluginFileConfig {
811            id: "test-plugin".to_string(),
812            path: "test-path".to_string(),
813            timeout: None,
814            emit_logs: false,
815            emit_traces: false,
816            raw_response: false,
817            allow_get_invocation: false,
818            config: None,
819            forward_logs: false,
820        };
821
822        let result = PluginModel::try_from(config);
823        assert!(result.is_ok());
824
825        let plugin = result.unwrap();
826        assert_eq!(plugin.id, "test-plugin");
827        assert_eq!(plugin.path, "test-path");
828        assert_eq!(
829            plugin.timeout,
830            Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS)
831        );
832    }
833
834    #[tokio::test]
835    async fn test_plugin_model_try_from_config_with_timeout() {
836        let mut config_map = serde_json::Map::new();
837        config_map.insert("key".to_string(), serde_json::json!("value"));
838
839        let config = PluginFileConfig {
840            id: "test-plugin".to_string(),
841            path: "test-path".to_string(),
842            timeout: Some(120),
843            emit_logs: true,
844            emit_traces: true,
845            raw_response: true,
846            allow_get_invocation: true,
847            config: Some(config_map),
848            forward_logs: true,
849        };
850
851        let result = PluginModel::try_from(config);
852        assert!(result.is_ok());
853
854        let plugin = result.unwrap();
855        assert_eq!(plugin.timeout, Duration::from_secs(120));
856        assert!(plugin.emit_logs);
857        assert!(plugin.emit_traces);
858        assert!(plugin.raw_response);
859        assert!(plugin.allow_get_invocation);
860        assert!(plugin.config.is_some());
861        assert!(plugin.forward_logs);
862    }
863
864    // ============================================
865    // Compiled cache independence from store
866    // ============================================
867
868    #[tokio::test]
869    async fn test_compiled_cache_independent_of_plugin_store() {
870        let repo = InMemoryPluginRepository::new();
871
872        // Store compiled code without adding plugin to store
873        repo.store_compiled_code("plugin-1", "compiled", None)
874            .await
875            .unwrap();
876
877        // Plugin doesn't exist in store
878        assert!(repo.get_by_id("plugin-1").await.unwrap().is_none());
879
880        // But compiled code exists in cache
881        assert!(repo.has_compiled_code("plugin-1").await.unwrap());
882    }
883
884    #[tokio::test]
885    async fn test_drop_all_entries_does_not_clear_compiled_cache() {
886        let repo = InMemoryPluginRepository::new();
887
888        repo.add(create_test_plugin("plugin-1")).await.unwrap();
889        repo.store_compiled_code("plugin-1", "compiled", None)
890            .await
891            .unwrap();
892
893        repo.drop_all_entries().await.unwrap();
894
895        // Plugin gone
896        assert!(repo.get_by_id("plugin-1").await.unwrap().is_none());
897
898        // But compiled cache still has entry
899        assert!(repo.has_compiled_code("plugin-1").await.unwrap());
900    }
901
902    #[tokio::test]
903    async fn test_invalidate_all_compiled_does_not_clear_store() {
904        let repo = InMemoryPluginRepository::new();
905
906        repo.add(create_test_plugin("plugin-1")).await.unwrap();
907        repo.store_compiled_code("plugin-1", "compiled", None)
908            .await
909            .unwrap();
910
911        repo.invalidate_all_compiled_code().await.unwrap();
912
913        // Compiled cache cleared
914        assert!(!repo.has_compiled_code("plugin-1").await.unwrap());
915
916        // But plugin still exists in store
917        assert!(repo.get_by_id("plugin-1").await.unwrap().is_some());
918    }
919}