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}