1use crate::spec::{Spec, SpecStatus};
8use anyhow::Result;
9use std::path::Path;
10
11pub fn is_member_of(member_id: &str, driver_id: &str) -> bool {
25 if !member_id.starts_with(driver_id) {
27 return false;
28 }
29
30 let suffix = &member_id[driver_id.len()..];
31 suffix.starts_with('.') && suffix.len() > 1
32}
33
34pub fn get_members<'a>(driver_id: &str, specs: &'a [Spec]) -> Vec<&'a Spec> {
50 specs
51 .iter()
52 .filter(|s| is_member_of(&s.id, driver_id))
53 .collect()
54}
55
56pub fn all_members_completed(driver_id: &str, specs: &[Spec]) -> bool {
75 let members = get_members(driver_id, specs);
76 if members.is_empty() {
77 return true; }
79 members
80 .iter()
81 .all(|m| m.frontmatter.status == SpecStatus::Completed)
82}
83
84pub fn get_incomplete_members(driver_id: &str, all_specs: &[Spec]) -> Vec<String> {
103 get_members(driver_id, all_specs)
104 .into_iter()
105 .filter(|m| m.frontmatter.status != SpecStatus::Completed)
106 .map(|m| m.id.clone())
107 .collect()
108}
109
110pub fn extract_driver_id(member_id: &str) -> Option<String> {
124 if let Some(pos) = member_id.find('.') {
126 let (prefix, suffix) = member_id.split_at(pos);
127 if suffix.len() > 1
129 && suffix[1..]
130 .chars()
131 .next()
132 .is_some_and(|c| c.is_ascii_digit())
133 {
134 return Some(prefix.to_string());
135 }
136 }
137 None
138}
139
140pub fn extract_member_number(member_id: &str) -> Option<u32> {
156 if let Some(pos) = member_id.find('.') {
157 let suffix = &member_id[pos + 1..];
158 let num_str: String = suffix.chars().take_while(|c| c.is_ascii_digit()).collect();
160 if !num_str.is_empty() {
161 return num_str.parse::<u32>().ok();
162 }
163 }
164 None
165}
166
167pub fn compare_spec_ids(a: &str, b: &str) -> std::cmp::Ordering {
192 use std::cmp::Ordering;
193
194 let a_driver = extract_driver_id(a);
196 let b_driver = extract_driver_id(b);
197
198 match (a_driver, b_driver) {
199 (Some(a_base), Some(b_base)) => {
200 match compare_base_ids(&a_base, &b_base) {
203 Ordering::Equal => {
204 let a_num = extract_member_number(a).unwrap_or(u32::MAX);
206 let b_num = extract_member_number(b).unwrap_or(u32::MAX);
207 a_num.cmp(&b_num)
208 }
209 other => other,
210 }
211 }
212 (Some(a_base), None) => {
213 match compare_base_ids(&a_base, b) {
216 Ordering::Equal => {
217 Ordering::Greater
219 }
220 other => other,
221 }
222 }
223 (None, Some(b_base)) => {
224 match compare_base_ids(a, &b_base) {
227 Ordering::Equal => {
228 Ordering::Less
230 }
231 other => other,
232 }
233 }
234 (None, None) => {
235 compare_base_ids(a, b)
237 }
238 }
239}
240
241fn compare_base_ids(a: &str, b: &str) -> std::cmp::Ordering {
248 use std::cmp::Ordering;
249
250 let a_parts = parse_spec_id_parts(a);
252 let b_parts = parse_spec_id_parts(b);
253
254 match (a_parts, b_parts) {
255 (Some((a_date, a_seq, a_suffix)), Some((b_date, b_seq, b_suffix))) => {
256 match a_date.cmp(b_date) {
258 Ordering::Equal => {
259 match a_seq.cmp(&b_seq) {
261 Ordering::Equal => {
262 a_suffix.cmp(b_suffix)
264 }
265 other => other,
266 }
267 }
268 other => other,
269 }
270 }
271 _ => a.cmp(b),
273 }
274}
275
276fn parse_spec_id_parts(id: &str) -> Option<(&str, u32, &str)> {
279 let parts: Vec<&str> = id.split('-').collect();
280
281 if parts.len() < 5 {
283 return None;
284 }
285
286 let date = &id[..10]; let seq = crate::id::parse_base36(parts[3])?;
291
292 let suffix = parts[4];
294
295 Some((date, seq, suffix))
296}
297
298pub fn all_prior_siblings_completed(member_id: &str, all_specs: &[Spec]) -> bool {
316 if let Some(member_spec) = all_specs.iter().find(|s| s.id == member_id) {
318 if member_spec.frontmatter.depends_on.is_some() {
320 return true;
321 }
322 }
323
324 if let Some(driver_id) = extract_driver_id(member_id) {
326 if let Some(member_num) = extract_member_number(member_id) {
327 for i in 1..member_num {
329 let sibling_id = format!("{}.{}", driver_id, i);
330 let sibling = all_specs.iter().find(|s| s.id == sibling_id);
331 if let Some(s) = sibling {
332 if s.frontmatter.status != SpecStatus::Completed {
333 return false;
334 }
335 } else {
336 return false;
338 }
339 }
340 return true;
341 }
342 }
343 true
345}
346
347pub fn mark_driver_in_progress_conditional(
378 specs_dir: &Path,
379 member_id: &str,
380 skip: bool,
381) -> Result<()> {
382 use crate::spec::TransitionBuilder;
383
384 if skip {
385 return Ok(());
386 }
387
388 if let Some(driver_id) = extract_driver_id(member_id) {
389 let driver_path = specs_dir.join(format!("{}.md", driver_id));
391 if driver_path.exists() {
392 let mut driver = Spec::load(&driver_path)?;
393 if driver.frontmatter.status == SpecStatus::Pending {
394 TransitionBuilder::new(&mut driver).to(SpecStatus::InProgress)?;
395 driver.save(&driver_path)?;
396 }
397 }
398 }
399 Ok(())
400}
401
402pub fn mark_driver_in_progress(specs_dir: &Path, member_id: &str) -> Result<()> {
407 mark_driver_in_progress_conditional(specs_dir, member_id, false)
408}
409
410pub fn auto_complete_driver_if_ready(
436 member_id: &str,
437 all_specs: &[Spec],
438 specs_dir: &Path,
439) -> Result<bool> {
440 let Some(driver_id) = extract_driver_id(member_id) else {
442 return Ok(false);
443 };
444
445 let Some(driver_spec) = all_specs.iter().find(|s| s.id == driver_id) else {
447 return Ok(false);
448 };
449
450 if driver_spec.frontmatter.status != SpecStatus::InProgress
453 && driver_spec.frontmatter.status != SpecStatus::Pending
454 {
455 return Ok(false);
456 }
457
458 if !all_members_completed(&driver_id, all_specs) {
460 return Ok(false);
461 }
462
463 let driver_path = specs_dir.join(format!("{}.md", driver_id));
465 let mut driver = Spec::load(&driver_path)?;
466
467 use crate::spec::TransitionBuilder;
468 TransitionBuilder::new(&mut driver)
470 .force()
471 .to(SpecStatus::Completed)?;
472 driver.frontmatter.completed_at = Some(crate::utc_now_iso());
473 driver.frontmatter.model = Some("auto-completed".to_string());
474
475 driver.save(&driver_path)?;
476
477 Ok(true)
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn test_is_member_of() {
486 assert!(is_member_of("2026-01-22-001-x7m.1", "2026-01-22-001-x7m"));
487 assert!(is_member_of("2026-01-22-001-x7m.2.1", "2026-01-22-001-x7m"));
488 assert!(!is_member_of("2026-01-22-001-x7m", "2026-01-22-001-x7m"));
489 assert!(!is_member_of("2026-01-22-002-y8n", "2026-01-22-001-x7m"));
490 }
491
492 #[test]
493 fn test_extract_driver_id() {
494 assert_eq!(
495 extract_driver_id("2026-01-22-001-x7m.1"),
496 Some("2026-01-22-001-x7m".to_string())
497 );
498 assert_eq!(
499 extract_driver_id("2026-01-22-001-x7m.2.1"),
500 Some("2026-01-22-001-x7m".to_string())
501 );
502 assert_eq!(extract_driver_id("2026-01-22-001-x7m"), None);
503 assert_eq!(extract_driver_id("2026-01-22-001-x7m.abc"), None);
504 }
505
506 #[test]
507 fn test_extract_member_number() {
508 assert_eq!(extract_member_number("2026-01-24-001-abc.1"), Some(1));
509 assert_eq!(extract_member_number("2026-01-24-001-abc.3"), Some(3));
510 assert_eq!(extract_member_number("2026-01-24-001-abc.10"), Some(10));
511 assert_eq!(extract_member_number("2026-01-24-001-abc.3.2"), Some(3));
512 assert_eq!(extract_member_number("2026-01-24-001-abc"), None);
513 assert_eq!(extract_member_number("2026-01-24-001-abc.abc"), None);
514 }
515
516 #[test]
517 fn test_all_prior_siblings_completed() {
518 let spec1 = Spec::parse(
520 "2026-01-24-001-abc.1",
521 r#"---
522status: pending
523---
524# Test
525"#,
526 )
527 .unwrap();
528
529 assert!(all_prior_siblings_completed(&spec1.id, &[]));
531
532 let spec_prior_1 = Spec::parse(
534 "2026-01-24-001-abc.1",
535 r#"---
536status: completed
537---
538# Test
539"#,
540 )
541 .unwrap();
542
543 let spec_prior_2 = Spec::parse(
544 "2026-01-24-001-abc.2",
545 r#"---
546status: completed
547---
548# Test
549"#,
550 )
551 .unwrap();
552
553 let spec3 = Spec::parse(
554 "2026-01-24-001-abc.3",
555 r#"---
556status: pending
557---
558# Test
559"#,
560 )
561 .unwrap();
562
563 let all_specs = vec![spec_prior_1, spec_prior_2, spec3.clone()];
564 assert!(all_prior_siblings_completed(&spec3.id, &all_specs));
565 }
566
567 #[test]
568 fn test_all_prior_siblings_completed_missing() {
569 let spec_prior_1 = Spec::parse(
571 "2026-01-24-001-abc.1",
572 r#"---
573status: completed
574---
575# Test
576"#,
577 )
578 .unwrap();
579
580 let spec3 = Spec::parse(
581 "2026-01-24-001-abc.3",
582 r#"---
583status: pending
584---
585# Test
586"#,
587 )
588 .unwrap();
589
590 let all_specs = vec![spec_prior_1, spec3.clone()];
592 assert!(!all_prior_siblings_completed(&spec3.id, &all_specs));
593 }
594
595 #[test]
596 fn test_all_prior_siblings_completed_not_completed() {
597 let spec_prior_1 = Spec::parse(
599 "2026-01-24-001-abc.1",
600 r#"---
601status: pending
602---
603# Test
604"#,
605 )
606 .unwrap();
607
608 let spec2 = Spec::parse(
609 "2026-01-24-001-abc.2",
610 r#"---
611status: pending
612---
613# Test
614"#,
615 )
616 .unwrap();
617
618 let all_specs = vec![spec_prior_1, spec2.clone()];
619 assert!(!all_prior_siblings_completed(&spec2.id, &all_specs));
620 }
621
622 #[test]
623 fn test_mark_driver_in_progress_when_member_starts() {
624 use tempfile::TempDir;
625
626 let temp_dir = TempDir::new().unwrap();
627 let specs_dir = temp_dir.path();
628
629 let driver_spec = Spec {
631 id: "2026-01-24-001-abc".to_string(),
632 frontmatter: crate::spec::SpecFrontmatter {
633 status: SpecStatus::Pending,
634 ..Default::default()
635 },
636 title: Some("Driver spec".to_string()),
637 body: "# Driver spec\n\nBody content.".to_string(),
638 };
639
640 let driver_path = specs_dir.join("2026-01-24-001-abc.md");
641 driver_spec.save(&driver_path).unwrap();
642
643 mark_driver_in_progress(specs_dir, "2026-01-24-001-abc.1").unwrap();
645
646 let updated_driver = Spec::load(&driver_path).unwrap();
648 assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
649 }
650
651 #[test]
652 fn test_mark_driver_in_progress_skips_if_already_in_progress() {
653 use tempfile::TempDir;
654
655 let temp_dir = TempDir::new().unwrap();
656 let specs_dir = temp_dir.path();
657
658 let driver_spec = Spec {
660 id: "2026-01-24-002-def".to_string(),
661 frontmatter: crate::spec::SpecFrontmatter {
662 status: SpecStatus::InProgress,
663 ..Default::default()
664 },
665 title: Some("Driver spec".to_string()),
666 body: "# Driver spec\n\nBody content.".to_string(),
667 };
668
669 let driver_path = specs_dir.join("2026-01-24-002-def.md");
670 driver_spec.save(&driver_path).unwrap();
671
672 mark_driver_in_progress(specs_dir, "2026-01-24-002-def.1").unwrap();
674
675 let updated_driver = Spec::load(&driver_path).unwrap();
677 assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
678 }
679
680 #[test]
681 fn test_mark_driver_in_progress_nonexistent_driver() {
682 use tempfile::TempDir;
683
684 let temp_dir = TempDir::new().unwrap();
685 let specs_dir = temp_dir.path();
686
687 mark_driver_in_progress(specs_dir, "2026-01-24-003-ghi.1").unwrap();
690 }
691
692 #[test]
693 fn test_get_incomplete_members() {
694 let driver = Spec::parse(
696 "2026-01-24-005-mno",
697 r#"---
698status: in_progress
699---
700# Driver
701"#,
702 )
703 .unwrap();
704
705 let member1 = Spec::parse(
706 "2026-01-24-005-mno.1",
707 r#"---
708status: completed
709---
710# Member 1
711"#,
712 )
713 .unwrap();
714
715 let member2 = Spec::parse(
716 "2026-01-24-005-mno.2",
717 r#"---
718status: pending
719---
720# Member 2
721"#,
722 )
723 .unwrap();
724
725 let member3 = Spec::parse(
726 "2026-01-24-005-mno.3",
727 r#"---
728status: in_progress
729---
730# Member 3
731"#,
732 )
733 .unwrap();
734
735 let all_specs = vec![driver.clone(), member1, member2, member3];
736 let incomplete = get_incomplete_members(&driver.id, &all_specs);
737 assert_eq!(incomplete.len(), 2);
738 assert!(incomplete.contains(&"2026-01-24-005-mno.2".to_string()));
739 assert!(incomplete.contains(&"2026-01-24-005-mno.3".to_string()));
740 }
741
742 #[test]
743 fn test_auto_complete_driver_not_member_spec() {
744 use tempfile::TempDir;
745
746 let temp_dir = TempDir::new().unwrap();
747 let specs_dir = temp_dir.path();
748
749 let driver_spec = Spec::parse(
751 "2026-01-24-006-pqr",
752 r#"---
753status: in_progress
754---
755# Driver spec
756"#,
757 )
758 .unwrap();
759
760 let result =
761 auto_complete_driver_if_ready("2026-01-24-006-pqr", &[driver_spec], specs_dir).unwrap();
762 assert!(
763 !result,
764 "Non-member spec should not trigger auto-completion"
765 );
766 }
767
768 #[test]
769 fn test_auto_complete_driver_when_already_completed() {
770 use tempfile::TempDir;
771
772 let temp_dir = TempDir::new().unwrap();
773 let specs_dir = temp_dir.path();
774
775 let driver_spec = Spec::parse(
777 "2026-01-24-007-stu",
778 r#"---
779status: completed
780---
781# Driver spec
782"#,
783 )
784 .unwrap();
785
786 let member_spec = Spec::parse(
787 "2026-01-24-007-stu.1",
788 r#"---
789status: completed
790---
791# Member 1
792"#,
793 )
794 .unwrap();
795
796 let all_specs = vec![driver_spec, member_spec];
797 let result =
798 auto_complete_driver_if_ready("2026-01-24-007-stu.1", &all_specs, specs_dir).unwrap();
799 assert!(
800 !result,
801 "Driver already completed should not be re-completed"
802 );
803 }
804
805 #[test]
806 fn test_auto_complete_driver_from_pending() {
807 use std::fs;
808 use tempfile::TempDir;
809
810 let temp_dir = TempDir::new().unwrap();
811 let specs_dir = temp_dir.path();
812
813 let driver_content = r#"---
815status: pending
816---
817# Driver spec
818"#;
819 fs::write(specs_dir.join("2026-01-24-009-xyz.md"), driver_content).unwrap();
820
821 let driver_spec = Spec::parse("2026-01-24-009-xyz", driver_content).unwrap();
823
824 let member_spec = Spec::parse(
825 "2026-01-24-009-xyz.1",
826 r#"---
827status: completed
828---
829# Member 1
830"#,
831 )
832 .unwrap();
833
834 let all_specs = vec![driver_spec, member_spec];
835
836 let result =
838 auto_complete_driver_if_ready("2026-01-24-009-xyz.1", &all_specs, specs_dir).unwrap();
839 assert!(
840 result,
841 "Pending driver should be auto-completed when all members are done (chain mode)"
842 );
843
844 let updated_driver = Spec::load(&specs_dir.join("2026-01-24-009-xyz.md")).unwrap();
846 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
847 }
848
849 #[test]
850 fn test_auto_complete_driver_incomplete_members() {
851 use tempfile::TempDir;
852
853 let temp_dir = TempDir::new().unwrap();
854 let specs_dir = temp_dir.path();
855
856 let driver_spec = Spec {
858 id: "2026-01-24-008-vwx".to_string(),
859 frontmatter: crate::spec::SpecFrontmatter {
860 status: SpecStatus::InProgress,
861 ..Default::default()
862 },
863 title: Some("Driver".to_string()),
864 body: "# Driver\n\nBody.".to_string(),
865 };
866
867 let driver_path = specs_dir.join("2026-01-24-008-vwx.md");
868 driver_spec.save(&driver_path).unwrap();
869
870 let member1 = Spec::parse(
872 "2026-01-24-008-vwx.1",
873 r#"---
874status: completed
875---
876# Member 1
877"#,
878 )
879 .unwrap();
880
881 let member2 = Spec::parse(
882 "2026-01-24-008-vwx.2",
883 r#"---
884status: in_progress
885---
886# Member 2
887"#,
888 )
889 .unwrap();
890
891 let all_specs = vec![driver_spec, member1, member2];
892 let result =
893 auto_complete_driver_if_ready("2026-01-24-008-vwx.1", &all_specs, specs_dir).unwrap();
894 assert!(
895 !result,
896 "Driver should not complete when members are incomplete"
897 );
898 }
899
900 #[test]
901 fn test_auto_complete_driver_success() {
902 use tempfile::TempDir;
903
904 let temp_dir = TempDir::new().unwrap();
905 let specs_dir = temp_dir.path();
906
907 let driver_spec = Spec {
909 id: "2026-01-24-009-yz0".to_string(),
910 frontmatter: crate::spec::SpecFrontmatter {
911 status: SpecStatus::InProgress,
912 ..Default::default()
913 },
914 title: Some("Driver".to_string()),
915 body: "# Driver\n\nBody.".to_string(),
916 };
917
918 let driver_path = specs_dir.join("2026-01-24-009-yz0.md");
919 driver_spec.save(&driver_path).unwrap();
920
921 let member1 = Spec::parse(
923 "2026-01-24-009-yz0.1",
924 r#"---
925status: completed
926---
927# Member 1
928"#,
929 )
930 .unwrap();
931
932 let member2 = Spec::parse(
933 "2026-01-24-009-yz0.2",
934 r#"---
935status: completed
936---
937# Member 2
938"#,
939 )
940 .unwrap();
941
942 let all_specs = vec![driver_spec, member1, member2];
943
944 let result =
946 auto_complete_driver_if_ready("2026-01-24-009-yz0.2", &all_specs, specs_dir).unwrap();
947 assert!(
948 result,
949 "Driver should be auto-completed when all members are completed"
950 );
951
952 let updated_driver = Spec::load(&driver_path).unwrap();
954 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
955 assert_eq!(
956 updated_driver.frontmatter.model,
957 Some("auto-completed".to_string())
958 );
959 assert!(updated_driver.frontmatter.completed_at.is_some());
960 }
961
962 #[test]
963 fn test_auto_complete_driver_nonexistent_driver() {
964 use tempfile::TempDir;
965
966 let temp_dir = TempDir::new().unwrap();
967 let specs_dir = temp_dir.path();
968
969 let all_specs = vec![];
971 let result =
972 auto_complete_driver_if_ready("2026-01-24-010-abc.1", &all_specs, specs_dir).unwrap();
973 assert!(
974 !result,
975 "Should return false when driver spec doesn't exist"
976 );
977 }
978
979 #[test]
980 fn test_auto_complete_driver_single_member() {
981 use tempfile::TempDir;
982
983 let temp_dir = TempDir::new().unwrap();
984 let specs_dir = temp_dir.path();
985
986 let driver_spec = Spec {
988 id: "2026-01-24-011-def".to_string(),
989 frontmatter: crate::spec::SpecFrontmatter {
990 status: SpecStatus::InProgress,
991 ..Default::default()
992 },
993 title: Some("Driver".to_string()),
994 body: "# Driver\n\nBody.".to_string(),
995 };
996
997 let driver_path = specs_dir.join("2026-01-24-011-def.md");
998 driver_spec.save(&driver_path).unwrap();
999
1000 let member = Spec::parse(
1002 "2026-01-24-011-def.1",
1003 r#"---
1004status: completed
1005---
1006# Member 1
1007"#,
1008 )
1009 .unwrap();
1010
1011 let all_specs = vec![driver_spec, member];
1012
1013 let result =
1015 auto_complete_driver_if_ready("2026-01-24-011-def.1", &all_specs, specs_dir).unwrap();
1016 assert!(
1017 result,
1018 "Driver should be auto-completed when single member completes"
1019 );
1020
1021 let updated_driver = Spec::load(&driver_path).unwrap();
1023 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
1024 assert_eq!(
1025 updated_driver.frontmatter.model,
1026 Some("auto-completed".to_string())
1027 );
1028 }
1029
1030 #[test]
1031 fn test_compare_spec_ids_member_numeric_sort() {
1032 use std::cmp::Ordering;
1033
1034 assert_eq!(
1036 compare_spec_ids("2026-01-25-00y-abc.2", "2026-01-25-00y-abc.10"),
1037 Ordering::Less
1038 );
1039 assert_eq!(
1040 compare_spec_ids("2026-01-25-00y-abc.10", "2026-01-25-00y-abc.2"),
1041 Ordering::Greater
1042 );
1043 assert_eq!(
1044 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc.1"),
1045 Ordering::Equal
1046 );
1047
1048 assert_eq!(
1050 compare_spec_ids("2026-01-25-00y-abc.99", "2026-01-25-00y-abc.100"),
1051 Ordering::Less
1052 );
1053 }
1054
1055 #[test]
1056 fn test_compare_spec_ids_different_drivers() {
1057 use std::cmp::Ordering;
1058
1059 assert_eq!(
1061 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-def.1"),
1062 Ordering::Less
1063 );
1064 assert_eq!(
1065 compare_spec_ids("2026-01-25-00y-def.1", "2026-01-25-00y-abc.1"),
1066 Ordering::Greater
1067 );
1068 }
1069
1070 #[test]
1071 fn test_compare_spec_ids_non_member_specs() {
1072 use std::cmp::Ordering;
1073
1074 assert_eq!(
1076 compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-def"),
1077 Ordering::Less
1078 );
1079 assert_eq!(
1080 compare_spec_ids("2026-01-25-00y-def", "2026-01-25-00y-abc"),
1081 Ordering::Greater
1082 );
1083 }
1084
1085 #[test]
1086 fn test_compare_spec_ids_driver_vs_member() {
1087 use std::cmp::Ordering;
1088
1089 assert_eq!(
1091 compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-abc.1"),
1092 Ordering::Less
1093 );
1094 assert_eq!(
1095 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc"),
1096 Ordering::Greater
1097 );
1098 }
1099
1100 #[test]
1101 fn test_compare_spec_ids_sorting_list() {
1102 let mut ids = vec![
1104 "2026-01-25-00y-abc.10",
1105 "2026-01-25-00y-abc.2",
1106 "2026-01-25-00y-abc.1",
1107 "2026-01-25-00y-abc",
1108 "2026-01-25-00y-abc.3",
1109 ];
1110
1111 ids.sort_by(|a, b| compare_spec_ids(a, b));
1112
1113 assert_eq!(
1114 ids,
1115 vec![
1116 "2026-01-25-00y-abc",
1117 "2026-01-25-00y-abc.1",
1118 "2026-01-25-00y-abc.2",
1119 "2026-01-25-00y-abc.3",
1120 "2026-01-25-00y-abc.10",
1121 ]
1122 );
1123 }
1124
1125 #[test]
1126 fn test_compare_spec_ids_base36_sequence_rollover() {
1127 use std::cmp::Ordering;
1128
1129 assert_eq!(
1131 compare_spec_ids("2026-01-25-010-xxx", "2026-01-25-00z-yyy"),
1132 Ordering::Greater
1133 );
1134 assert_eq!(
1135 compare_spec_ids("2026-01-25-00z-yyy", "2026-01-25-010-xxx"),
1136 Ordering::Less
1137 );
1138
1139 let mut ids = vec![
1141 "2026-01-25-010-aaa",
1142 "2026-01-25-00a-bbb",
1143 "2026-01-25-00z-ccc",
1144 "2026-01-25-001-ddd",
1145 "2026-01-25-011-eee",
1146 ];
1147
1148 ids.sort_by(|a, b| compare_spec_ids(a, b));
1149
1150 assert_eq!(
1151 ids,
1152 vec![
1153 "2026-01-25-001-ddd", "2026-01-25-00a-bbb", "2026-01-25-00z-ccc", "2026-01-25-010-aaa", "2026-01-25-011-eee", ]
1159 );
1160 }
1161
1162 #[test]
1163 fn test_driver_auto_completion_with_two_members() {
1164 use tempfile::TempDir;
1165
1166 let temp_dir = TempDir::new().unwrap();
1167 let specs_dir = temp_dir.path();
1168
1169 let driver_spec = Spec {
1171 id: "2026-01-24-012-ghi".to_string(),
1172 frontmatter: crate::spec::SpecFrontmatter {
1173 status: SpecStatus::Pending,
1174 ..Default::default()
1175 },
1176 title: Some("Driver spec with 2 members".to_string()),
1177 body: "# Driver\n\nBody.".to_string(),
1178 };
1179
1180 let driver_path = specs_dir.join("2026-01-24-012-ghi.md");
1181 driver_spec.save(&driver_path).unwrap();
1182
1183 let _member1 = Spec::parse(
1185 "2026-01-24-012-ghi.1",
1186 r#"---
1187status: pending
1188---
1189# Member 1
1190"#,
1191 )
1192 .unwrap();
1193
1194 let member2 = Spec::parse(
1196 "2026-01-24-012-ghi.2",
1197 r#"---
1198status: pending
1199---
1200# Member 2
1201"#,
1202 )
1203 .unwrap();
1204
1205 mark_driver_in_progress(specs_dir, "2026-01-24-012-ghi.1").unwrap();
1207
1208 let updated_driver = Spec::load(&driver_path).unwrap();
1209 assert_eq!(
1210 updated_driver.frontmatter.status,
1211 SpecStatus::InProgress,
1212 "Driver should be in_progress after first member starts"
1213 );
1214
1215 let member1_completed = Spec::parse(
1217 "2026-01-24-012-ghi.1",
1218 r#"---
1219status: completed
1220---
1221# Member 1
1222"#,
1223 )
1224 .unwrap();
1225
1226 let all_specs = vec![
1227 updated_driver.clone(),
1228 member1_completed.clone(),
1229 member2.clone(),
1230 ];
1231 let result =
1232 auto_complete_driver_if_ready("2026-01-24-012-ghi.1", &all_specs, specs_dir).unwrap();
1233 assert!(
1234 !result,
1235 "Driver should NOT auto-complete when first member is done but second is pending"
1236 );
1237
1238 let still_in_progress = Spec::load(&driver_path).unwrap();
1239 assert_eq!(
1240 still_in_progress.frontmatter.status,
1241 SpecStatus::InProgress,
1242 "Driver should still be in_progress"
1243 );
1244
1245 let member2_completed = Spec::parse(
1247 "2026-01-24-012-ghi.2",
1248 r#"---
1249status: completed
1250---
1251# Member 2
1252"#,
1253 )
1254 .unwrap();
1255
1256 let all_specs = vec![
1257 still_in_progress.clone(),
1258 member1_completed.clone(),
1259 member2_completed.clone(),
1260 ];
1261 let result =
1262 auto_complete_driver_if_ready("2026-01-24-012-ghi.2", &all_specs, specs_dir).unwrap();
1263 assert!(
1264 result,
1265 "Driver should auto-complete when all members are completed"
1266 );
1267
1268 let final_driver = Spec::load(&driver_path).unwrap();
1269 assert_eq!(
1270 final_driver.frontmatter.status,
1271 SpecStatus::Completed,
1272 "Driver should be completed after all members complete"
1273 );
1274 assert_eq!(
1275 final_driver.frontmatter.model,
1276 Some("auto-completed".to_string()),
1277 "Driver should have auto-completed model"
1278 );
1279 assert!(
1280 final_driver.frontmatter.completed_at.is_some(),
1281 "Driver should have completed_at timestamp"
1282 );
1283 }
1284}