Skip to main content

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}