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 pub probes: introspection::AppStateProbes,
176}
177
178pub struct VictauriBuilder {
190 port: Option<u16>,
191 event_capacity: usize,
192 recorder_capacity: usize,
193 eval_timeout: std::time::Duration,
194 auth_token: Option<String>,
195 auth_explicitly_enabled: bool,
196 auth_explicitly_disabled: bool,
197 disabled_tools: Vec<String>,
198 command_allowlist: Option<Vec<String>>,
199 command_blocklist: Vec<String>,
200 storage_key_blocklist: Vec<String>,
201 redaction_patterns: Vec<String>,
202 redaction_enabled: bool,
203 strict_privacy: bool,
204 privacy_profile: Option<privacy::PrivacyProfile>,
205 bridge_capacities: js_bridge::BridgeCapacities,
206 on_ready: Option<Box<dyn FnOnce(u16) + Send + 'static>>,
207 commands: Vec<victauri_core::CommandInfo>,
208 allow_file_navigation: bool,
209 listen_events: Vec<String>,
210 db_search_paths: Vec<std::path::PathBuf>,
211 probes: Vec<(String, std::sync::Arc<introspection::ProbeFn>)>,
212}
213
214impl Default for VictauriBuilder {
215 fn default() -> Self {
216 Self {
217 port: None,
218 event_capacity: DEFAULT_EVENT_CAPACITY,
219 recorder_capacity: DEFAULT_RECORDER_CAPACITY,
220 eval_timeout: DEFAULT_EVAL_TIMEOUT,
221 auth_token: None,
222 auth_explicitly_enabled: false,
223 auth_explicitly_disabled: false,
224 disabled_tools: Vec::new(),
225 command_allowlist: None,
226 command_blocklist: Vec::new(),
227 storage_key_blocklist: Vec::new(),
228 redaction_patterns: Vec::new(),
229 redaction_enabled: false,
230 strict_privacy: false,
231 privacy_profile: None,
232 bridge_capacities: js_bridge::BridgeCapacities::default(),
233 on_ready: None,
234 commands: Vec::new(),
235 allow_file_navigation: false,
236 listen_events: Vec::new(),
237 db_search_paths: Vec::new(),
238 probes: Vec::new(),
239 }
240 }
241}
242
243impl VictauriBuilder {
244 #[must_use]
246 pub fn new() -> Self {
247 Self::default()
248 }
249
250 #[must_use]
252 pub fn port(mut self, port: u16) -> Self {
253 self.port = Some(port);
254 self
255 }
256
257 #[must_use]
259 pub fn event_capacity(mut self, capacity: usize) -> Self {
260 self.event_capacity = capacity;
261 self
262 }
263
264 #[must_use]
266 pub fn recorder_capacity(mut self, capacity: usize) -> Self {
267 self.recorder_capacity = capacity;
268 self
269 }
270
271 #[must_use]
273 pub fn eval_timeout(mut self, timeout: std::time::Duration) -> Self {
274 self.eval_timeout = timeout;
275 self
276 }
277
278 #[must_use]
282 pub fn auth_token(mut self, token: impl Into<String>) -> Self {
283 self.auth_token = Some(token.into());
284 self
285 }
286
287 #[must_use]
295 pub fn auth_enabled(mut self) -> Self {
296 self.auth_explicitly_enabled = true;
297 self
298 }
299
300 #[must_use]
302 pub fn generate_auth_token(mut self) -> Self {
303 self.auth_explicitly_enabled = true;
304 self
305 }
306
307 #[must_use]
317 pub fn auth_disabled(mut self) -> Self {
318 self.auth_explicitly_disabled = true;
319 self
320 }
321
322 #[must_use]
324 pub fn disable_tools(mut self, tools: &[&str]) -> Self {
325 self.disabled_tools = tools.iter().map(std::string::ToString::to_string).collect();
326 self
327 }
328
329 #[must_use]
331 pub fn command_allowlist(mut self, commands: &[&str]) -> Self {
332 self.command_allowlist = Some(
333 commands
334 .iter()
335 .map(std::string::ToString::to_string)
336 .collect(),
337 );
338 self
339 }
340
341 #[must_use]
343 pub fn command_blocklist(mut self, commands: &[&str]) -> Self {
344 self.command_blocklist = commands
345 .iter()
346 .map(std::string::ToString::to_string)
347 .collect();
348 self
349 }
350
351 #[must_use]
356 pub fn storage_key_blocklist(mut self, keys: &[&str]) -> Self {
357 self.storage_key_blocklist = keys.iter().map(std::string::ToString::to_string).collect();
358 self
359 }
360
361 #[must_use]
363 pub fn add_redaction_pattern(mut self, pattern: impl Into<String>) -> Self {
364 self.redaction_patterns.push(pattern.into());
365 self
366 }
367
368 #[must_use]
370 pub fn enable_redaction(mut self) -> Self {
371 self.redaction_enabled = true;
372 self
373 }
374
375 #[must_use]
382 pub fn strict_privacy_mode(mut self) -> Self {
383 self.strict_privacy = true;
384 self.privacy_profile = Some(privacy::PrivacyProfile::Observe);
385 self
386 }
387
388 #[must_use]
397 pub fn privacy_profile(mut self, profile: privacy::PrivacyProfile) -> Self {
398 self.privacy_profile = Some(profile);
399 if matches!(
400 profile,
401 privacy::PrivacyProfile::Observe | privacy::PrivacyProfile::Test
402 ) {
403 self.redaction_enabled = true;
404 }
405 self
406 }
407
408 #[must_use]
410 pub fn console_log_capacity(mut self, capacity: usize) -> Self {
411 self.bridge_capacities.console_logs = capacity;
412 self
413 }
414
415 #[must_use]
417 pub fn network_log_capacity(mut self, capacity: usize) -> Self {
418 self.bridge_capacities.network_log = capacity;
419 self
420 }
421
422 #[must_use]
424 pub fn navigation_log_capacity(mut self, capacity: usize) -> Self {
425 self.bridge_capacities.navigation_log = capacity;
426 self
427 }
428
429 #[must_use]
444 pub fn commands(mut self, schemas: &[victauri_core::CommandInfo]) -> Self {
445 self.commands = schemas.to_vec();
446 self
447 }
448
449 #[must_use]
465 pub fn register_command_names(mut self, names: &[&str]) -> Self {
466 self.commands
467 .extend(names.iter().map(|n| victauri_core::CommandInfo::new(*n)));
468 self
469 }
470
471 #[must_use]
487 pub fn auto_discover(mut self) -> Self {
488 self.commands
489 .extend(victauri_core::auto_discovered_commands());
490 self
491 }
492
493 #[must_use]
504 pub fn listen_events(mut self, events: &[&str]) -> Self {
505 self.listen_events = events
506 .iter()
507 .map(std::string::ToString::to_string)
508 .collect();
509 self
510 }
511
512 #[must_use]
521 pub fn allow_file_navigation(mut self) -> Self {
522 self.allow_file_navigation = true;
523 self
524 }
525
526 #[must_use]
539 pub fn db_search_paths<I, P>(mut self, paths: I) -> Self
540 where
541 I: IntoIterator<Item = P>,
542 P: Into<std::path::PathBuf>,
543 {
544 self.db_search_paths
545 .extend(paths.into_iter().map(Into::into));
546 self
547 }
548
549 #[must_use]
573 pub fn probe<F>(mut self, name: impl Into<String>, probe: F) -> Self
574 where
575 F: Fn() -> serde_json::Value + Send + Sync + 'static,
576 {
577 self.probes.push((name.into(), std::sync::Arc::new(probe)));
578 self
579 }
580
581 #[must_use]
584 pub fn on_ready(mut self, f: impl FnOnce(u16) + Send + 'static) -> Self {
585 self.on_ready = Some(Box::new(f));
586 self
587 }
588
589 fn resolve_port(&self) -> u16 {
590 self.port
591 .or_else(|| std::env::var("VICTAURI_PORT").ok()?.parse().ok())
592 .unwrap_or(DEFAULT_PORT)
593 }
594
595 fn resolve_auth_token(&self) -> Option<String> {
596 if self.auth_explicitly_disabled {
597 return None;
598 }
599 if let Some(ref token) = self.auth_token {
600 return Some(token.clone());
601 }
602 if let Ok(token) = std::env::var("VICTAURI_AUTH_TOKEN") {
603 return Some(token);
604 }
605 Some(auth::generate_token())
606 }
607
608 fn resolve_eval_timeout(&self) -> std::time::Duration {
609 std::env::var("VICTAURI_EVAL_TIMEOUT")
610 .ok()
611 .and_then(|s| s.parse::<u64>().ok())
612 .map_or(self.eval_timeout, std::time::Duration::from_secs)
613 }
614
615 fn build_privacy_config(&self) -> privacy::PrivacyConfig {
616 let profile = self
617 .privacy_profile
618 .unwrap_or(privacy::PrivacyProfile::FullControl);
619
620 let redaction_enabled = self.redaction_enabled
621 || self.strict_privacy
622 || matches!(
623 profile,
624 privacy::PrivacyProfile::Observe | privacy::PrivacyProfile::Test
625 );
626
627 privacy::PrivacyConfig {
628 profile,
629 command_allowlist: self
630 .command_allowlist
631 .as_ref()
632 .map(|v| v.iter().cloned().collect::<HashSet<String>>()),
633 command_blocklist: self.command_blocklist.iter().cloned().collect(),
634 disabled_tools: self.disabled_tools.iter().cloned().collect(),
635 storage_key_blocklist: self.storage_key_blocklist.iter().cloned().collect(),
636 redactor: redaction::Redactor::new(&self.redaction_patterns),
637 redaction_enabled,
638 }
639 }
640
641 fn validate(&self) -> Result<(), BuilderError> {
642 let port = self.resolve_port();
643 if port == 0 {
644 return Err(BuilderError::InvalidPort {
645 port,
646 reason: "port 0 is reserved".to_string(),
647 });
648 }
649
650 if self.event_capacity == 0 || self.event_capacity > MAX_EVENT_CAPACITY {
651 return Err(BuilderError::InvalidEventCapacity {
652 capacity: self.event_capacity,
653 reason: format!("must be between 1 and {MAX_EVENT_CAPACITY}"),
654 });
655 }
656
657 if self.recorder_capacity == 0 || self.recorder_capacity > MAX_RECORDER_CAPACITY {
658 return Err(BuilderError::InvalidRecorderCapacity {
659 capacity: self.recorder_capacity,
660 reason: format!("must be between 1 and {MAX_RECORDER_CAPACITY}"),
661 });
662 }
663
664 let timeout = self.resolve_eval_timeout();
665 if timeout.as_secs() == 0 || timeout.as_secs() > MAX_EVAL_TIMEOUT_SECS {
666 return Err(BuilderError::InvalidEvalTimeout {
667 timeout_secs: timeout.as_secs(),
668 reason: format!("must be between 1 and {MAX_EVAL_TIMEOUT_SECS} seconds"),
669 });
670 }
671
672 Ok(())
673 }
674
675 pub fn build<R: Runtime>(self) -> Result<TauriPlugin<R>, BuilderError> {
685 #[cfg(not(debug_assertions))]
686 {
687 Ok(Builder::new("victauri").build())
688 }
689
690 #[cfg(debug_assertions)]
691 {
692 if env_truthy("VICTAURI_DISABLE") {
696 tracing::info!("Victauri disabled via VICTAURI_DISABLE — returning no-op plugin");
697 return Ok(Builder::new("victauri").build());
698 }
699
700 self.validate()?;
701
702 let port = self.resolve_port();
703 let event_capacity = self.event_capacity;
704 let recorder_capacity = self.recorder_capacity;
705 let eval_timeout = self.resolve_eval_timeout();
706 let auth_token = self.resolve_auth_token();
707 let privacy_config = self.build_privacy_config();
708 let allow_file_navigation = self.allow_file_navigation;
709 let db_search_paths = self.db_search_paths;
710 let on_ready = self.on_ready;
711 let commands = self.commands;
712 let listen_events = self.listen_events;
713 let probes = self.probes;
714 let js_init = js_bridge::init_script(&self.bridge_capacities);
715
716 Ok(Builder::new("victauri")
717 .setup(move |app, _api| {
718 let startup_timeline = introspection::StartupTimeline::new();
719 let event_log = EventLog::new(event_capacity);
720 startup_timeline.mark("event_log_created");
721 let registry = CommandRegistry::new();
722 startup_timeline.mark("registry_created");
723 let (shutdown_tx, shutdown_rx) = watch::channel(false);
724
725 let state = Arc::new(VictauriState {
726 event_log,
727 registry,
728 port: AtomicU16::new(port),
729 pending_evals: Arc::new(Mutex::new(HashMap::new())),
730 recorder: EventRecorder::new(recorder_capacity),
731 privacy: privacy_config,
732 eval_timeout,
733 shutdown_tx,
734 started_at: std::time::Instant::now(),
735 tool_invocations: AtomicU64::new(0),
736 allow_file_navigation,
737 command_timings: introspection::CommandTimings::new(),
738 fault_registry: introspection::FaultRegistry::new(),
739 contract_store: introspection::ContractStore::new(),
740 startup_timeline,
741 event_bus: introspection::EventBusMonitor::default(),
742 task_tracker: introspection::TaskTracker::new(),
743 bridge_ready: AtomicBool::new(false),
744 bridge_notify: tokio::sync::Notify::new(),
745 screencast: Arc::new(screencast::Screencast::default()),
746 db_search_paths,
747 probes: introspection::AppStateProbes::default(),
748 });
749 state.startup_timeline.mark("state_created");
750
751 for (name, probe) in probes {
752 state.probes.register(name, probe);
753 }
754
755 app.manage(state.clone());
756
757 for cmd in commands {
758 state.registry.register(cmd);
759 }
760 state.startup_timeline.mark("commands_registered");
761
762 for event_name in &listen_events {
764 let bus = state.event_bus.clone();
765 let name = event_name.clone();
766 app.listen_any(event_name.clone(), move |event| {
767 let payload =
768 serde_json::from_str::<serde_json::Value>(event.payload())
769 .map_or_else(
770 |_| event.payload().to_string(),
771 |v| v.to_string(),
772 );
773 bus.push(introspection::CapturedTauriEvent {
774 name: name.clone(),
775 payload,
776 timestamp: chrono::Utc::now()
777 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
778 });
779 });
780 }
781 state
782 .startup_timeline
783 .mark("event_bus_listeners_registered");
784
785 if let Some(ref token) = auth_token {
786 let prefix_len = token.len().min(8);
787 let suffix_start = token.len().saturating_sub(4);
788 tracing::info!(
789 "Victauri MCP server auth enabled — token: {}…{}",
790 &token[..prefix_len],
791 &token[suffix_start..]
792 );
793 } else {
794 tracing::warn!(
795 "Victauri MCP server running WITHOUT auth — any localhost process can \
796 access all tools. Use VictauriBuilder::auth_enabled() or set \
797 VICTAURI_AUTH_TOKEN for shared/CI environments."
798 );
799 }
800
801 state.startup_timeline.mark("server_spawning");
802 let app_handle = app.clone();
803 let ready_state = state.clone();
804 let server_finished = state.task_tracker.track("mcp_server");
805 tauri::async_runtime::spawn(async move {
806 match mcp::start_server_with_options(
807 app_handle,
808 state,
809 port,
810 auth_token,
811 shutdown_rx,
812 )
813 .await
814 {
815 Ok(()) => {
816 tracing::info!("Victauri MCP server stopped");
817 }
818 Err(e) => {
819 tracing::error!("Victauri MCP server failed: {e}");
820 }
821 }
822 server_finished.store(true, std::sync::atomic::Ordering::Relaxed);
823 });
824
825 if let Some(cb) = on_ready {
826 let ready_finished = ready_state.task_tracker.track("on_ready_probe");
827 tauri::async_runtime::spawn(async move {
828 for _ in 0..50 {
829 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
830 let actual_port =
831 ready_state.port.load(std::sync::atomic::Ordering::Relaxed);
832 if tokio::net::TcpStream::connect(format!(
833 "127.0.0.1:{actual_port}"
834 ))
835 .await
836 .is_ok()
837 {
838 cb(actual_port);
839 ready_finished
840 .store(true, std::sync::atomic::Ordering::Relaxed);
841 return;
842 }
843 }
844 let actual_port =
845 ready_state.port.load(std::sync::atomic::Ordering::Relaxed);
846 tracing::warn!(
847 "Victauri on_ready: server did not become ready within 5s"
848 );
849 cb(actual_port);
850 ready_finished.store(true, std::sync::atomic::Ordering::Relaxed);
851 });
852 }
853
854 emit_security_banner(port);
855 Ok(())
856 })
857 .on_event(|app, event| {
858 let Some(state) = app.try_state::<Arc<VictauriState>>() else {
859 return;
860 };
861 match event {
862 RunEvent::Exit => {
863 let _ = state.shutdown_tx.send(true);
864 tracing::info!("Victauri shutdown signal sent");
865 }
866 RunEvent::ExitRequested { .. } => {
867 state.event_bus.push(introspection::CapturedTauriEvent {
868 name: "tauri://exit-requested".to_string(),
869 payload: String::new(),
870 timestamp: chrono::Utc::now()
871 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
872 });
873 }
874 RunEvent::WindowEvent {
875 label,
876 event: win_event,
877 ..
878 } => {
879 let (name, payload) = format_window_event(label, win_event);
880 state.event_bus.push(introspection::CapturedTauriEvent {
881 name,
882 payload,
883 timestamp: chrono::Utc::now()
884 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
885 });
886 }
887 _ => {}
888 }
889 })
890 .js_init_script(js_init)
891 .invoke_handler(tauri::generate_handler![
892 tools::victauri_eval_js,
893 tools::victauri_eval_callback,
894 tools::victauri_get_window_state,
895 tools::victauri_list_windows,
896 tools::victauri_get_ipc_log,
897 tools::victauri_get_registry,
898 tools::victauri_get_memory_stats,
899 tools::victauri_dom_snapshot,
900 tools::victauri_verify_state,
901 tools::victauri_detect_ghost_commands,
902 tools::victauri_check_ipc_integrity,
903 ])
904 .build())
905 }
906 }
907}
908
909#[cfg(debug_assertions)]
910fn format_window_event(label: &str, event: &tauri::WindowEvent) -> (String, String) {
911 match event {
912 tauri::WindowEvent::Resized(size) => (
913 format!("window:{label}:resized"),
914 serde_json::json!({"width": size.width, "height": size.height}).to_string(),
915 ),
916 tauri::WindowEvent::Moved(pos) => (
917 format!("window:{label}:moved"),
918 serde_json::json!({"x": pos.x, "y": pos.y}).to_string(),
919 ),
920 tauri::WindowEvent::CloseRequested { .. } => {
921 (format!("window:{label}:close-requested"), String::new())
922 }
923 tauri::WindowEvent::Destroyed => (format!("window:{label}:destroyed"), String::new()),
924 tauri::WindowEvent::Focused(focused) => (
925 format!("window:{label}:focused"),
926 serde_json::json!({"focused": focused}).to_string(),
927 ),
928 tauri::WindowEvent::ScaleFactorChanged { scale_factor, .. } => (
929 format!("window:{label}:scale-factor-changed"),
930 serde_json::json!({"scale_factor": scale_factor}).to_string(),
931 ),
932 tauri::WindowEvent::ThemeChanged(theme) => (
933 format!("window:{label}:theme-changed"),
934 serde_json::json!({"theme": format!("{theme:?}")}).to_string(),
935 ),
936 tauri::WindowEvent::DragDrop(drag_event) => (
937 format!("window:{label}:drag-drop"),
938 format!("{drag_event:?}"),
939 ),
940 _ => (format!("window:{label}:other"), format!("{event:?}")),
941 }
942}
943
944#[cfg(debug_assertions)]
948fn env_truthy(name: &str) -> bool {
949 std::env::var(name).is_ok_and(|v| {
950 matches!(
951 v.trim().to_ascii_lowercase().as_str(),
952 "1" | "true" | "yes" | "on"
953 )
954 })
955}
956
957#[cfg(debug_assertions)]
966fn emit_security_banner(port: u16) {
967 tracing::warn!(
968 "┌─ VICTAURI INTROSPECTION SERVER ACTIVE ─────────────────────────────\n\
969 │ Listening on http://127.0.0.1:{port} — exposes JS eval, IPC, the\n\
970 │ filesystem, and SQLite to local clients. DEBUG-ONLY developer tool;\n\
971 │ it must never reach end users.\n\
972 │ Seeing this in a shipped/release build means your release profile has\n\
973 │ `debug-assertions = true`. Turn that off, or hard-disable Victauri\n\
974 │ with the VICTAURI_DISABLE=1 environment variable.\n\
975 └────────────────────────────────────────────────────────────────────"
976 );
977}
978
979#[must_use]
999pub fn init<R: Runtime>() -> TauriPlugin<R> {
1000 VictauriBuilder::new()
1001 .build()
1002 .expect("default Victauri configuration is always valid")
1003}
1004
1005#[must_use]
1014pub fn init_auto_discover<R: Runtime>() -> TauriPlugin<R> {
1015 VictauriBuilder::new()
1016 .auto_discover()
1017 .build()
1018 .expect("default Victauri configuration is always valid")
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023 use super::*;
1024
1025 #[test]
1026 fn builder_default_values() {
1027 let builder = VictauriBuilder::new();
1028 assert_eq!(builder.event_capacity, DEFAULT_EVENT_CAPACITY);
1029 assert_eq!(builder.recorder_capacity, DEFAULT_RECORDER_CAPACITY);
1030 assert!(builder.auth_token.is_none());
1031 assert!(!builder.auth_explicitly_enabled);
1032 assert!(!builder.auth_explicitly_disabled);
1033 let resolved = builder.resolve_auth_token();
1034 assert!(
1035 resolved.is_some(),
1036 "auth should be enabled by default (auto-generated token)"
1037 );
1038 assert_eq!(
1039 resolved.unwrap().len(),
1040 36,
1041 "auto-generated token should be UUID v4"
1042 );
1043 assert!(builder.disabled_tools.is_empty());
1044 assert!(builder.command_allowlist.is_none());
1045 assert!(builder.command_blocklist.is_empty());
1046 assert!(!builder.redaction_enabled);
1047 assert!(!builder.strict_privacy);
1048 }
1049
1050 #[test]
1051 fn builder_port_override() {
1052 let builder = VictauriBuilder::new().port(9090);
1053 assert_eq!(builder.resolve_port(), 9090);
1054 }
1055
1056 #[test]
1057 #[allow(unsafe_code)]
1058 fn builder_default_port() {
1059 let builder = VictauriBuilder::new();
1060 unsafe { std::env::remove_var("VICTAURI_PORT") };
1062 assert_eq!(builder.resolve_port(), DEFAULT_PORT);
1063 }
1064
1065 #[test]
1066 fn builder_auth_token_explicit() {
1067 let builder = VictauriBuilder::new().auth_token("my-secret");
1068 assert_eq!(builder.resolve_auth_token(), Some("my-secret".to_string()));
1069 }
1070
1071 #[test]
1072 #[allow(unsafe_code)]
1073 fn env_truthy_recognizes_kill_switch_values() {
1074 let key = "VICTAURI_TEST_KILL_SWITCH";
1076 unsafe { std::env::remove_var(key) };
1078 assert!(!env_truthy(key), "unset should be false");
1079
1080 for v in ["1", "true", "TRUE", " yes ", "On"] {
1081 unsafe { std::env::set_var(key, v) };
1083 assert!(env_truthy(key), "{v:?} should be truthy");
1084 }
1085 for v in ["0", "false", "", "nope"] {
1086 unsafe { std::env::set_var(key, v) };
1088 assert!(!env_truthy(key), "{v:?} should be falsy");
1089 }
1090 unsafe { std::env::remove_var(key) };
1092 }
1093
1094 #[test]
1095 fn builder_auth_enabled() {
1096 let builder = VictauriBuilder::new().auth_enabled();
1097 assert!(builder.auth_explicitly_enabled);
1098 let token = builder.resolve_auth_token().unwrap();
1099 assert_eq!(token.len(), 36, "auto-generated token should be a UUID");
1100 }
1101
1102 #[test]
1103 fn builder_auth_generate_token() {
1104 let builder = VictauriBuilder::new().generate_auth_token();
1105 let token = builder.resolve_auth_token().unwrap();
1106 assert_eq!(token.len(), 36);
1107 }
1108
1109 #[test]
1110 fn builder_auth_disabled_suppresses_default_token() {
1111 let builder = VictauriBuilder::new().auth_disabled();
1112 assert!(
1113 builder.resolve_auth_token().is_none(),
1114 "auth_disabled must suppress the default auto-generated token (auth is ON by default)"
1115 );
1116 }
1117
1118 #[test]
1119 fn builder_auth_disabled_returns_none() {
1120 let builder = VictauriBuilder::new().auth_disabled();
1121 assert!(
1122 builder.resolve_auth_token().is_none(),
1123 "auth_disabled should suppress auto-generated token"
1124 );
1125 }
1126
1127 #[test]
1128 fn builder_auth_disabled_overrides_explicit_token() {
1129 let builder = VictauriBuilder::new()
1130 .auth_token("my-secret")
1131 .auth_disabled();
1132 assert!(
1133 builder.resolve_auth_token().is_none(),
1134 "auth_disabled should override explicit token"
1135 );
1136 }
1137
1138 #[test]
1139 fn builder_capacities() {
1140 let builder = VictauriBuilder::new()
1141 .event_capacity(500)
1142 .recorder_capacity(2000);
1143 assert_eq!(builder.event_capacity, 500);
1144 assert_eq!(builder.recorder_capacity, 2000);
1145 }
1146
1147 #[test]
1148 fn builder_disable_tools() {
1149 let builder = VictauriBuilder::new().disable_tools(&["eval_js", "screenshot"]);
1150 assert_eq!(builder.disabled_tools.len(), 2);
1151 assert!(builder.disabled_tools.contains(&"eval_js".to_string()));
1152 }
1153
1154 #[test]
1155 fn builder_command_allowlist() {
1156 let builder = VictauriBuilder::new().command_allowlist(&["greet", "increment"]);
1157 assert!(builder.command_allowlist.is_some());
1158 assert_eq!(builder.command_allowlist.as_ref().unwrap().len(), 2);
1159 }
1160
1161 #[test]
1162 fn builder_command_blocklist() {
1163 let builder = VictauriBuilder::new().command_blocklist(&["dangerous_cmd"]);
1164 assert_eq!(builder.command_blocklist.len(), 1);
1165 }
1166
1167 #[test]
1168 fn builder_redaction() {
1169 let builder = VictauriBuilder::new()
1170 .add_redaction_pattern(r"SECRET_\w+")
1171 .enable_redaction();
1172 assert!(builder.redaction_enabled);
1173 assert_eq!(builder.redaction_patterns.len(), 1);
1174 }
1175
1176 #[test]
1177 fn builder_strict_privacy_config() {
1178 let builder = VictauriBuilder::new().strict_privacy_mode();
1179 let config = builder.build_privacy_config();
1180 assert!(config.redaction_enabled);
1181 assert_eq!(config.profile, crate::privacy::PrivacyProfile::Observe);
1182 assert!(!config.is_tool_enabled("eval_js"));
1183 assert!(!config.is_tool_enabled("screenshot"));
1184 assert!(!config.is_tool_enabled("interact"));
1185 assert!(config.is_tool_enabled("dom_snapshot"));
1186 }
1187
1188 #[test]
1189 fn builder_normal_privacy_config() {
1190 let builder = VictauriBuilder::new()
1191 .command_blocklist(&["secret_cmd"])
1192 .disable_tools(&["eval_js"]);
1193 let config = builder.build_privacy_config();
1194 assert!(config.command_blocklist.contains("secret_cmd"));
1195 assert!(!config.is_tool_enabled("eval_js"));
1196 assert!(!config.redaction_enabled);
1197 }
1198
1199 #[test]
1200 fn builder_strict_with_extra_blocklist() {
1201 let builder = VictauriBuilder::new()
1202 .strict_privacy_mode()
1203 .command_blocklist(&["extra_dangerous"]);
1204 let config = builder.build_privacy_config();
1205 assert!(config.command_blocklist.contains("extra_dangerous"));
1206 assert!(!config.is_tool_enabled("eval_js"));
1207 }
1208
1209 #[test]
1210 fn builder_test_profile() {
1211 let builder = VictauriBuilder::new().privacy_profile(crate::privacy::PrivacyProfile::Test);
1212 let config = builder.build_privacy_config();
1213 assert_eq!(config.profile, crate::privacy::PrivacyProfile::Test);
1214 assert!(config.redaction_enabled);
1215 assert!(config.is_tool_enabled("interact"));
1216 assert!(config.is_tool_enabled("fill"));
1217 assert!(config.is_tool_enabled("recording"));
1218 assert!(!config.is_tool_enabled("eval_js"));
1219 assert!(!config.is_tool_enabled("screenshot"));
1220 assert!(!config.is_tool_enabled("navigate"));
1221 }
1222
1223 #[test]
1224 fn builder_profile_with_extra_disables() {
1225 let builder = VictauriBuilder::new()
1226 .privacy_profile(crate::privacy::PrivacyProfile::Test)
1227 .disable_tools(&["interact"]);
1228 let config = builder.build_privacy_config();
1229 assert!(!config.is_tool_enabled("interact"));
1230 assert!(config.is_tool_enabled("fill"));
1231 }
1232
1233 #[test]
1234 fn builder_bridge_capacities() {
1235 let builder = VictauriBuilder::new()
1236 .console_log_capacity(5000)
1237 .network_log_capacity(2000)
1238 .navigation_log_capacity(500);
1239 assert_eq!(builder.bridge_capacities.console_logs, 5000);
1240 assert_eq!(builder.bridge_capacities.network_log, 2000);
1241 assert_eq!(builder.bridge_capacities.navigation_log, 500);
1242 assert_eq!(builder.bridge_capacities.mutation_log, 500);
1243 assert_eq!(builder.bridge_capacities.dialog_log, 100);
1244 }
1245
1246 #[test]
1247 fn builder_on_ready_sets_callback() {
1248 let builder = VictauriBuilder::new().on_ready(|_port| {});
1249 assert!(builder.on_ready.is_some());
1250 }
1251
1252 #[test]
1253 fn builder_file_navigation_disabled_by_default() {
1254 let builder = VictauriBuilder::new();
1255 assert!(
1256 !builder.allow_file_navigation,
1257 "file navigation should be disabled by default"
1258 );
1259 }
1260
1261 #[test]
1262 fn builder_allow_file_navigation() {
1263 let builder = VictauriBuilder::new().allow_file_navigation();
1264 assert!(builder.allow_file_navigation);
1265 }
1266
1267 #[test]
1268 fn builder_listen_events() {
1269 let builder =
1270 VictauriBuilder::new().listen_events(&["notification-added", "settings-changed"]);
1271 assert_eq!(builder.listen_events.len(), 2);
1272 assert!(
1273 builder
1274 .listen_events
1275 .contains(&"notification-added".to_string())
1276 );
1277 assert!(
1278 builder
1279 .listen_events
1280 .contains(&"settings-changed".to_string())
1281 );
1282 }
1283
1284 #[test]
1285 fn builder_listen_events_empty_by_default() {
1286 let builder = VictauriBuilder::new();
1287 assert!(builder.listen_events.is_empty());
1288 }
1289
1290 #[test]
1291 fn init_script_contains_custom_capacities() {
1292 let caps = js_bridge::BridgeCapacities {
1293 console_logs: 3000,
1294 mutation_log: 750,
1295 network_log: 5000,
1296 navigation_log: 400,
1297 dialog_log: 250,
1298 long_tasks: 200,
1299 };
1300 let script = js_bridge::init_script(&caps);
1301 assert!(script.contains("CAP_CONSOLE = 3000"));
1302 assert!(script.contains("CAP_MUTATION = 750"));
1303 assert!(script.contains("CAP_NETWORK = 5000"));
1304 assert!(script.contains("CAP_NAVIGATION = 400"));
1305 assert!(script.contains("CAP_DIALOG = 250"));
1306 assert!(script.contains("CAP_LONG_TASKS = 200"));
1307 }
1308
1309 #[test]
1310 fn init_script_default_contains_standard_capacities() {
1311 let caps = js_bridge::BridgeCapacities::default();
1312 let script = js_bridge::init_script(&caps);
1313 assert!(script.contains("CAP_CONSOLE = 1000"));
1314 assert!(script.contains("CAP_NETWORK = 1000"));
1315 assert!(script.contains("window.__VICTAURI__"));
1316 }
1317
1318 #[test]
1319 fn builder_validates_defaults() {
1320 let builder = VictauriBuilder::new();
1321 assert!(builder.validate().is_ok());
1322 }
1323
1324 #[test]
1325 fn builder_rejects_zero_port() {
1326 let builder = VictauriBuilder::new().port(0);
1327 let err = builder.validate().unwrap_err();
1328 assert!(matches!(err, BuilderError::InvalidPort { port: 0, .. }));
1329 }
1330
1331 #[test]
1332 fn builder_rejects_zero_event_capacity() {
1333 let builder = VictauriBuilder::new().event_capacity(0);
1334 let err = builder.validate().unwrap_err();
1335 assert!(matches!(
1336 err,
1337 BuilderError::InvalidEventCapacity { capacity: 0, .. }
1338 ));
1339 }
1340
1341 #[test]
1342 fn builder_rejects_excessive_event_capacity() {
1343 let builder = VictauriBuilder::new().event_capacity(2_000_000);
1344 assert!(builder.validate().is_err());
1345 }
1346
1347 #[test]
1348 fn builder_rejects_zero_recorder_capacity() {
1349 let builder = VictauriBuilder::new().recorder_capacity(0);
1350 assert!(builder.validate().is_err());
1351 }
1352
1353 #[test]
1354 fn builder_rejects_zero_eval_timeout() {
1355 let builder = VictauriBuilder::new().eval_timeout(std::time::Duration::from_secs(0));
1356 assert!(builder.validate().is_err());
1357 }
1358
1359 #[test]
1360 fn builder_rejects_excessive_eval_timeout() {
1361 let builder = VictauriBuilder::new().eval_timeout(std::time::Duration::from_secs(600));
1362 assert!(builder.validate().is_err());
1363 }
1364
1365 #[test]
1366 fn builder_accepts_edge_values() {
1367 let builder = VictauriBuilder::new()
1368 .port(1)
1369 .event_capacity(1)
1370 .recorder_capacity(1)
1371 .eval_timeout(std::time::Duration::from_secs(1));
1372 assert!(builder.validate().is_ok());
1373
1374 let builder = VictauriBuilder::new()
1375 .port(65535)
1376 .event_capacity(MAX_EVENT_CAPACITY)
1377 .recorder_capacity(MAX_RECORDER_CAPACITY)
1378 .eval_timeout(std::time::Duration::from_secs(MAX_EVAL_TIMEOUT_SECS));
1379 assert!(builder.validate().is_ok());
1380 }
1381}