subx_cli/lib.rs
1//! SubX: Intelligent Subtitle Processing Library
2//!
3//! SubX is a comprehensive Rust library for intelligent subtitle file processing,
4//! featuring AI-powered matching, format conversion, audio synchronization,
5//! and advanced encoding detection capabilities.
6//!
7//! # Key Features
8//!
9//! - **AI-Powered Matching**: Intelligent subtitle file matching and renaming
10//! - **Format Conversion**: Support for multiple subtitle formats (SRT, ASS, VTT, etc.)
11//! - **Audio Synchronization**: Advanced audio-subtitle timing adjustment
12//! - **Encoding Detection**: Automatic character encoding detection and conversion
13//! - **Parallel Processing**: High-performance batch operations
14//! - **Configuration Management**: Flexible multi-source configuration system
15//!
16//! # Architecture Overview
17//!
18//! The library is organized into several key modules:
19//!
20//! - [`cli`] - Command-line interface and argument parsing
21//! - [`commands`] - Implementation of all SubX commands
22//! - [`config`] - Configuration management and validation
23//! - [`core`] - Core processing engines (formats, matching, sync)
24//! - [`error`] - Comprehensive error handling system
25//! - [`services`] - External service integrations (AI, audio processing)
26//!
27//! # Quick Start
28//!
29//! ```rust,no_run
30//! use subx_cli::config::{TestConfigService, ConfigService};
31//!
32//! // Create a configuration service
33//! let config_service = TestConfigService::with_defaults();
34//! let config = config_service.config();
35//!
36//! // Use the configuration for processing...
37//! ```
38//!
39//! # Error Handling
40//!
41//! All operations return a [`Result<T>`] type that wraps [`error::SubXError`]:
42//!
43//! ```rust
44//! use subx_cli::{Result, error::SubXError};
45//!
46//! fn example_operation() -> Result<String> {
47//! // This could fail with various error types
48//! Err(SubXError::config("Missing configuration"))
49//! }
50//! ```
51//!
52//! # Configuration
53//!
54//! SubX supports dependency injection-based configuration:
55//!
56//! ```rust,no_run
57//! use subx_cli::config::{TestConfigService, Config};
58//!
59//! // Create configuration service with AI settings
60//! let config_service = TestConfigService::with_ai_settings("openai", "gpt-4.1");
61//! let config = config_service.config();
62//!
63//! // Access configuration values
64//! println!("AI Provider: {}", config.ai.provider);
65//! println!("AI Model: {}", config.ai.model);
66//! ```
67//!
68//! # Performance Considerations
69//!
70//! - Use [`core::parallel`] for batch operations on large file sets
71//! - Configure appropriate cache settings for repeated operations
72//! - Consider memory usage when processing large audio files
73//!
74//! # Thread Safety
75//!
76//! The library is designed to be thread-safe where appropriate:
77//! - Configuration manager uses `Arc<RwLock<T>>` for shared state
78//! - File operations include rollback capabilities for atomicity
79//! - Parallel processing uses safe concurrency patterns
80//!
81//! # Feature Flags
82//!
83//! SubX supports several optional features:
84//! ```text
85//! - ai - AI service integrations (default)
86//! - audio - Audio processing capabilities (default)
87//! - parallel - Parallel processing support (default)
88//! ```
89
90#![allow(
91 clippy::new_without_default,
92 clippy::manual_clamp,
93 clippy::useless_vec,
94 clippy::items_after_test_module,
95 clippy::needless_borrow,
96 clippy::uninlined_format_args,
97 clippy::collapsible_if
98)]
99#![warn(missing_docs)]
100#![warn(rustdoc::missing_crate_level_docs)]
101
102/// Library version string.
103///
104/// This constant provides the current version of the SubX library,
105/// automatically populated from `Cargo.toml` at compile time.
106///
107/// # Examples
108///
109/// ```rust
110/// use subx_cli::VERSION;
111///
112/// println!("SubX version: {}", VERSION);
113/// ```
114pub const VERSION: &str = env!("CARGO_PKG_VERSION");
115
116pub mod cli;
117pub mod commands;
118pub mod config;
119pub use config::Config;
120// Re-export new configuration service system
121pub use config::{
122 ConfigService, EnvironmentProvider, ProductionConfigService, SystemEnvironmentProvider,
123 TestConfigBuilder, TestConfigService, TestEnvironmentProvider,
124};
125pub mod core;
126pub mod error;
127/// Convenient type alias for `Result<T, SubXError>`.
128///
129/// This type alias simplifies error handling throughout the SubX library
130/// by providing a default error type for all fallible operations.
131pub type Result<T> = error::SubXResult<T>;
132
133pub mod services;
134
135/// Main application structure with dependency injection support.
136///
137/// The `App` struct provides a programmatic interface to SubX functionality,
138/// designed for embedding SubX in other Rust applications or for advanced
139/// use cases requiring fine-grained control over configuration and execution.
140///
141/// # Use Cases
142///
143/// - **Embedding**: Use SubX as a library component in larger applications
144/// - **Testing**: Programmatic testing of SubX functionality with custom configurations
145/// - **Automation**: Scripted execution of SubX operations without shell commands
146/// - **Custom Workflows**: Building complex workflows that combine multiple SubX operations
147///
148/// # vs CLI Interface
149///
150/// | Feature | CLI (`subx` command) | App (Library API) |
151/// |---------|---------------------|-------------------|
152/// | Usage | Command line tool | Embedded in Rust code |
153/// | Config | Files + Environment | Programmatic injection |
154/// | Output | Terminal/stdout | Programmatic control |
155/// | Error Handling | Exit codes | Result types |
156///
157/// # Examples
158///
159/// ## Basic Usage
160///
161/// ```rust,no_run
162/// use subx_cli::{App, config::ProductionConfigService};
163/// use std::sync::Arc;
164///
165/// # async fn example() -> subx_cli::Result<()> {
166/// let config_service = Arc::new(ProductionConfigService::new()?);
167/// let app = App::new(config_service);
168///
169/// // Execute operations programmatically
170/// app.match_files("/movies", true).await?; // dry run
171/// app.convert_files("/subs", "srt", Some("/output")).await?;
172/// # Ok(())
173/// # }
174/// ```
175///
176/// ## With Custom Configuration
177///
178/// ```rust,no_run
179/// use subx_cli::{App, config::{TestConfigService, Config}};
180/// use std::sync::Arc;
181///
182/// # async fn example() -> subx_cli::Result<()> {
183/// let mut config_service = TestConfigService::with_ai_settings("openai", "gpt-4");
184///
185/// let app = App::new(Arc::new(config_service));
186/// app.match_files("/path", false).await?;
187/// # Ok(())
188/// # }
189/// ```
190pub struct App {
191 config_service: std::sync::Arc<dyn config::ConfigService>,
192}
193
194impl App {
195 /// Create a new application instance with the provided configuration service.
196 ///
197 /// # Arguments
198 ///
199 /// * `config_service` - The configuration service to use
200 ///
201 /// # Examples
202 ///
203 /// ```rust,no_run
204 /// use subx_cli::{App, config::TestConfigService};
205 /// use std::sync::Arc;
206 ///
207 /// let config_service = Arc::new(TestConfigService::with_defaults());
208 /// let app = App::new(config_service);
209 /// ```
210 pub fn new(config_service: std::sync::Arc<dyn config::ConfigService>) -> Self {
211 Self { config_service }
212 }
213
214 /// Create a new application instance with the production configuration service.
215 ///
216 /// This is the default way to create an application instance for production use.
217 ///
218 /// # Examples
219 ///
220 /// ```rust,no_run
221 /// use subx_cli::App;
222 ///
223 /// # async fn example() -> subx_cli::Result<()> {
224 /// let app = App::new_with_production_config()?;
225 /// // Ready to use with production configuration
226 /// # Ok(())
227 /// # }
228 /// ```
229 ///
230 /// # Errors
231 ///
232 /// Returns an error if the production configuration service cannot be created.
233 pub fn new_with_production_config() -> Result<Self> {
234 let config_service = std::sync::Arc::new(config::ProductionConfigService::new()?);
235 Ok(Self::new(config_service))
236 }
237
238 /// Run the application with command-line argument parsing.
239 ///
240 /// This method provides a programmatic way to run SubX with CLI-style
241 /// arguments, useful for embedding SubX in other Rust applications.
242 ///
243 /// # Examples
244 ///
245 /// ```rust,no_run
246 /// use subx_cli::{App, config::ProductionConfigService};
247 /// use std::sync::Arc;
248 ///
249 /// # async fn example() -> subx_cli::Result<()> {
250 /// let config_service = Arc::new(ProductionConfigService::new()?);
251 /// let app = App::new(config_service);
252 ///
253 /// // This parses std::env::args() just like the CLI
254 /// app.run().await?;
255 /// # Ok(())
256 /// # }
257 /// ```
258 ///
259 /// # Errors
260 ///
261 /// Returns an error if command execution fails.
262 pub async fn run(&self) -> Result<()> {
263 let cli = <cli::Cli as clap::Parser>::parse();
264 self.handle_command(cli.command).await
265 }
266
267 /// Handle a specific command with the current configuration.
268 ///
269 /// This method allows programmatic execution of specific SubX commands
270 /// without parsing command-line arguments.
271 ///
272 /// # Examples
273 ///
274 /// ```rust,no_run
275 /// use subx_cli::{App, cli::{Commands, MatchArgs}, config::TestConfigService};
276 /// use std::sync::Arc;
277 ///
278 /// # async fn example() -> subx_cli::Result<()> {
279 /// let config_service = Arc::new(TestConfigService::with_defaults());
280 /// let app = App::new(config_service);
281 ///
282 /// let match_args = MatchArgs {
283 /// path: Some("/path/to/files".into()),
284 /// input_paths: vec![],
285 /// dry_run: true,
286 /// confidence: 80,
287 /// recursive: false,
288 /// backup: false,
289 /// copy: false,
290 /// move_files: false,
291 /// no_extract: false,
292 /// };
293 ///
294 /// app.handle_command(Commands::Match(match_args)).await?;
295 /// # Ok(())
296 /// # }
297 /// ```
298 ///
299 /// # Arguments
300 ///
301 /// * `command` - The command to execute
302 ///
303 /// # Errors
304 ///
305 /// Returns an error if command execution fails.
306 pub async fn handle_command(&self, command: cli::Commands) -> Result<()> {
307 // Use the centralized dispatcher to eliminate code duplication
308 crate::commands::dispatcher::dispatch_command(command, self.config_service.clone()).await
309 }
310
311 /// Execute a match operation programmatically.
312 ///
313 /// This is a convenience method for programmatic usage without
314 /// needing to construct the Commands enum manually.
315 ///
316 /// # Examples
317 ///
318 /// ```rust,no_run
319 /// use subx_cli::{App, config::TestConfigService};
320 /// use std::sync::Arc;
321 ///
322 /// # async fn example() -> subx_cli::Result<()> {
323 /// let config_service = Arc::new(TestConfigService::with_defaults());
324 /// let app = App::new(config_service);
325 ///
326 /// // Match files programmatically
327 /// app.match_files("/path/to/files", true).await?; // dry_run = true
328 /// # Ok(())
329 /// # }
330 /// ```
331 ///
332 /// # Arguments
333 ///
334 /// * `input_path` - Path to the directory or file to process
335 /// * `dry_run` - Whether to perform a dry run (no actual changes)
336 ///
337 /// # Errors
338 ///
339 /// Returns an error if the match operation fails.
340 pub async fn match_files(&self, input_path: &str, dry_run: bool) -> Result<()> {
341 let args = cli::MatchArgs {
342 path: Some(input_path.into()),
343 input_paths: vec![],
344 dry_run,
345 confidence: 80,
346 recursive: false,
347 backup: false,
348 copy: false,
349 move_files: false,
350 no_extract: false,
351 };
352 self.handle_command(cli::Commands::Match(args)).await
353 }
354
355 /// Convert subtitle files programmatically.
356 ///
357 /// # Examples
358 ///
359 /// ```rust,no_run
360 /// use subx_cli::{App, config::TestConfigService};
361 /// use std::sync::Arc;
362 ///
363 /// # async fn example() -> subx_cli::Result<()> {
364 /// let config_service = Arc::new(TestConfigService::with_defaults());
365 /// let app = App::new(config_service);
366 ///
367 /// // Convert to SRT format
368 /// app.convert_files("/path/to/subtitles", "srt", Some("/output/path")).await?;
369 /// # Ok(())
370 /// # }
371 /// ```
372 ///
373 /// # Arguments
374 ///
375 /// * `input_path` - Path to subtitle files to convert
376 /// * `output_format` - Target format ("srt", "ass", "vtt", etc.)
377 /// * `output_path` - Optional output directory path
378 ///
379 /// # Errors
380 ///
381 /// Returns an error if the conversion fails.
382 pub async fn convert_files(
383 &self,
384 input_path: &str,
385 output_format: &str,
386 output_path: Option<&str>,
387 ) -> Result<()> {
388 let format = match output_format.to_lowercase().as_str() {
389 "srt" => cli::OutputSubtitleFormat::Srt,
390 "ass" => cli::OutputSubtitleFormat::Ass,
391 "vtt" => cli::OutputSubtitleFormat::Vtt,
392 "sub" => cli::OutputSubtitleFormat::Sub,
393 _ => {
394 return Err(error::SubXError::CommandExecution(format!(
395 "Unsupported output format: {output_format}. Supported formats: srt, ass, vtt, sub"
396 )));
397 }
398 };
399
400 let args = cli::ConvertArgs {
401 input: Some(input_path.into()),
402 input_paths: vec![],
403 recursive: false,
404 format: Some(format),
405 output: output_path.map(Into::into),
406 keep_original: false,
407 encoding: "utf-8".to_string(),
408 no_extract: false,
409 };
410 self.handle_command(cli::Commands::Convert(args)).await
411 }
412
413 /// Synchronize subtitle files programmatically.
414 ///
415 /// # Examples
416 ///
417 /// ```rust,no_run
418 /// use subx_cli::{App, config::TestConfigService};
419 /// use std::sync::Arc;
420 ///
421 /// # async fn example() -> subx_cli::Result<()> {
422 /// let config_service = Arc::new(TestConfigService::with_defaults());
423 /// let app = App::new(config_service);
424 ///
425 /// // Synchronize using VAD method
426 /// app.sync_files("/path/to/video.mp4", "/path/to/subtitle.srt", "vad").await?;
427 /// # Ok(())
428 /// # }
429 /// ```
430 ///
431 /// # Arguments
432 ///
433 /// * `video_path` - Path to video file for audio analysis
434 /// * `subtitle_path` - Path to subtitle file to synchronize
435 /// * `method` - Synchronization method ("vad", "manual")
436 ///
437 /// # Errors
438 ///
439 /// Returns an error if synchronization fails.
440 pub async fn sync_files(
441 &self,
442 video_path: &str,
443 subtitle_path: &str,
444 method: &str,
445 ) -> Result<()> {
446 let sync_method = match method.to_lowercase().as_str() {
447 "vad" => Some(cli::SyncMethodArg::Vad),
448 "manual" => Some(cli::SyncMethodArg::Manual),
449 _ => {
450 return Err(error::SubXError::CommandExecution(format!(
451 "Unsupported sync method: {method}. Supported methods: vad, manual"
452 )));
453 }
454 };
455
456 let args = cli::SyncArgs {
457 positional_paths: Vec::new(),
458 video: Some(video_path.into()),
459 subtitle: Some(subtitle_path.into()),
460 input_paths: vec![],
461 recursive: false,
462 offset: None,
463 method: sync_method,
464 window: 30,
465 vad_sensitivity: None,
466 output: None,
467 verbose: false,
468 dry_run: false,
469 force: false,
470 batch: None,
471 no_extract: false,
472 };
473 self.handle_command(cli::Commands::Sync(args)).await
474 }
475
476 /// Synchronize subtitle files with manual offset.
477 ///
478 /// # Examples
479 ///
480 /// ```rust,no_run
481 /// use subx_cli::{App, config::TestConfigService};
482 /// use std::sync::Arc;
483 ///
484 /// # async fn example() -> subx_cli::Result<()> {
485 /// let config_service = Arc::new(TestConfigService::with_defaults());
486 /// let app = App::new(config_service);
487 ///
488 /// // Apply +2.5 second offset to subtitles
489 /// app.sync_files_with_offset("/path/to/subtitle.srt", 2.5).await?;
490 /// # Ok(())
491 /// # }
492 /// ```
493 ///
494 /// # Arguments
495 ///
496 /// * `subtitle_path` - Path to subtitle file to synchronize
497 /// * `offset` - Time offset in seconds (positive delays, negative advances)
498 ///
499 /// # Errors
500 ///
501 /// Returns an error if synchronization fails.
502 pub async fn sync_files_with_offset(&self, subtitle_path: &str, offset: f32) -> Result<()> {
503 let args = cli::SyncArgs {
504 positional_paths: Vec::new(),
505 video: None,
506 subtitle: Some(subtitle_path.into()),
507 input_paths: vec![],
508 recursive: false,
509 offset: Some(offset),
510 method: None,
511 window: 30,
512 vad_sensitivity: None,
513 output: None,
514 verbose: false,
515 dry_run: false,
516 force: false,
517 batch: None,
518 no_extract: false,
519 };
520 self.handle_command(cli::Commands::Sync(args)).await
521 }
522
523 /// Get a reference to the configuration service.
524 ///
525 /// This allows access to the configuration service for testing or
526 /// advanced use cases.
527 pub fn config_service(&self) -> &std::sync::Arc<dyn config::ConfigService> {
528 &self.config_service
529 }
530
531 /// Get the current configuration.
532 ///
533 /// This is a convenience method that retrieves the configuration
534 /// from the configured service.
535 ///
536 /// # Errors
537 ///
538 /// Returns an error if configuration loading fails.
539 pub fn get_config(&self) -> Result<config::Config> {
540 self.config_service.get_config()
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547 use std::sync::Arc;
548
549 fn is_expected_test_error(e: &error::SubXError) -> bool {
550 let msg = format!("{e:?}");
551 msg.contains("NotFound")
552 || msg.contains("No subtitle files found")
553 || msg.contains("No video files found")
554 || msg.contains("Config")
555 || msg.contains("no such file")
556 || msg.contains("cannot find")
557 || msg.contains("No input")
558 || msg.contains("No files")
559 || msg.contains("FileNotFound")
560 || msg.contains("IoError")
561 || msg.contains("PathNotFound")
562 || msg.contains("InvalidInput")
563 || msg.contains("CommandExecution")
564 || msg.contains("NoInputSpecified")
565 }
566
567 #[test]
568 fn test_version_is_not_empty() {
569 assert!(!VERSION.is_empty());
570 }
571
572 #[test]
573 fn test_version_matches_cargo_pkg_version() {
574 assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
575 }
576
577 #[test]
578 fn test_app_new_stores_config_service() {
579 let config_service = Arc::new(TestConfigService::with_defaults());
580 let app = App::new(config_service.clone());
581 // Verify config_service() returns a valid Arc
582 let _ = app.config_service();
583 }
584
585 #[test]
586 fn test_app_get_config_returns_config() {
587 let config_service = Arc::new(TestConfigService::with_defaults());
588 let app = App::new(config_service);
589 let config = app.get_config().expect("get_config should succeed");
590 // Just verify we got a config object with a valid AI provider field
591 let _ = config.ai.provider;
592 }
593
594 #[test]
595 fn test_app_get_config_with_ai_settings() {
596 let config_service = Arc::new(TestConfigService::with_ai_settings("openai", "gpt-4.1"));
597 let app = App::new(config_service);
598 let config = app.get_config().expect("get_config should succeed");
599 assert_eq!(config.ai.provider, "openai");
600 assert_eq!(config.ai.model, "gpt-4.1");
601 }
602
603 #[test]
604 fn test_app_config_service_getter() {
605 let config_service = Arc::new(TestConfigService::with_defaults());
606 let app = App::new(config_service);
607 // The returned Arc should be usable (get_config succeeds)
608 let svc = app.config_service();
609 assert!(svc.get_config().is_ok());
610 }
611
612 #[tokio::test]
613 async fn test_convert_files_unknown_format_returns_error() {
614 let config_service = Arc::new(TestConfigService::with_defaults());
615 let app = App::new(config_service);
616 let result = app.convert_files("/nonexistent", "xyz_unknown", None).await;
617 assert!(result.is_err());
618 let err_msg = format!("{:?}", result.unwrap_err());
619 assert!(
620 err_msg.contains("Unsupported output format"),
621 "Error should mention unsupported format, got: {err_msg}"
622 );
623 }
624
625 #[tokio::test]
626 async fn test_convert_files_srt_format_accepted() {
627 let config_service = Arc::new(TestConfigService::with_defaults());
628 let app = App::new(config_service);
629 let result = app.convert_files("/nonexistent_path", "srt", None).await;
630 match result {
631 Ok(_) => {}
632 Err(e) => assert!(
633 is_expected_test_error(&e),
634 "Unexpected error for srt format: {e:?}"
635 ),
636 }
637 }
638
639 #[tokio::test]
640 async fn test_convert_files_ass_format_accepted() {
641 let config_service = Arc::new(TestConfigService::with_defaults());
642 let app = App::new(config_service);
643 let result = app.convert_files("/nonexistent_path", "ass", None).await;
644 match result {
645 Ok(_) => {}
646 Err(e) => assert!(
647 is_expected_test_error(&e),
648 "Unexpected error for ass format: {e:?}"
649 ),
650 }
651 }
652
653 #[tokio::test]
654 async fn test_convert_files_vtt_format_accepted() {
655 let config_service = Arc::new(TestConfigService::with_defaults());
656 let app = App::new(config_service);
657 let result = app.convert_files("/nonexistent_path", "vtt", None).await;
658 match result {
659 Ok(_) => {}
660 Err(e) => assert!(
661 is_expected_test_error(&e),
662 "Unexpected error for vtt format: {e:?}"
663 ),
664 }
665 }
666
667 #[tokio::test]
668 async fn test_convert_files_sub_format_accepted() {
669 let config_service = Arc::new(TestConfigService::with_defaults());
670 let app = App::new(config_service);
671 let result = app.convert_files("/nonexistent_path", "sub", None).await;
672 match result {
673 Ok(_) => {}
674 Err(e) => assert!(
675 is_expected_test_error(&e),
676 "Unexpected error for sub format: {e:?}"
677 ),
678 }
679 }
680
681 #[tokio::test]
682 async fn test_convert_files_format_case_insensitive() {
683 let config_service = Arc::new(TestConfigService::with_defaults());
684 let app = App::new(config_service);
685 // Uppercase format should be treated the same as lowercase
686 let result = app.convert_files("/nonexistent_path", "SRT", None).await;
687 match result {
688 Ok(_) => {}
689 Err(e) => assert!(
690 is_expected_test_error(&e),
691 "Unexpected error for uppercase SRT: {e:?}"
692 ),
693 }
694 }
695
696 #[tokio::test]
697 async fn test_sync_files_unknown_method_returns_error() {
698 let config_service = Arc::new(TestConfigService::with_defaults());
699 let app = App::new(config_service);
700 let result = app
701 .sync_files("/video.mp4", "/subtitle.srt", "unknown_method")
702 .await;
703 assert!(result.is_err());
704 let err_msg = format!("{:?}", result.unwrap_err());
705 assert!(
706 err_msg.contains("Unsupported sync method"),
707 "Error should mention unsupported method, got: {err_msg}"
708 );
709 }
710
711 #[tokio::test]
712 async fn test_sync_files_vad_method_accepted() {
713 let config_service = Arc::new(TestConfigService::with_defaults());
714 let app = App::new(config_service);
715 let result = app
716 .sync_files("/nonexistent_video.mp4", "/nonexistent_sub.srt", "vad")
717 .await;
718 match result {
719 Ok(_) => {}
720 Err(e) => assert!(
721 is_expected_test_error(&e),
722 "Unexpected error for vad method: {e:?}"
723 ),
724 }
725 }
726
727 #[tokio::test]
728 async fn test_sync_files_manual_method_accepted() {
729 let config_service = Arc::new(TestConfigService::with_defaults());
730 let app = App::new(config_service);
731 let result = app
732 .sync_files("/nonexistent_video.mp4", "/nonexistent_sub.srt", "manual")
733 .await;
734 match result {
735 Ok(_) => {}
736 Err(e) => assert!(
737 is_expected_test_error(&e),
738 "Unexpected error for manual method: {e:?}"
739 ),
740 }
741 }
742
743 #[tokio::test]
744 async fn test_sync_files_method_case_insensitive() {
745 let config_service = Arc::new(TestConfigService::with_defaults());
746 let app = App::new(config_service);
747 let result = app
748 .sync_files("/nonexistent_video.mp4", "/nonexistent_sub.srt", "VAD")
749 .await;
750 match result {
751 Ok(_) => {}
752 Err(e) => assert!(
753 is_expected_test_error(&e),
754 "Uppercase VAD should not give format error, got: {e:?}"
755 ),
756 }
757 }
758
759 #[tokio::test]
760 async fn test_sync_files_with_offset_accepted() {
761 let config_service = Arc::new(TestConfigService::with_defaults());
762 let app = App::new(config_service);
763 let result = app
764 .sync_files_with_offset("/nonexistent_sub.srt", 2.5)
765 .await;
766 match result {
767 Ok(_) => {}
768 Err(e) => assert!(
769 is_expected_test_error(&e),
770 "Unexpected error for sync with offset: {e:?}"
771 ),
772 }
773 }
774
775 #[tokio::test]
776 async fn test_sync_files_with_negative_offset() {
777 let config_service = Arc::new(TestConfigService::with_defaults());
778 let app = App::new(config_service);
779 let result = app
780 .sync_files_with_offset("/nonexistent_sub.srt", -1.5)
781 .await;
782 match result {
783 Ok(_) => {}
784 Err(e) => assert!(
785 is_expected_test_error(&e),
786 "Unexpected error for sync with negative offset: {e:?}"
787 ),
788 }
789 }
790
791 #[tokio::test]
792 async fn test_match_files_dry_run() {
793 let config_service = Arc::new(TestConfigService::with_ai_settings(
794 "test_provider",
795 "test_model",
796 ));
797 let app = App::new(config_service);
798 let result = app.match_files("/nonexistent_subx_test_path", true).await;
799 match result {
800 Ok(_) => {}
801 Err(e) => assert!(
802 is_expected_test_error(&e),
803 "Unexpected error for match dry run: {e:?}"
804 ),
805 }
806 }
807
808 #[test]
809 fn test_result_type_alias_ok() {
810 let r: Result<i32> = Ok(42);
811 assert_eq!(r.unwrap(), 42);
812 }
813
814 #[test]
815 fn test_result_type_alias_err() {
816 let r: Result<i32> = Err(error::SubXError::config("test error"));
817 assert!(r.is_err());
818 }
819}