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