1use anyhow::{bail, Context, Result};
18use libloading::{Library, Symbol};
19use oxi_agent::{AgentEvent, AgentTool, AgentToolResult};
20use parking_lot::RwLock;
21use serde::{Deserialize, Serialize};
22use serde_json::Value;
23use std::collections::HashMap;
24use std::ffi::OsStr;
25use std::fmt;
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum ExtensionPermission {
37 FileRead,
39 FileWrite,
41 Bash,
43 Network,
45}
46
47impl fmt::Display for ExtensionPermission {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 match self {
50 ExtensionPermission::FileRead => write!(f, "file_read"),
51 ExtensionPermission::FileWrite => write!(f, "file_write"),
52 ExtensionPermission::Bash => write!(f, "bash"),
53 ExtensionPermission::Network => write!(f, "network"),
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ExtensionManifest {
68 pub name: String,
70 pub version: String,
72 #[serde(default)]
74 pub description: String,
75 #[serde(default)]
77 pub author: String,
78 #[serde(default)]
80 pub permissions: Vec<ExtensionPermission>,
81 #[serde(default)]
83 pub config_schema: Option<Value>,
84}
85
86impl ExtensionManifest {
87 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
89 Self {
90 name: name.into(),
91 version: version.into(),
92 description: String::new(),
93 author: String::new(),
94 permissions: Vec::new(),
95 config_schema: None,
96 }
97 }
98
99 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
101 self.description = desc.into();
102 self
103 }
104
105 pub fn with_author(mut self, author: impl Into<String>) -> Self {
107 self.author = author.into();
108 self
109 }
110
111 pub fn with_permission(mut self, perm: ExtensionPermission) -> Self {
113 if !self.permissions.contains(&perm) {
114 self.permissions.push(perm);
115 }
116 self
117 }
118
119 pub fn with_config_schema(mut self, schema: Value) -> Self {
121 self.config_schema = Some(schema);
122 self
123 }
124
125 pub fn has_permission(&self, perm: ExtensionPermission) -> bool {
127 self.permissions.contains(&perm)
128 }
129}
130
131#[derive(Debug, thiserror::Error)]
137pub enum ExtensionError {
138 #[error("Extension '{name}' not found")]
140 NotFound { name: String },
141
142 #[error("Failed to load extension '{name}': {reason}")]
144 LoadFailed { name: String, reason: String },
145
146 #[error("Extension '{name}' hook '{hook}' failed: {error}")]
148 HookFailed {
149 name: String,
150 hook: String,
151 error: String,
152 },
153
154 #[error("Extension '{name}' requires permission '{permission}'")]
156 PermissionDenied {
157 name: String,
158 permission: ExtensionPermission,
159 },
160
161 #[error("Extension '{name}' is disabled")]
163 Disabled { name: String },
164
165 #[error("Hot-reload of extension '{name}' failed: {reason}")]
167 HotReloadFailed { name: String, reason: String },
168
169 #[error("Invalid configuration for extension '{name}': {reason}")]
171 InvalidConfig { name: String, reason: String },
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct ExtensionErrorRecord {
177 pub extension_name: String,
179 pub event: String,
181 pub error: String,
183 #[serde(default)]
185 pub stack: Option<String>,
186 pub timestamp: i64,
188}
189
190impl ExtensionErrorRecord {
191 pub fn new(extension_name: impl Into<String>, event: impl Into<String>, error: impl Into<String>) -> Self {
193 Self {
194 extension_name: extension_name.into(),
195 event: event.into(),
196 error: error.into(),
197 stack: None,
198 timestamp: chrono::Utc::now().timestamp_millis(),
199 }
200 }
201}
202
203pub struct ExtensionContext {
212 pub cwd: PathBuf,
214 settings: Arc<RwLock<crate::settings::Settings>>,
216 pub config: Value,
218 pub session_id: Option<String>,
220 idle: Arc<RwLock<bool>>,
222 tool_registrar: Arc<dyn Fn(Arc<dyn AgentTool>) + Send + Sync>,
224 message_sender: Arc<dyn Fn(&str) + Send + Sync>,
226 errors: Arc<RwLock<Vec<ExtensionErrorRecord>>>,
228}
229
230impl fmt::Debug for ExtensionContext {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 f.debug_struct("ExtensionContext")
233 .field("cwd", &self.cwd)
234 .field("session_id", &self.session_id)
235 .field("idle", &self.idle.read())
236 .finish()
237 }
238}
239
240impl ExtensionContext {
241 pub fn new(
245 cwd: PathBuf,
246 settings: Arc<RwLock<crate::settings::Settings>>,
247 config: Value,
248 session_id: Option<String>,
249 idle: Arc<RwLock<bool>>,
250 tool_registrar: Arc<dyn Fn(Arc<dyn AgentTool>) + Send + Sync>,
251 message_sender: Arc<dyn Fn(&str) + Send + Sync>,
252 errors: Arc<RwLock<Vec<ExtensionErrorRecord>>>,
253 ) -> Self {
254 Self {
255 cwd,
256 settings,
257 config,
258 session_id,
259 idle,
260 tool_registrar,
261 message_sender,
262 errors,
263 }
264 }
265
266 pub fn settings(&self) -> crate::settings::Settings {
268 self.settings.read().clone()
269 }
270
271 pub fn is_idle(&self) -> bool {
273 *self.idle.read()
274 }
275
276 pub fn register_tool(&self, tool: Arc<dyn AgentTool>) {
278 (self.tool_registrar)(tool);
279 }
280
281 pub fn send_message(&self, text: &str) {
283 (self.message_sender)(text);
284 }
285
286 pub fn record_error(&self, extension_name: &str, event: &str, error: &str) {
288 let record = ExtensionErrorRecord::new(extension_name, event, error);
289 tracing::warn!(
290 extension = extension_name,
291 event = event,
292 error = error,
293 "Extension error recorded"
294 );
295 self.errors.write().push(record);
296 }
297
298 pub fn errors(&self) -> Vec<ExtensionErrorRecord> {
300 self.errors.read().clone()
301 }
302
303 pub fn clear_errors(&self) {
305 self.errors.write().clear();
306 }
307
308 pub fn config_get(&self, path: &str) -> Option<Value> {
312 let mut current = &self.config;
313 for key in path.split('.') {
314 match current {
315 Value::Object(map) => current = map.get(key)?,
316 _ => return None,
317 }
318 }
319 Some(current.clone())
320 }
321
322 pub fn read_file(&self, relative_path: &Path) -> Result<String> {
324 let full_path = self.cwd.join(relative_path);
325 std::fs::read_to_string(&full_path)
326 .with_context(|| format!("Failed to read file: {}", full_path.display()))
327 }
328}
329
330pub struct ExtensionContextBuilder {
332 cwd: PathBuf,
333 settings: Option<Arc<RwLock<crate::settings::Settings>>>,
334 config: Value,
335 session_id: Option<String>,
336 idle: Arc<RwLock<bool>>,
337 tool_registrar: Option<Arc<dyn Fn(Arc<dyn AgentTool>) + Send + Sync>>,
338 message_sender: Option<Arc<dyn Fn(&str) + Send + Sync>>,
339 errors: Option<Arc<RwLock<Vec<ExtensionErrorRecord>>>>,
340}
341
342impl ExtensionContextBuilder {
343 pub fn new(cwd: PathBuf) -> Self {
345 Self {
346 cwd,
347 settings: None,
348 config: Value::Null,
349 session_id: None,
350 idle: Arc::new(RwLock::new(true)),
351 tool_registrar: None,
352 message_sender: None,
353 errors: None,
354 }
355 }
356
357 pub fn settings(mut self, settings: Arc<RwLock<crate::settings::Settings>>) -> Self {
359 self.settings = Some(settings);
360 self
361 }
362
363 pub fn config(mut self, config: Value) -> Self {
365 self.config = config;
366 self
367 }
368
369 pub fn session_id(mut self, id: impl Into<String>) -> Self {
371 self.session_id = Some(id.into());
372 self
373 }
374
375 pub fn idle(mut self, idle: Arc<RwLock<bool>>) -> Self {
377 self.idle = idle;
378 self
379 }
380
381 pub fn tool_registrar(mut self, registrar: Arc<dyn Fn(Arc<dyn AgentTool>) + Send + Sync>) -> Self {
383 self.tool_registrar = Some(registrar);
384 self
385 }
386
387 pub fn message_sender(mut self, sender: Arc<dyn Fn(&str) + Send + Sync>) -> Self {
389 self.message_sender = Some(sender);
390 self
391 }
392
393 pub fn errors(mut self, errors: Arc<RwLock<Vec<ExtensionErrorRecord>>>) -> Self {
395 self.errors = Some(errors);
396 self
397 }
398
399 pub fn build(self) -> ExtensionContext {
401 ExtensionContext {
402 cwd: self.cwd,
403 settings: self.settings.unwrap_or_else(|| {
404 Arc::new(RwLock::new(crate::settings::Settings::default()))
405 }),
406 config: self.config,
407 session_id: self.session_id,
408 idle: self.idle,
409 tool_registrar: self.tool_registrar.unwrap_or_else(|| {
410 Arc::new(|_tool| {
411 tracing::debug!("Tool registration attempted with no registrar");
412 })
413 }),
414 message_sender: self.message_sender.unwrap_or_else(|| {
415 Arc::new(|_msg| {
416 tracing::debug!("Message send attempted with no sender");
417 })
418 }),
419 errors: self.errors.unwrap_or_default(),
420 }
421 }
422}
423
424#[derive(Debug, Clone)]
430pub struct Command {
431 pub name: String,
433 pub description: String,
435 pub usage: String,
437}
438
439impl Command {
440 pub fn new(
441 name: impl Into<String>,
442 description: impl Into<String>,
443 usage: impl Into<String>,
444 ) -> Self {
445 Self {
446 name: name.into(),
447 description: description.into(),
448 usage: usage.into(),
449 }
450 }
451}
452
453pub trait Extension: Send + Sync {
465 fn name(&self) -> &str;
469
470 fn description(&self) -> &str;
472
473 fn manifest(&self) -> ExtensionManifest {
478 ExtensionManifest::new(self.name(), "0.0.0")
479 .with_description(self.description())
480 }
481
482 fn register_tools(&self) -> Vec<Arc<dyn AgentTool>> {
486 vec![]
487 }
488
489 fn register_commands(&self) -> Vec<Command> {
491 vec![]
492 }
493
494 fn on_load(&self, _ctx: &ExtensionContext) {}
501
502 fn on_unload(&self) {}
506
507 fn on_message_sent(&self, _msg: &str) {}
511
512 fn on_message_received(&self, _msg: &str) {}
514
515 fn on_tool_call(&self, _tool: &str, _params: &Value) {}
520
521 fn on_tool_result(&self, _tool: &str, _result: &AgentToolResult) {}
523
524 fn on_session_start(&self, _session_id: &str) {}
528
529 fn on_session_end(&self, _session_id: &str) {}
531
532 fn on_settings_changed(&self, _settings: &crate::settings::Settings) {}
536
537 fn on_event(&self, _event: &AgentEvent) {}
544}
545
546struct LoadedExtension {
552 extension: Arc<dyn Extension>,
554 enabled: bool,
556 source_path: Option<PathBuf>,
558}
559
560pub struct ExtensionRegistry {
573 entries: HashMap<String, LoadedExtension>,
575 errors: Arc<RwLock<Vec<ExtensionErrorRecord>>>,
577 #[allow(dead_code)]
579 libraries: Vec<Library>,
580}
581
582impl Default for ExtensionRegistry {
583 fn default() -> Self {
584 Self::new()
585 }
586}
587
588impl ExtensionRegistry {
589 pub fn new() -> Self {
591 Self {
592 entries: HashMap::new(),
593 errors: Arc::new(RwLock::new(Vec::new())),
594 libraries: Vec::new(),
595 }
596 }
597
598 pub fn register(&mut self, ext: Arc<dyn Extension>) {
604 let name = ext.name().to_string();
605 tracing::info!(name = %name, "extension registered");
606 self.entries.insert(
607 name,
608 LoadedExtension {
609 extension: ext,
610 enabled: true,
611 source_path: None,
612 },
613 );
614 }
615
616 pub fn register_with_library(
619 &mut self,
620 ext: Arc<dyn Extension>,
621 source_path: PathBuf,
622 library: Library,
623 ) {
624 let name = ext.name().to_string();
625 tracing::info!(name = %name, path = %source_path.display(), "extension registered (dynamic)");
626 self.libraries.push(library);
627 self.entries.insert(
628 name,
629 LoadedExtension {
630 extension: ext,
631 enabled: true,
632 source_path: Some(source_path),
633 },
634 );
635 }
636
637 pub fn unregister(&mut self, name: &str) -> bool {
642 if let Some(entry) = self.entries.remove(name) {
643 self.call_hook_safe(name, "on_unload", || {
644 entry.extension.on_unload();
645 });
646 tracing::info!(name = %name, "extension unregistered");
647 true
648 } else {
649 false
650 }
651 }
652
653 pub fn disable(&mut self, name: &str) -> Result<(), ExtensionError> {
660 let ext = {
661 let entry = self
662 .entries
663 .get_mut(name)
664 .ok_or_else(|| ExtensionError::NotFound {
665 name: name.to_string(),
666 })?;
667 if !entry.enabled {
668 return Ok(());
669 }
670 entry.enabled = false;
671 Arc::clone(&entry.extension)
672 };
673 self.call_hook_safe(name, "on_unload", || {
674 ext.on_unload();
675 });
676 tracing::info!(name = %name, "extension disabled");
677 Ok(())
678 }
679
680 pub fn enable(&mut self, name: &str, ctx: &ExtensionContext) -> Result<(), ExtensionError> {
682 let ext = {
683 let entry = self
684 .entries
685 .get_mut(name)
686 .ok_or_else(|| ExtensionError::NotFound {
687 name: name.to_string(),
688 })?;
689 if entry.enabled {
690 return Ok(());
691 }
692 entry.enabled = true;
693 Arc::clone(&entry.extension)
694 };
695 self.call_hook_safe(name, "on_load", || {
696 ext.on_load(ctx);
697 });
698 tracing::info!(name = %name, "extension enabled");
699 Ok(())
700 }
701
702 pub fn is_enabled(&self, name: &str) -> bool {
704 self.entries
705 .get(name)
706 .map(|e| e.enabled)
707 .unwrap_or(false)
708 }
709
710 pub fn hot_reload(&mut self, name: &str, ctx: &ExtensionContext) -> Result<(), ExtensionError> {
718 let source_path = {
719 let entry = self
720 .entries
721 .get(name)
722 .ok_or_else(|| ExtensionError::NotFound {
723 name: name.to_string(),
724 })?;
725 entry.source_path.clone()
726 };
727
728 let source_path = source_path.ok_or_else(|| ExtensionError::HotReloadFailed {
729 name: name.to_string(),
730 reason: "no source path recorded (in-memory extension)".to_string(),
731 })?;
732
733 self.unregister(name);
735
736 let new_ext = load_extension(&source_path).map_err(|e| ExtensionError::HotReloadFailed {
738 name: name.to_string(),
739 reason: e.to_string(),
740 })?;
741
742 let library = unsafe {
743 Library::new(&source_path)
744 .map_err(|e| ExtensionError::HotReloadFailed {
745 name: name.to_string(),
746 reason: format!("Failed to re-open library: {}", e),
747 })?
748 };
749
750 self.call_hook_safe(name, "on_load", || {
752 new_ext.on_load(ctx);
753 });
754
755 self.register_with_library(new_ext, source_path, library);
756 tracing::info!(name = %name, "extension hot-reloaded");
757 Ok(())
758 }
759
760 pub fn all_tools(&self) -> Vec<Arc<dyn AgentTool>> {
764 self.entries
765 .values()
766 .filter(|e| e.enabled)
767 .flat_map(|e| e.extension.register_tools())
768 .collect()
769 }
770
771 pub fn all_commands(&self) -> Vec<Command> {
773 self.entries
774 .values()
775 .filter(|e| e.enabled)
776 .flat_map(|e| e.extension.register_commands())
777 .collect()
778 }
779
780 pub fn emit_load(&self, ctx: &ExtensionContext) {
784 for entry in self.entries.values().filter(|e| e.enabled) {
785 let name = entry.extension.name();
786 self.call_hook_safe(name, "on_load", || {
787 entry.extension.on_load(ctx);
788 });
789 }
790 }
791
792 pub fn emit_unload(&self) {
794 for entry in self.entries.values().filter(|e| e.enabled) {
795 let name = entry.extension.name();
796 self.call_hook_safe(name, "on_unload", || {
797 entry.extension.on_unload();
798 });
799 }
800 }
801
802 pub fn emit_message_sent(&self, msg: &str) {
804 for entry in self.entries.values().filter(|e| e.enabled) {
805 let name = entry.extension.name();
806 self.call_hook_safe(name, "on_message_sent", || {
807 entry.extension.on_message_sent(msg);
808 });
809 }
810 }
811
812 pub fn emit_message_received(&self, msg: &str) {
814 for entry in self.entries.values().filter(|e| e.enabled) {
815 let name = entry.extension.name();
816 self.call_hook_safe(name, "on_message_received", || {
817 entry.extension.on_message_received(msg);
818 });
819 }
820 }
821
822 pub fn emit_tool_call(&self, tool: &str, params: &Value) {
824 for entry in self.entries.values().filter(|e| e.enabled) {
825 let name = entry.extension.name();
826 self.call_hook_safe(name, "on_tool_call", || {
827 entry.extension.on_tool_call(tool, params);
828 });
829 }
830 }
831
832 pub fn emit_tool_result(&self, tool: &str, result: &AgentToolResult) {
834 for entry in self.entries.values().filter(|e| e.enabled) {
835 let name = entry.extension.name();
836 self.call_hook_safe(name, "on_tool_result", || {
837 entry.extension.on_tool_result(tool, result);
838 });
839 }
840 }
841
842 pub fn emit_session_start(&self, session_id: &str) {
844 for entry in self.entries.values().filter(|e| e.enabled) {
845 let name = entry.extension.name();
846 self.call_hook_safe(name, "on_session_start", || {
847 entry.extension.on_session_start(session_id);
848 });
849 }
850 }
851
852 pub fn emit_session_end(&self, session_id: &str) {
854 for entry in self.entries.values().filter(|e| e.enabled) {
855 let name = entry.extension.name();
856 self.call_hook_safe(name, "on_session_end", || {
857 entry.extension.on_session_end(session_id);
858 });
859 }
860 }
861
862 pub fn emit_settings_changed(&self, settings: &crate::settings::Settings) {
864 for entry in self.entries.values().filter(|e| e.enabled) {
865 let name = entry.extension.name();
866 self.call_hook_safe(name, "on_settings_changed", || {
867 entry.extension.on_settings_changed(settings);
868 });
869 }
870 }
871
872 pub fn emit_event(&self, event: &AgentEvent) {
874 for entry in self.entries.values().filter(|e| e.enabled) {
875 let name = entry.extension.name();
876 self.call_hook_safe(name, "on_event", || {
877 entry.extension.on_event(event);
878 });
879 }
880 }
881
882 pub fn get(&self, name: &str) -> Option<Arc<dyn Extension>> {
886 self.entries
887 .get(name)
888 .map(|e| Arc::clone(&e.extension))
889 }
890
891 pub fn names(&self) -> impl Iterator<Item = &str> {
893 self.entries.keys().map(|s| s.as_str())
894 }
895
896 pub fn extensions(&self) -> impl Iterator<Item = &Arc<dyn Extension>> {
898 self.entries.values().map(|e| &e.extension)
899 }
900
901 pub fn manifest(&self, name: &str) -> Option<ExtensionManifest> {
903 self.entries.get(name).map(|e| e.extension.manifest())
904 }
905
906 pub fn len(&self) -> usize {
908 self.entries.len()
909 }
910
911 pub fn is_empty(&self) -> bool {
913 self.entries.is_empty()
914 }
915
916 pub fn errors(&self) -> Vec<ExtensionErrorRecord> {
918 self.errors.read().clone()
919 }
920
921 pub fn clear_errors(&self) {
923 self.errors.write().clear();
924 }
925
926 fn call_hook_safe<F>(&self, ext_name: &str, hook: &str, f: F)
932 where
933 F: FnOnce(),
934 {
935 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
936 if let Err(payload) = result {
937 let msg = if let Some(s) = payload.downcast_ref::<&str>() {
938 s.to_string()
939 } else if let Some(s) = payload.downcast_ref::<String>() {
940 s.clone()
941 } else {
942 "unknown panic".to_string()
943 };
944 tracing::error!(
945 extension = ext_name,
946 hook = hook,
947 error = %msg,
948 "Extension hook panicked — graceful degradation"
949 );
950 self.errors.write().push(ExtensionErrorRecord::new(
951 ext_name,
952 hook,
953 &format!("panic: {}", msg),
954 ));
955 }
956 }
957}
958
959impl fmt::Debug for ExtensionRegistry {
960 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
961 f.debug_struct("ExtensionRegistry")
962 .field("count", &self.entries.len())
963 .field(
964 "names",
965 &self.entries.keys().cloned().collect::<Vec<_>>(),
966 )
967 .finish()
968 }
969}
970
971const ENTRY_SYMBOL: &[u8] = b"oxi_extension_create\0";
977
978type CreateFn = unsafe fn() -> *mut dyn Extension;
986
987pub fn load_extension(path: &Path) -> Result<Arc<dyn Extension>> {
992 let extension = load_extension_inner(path)?;
993 Ok(extension)
994}
995
996fn load_extension_inner(path: &Path) -> Result<Arc<dyn Extension>> {
997 let ext = path.extension().and_then(OsStr::to_str).unwrap_or("");
999
1000 let valid = matches!(ext, "so" | "dylib" | "dll");
1001 if !valid {
1002 bail!(
1003 "Unsupported extension file format: .{}. Expected .so, .dylib, or .dll",
1004 ext
1005 );
1006 }
1007
1008 if !path.exists() {
1009 bail!("Extension file not found: {}", path.display());
1010 }
1011
1012 let library = unsafe {
1015 Library::new(path).with_context(|| format!("Failed to load library: {}", path.display()))?
1016 };
1017
1018 let create: Symbol<CreateFn> = unsafe {
1019 library.get(ENTRY_SYMBOL).with_context(|| {
1020 format!(
1021 "Symbol `oxi_extension_create` not found in {}",
1022 path.display()
1023 )
1024 })?
1025 };
1026
1027 let raw_ptr = unsafe { create() };
1028 if raw_ptr.is_null() {
1029 bail!("oxi_extension_create returned null in {}", path.display());
1030 }
1031
1032 let boxed: Box<dyn Extension> = unsafe { Box::from_raw(raw_ptr) };
1034 Ok(Arc::from(boxed))
1035}
1036
1037pub fn load_extensions(paths: &[&Path]) -> (Vec<Arc<dyn Extension>>, Vec<anyhow::Error>) {
1039 let mut loaded = Vec::with_capacity(paths.len());
1040 let mut errors = Vec::new();
1041
1042 for &path in paths {
1043 match load_extension(path) {
1044 Ok(ext) => loaded.push(ext),
1045 Err(e) => {
1046 errors.push(e.context(format!("Failed to load extension: {}", path.display())))
1047 }
1048 }
1049 }
1050
1051 (loaded, errors)
1052}
1053
1054pub struct NoopExtension;
1060
1061impl Extension for NoopExtension {
1062 fn name(&self) -> &str {
1063 "noop"
1064 }
1065
1066 fn description(&self) -> &str {
1067 "Built-in no-op extension"
1068 }
1069}
1070
1071#[cfg(test)]
1077pub struct RecordingExtension {
1078 pub name: String,
1079 pub calls: std::sync::Mutex<Vec<String>>,
1080}
1081
1082#[cfg(test)]
1083impl RecordingExtension {
1084 pub fn new(name: impl Into<String>) -> Self {
1085 Self {
1086 name: name.into(),
1087 calls: std::sync::Mutex::new(Vec::new()),
1088 }
1089 }
1090
1091 pub fn push(&self, call: &str) {
1092 self.calls.lock().unwrap().push(call.to_string());
1093 }
1094
1095 pub fn calls(&self) -> Vec<String> {
1096 self.calls.lock().unwrap().clone()
1097 }
1098}
1099
1100#[cfg(test)]
1101impl Extension for RecordingExtension {
1102 fn name(&self) -> &str {
1103 &self.name
1104 }
1105
1106 fn description(&self) -> &str {
1107 "recording test extension"
1108 }
1109
1110 fn on_load(&self, _ctx: &ExtensionContext) {
1111 self.push("on_load");
1112 }
1113
1114 fn on_unload(&self) {
1115 self.push("on_unload");
1116 }
1117
1118 fn on_message_sent(&self, msg: &str) {
1119 self.push(&format!("on_message_sent({})", msg));
1120 }
1121
1122 fn on_message_received(&self, msg: &str) {
1123 self.push(&format!("on_message_received({})", msg));
1124 }
1125
1126 fn on_tool_call(&self, tool: &str, _params: &Value) {
1127 self.push(&format!("on_tool_call({})", tool));
1128 }
1129
1130 fn on_tool_result(&self, tool: &str, _result: &AgentToolResult) {
1131 self.push(&format!("on_tool_result({})", tool));
1132 }
1133
1134 fn on_session_start(&self, session_id: &str) {
1135 self.push(&format!("on_session_start({})", session_id));
1136 }
1137
1138 fn on_session_end(&self, session_id: &str) {
1139 self.push(&format!("on_session_end({})", session_id));
1140 }
1141
1142 fn on_settings_changed(&self, _settings: &crate::settings::Settings) {
1143 self.push("on_settings_changed");
1144 }
1145
1146 fn on_event(&self, _event: &AgentEvent) {
1147 self.push("on_event");
1148 }
1149}
1150
1151#[cfg(test)]
1156mod tests {
1157 use super::*;
1158 use crate::settings::Settings;
1159
1160 #[test]
1163 fn test_manifest_builder() {
1164 let manifest = ExtensionManifest::new("my-ext", "1.0.0")
1165 .with_description("A test extension")
1166 .with_author("test-author")
1167 .with_permission(ExtensionPermission::FileRead)
1168 .with_permission(ExtensionPermission::Bash)
1169 .with_config_schema(serde_json::json!({
1170 "type": "object",
1171 "properties": {
1172 "api_key": { "type": "string" }
1173 }
1174 }));
1175
1176 assert_eq!(manifest.name, "my-ext");
1177 assert_eq!(manifest.version, "1.0.0");
1178 assert_eq!(manifest.description, "A test extension");
1179 assert_eq!(manifest.author, "test-author");
1180 assert!(manifest.has_permission(ExtensionPermission::FileRead));
1181 assert!(manifest.has_permission(ExtensionPermission::Bash));
1182 assert!(!manifest.has_permission(ExtensionPermission::Network));
1183 assert!(manifest.config_schema.is_some());
1184 }
1185
1186 #[test]
1187 fn test_manifest_serialization() {
1188 let manifest = ExtensionManifest::new("test", "0.1.0")
1189 .with_permission(ExtensionPermission::Network);
1190
1191 let json = serde_json::to_string(&manifest).unwrap();
1192 let parsed: ExtensionManifest = serde_json::from_str(&json).unwrap();
1193 assert_eq!(parsed.name, "test");
1194 assert_eq!(parsed.version, "0.1.0");
1195 assert!(parsed.has_permission(ExtensionPermission::Network));
1196 }
1197
1198 #[test]
1199 fn test_permission_display() {
1200 assert_eq!(ExtensionPermission::FileRead.to_string(), "file_read");
1201 assert_eq!(ExtensionPermission::FileWrite.to_string(), "file_write");
1202 assert_eq!(ExtensionPermission::Bash.to_string(), "bash");
1203 assert_eq!(ExtensionPermission::Network.to_string(), "network");
1204 }
1205
1206 #[test]
1209 fn test_extension_error_display() {
1210 let err = ExtensionError::NotFound {
1211 name: "test".to_string(),
1212 };
1213 assert!(err.to_string().contains("test"));
1214 assert!(err.to_string().contains("not found"));
1215
1216 let err = ExtensionError::LoadFailed {
1217 name: "bad".to_string(),
1218 reason: "missing symbol".to_string(),
1219 };
1220 assert!(err.to_string().contains("bad"));
1221 assert!(err.to_string().contains("missing symbol"));
1222
1223 let err = ExtensionError::HookFailed {
1224 name: "ext".to_string(),
1225 hook: "on_load".to_string(),
1226 error: "boom".to_string(),
1227 };
1228 assert!(err.to_string().contains("on_load"));
1229
1230 let err = ExtensionError::PermissionDenied {
1231 name: "ext".to_string(),
1232 permission: ExtensionPermission::Network,
1233 };
1234 assert!(err.to_string().contains("network"));
1235
1236 let err = ExtensionError::Disabled {
1237 name: "ext".to_string(),
1238 };
1239 assert!(err.to_string().contains("disabled"));
1240
1241 let err = ExtensionError::HotReloadFailed {
1242 name: "ext".to_string(),
1243 reason: "no path".to_string(),
1244 };
1245 assert!(err.to_string().contains("Hot-reload"));
1246 }
1247
1248 #[test]
1249 fn test_error_record() {
1250 let record = ExtensionErrorRecord::new("my-ext", "on_load", "something broke");
1251 assert_eq!(record.extension_name, "my-ext");
1252 assert_eq!(record.event, "on_load");
1253 assert_eq!(record.error, "something broke");
1254 assert!(record.timestamp > 0);
1255 }
1256
1257 #[test]
1258 fn test_error_record_serialization() {
1259 let record = ExtensionErrorRecord::new("ext", "hook", "err");
1260 let json = serde_json::to_string(&record).unwrap();
1261 let parsed: ExtensionErrorRecord = serde_json::from_str(&json).unwrap();
1262 assert_eq!(parsed.extension_name, "ext");
1263 assert_eq!(parsed.event, "hook");
1264 }
1265
1266 #[test]
1269 fn test_context_builder_minimal() {
1270 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp"))
1271 .build();
1272 assert_eq!(ctx.cwd, PathBuf::from("/tmp"));
1273 assert!(ctx.session_id.is_none());
1274 assert!(ctx.is_idle());
1275 }
1276
1277 #[test]
1278 fn test_context_builder_full() {
1279 let settings = Arc::new(RwLock::new(Settings::default()));
1280 let errors = Arc::new(RwLock::new(Vec::new()));
1281 let tools_registered = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
1282 let messages_sent = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
1283 let tools_ref = tools_registered.clone();
1284 let msgs_ref = messages_sent.clone();
1285
1286 let ctx = ExtensionContextBuilder::new(PathBuf::from("/home"))
1287 .settings(settings)
1288 .config(serde_json::json!({"key": "value"}))
1289 .session_id("sess-123")
1290 .errors(errors)
1291 .tool_registrar(Arc::new(move |tool: Arc<dyn AgentTool>| {
1292 tools_ref.lock().unwrap().push(tool.name().to_string());
1293 }))
1294 .message_sender(Arc::new(move |msg: &str| {
1295 msgs_ref.lock().unwrap().push(msg.to_string());
1296 }))
1297 .build();
1298
1299 assert_eq!(ctx.cwd, PathBuf::from("/home"));
1300 assert_eq!(ctx.session_id, Some("sess-123".to_string()));
1301 assert!(ctx.is_idle());
1302
1303 assert_eq!(
1305 ctx.config_get("key"),
1306 Some(serde_json::json!("value"))
1307 );
1308 assert_eq!(ctx.config_get("missing"), None);
1309 }
1310
1311 #[test]
1312 fn test_context_config_nested() {
1313 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp"))
1314 .config(serde_json::json!({
1315 "database": {
1316 "host": "localhost",
1317 "port": 5432
1318 }
1319 }))
1320 .build();
1321
1322 assert_eq!(
1323 ctx.config_get("database.host"),
1324 Some(serde_json::json!("localhost"))
1325 );
1326 assert_eq!(
1327 ctx.config_get("database.port"),
1328 Some(serde_json::json!(5432))
1329 );
1330 assert_eq!(ctx.config_get("database.missing"), None);
1331 }
1332
1333 #[test]
1334 fn test_context_tool_registration() {
1335 let registered = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
1336 let reg_ref = registered.clone();
1337
1338 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp"))
1339 .tool_registrar(Arc::new(move |tool: Arc<dyn AgentTool>| {
1340 reg_ref.lock().unwrap().push(tool.name().to_string());
1341 }))
1342 .build();
1343
1344 ctx.register_tool(Arc::new(oxi_agent::ReadTool::new()));
1346 assert_eq!(registered.lock().unwrap()[0], "read");
1347 }
1348
1349 #[test]
1350 fn test_context_message_sending() {
1351 let sent = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
1352 let sent_ref = sent.clone();
1353
1354 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp"))
1355 .message_sender(Arc::new(move |msg: &str| {
1356 sent_ref.lock().unwrap().push(msg.to_string());
1357 }))
1358 .build();
1359
1360 ctx.send_message("hello");
1361 ctx.send_message("world");
1362 assert_eq!(*sent.lock().unwrap(), vec!["hello", "world"]);
1363 }
1364
1365 #[test]
1366 fn test_context_error_recording() {
1367 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp")).build();
1368 assert!(ctx.errors().is_empty());
1369
1370 ctx.record_error("ext1", "on_load", "fail");
1371 ctx.record_error("ext2", "on_tool_call", "oops");
1372
1373 let errs = ctx.errors();
1374 assert_eq!(errs.len(), 2);
1375 assert_eq!(errs[0].extension_name, "ext1");
1376 assert_eq!(errs[1].extension_name, "ext2");
1377
1378 ctx.clear_errors();
1379 assert!(ctx.errors().is_empty());
1380 }
1381
1382 #[test]
1383 fn test_context_settings() {
1384 let settings = Arc::new(RwLock::new(Settings::default()));
1385 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp"))
1386 .settings(settings.clone())
1387 .build();
1388
1389 let s = ctx.settings();
1390 assert_eq!(s.version, Settings::default().version);
1391 }
1392
1393 #[test]
1394 fn test_context_noop_callbacks() {
1395 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp")).build();
1397 ctx.register_tool(Arc::new(oxi_agent::ReadTool::new()));
1398 ctx.send_message("test");
1399 }
1400
1401 #[test]
1404 fn test_registry_register_and_collect() {
1405 let mut reg = ExtensionRegistry::new();
1406 reg.register(Arc::new(NoopExtension));
1407
1408 assert_eq!(reg.len(), 1);
1409 assert!(!reg.is_empty());
1410 assert!(reg.all_tools().is_empty());
1411 assert!(reg.all_commands().is_empty());
1412 }
1413
1414 #[test]
1415 fn test_registry_names() {
1416 let mut reg = ExtensionRegistry::new();
1417 reg.register(Arc::new(NoopExtension));
1418 let names: Vec<&str> = reg.names().collect();
1419 assert_eq!(names, vec!["noop"]);
1420 }
1421
1422 #[test]
1423 fn test_registry_get() {
1424 let mut reg = ExtensionRegistry::new();
1425 reg.register(Arc::new(NoopExtension));
1426
1427 assert!(reg.get("noop").is_some());
1428 assert!(reg.get("nonexistent").is_none());
1429 }
1430
1431 #[test]
1432 fn test_registry_manifest() {
1433 let mut reg = ExtensionRegistry::new();
1434 reg.register(Arc::new(NoopExtension));
1435
1436 let m = reg.manifest("noop").unwrap();
1437 assert_eq!(m.name, "noop");
1438 assert!(reg.manifest("missing").is_none());
1439 }
1440
1441 #[test]
1442 fn test_registry_unregister() {
1443 let mut reg = ExtensionRegistry::new();
1444 reg.register(Arc::new(NoopExtension));
1445 assert_eq!(reg.len(), 1);
1446
1447 assert!(reg.unregister("noop"));
1448 assert!(reg.is_empty());
1449 assert!(!reg.unregister("noop")); }
1451
1452 #[test]
1455 fn test_registry_enable_disable() {
1456 let mut reg = ExtensionRegistry::new();
1457 let ext = Arc::new(RecordingExtension::new("rec"));
1458 reg.register(ext);
1459
1460 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp")).build();
1461
1462 assert!(reg.is_enabled("rec"));
1464
1465 reg.disable("rec").unwrap();
1467 assert!(!reg.is_enabled("rec"));
1468
1469 assert!(reg.all_tools().is_empty());
1471
1472 reg.enable("rec", &ctx).unwrap();
1474 assert!(reg.is_enabled("rec"));
1475 }
1476
1477 #[test]
1478 fn test_registry_disable_not_found() {
1479 let mut reg = ExtensionRegistry::new();
1480 let result = reg.disable("nonexistent");
1481 assert!(result.is_err());
1482 match result {
1483 Err(ExtensionError::NotFound { name }) => assert_eq!(name, "nonexistent"),
1484 _ => panic!("Expected NotFound error"),
1485 }
1486 }
1487
1488 #[test]
1489 fn test_registry_enable_not_found() {
1490 let mut reg = ExtensionRegistry::new();
1491 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp")).build();
1492 let result = reg.enable("nonexistent", &ctx);
1493 assert!(result.is_err());
1494 }
1495
1496 #[test]
1497 fn test_registry_disable_already_disabled() {
1498 let mut reg = ExtensionRegistry::new();
1499 reg.register(Arc::new(NoopExtension));
1500 reg.disable("noop").unwrap();
1501 reg.disable("noop").unwrap();
1503 assert!(!reg.is_enabled("noop"));
1504 }
1505
1506 #[test]
1507 fn test_registry_enable_already_enabled() {
1508 let mut reg = ExtensionRegistry::new();
1509 reg.register(Arc::new(NoopExtension));
1510 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp")).build();
1511 reg.enable("noop", &ctx).unwrap();
1513 assert!(reg.is_enabled("noop"));
1514 }
1515
1516 #[test]
1519 fn test_emit_load() {
1520 let mut reg = ExtensionRegistry::new();
1521 let ext = Arc::new(RecordingExtension::new("rec"));
1522 reg.register(ext.clone());
1523 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp")).build();
1524
1525 reg.emit_load(&ctx);
1526 assert_eq!(ext.calls(), vec!["on_load"]);
1527 }
1528
1529 #[test]
1530 fn test_emit_unload() {
1531 let mut reg = ExtensionRegistry::new();
1532 let ext = Arc::new(RecordingExtension::new("rec"));
1533 reg.register(ext.clone());
1534
1535 reg.emit_unload();
1536 assert_eq!(ext.calls(), vec!["on_unload"]);
1537 }
1538
1539 #[test]
1540 fn test_emit_message_sent() {
1541 let mut reg = ExtensionRegistry::new();
1542 let ext = Arc::new(RecordingExtension::new("rec"));
1543 reg.register(ext.clone());
1544
1545 reg.emit_message_sent("hello");
1546 assert_eq!(ext.calls(), vec!["on_message_sent(hello)"]);
1547 }
1548
1549 #[test]
1550 fn test_emit_message_received() {
1551 let mut reg = ExtensionRegistry::new();
1552 let ext = Arc::new(RecordingExtension::new("rec"));
1553 reg.register(ext.clone());
1554
1555 reg.emit_message_received("world");
1556 assert_eq!(ext.calls(), vec!["on_message_received(world)"]);
1557 }
1558
1559 #[test]
1560 fn test_emit_tool_call() {
1561 let mut reg = ExtensionRegistry::new();
1562 let ext = Arc::new(RecordingExtension::new("rec"));
1563 reg.register(ext.clone());
1564
1565 reg.emit_tool_call("bash", &serde_json::json!({"command": "ls"}));
1566 assert_eq!(ext.calls(), vec!["on_tool_call(bash)"]);
1567 }
1568
1569 #[test]
1570 fn test_emit_tool_result() {
1571 let mut reg = ExtensionRegistry::new();
1572 let ext = Arc::new(RecordingExtension::new("rec"));
1573 reg.register(ext.clone());
1574
1575 let result = AgentToolResult::success("done");
1576 reg.emit_tool_result("bash", &result);
1577 assert_eq!(ext.calls(), vec!["on_tool_result(bash)"]);
1578 }
1579
1580 #[test]
1581 fn test_emit_session_start() {
1582 let mut reg = ExtensionRegistry::new();
1583 let ext = Arc::new(RecordingExtension::new("rec"));
1584 reg.register(ext.clone());
1585
1586 reg.emit_session_start("sess-1");
1587 assert_eq!(ext.calls(), vec!["on_session_start(sess-1)"]);
1588 }
1589
1590 #[test]
1591 fn test_emit_session_end() {
1592 let mut reg = ExtensionRegistry::new();
1593 let ext = Arc::new(RecordingExtension::new("rec"));
1594 reg.register(ext.clone());
1595
1596 reg.emit_session_end("sess-1");
1597 assert_eq!(ext.calls(), vec!["on_session_end(sess-1)"]);
1598 }
1599
1600 #[test]
1601 fn test_emit_settings_changed() {
1602 let mut reg = ExtensionRegistry::new();
1603 let ext = Arc::new(RecordingExtension::new("rec"));
1604 reg.register(ext.clone());
1605
1606 let settings = Settings::default();
1607 reg.emit_settings_changed(&settings);
1608 assert_eq!(ext.calls(), vec!["on_settings_changed"]);
1609 }
1610
1611 #[test]
1612 fn test_emit_event() {
1613 let mut reg = ExtensionRegistry::new();
1614 let ext = Arc::new(RecordingExtension::new("rec"));
1615 reg.register(ext.clone());
1616
1617 reg.emit_event(&AgentEvent::Thinking);
1618 assert_eq!(ext.calls(), vec!["on_event"]);
1619 }
1620
1621 #[test]
1624 fn test_disabled_extension_skips_broadcasts() {
1625 let mut reg = ExtensionRegistry::new();
1626 let ext = Arc::new(RecordingExtension::new("rec"));
1627 reg.register(ext.clone());
1628 reg.disable("rec").unwrap();
1629
1630 {
1632 let mut calls = ext.calls.lock().unwrap();
1633 calls.clear();
1634 }
1635
1636 reg.emit_message_sent("hello");
1637 reg.emit_event(&AgentEvent::Thinking);
1638 reg.emit_session_start("s1");
1639
1640 assert!(ext.calls().is_empty());
1642 }
1643
1644 #[test]
1647 fn test_graceful_degradation_on_panic() {
1648 struct PanickingExtension;
1649 impl Extension for PanickingExtension {
1650 fn name(&self) -> &str { "panicker" }
1651 fn description(&self) -> &str { "Panics" }
1652 fn on_load(&self, _ctx: &ExtensionContext) {
1653 panic!("intentional panic in on_load");
1654 }
1655 fn on_message_sent(&self, _msg: &str) {
1656 panic!("intentional panic in on_message_sent");
1657 }
1658 }
1659
1660 let mut reg = ExtensionRegistry::new();
1661 reg.register(Arc::new(PanickingExtension));
1662 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp")).build();
1663
1664 reg.emit_load(&ctx);
1666 reg.emit_message_sent("hello");
1667
1668 let errors = reg.errors();
1670 assert_eq!(errors.len(), 2);
1671 assert_eq!(errors[0].event, "on_load");
1672 assert!(errors[0].error.contains("intentional panic"));
1673 assert_eq!(errors[1].event, "on_message_sent");
1674 }
1675
1676 #[test]
1679 fn test_command_new() {
1680 let cmd = Command::new("deploy", "Deploy the project", "/deploy <target>");
1681 assert_eq!(cmd.name, "deploy");
1682 assert_eq!(cmd.description, "Deploy the project");
1683 assert_eq!(cmd.usage, "/deploy <target>");
1684 }
1685
1686 #[test]
1689 fn test_load_extension_missing_file() {
1690 let result = load_extension(Path::new("/nonexistent/extension.so"));
1691 assert!(result.is_err());
1692 }
1693
1694 #[test]
1695 fn test_load_extension_wrong_extension() {
1696 let result = load_extension(Path::new("something.txt"));
1697 assert!(result.is_err());
1698 let msg = match result {
1699 Err(e) => e.to_string(),
1700 Ok(_) => panic!("Expected error"),
1701 };
1702 assert!(msg.contains("Unsupported extension file format"));
1703 }
1704
1705 #[test]
1706 fn test_load_extensions_collects_errors() {
1707 let paths: Vec<&Path> = vec![Path::new("/nonexistent1.so"), Path::new("/nonexistent2.so")];
1708 let (loaded, errors) = load_extensions(&paths);
1709 assert!(loaded.is_empty());
1710 assert_eq!(errors.len(), 2);
1711 }
1712
1713 #[test]
1716 fn test_registry_debug() {
1717 let reg = ExtensionRegistry::new();
1718 let debug_str = format!("{:?}", reg);
1719 assert!(debug_str.contains("count"));
1720 }
1721
1722 #[test]
1725 fn test_hot_reload_no_source_path() {
1726 let mut reg = ExtensionRegistry::new();
1727 reg.register(Arc::new(NoopExtension));
1728 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp")).build();
1729
1730 let result = reg.hot_reload("noop", &ctx);
1731 assert!(result.is_err());
1732 match result {
1733 Err(ExtensionError::HotReloadFailed { name, reason }) => {
1734 assert_eq!(name, "noop");
1735 assert!(reason.contains("no source path"));
1736 }
1737 _ => panic!("Expected HotReloadFailed error"),
1738 }
1739 }
1740
1741 #[test]
1742 fn test_hot_reload_not_found() {
1743 let mut reg = ExtensionRegistry::new();
1744 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp")).build();
1745
1746 let result = reg.hot_reload("nonexistent", &ctx);
1747 assert!(result.is_err());
1748 }
1749
1750 #[test]
1753 fn test_broadcast_to_multiple_extensions() {
1754 let mut reg = ExtensionRegistry::new();
1755 let ext1 = Arc::new(RecordingExtension::new("ext1"));
1756 let ext2 = Arc::new(RecordingExtension::new("ext2"));
1757 reg.register(ext1.clone());
1758 reg.register(ext2.clone());
1759
1760 reg.emit_message_sent("hello");
1761
1762 assert!(ext1.calls().contains(&"on_message_sent(hello)".to_string()));
1763 assert!(ext2.calls().contains(&"on_message_sent(hello)".to_string()));
1764 }
1765
1766 #[test]
1767 fn test_unregister_calls_on_unload() {
1768 let mut reg = ExtensionRegistry::new();
1769 let ext = Arc::new(RecordingExtension::new("rec"));
1770 reg.register(ext.clone());
1771
1772 reg.unregister("rec");
1773 assert_eq!(ext.calls(), vec!["on_unload"]);
1774 }
1775
1776 #[test]
1777 fn test_registry_errors() {
1778 let reg = ExtensionRegistry::new();
1779 assert!(reg.errors().is_empty());
1780 reg.clear_errors(); }
1782
1783 #[test]
1784 fn test_emit_event_does_not_panic() {
1785 let mut reg = ExtensionRegistry::new();
1786 reg.register(Arc::new(NoopExtension));
1787 reg.emit_event(&AgentEvent::Thinking);
1788 }
1789
1790 #[test]
1791 fn test_multiple_lifecycle_hooks() {
1792 let mut reg = ExtensionRegistry::new();
1793 let ext = Arc::new(RecordingExtension::new("rec"));
1794 reg.register(ext.clone());
1795
1796 let ctx = ExtensionContextBuilder::new(PathBuf::from("/tmp")).build();
1797 reg.emit_load(&ctx);
1798 reg.emit_session_start("s1");
1799 reg.emit_message_sent("hello");
1800 reg.emit_tool_call("bash", &serde_json::json!({}));
1801 let result = AgentToolResult::success("ok");
1802 reg.emit_tool_result("bash", &result);
1803 reg.emit_message_received("response");
1804 reg.emit_session_end("s1");
1805 reg.emit_unload();
1806
1807 let calls = ext.calls();
1808 assert!(calls.contains(&"on_load".to_string()));
1809 assert!(calls.contains(&"on_session_start(s1)".to_string()));
1810 assert!(calls.contains(&"on_message_sent(hello)".to_string()));
1811 assert!(calls.contains(&"on_tool_call(bash)".to_string()));
1812 assert!(calls.contains(&"on_tool_result(bash)".to_string()));
1813 assert!(calls.contains(&"on_message_received(response)".to_string()));
1814 assert!(calls.contains(&"on_session_end(s1)".to_string()));
1815 assert!(calls.contains(&"on_unload".to_string()));
1816 }
1817}