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