Skip to main content

cpex_core/
registry.rs

1// Location: ./crates/cpex-core/src/registry.rs
2// Copyright 2025
3// SPDX-License-Identifier: Apache-2.0
4// Authors: Teryl Taylor
5//
6// Plugin and hook registries.
7//
8// PluginRef wraps a plugin implementation with the manager's
9// authoritative config. The config comes from the config loader,
10// NOT from the plugin — the plugin never provides its own config
11// to the manager. This prevents a plugin from tampering with its
12// own priority, mode, or capabilities.
13//
14// Trust flows one direction:
15//   config loader → manager → PluginRef → executor
16// The plugin is just a recipient, not a source.
17//
18// The registry supports two registration paths:
19//
20// 1. **Typed** (`register::<H>()`) — for Rust plugins implementing
21//    a handler trait generated by define_hook!. The handler is stored
22//    type-erased alongside the PluginRef. At dispatch time, the typed
23//    path (`invoke::<H>()`) downcasts back; the dynamic path
24//    (`invoke_by_name()`) calls through the type-erased interface.
25//
26// 2. **Name-based** (`register_for_names::<H>()`) — same handler
27//    registered under multiple hook names (the CMF pattern).
28//
29// Mirrors the Python framework's PluginRef and PluginInstanceRegistry
30// in cpex/framework/base.py and cpex/framework/registry.py.
31
32use std::collections::HashMap;
33use std::sync::atomic::{AtomicBool, Ordering};
34
35use std::sync::Arc;
36use uuid::Uuid;
37
38use crate::context::PluginContext;
39use crate::hooks::payload::{Extensions, PluginPayload};
40use crate::hooks::trait_def::HookTypeDef;
41use crate::hooks::HookType;
42use crate::plugin::{Plugin, PluginConfig, PluginMode};
43
44// ---------------------------------------------------------------------------
45// Plugin Ref — trusted wrapper
46// ---------------------------------------------------------------------------
47
48/// Manager-owned wrapper that pairs a plugin with its authoritative config.
49///
50/// The `trusted_config` comes from the config loader / manager — never
51/// from the plugin itself. The executor reads all scheduling decisions
52/// (mode, priority, hooks, capabilities, on_error) from this config.
53///
54/// The plugin receives a copy of its config at construction time so it
55/// can read its own settings during hook execution. But the manager/executor
56/// never reads config back from the plugin.
57///
58/// Trust flow:
59/// ```text
60/// config loader → manager → PluginRef.trusted_config → executor
61///                        ↘ plugin (receives a copy, cannot influence scheduling)
62/// ```
63#[derive(Clone)]
64pub struct PluginRef {
65    /// The plugin implementation.
66    plugin: Arc<dyn Plugin>,
67
68    /// Authoritative config from the config loader.
69    /// The executor uses this for all scheduling and capability decisions.
70    trusted_config: PluginConfig,
71
72    /// Unique identifier assigned by the registry.
73    /// Stored as `Uuid` (16 bytes, `Copy`) rather than a 36-char `String`
74    /// to avoid heap allocation per registered plugin and to give
75    /// downstream `HashMap<Uuid, _>` keys fixed-size hashing.
76    id: Uuid,
77
78    /// Runtime circuit breaker — set to true when `on_error: Disable`
79    /// triggers. Once set, `mode()` returns `Disabled` and the plugin
80    /// is skipped by `group_by_mode()` on all subsequent invocations.
81    /// Uses `Arc<AtomicBool>` so clones (in HookEntry) share the same flag.
82    disabled: Arc<AtomicBool>,
83}
84
85impl PluginRef {
86    /// Create a new PluginRef with an independently-sourced config.
87    ///
88    /// The `trusted_config` must come from the config loader or manager,
89    /// NOT from `plugin.config()`. The plugin may hold its own copy
90    /// for reading during execute(), but the manager never consults it.
91    pub fn new(plugin: Arc<dyn Plugin>, trusted_config: PluginConfig) -> Self {
92        Self {
93            plugin,
94            trusted_config,
95            id: Uuid::new_v4(),
96            disabled: Arc::new(AtomicBool::new(false)),
97        }
98    }
99
100    /// The authoritative config used by the executor for all decisions.
101    pub fn trusted_config(&self) -> &PluginConfig {
102        &self.trusted_config
103    }
104
105    /// The plugin implementation (for calling initialize/shutdown).
106    pub fn plugin(&self) -> &Arc<dyn Plugin> {
107        &self.plugin
108    }
109
110    /// Unique identifier assigned at registration.
111    /// Returned by value — `Uuid` is `Copy` (16 bytes).
112    pub fn id(&self) -> Uuid {
113        self.id
114    }
115
116    /// Convenience: plugin name from the trusted config.
117    pub fn name(&self) -> &str {
118        &self.trusted_config.name
119    }
120
121    /// Effective mode — returns `Disabled` if the runtime circuit breaker
122    /// has tripped, otherwise returns the configured mode.
123    ///
124    /// `Acquire` on the load pairs with `Release` on the disable() store
125    /// so weak-memory-ordering hardware (ARM64) propagates the disable
126    /// promptly across threads.
127    pub fn mode(&self) -> PluginMode {
128        if self.disabled.load(Ordering::Acquire) {
129            PluginMode::Disabled
130        } else {
131            self.trusted_config.mode
132        }
133    }
134
135    /// Runtime-disable this plugin (one-way circuit breaker).
136    ///
137    /// Called by the executor when a plugin errors with `on_error: Disable`.
138    /// All clones of this PluginRef (in HookEntry, etc.) share the same
139    /// `AtomicBool`, so the disable is visible across the system.
140    ///
141    /// `Release` ordering establishes a happens-before with `Acquire`
142    /// loads in `is_disabled()` and `mode()` — required for correctness
143    /// on weak-memory hardware (ARM64) where `Relaxed` allows the new
144    /// value to remain unobserved by other threads for an unbounded window.
145    pub fn disable(&self) {
146        self.disabled.store(true, Ordering::Release);
147    }
148
149    /// Whether this plugin has been runtime-disabled.
150    /// `Acquire` pairs with the `Release` in `disable()` (see `mode()`).
151    pub fn is_disabled(&self) -> bool {
152        self.disabled.load(Ordering::Acquire)
153    }
154
155    /// Convenience: plugin priority from the trusted config.
156    pub fn priority(&self) -> i32 {
157        self.trusted_config.priority
158    }
159}
160
161// ---------------------------------------------------------------------------
162// Type-Erased Hook Handler
163// ---------------------------------------------------------------------------
164
165/// Type-erased interface for calling a hook handler.
166///
167/// The executor uses this to dispatch hooks without knowing the
168/// concrete handler trait at compile time. Each handler wraps a
169/// plugin that implements a specific handler trait (e.g.,
170/// `CmfHookHandler`) and translates between type-erased payloads
171/// and the typed handler method.
172///
173/// The executor dispatches through this trait for all five phases.
174/// The handler receives a borrowed payload — the framework retains
175/// ownership. Plugins clone only when modifying.
176///
177/// `invoke` is async so that plugins can perform I/O (HTTP calls,
178/// Redis, vault lookups) without blocking the tokio runtime, and
179/// so that `tokio::time::timeout` can actually observe and cancel
180/// long-running handlers.
181#[async_trait::async_trait]
182pub trait AnyHookHandler: Send + Sync {
183    /// Call the handler with a borrowed payload.
184    ///
185    /// Returns an `ErasedResultFields` (see executor module) wrapped
186    /// as `Box<dyn Any>`. If the handler modified the payload, the
187    /// modified copy is in `ErasedResultFields.modified_payload`.
188    async fn invoke(
189        &self,
190        payload: &dyn PluginPayload,
191        extensions: &Extensions,
192        ctx: &mut PluginContext,
193    ) -> Result<Box<dyn std::any::Any + Send + Sync>, Box<crate::error::PluginError>>;
194
195    /// The hook type name this handler was registered for.
196    fn hook_type_name(&self) -> &'static str;
197}
198
199// ---------------------------------------------------------------------------
200// Hook Entry — PluginRef + handler paired together
201// ---------------------------------------------------------------------------
202
203/// A registered hook handler paired with its PluginRef.
204///
205/// The executor uses `plugin_ref` for scheduling decisions (mode,
206/// priority, capabilities) and `handler` for actual dispatch.
207///
208/// `plugin_ref` is `Arc<PluginRef>` so cloning a `HookEntry` is two
209/// reference-count bumps rather than a deep clone of the embedded
210/// `PluginConfig`. `group_by_mode` (called once per invoke) clones N
211/// entries — keeping that cheap matters at high request rates.
212#[derive(Clone)]
213pub struct HookEntry {
214    /// The plugin wrapper with authoritative config.
215    pub plugin_ref: Arc<PluginRef>,
216
217    /// The type-erased handler for this specific hook.
218    pub handler: Arc<dyn AnyHookHandler>,
219}
220
221// ---------------------------------------------------------------------------
222// Plugin Registry
223// ---------------------------------------------------------------------------
224
225/// Manages registered plugin instances and hook handler mappings.
226///
227/// Stores `PluginRef` wrappers by name and `HookEntry` (PluginRef +
228/// handler) by hook name. The executor reads scheduling decisions
229/// from `PluginRef.trusted_config` and dispatches through the
230/// type-erased handler.
231///
232/// Supports two registration patterns:
233///
234/// - `register::<H>()` — typed registration for a single hook name
235///   (derived from `H::NAME`).
236/// - `register_for_names::<H>()` — typed registration for multiple
237///   hook names (the CMF pattern where one handler covers
238///   `cmf.tool_pre_invoke`, `cmf.llm_input`, etc.).
239///
240/// `Clone` is cheap-ish: the HashMaps duplicate, but their values are all
241/// `Arc`-counted (`Arc<PluginRef>`, `Arc<dyn AnyHookHandler>`), so the
242/// inner data is shared. Used by `PluginManager`'s `ArcSwap` snapshot
243/// pattern, where every mutating method clones the registry, mutates the
244/// clone, and atomically swaps in a new snapshot.
245#[derive(Clone)]
246pub struct PluginRegistry {
247    /// Plugins keyed by name (for lookup and lifecycle). Wrapped in `Arc`
248    /// so the same instance is shared with every `HookEntry` in
249    /// `hook_index` — registering a plugin allocates one `PluginRef`,
250    /// not one per hook.
251    plugins: HashMap<String, Arc<PluginRef>>,
252
253    /// Hook name → list of HookEntries, sorted by priority.
254    hook_index: HashMap<HookType, Vec<HookEntry>>,
255}
256
257impl PluginRegistry {
258    /// Create an empty registry.
259    pub fn new() -> Self {
260        Self {
261            plugins: HashMap::new(),
262            hook_index: HashMap::new(),
263        }
264    }
265
266    /// Register a typed hook handler for its primary hook name.
267    ///
268    /// The handler is registered under `H::NAME`. The `config` must
269    /// come from the config loader — not from the plugin. The plugin
270    /// must implement the handler trait generated by `define_hook!`.
271    ///
272    /// # Type Parameters
273    ///
274    /// - `H` — the hook type (implements `HookTypeDef`).
275    ///
276    /// # Arguments
277    ///
278    /// - `plugin` — the plugin implementation (must also implement the handler trait).
279    /// - `config` — authoritative config from the config loader.
280    /// - `handler` — type-erased handler wrapping the plugin's handler trait impl.
281    pub fn register<H: HookTypeDef>(
282        &mut self,
283        plugin: Arc<dyn Plugin>,
284        config: PluginConfig,
285        handler: Arc<dyn AnyHookHandler>,
286    ) -> Result<(), String> {
287        self.register_for_names_inner(plugin, config, handler, &[H::NAME])
288    }
289
290    /// Register a typed hook handler for multiple hook names.
291    ///
292    /// This is the CMF pattern — one handler trait impl covers multiple
293    /// hook names (`cmf.tool_pre_invoke`, `cmf.llm_input`, etc.).
294    ///
295    /// # Arguments
296    ///
297    /// - `plugin` — the plugin implementation.
298    /// - `config` — authoritative config from the config loader.
299    /// - `handler` — type-erased handler.
300    /// - `names` — hook names to register under.
301    pub fn register_for_names<H: HookTypeDef>(
302        &mut self,
303        plugin: Arc<dyn Plugin>,
304        config: PluginConfig,
305        handler: Arc<dyn AnyHookHandler>,
306        names: &[&str],
307    ) -> Result<(), String> {
308        self.register_for_names_inner(plugin, config, handler, names)
309    }
310
311    /// Register a plugin with a handler for multiple hook names.
312    ///
313    /// Like `register_for_names` but without requiring a `HookTypeDef`
314    /// type parameter. Used by the config-driven factory path where
315    /// the hook type is not known at compile time — the factory
316    /// provides the handler directly.
317    pub fn register_for_names_with_handler(
318        &mut self,
319        plugin: Arc<dyn Plugin>,
320        config: PluginConfig,
321        handler: Arc<dyn AnyHookHandler>,
322        names: &[&str],
323    ) -> Result<(), String> {
324        self.register_for_names_inner(plugin, config, handler, names)
325    }
326
327    /// Register a plugin with multiple handlers, each for a specific hook.
328    ///
329    /// Used when a plugin implements multiple hook types with different
330    /// payloads (e.g., `ToolPreInvoke` and `ToolPostInvoke`). Each
331    /// handler is registered under its paired hook name.
332    ///
333    /// The plugin is registered once in the name index. Each handler
334    /// gets its own `HookEntry` in the hook index under the specified name.
335    pub fn register_multi_handler(
336        &mut self,
337        plugin: Arc<dyn Plugin>,
338        config: PluginConfig,
339        handlers: Vec<(&str, Arc<dyn AnyHookHandler>)>,
340    ) -> Result<(), String> {
341        let name = config.name.clone();
342
343        if self.plugins.contains_key(&name) {
344            return Err(format!("plugin '{}' is already registered", name));
345        }
346
347        let plugin_ref = Arc::new(PluginRef::new(plugin, config));
348
349        for (hook_name, handler) in &handlers {
350            let hook_type = HookType::new(*hook_name);
351            let entry = HookEntry {
352                plugin_ref: Arc::clone(&plugin_ref),
353                handler: Arc::clone(handler),
354            };
355            self.hook_index.entry(hook_type).or_default().push(entry);
356        }
357
358        // Sort each affected hook's entry list by trusted priority
359        for (hook_name, _) in &handlers {
360            let hook_type = HookType::new(*hook_name);
361            if let Some(entries) = self.hook_index.get_mut(&hook_type) {
362                entries.sort_by_key(|e| e.plugin_ref.priority());
363            }
364        }
365
366        self.plugins.insert(name, plugin_ref);
367        Ok(())
368    }
369
370    /// Internal: register handler under one or more hook names.
371    fn register_for_names_inner(
372        &mut self,
373        plugin: Arc<dyn Plugin>,
374        config: PluginConfig,
375        handler: Arc<dyn AnyHookHandler>,
376        names: &[&str],
377    ) -> Result<(), String> {
378        let name = config.name.clone();
379
380        if self.plugins.contains_key(&name) {
381            return Err(format!("plugin '{}' is already registered", name));
382        }
383
384        let plugin_ref = Arc::new(PluginRef::new(plugin, config));
385
386        // Add to hook index for each specified hook name
387        for hook_name in names {
388            let hook_type = HookType::new(*hook_name);
389            let entry = HookEntry {
390                plugin_ref: Arc::clone(&plugin_ref),
391                handler: Arc::clone(&handler),
392            };
393            self.hook_index.entry(hook_type).or_default().push(entry);
394        }
395
396        // Sort each affected hook's entry list by trusted priority
397        for hook_name in names {
398            let hook_type = HookType::new(*hook_name);
399            if let Some(entries) = self.hook_index.get_mut(&hook_type) {
400                entries.sort_by_key(|e| e.plugin_ref.priority());
401            }
402        }
403
404        self.plugins.insert(name, plugin_ref);
405        Ok(())
406    }
407
408    /// Unregister a plugin by name.
409    ///
410    /// Removes the PluginRef from the name index and all HookEntries
411    /// from the hook index. Returns the (Arc-wrapped) PluginRef if found.
412    pub fn unregister(&mut self, name: &str) -> Option<Arc<PluginRef>> {
413        let plugin_ref = self.plugins.remove(name)?;
414
415        // Remove from hook index
416        for entries in self.hook_index.values_mut() {
417            entries.retain(|e| e.plugin_ref.name() != name);
418        }
419
420        // Clean up empty hook entries
421        self.hook_index.retain(|_, entries| !entries.is_empty());
422
423        Some(plugin_ref)
424    }
425
426    /// Look up a PluginRef by name. Returns an `Arc` clone so callers
427    /// don't hold borrows on internal storage — works with snapshot-based
428    /// dispatch where the registry may sit behind a transient guard.
429    pub fn get(&self, name: &str) -> Option<Arc<PluginRef>> {
430        self.plugins.get(name).map(Arc::clone)
431    }
432
433    /// Returns all HookEntries for a given hook name, sorted by priority.
434    ///
435    /// Returns an empty slice if no plugins are registered for the hook.
436    pub fn entries_for_hook(&self, hook_type: &HookType) -> &[HookEntry] {
437        self.hook_index
438            .get(hook_type)
439            .map(|v| v.as_slice())
440            .unwrap_or(&[])
441    }
442
443    /// Whether any plugins are registered for the given hook name.
444    pub fn has_hooks_for(&self, hook_type: &HookType) -> bool {
445        self.hook_index
446            .get(hook_type)
447            .map(|v| !v.is_empty())
448            .unwrap_or(false)
449    }
450
451    /// Total number of registered plugins.
452    pub fn plugin_count(&self) -> usize {
453        self.plugins.len()
454    }
455
456    /// All registered plugin names. Returns owned `String`s so callers
457    /// don't hold borrows on internal storage — works with snapshot-based
458    /// dispatch where the registry may sit behind a transient guard.
459    pub fn plugin_names(&self) -> Vec<String> {
460        self.plugins.keys().cloned().collect()
461    }
462
463    /// Returns every (hook_name, HookEntry) pair where the entry's plugin
464    /// matches the given name. Used by external orchestrators that need
465    /// to build pre-resolved dispatch lineups for a single plugin across
466    /// every hook it registered to (e.g. apl-cpex deciding which entry
467    /// handles step-style invocations vs field-style invocations for the
468    /// same plugin). Owned tuples — no borrows held on the registry.
469    pub fn entries_for_plugin(&self, plugin_name: &str) -> Vec<(String, HookEntry)> {
470        let mut out = Vec::new();
471        for (hook_type, entries) in &self.hook_index {
472            for entry in entries {
473                if entry.plugin_ref.name() == plugin_name {
474                    out.push((hook_type.as_str().to_string(), entry.clone()));
475                }
476            }
477        }
478        out
479    }
480}
481
482impl Default for PluginRegistry {
483    fn default() -> Self {
484        Self::new()
485    }
486}
487
488// ---------------------------------------------------------------------------
489// Group HookEntries by mode (used by the executor)
490// ---------------------------------------------------------------------------
491
492/// Groups a list of HookEntries by their execution mode.
493///
494/// Reads mode from `plugin_ref.trusted_config` — never from the plugin.
495/// Returns a tuple of five vectors in execution order:
496/// (sequential, transform, audit, concurrent, fire_and_forget).
497/// Disabled plugins are excluded.
498pub type GroupedHookEntries = (
499    Vec<HookEntry>,
500    Vec<HookEntry>,
501    Vec<HookEntry>,
502    Vec<HookEntry>,
503    Vec<HookEntry>,
504);
505
506pub fn group_by_mode(entries: &[HookEntry]) -> GroupedHookEntries {
507    let mut sequential = Vec::new();
508    let mut transform = Vec::new();
509    let mut audit = Vec::new();
510    let mut concurrent = Vec::new();
511    let mut fire_and_forget = Vec::new();
512
513    for entry in entries {
514        match entry.plugin_ref.mode() {
515            PluginMode::Sequential => sequential.push(entry.clone()),
516            PluginMode::Transform => transform.push(entry.clone()),
517            PluginMode::Audit => audit.push(entry.clone()),
518            PluginMode::Concurrent => concurrent.push(entry.clone()),
519            PluginMode::FireAndForget => fire_and_forget.push(entry.clone()),
520            PluginMode::Disabled => {}, // skip
521        }
522    }
523
524    (sequential, transform, audit, concurrent, fire_and_forget)
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use crate::error::PluginError;
531    use crate::hooks::payload::PluginPayload;
532    use crate::hooks::PluginResult;
533    use async_trait::async_trait;
534
535    // -- Test payload and hook type --
536
537    #[derive(Debug, Clone)]
538    #[allow(dead_code)] // test fixture — typed shape is the point, not field reads
539    struct TestPayload {
540        value: String,
541    }
542    crate::impl_plugin_payload!(TestPayload);
543
544    // -- Test handler (type-erased wrapper) --
545
546    /// A simple AnyHookHandler that wraps a function for testing.
547    struct TestHandler;
548
549    #[async_trait]
550    impl AnyHookHandler for TestHandler {
551        async fn invoke(
552            &self,
553            _payload: &dyn PluginPayload,
554            _extensions: &Extensions,
555            _ctx: &mut PluginContext,
556        ) -> Result<Box<dyn std::any::Any + Send + Sync>, Box<PluginError>> {
557            let result: PluginResult<TestPayload> = PluginResult::allow();
558            Ok(crate::executor::erase_result(result))
559        }
560
561        fn hook_type_name(&self) -> &'static str {
562            "test_hook"
563        }
564    }
565
566    // -- Test plugin --
567
568    struct TestPlugin {
569        cfg: PluginConfig,
570    }
571
572    fn make_config(name: &str, hooks: Vec<&str>, priority: i32) -> PluginConfig {
573        PluginConfig {
574            name: name.to_string(),
575            kind: "test".to_string(),
576            description: None,
577            author: None,
578            version: None,
579            hooks: hooks.into_iter().map(String::from).collect(),
580            mode: PluginMode::Sequential,
581            priority,
582            on_error: Default::default(),
583            capabilities: Default::default(),
584            tags: Vec::new(),
585            conditions: Vec::new(),
586            config: None,
587        }
588    }
589
590    impl TestPlugin {
591        fn new(cfg: PluginConfig) -> Self {
592            Self { cfg }
593        }
594    }
595
596    #[async_trait]
597    impl Plugin for TestPlugin {
598        fn config(&self) -> &PluginConfig {
599            &self.cfg
600        }
601        async fn initialize(&self) -> Result<(), Box<PluginError>> {
602            Ok(())
603        }
604        async fn shutdown(&self) -> Result<(), Box<PluginError>> {
605            Ok(())
606        }
607    }
608
609    // -- Tests --
610
611    #[test]
612    fn test_register_typed_and_lookup() {
613        let mut reg = PluginRegistry::new();
614        let config = make_config("test-plugin", vec!["test_hook"], 10);
615        let plugin = Arc::new(TestPlugin::new(config.clone()));
616        let handler: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
617
618        // Use register_for_names_inner directly since we don't have a real HookTypeDef
619        reg.register_for_names_inner(plugin, config, handler, &["test_hook"])
620            .unwrap();
621
622        assert_eq!(reg.plugin_count(), 1);
623        assert!(reg.get("test-plugin").is_some());
624        assert!(reg.has_hooks_for(&HookType::new("test_hook")));
625        assert!(!reg.has_hooks_for(&HookType::new("other_hook")));
626
627        let entries = reg.entries_for_hook(&HookType::new("test_hook"));
628        assert_eq!(entries.len(), 1);
629        assert_eq!(entries[0].plugin_ref.name(), "test-plugin");
630    }
631
632    #[test]
633    fn test_register_for_multiple_names() {
634        let mut reg = PluginRegistry::new();
635        let config = make_config("cmf-plugin", vec![], 10);
636        let plugin = Arc::new(TestPlugin::new(config.clone()));
637        let handler: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
638
639        reg.register_for_names_inner(
640            plugin,
641            config,
642            handler,
643            &[
644                "cmf.tool_pre_invoke",
645                "cmf.tool_post_invoke",
646                "cmf.llm_input",
647            ],
648        )
649        .unwrap();
650
651        assert_eq!(reg.plugin_count(), 1);
652        assert!(reg.has_hooks_for(&HookType::new("cmf.tool_pre_invoke")));
653        assert!(reg.has_hooks_for(&HookType::new("cmf.tool_post_invoke")));
654        assert!(reg.has_hooks_for(&HookType::new("cmf.llm_input")));
655        assert!(!reg.has_hooks_for(&HookType::new("cmf.llm_output")));
656    }
657
658    #[test]
659    fn test_duplicate_registration_fails() {
660        let mut reg = PluginRegistry::new();
661        let c1 = make_config("dup", vec![], 10);
662        let c2 = make_config("dup", vec![], 20);
663        let p1 = Arc::new(TestPlugin::new(c1.clone()));
664        let p2 = Arc::new(TestPlugin::new(c2.clone()));
665        let h1: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
666        let h2: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
667
668        assert!(reg
669            .register_for_names_inner(p1, c1, h1, &["hook_a"])
670            .is_ok());
671        assert!(reg
672            .register_for_names_inner(p2, c2, h2, &["hook_a"])
673            .is_err());
674    }
675
676    #[test]
677    fn test_priority_ordering_uses_trusted_config() {
678        let mut reg = PluginRegistry::new();
679        let c_low = make_config("low", vec![], 100);
680        let c_high = make_config("high", vec![], 10);
681        let p_low = Arc::new(TestPlugin::new(c_low.clone()));
682        let p_high = Arc::new(TestPlugin::new(c_high.clone()));
683        let h1: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
684        let h2: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
685
686        reg.register_for_names_inner(p_low, c_low, h1, &["hook_a"])
687            .unwrap();
688        reg.register_for_names_inner(p_high, c_high, h2, &["hook_a"])
689            .unwrap();
690
691        let entries = reg.entries_for_hook(&HookType::new("hook_a"));
692        assert_eq!(entries[0].plugin_ref.name(), "high"); // priority 10 first
693        assert_eq!(entries[1].plugin_ref.name(), "low"); // priority 100 second
694    }
695
696    #[test]
697    fn test_unregister() {
698        let mut reg = PluginRegistry::new();
699        let config = make_config("removable", vec![], 10);
700        let plugin = Arc::new(TestPlugin::new(config.clone()));
701        let handler: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
702
703        reg.register_for_names_inner(plugin, config, handler, &["hook_a"])
704            .unwrap();
705
706        assert_eq!(reg.plugin_count(), 1);
707        reg.unregister("removable");
708        assert_eq!(reg.plugin_count(), 0);
709        assert!(!reg.has_hooks_for(&HookType::new("hook_a")));
710    }
711
712    #[test]
713    fn test_plugin_ref_id_is_unique() {
714        let c1 = make_config("a", vec![], 10);
715        let c2 = make_config("b", vec![], 10);
716        let p1 = Arc::new(TestPlugin::new(c1.clone()));
717        let p2 = Arc::new(TestPlugin::new(c2.clone()));
718        let ref1 = PluginRef::new(p1, c1);
719        let ref2 = PluginRef::new(p2, c2);
720        assert_ne!(ref1.id(), ref2.id());
721    }
722
723    #[test]
724    fn test_tampered_plugin_config_ignored() {
725        let trusted = make_config("sneaky", vec![], 100);
726        let mut tampered = trusted.clone();
727        tampered.priority = 1;
728        let plugin = Arc::new(TestPlugin::new(tampered));
729
730        let plugin_ref = PluginRef::new(plugin, trusted);
731        assert_eq!(plugin_ref.priority(), 100);
732    }
733
734    #[tokio::test]
735    async fn test_handler_invoke() {
736        let handler = TestHandler;
737        let payload = TestPayload {
738            value: "test".into(),
739        };
740        let ext = Extensions::default();
741        let mut ctx = PluginContext::new();
742
743        let result = handler
744            .invoke(&payload as &dyn PluginPayload, &ext, &mut ctx)
745            .await
746            .unwrap();
747        let fields = crate::executor::extract_erased(result).unwrap();
748        assert!(fields.continue_processing);
749    }
750}