1#![deny(missing_docs)]
2pub mod bridge;
40#[cfg(feature = "sqlite")]
42pub mod database;
43pub mod error;
44pub mod js_bridge;
46pub mod mcp;
48mod memory;
49pub mod privacy;
51pub mod redaction;
53pub(crate) mod screenshot;
54mod tools;
55
56pub mod auth;
58pub mod introspection;
60
61use std::collections::{HashMap, HashSet};
62use std::sync::Arc;
63use std::sync::atomic::{AtomicU16, AtomicU64};
64use tauri::plugin::{Builder, TauriPlugin};
65use tauri::{Manager, RunEvent, Runtime};
66use tokio::sync::{Mutex, oneshot, watch};
67use victauri_core::{CommandRegistry, EventLog, EventRecorder};
68
69pub use error::BuilderError;
70pub use privacy::PrivacyProfile;
71
72pub use victauri_core::CommandInfo;
73pub use victauri_macros::inspectable;
74
75#[macro_export]
96macro_rules! register_commands {
97 ($app:expr, $($schema_call:expr),+ $(,)?) => {{
98 let state = $app.state::<std::sync::Arc<$crate::VictauriState>>();
99 $(
100 state.registry.register($schema_call);
101 )+
102 }};
103}
104
105const DEFAULT_PORT: u16 = 7373;
106const DEFAULT_EVENT_CAPACITY: usize = 10_000;
107const DEFAULT_RECORDER_CAPACITY: usize = 50_000;
108const DEFAULT_EVAL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
109const MAX_EVENT_CAPACITY: usize = 1_000_000;
110const MAX_RECORDER_CAPACITY: usize = 1_000_000;
111const MAX_EVAL_TIMEOUT_SECS: u64 = 300;
112
113pub type PendingCallbacks = Arc<Mutex<HashMap<String, oneshot::Sender<String>>>>;
116
117pub struct VictauriState {
119 pub event_log: EventLog,
121 pub registry: CommandRegistry,
123 pub port: AtomicU16,
125 pub pending_evals: PendingCallbacks,
127 pub recorder: EventRecorder,
129 pub privacy: privacy::PrivacyConfig,
131 pub eval_timeout: std::time::Duration,
133 pub shutdown_tx: watch::Sender<bool>,
135 pub started_at: std::time::Instant,
137 pub tool_invocations: AtomicU64,
139 pub allow_file_navigation: bool,
143 pub command_timings: introspection::CommandTimings,
145 pub fault_registry: introspection::FaultRegistry,
147 pub contract_store: introspection::ContractStore,
149 pub startup_timeline: introspection::StartupTimeline,
151 pub event_bus: introspection::EventBusMonitor,
153 pub task_tracker: introspection::TaskTracker,
155}
156
157pub struct VictauriBuilder {
169 port: Option<u16>,
170 event_capacity: usize,
171 recorder_capacity: usize,
172 eval_timeout: std::time::Duration,
173 auth_token: Option<String>,
174 auth_explicitly_enabled: bool,
175 disabled_tools: Vec<String>,
176 command_allowlist: Option<Vec<String>>,
177 command_blocklist: Vec<String>,
178 redaction_patterns: Vec<String>,
179 redaction_enabled: bool,
180 strict_privacy: bool,
181 privacy_profile: Option<privacy::PrivacyProfile>,
182 bridge_capacities: js_bridge::BridgeCapacities,
183 on_ready: Option<Box<dyn FnOnce(u16) + Send + 'static>>,
184 commands: Vec<victauri_core::CommandInfo>,
185 allow_file_navigation: bool,
186}
187
188impl Default for VictauriBuilder {
189 fn default() -> Self {
190 Self {
191 port: None,
192 event_capacity: DEFAULT_EVENT_CAPACITY,
193 recorder_capacity: DEFAULT_RECORDER_CAPACITY,
194 eval_timeout: DEFAULT_EVAL_TIMEOUT,
195 auth_token: None,
196 auth_explicitly_enabled: false,
197 disabled_tools: Vec::new(),
198 command_allowlist: None,
199 command_blocklist: Vec::new(),
200 redaction_patterns: Vec::new(),
201 redaction_enabled: false,
202 strict_privacy: false,
203 privacy_profile: None,
204 bridge_capacities: js_bridge::BridgeCapacities::default(),
205 on_ready: None,
206 commands: Vec::new(),
207 allow_file_navigation: false,
208 }
209 }
210}
211
212impl VictauriBuilder {
213 #[must_use]
215 pub fn new() -> Self {
216 Self::default()
217 }
218
219 #[must_use]
221 pub fn port(mut self, port: u16) -> Self {
222 self.port = Some(port);
223 self
224 }
225
226 #[must_use]
228 pub fn event_capacity(mut self, capacity: usize) -> Self {
229 self.event_capacity = capacity;
230 self
231 }
232
233 #[must_use]
235 pub fn recorder_capacity(mut self, capacity: usize) -> Self {
236 self.recorder_capacity = capacity;
237 self
238 }
239
240 #[must_use]
242 pub fn eval_timeout(mut self, timeout: std::time::Duration) -> Self {
243 self.eval_timeout = timeout;
244 self
245 }
246
247 #[must_use]
251 pub fn auth_token(mut self, token: impl Into<String>) -> Self {
252 self.auth_token = Some(token.into());
253 self
254 }
255
256 #[must_use]
266 pub fn auth_enabled(mut self) -> Self {
267 self.auth_explicitly_enabled = true;
268 self
269 }
270
271 #[must_use]
273 pub fn generate_auth_token(mut self) -> Self {
274 self.auth_explicitly_enabled = true;
275 self
276 }
277
278 #[must_use]
285 pub fn auth_disabled(self) -> Self {
286 self
287 }
288
289 #[must_use]
291 pub fn disable_tools(mut self, tools: &[&str]) -> Self {
292 self.disabled_tools = tools.iter().map(std::string::ToString::to_string).collect();
293 self
294 }
295
296 #[must_use]
298 pub fn command_allowlist(mut self, commands: &[&str]) -> Self {
299 self.command_allowlist = Some(
300 commands
301 .iter()
302 .map(std::string::ToString::to_string)
303 .collect(),
304 );
305 self
306 }
307
308 #[must_use]
310 pub fn command_blocklist(mut self, commands: &[&str]) -> Self {
311 self.command_blocklist = commands
312 .iter()
313 .map(std::string::ToString::to_string)
314 .collect();
315 self
316 }
317
318 #[must_use]
320 pub fn add_redaction_pattern(mut self, pattern: impl Into<String>) -> Self {
321 self.redaction_patterns.push(pattern.into());
322 self
323 }
324
325 #[must_use]
327 pub fn enable_redaction(mut self) -> Self {
328 self.redaction_enabled = true;
329 self
330 }
331
332 #[must_use]
339 pub fn strict_privacy_mode(mut self) -> Self {
340 self.strict_privacy = true;
341 self.privacy_profile = Some(privacy::PrivacyProfile::Observe);
342 self
343 }
344
345 #[must_use]
354 pub fn privacy_profile(mut self, profile: privacy::PrivacyProfile) -> Self {
355 self.privacy_profile = Some(profile);
356 if matches!(
357 profile,
358 privacy::PrivacyProfile::Observe | privacy::PrivacyProfile::Test
359 ) {
360 self.redaction_enabled = true;
361 }
362 self
363 }
364
365 #[must_use]
367 pub fn console_log_capacity(mut self, capacity: usize) -> Self {
368 self.bridge_capacities.console_logs = capacity;
369 self
370 }
371
372 #[must_use]
374 pub fn network_log_capacity(mut self, capacity: usize) -> Self {
375 self.bridge_capacities.network_log = capacity;
376 self
377 }
378
379 #[must_use]
381 pub fn navigation_log_capacity(mut self, capacity: usize) -> Self {
382 self.bridge_capacities.navigation_log = capacity;
383 self
384 }
385
386 #[must_use]
401 pub fn commands(mut self, schemas: &[victauri_core::CommandInfo]) -> Self {
402 self.commands = schemas.to_vec();
403 self
404 }
405
406 #[must_use]
422 pub fn register_command_names(mut self, names: &[&str]) -> Self {
423 self.commands
424 .extend(names.iter().map(|n| victauri_core::CommandInfo::new(*n)));
425 self
426 }
427
428 #[must_use]
444 pub fn auto_discover(mut self) -> Self {
445 self.commands
446 .extend(victauri_core::auto_discovered_commands());
447 self
448 }
449
450 #[must_use]
459 pub fn allow_file_navigation(mut self) -> Self {
460 self.allow_file_navigation = true;
461 self
462 }
463
464 #[must_use]
467 pub fn on_ready(mut self, f: impl FnOnce(u16) + Send + 'static) -> Self {
468 self.on_ready = Some(Box::new(f));
469 self
470 }
471
472 fn resolve_port(&self) -> u16 {
473 self.port
474 .or_else(|| std::env::var("VICTAURI_PORT").ok()?.parse().ok())
475 .unwrap_or(DEFAULT_PORT)
476 }
477
478 fn resolve_auth_token(&self) -> Option<String> {
479 if let Some(ref token) = self.auth_token {
480 return Some(token.clone());
481 }
482 if let Ok(token) = std::env::var("VICTAURI_AUTH_TOKEN") {
483 return Some(token);
484 }
485 if self.auth_explicitly_enabled {
486 return Some(auth::generate_token());
487 }
488 None
489 }
490
491 fn resolve_eval_timeout(&self) -> std::time::Duration {
492 std::env::var("VICTAURI_EVAL_TIMEOUT")
493 .ok()
494 .and_then(|s| s.parse::<u64>().ok())
495 .map_or(self.eval_timeout, std::time::Duration::from_secs)
496 }
497
498 fn build_privacy_config(&self) -> privacy::PrivacyConfig {
499 let profile = self
500 .privacy_profile
501 .unwrap_or(privacy::PrivacyProfile::FullControl);
502
503 let redaction_enabled = self.redaction_enabled
504 || self.strict_privacy
505 || matches!(
506 profile,
507 privacy::PrivacyProfile::Observe | privacy::PrivacyProfile::Test
508 );
509
510 privacy::PrivacyConfig {
511 profile,
512 command_allowlist: self
513 .command_allowlist
514 .as_ref()
515 .map(|v| v.iter().cloned().collect::<HashSet<String>>()),
516 command_blocklist: self.command_blocklist.iter().cloned().collect(),
517 disabled_tools: self.disabled_tools.iter().cloned().collect(),
518 redactor: redaction::Redactor::new(&self.redaction_patterns),
519 redaction_enabled,
520 }
521 }
522
523 fn validate(&self) -> Result<(), BuilderError> {
524 let port = self.resolve_port();
525 if port == 0 {
526 return Err(BuilderError::InvalidPort {
527 port,
528 reason: "port 0 is reserved".to_string(),
529 });
530 }
531
532 if self.event_capacity == 0 || self.event_capacity > MAX_EVENT_CAPACITY {
533 return Err(BuilderError::InvalidEventCapacity {
534 capacity: self.event_capacity,
535 reason: format!("must be between 1 and {MAX_EVENT_CAPACITY}"),
536 });
537 }
538
539 if self.recorder_capacity == 0 || self.recorder_capacity > MAX_RECORDER_CAPACITY {
540 return Err(BuilderError::InvalidRecorderCapacity {
541 capacity: self.recorder_capacity,
542 reason: format!("must be between 1 and {MAX_RECORDER_CAPACITY}"),
543 });
544 }
545
546 let timeout = self.resolve_eval_timeout();
547 if timeout.as_secs() == 0 || timeout.as_secs() > MAX_EVAL_TIMEOUT_SECS {
548 return Err(BuilderError::InvalidEvalTimeout {
549 timeout_secs: timeout.as_secs(),
550 reason: format!("must be between 1 and {MAX_EVAL_TIMEOUT_SECS} seconds"),
551 });
552 }
553
554 Ok(())
555 }
556
557 pub fn build<R: Runtime>(self) -> Result<TauriPlugin<R>, BuilderError> {
567 #[cfg(not(debug_assertions))]
568 {
569 Ok(Builder::new("victauri").build())
570 }
571
572 #[cfg(debug_assertions)]
573 {
574 self.validate()?;
575
576 let port = self.resolve_port();
577 let event_capacity = self.event_capacity;
578 let recorder_capacity = self.recorder_capacity;
579 let eval_timeout = self.resolve_eval_timeout();
580 let auth_token = self.resolve_auth_token();
581 let privacy_config = self.build_privacy_config();
582 let allow_file_navigation = self.allow_file_navigation;
583 let on_ready = self.on_ready;
584 let commands = self.commands;
585 let js_init = js_bridge::init_script(&self.bridge_capacities);
586
587 Ok(Builder::new("victauri")
588 .setup(move |app, _api| {
589 let startup_timeline = introspection::StartupTimeline::new();
590 let event_log = EventLog::new(event_capacity);
591 startup_timeline.mark("event_log_created");
592 let registry = CommandRegistry::new();
593 startup_timeline.mark("registry_created");
594 let (shutdown_tx, shutdown_rx) = watch::channel(false);
595
596 let state = Arc::new(VictauriState {
597 event_log,
598 registry,
599 port: AtomicU16::new(port),
600 pending_evals: Arc::new(Mutex::new(HashMap::new())),
601 recorder: EventRecorder::new(recorder_capacity),
602 privacy: privacy_config,
603 eval_timeout,
604 shutdown_tx,
605 started_at: std::time::Instant::now(),
606 tool_invocations: AtomicU64::new(0),
607 allow_file_navigation,
608 command_timings: introspection::CommandTimings::new(),
609 fault_registry: introspection::FaultRegistry::new(),
610 contract_store: introspection::ContractStore::new(),
611 startup_timeline,
612 event_bus: introspection::EventBusMonitor::default(),
613 task_tracker: introspection::TaskTracker::new(),
614 });
615 state.startup_timeline.mark("state_created");
616
617 app.manage(state.clone());
618
619 for cmd in commands {
620 state.registry.register(cmd);
621 }
622 state.startup_timeline.mark("commands_registered");
623
624 if let Some(ref token) = auth_token {
625 tracing::info!(
626 "Victauri MCP server auth enabled — token: {}…{}",
627 &token[..8],
628 &token[token.len().saturating_sub(4)..]
629 );
630 } else {
631 tracing::info!(
632 "Victauri MCP server running without auth (localhost-only, debug build)"
633 );
634 }
635
636 state.startup_timeline.mark("server_spawning");
637 let app_handle = app.clone();
638 let ready_state = state.clone();
639 let server_finished = state.task_tracker.track("mcp_server");
640 tauri::async_runtime::spawn(async move {
641 match mcp::start_server_with_options(
642 app_handle,
643 state,
644 port,
645 auth_token,
646 shutdown_rx,
647 )
648 .await
649 {
650 Ok(()) => {
651 tracing::info!("Victauri MCP server stopped");
652 }
653 Err(e) => {
654 tracing::error!("Victauri MCP server failed: {e}");
655 }
656 }
657 server_finished.store(true, std::sync::atomic::Ordering::Relaxed);
658 });
659
660 if let Some(cb) = on_ready {
661 let ready_finished = ready_state.task_tracker.track("on_ready_probe");
662 tauri::async_runtime::spawn(async move {
663 for _ in 0..50 {
664 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
665 let actual_port =
666 ready_state.port.load(std::sync::atomic::Ordering::Relaxed);
667 if tokio::net::TcpStream::connect(format!(
668 "127.0.0.1:{actual_port}"
669 ))
670 .await
671 .is_ok()
672 {
673 cb(actual_port);
674 ready_finished
675 .store(true, std::sync::atomic::Ordering::Relaxed);
676 return;
677 }
678 }
679 let actual_port =
680 ready_state.port.load(std::sync::atomic::Ordering::Relaxed);
681 tracing::warn!(
682 "Victauri on_ready: server did not become ready within 5s"
683 );
684 cb(actual_port);
685 ready_finished.store(true, std::sync::atomic::Ordering::Relaxed);
686 });
687 }
688
689 tracing::info!("Victauri plugin initialized — MCP server on port {port}");
690 Ok(())
691 })
692 .on_event(|app, event| {
693 if let RunEvent::Exit = event
694 && let Some(state) = app.try_state::<Arc<VictauriState>>()
695 {
696 let _ = state.shutdown_tx.send(true);
697 tracing::info!("Victauri shutdown signal sent");
698 }
699 })
700 .js_init_script(js_init)
701 .invoke_handler(tauri::generate_handler![
702 tools::victauri_eval_js,
703 tools::victauri_eval_callback,
704 tools::victauri_get_window_state,
705 tools::victauri_list_windows,
706 tools::victauri_get_ipc_log,
707 tools::victauri_get_registry,
708 tools::victauri_get_memory_stats,
709 tools::victauri_dom_snapshot,
710 tools::victauri_verify_state,
711 tools::victauri_detect_ghost_commands,
712 tools::victauri_check_ipc_integrity,
713 ])
714 .build())
715 }
716 }
717}
718
719#[must_use]
733pub fn init<R: Runtime>() -> TauriPlugin<R> {
734 VictauriBuilder::new()
735 .build()
736 .expect("default Victauri configuration is always valid")
737}
738
739#[must_use]
748pub fn init_auto_discover<R: Runtime>() -> TauriPlugin<R> {
749 VictauriBuilder::new()
750 .auto_discover()
751 .build()
752 .expect("default Victauri configuration is always valid")
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 #[test]
760 fn builder_default_values() {
761 let builder = VictauriBuilder::new();
762 assert_eq!(builder.event_capacity, DEFAULT_EVENT_CAPACITY);
763 assert_eq!(builder.recorder_capacity, DEFAULT_RECORDER_CAPACITY);
764 assert!(builder.auth_token.is_none());
765 assert!(!builder.auth_explicitly_enabled);
766 let resolved = builder.resolve_auth_token();
767 assert!(resolved.is_none(), "auth should be disabled by default");
768 assert!(builder.disabled_tools.is_empty());
769 assert!(builder.command_allowlist.is_none());
770 assert!(builder.command_blocklist.is_empty());
771 assert!(!builder.redaction_enabled);
772 assert!(!builder.strict_privacy);
773 }
774
775 #[test]
776 fn builder_port_override() {
777 let builder = VictauriBuilder::new().port(9090);
778 assert_eq!(builder.resolve_port(), 9090);
779 }
780
781 #[test]
782 #[allow(unsafe_code)]
783 fn builder_default_port() {
784 let builder = VictauriBuilder::new();
785 unsafe { std::env::remove_var("VICTAURI_PORT") };
787 assert_eq!(builder.resolve_port(), DEFAULT_PORT);
788 }
789
790 #[test]
791 fn builder_auth_token_explicit() {
792 let builder = VictauriBuilder::new().auth_token("my-secret");
793 assert_eq!(builder.resolve_auth_token(), Some("my-secret".to_string()));
794 }
795
796 #[test]
797 fn builder_auth_enabled() {
798 let builder = VictauriBuilder::new().auth_enabled();
799 assert!(builder.auth_explicitly_enabled);
800 let token = builder.resolve_auth_token().unwrap();
801 assert_eq!(token.len(), 36, "auto-generated token should be a UUID");
802 }
803
804 #[test]
805 fn builder_auth_generate_token() {
806 let builder = VictauriBuilder::new().generate_auth_token();
807 let token = builder.resolve_auth_token().unwrap();
808 assert_eq!(token.len(), 36);
809 }
810
811 #[test]
812 fn builder_auth_disabled_is_noop() {
813 let builder = VictauriBuilder::new().auth_disabled();
814 assert!(
815 builder.resolve_auth_token().is_none(),
816 "auth_disabled is a no-op, auth stays off by default"
817 );
818 }
819
820 #[test]
821 fn builder_auth_disabled_does_not_override_explicit_token() {
822 let builder = VictauriBuilder::new()
823 .auth_token("my-secret")
824 .auth_disabled();
825 assert_eq!(
826 builder.resolve_auth_token(),
827 Some("my-secret".to_string()),
828 "auth_disabled is a no-op, explicit token should remain"
829 );
830 }
831
832 #[test]
833 fn builder_capacities() {
834 let builder = VictauriBuilder::new()
835 .event_capacity(500)
836 .recorder_capacity(2000);
837 assert_eq!(builder.event_capacity, 500);
838 assert_eq!(builder.recorder_capacity, 2000);
839 }
840
841 #[test]
842 fn builder_disable_tools() {
843 let builder = VictauriBuilder::new().disable_tools(&["eval_js", "screenshot"]);
844 assert_eq!(builder.disabled_tools.len(), 2);
845 assert!(builder.disabled_tools.contains(&"eval_js".to_string()));
846 }
847
848 #[test]
849 fn builder_command_allowlist() {
850 let builder = VictauriBuilder::new().command_allowlist(&["greet", "increment"]);
851 assert!(builder.command_allowlist.is_some());
852 assert_eq!(builder.command_allowlist.as_ref().unwrap().len(), 2);
853 }
854
855 #[test]
856 fn builder_command_blocklist() {
857 let builder = VictauriBuilder::new().command_blocklist(&["dangerous_cmd"]);
858 assert_eq!(builder.command_blocklist.len(), 1);
859 }
860
861 #[test]
862 fn builder_redaction() {
863 let builder = VictauriBuilder::new()
864 .add_redaction_pattern(r"SECRET_\w+")
865 .enable_redaction();
866 assert!(builder.redaction_enabled);
867 assert_eq!(builder.redaction_patterns.len(), 1);
868 }
869
870 #[test]
871 fn builder_strict_privacy_config() {
872 let builder = VictauriBuilder::new().strict_privacy_mode();
873 let config = builder.build_privacy_config();
874 assert!(config.redaction_enabled);
875 assert_eq!(config.profile, crate::privacy::PrivacyProfile::Observe);
876 assert!(!config.is_tool_enabled("eval_js"));
877 assert!(!config.is_tool_enabled("screenshot"));
878 assert!(!config.is_tool_enabled("interact"));
879 assert!(config.is_tool_enabled("dom_snapshot"));
880 }
881
882 #[test]
883 fn builder_normal_privacy_config() {
884 let builder = VictauriBuilder::new()
885 .command_blocklist(&["secret_cmd"])
886 .disable_tools(&["eval_js"]);
887 let config = builder.build_privacy_config();
888 assert!(config.command_blocklist.contains("secret_cmd"));
889 assert!(!config.is_tool_enabled("eval_js"));
890 assert!(!config.redaction_enabled);
891 }
892
893 #[test]
894 fn builder_strict_with_extra_blocklist() {
895 let builder = VictauriBuilder::new()
896 .strict_privacy_mode()
897 .command_blocklist(&["extra_dangerous"]);
898 let config = builder.build_privacy_config();
899 assert!(config.command_blocklist.contains("extra_dangerous"));
900 assert!(!config.is_tool_enabled("eval_js"));
901 }
902
903 #[test]
904 fn builder_test_profile() {
905 let builder = VictauriBuilder::new().privacy_profile(crate::privacy::PrivacyProfile::Test);
906 let config = builder.build_privacy_config();
907 assert_eq!(config.profile, crate::privacy::PrivacyProfile::Test);
908 assert!(config.redaction_enabled);
909 assert!(config.is_tool_enabled("interact"));
910 assert!(config.is_tool_enabled("fill"));
911 assert!(config.is_tool_enabled("recording"));
912 assert!(!config.is_tool_enabled("eval_js"));
913 assert!(!config.is_tool_enabled("screenshot"));
914 assert!(!config.is_tool_enabled("navigate"));
915 }
916
917 #[test]
918 fn builder_profile_with_extra_disables() {
919 let builder = VictauriBuilder::new()
920 .privacy_profile(crate::privacy::PrivacyProfile::Test)
921 .disable_tools(&["interact"]);
922 let config = builder.build_privacy_config();
923 assert!(!config.is_tool_enabled("interact"));
924 assert!(config.is_tool_enabled("fill"));
925 }
926
927 #[test]
928 fn builder_bridge_capacities() {
929 let builder = VictauriBuilder::new()
930 .console_log_capacity(5000)
931 .network_log_capacity(2000)
932 .navigation_log_capacity(500);
933 assert_eq!(builder.bridge_capacities.console_logs, 5000);
934 assert_eq!(builder.bridge_capacities.network_log, 2000);
935 assert_eq!(builder.bridge_capacities.navigation_log, 500);
936 assert_eq!(builder.bridge_capacities.mutation_log, 500);
937 assert_eq!(builder.bridge_capacities.dialog_log, 100);
938 }
939
940 #[test]
941 fn builder_on_ready_sets_callback() {
942 let builder = VictauriBuilder::new().on_ready(|_port| {});
943 assert!(builder.on_ready.is_some());
944 }
945
946 #[test]
947 fn builder_file_navigation_disabled_by_default() {
948 let builder = VictauriBuilder::new();
949 assert!(
950 !builder.allow_file_navigation,
951 "file navigation should be disabled by default"
952 );
953 }
954
955 #[test]
956 fn builder_allow_file_navigation() {
957 let builder = VictauriBuilder::new().allow_file_navigation();
958 assert!(builder.allow_file_navigation);
959 }
960
961 #[test]
962 fn init_script_contains_custom_capacities() {
963 let caps = js_bridge::BridgeCapacities {
964 console_logs: 3000,
965 mutation_log: 750,
966 network_log: 5000,
967 navigation_log: 400,
968 dialog_log: 250,
969 long_tasks: 200,
970 };
971 let script = js_bridge::init_script(&caps);
972 assert!(script.contains("CAP_CONSOLE = 3000"));
973 assert!(script.contains("CAP_MUTATION = 750"));
974 assert!(script.contains("CAP_NETWORK = 5000"));
975 assert!(script.contains("CAP_NAVIGATION = 400"));
976 assert!(script.contains("CAP_DIALOG = 250"));
977 assert!(script.contains("CAP_LONG_TASKS = 200"));
978 }
979
980 #[test]
981 fn init_script_default_contains_standard_capacities() {
982 let caps = js_bridge::BridgeCapacities::default();
983 let script = js_bridge::init_script(&caps);
984 assert!(script.contains("CAP_CONSOLE = 1000"));
985 assert!(script.contains("CAP_NETWORK = 1000"));
986 assert!(script.contains("window.__VICTAURI__"));
987 }
988
989 #[test]
990 fn builder_validates_defaults() {
991 let builder = VictauriBuilder::new();
992 assert!(builder.validate().is_ok());
993 }
994
995 #[test]
996 fn builder_rejects_zero_port() {
997 let builder = VictauriBuilder::new().port(0);
998 let err = builder.validate().unwrap_err();
999 assert!(matches!(err, BuilderError::InvalidPort { port: 0, .. }));
1000 }
1001
1002 #[test]
1003 fn builder_rejects_zero_event_capacity() {
1004 let builder = VictauriBuilder::new().event_capacity(0);
1005 let err = builder.validate().unwrap_err();
1006 assert!(matches!(
1007 err,
1008 BuilderError::InvalidEventCapacity { capacity: 0, .. }
1009 ));
1010 }
1011
1012 #[test]
1013 fn builder_rejects_excessive_event_capacity() {
1014 let builder = VictauriBuilder::new().event_capacity(2_000_000);
1015 assert!(builder.validate().is_err());
1016 }
1017
1018 #[test]
1019 fn builder_rejects_zero_recorder_capacity() {
1020 let builder = VictauriBuilder::new().recorder_capacity(0);
1021 assert!(builder.validate().is_err());
1022 }
1023
1024 #[test]
1025 fn builder_rejects_zero_eval_timeout() {
1026 let builder = VictauriBuilder::new().eval_timeout(std::time::Duration::from_secs(0));
1027 assert!(builder.validate().is_err());
1028 }
1029
1030 #[test]
1031 fn builder_rejects_excessive_eval_timeout() {
1032 let builder = VictauriBuilder::new().eval_timeout(std::time::Duration::from_secs(600));
1033 assert!(builder.validate().is_err());
1034 }
1035
1036 #[test]
1037 fn builder_accepts_edge_values() {
1038 let builder = VictauriBuilder::new()
1039 .port(1)
1040 .event_capacity(1)
1041 .recorder_capacity(1)
1042 .eval_timeout(std::time::Duration::from_secs(1));
1043 assert!(builder.validate().is_ok());
1044
1045 let builder = VictauriBuilder::new()
1046 .port(65535)
1047 .event_capacity(MAX_EVENT_CAPACITY)
1048 .recorder_capacity(MAX_RECORDER_CAPACITY)
1049 .eval_timeout(std::time::Duration::from_secs(MAX_EVAL_TIMEOUT_SECS));
1050 assert!(builder.validate().is_ok());
1051 }
1052}