Skip to main content

batuta/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4/// Batuta project configuration
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct BatutaConfig {
7    /// Configuration file version
8    pub version: String,
9
10    /// Project metadata
11    pub project: ProjectConfig,
12
13    /// Source code configuration
14    pub source: SourceConfig,
15
16    /// Transpilation settings
17    pub transpilation: TranspilationConfig,
18
19    /// Optimization settings
20    pub optimization: OptimizationConfig,
21
22    /// Validation settings
23    pub validation: ValidationConfig,
24
25    /// Build settings
26    pub build: BuildConfig,
27}
28
29impl Default for BatutaConfig {
30    fn default() -> Self {
31        Self {
32            version: "1.0".to_string(),
33            project: ProjectConfig::default(),
34            source: SourceConfig::default(),
35            transpilation: TranspilationConfig::default(),
36            optimization: OptimizationConfig::default(),
37            validation: ValidationConfig::default(),
38            build: BuildConfig::default(),
39        }
40    }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ProjectConfig {
45    /// Project name
46    pub name: String,
47
48    /// Project description
49    pub description: Option<String>,
50
51    /// Primary language of source project
52    pub primary_language: Option<String>,
53
54    /// Authors
55    pub authors: Vec<String>,
56
57    /// License
58    pub license: Option<String>,
59}
60
61impl Default for ProjectConfig {
62    fn default() -> Self {
63        Self {
64            name: "untitled".to_string(),
65            description: None,
66            primary_language: None,
67            authors: vec![],
68            license: Some("MIT".to_string()),
69        }
70    }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct SourceConfig {
75    /// Source code directory (relative to config file)
76    pub path: PathBuf,
77
78    /// Files/directories to exclude
79    pub exclude: Vec<String>,
80
81    /// Files/directories to include (overrides exclude)
82    pub include: Vec<String>,
83}
84
85impl Default for SourceConfig {
86    fn default() -> Self {
87        Self {
88            path: PathBuf::from("."),
89            exclude: vec![
90                ".git".to_string(),
91                "target".to_string(),
92                "build".to_string(),
93                "dist".to_string(),
94                "node_modules".to_string(),
95                "__pycache__".to_string(),
96                "*.pyc".to_string(),
97                ".venv".to_string(),
98                "venv".to_string(),
99            ],
100            include: vec![],
101        }
102    }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct TranspilationConfig {
107    /// Output directory for generated Rust code
108    pub output_dir: PathBuf,
109
110    /// Enable incremental compilation
111    pub incremental: bool,
112
113    /// Enable caching
114    pub cache: bool,
115
116    /// Generate Ruchy instead of pure Rust
117    pub use_ruchy: bool,
118
119    /// Ruchy strictness level (permissive, gradual, strict)
120    pub ruchy_strictness: Option<String>,
121
122    /// Specific modules to transpile (empty = all)
123    pub modules: Vec<String>,
124
125    /// Tool-specific settings
126    pub decy: DecyConfig,
127    pub depyler: DepylerConfig,
128    pub bashrs: BashrsConfig,
129}
130
131impl Default for TranspilationConfig {
132    fn default() -> Self {
133        Self {
134            output_dir: PathBuf::from("./rust-output"),
135            incremental: true,
136            cache: true,
137            use_ruchy: false,
138            ruchy_strictness: Some("gradual".to_string()),
139            modules: vec![],
140            decy: DecyConfig::default(),
141            depyler: DepylerConfig::default(),
142            bashrs: BashrsConfig::default(),
143        }
144    }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct DecyConfig {
149    /// Enable ownership inference
150    pub ownership_inference: bool,
151
152    /// Generate actionable diagnostics
153    pub actionable_diagnostics: bool,
154
155    /// Use StaticFixer integration
156    pub use_static_fixer: bool,
157}
158
159impl Default for DecyConfig {
160    fn default() -> Self {
161        Self { ownership_inference: true, actionable_diagnostics: true, use_static_fixer: true }
162    }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct DepylerConfig {
167    /// Enable type inference
168    pub type_inference: bool,
169
170    /// Convert NumPy to Trueno
171    pub numpy_to_trueno: bool,
172
173    /// Convert sklearn to Aprender
174    pub sklearn_to_aprender: bool,
175
176    /// Convert PyTorch to Realizar
177    pub pytorch_to_realizar: bool,
178}
179
180impl Default for DepylerConfig {
181    fn default() -> Self {
182        Self {
183            type_inference: true,
184            numpy_to_trueno: true,
185            sklearn_to_aprender: true,
186            pytorch_to_realizar: true,
187        }
188    }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct BashrsConfig {
193    /// Target shell compatibility
194    pub target_shell: String,
195
196    /// Generate CLI using clap
197    pub use_clap: bool,
198}
199
200impl Default for BashrsConfig {
201    fn default() -> Self {
202        Self { target_shell: "bash".to_string(), use_clap: true }
203    }
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct OptimizationConfig {
208    /// Optimization profile (fast, balanced, aggressive)
209    pub profile: String,
210
211    /// Enable SIMD vectorization
212    pub enable_simd: bool,
213
214    /// Enable GPU acceleration
215    pub enable_gpu: bool,
216
217    /// GPU dispatch threshold (matrix size)
218    pub gpu_threshold: usize,
219
220    /// Use Mixture-of-Experts routing
221    pub use_moe_routing: bool,
222
223    /// Trueno backend preferences
224    pub trueno: TruenoConfig,
225}
226
227impl Default for OptimizationConfig {
228    fn default() -> Self {
229        Self {
230            profile: "balanced".to_string(),
231            enable_simd: true,
232            enable_gpu: false,
233            gpu_threshold: 500,
234            use_moe_routing: false,
235            trueno: TruenoConfig::default(),
236        }
237    }
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct TruenoConfig {
242    /// Preferred backends in priority order
243    pub backends: Vec<String>,
244
245    /// Enable adaptive threshold learning
246    pub adaptive_thresholds: bool,
247
248    /// Initial CPU threshold
249    pub cpu_threshold: usize,
250}
251
252impl Default for TruenoConfig {
253    fn default() -> Self {
254        Self {
255            backends: vec!["simd".to_string(), "cpu".to_string()],
256            adaptive_thresholds: false,
257            cpu_threshold: 500,
258        }
259    }
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct ValidationConfig {
264    /// Enable syscall tracing
265    pub trace_syscalls: bool,
266
267    /// Run original test suite
268    pub run_original_tests: bool,
269
270    /// Generate diff output
271    pub diff_output: bool,
272
273    /// Run benchmarks
274    pub benchmark: bool,
275
276    /// Renacer configuration
277    pub renacer: RenacerConfig,
278}
279
280impl Default for ValidationConfig {
281    fn default() -> Self {
282        Self {
283            trace_syscalls: true,
284            run_original_tests: true,
285            diff_output: true,
286            benchmark: false,
287            renacer: RenacerConfig::default(),
288        }
289    }
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct RenacerConfig {
294    /// Syscalls to trace (empty = all)
295    pub trace_syscalls: Vec<String>,
296
297    /// Output format
298    pub output_format: String,
299}
300
301impl Default for RenacerConfig {
302    fn default() -> Self {
303        Self { trace_syscalls: vec![], output_format: "json".to_string() }
304    }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct BuildConfig {
309    /// Build in release mode
310    pub release: bool,
311
312    /// Target platform (empty = native)
313    pub target: Option<String>,
314
315    /// Enable WebAssembly build
316    pub wasm: bool,
317
318    /// Additional cargo flags
319    pub cargo_flags: Vec<String>,
320}
321
322impl Default for BuildConfig {
323    fn default() -> Self {
324        Self { release: true, target: None, wasm: false, cargo_flags: vec![] }
325    }
326}
327
328// ============================================================================
329// Private RAG Configuration
330// ============================================================================
331
332/// Filename for the private configuration file (git-ignored).
333pub const PRIVATE_CONFIG_FILENAME: &str = ".batuta-private.toml";
334
335/// Top-level private configuration loaded from `.batuta-private.toml`.
336#[derive(Debug, Clone, Default, Serialize, Deserialize)]
337pub struct PrivateConfig {
338    /// Private directory extensions for RAG indexing.
339    #[serde(default)]
340    pub private: PrivateExtensions,
341}
342
343/// Private directories and endpoints to merge into the RAG index.
344#[derive(Debug, Clone, Default, Serialize, Deserialize)]
345pub struct PrivateExtensions {
346    /// Additional Rust stack directories to index.
347    #[serde(default)]
348    pub rust_stack_dirs: Vec<String>,
349
350    /// Additional Rust corpus directories to index.
351    #[serde(default)]
352    pub rust_corpus_dirs: Vec<String>,
353
354    /// Additional Python corpus directories to index.
355    #[serde(default)]
356    pub python_corpus_dirs: Vec<String>,
357
358    /// Future: remote RAG endpoints (Phase 2).
359    #[serde(default)]
360    pub endpoints: Vec<PrivateEndpoint>,
361}
362
363/// A remote RAG endpoint (Phase 2 — not yet implemented).
364#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct PrivateEndpoint {
366    pub name: String,
367    #[serde(rename = "type")]
368    pub endpoint_type: String,
369    pub host: String,
370    pub index_path: String,
371}
372
373impl PrivateConfig {
374    /// Load from `.batuta-private.toml`, searching multiple locations.
375    ///
376    /// Search order:
377    /// 1. `$HOME/.batuta-private.toml` (user-global config)
378    /// 2. `./.batuta-private.toml` (current directory, for development)
379    ///
380    /// Returns `Ok(None)` if no config file exists, `Err` if malformed.
381    #[cfg(feature = "native")]
382    pub fn load_optional() -> anyhow::Result<Option<Self>> {
383        if let Some(path) = Self::find_config_path() {
384            let content = std::fs::read_to_string(&path)?;
385            let config: Self = toml::from_str(&content)?;
386            return Ok(Some(config));
387        }
388        Ok(None)
389    }
390
391    /// Stub when native feature is disabled — no config file support.
392    #[cfg(not(feature = "native"))]
393    pub fn load_optional() -> anyhow::Result<Option<Self>> {
394        Ok(None)
395    }
396
397    /// Find the config file path, checking home dir then cwd.
398    #[cfg(feature = "native")]
399    fn find_config_path() -> Option<std::path::PathBuf> {
400        // 1. Home directory (works from any cwd)
401        if let Some(home) = dirs::home_dir() {
402            let home_path = home.join(PRIVATE_CONFIG_FILENAME);
403            if home_path.exists() {
404                return Some(home_path);
405            }
406        }
407        // 2. Current directory (development fallback)
408        let cwd_path = std::path::PathBuf::from(PRIVATE_CONFIG_FILENAME);
409        if cwd_path.exists() {
410            return Some(cwd_path);
411        }
412        None
413    }
414
415    /// Whether any private directories are configured.
416    pub fn has_dirs(&self) -> bool {
417        !self.private.rust_stack_dirs.is_empty()
418            || !self.private.rust_corpus_dirs.is_empty()
419            || !self.private.python_corpus_dirs.is_empty()
420    }
421
422    /// Total number of private directories across all categories.
423    pub fn dir_count(&self) -> usize {
424        self.private.rust_stack_dirs.len()
425            + self.private.rust_corpus_dirs.len()
426            + self.private.python_corpus_dirs.len()
427    }
428}
429
430impl BatutaConfig {
431    /// Load configuration from TOML file
432    #[cfg(feature = "native")]
433    pub fn load(path: &std::path::Path) -> anyhow::Result<Self> {
434        let content = std::fs::read_to_string(path)?;
435        let config = toml::from_str(&content)?;
436        Ok(config)
437    }
438
439    /// Save configuration to TOML file
440    #[cfg(feature = "native")]
441    pub fn save(&self, path: &std::path::Path) -> anyhow::Result<()> {
442        let content = toml::to_string_pretty(self)?;
443        std::fs::write(path, content)?;
444        Ok(())
445    }
446
447    /// Create a new config from project analysis
448    pub fn from_analysis(analysis: &crate::types::ProjectAnalysis) -> Self {
449        let mut config = Self::default();
450
451        // Set project name from directory
452        if let Some(name) = analysis.root_path.file_name() {
453            config.project.name = name.to_string_lossy().to_string();
454        }
455
456        // Set primary language
457        if let Some(lang) = &analysis.primary_language {
458            config.project.primary_language = Some(format!("{}", lang));
459        }
460
461        // Configure transpilation based on detected dependencies
462        if analysis.has_ml_dependencies() {
463            config.transpilation.depyler.numpy_to_trueno = true;
464            config.transpilation.depyler.sklearn_to_aprender = true;
465            config.transpilation.depyler.pytorch_to_realizar = true;
466        }
467
468        config
469    }
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475    use std::path::PathBuf;
476    use tempfile::TempDir;
477
478    // ============================================================================
479    // DEFAULT VALUE TESTS
480    // ============================================================================
481
482    #[test]
483    fn test_batuta_config_default() {
484        let config = BatutaConfig::default();
485
486        assert_eq!(config.version, "1.0");
487        assert_eq!(config.project.name, "untitled");
488        assert_eq!(config.source.path, PathBuf::from("."));
489        assert_eq!(config.transpilation.output_dir, PathBuf::from("./rust-output"));
490        assert_eq!(config.optimization.profile, "balanced");
491        assert!(config.validation.trace_syscalls);
492        assert!(config.build.release);
493    }
494
495    #[test]
496    fn test_project_config_default() {
497        let config = ProjectConfig::default();
498
499        assert_eq!(config.name, "untitled");
500        assert!(config.description.is_none());
501        assert!(config.primary_language.is_none());
502        assert!(config.authors.is_empty());
503        assert_eq!(config.license, Some("MIT".to_string()));
504    }
505
506    #[test]
507    fn test_source_config_default() {
508        let config = SourceConfig::default();
509
510        assert_eq!(config.path, PathBuf::from("."));
511        assert!(config.exclude.contains(&".git".to_string()));
512        assert!(config.exclude.contains(&"target".to_string()));
513        assert!(config.exclude.contains(&"node_modules".to_string()));
514        assert!(config.exclude.contains(&"__pycache__".to_string()));
515        assert!(config.include.is_empty());
516    }
517
518    #[test]
519    fn test_transpilation_config_default() {
520        let config = TranspilationConfig::default();
521
522        assert_eq!(config.output_dir, PathBuf::from("./rust-output"));
523        assert!(config.incremental);
524        assert!(config.cache);
525        assert!(!config.use_ruchy);
526        assert_eq!(config.ruchy_strictness, Some("gradual".to_string()));
527        assert!(config.modules.is_empty());
528    }
529
530    #[test]
531    fn test_decy_config_default() {
532        let config = DecyConfig::default();
533
534        assert!(config.ownership_inference);
535        assert!(config.actionable_diagnostics);
536        assert!(config.use_static_fixer);
537    }
538
539    #[test]
540    fn test_depyler_config_default() {
541        let config = DepylerConfig::default();
542
543        assert!(config.type_inference);
544        assert!(config.numpy_to_trueno);
545        assert!(config.sklearn_to_aprender);
546        assert!(config.pytorch_to_realizar);
547    }
548
549    #[test]
550    fn test_bashrs_config_default() {
551        let config = BashrsConfig::default();
552
553        assert_eq!(config.target_shell, "bash");
554        assert!(config.use_clap);
555    }
556
557    #[test]
558    fn test_optimization_config_default() {
559        let config = OptimizationConfig::default();
560
561        assert_eq!(config.profile, "balanced");
562        assert!(config.enable_simd);
563        assert!(!config.enable_gpu);
564        assert_eq!(config.gpu_threshold, 500);
565        assert!(!config.use_moe_routing);
566    }
567
568    #[test]
569    fn test_trueno_config_default() {
570        let config = TruenoConfig::default();
571
572        assert_eq!(config.backends, vec!["simd".to_string(), "cpu".to_string()]);
573        assert!(!config.adaptive_thresholds);
574        assert_eq!(config.cpu_threshold, 500);
575    }
576
577    #[test]
578    fn test_validation_config_default() {
579        let config = ValidationConfig::default();
580
581        assert!(config.trace_syscalls);
582        assert!(config.run_original_tests);
583        assert!(config.diff_output);
584        assert!(!config.benchmark);
585    }
586
587    #[test]
588    fn test_renacer_config_default() {
589        let config = RenacerConfig::default();
590
591        assert!(config.trace_syscalls.is_empty());
592        assert_eq!(config.output_format, "json");
593    }
594
595    #[test]
596    fn test_build_config_default() {
597        let config = BuildConfig::default();
598
599        assert!(config.release);
600        assert!(config.target.is_none());
601        assert!(!config.wasm);
602        assert!(config.cargo_flags.is_empty());
603    }
604
605    // ============================================================================
606    // LOAD/SAVE TESTS
607    // ============================================================================
608
609    #[test]
610    fn test_save_and_load_config() {
611        let temp_dir = TempDir::new().expect("tempdir creation failed");
612        let config_path = temp_dir.path().join("batuta.toml");
613
614        // Create a config with custom values
615        let mut config = BatutaConfig::default();
616        config.project.name = "test-project".to_string();
617        config.project.description = Some("A test project".to_string());
618        config.optimization.enable_gpu = true;
619        config.optimization.gpu_threshold = 1000;
620
621        // Save config
622        config.save(&config_path).expect("save failed");
623
624        // Verify file exists
625        assert!(config_path.exists());
626
627        // Load config
628        let loaded_config = BatutaConfig::load(&config_path).expect("unexpected failure");
629
630        // Verify loaded values match
631        assert_eq!(loaded_config.project.name, "test-project");
632        assert_eq!(loaded_config.project.description, Some("A test project".to_string()));
633        assert!(loaded_config.optimization.enable_gpu);
634        assert_eq!(loaded_config.optimization.gpu_threshold, 1000);
635    }
636
637    #[test]
638    fn test_load_nonexistent_file() {
639        let result = BatutaConfig::load(std::path::Path::new("/nonexistent/file.toml"));
640        assert!(result.is_err());
641    }
642
643    #[test]
644    fn test_load_invalid_toml() {
645        let temp_dir = TempDir::new().expect("tempdir creation failed");
646        let config_path = temp_dir.path().join("invalid.toml");
647
648        // Write invalid TOML
649        std::fs::write(&config_path, "invalid toml content [[[").expect("fs write failed");
650
651        let result = BatutaConfig::load(&config_path);
652        assert!(result.is_err());
653    }
654
655    #[test]
656    fn test_save_config_creates_parent_dirs() {
657        let temp_dir = TempDir::new().expect("tempdir creation failed");
658        let nested_path = temp_dir.path().join("nested").join("dir").join("batuta.toml");
659
660        // Create parent directories
661        if let Some(parent) = nested_path.parent() {
662            std::fs::create_dir_all(parent).expect("mkdir failed");
663        }
664
665        let config = BatutaConfig::default();
666        let result = config.save(&nested_path);
667
668        assert!(result.is_ok());
669        assert!(nested_path.exists());
670    }
671
672    #[test]
673    fn test_save_config_toml_format() {
674        let temp_dir = TempDir::new().expect("tempdir creation failed");
675        let config_path = temp_dir.path().join("batuta.toml");
676
677        let config = BatutaConfig::default();
678        config.save(&config_path).expect("save failed");
679
680        // Read the TOML file
681        let content = std::fs::read_to_string(&config_path).expect("fs read failed");
682
683        // Verify it contains expected sections
684        assert!(content.contains("[project]"));
685        assert!(content.contains("[source]"));
686        assert!(content.contains("[transpilation]"));
687        assert!(content.contains("[optimization]"));
688        assert!(content.contains("[validation]"));
689        assert!(content.contains("[build]"));
690    }
691
692    // ============================================================================
693    // FROM_ANALYSIS TESTS
694    // ============================================================================
695
696    #[test]
697    fn test_from_analysis_basic() {
698        let analysis = crate::types::ProjectAnalysis {
699            root_path: PathBuf::from("/home/user/my-project"),
700            total_files: 10,
701            total_lines: 1000,
702            languages: vec![],
703            primary_language: Some(crate::types::Language::Python),
704            dependencies: vec![],
705            tdg_score: Some(85.0),
706        };
707
708        let config = BatutaConfig::from_analysis(&analysis);
709
710        assert_eq!(config.project.name, "my-project");
711        assert_eq!(config.project.primary_language, Some("Python".to_string()));
712    }
713
714    #[test]
715    fn test_from_analysis_with_ml_dependencies() {
716        let analysis = crate::types::ProjectAnalysis {
717            root_path: PathBuf::from("/test/project"),
718            total_files: 5,
719            total_lines: 500,
720            languages: vec![],
721            primary_language: Some(crate::types::Language::Python),
722            dependencies: vec![crate::types::DependencyInfo {
723                manager: crate::types::DependencyManager::Pip,
724                file_path: PathBuf::from("requirements.txt"),
725                count: Some(3),
726            }],
727            tdg_score: None,
728        };
729
730        let config = BatutaConfig::from_analysis(&analysis);
731
732        // ML frameworks should be enabled by default
733        assert!(config.transpilation.depyler.numpy_to_trueno);
734        assert!(config.transpilation.depyler.sklearn_to_aprender);
735        assert!(config.transpilation.depyler.pytorch_to_realizar);
736    }
737
738    #[test]
739    fn test_from_analysis_without_ml_dependencies() {
740        let analysis = crate::types::ProjectAnalysis {
741            root_path: PathBuf::from("/test/project"),
742            total_files: 5,
743            total_lines: 500,
744            languages: vec![],
745            primary_language: Some(crate::types::Language::Python),
746            dependencies: vec![crate::types::DependencyInfo {
747                manager: crate::types::DependencyManager::Pip,
748                file_path: PathBuf::from("requirements.txt"),
749                count: Some(1),
750            }],
751            tdg_score: None,
752        };
753
754        let config = BatutaConfig::from_analysis(&analysis);
755
756        // Should still have ML framework support enabled by default
757        assert!(config.transpilation.depyler.numpy_to_trueno);
758    }
759
760    #[test]
761    fn test_from_analysis_rust_project() {
762        let analysis = crate::types::ProjectAnalysis {
763            root_path: PathBuf::from("/rust/project"),
764            total_files: 20,
765            total_lines: 2000,
766            languages: vec![],
767            primary_language: Some(crate::types::Language::Rust),
768            dependencies: vec![],
769            tdg_score: Some(95.0),
770        };
771
772        let config = BatutaConfig::from_analysis(&analysis);
773
774        assert_eq!(config.project.name, "project");
775        assert_eq!(config.project.primary_language, Some("Rust".to_string()));
776    }
777
778    #[test]
779    fn test_from_analysis_no_primary_language() {
780        let analysis = crate::types::ProjectAnalysis {
781            root_path: PathBuf::from("/unknown/project"),
782            total_files: 1,
783            total_lines: 10,
784            languages: vec![],
785            primary_language: None,
786            dependencies: vec![],
787            tdg_score: None,
788        };
789
790        let config = BatutaConfig::from_analysis(&analysis);
791
792        assert_eq!(config.project.name, "project");
793        assert!(config.project.primary_language.is_none());
794    }
795
796    // ============================================================================
797    // SERIALIZATION TESTS
798    // ============================================================================
799
800    #[test]
801    fn test_serialize_deserialize_batuta_config() {
802        let config = BatutaConfig::default();
803
804        let serialized = toml::to_string(&config).expect("toml serialize failed");
805        let deserialized: BatutaConfig = toml::from_str(&serialized).expect("toml parse failed");
806
807        assert_eq!(config.version, deserialized.version);
808        assert_eq!(config.project.name, deserialized.project.name);
809        assert_eq!(config.optimization.profile, deserialized.optimization.profile);
810    }
811
812    #[test]
813    fn test_serialize_deserialize_with_optional_fields() {
814        let mut config = BatutaConfig::default();
815        config.project.description = Some("Test description".to_string());
816        config.project.primary_language = Some("Python".to_string());
817        config.build.target = Some("x86_64-unknown-linux-gnu".to_string());
818
819        let serialized = toml::to_string(&config).expect("toml serialize failed");
820        let deserialized: BatutaConfig = toml::from_str(&serialized).expect("toml parse failed");
821
822        assert_eq!(config.project.description, deserialized.project.description);
823        assert_eq!(config.project.primary_language, deserialized.project.primary_language);
824        assert_eq!(config.build.target, deserialized.build.target);
825    }
826
827    #[test]
828    fn test_serialize_deserialize_with_vectors() {
829        let mut config = BatutaConfig::default();
830        config.project.authors = vec!["Alice".to_string(), "Bob".to_string()];
831        config.source.exclude = vec!["test".to_string(), "docs".to_string()];
832        config.transpilation.modules = vec!["mod1".to_string(), "mod2".to_string()];
833
834        let serialized = toml::to_string(&config).expect("toml serialize failed");
835        let deserialized: BatutaConfig = toml::from_str(&serialized).expect("toml parse failed");
836
837        assert_eq!(config.project.authors, deserialized.project.authors);
838        assert_eq!(config.source.exclude, deserialized.source.exclude);
839        assert_eq!(config.transpilation.modules, deserialized.transpilation.modules);
840    }
841
842    #[test]
843    fn test_full_toml_deserialization() {
844        // Test deserializing a complete TOML configuration
845        let config = BatutaConfig::default();
846        let serialized = toml::to_string(&config).expect("toml serialize failed");
847        let deserialized: BatutaConfig = toml::from_str(&serialized).expect("toml parse failed");
848
849        assert_eq!(config.version, deserialized.version);
850        assert_eq!(config.project.name, deserialized.project.name);
851        assert_eq!(config.optimization.profile, deserialized.optimization.profile);
852    }
853
854    #[test]
855    fn test_modified_toml_deserialization() {
856        // Test deserializing a modified configuration
857        let mut config = BatutaConfig::default();
858        config.project.name = "custom-name".to_string();
859        config.optimization.profile = "aggressive".to_string();
860        config.build.release = false;
861
862        let serialized = toml::to_string(&config).expect("toml serialize failed");
863        let deserialized: BatutaConfig = toml::from_str(&serialized).expect("toml parse failed");
864
865        assert_eq!(deserialized.project.name, "custom-name");
866        assert_eq!(deserialized.optimization.profile, "aggressive");
867        assert!(!deserialized.build.release);
868    }
869
870    // ============================================================================
871    // NESTED CONFIG TESTS
872    // ============================================================================
873
874    #[test]
875    fn test_decy_config_in_transpilation() {
876        let config = BatutaConfig::default();
877
878        assert!(config.transpilation.decy.ownership_inference);
879        assert!(config.transpilation.decy.actionable_diagnostics);
880        assert!(config.transpilation.decy.use_static_fixer);
881    }
882
883    #[test]
884    fn test_depyler_config_in_transpilation() {
885        let config = BatutaConfig::default();
886
887        assert!(config.transpilation.depyler.type_inference);
888        assert!(config.transpilation.depyler.numpy_to_trueno);
889        assert!(config.transpilation.depyler.sklearn_to_aprender);
890        assert!(config.transpilation.depyler.pytorch_to_realizar);
891    }
892
893    #[test]
894    fn test_bashrs_config_in_transpilation() {
895        let config = BatutaConfig::default();
896
897        assert_eq!(config.transpilation.bashrs.target_shell, "bash");
898        assert!(config.transpilation.bashrs.use_clap);
899    }
900
901    #[test]
902    fn test_trueno_config_in_optimization() {
903        let config = BatutaConfig::default();
904
905        assert_eq!(
906            config.optimization.trueno.backends,
907            vec!["simd".to_string(), "cpu".to_string()]
908        );
909        assert!(!config.optimization.trueno.adaptive_thresholds);
910        assert_eq!(config.optimization.trueno.cpu_threshold, 500);
911    }
912
913    #[test]
914    fn test_renacer_config_in_validation() {
915        let config = BatutaConfig::default();
916
917        assert!(config.validation.renacer.trace_syscalls.is_empty());
918        assert_eq!(config.validation.renacer.output_format, "json");
919    }
920
921    // ============================================================================
922    // MODIFICATION TESTS
923    // ============================================================================
924
925    #[test]
926    fn test_config_modification() {
927        let mut config = BatutaConfig::default();
928
929        // Modify various fields
930        config.project.name = "new-name".to_string();
931        config.optimization.enable_gpu = true;
932        config.optimization.gpu_threshold = 2000;
933        config.transpilation.incremental = false;
934
935        assert_eq!(config.project.name, "new-name");
936        assert!(config.optimization.enable_gpu);
937        assert_eq!(config.optimization.gpu_threshold, 2000);
938        assert!(!config.transpilation.incremental);
939    }
940
941    #[test]
942    fn test_config_clone() {
943        let config = BatutaConfig::default();
944        let cloned = config.clone();
945
946        assert_eq!(config.version, cloned.version);
947        assert_eq!(config.project.name, cloned.project.name);
948        assert_eq!(config.optimization.profile, cloned.optimization.profile);
949    }
950
951    #[test]
952    fn test_save_modified_config() {
953        let temp_dir = TempDir::new().expect("tempdir creation failed");
954        let config_path = temp_dir.path().join("config.toml");
955
956        let mut config = BatutaConfig::default();
957        config.project.name = "modified-project".to_string();
958        config.project.authors = vec!["Author1".to_string(), "Author2".to_string()];
959        config.optimization.enable_gpu = true;
960
961        config.save(&config_path).expect("save failed");
962
963        let loaded = BatutaConfig::load(&config_path).expect("unexpected failure");
964
965        assert_eq!(loaded.project.name, "modified-project");
966        assert_eq!(loaded.project.authors.len(), 2);
967        assert!(loaded.optimization.enable_gpu);
968    }
969
970    // ============================================================================
971    // PRIVATE CONFIG TESTS
972    // ============================================================================
973
974    #[test]
975    fn test_private_config_default() {
976        let config = PrivateConfig::default();
977        assert!(config.private.rust_stack_dirs.is_empty());
978        assert!(config.private.rust_corpus_dirs.is_empty());
979        assert!(config.private.python_corpus_dirs.is_empty());
980        assert!(config.private.endpoints.is_empty());
981        assert!(!config.has_dirs());
982        assert_eq!(config.dir_count(), 0);
983    }
984
985    #[test]
986    fn test_private_config_deserialize_full() {
987        let toml_str = r#"
988[private]
989rust_stack_dirs = ["../rmedia", "../infra"]
990rust_corpus_dirs = ["../internal-cookbook"]
991python_corpus_dirs = ["../private-notebooks"]
992"#;
993        let config: PrivateConfig = toml::from_str(toml_str).expect("toml parse failed");
994        assert_eq!(config.private.rust_stack_dirs.len(), 2);
995        assert_eq!(config.private.rust_corpus_dirs.len(), 1);
996        assert_eq!(config.private.python_corpus_dirs.len(), 1);
997        assert!(config.has_dirs());
998        assert_eq!(config.dir_count(), 4);
999    }
1000
1001    #[test]
1002    fn test_private_config_deserialize_partial() {
1003        let toml_str = r#"
1004[private]
1005rust_stack_dirs = ["../rmedia"]
1006"#;
1007        let config: PrivateConfig = toml::from_str(toml_str).expect("toml parse failed");
1008        assert_eq!(config.private.rust_stack_dirs, vec!["../rmedia"]);
1009        assert!(config.private.rust_corpus_dirs.is_empty());
1010        assert!(config.private.python_corpus_dirs.is_empty());
1011        assert!(config.has_dirs());
1012        assert_eq!(config.dir_count(), 1);
1013    }
1014
1015    #[test]
1016    fn test_private_config_deserialize_empty_private() {
1017        let toml_str = r#"
1018[private]
1019"#;
1020        let config: PrivateConfig = toml::from_str(toml_str).expect("toml parse failed");
1021        assert!(!config.has_dirs());
1022        assert_eq!(config.dir_count(), 0);
1023    }
1024
1025    #[test]
1026    fn test_private_config_with_endpoints() {
1027        let toml_str = r#"
1028[private]
1029rust_stack_dirs = ["../rmedia"]
1030
1031[[private.endpoints]]
1032name = "intel"
1033type = "ssh"
1034host = "intel.local"
1035index_path = "/tmp/batuta/rag/index.sqlite"
1036"#;
1037        let config: PrivateConfig = toml::from_str(toml_str).expect("toml parse failed");
1038        assert_eq!(config.private.endpoints.len(), 1);
1039        assert_eq!(config.private.endpoints[0].name, "intel");
1040        assert_eq!(config.private.endpoints[0].endpoint_type, "ssh");
1041        assert_eq!(config.private.endpoints[0].host, "intel.local");
1042    }
1043
1044    #[test]
1045    fn test_private_config_serialize_roundtrip() {
1046        let toml_str = r#"
1047[private]
1048rust_stack_dirs = ["../rmedia", "../infra"]
1049rust_corpus_dirs = ["../internal-cookbook"]
1050python_corpus_dirs = []
1051"#;
1052        let config: PrivateConfig = toml::from_str(toml_str).expect("toml parse failed");
1053        let serialized = toml::to_string(&config).expect("toml serialize failed");
1054        let roundtripped: PrivateConfig = toml::from_str(&serialized).expect("toml parse failed");
1055        assert_eq!(config.private.rust_stack_dirs, roundtripped.private.rust_stack_dirs);
1056        assert_eq!(config.private.rust_corpus_dirs, roundtripped.private.rust_corpus_dirs);
1057    }
1058
1059    #[test]
1060    fn test_private_config_find_config_path_checks_home() {
1061        // find_config_path checks home dir first, then cwd.
1062        // We can't easily mock the filesystem, but we can verify the function
1063        // returns Some when $HOME/.batuta-private.toml exists (which it does
1064        // in dev) or None when it doesn't.
1065        let result = PrivateConfig::find_config_path();
1066        // Result depends on environment — just verify it doesn't panic
1067        let _ = result;
1068    }
1069
1070    #[test]
1071    fn test_private_config_has_dirs() {
1072        let mut config = PrivateConfig::default();
1073        assert!(!config.has_dirs());
1074
1075        config.private.rust_stack_dirs.push("../foo".to_string());
1076        assert!(config.has_dirs());
1077    }
1078
1079    #[test]
1080    fn test_private_config_dir_count() {
1081        let mut config = PrivateConfig::default();
1082        assert_eq!(config.dir_count(), 0);
1083
1084        config.private.rust_stack_dirs.push("../a".to_string());
1085        config.private.rust_corpus_dirs.push("../b".to_string());
1086        config.private.python_corpus_dirs.push("../c".to_string());
1087        assert_eq!(config.dir_count(), 3);
1088    }
1089}