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