1#![deny(missing_docs)]
2pub mod bridge;
41#[cfg(feature = "sqlite")]
43pub mod database;
44pub mod error;
45pub mod js_bridge;
47pub mod mcp;
49mod memory;
50pub mod privacy;
52pub mod redaction;
54pub(crate) mod screenshot;
55mod tools;
56
57pub mod auth;
59pub mod introspection;
61
62use std::collections::{HashMap, HashSet};
63use std::sync::Arc;
64use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU64};
65use tauri::plugin::{Builder, TauriPlugin};
66use tauri::{Listener, Manager, RunEvent, Runtime};
67use tokio::sync::{Mutex, oneshot, watch};
68use victauri_core::{CommandRegistry, EventLog, EventRecorder};
69
70pub use error::BuilderError;
71pub use privacy::PrivacyProfile;
72
73pub use victauri_core::CommandInfo;
74pub use victauri_macros::inspectable;
75
76#[macro_export]
97macro_rules! register_commands {
98 ($app:expr, $($schema_call:expr),+ $(,)?) => {{
99 let state = $app.state::<std::sync::Arc<$crate::VictauriState>>();
100 $(
101 state.registry.register($schema_call);
102 )+
103 }};
104}
105
106const DEFAULT_PORT: u16 = 7373;
107const DEFAULT_EVENT_CAPACITY: usize = 10_000;
108const DEFAULT_RECORDER_CAPACITY: usize = 50_000;
109const DEFAULT_EVAL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
110const MAX_EVENT_CAPACITY: usize = 1_000_000;
111const MAX_RECORDER_CAPACITY: usize = 1_000_000;
112const MAX_EVAL_TIMEOUT_SECS: u64 = 300;
113
114pub type PendingCallbacks = Arc<Mutex<HashMap<String, oneshot::Sender<String>>>>;
117
118pub struct VictauriState {
120 pub event_log: EventLog,
122 pub registry: CommandRegistry,
124 pub port: AtomicU16,
126 pub pending_evals: PendingCallbacks,
128 pub recorder: EventRecorder,
130 pub privacy: privacy::PrivacyConfig,
132 pub eval_timeout: std::time::Duration,
134 pub shutdown_tx: watch::Sender<bool>,
136 pub started_at: std::time::Instant,
138 pub tool_invocations: AtomicU64,
140 pub allow_file_navigation: bool,
144 pub command_timings: introspection::CommandTimings,
146 pub fault_registry: introspection::FaultRegistry,
148 pub contract_store: introspection::ContractStore,
150 pub startup_timeline: introspection::StartupTimeline,
152 pub event_bus: introspection::EventBusMonitor,
154 pub task_tracker: introspection::TaskTracker,
156 pub bridge_ready: AtomicBool,
158 pub bridge_notify: tokio::sync::Notify,
160}
161
162pub struct VictauriBuilder {
174 port: Option<u16>,
175 event_capacity: usize,
176 recorder_capacity: usize,
177 eval_timeout: std::time::Duration,
178 auth_token: Option<String>,
179 auth_explicitly_enabled: bool,
180 auth_explicitly_disabled: bool,
181 disabled_tools: Vec<String>,
182 command_allowlist: Option<Vec<String>>,
183 command_blocklist: Vec<String>,
184 redaction_patterns: Vec<String>,
185 redaction_enabled: bool,
186 strict_privacy: bool,
187 privacy_profile: Option<privacy::PrivacyProfile>,
188 bridge_capacities: js_bridge::BridgeCapacities,
189 on_ready: Option<Box<dyn FnOnce(u16) + Send + 'static>>,
190 commands: Vec<victauri_core::CommandInfo>,
191 allow_file_navigation: bool,
192 listen_events: Vec<String>,
193}
194
195impl Default for VictauriBuilder {
196 fn default() -> Self {
197 Self {
198 port: None,
199 event_capacity: DEFAULT_EVENT_CAPACITY,
200 recorder_capacity: DEFAULT_RECORDER_CAPACITY,
201 eval_timeout: DEFAULT_EVAL_TIMEOUT,
202 auth_token: None,
203 auth_explicitly_enabled: false,
204 auth_explicitly_disabled: false,
205 disabled_tools: Vec::new(),
206 command_allowlist: None,
207 command_blocklist: Vec::new(),
208 redaction_patterns: Vec::new(),
209 redaction_enabled: false,
210 strict_privacy: false,
211 privacy_profile: None,
212 bridge_capacities: js_bridge::BridgeCapacities::default(),
213 on_ready: None,
214 commands: Vec::new(),
215 allow_file_navigation: false,
216 listen_events: Vec::new(),
217 }
218 }
219}
220
221impl VictauriBuilder {
222 #[must_use]
224 pub fn new() -> Self {
225 Self::default()
226 }
227
228 #[must_use]
230 pub fn port(mut self, port: u16) -> Self {
231 self.port = Some(port);
232 self
233 }
234
235 #[must_use]
237 pub fn event_capacity(mut self, capacity: usize) -> Self {
238 self.event_capacity = capacity;
239 self
240 }
241
242 #[must_use]
244 pub fn recorder_capacity(mut self, capacity: usize) -> Self {
245 self.recorder_capacity = capacity;
246 self
247 }
248
249 #[must_use]
251 pub fn eval_timeout(mut self, timeout: std::time::Duration) -> Self {
252 self.eval_timeout = timeout;
253 self
254 }
255
256 #[must_use]
260 pub fn auth_token(mut self, token: impl Into<String>) -> Self {
261 self.auth_token = Some(token.into());
262 self
263 }
264
265 #[must_use]
275 pub fn auth_enabled(mut self) -> Self {
276 self.auth_explicitly_enabled = true;
277 self
278 }
279
280 #[must_use]
282 pub fn generate_auth_token(mut self) -> Self {
283 self.auth_explicitly_enabled = true;
284 self
285 }
286
287 #[must_use]
297 pub fn auth_disabled(mut self) -> Self {
298 self.auth_explicitly_disabled = true;
299 self
300 }
301
302 #[must_use]
304 pub fn disable_tools(mut self, tools: &[&str]) -> Self {
305 self.disabled_tools = tools.iter().map(std::string::ToString::to_string).collect();
306 self
307 }
308
309 #[must_use]
311 pub fn command_allowlist(mut self, commands: &[&str]) -> Self {
312 self.command_allowlist = Some(
313 commands
314 .iter()
315 .map(std::string::ToString::to_string)
316 .collect(),
317 );
318 self
319 }
320
321 #[must_use]
323 pub fn command_blocklist(mut self, commands: &[&str]) -> Self {
324 self.command_blocklist = commands
325 .iter()
326 .map(std::string::ToString::to_string)
327 .collect();
328 self
329 }
330
331 #[must_use]
333 pub fn add_redaction_pattern(mut self, pattern: impl Into<String>) -> Self {
334 self.redaction_patterns.push(pattern.into());
335 self
336 }
337
338 #[must_use]
340 pub fn enable_redaction(mut self) -> Self {
341 self.redaction_enabled = true;
342 self
343 }
344
345 #[must_use]
352 pub fn strict_privacy_mode(mut self) -> Self {
353 self.strict_privacy = true;
354 self.privacy_profile = Some(privacy::PrivacyProfile::Observe);
355 self
356 }
357
358 #[must_use]
367 pub fn privacy_profile(mut self, profile: privacy::PrivacyProfile) -> Self {
368 self.privacy_profile = Some(profile);
369 if matches!(
370 profile,
371 privacy::PrivacyProfile::Observe | privacy::PrivacyProfile::Test
372 ) {
373 self.redaction_enabled = true;
374 }
375 self
376 }
377
378 #[must_use]
380 pub fn console_log_capacity(mut self, capacity: usize) -> Self {
381 self.bridge_capacities.console_logs = capacity;
382 self
383 }
384
385 #[must_use]
387 pub fn network_log_capacity(mut self, capacity: usize) -> Self {
388 self.bridge_capacities.network_log = capacity;
389 self
390 }
391
392 #[must_use]
394 pub fn navigation_log_capacity(mut self, capacity: usize) -> Self {
395 self.bridge_capacities.navigation_log = capacity;
396 self
397 }
398
399 #[must_use]
414 pub fn commands(mut self, schemas: &[victauri_core::CommandInfo]) -> Self {
415 self.commands = schemas.to_vec();
416 self
417 }
418
419 #[must_use]
435 pub fn register_command_names(mut self, names: &[&str]) -> Self {
436 self.commands
437 .extend(names.iter().map(|n| victauri_core::CommandInfo::new(*n)));
438 self
439 }
440
441 #[must_use]
457 pub fn auto_discover(mut self) -> Self {
458 self.commands
459 .extend(victauri_core::auto_discovered_commands());
460 self
461 }
462
463 #[must_use]
474 pub fn listen_events(mut self, events: &[&str]) -> Self {
475 self.listen_events = events
476 .iter()
477 .map(std::string::ToString::to_string)
478 .collect();
479 self
480 }
481
482 #[must_use]
491 pub fn allow_file_navigation(mut self) -> Self {
492 self.allow_file_navigation = true;
493 self
494 }
495
496 #[must_use]
499 pub fn on_ready(mut self, f: impl FnOnce(u16) + Send + 'static) -> Self {
500 self.on_ready = Some(Box::new(f));
501 self
502 }
503
504 fn resolve_port(&self) -> u16 {
505 self.port
506 .or_else(|| std::env::var("VICTAURI_PORT").ok()?.parse().ok())
507 .unwrap_or(DEFAULT_PORT)
508 }
509
510 fn resolve_auth_token(&self) -> Option<String> {
511 if self.auth_explicitly_disabled {
512 return None;
513 }
514 if let Some(ref token) = self.auth_token {
515 return Some(token.clone());
516 }
517 if let Ok(token) = std::env::var("VICTAURI_AUTH_TOKEN") {
518 return Some(token);
519 }
520 Some(auth::generate_token())
521 }
522
523 fn resolve_eval_timeout(&self) -> std::time::Duration {
524 std::env::var("VICTAURI_EVAL_TIMEOUT")
525 .ok()
526 .and_then(|s| s.parse::<u64>().ok())
527 .map_or(self.eval_timeout, std::time::Duration::from_secs)
528 }
529
530 fn build_privacy_config(&self) -> privacy::PrivacyConfig {
531 let profile = self
532 .privacy_profile
533 .unwrap_or(privacy::PrivacyProfile::FullControl);
534
535 let redaction_enabled = self.redaction_enabled
536 || self.strict_privacy
537 || matches!(
538 profile,
539 privacy::PrivacyProfile::Observe | privacy::PrivacyProfile::Test
540 );
541
542 privacy::PrivacyConfig {
543 profile,
544 command_allowlist: self
545 .command_allowlist
546 .as_ref()
547 .map(|v| v.iter().cloned().collect::<HashSet<String>>()),
548 command_blocklist: self.command_blocklist.iter().cloned().collect(),
549 disabled_tools: self.disabled_tools.iter().cloned().collect(),
550 redactor: redaction::Redactor::new(&self.redaction_patterns),
551 redaction_enabled,
552 }
553 }
554
555 fn validate(&self) -> Result<(), BuilderError> {
556 let port = self.resolve_port();
557 if port == 0 {
558 return Err(BuilderError::InvalidPort {
559 port,
560 reason: "port 0 is reserved".to_string(),
561 });
562 }
563
564 if self.event_capacity == 0 || self.event_capacity > MAX_EVENT_CAPACITY {
565 return Err(BuilderError::InvalidEventCapacity {
566 capacity: self.event_capacity,
567 reason: format!("must be between 1 and {MAX_EVENT_CAPACITY}"),
568 });
569 }
570
571 if self.recorder_capacity == 0 || self.recorder_capacity > MAX_RECORDER_CAPACITY {
572 return Err(BuilderError::InvalidRecorderCapacity {
573 capacity: self.recorder_capacity,
574 reason: format!("must be between 1 and {MAX_RECORDER_CAPACITY}"),
575 });
576 }
577
578 let timeout = self.resolve_eval_timeout();
579 if timeout.as_secs() == 0 || timeout.as_secs() > MAX_EVAL_TIMEOUT_SECS {
580 return Err(BuilderError::InvalidEvalTimeout {
581 timeout_secs: timeout.as_secs(),
582 reason: format!("must be between 1 and {MAX_EVAL_TIMEOUT_SECS} seconds"),
583 });
584 }
585
586 Ok(())
587 }
588
589 pub fn build<R: Runtime>(self) -> Result<TauriPlugin<R>, BuilderError> {
599 #[cfg(not(debug_assertions))]
600 {
601 Ok(Builder::new("victauri").build())
602 }
603
604 #[cfg(debug_assertions)]
605 {
606 self.validate()?;
607
608 let port = self.resolve_port();
609 let event_capacity = self.event_capacity;
610 let recorder_capacity = self.recorder_capacity;
611 let eval_timeout = self.resolve_eval_timeout();
612 let auth_token = self.resolve_auth_token();
613 let privacy_config = self.build_privacy_config();
614 let allow_file_navigation = self.allow_file_navigation;
615 let on_ready = self.on_ready;
616 let commands = self.commands;
617 let listen_events = self.listen_events;
618 let js_init = js_bridge::init_script(&self.bridge_capacities);
619
620 Ok(Builder::new("victauri")
621 .setup(move |app, _api| {
622 let startup_timeline = introspection::StartupTimeline::new();
623 let event_log = EventLog::new(event_capacity);
624 startup_timeline.mark("event_log_created");
625 let registry = CommandRegistry::new();
626 startup_timeline.mark("registry_created");
627 let (shutdown_tx, shutdown_rx) = watch::channel(false);
628
629 let state = Arc::new(VictauriState {
630 event_log,
631 registry,
632 port: AtomicU16::new(port),
633 pending_evals: Arc::new(Mutex::new(HashMap::new())),
634 recorder: EventRecorder::new(recorder_capacity),
635 privacy: privacy_config,
636 eval_timeout,
637 shutdown_tx,
638 started_at: std::time::Instant::now(),
639 tool_invocations: AtomicU64::new(0),
640 allow_file_navigation,
641 command_timings: introspection::CommandTimings::new(),
642 fault_registry: introspection::FaultRegistry::new(),
643 contract_store: introspection::ContractStore::new(),
644 startup_timeline,
645 event_bus: introspection::EventBusMonitor::default(),
646 task_tracker: introspection::TaskTracker::new(),
647 bridge_ready: AtomicBool::new(false),
648 bridge_notify: tokio::sync::Notify::new(),
649 });
650 state.startup_timeline.mark("state_created");
651
652 app.manage(state.clone());
653
654 for cmd in commands {
655 state.registry.register(cmd);
656 }
657 state.startup_timeline.mark("commands_registered");
658
659 for event_name in &listen_events {
661 let bus = state.event_bus.clone();
662 let name = event_name.clone();
663 app.listen_any(event_name.clone(), move |event| {
664 let payload =
665 serde_json::from_str::<serde_json::Value>(event.payload())
666 .map_or_else(
667 |_| event.payload().to_string(),
668 |v| v.to_string(),
669 );
670 bus.push(introspection::CapturedTauriEvent {
671 name: name.clone(),
672 payload,
673 timestamp: chrono::Utc::now()
674 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
675 });
676 });
677 }
678 state
679 .startup_timeline
680 .mark("event_bus_listeners_registered");
681
682 if let Some(ref token) = auth_token {
683 let prefix_len = token.len().min(8);
684 let suffix_start = token.len().saturating_sub(4);
685 tracing::info!(
686 "Victauri MCP server auth enabled — token: {}…{}",
687 &token[..prefix_len],
688 &token[suffix_start..]
689 );
690 } else {
691 tracing::warn!(
692 "Victauri MCP server running WITHOUT auth — any localhost process can \
693 access all tools. Use VictauriBuilder::auth_enabled() or set \
694 VICTAURI_AUTH_TOKEN for shared/CI environments."
695 );
696 }
697
698 state.startup_timeline.mark("server_spawning");
699 let app_handle = app.clone();
700 let ready_state = state.clone();
701 let server_finished = state.task_tracker.track("mcp_server");
702 tauri::async_runtime::spawn(async move {
703 match mcp::start_server_with_options(
704 app_handle,
705 state,
706 port,
707 auth_token,
708 shutdown_rx,
709 )
710 .await
711 {
712 Ok(()) => {
713 tracing::info!("Victauri MCP server stopped");
714 }
715 Err(e) => {
716 tracing::error!("Victauri MCP server failed: {e}");
717 }
718 }
719 server_finished.store(true, std::sync::atomic::Ordering::Relaxed);
720 });
721
722 if let Some(cb) = on_ready {
723 let ready_finished = ready_state.task_tracker.track("on_ready_probe");
724 tauri::async_runtime::spawn(async move {
725 for _ in 0..50 {
726 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
727 let actual_port =
728 ready_state.port.load(std::sync::atomic::Ordering::Relaxed);
729 if tokio::net::TcpStream::connect(format!(
730 "127.0.0.1:{actual_port}"
731 ))
732 .await
733 .is_ok()
734 {
735 cb(actual_port);
736 ready_finished
737 .store(true, std::sync::atomic::Ordering::Relaxed);
738 return;
739 }
740 }
741 let actual_port =
742 ready_state.port.load(std::sync::atomic::Ordering::Relaxed);
743 tracing::warn!(
744 "Victauri on_ready: server did not become ready within 5s"
745 );
746 cb(actual_port);
747 ready_finished.store(true, std::sync::atomic::Ordering::Relaxed);
748 });
749 }
750
751 tracing::info!("Victauri plugin initialized — MCP server on port {port}");
752 Ok(())
753 })
754 .on_event(|app, event| {
755 let Some(state) = app.try_state::<Arc<VictauriState>>() else {
756 return;
757 };
758 match event {
759 RunEvent::Exit => {
760 let _ = state.shutdown_tx.send(true);
761 tracing::info!("Victauri shutdown signal sent");
762 }
763 RunEvent::ExitRequested { .. } => {
764 state.event_bus.push(introspection::CapturedTauriEvent {
765 name: "tauri://exit-requested".to_string(),
766 payload: String::new(),
767 timestamp: chrono::Utc::now()
768 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
769 });
770 }
771 RunEvent::WindowEvent {
772 label,
773 event: win_event,
774 ..
775 } => {
776 let (name, payload) = format_window_event(label, win_event);
777 state.event_bus.push(introspection::CapturedTauriEvent {
778 name,
779 payload,
780 timestamp: chrono::Utc::now()
781 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
782 });
783 }
784 _ => {}
785 }
786 })
787 .js_init_script(js_init)
788 .invoke_handler(tauri::generate_handler![
789 tools::victauri_eval_js,
790 tools::victauri_eval_callback,
791 tools::victauri_get_window_state,
792 tools::victauri_list_windows,
793 tools::victauri_get_ipc_log,
794 tools::victauri_get_registry,
795 tools::victauri_get_memory_stats,
796 tools::victauri_dom_snapshot,
797 tools::victauri_verify_state,
798 tools::victauri_detect_ghost_commands,
799 tools::victauri_check_ipc_integrity,
800 ])
801 .build())
802 }
803 }
804}
805
806#[cfg(debug_assertions)]
807fn format_window_event(label: &str, event: &tauri::WindowEvent) -> (String, String) {
808 match event {
809 tauri::WindowEvent::Resized(size) => (
810 format!("window:{label}:resized"),
811 serde_json::json!({"width": size.width, "height": size.height}).to_string(),
812 ),
813 tauri::WindowEvent::Moved(pos) => (
814 format!("window:{label}:moved"),
815 serde_json::json!({"x": pos.x, "y": pos.y}).to_string(),
816 ),
817 tauri::WindowEvent::CloseRequested { .. } => {
818 (format!("window:{label}:close-requested"), String::new())
819 }
820 tauri::WindowEvent::Destroyed => (format!("window:{label}:destroyed"), String::new()),
821 tauri::WindowEvent::Focused(focused) => (
822 format!("window:{label}:focused"),
823 serde_json::json!({"focused": focused}).to_string(),
824 ),
825 tauri::WindowEvent::ScaleFactorChanged { scale_factor, .. } => (
826 format!("window:{label}:scale-factor-changed"),
827 serde_json::json!({"scale_factor": scale_factor}).to_string(),
828 ),
829 tauri::WindowEvent::ThemeChanged(theme) => (
830 format!("window:{label}:theme-changed"),
831 serde_json::json!({"theme": format!("{theme:?}")}).to_string(),
832 ),
833 tauri::WindowEvent::DragDrop(drag_event) => (
834 format!("window:{label}:drag-drop"),
835 format!("{drag_event:?}"),
836 ),
837 _ => (format!("window:{label}:other"), format!("{event:?}")),
838 }
839}
840
841#[must_use]
856pub fn init<R: Runtime>() -> TauriPlugin<R> {
857 VictauriBuilder::new()
858 .build()
859 .expect("default Victauri configuration is always valid")
860}
861
862#[must_use]
871pub fn init_auto_discover<R: Runtime>() -> TauriPlugin<R> {
872 VictauriBuilder::new()
873 .auto_discover()
874 .build()
875 .expect("default Victauri configuration is always valid")
876}
877
878#[cfg(test)]
879mod tests {
880 use super::*;
881
882 #[test]
883 fn builder_default_values() {
884 let builder = VictauriBuilder::new();
885 assert_eq!(builder.event_capacity, DEFAULT_EVENT_CAPACITY);
886 assert_eq!(builder.recorder_capacity, DEFAULT_RECORDER_CAPACITY);
887 assert!(builder.auth_token.is_none());
888 assert!(!builder.auth_explicitly_enabled);
889 assert!(!builder.auth_explicitly_disabled);
890 let resolved = builder.resolve_auth_token();
891 assert!(
892 resolved.is_some(),
893 "auth should be enabled by default (auto-generated token)"
894 );
895 assert_eq!(
896 resolved.unwrap().len(),
897 36,
898 "auto-generated token should be UUID v4"
899 );
900 assert!(builder.disabled_tools.is_empty());
901 assert!(builder.command_allowlist.is_none());
902 assert!(builder.command_blocklist.is_empty());
903 assert!(!builder.redaction_enabled);
904 assert!(!builder.strict_privacy);
905 }
906
907 #[test]
908 fn builder_port_override() {
909 let builder = VictauriBuilder::new().port(9090);
910 assert_eq!(builder.resolve_port(), 9090);
911 }
912
913 #[test]
914 #[allow(unsafe_code)]
915 fn builder_default_port() {
916 let builder = VictauriBuilder::new();
917 unsafe { std::env::remove_var("VICTAURI_PORT") };
919 assert_eq!(builder.resolve_port(), DEFAULT_PORT);
920 }
921
922 #[test]
923 fn builder_auth_token_explicit() {
924 let builder = VictauriBuilder::new().auth_token("my-secret");
925 assert_eq!(builder.resolve_auth_token(), Some("my-secret".to_string()));
926 }
927
928 #[test]
929 fn builder_auth_enabled() {
930 let builder = VictauriBuilder::new().auth_enabled();
931 assert!(builder.auth_explicitly_enabled);
932 let token = builder.resolve_auth_token().unwrap();
933 assert_eq!(token.len(), 36, "auto-generated token should be a UUID");
934 }
935
936 #[test]
937 fn builder_auth_generate_token() {
938 let builder = VictauriBuilder::new().generate_auth_token();
939 let token = builder.resolve_auth_token().unwrap();
940 assert_eq!(token.len(), 36);
941 }
942
943 #[test]
944 fn builder_auth_disabled_is_noop() {
945 let builder = VictauriBuilder::new().auth_disabled();
946 assert!(
947 builder.resolve_auth_token().is_none(),
948 "auth_disabled is a no-op, auth stays off by default"
949 );
950 }
951
952 #[test]
953 fn builder_auth_disabled_returns_none() {
954 let builder = VictauriBuilder::new().auth_disabled();
955 assert!(
956 builder.resolve_auth_token().is_none(),
957 "auth_disabled should suppress auto-generated token"
958 );
959 }
960
961 #[test]
962 fn builder_auth_disabled_overrides_explicit_token() {
963 let builder = VictauriBuilder::new()
964 .auth_token("my-secret")
965 .auth_disabled();
966 assert!(
967 builder.resolve_auth_token().is_none(),
968 "auth_disabled should override explicit token"
969 );
970 }
971
972 #[test]
973 fn builder_capacities() {
974 let builder = VictauriBuilder::new()
975 .event_capacity(500)
976 .recorder_capacity(2000);
977 assert_eq!(builder.event_capacity, 500);
978 assert_eq!(builder.recorder_capacity, 2000);
979 }
980
981 #[test]
982 fn builder_disable_tools() {
983 let builder = VictauriBuilder::new().disable_tools(&["eval_js", "screenshot"]);
984 assert_eq!(builder.disabled_tools.len(), 2);
985 assert!(builder.disabled_tools.contains(&"eval_js".to_string()));
986 }
987
988 #[test]
989 fn builder_command_allowlist() {
990 let builder = VictauriBuilder::new().command_allowlist(&["greet", "increment"]);
991 assert!(builder.command_allowlist.is_some());
992 assert_eq!(builder.command_allowlist.as_ref().unwrap().len(), 2);
993 }
994
995 #[test]
996 fn builder_command_blocklist() {
997 let builder = VictauriBuilder::new().command_blocklist(&["dangerous_cmd"]);
998 assert_eq!(builder.command_blocklist.len(), 1);
999 }
1000
1001 #[test]
1002 fn builder_redaction() {
1003 let builder = VictauriBuilder::new()
1004 .add_redaction_pattern(r"SECRET_\w+")
1005 .enable_redaction();
1006 assert!(builder.redaction_enabled);
1007 assert_eq!(builder.redaction_patterns.len(), 1);
1008 }
1009
1010 #[test]
1011 fn builder_strict_privacy_config() {
1012 let builder = VictauriBuilder::new().strict_privacy_mode();
1013 let config = builder.build_privacy_config();
1014 assert!(config.redaction_enabled);
1015 assert_eq!(config.profile, crate::privacy::PrivacyProfile::Observe);
1016 assert!(!config.is_tool_enabled("eval_js"));
1017 assert!(!config.is_tool_enabled("screenshot"));
1018 assert!(!config.is_tool_enabled("interact"));
1019 assert!(config.is_tool_enabled("dom_snapshot"));
1020 }
1021
1022 #[test]
1023 fn builder_normal_privacy_config() {
1024 let builder = VictauriBuilder::new()
1025 .command_blocklist(&["secret_cmd"])
1026 .disable_tools(&["eval_js"]);
1027 let config = builder.build_privacy_config();
1028 assert!(config.command_blocklist.contains("secret_cmd"));
1029 assert!(!config.is_tool_enabled("eval_js"));
1030 assert!(!config.redaction_enabled);
1031 }
1032
1033 #[test]
1034 fn builder_strict_with_extra_blocklist() {
1035 let builder = VictauriBuilder::new()
1036 .strict_privacy_mode()
1037 .command_blocklist(&["extra_dangerous"]);
1038 let config = builder.build_privacy_config();
1039 assert!(config.command_blocklist.contains("extra_dangerous"));
1040 assert!(!config.is_tool_enabled("eval_js"));
1041 }
1042
1043 #[test]
1044 fn builder_test_profile() {
1045 let builder = VictauriBuilder::new().privacy_profile(crate::privacy::PrivacyProfile::Test);
1046 let config = builder.build_privacy_config();
1047 assert_eq!(config.profile, crate::privacy::PrivacyProfile::Test);
1048 assert!(config.redaction_enabled);
1049 assert!(config.is_tool_enabled("interact"));
1050 assert!(config.is_tool_enabled("fill"));
1051 assert!(config.is_tool_enabled("recording"));
1052 assert!(!config.is_tool_enabled("eval_js"));
1053 assert!(!config.is_tool_enabled("screenshot"));
1054 assert!(!config.is_tool_enabled("navigate"));
1055 }
1056
1057 #[test]
1058 fn builder_profile_with_extra_disables() {
1059 let builder = VictauriBuilder::new()
1060 .privacy_profile(crate::privacy::PrivacyProfile::Test)
1061 .disable_tools(&["interact"]);
1062 let config = builder.build_privacy_config();
1063 assert!(!config.is_tool_enabled("interact"));
1064 assert!(config.is_tool_enabled("fill"));
1065 }
1066
1067 #[test]
1068 fn builder_bridge_capacities() {
1069 let builder = VictauriBuilder::new()
1070 .console_log_capacity(5000)
1071 .network_log_capacity(2000)
1072 .navigation_log_capacity(500);
1073 assert_eq!(builder.bridge_capacities.console_logs, 5000);
1074 assert_eq!(builder.bridge_capacities.network_log, 2000);
1075 assert_eq!(builder.bridge_capacities.navigation_log, 500);
1076 assert_eq!(builder.bridge_capacities.mutation_log, 500);
1077 assert_eq!(builder.bridge_capacities.dialog_log, 100);
1078 }
1079
1080 #[test]
1081 fn builder_on_ready_sets_callback() {
1082 let builder = VictauriBuilder::new().on_ready(|_port| {});
1083 assert!(builder.on_ready.is_some());
1084 }
1085
1086 #[test]
1087 fn builder_file_navigation_disabled_by_default() {
1088 let builder = VictauriBuilder::new();
1089 assert!(
1090 !builder.allow_file_navigation,
1091 "file navigation should be disabled by default"
1092 );
1093 }
1094
1095 #[test]
1096 fn builder_allow_file_navigation() {
1097 let builder = VictauriBuilder::new().allow_file_navigation();
1098 assert!(builder.allow_file_navigation);
1099 }
1100
1101 #[test]
1102 fn builder_listen_events() {
1103 let builder =
1104 VictauriBuilder::new().listen_events(&["notification-added", "settings-changed"]);
1105 assert_eq!(builder.listen_events.len(), 2);
1106 assert!(
1107 builder
1108 .listen_events
1109 .contains(&"notification-added".to_string())
1110 );
1111 assert!(
1112 builder
1113 .listen_events
1114 .contains(&"settings-changed".to_string())
1115 );
1116 }
1117
1118 #[test]
1119 fn builder_listen_events_empty_by_default() {
1120 let builder = VictauriBuilder::new();
1121 assert!(builder.listen_events.is_empty());
1122 }
1123
1124 #[test]
1125 fn init_script_contains_custom_capacities() {
1126 let caps = js_bridge::BridgeCapacities {
1127 console_logs: 3000,
1128 mutation_log: 750,
1129 network_log: 5000,
1130 navigation_log: 400,
1131 dialog_log: 250,
1132 long_tasks: 200,
1133 };
1134 let script = js_bridge::init_script(&caps);
1135 assert!(script.contains("CAP_CONSOLE = 3000"));
1136 assert!(script.contains("CAP_MUTATION = 750"));
1137 assert!(script.contains("CAP_NETWORK = 5000"));
1138 assert!(script.contains("CAP_NAVIGATION = 400"));
1139 assert!(script.contains("CAP_DIALOG = 250"));
1140 assert!(script.contains("CAP_LONG_TASKS = 200"));
1141 }
1142
1143 #[test]
1144 fn init_script_default_contains_standard_capacities() {
1145 let caps = js_bridge::BridgeCapacities::default();
1146 let script = js_bridge::init_script(&caps);
1147 assert!(script.contains("CAP_CONSOLE = 1000"));
1148 assert!(script.contains("CAP_NETWORK = 1000"));
1149 assert!(script.contains("window.__VICTAURI__"));
1150 }
1151
1152 #[test]
1153 fn builder_validates_defaults() {
1154 let builder = VictauriBuilder::new();
1155 assert!(builder.validate().is_ok());
1156 }
1157
1158 #[test]
1159 fn builder_rejects_zero_port() {
1160 let builder = VictauriBuilder::new().port(0);
1161 let err = builder.validate().unwrap_err();
1162 assert!(matches!(err, BuilderError::InvalidPort { port: 0, .. }));
1163 }
1164
1165 #[test]
1166 fn builder_rejects_zero_event_capacity() {
1167 let builder = VictauriBuilder::new().event_capacity(0);
1168 let err = builder.validate().unwrap_err();
1169 assert!(matches!(
1170 err,
1171 BuilderError::InvalidEventCapacity { capacity: 0, .. }
1172 ));
1173 }
1174
1175 #[test]
1176 fn builder_rejects_excessive_event_capacity() {
1177 let builder = VictauriBuilder::new().event_capacity(2_000_000);
1178 assert!(builder.validate().is_err());
1179 }
1180
1181 #[test]
1182 fn builder_rejects_zero_recorder_capacity() {
1183 let builder = VictauriBuilder::new().recorder_capacity(0);
1184 assert!(builder.validate().is_err());
1185 }
1186
1187 #[test]
1188 fn builder_rejects_zero_eval_timeout() {
1189 let builder = VictauriBuilder::new().eval_timeout(std::time::Duration::from_secs(0));
1190 assert!(builder.validate().is_err());
1191 }
1192
1193 #[test]
1194 fn builder_rejects_excessive_eval_timeout() {
1195 let builder = VictauriBuilder::new().eval_timeout(std::time::Duration::from_secs(600));
1196 assert!(builder.validate().is_err());
1197 }
1198
1199 #[test]
1200 fn builder_accepts_edge_values() {
1201 let builder = VictauriBuilder::new()
1202 .port(1)
1203 .event_capacity(1)
1204 .recorder_capacity(1)
1205 .eval_timeout(std::time::Duration::from_secs(1));
1206 assert!(builder.validate().is_ok());
1207
1208 let builder = VictauriBuilder::new()
1209 .port(65535)
1210 .event_capacity(MAX_EVENT_CAPACITY)
1211 .recorder_capacity(MAX_RECORDER_CAPACITY)
1212 .eval_timeout(std::time::Duration::from_secs(MAX_EVAL_TIMEOUT_SECS));
1213 assert!(builder.validate().is_ok());
1214 }
1215}