1use crate::config::{DerivationSource, DerivedFieldConfig, EnterpriseConfig, ValidationRule};
13use regex::Regex;
14use std::collections::HashMap;
15use std::path::PathBuf;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum ValidationResult {
20 Valid,
22 Warning(String),
24}
25
26#[derive(Debug, Clone)]
28pub struct DerivationContext {
29 pub branch_name: Option<String>,
31 pub spec_path: Option<PathBuf>,
33 pub env_vars: HashMap<String, String>,
35 pub git_user_name: Option<String>,
37 pub git_user_email: Option<String>,
39}
40
41impl DerivationContext {
42 pub fn new() -> Self {
44 Self {
45 branch_name: None,
46 spec_path: None,
47 env_vars: HashMap::new(),
48 git_user_name: None,
49 git_user_email: None,
50 }
51 }
52
53 pub fn with_env_vars(env_vars: HashMap<String, String>) -> Self {
55 Self {
56 branch_name: None,
57 spec_path: None,
58 env_vars,
59 git_user_name: None,
60 git_user_email: None,
61 }
62 }
63}
64
65impl Default for DerivationContext {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71pub fn build_context(spec_id: &str, specs_dir: &std::path::Path) -> DerivationContext {
81 use crate::git;
82
83 let mut context = DerivationContext::new();
84
85 if let Ok(branch) = git::get_current_branch() {
87 context.branch_name = Some(branch);
88 }
89
90 let spec_path = specs_dir.join(format!("{}.md", spec_id));
92 context.spec_path = Some(spec_path);
93
94 context.env_vars = std::env::vars().collect();
96
97 let (name, email) = git::get_git_user_info();
99 context.git_user_name = name;
100 context.git_user_email = email;
101
102 context
103}
104
105#[derive(Debug, Clone)]
107pub struct DerivationEngine {
108 config: EnterpriseConfig,
109}
110
111impl DerivationEngine {
112 pub fn new(config: EnterpriseConfig) -> Self {
114 Self { config }
115 }
116
117 pub fn derive_fields(&self, context: &DerivationContext) -> HashMap<String, String> {
123 if self.config.derived.is_empty() {
125 return HashMap::new();
126 }
127
128 let mut result = HashMap::new();
129
130 for (field_name, field_config) in &self.config.derived {
131 if let Some(value) = self.derive_field(field_name, field_config, context) {
132 result.insert(field_name.clone(), value);
133 }
134 }
135
136 result
137 }
138
139 fn derive_field(
146 &self,
147 field_name: &str,
148 config: &DerivedFieldConfig,
149 context: &DerivationContext,
150 ) -> Option<String> {
151 let value = match config.from {
152 DerivationSource::Branch => {
153 let source_value = self.extract_from_branch(context)?;
154 self.apply_pattern(&config.pattern, &source_value)
155 .or_else(|| {
156 eprintln!(
157 "Warning: derivation pattern for field '{}' did not match source",
158 field_name
159 );
160 None
161 })?
162 }
163 DerivationSource::Path => {
164 let source_value = self.extract_from_path(context)?;
165 self.apply_pattern(&config.pattern, &source_value)
166 .or_else(|| {
167 eprintln!(
168 "Warning: derivation pattern for field '{}' did not match source",
169 field_name
170 );
171 None
172 })?
173 }
174 DerivationSource::Env => {
175 self.extract_from_env(context, &config.pattern)?
177 }
178 DerivationSource::GitUser => {
179 self.extract_from_git_user(context, &config.pattern)?
181 }
182 };
183
184 if let Some(validation) = &config.validate {
186 match self.validate_derived_value(field_name, &value, validation) {
187 ValidationResult::Valid => {
188 }
190 ValidationResult::Warning(msg) => {
191 eprintln!("Warning: {}", msg);
193 }
194 }
195 }
196
197 Some(value)
198 }
199
200 fn extract_from_branch(&self, context: &DerivationContext) -> Option<String> {
202 context.branch_name.clone()
203 }
204
205 fn extract_from_path(&self, context: &DerivationContext) -> Option<String> {
207 context
208 .spec_path
209 .as_ref()
210 .and_then(|path| path.to_str().map(|s| s.to_string()))
211 }
212
213 fn extract_from_env(&self, context: &DerivationContext, env_name: &str) -> Option<String> {
217 context.env_vars.get(env_name).cloned()
218 }
219
220 fn extract_from_git_user(
224 &self,
225 context: &DerivationContext,
226 field_type: &str,
227 ) -> Option<String> {
228 match field_type {
229 "name" => context.git_user_name.clone(),
230 "email" => context.git_user_email.clone(),
231 _ => None,
232 }
233 }
234
235 fn apply_pattern(&self, pattern: &str, source: &str) -> Option<String> {
239 let regex = Regex::new(pattern).ok()?;
240 regex
241 .captures(source)?
242 .get(1)
243 .map(|m| m.as_str().to_string())
244 }
245
246 fn validate_derived_value(
251 &self,
252 field_name: &str,
253 value: &str,
254 validation: &ValidationRule,
255 ) -> ValidationResult {
256 match validation {
257 ValidationRule::Enum { values } => {
258 if values.contains(&value.to_string()) {
259 ValidationResult::Valid
260 } else {
261 ValidationResult::Warning(format!(
262 "Field '{}' value '{}' is not in allowed enum values: {}",
263 field_name,
264 value,
265 values.join(", ")
266 ))
267 }
268 }
269 }
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use std::path::PathBuf;
277
278 fn create_test_engine(derived: HashMap<String, DerivedFieldConfig>) -> DerivationEngine {
279 DerivationEngine::new(EnterpriseConfig {
280 derived,
281 required: vec![],
282 })
283 }
284
285 fn assert_derive_from_source(
291 field_name: &str,
292 config: DerivedFieldConfig,
293 context: DerivationContext,
294 expected: Option<&str>,
295 ) {
296 let mut derived = HashMap::new();
297 derived.insert(field_name.to_string(), config);
298 let engine = create_test_engine(derived);
299 let result = engine.derive_fields(&context);
300
301 match expected {
302 Some(val) => assert_eq!(result.get(field_name), Some(&val.to_string())),
303 None => assert!(!result.contains_key(field_name)),
304 }
305 }
306
307 #[test]
312 fn test_derive_from_branch_basic() {
313 let config = DerivedFieldConfig {
314 from: DerivationSource::Branch,
315 pattern: r"^(dev|staging|prod)".to_string(),
316 validate: None,
317 };
318 let mut context = DerivationContext::new();
319 context.branch_name = Some("prod/feature-123".to_string());
320
321 assert_derive_from_source("env", config, context, Some("prod"));
322 }
323
324 #[test]
325 fn test_derive_from_branch_with_capture_group() {
326 let config = DerivedFieldConfig {
327 from: DerivationSource::Branch,
328 pattern: r"sprint/.*/(PROJ-\d+)".to_string(),
329 validate: None,
330 };
331 let mut context = DerivationContext::new();
332 context.branch_name = Some("sprint/2026-Q1-W4/PROJ-123".to_string());
333
334 assert_derive_from_source("project", config, context, Some("PROJ-123"));
335 }
336
337 #[test]
338 fn test_derive_from_branch_no_match() {
339 let config = DerivedFieldConfig {
340 from: DerivationSource::Branch,
341 pattern: r"^(dev|staging|prod)".to_string(),
342 validate: None,
343 };
344 let mut context = DerivationContext::new();
345 context.branch_name = Some("feature/my-branch".to_string());
346
347 assert_derive_from_source("env", config, context, None);
348 }
349
350 #[test]
351 fn test_derive_from_branch_missing() {
352 let config = DerivedFieldConfig {
353 from: DerivationSource::Branch,
354 pattern: r"^(dev|staging|prod)".to_string(),
355 validate: None,
356 };
357 let context = DerivationContext::new(); assert_derive_from_source("env", config, context, None);
360 }
361
362 #[test]
367 fn test_derive_from_path_basic() {
368 let config = DerivedFieldConfig {
369 from: DerivationSource::Path,
370 pattern: r"specs/([a-z]+)/".to_string(),
371 validate: None,
372 };
373 let mut context = DerivationContext::new();
374 context.spec_path = Some(PathBuf::from(".chant/specs/platform/feature.md"));
375
376 assert_derive_from_source("team", config, context, Some("platform"));
377 }
378
379 #[test]
380 fn test_derive_from_path_with_multiple_captures() {
381 let config = DerivedFieldConfig {
382 from: DerivationSource::Path,
383 pattern: r"specs/([a-z]+)/([A-Z0-9]+)-".to_string(),
384 validate: None,
385 };
386 let mut context = DerivationContext::new();
387 context.spec_path = Some(PathBuf::from(".chant/specs/teams/PROJ-123-feature.md"));
388
389 assert_derive_from_source("project", config, context, Some("teams"));
391 }
392
393 #[test]
394 fn test_derive_from_path_no_match() {
395 let config = DerivedFieldConfig {
396 from: DerivationSource::Path,
397 pattern: r"specs/([a-z]+)/".to_string(),
398 validate: None,
399 };
400 let mut context = DerivationContext::new();
401 context.spec_path = Some(PathBuf::from(".chant/specs/feature.md"));
402
403 assert_derive_from_source("team", config, context, None);
404 }
405
406 #[test]
407 fn test_derive_from_path_missing() {
408 let config = DerivedFieldConfig {
409 from: DerivationSource::Path,
410 pattern: r"specs/([a-z]+)/".to_string(),
411 validate: None,
412 };
413 let context = DerivationContext::new(); assert_derive_from_source("team", config, context, None);
416 }
417
418 #[test]
423 fn test_derive_from_env_basic() {
424 let config = DerivedFieldConfig {
425 from: DerivationSource::Env,
426 pattern: "TEAM_NAME".to_string(),
427 validate: None,
428 };
429 let mut env_vars = HashMap::new();
430 env_vars.insert("TEAM_NAME".to_string(), "platform".to_string());
431 let context = DerivationContext::with_env_vars(env_vars);
432
433 assert_derive_from_source("team", config, context, Some("platform"));
434 }
435
436 #[test]
437 fn test_derive_from_env_with_pattern_match() {
438 let config = DerivedFieldConfig {
439 from: DerivationSource::Env,
440 pattern: "ENVIRONMENT".to_string(),
441 validate: None,
442 };
443 let mut env_vars = HashMap::new();
444 env_vars.insert("ENVIRONMENT".to_string(), "production".to_string());
445 let context = DerivationContext::with_env_vars(env_vars);
446
447 assert_derive_from_source("env_name", config, context, Some("production"));
448 }
449
450 #[test]
451 fn test_derive_from_env_missing_variable() {
452 let config = DerivedFieldConfig {
453 from: DerivationSource::Env,
454 pattern: "TEAM_NAME".to_string(),
455 validate: None,
456 };
457 let context = DerivationContext::new(); assert_derive_from_source("team", config, context, None);
460 }
461
462 #[test]
463 fn test_derive_from_env_undefined_variable() {
464 let config = DerivedFieldConfig {
465 from: DerivationSource::Env,
466 pattern: "TEAM_NAME".to_string(),
467 validate: None,
468 };
469 let mut env_vars = HashMap::new();
470 env_vars.insert("OTHER_VAR".to_string(), "value".to_string());
471 let context = DerivationContext::with_env_vars(env_vars);
472
473 assert_derive_from_source("team", config, context, None);
474 }
475
476 #[test]
481 fn test_derive_from_git_user_name() {
482 let config = DerivedFieldConfig {
483 from: DerivationSource::GitUser,
484 pattern: "name".to_string(),
485 validate: None,
486 };
487 let mut context = DerivationContext::new();
488 context.git_user_name = Some("John Doe".to_string());
489
490 assert_derive_from_source("author", config, context, Some("John Doe"));
491 }
492
493 #[test]
494 fn test_derive_from_git_user_email() {
495 let config = DerivedFieldConfig {
496 from: DerivationSource::GitUser,
497 pattern: "email".to_string(),
498 validate: None,
499 };
500 let mut context = DerivationContext::new();
501 context.git_user_email = Some("john@example.com".to_string());
502
503 assert_derive_from_source("author_email", config, context, Some("john@example.com"));
504 }
505
506 #[test]
507 fn test_derive_from_git_user_invalid_field() {
508 let config = DerivedFieldConfig {
509 from: DerivationSource::GitUser,
510 pattern: "invalid".to_string(),
511 validate: None,
512 };
513 let mut context = DerivationContext::new();
514 context.git_user_name = Some("John Doe".to_string());
515
516 assert_derive_from_source("author", config, context, None);
517 }
518
519 #[test]
520 fn test_derive_from_git_user_missing_name() {
521 let config = DerivedFieldConfig {
522 from: DerivationSource::GitUser,
523 pattern: "name".to_string(),
524 validate: None,
525 };
526 let context = DerivationContext::new(); assert_derive_from_source("author", config, context, None);
529 }
530
531 #[test]
536 fn test_invalid_regex_pattern() {
537 let mut derived = HashMap::new();
538 derived.insert(
539 "test".to_string(),
540 DerivedFieldConfig {
541 from: DerivationSource::Branch,
542 pattern: "[invalid regex".to_string(), validate: None,
544 },
545 );
546
547 let engine = create_test_engine(derived);
548 let mut context = DerivationContext::new();
549 context.branch_name = Some("test".to_string());
550
551 let result = engine.derive_fields(&context);
552 assert!(!result.contains_key("test"));
554 }
555
556 #[test]
561 fn test_empty_config_returns_empty_map() {
562 let engine = create_test_engine(HashMap::new());
563 let mut context = DerivationContext::new();
564 context.branch_name = Some("main".to_string());
565 context.spec_path = Some(PathBuf::from(".chant/specs/test.md"));
566 let mut env_vars = HashMap::new();
567 env_vars.insert("TEST_VAR".to_string(), "value".to_string());
568 context.env_vars = env_vars;
569 context.git_user_name = Some("Test User".to_string());
570
571 let result = engine.derive_fields(&context);
572 assert!(result.is_empty());
573 }
574
575 #[test]
580 fn test_derive_multiple_fields() {
581 let mut derived = HashMap::new();
582 derived.insert(
583 "env".to_string(),
584 DerivedFieldConfig {
585 from: DerivationSource::Branch,
586 pattern: r"^(dev|staging|prod)".to_string(),
587 validate: None,
588 },
589 );
590 derived.insert(
591 "team".to_string(),
592 DerivedFieldConfig {
593 from: DerivationSource::Env,
594 pattern: "TEAM_NAME".to_string(),
595 validate: None,
596 },
597 );
598 derived.insert(
599 "author".to_string(),
600 DerivedFieldConfig {
601 from: DerivationSource::GitUser,
602 pattern: "name".to_string(),
603 validate: None,
604 },
605 );
606
607 let engine = create_test_engine(derived);
608 let mut context = DerivationContext::new();
609 context.branch_name = Some("prod/feature".to_string());
610 let mut env_vars = HashMap::new();
611 env_vars.insert("TEAM_NAME".to_string(), "platform".to_string());
612 context.env_vars = env_vars;
613 context.git_user_name = Some("Jane Doe".to_string());
614
615 let result = engine.derive_fields(&context);
616 assert_eq!(result.len(), 3);
617 assert_eq!(result.get("env"), Some(&"prod".to_string()));
618 assert_eq!(result.get("team"), Some(&"platform".to_string()));
619 assert_eq!(result.get("author"), Some(&"Jane Doe".to_string()));
620 }
621
622 #[test]
623 fn test_derive_multiple_fields_partial_success() {
624 let mut derived = HashMap::new();
625 derived.insert(
626 "env".to_string(),
627 DerivedFieldConfig {
628 from: DerivationSource::Branch,
629 pattern: r"^(dev|staging|prod)".to_string(),
630 validate: None,
631 },
632 );
633 derived.insert(
634 "team".to_string(),
635 DerivedFieldConfig {
636 from: DerivationSource::Env,
637 pattern: "MISSING_VAR".to_string(),
638 validate: None,
639 },
640 );
641 derived.insert(
642 "author".to_string(),
643 DerivedFieldConfig {
644 from: DerivationSource::GitUser,
645 pattern: "name".to_string(),
646 validate: None,
647 },
648 );
649
650 let engine = create_test_engine(derived);
651 let mut context = DerivationContext::new();
652 context.branch_name = Some("prod/feature".to_string());
653 context.git_user_name = Some("Jane Doe".to_string());
654
655 let result = engine.derive_fields(&context);
656 assert_eq!(result.len(), 2);
658 assert_eq!(result.get("env"), Some(&"prod".to_string()));
659 assert!(!result.contains_key("team"));
660 assert_eq!(result.get("author"), Some(&"Jane Doe".to_string()));
661 }
662
663 #[test]
668 fn test_enum_validation_valid_value() {
669 let mut derived = HashMap::new();
670 derived.insert(
671 "team".to_string(),
672 DerivedFieldConfig {
673 from: DerivationSource::Env,
674 pattern: "TEAM_NAME".to_string(),
675 validate: Some(ValidationRule::Enum {
676 values: vec![
677 "platform".to_string(),
678 "frontend".to_string(),
679 "backend".to_string(),
680 ],
681 }),
682 },
683 );
684
685 let engine = create_test_engine(derived);
686 let mut env_vars = HashMap::new();
687 env_vars.insert("TEAM_NAME".to_string(), "platform".to_string());
688 let context = DerivationContext::with_env_vars(env_vars);
689
690 let result = engine.derive_fields(&context);
691 assert_eq!(result.get("team"), Some(&"platform".to_string()));
693 }
694
695 #[test]
696 fn test_enum_validation_invalid_value() {
697 let mut derived = HashMap::new();
698 derived.insert(
699 "team".to_string(),
700 DerivedFieldConfig {
701 from: DerivationSource::Env,
702 pattern: "TEAM_NAME".to_string(),
703 validate: Some(ValidationRule::Enum {
704 values: vec![
705 "platform".to_string(),
706 "frontend".to_string(),
707 "backend".to_string(),
708 ],
709 }),
710 },
711 );
712
713 let engine = create_test_engine(derived);
714 let mut env_vars = HashMap::new();
715 env_vars.insert("TEAM_NAME".to_string(), "invalid-team".to_string());
716 let context = DerivationContext::with_env_vars(env_vars);
717
718 let result = engine.derive_fields(&context);
719 assert_eq!(result.get("team"), Some(&"invalid-team".to_string()));
721 }
722
723 #[test]
724 fn test_enum_validation_with_branch_source() {
725 let mut derived = HashMap::new();
726 derived.insert(
727 "environment".to_string(),
728 DerivedFieldConfig {
729 from: DerivationSource::Branch,
730 pattern: r"^(dev|staging|prod)".to_string(),
731 validate: Some(ValidationRule::Enum {
732 values: vec!["dev".to_string(), "staging".to_string(), "prod".to_string()],
733 }),
734 },
735 );
736
737 let engine = create_test_engine(derived);
738 let mut context = DerivationContext::new();
739 context.branch_name = Some("staging/new-feature".to_string());
740
741 let result = engine.derive_fields(&context);
742 assert_eq!(result.get("environment"), Some(&"staging".to_string()));
743 }
744
745 #[test]
746 fn test_enum_validation_with_branch_source_invalid() {
747 let mut derived = HashMap::new();
748 derived.insert(
749 "environment".to_string(),
750 DerivedFieldConfig {
751 from: DerivationSource::Branch,
752 pattern: r"^([a-z]+)".to_string(),
753 validate: Some(ValidationRule::Enum {
754 values: vec!["dev".to_string(), "staging".to_string(), "prod".to_string()],
755 }),
756 },
757 );
758
759 let engine = create_test_engine(derived);
760 let mut context = DerivationContext::new();
761 context.branch_name = Some("testing/new-feature".to_string());
762
763 let result = engine.derive_fields(&context);
764 assert_eq!(result.get("environment"), Some(&"testing".to_string()));
766 }
767
768 #[test]
769 fn test_validation_skipped_when_no_rule_configured() {
770 let mut derived = HashMap::new();
771 derived.insert(
772 "team".to_string(),
773 DerivedFieldConfig {
774 from: DerivationSource::Env,
775 pattern: "TEAM_NAME".to_string(),
776 validate: None, },
778 );
779
780 let engine = create_test_engine(derived);
781 let mut env_vars = HashMap::new();
782 env_vars.insert("TEAM_NAME".to_string(), "any-value".to_string());
783 let context = DerivationContext::with_env_vars(env_vars);
784
785 let result = engine.derive_fields(&context);
786 assert_eq!(result.get("team"), Some(&"any-value".to_string()));
788 }
789
790 #[test]
791 fn test_enum_validation_with_path_source() {
792 let mut derived = HashMap::new();
793 derived.insert(
794 "team".to_string(),
795 DerivedFieldConfig {
796 from: DerivationSource::Path,
797 pattern: r"specs/([a-z]+)/".to_string(),
798 validate: Some(ValidationRule::Enum {
799 values: vec![
800 "platform".to_string(),
801 "frontend".to_string(),
802 "backend".to_string(),
803 ],
804 }),
805 },
806 );
807
808 let engine = create_test_engine(derived);
809 let mut context = DerivationContext::new();
810 context.spec_path = Some(PathBuf::from(".chant/specs/backend/feature.md"));
811
812 let result = engine.derive_fields(&context);
813 assert_eq!(result.get("team"), Some(&"backend".to_string()));
814 }
815
816 #[test]
817 fn test_enum_validation_case_sensitive() {
818 let mut derived = HashMap::new();
819 derived.insert(
820 "team".to_string(),
821 DerivedFieldConfig {
822 from: DerivationSource::Env,
823 pattern: "TEAM_NAME".to_string(),
824 validate: Some(ValidationRule::Enum {
825 values: vec!["Platform".to_string(), "Frontend".to_string()],
826 }),
827 },
828 );
829
830 let engine = create_test_engine(derived);
831 let mut env_vars = HashMap::new();
832 env_vars.insert("TEAM_NAME".to_string(), "platform".to_string());
833 let context = DerivationContext::with_env_vars(env_vars);
834
835 let result = engine.derive_fields(&context);
836 assert_eq!(result.get("team"), Some(&"platform".to_string()));
839 }
840
841 #[test]
842 fn test_multiple_fields_with_mixed_validation() {
843 let mut derived = HashMap::new();
844 derived.insert(
845 "team".to_string(),
846 DerivedFieldConfig {
847 from: DerivationSource::Env,
848 pattern: "TEAM_NAME".to_string(),
849 validate: Some(ValidationRule::Enum {
850 values: vec!["platform".to_string(), "frontend".to_string()],
851 }),
852 },
853 );
854 derived.insert(
855 "environment".to_string(),
856 DerivedFieldConfig {
857 from: DerivationSource::Branch,
858 pattern: r"^(dev|staging|prod)".to_string(),
859 validate: None, },
861 );
862 derived.insert(
863 "author".to_string(),
864 DerivedFieldConfig {
865 from: DerivationSource::GitUser,
866 pattern: "name".to_string(),
867 validate: Some(ValidationRule::Enum {
868 values: vec!["Alice".to_string(), "Bob".to_string()],
869 }),
870 },
871 );
872
873 let engine = create_test_engine(derived);
874 let mut context = DerivationContext::new();
875 let mut env_vars = HashMap::new();
876 env_vars.insert("TEAM_NAME".to_string(), "backend".to_string()); context.env_vars = env_vars;
878 context.branch_name = Some("prod/feature".to_string());
879 context.git_user_name = Some("Charlie".to_string()); let result = engine.derive_fields(&context);
882 assert_eq!(result.len(), 3);
884 assert_eq!(result.get("team"), Some(&"backend".to_string()));
885 assert_eq!(result.get("environment"), Some(&"prod".to_string()));
886 assert_eq!(result.get("author"), Some(&"Charlie".to_string()));
887 }
888
889 #[test]
894 fn test_branch_with_unicode_characters() {
895 let mut derived = HashMap::new();
896 derived.insert(
897 "project".to_string(),
898 DerivedFieldConfig {
899 from: DerivationSource::Branch,
900 pattern: "feature/([^/]+)/".to_string(),
901 validate: None,
902 },
903 );
904 derived.insert(
905 "description".to_string(),
906 DerivedFieldConfig {
907 from: DerivationSource::Branch,
908 pattern: "feature/[^/]+/(.+)".to_string(),
909 validate: None,
910 },
911 );
912
913 let engine = create_test_engine(derived);
914 let mut context = DerivationContext::new();
915 context.branch_name = Some("feature/项目-123/amélioration".to_string());
916
917 let result = engine.derive_fields(&context);
918 assert_eq!(result.get("project"), Some(&"项目-123".to_string()));
919 assert_eq!(result.get("description"), Some(&"amélioration".to_string()));
920 }
921
922 #[test]
923 fn test_env_value_with_unicode() {
924 let mut derived = HashMap::new();
925 derived.insert(
926 "author".to_string(),
927 DerivedFieldConfig {
928 from: DerivationSource::Env,
929 pattern: "AUTHOR_NAME".to_string(),
930 validate: None,
931 },
932 );
933 derived.insert(
934 "team".to_string(),
935 DerivedFieldConfig {
936 from: DerivationSource::Env,
937 pattern: "TEAM_NAME".to_string(),
938 validate: None,
939 },
940 );
941 derived.insert(
942 "desc".to_string(),
943 DerivedFieldConfig {
944 from: DerivationSource::Env,
945 pattern: "DESCRIPTION".to_string(),
946 validate: None,
947 },
948 );
949
950 let engine = create_test_engine(derived);
951 let mut env_vars = HashMap::new();
952 env_vars.insert("AUTHOR_NAME".to_string(), "José García".to_string());
953 env_vars.insert("TEAM_NAME".to_string(), "Платформа".to_string());
954 env_vars.insert("DESCRIPTION".to_string(), "Fix 🐛 in parser".to_string());
955 let context = DerivationContext::with_env_vars(env_vars);
956
957 let result = engine.derive_fields(&context);
958 assert_eq!(result.get("author"), Some(&"José García".to_string()));
959 assert_eq!(result.get("team"), Some(&"Платформа".to_string()));
960 assert_eq!(result.get("desc"), Some(&"Fix 🐛 in parser".to_string()));
961 }
962
963 #[test]
964 fn test_git_user_with_unicode() {
965 let mut derived = HashMap::new();
966 derived.insert(
967 "author".to_string(),
968 DerivedFieldConfig {
969 from: DerivationSource::GitUser,
970 pattern: "name".to_string(),
971 validate: None,
972 },
973 );
974 derived.insert(
975 "email".to_string(),
976 DerivedFieldConfig {
977 from: DerivationSource::GitUser,
978 pattern: "email".to_string(),
979 validate: None,
980 },
981 );
982
983 let engine = create_test_engine(derived);
984 let mut context = DerivationContext::new();
985 context.git_user_name = Some("François Müller".to_string());
986 context.git_user_email = Some("françois.müller@example.com".to_string());
987
988 let result = engine.derive_fields(&context);
989 assert_eq!(result.get("author"), Some(&"François Müller".to_string()));
990 assert_eq!(
991 result.get("email"),
992 Some(&"françois.müller@example.com".to_string())
993 );
994 }
995
996 #[test]
997 fn test_path_with_unicode_directory_names() {
998 let mut derived = HashMap::new();
999 derived.insert(
1000 "team".to_string(),
1001 DerivedFieldConfig {
1002 from: DerivationSource::Path,
1003 pattern: "specs/([^/]+)/".to_string(),
1004 validate: None,
1005 },
1006 );
1007
1008 let engine = create_test_engine(derived);
1009 let mut context = DerivationContext::new();
1010 context.spec_path = Some(PathBuf::from(".chant/specs/平台/文档.md"));
1011
1012 let result = engine.derive_fields(&context);
1013 assert_eq!(result.get("team"), Some(&"平台".to_string()));
1014 }
1015
1016 #[test]
1021 fn test_special_characters_branch_with_slashes_hyphens_dots() {
1022 let config1 = DerivedFieldConfig {
1023 from: DerivationSource::Branch,
1024 pattern: "([A-Z]+-\\d+)".to_string(),
1025 validate: None,
1026 };
1027 let config2 = DerivedFieldConfig {
1028 from: DerivationSource::Branch,
1029 pattern: "feature/(.+)".to_string(),
1030 validate: None,
1031 };
1032 let mut context = DerivationContext::new();
1033 context.branch_name = Some("feature/ABC-123/user-name.test".to_string());
1034
1035 assert_derive_from_source("ticket", config1, context.clone(), Some("ABC-123"));
1036 assert_derive_from_source(
1037 "full_path",
1038 config2,
1039 context,
1040 Some("ABC-123/user-name.test"),
1041 );
1042 }
1043
1044 #[test]
1045 fn test_special_characters_env_value_with_spaces_and_quotes() {
1046 let mut env_vars = HashMap::new();
1047 env_vars.insert("TEAM_NAME".to_string(), "Platform Team".to_string());
1048 env_vars.insert(
1049 "DESCRIPTION".to_string(),
1050 "This is a \"test\" value".to_string(),
1051 );
1052 env_vars.insert(
1053 "NOTES".to_string(),
1054 "Value with 'single' and \"double\" quotes".to_string(),
1055 );
1056
1057 let config1 = DerivedFieldConfig {
1058 from: DerivationSource::Env,
1059 pattern: "TEAM_NAME".to_string(),
1060 validate: None,
1061 };
1062 let config2 = DerivedFieldConfig {
1063 from: DerivationSource::Env,
1064 pattern: "DESCRIPTION".to_string(),
1065 validate: None,
1066 };
1067 let config3 = DerivedFieldConfig {
1068 from: DerivationSource::Env,
1069 pattern: "NOTES".to_string(),
1070 validate: None,
1071 };
1072 let context = DerivationContext::with_env_vars(env_vars);
1073
1074 assert_derive_from_source("team", config1, context.clone(), Some("Platform Team"));
1075 assert_derive_from_source(
1076 "desc",
1077 config2,
1078 context.clone(),
1079 Some("This is a \"test\" value"),
1080 );
1081 assert_derive_from_source(
1082 "notes",
1083 config3,
1084 context,
1085 Some("Value with 'single' and \"double\" quotes"),
1086 );
1087 }
1088
1089 #[test]
1090 fn test_special_characters_path_with_dots_and_hyphens() {
1091 let config1 = DerivedFieldConfig {
1092 from: DerivationSource::Path,
1093 pattern: "specs/([^/]+)/".to_string(),
1094 validate: None,
1095 };
1096 let config2 = DerivedFieldConfig {
1097 from: DerivationSource::Path,
1098 pattern: "/([^/]+\\.md)$".to_string(),
1099 validate: None,
1100 };
1101 let mut context = DerivationContext::new();
1102 context.spec_path = Some(PathBuf::from(".chant/specs/platform-team/feature.v2.md"));
1103
1104 assert_derive_from_source("component", config1, context.clone(), Some("platform-team"));
1105 assert_derive_from_source("filename", config2, context, Some("feature.v2.md"));
1106 }
1107
1108 #[test]
1109 fn test_special_characters_value_with_regex_metacharacters() {
1110 let config = DerivedFieldConfig {
1111 from: DerivationSource::Branch,
1112 pattern: "feature/(.+)".to_string(),
1113 validate: None,
1114 };
1115 let mut context = DerivationContext::new();
1116 context.branch_name = Some("feature/fix-[bug]-in-(parser)".to_string());
1117
1118 assert_derive_from_source(
1119 "description",
1120 config,
1121 context,
1122 Some("fix-[bug]-in-(parser)"),
1123 );
1124 }
1125
1126 #[test]
1127 fn test_special_characters_env_value_with_commas_and_special_chars() {
1128 let mut env_vars = HashMap::new();
1129 env_vars.insert("TAGS".to_string(), "bug,feature,urgent".to_string());
1130 env_vars.insert("EXPRESSION".to_string(), "value = 1 + 2 * 3".to_string());
1131 env_vars.insert(
1132 "PATH_LIKE".to_string(),
1133 "/usr/bin:/usr/local/bin".to_string(),
1134 );
1135
1136 let config1 = DerivedFieldConfig {
1137 from: DerivationSource::Env,
1138 pattern: "TAGS".to_string(),
1139 validate: None,
1140 };
1141 let config2 = DerivedFieldConfig {
1142 from: DerivationSource::Env,
1143 pattern: "EXPRESSION".to_string(),
1144 validate: None,
1145 };
1146 let config3 = DerivedFieldConfig {
1147 from: DerivationSource::Env,
1148 pattern: "PATH_LIKE".to_string(),
1149 validate: None,
1150 };
1151 let context = DerivationContext::with_env_vars(env_vars);
1152
1153 assert_derive_from_source("tags", config1, context.clone(), Some("bug,feature,urgent"));
1154 assert_derive_from_source("expr", config2, context.clone(), Some("value = 1 + 2 * 3"));
1155 assert_derive_from_source("path", config3, context, Some("/usr/bin:/usr/local/bin"));
1156 }
1157
1158 #[test]
1159 fn test_special_characters_branch_with_multiple_regex_metacharacters() {
1160 let config = DerivedFieldConfig {
1161 from: DerivationSource::Branch,
1162 pattern: "fix/(.+)".to_string(),
1163 validate: None,
1164 };
1165 let mut context = DerivationContext::new();
1166 context.branch_name = Some("fix/handle-$var.and^chars+more*stuff".to_string());
1167
1168 assert_derive_from_source(
1169 "desc",
1170 config,
1171 context,
1172 Some("handle-$var.and^chars+more*stuff"),
1173 );
1174 }
1175}