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 pub db_search_paths: Vec<std::path::PathBuf>,
167}
168
169pub struct VictauriBuilder {
181 port: Option<u16>,
182 event_capacity: usize,
183 recorder_capacity: usize,
184 eval_timeout: std::time::Duration,
185 auth_token: Option<String>,
186 auth_explicitly_enabled: bool,
187 auth_explicitly_disabled: bool,
188 disabled_tools: Vec<String>,
189 command_allowlist: Option<Vec<String>>,
190 command_blocklist: Vec<String>,
191 redaction_patterns: Vec<String>,
192 redaction_enabled: bool,
193 strict_privacy: bool,
194 privacy_profile: Option<privacy::PrivacyProfile>,
195 bridge_capacities: js_bridge::BridgeCapacities,
196 on_ready: Option<Box<dyn FnOnce(u16) + Send + 'static>>,
197 commands: Vec<victauri_core::CommandInfo>,
198 allow_file_navigation: bool,
199 listen_events: Vec<String>,
200 db_search_paths: Vec<std::path::PathBuf>,
201}
202
203impl Default for VictauriBuilder {
204 fn default() -> Self {
205 Self {
206 port: None,
207 event_capacity: DEFAULT_EVENT_CAPACITY,
208 recorder_capacity: DEFAULT_RECORDER_CAPACITY,
209 eval_timeout: DEFAULT_EVAL_TIMEOUT,
210 auth_token: None,
211 auth_explicitly_enabled: false,
212 auth_explicitly_disabled: false,
213 disabled_tools: Vec::new(),
214 command_allowlist: None,
215 command_blocklist: Vec::new(),
216 redaction_patterns: Vec::new(),
217 redaction_enabled: false,
218 strict_privacy: false,
219 privacy_profile: None,
220 bridge_capacities: js_bridge::BridgeCapacities::default(),
221 on_ready: None,
222 commands: Vec::new(),
223 allow_file_navigation: false,
224 listen_events: Vec::new(),
225 db_search_paths: Vec::new(),
226 }
227 }
228}
229
230impl VictauriBuilder {
231 #[must_use]
233 pub fn new() -> Self {
234 Self::default()
235 }
236
237 #[must_use]
239 pub fn port(mut self, port: u16) -> Self {
240 self.port = Some(port);
241 self
242 }
243
244 #[must_use]
246 pub fn event_capacity(mut self, capacity: usize) -> Self {
247 self.event_capacity = capacity;
248 self
249 }
250
251 #[must_use]
253 pub fn recorder_capacity(mut self, capacity: usize) -> Self {
254 self.recorder_capacity = capacity;
255 self
256 }
257
258 #[must_use]
260 pub fn eval_timeout(mut self, timeout: std::time::Duration) -> Self {
261 self.eval_timeout = timeout;
262 self
263 }
264
265 #[must_use]
269 pub fn auth_token(mut self, token: impl Into<String>) -> Self {
270 self.auth_token = Some(token.into());
271 self
272 }
273
274 #[must_use]
284 pub fn auth_enabled(mut self) -> Self {
285 self.auth_explicitly_enabled = true;
286 self
287 }
288
289 #[must_use]
291 pub fn generate_auth_token(mut self) -> Self {
292 self.auth_explicitly_enabled = true;
293 self
294 }
295
296 #[must_use]
306 pub fn auth_disabled(mut self) -> Self {
307 self.auth_explicitly_disabled = true;
308 self
309 }
310
311 #[must_use]
313 pub fn disable_tools(mut self, tools: &[&str]) -> Self {
314 self.disabled_tools = tools.iter().map(std::string::ToString::to_string).collect();
315 self
316 }
317
318 #[must_use]
320 pub fn command_allowlist(mut self, commands: &[&str]) -> Self {
321 self.command_allowlist = Some(
322 commands
323 .iter()
324 .map(std::string::ToString::to_string)
325 .collect(),
326 );
327 self
328 }
329
330 #[must_use]
332 pub fn command_blocklist(mut self, commands: &[&str]) -> Self {
333 self.command_blocklist = commands
334 .iter()
335 .map(std::string::ToString::to_string)
336 .collect();
337 self
338 }
339
340 #[must_use]
342 pub fn add_redaction_pattern(mut self, pattern: impl Into<String>) -> Self {
343 self.redaction_patterns.push(pattern.into());
344 self
345 }
346
347 #[must_use]
349 pub fn enable_redaction(mut self) -> Self {
350 self.redaction_enabled = true;
351 self
352 }
353
354 #[must_use]
361 pub fn strict_privacy_mode(mut self) -> Self {
362 self.strict_privacy = true;
363 self.privacy_profile = Some(privacy::PrivacyProfile::Observe);
364 self
365 }
366
367 #[must_use]
376 pub fn privacy_profile(mut self, profile: privacy::PrivacyProfile) -> Self {
377 self.privacy_profile = Some(profile);
378 if matches!(
379 profile,
380 privacy::PrivacyProfile::Observe | privacy::PrivacyProfile::Test
381 ) {
382 self.redaction_enabled = true;
383 }
384 self
385 }
386
387 #[must_use]
389 pub fn console_log_capacity(mut self, capacity: usize) -> Self {
390 self.bridge_capacities.console_logs = capacity;
391 self
392 }
393
394 #[must_use]
396 pub fn network_log_capacity(mut self, capacity: usize) -> Self {
397 self.bridge_capacities.network_log = capacity;
398 self
399 }
400
401 #[must_use]
403 pub fn navigation_log_capacity(mut self, capacity: usize) -> Self {
404 self.bridge_capacities.navigation_log = capacity;
405 self
406 }
407
408 #[must_use]
423 pub fn commands(mut self, schemas: &[victauri_core::CommandInfo]) -> Self {
424 self.commands = schemas.to_vec();
425 self
426 }
427
428 #[must_use]
444 pub fn register_command_names(mut self, names: &[&str]) -> Self {
445 self.commands
446 .extend(names.iter().map(|n| victauri_core::CommandInfo::new(*n)));
447 self
448 }
449
450 #[must_use]
466 pub fn auto_discover(mut self) -> Self {
467 self.commands
468 .extend(victauri_core::auto_discovered_commands());
469 self
470 }
471
472 #[must_use]
483 pub fn listen_events(mut self, events: &[&str]) -> Self {
484 self.listen_events = events
485 .iter()
486 .map(std::string::ToString::to_string)
487 .collect();
488 self
489 }
490
491 #[must_use]
500 pub fn allow_file_navigation(mut self) -> Self {
501 self.allow_file_navigation = true;
502 self
503 }
504
505 #[must_use]
518 pub fn db_search_paths<I, P>(mut self, paths: I) -> Self
519 where
520 I: IntoIterator<Item = P>,
521 P: Into<std::path::PathBuf>,
522 {
523 self.db_search_paths
524 .extend(paths.into_iter().map(Into::into));
525 self
526 }
527
528 #[must_use]
531 pub fn on_ready(mut self, f: impl FnOnce(u16) + Send + 'static) -> Self {
532 self.on_ready = Some(Box::new(f));
533 self
534 }
535
536 fn resolve_port(&self) -> u16 {
537 self.port
538 .or_else(|| std::env::var("VICTAURI_PORT").ok()?.parse().ok())
539 .unwrap_or(DEFAULT_PORT)
540 }
541
542 fn resolve_auth_token(&self) -> Option<String> {
543 if self.auth_explicitly_disabled {
544 return None;
545 }
546 if let Some(ref token) = self.auth_token {
547 return Some(token.clone());
548 }
549 if let Ok(token) = std::env::var("VICTAURI_AUTH_TOKEN") {
550 return Some(token);
551 }
552 Some(auth::generate_token())
553 }
554
555 fn resolve_eval_timeout(&self) -> std::time::Duration {
556 std::env::var("VICTAURI_EVAL_TIMEOUT")
557 .ok()
558 .and_then(|s| s.parse::<u64>().ok())
559 .map_or(self.eval_timeout, std::time::Duration::from_secs)
560 }
561
562 fn build_privacy_config(&self) -> privacy::PrivacyConfig {
563 let profile = self
564 .privacy_profile
565 .unwrap_or(privacy::PrivacyProfile::FullControl);
566
567 let redaction_enabled = self.redaction_enabled
568 || self.strict_privacy
569 || matches!(
570 profile,
571 privacy::PrivacyProfile::Observe | privacy::PrivacyProfile::Test
572 );
573
574 privacy::PrivacyConfig {
575 profile,
576 command_allowlist: self
577 .command_allowlist
578 .as_ref()
579 .map(|v| v.iter().cloned().collect::<HashSet<String>>()),
580 command_blocklist: self.command_blocklist.iter().cloned().collect(),
581 disabled_tools: self.disabled_tools.iter().cloned().collect(),
582 redactor: redaction::Redactor::new(&self.redaction_patterns),
583 redaction_enabled,
584 }
585 }
586
587 fn validate(&self) -> Result<(), BuilderError> {
588 let port = self.resolve_port();
589 if port == 0 {
590 return Err(BuilderError::InvalidPort {
591 port,
592 reason: "port 0 is reserved".to_string(),
593 });
594 }
595
596 if self.event_capacity == 0 || self.event_capacity > MAX_EVENT_CAPACITY {
597 return Err(BuilderError::InvalidEventCapacity {
598 capacity: self.event_capacity,
599 reason: format!("must be between 1 and {MAX_EVENT_CAPACITY}"),
600 });
601 }
602
603 if self.recorder_capacity == 0 || self.recorder_capacity > MAX_RECORDER_CAPACITY {
604 return Err(BuilderError::InvalidRecorderCapacity {
605 capacity: self.recorder_capacity,
606 reason: format!("must be between 1 and {MAX_RECORDER_CAPACITY}"),
607 });
608 }
609
610 let timeout = self.resolve_eval_timeout();
611 if timeout.as_secs() == 0 || timeout.as_secs() > MAX_EVAL_TIMEOUT_SECS {
612 return Err(BuilderError::InvalidEvalTimeout {
613 timeout_secs: timeout.as_secs(),
614 reason: format!("must be between 1 and {MAX_EVAL_TIMEOUT_SECS} seconds"),
615 });
616 }
617
618 Ok(())
619 }
620
621 pub fn build<R: Runtime>(self) -> Result<TauriPlugin<R>, BuilderError> {
631 #[cfg(not(debug_assertions))]
632 {
633 Ok(Builder::new("victauri").build())
634 }
635
636 #[cfg(debug_assertions)]
637 {
638 self.validate()?;
639
640 let port = self.resolve_port();
641 let event_capacity = self.event_capacity;
642 let recorder_capacity = self.recorder_capacity;
643 let eval_timeout = self.resolve_eval_timeout();
644 let auth_token = self.resolve_auth_token();
645 let privacy_config = self.build_privacy_config();
646 let allow_file_navigation = self.allow_file_navigation;
647 let db_search_paths = self.db_search_paths;
648 let on_ready = self.on_ready;
649 let commands = self.commands;
650 let listen_events = self.listen_events;
651 let js_init = js_bridge::init_script(&self.bridge_capacities);
652
653 Ok(Builder::new("victauri")
654 .setup(move |app, _api| {
655 let startup_timeline = introspection::StartupTimeline::new();
656 let event_log = EventLog::new(event_capacity);
657 startup_timeline.mark("event_log_created");
658 let registry = CommandRegistry::new();
659 startup_timeline.mark("registry_created");
660 let (shutdown_tx, shutdown_rx) = watch::channel(false);
661
662 let state = Arc::new(VictauriState {
663 event_log,
664 registry,
665 port: AtomicU16::new(port),
666 pending_evals: Arc::new(Mutex::new(HashMap::new())),
667 recorder: EventRecorder::new(recorder_capacity),
668 privacy: privacy_config,
669 eval_timeout,
670 shutdown_tx,
671 started_at: std::time::Instant::now(),
672 tool_invocations: AtomicU64::new(0),
673 allow_file_navigation,
674 command_timings: introspection::CommandTimings::new(),
675 fault_registry: introspection::FaultRegistry::new(),
676 contract_store: introspection::ContractStore::new(),
677 startup_timeline,
678 event_bus: introspection::EventBusMonitor::default(),
679 task_tracker: introspection::TaskTracker::new(),
680 bridge_ready: AtomicBool::new(false),
681 bridge_notify: tokio::sync::Notify::new(),
682 db_search_paths,
683 });
684 state.startup_timeline.mark("state_created");
685
686 app.manage(state.clone());
687
688 for cmd in commands {
689 state.registry.register(cmd);
690 }
691 state.startup_timeline.mark("commands_registered");
692
693 for event_name in &listen_events {
695 let bus = state.event_bus.clone();
696 let name = event_name.clone();
697 app.listen_any(event_name.clone(), move |event| {
698 let payload =
699 serde_json::from_str::<serde_json::Value>(event.payload())
700 .map_or_else(
701 |_| event.payload().to_string(),
702 |v| v.to_string(),
703 );
704 bus.push(introspection::CapturedTauriEvent {
705 name: name.clone(),
706 payload,
707 timestamp: chrono::Utc::now()
708 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
709 });
710 });
711 }
712 state
713 .startup_timeline
714 .mark("event_bus_listeners_registered");
715
716 if let Some(ref token) = auth_token {
717 let prefix_len = token.len().min(8);
718 let suffix_start = token.len().saturating_sub(4);
719 tracing::info!(
720 "Victauri MCP server auth enabled — token: {}…{}",
721 &token[..prefix_len],
722 &token[suffix_start..]
723 );
724 } else {
725 tracing::warn!(
726 "Victauri MCP server running WITHOUT auth — any localhost process can \
727 access all tools. Use VictauriBuilder::auth_enabled() or set \
728 VICTAURI_AUTH_TOKEN for shared/CI environments."
729 );
730 }
731
732 state.startup_timeline.mark("server_spawning");
733 let app_handle = app.clone();
734 let ready_state = state.clone();
735 let server_finished = state.task_tracker.track("mcp_server");
736 tauri::async_runtime::spawn(async move {
737 match mcp::start_server_with_options(
738 app_handle,
739 state,
740 port,
741 auth_token,
742 shutdown_rx,
743 )
744 .await
745 {
746 Ok(()) => {
747 tracing::info!("Victauri MCP server stopped");
748 }
749 Err(e) => {
750 tracing::error!("Victauri MCP server failed: {e}");
751 }
752 }
753 server_finished.store(true, std::sync::atomic::Ordering::Relaxed);
754 });
755
756 if let Some(cb) = on_ready {
757 let ready_finished = ready_state.task_tracker.track("on_ready_probe");
758 tauri::async_runtime::spawn(async move {
759 for _ in 0..50 {
760 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
761 let actual_port =
762 ready_state.port.load(std::sync::atomic::Ordering::Relaxed);
763 if tokio::net::TcpStream::connect(format!(
764 "127.0.0.1:{actual_port}"
765 ))
766 .await
767 .is_ok()
768 {
769 cb(actual_port);
770 ready_finished
771 .store(true, std::sync::atomic::Ordering::Relaxed);
772 return;
773 }
774 }
775 let actual_port =
776 ready_state.port.load(std::sync::atomic::Ordering::Relaxed);
777 tracing::warn!(
778 "Victauri on_ready: server did not become ready within 5s"
779 );
780 cb(actual_port);
781 ready_finished.store(true, std::sync::atomic::Ordering::Relaxed);
782 });
783 }
784
785 tracing::info!("Victauri plugin initialized — MCP server on port {port}");
786 Ok(())
787 })
788 .on_event(|app, event| {
789 let Some(state) = app.try_state::<Arc<VictauriState>>() else {
790 return;
791 };
792 match event {
793 RunEvent::Exit => {
794 let _ = state.shutdown_tx.send(true);
795 tracing::info!("Victauri shutdown signal sent");
796 }
797 RunEvent::ExitRequested { .. } => {
798 state.event_bus.push(introspection::CapturedTauriEvent {
799 name: "tauri://exit-requested".to_string(),
800 payload: String::new(),
801 timestamp: chrono::Utc::now()
802 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
803 });
804 }
805 RunEvent::WindowEvent {
806 label,
807 event: win_event,
808 ..
809 } => {
810 let (name, payload) = format_window_event(label, win_event);
811 state.event_bus.push(introspection::CapturedTauriEvent {
812 name,
813 payload,
814 timestamp: chrono::Utc::now()
815 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
816 });
817 }
818 _ => {}
819 }
820 })
821 .js_init_script(js_init)
822 .invoke_handler(tauri::generate_handler![
823 tools::victauri_eval_js,
824 tools::victauri_eval_callback,
825 tools::victauri_get_window_state,
826 tools::victauri_list_windows,
827 tools::victauri_get_ipc_log,
828 tools::victauri_get_registry,
829 tools::victauri_get_memory_stats,
830 tools::victauri_dom_snapshot,
831 tools::victauri_verify_state,
832 tools::victauri_detect_ghost_commands,
833 tools::victauri_check_ipc_integrity,
834 ])
835 .build())
836 }
837 }
838}
839
840#[cfg(debug_assertions)]
841fn format_window_event(label: &str, event: &tauri::WindowEvent) -> (String, String) {
842 match event {
843 tauri::WindowEvent::Resized(size) => (
844 format!("window:{label}:resized"),
845 serde_json::json!({"width": size.width, "height": size.height}).to_string(),
846 ),
847 tauri::WindowEvent::Moved(pos) => (
848 format!("window:{label}:moved"),
849 serde_json::json!({"x": pos.x, "y": pos.y}).to_string(),
850 ),
851 tauri::WindowEvent::CloseRequested { .. } => {
852 (format!("window:{label}:close-requested"), String::new())
853 }
854 tauri::WindowEvent::Destroyed => (format!("window:{label}:destroyed"), String::new()),
855 tauri::WindowEvent::Focused(focused) => (
856 format!("window:{label}:focused"),
857 serde_json::json!({"focused": focused}).to_string(),
858 ),
859 tauri::WindowEvent::ScaleFactorChanged { scale_factor, .. } => (
860 format!("window:{label}:scale-factor-changed"),
861 serde_json::json!({"scale_factor": scale_factor}).to_string(),
862 ),
863 tauri::WindowEvent::ThemeChanged(theme) => (
864 format!("window:{label}:theme-changed"),
865 serde_json::json!({"theme": format!("{theme:?}")}).to_string(),
866 ),
867 tauri::WindowEvent::DragDrop(drag_event) => (
868 format!("window:{label}:drag-drop"),
869 format!("{drag_event:?}"),
870 ),
871 _ => (format!("window:{label}:other"), format!("{event:?}")),
872 }
873}
874
875#[must_use]
890pub fn init<R: Runtime>() -> TauriPlugin<R> {
891 VictauriBuilder::new()
892 .build()
893 .expect("default Victauri configuration is always valid")
894}
895
896#[must_use]
905pub fn init_auto_discover<R: Runtime>() -> TauriPlugin<R> {
906 VictauriBuilder::new()
907 .auto_discover()
908 .build()
909 .expect("default Victauri configuration is always valid")
910}
911
912#[cfg(test)]
913mod tests {
914 use super::*;
915
916 #[test]
917 fn builder_default_values() {
918 let builder = VictauriBuilder::new();
919 assert_eq!(builder.event_capacity, DEFAULT_EVENT_CAPACITY);
920 assert_eq!(builder.recorder_capacity, DEFAULT_RECORDER_CAPACITY);
921 assert!(builder.auth_token.is_none());
922 assert!(!builder.auth_explicitly_enabled);
923 assert!(!builder.auth_explicitly_disabled);
924 let resolved = builder.resolve_auth_token();
925 assert!(
926 resolved.is_some(),
927 "auth should be enabled by default (auto-generated token)"
928 );
929 assert_eq!(
930 resolved.unwrap().len(),
931 36,
932 "auto-generated token should be UUID v4"
933 );
934 assert!(builder.disabled_tools.is_empty());
935 assert!(builder.command_allowlist.is_none());
936 assert!(builder.command_blocklist.is_empty());
937 assert!(!builder.redaction_enabled);
938 assert!(!builder.strict_privacy);
939 }
940
941 #[test]
942 fn builder_port_override() {
943 let builder = VictauriBuilder::new().port(9090);
944 assert_eq!(builder.resolve_port(), 9090);
945 }
946
947 #[test]
948 #[allow(unsafe_code)]
949 fn builder_default_port() {
950 let builder = VictauriBuilder::new();
951 unsafe { std::env::remove_var("VICTAURI_PORT") };
953 assert_eq!(builder.resolve_port(), DEFAULT_PORT);
954 }
955
956 #[test]
957 fn builder_auth_token_explicit() {
958 let builder = VictauriBuilder::new().auth_token("my-secret");
959 assert_eq!(builder.resolve_auth_token(), Some("my-secret".to_string()));
960 }
961
962 #[test]
963 fn builder_auth_enabled() {
964 let builder = VictauriBuilder::new().auth_enabled();
965 assert!(builder.auth_explicitly_enabled);
966 let token = builder.resolve_auth_token().unwrap();
967 assert_eq!(token.len(), 36, "auto-generated token should be a UUID");
968 }
969
970 #[test]
971 fn builder_auth_generate_token() {
972 let builder = VictauriBuilder::new().generate_auth_token();
973 let token = builder.resolve_auth_token().unwrap();
974 assert_eq!(token.len(), 36);
975 }
976
977 #[test]
978 fn builder_auth_disabled_is_noop() {
979 let builder = VictauriBuilder::new().auth_disabled();
980 assert!(
981 builder.resolve_auth_token().is_none(),
982 "auth_disabled is a no-op, auth stays off by default"
983 );
984 }
985
986 #[test]
987 fn builder_auth_disabled_returns_none() {
988 let builder = VictauriBuilder::new().auth_disabled();
989 assert!(
990 builder.resolve_auth_token().is_none(),
991 "auth_disabled should suppress auto-generated token"
992 );
993 }
994
995 #[test]
996 fn builder_auth_disabled_overrides_explicit_token() {
997 let builder = VictauriBuilder::new()
998 .auth_token("my-secret")
999 .auth_disabled();
1000 assert!(
1001 builder.resolve_auth_token().is_none(),
1002 "auth_disabled should override explicit token"
1003 );
1004 }
1005
1006 #[test]
1007 fn builder_capacities() {
1008 let builder = VictauriBuilder::new()
1009 .event_capacity(500)
1010 .recorder_capacity(2000);
1011 assert_eq!(builder.event_capacity, 500);
1012 assert_eq!(builder.recorder_capacity, 2000);
1013 }
1014
1015 #[test]
1016 fn builder_disable_tools() {
1017 let builder = VictauriBuilder::new().disable_tools(&["eval_js", "screenshot"]);
1018 assert_eq!(builder.disabled_tools.len(), 2);
1019 assert!(builder.disabled_tools.contains(&"eval_js".to_string()));
1020 }
1021
1022 #[test]
1023 fn builder_command_allowlist() {
1024 let builder = VictauriBuilder::new().command_allowlist(&["greet", "increment"]);
1025 assert!(builder.command_allowlist.is_some());
1026 assert_eq!(builder.command_allowlist.as_ref().unwrap().len(), 2);
1027 }
1028
1029 #[test]
1030 fn builder_command_blocklist() {
1031 let builder = VictauriBuilder::new().command_blocklist(&["dangerous_cmd"]);
1032 assert_eq!(builder.command_blocklist.len(), 1);
1033 }
1034
1035 #[test]
1036 fn builder_redaction() {
1037 let builder = VictauriBuilder::new()
1038 .add_redaction_pattern(r"SECRET_\w+")
1039 .enable_redaction();
1040 assert!(builder.redaction_enabled);
1041 assert_eq!(builder.redaction_patterns.len(), 1);
1042 }
1043
1044 #[test]
1045 fn builder_strict_privacy_config() {
1046 let builder = VictauriBuilder::new().strict_privacy_mode();
1047 let config = builder.build_privacy_config();
1048 assert!(config.redaction_enabled);
1049 assert_eq!(config.profile, crate::privacy::PrivacyProfile::Observe);
1050 assert!(!config.is_tool_enabled("eval_js"));
1051 assert!(!config.is_tool_enabled("screenshot"));
1052 assert!(!config.is_tool_enabled("interact"));
1053 assert!(config.is_tool_enabled("dom_snapshot"));
1054 }
1055
1056 #[test]
1057 fn builder_normal_privacy_config() {
1058 let builder = VictauriBuilder::new()
1059 .command_blocklist(&["secret_cmd"])
1060 .disable_tools(&["eval_js"]);
1061 let config = builder.build_privacy_config();
1062 assert!(config.command_blocklist.contains("secret_cmd"));
1063 assert!(!config.is_tool_enabled("eval_js"));
1064 assert!(!config.redaction_enabled);
1065 }
1066
1067 #[test]
1068 fn builder_strict_with_extra_blocklist() {
1069 let builder = VictauriBuilder::new()
1070 .strict_privacy_mode()
1071 .command_blocklist(&["extra_dangerous"]);
1072 let config = builder.build_privacy_config();
1073 assert!(config.command_blocklist.contains("extra_dangerous"));
1074 assert!(!config.is_tool_enabled("eval_js"));
1075 }
1076
1077 #[test]
1078 fn builder_test_profile() {
1079 let builder = VictauriBuilder::new().privacy_profile(crate::privacy::PrivacyProfile::Test);
1080 let config = builder.build_privacy_config();
1081 assert_eq!(config.profile, crate::privacy::PrivacyProfile::Test);
1082 assert!(config.redaction_enabled);
1083 assert!(config.is_tool_enabled("interact"));
1084 assert!(config.is_tool_enabled("fill"));
1085 assert!(config.is_tool_enabled("recording"));
1086 assert!(!config.is_tool_enabled("eval_js"));
1087 assert!(!config.is_tool_enabled("screenshot"));
1088 assert!(!config.is_tool_enabled("navigate"));
1089 }
1090
1091 #[test]
1092 fn builder_profile_with_extra_disables() {
1093 let builder = VictauriBuilder::new()
1094 .privacy_profile(crate::privacy::PrivacyProfile::Test)
1095 .disable_tools(&["interact"]);
1096 let config = builder.build_privacy_config();
1097 assert!(!config.is_tool_enabled("interact"));
1098 assert!(config.is_tool_enabled("fill"));
1099 }
1100
1101 #[test]
1102 fn builder_bridge_capacities() {
1103 let builder = VictauriBuilder::new()
1104 .console_log_capacity(5000)
1105 .network_log_capacity(2000)
1106 .navigation_log_capacity(500);
1107 assert_eq!(builder.bridge_capacities.console_logs, 5000);
1108 assert_eq!(builder.bridge_capacities.network_log, 2000);
1109 assert_eq!(builder.bridge_capacities.navigation_log, 500);
1110 assert_eq!(builder.bridge_capacities.mutation_log, 500);
1111 assert_eq!(builder.bridge_capacities.dialog_log, 100);
1112 }
1113
1114 #[test]
1115 fn builder_on_ready_sets_callback() {
1116 let builder = VictauriBuilder::new().on_ready(|_port| {});
1117 assert!(builder.on_ready.is_some());
1118 }
1119
1120 #[test]
1121 fn builder_file_navigation_disabled_by_default() {
1122 let builder = VictauriBuilder::new();
1123 assert!(
1124 !builder.allow_file_navigation,
1125 "file navigation should be disabled by default"
1126 );
1127 }
1128
1129 #[test]
1130 fn builder_allow_file_navigation() {
1131 let builder = VictauriBuilder::new().allow_file_navigation();
1132 assert!(builder.allow_file_navigation);
1133 }
1134
1135 #[test]
1136 fn builder_listen_events() {
1137 let builder =
1138 VictauriBuilder::new().listen_events(&["notification-added", "settings-changed"]);
1139 assert_eq!(builder.listen_events.len(), 2);
1140 assert!(
1141 builder
1142 .listen_events
1143 .contains(&"notification-added".to_string())
1144 );
1145 assert!(
1146 builder
1147 .listen_events
1148 .contains(&"settings-changed".to_string())
1149 );
1150 }
1151
1152 #[test]
1153 fn builder_listen_events_empty_by_default() {
1154 let builder = VictauriBuilder::new();
1155 assert!(builder.listen_events.is_empty());
1156 }
1157
1158 #[test]
1159 fn init_script_contains_custom_capacities() {
1160 let caps = js_bridge::BridgeCapacities {
1161 console_logs: 3000,
1162 mutation_log: 750,
1163 network_log: 5000,
1164 navigation_log: 400,
1165 dialog_log: 250,
1166 long_tasks: 200,
1167 };
1168 let script = js_bridge::init_script(&caps);
1169 assert!(script.contains("CAP_CONSOLE = 3000"));
1170 assert!(script.contains("CAP_MUTATION = 750"));
1171 assert!(script.contains("CAP_NETWORK = 5000"));
1172 assert!(script.contains("CAP_NAVIGATION = 400"));
1173 assert!(script.contains("CAP_DIALOG = 250"));
1174 assert!(script.contains("CAP_LONG_TASKS = 200"));
1175 }
1176
1177 #[test]
1178 fn init_script_default_contains_standard_capacities() {
1179 let caps = js_bridge::BridgeCapacities::default();
1180 let script = js_bridge::init_script(&caps);
1181 assert!(script.contains("CAP_CONSOLE = 1000"));
1182 assert!(script.contains("CAP_NETWORK = 1000"));
1183 assert!(script.contains("window.__VICTAURI__"));
1184 }
1185
1186 #[test]
1187 fn builder_validates_defaults() {
1188 let builder = VictauriBuilder::new();
1189 assert!(builder.validate().is_ok());
1190 }
1191
1192 #[test]
1193 fn builder_rejects_zero_port() {
1194 let builder = VictauriBuilder::new().port(0);
1195 let err = builder.validate().unwrap_err();
1196 assert!(matches!(err, BuilderError::InvalidPort { port: 0, .. }));
1197 }
1198
1199 #[test]
1200 fn builder_rejects_zero_event_capacity() {
1201 let builder = VictauriBuilder::new().event_capacity(0);
1202 let err = builder.validate().unwrap_err();
1203 assert!(matches!(
1204 err,
1205 BuilderError::InvalidEventCapacity { capacity: 0, .. }
1206 ));
1207 }
1208
1209 #[test]
1210 fn builder_rejects_excessive_event_capacity() {
1211 let builder = VictauriBuilder::new().event_capacity(2_000_000);
1212 assert!(builder.validate().is_err());
1213 }
1214
1215 #[test]
1216 fn builder_rejects_zero_recorder_capacity() {
1217 let builder = VictauriBuilder::new().recorder_capacity(0);
1218 assert!(builder.validate().is_err());
1219 }
1220
1221 #[test]
1222 fn builder_rejects_zero_eval_timeout() {
1223 let builder = VictauriBuilder::new().eval_timeout(std::time::Duration::from_secs(0));
1224 assert!(builder.validate().is_err());
1225 }
1226
1227 #[test]
1228 fn builder_rejects_excessive_eval_timeout() {
1229 let builder = VictauriBuilder::new().eval_timeout(std::time::Duration::from_secs(600));
1230 assert!(builder.validate().is_err());
1231 }
1232
1233 #[test]
1234 fn builder_accepts_edge_values() {
1235 let builder = VictauriBuilder::new()
1236 .port(1)
1237 .event_capacity(1)
1238 .recorder_capacity(1)
1239 .eval_timeout(std::time::Duration::from_secs(1));
1240 assert!(builder.validate().is_ok());
1241
1242 let builder = VictauriBuilder::new()
1243 .port(65535)
1244 .event_capacity(MAX_EVENT_CAPACITY)
1245 .recorder_capacity(MAX_RECORDER_CAPACITY)
1246 .eval_timeout(std::time::Duration::from_secs(MAX_EVAL_TIMEOUT_SECS));
1247 assert!(builder.validate().is_ok());
1248 }
1249}