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.iter().all(|m| {
80 m.frontmatter.status == SpecStatus::Completed
81 || m.frontmatter.status == SpecStatus::Cancelled
82 })
83}
84
85pub fn get_incomplete_members(driver_id: &str, all_specs: &[Spec]) -> Vec<String> {
104 get_members(driver_id, all_specs)
105 .into_iter()
106 .filter(|m| m.frontmatter.status != SpecStatus::Completed)
107 .map(|m| m.id.clone())
108 .collect()
109}
110
111pub fn extract_driver_id(member_id: &str) -> Option<String> {
125 if let Some(pos) = member_id.find('.') {
127 let (prefix, suffix) = member_id.split_at(pos);
128 if suffix.len() > 1
130 && suffix[1..]
131 .chars()
132 .next()
133 .is_some_and(|c| c.is_ascii_digit())
134 {
135 return Some(prefix.to_string());
136 }
137 }
138 None
139}
140
141pub fn extract_member_number(member_id: &str) -> Option<u32> {
157 if let Some(pos) = member_id.find('.') {
158 let suffix = &member_id[pos + 1..];
159 let num_str: String = suffix.chars().take_while(|c| c.is_ascii_digit()).collect();
161 if !num_str.is_empty() {
162 return num_str.parse::<u32>().ok();
163 }
164 }
165 None
166}
167
168pub fn compare_spec_ids(a: &str, b: &str) -> std::cmp::Ordering {
193 use std::cmp::Ordering;
194
195 let a_driver = extract_driver_id(a);
197 let b_driver = extract_driver_id(b);
198
199 match (a_driver, b_driver) {
200 (Some(a_base), Some(b_base)) => {
201 match compare_base_ids(&a_base, &b_base) {
204 Ordering::Equal => {
205 let a_num = extract_member_number(a).unwrap_or(u32::MAX);
207 let b_num = extract_member_number(b).unwrap_or(u32::MAX);
208 a_num.cmp(&b_num)
209 }
210 other => other,
211 }
212 }
213 (Some(a_base), None) => {
214 match compare_base_ids(&a_base, b) {
217 Ordering::Equal => {
218 Ordering::Greater
220 }
221 other => other,
222 }
223 }
224 (None, Some(b_base)) => {
225 match compare_base_ids(a, &b_base) {
228 Ordering::Equal => {
229 Ordering::Less
231 }
232 other => other,
233 }
234 }
235 (None, None) => {
236 compare_base_ids(a, b)
238 }
239 }
240}
241
242fn compare_base_ids(a: &str, b: &str) -> std::cmp::Ordering {
249 use std::cmp::Ordering;
250
251 let a_parts = parse_spec_id_parts(a);
253 let b_parts = parse_spec_id_parts(b);
254
255 match (a_parts, b_parts) {
256 (Some((a_date, a_seq, a_suffix)), Some((b_date, b_seq, b_suffix))) => {
257 match a_date.cmp(b_date) {
259 Ordering::Equal => {
260 match a_seq.cmp(&b_seq) {
262 Ordering::Equal => {
263 a_suffix.cmp(b_suffix)
265 }
266 other => other,
267 }
268 }
269 other => other,
270 }
271 }
272 _ => a.cmp(b),
274 }
275}
276
277fn parse_spec_id_parts(id: &str) -> Option<(&str, u32, &str)> {
280 let parts: Vec<&str> = id.split('-').collect();
281
282 if parts.len() < 5 {
284 return None;
285 }
286
287 let date = &id[..10]; let seq = crate::id::parse_base36(parts[3])?;
292
293 let suffix = parts[4];
295
296 Some((date, seq, suffix))
297}
298
299pub fn all_prior_siblings_completed(member_id: &str, all_specs: &[Spec]) -> bool {
317 if let Some(member_spec) = all_specs.iter().find(|s| s.id == member_id) {
319 if member_spec.frontmatter.depends_on.is_some() {
321 return true;
322 }
323 }
324
325 if let Some(driver_id) = extract_driver_id(member_id) {
327 if let Some(member_num) = extract_member_number(member_id) {
328 for i in 1..member_num {
330 let sibling_id = format!("{}.{}", driver_id, i);
331 let sibling = all_specs.iter().find(|s| s.id == sibling_id);
332 if let Some(s) = sibling {
333 if s.frontmatter.status != SpecStatus::Completed
334 && s.frontmatter.status != SpecStatus::Cancelled
335 {
336 return false;
337 }
338 }
339 }
341 return true;
342 }
343 }
344 true
346}
347
348pub fn mark_driver_in_progress_conditional(
379 specs_dir: &Path,
380 member_id: &str,
381 skip: bool,
382) -> Result<()> {
383 use crate::spec::TransitionBuilder;
384
385 if skip {
386 return Ok(());
387 }
388
389 if let Some(driver_id) = extract_driver_id(member_id) {
390 let driver_path = specs_dir.join(format!("{}.md", driver_id));
392 if driver_path.exists() {
393 let mut driver = Spec::load(&driver_path)?;
394 if driver.frontmatter.status == SpecStatus::Pending {
395 TransitionBuilder::new(&mut driver).to(SpecStatus::InProgress)?;
396 driver.save(&driver_path)?;
397 }
398 }
399 }
400 Ok(())
401}
402
403pub fn mark_driver_in_progress(specs_dir: &Path, member_id: &str) -> Result<()> {
408 mark_driver_in_progress_conditional(specs_dir, member_id, false)
409}
410
411pub fn auto_complete_driver_if_ready(
437 member_id: &str,
438 all_specs: &[Spec],
439 specs_dir: &Path,
440) -> Result<bool> {
441 let Some(driver_id) = extract_driver_id(member_id) else {
443 return Ok(false);
444 };
445
446 let Some(driver_spec) = all_specs.iter().find(|s| s.id == driver_id) else {
448 return Ok(false);
449 };
450
451 if driver_spec.frontmatter.status != SpecStatus::InProgress
454 && driver_spec.frontmatter.status != SpecStatus::Pending
455 {
456 return Ok(false);
457 }
458
459 if !all_members_completed(&driver_id, all_specs) {
461 return Ok(false);
462 }
463
464 let driver_path = specs_dir.join(format!("{}.md", driver_id));
466 let mut driver = Spec::load(&driver_path)?;
467
468 use crate::spec::TransitionBuilder;
469 TransitionBuilder::new(&mut driver)
471 .force()
472 .to(SpecStatus::Completed)?;
473 driver.frontmatter.completed_at = Some(crate::utc_now_iso());
474 driver.frontmatter.model = Some("auto-completed".to_string());
475
476 driver.save(&driver_path)?;
477
478 Ok(true)
479}
480
481pub fn mark_driver_failed_on_member_failure(member_id: &str, specs_dir: &Path) -> Result<bool> {
497 let Some(driver_id) = extract_driver_id(member_id) else {
499 return Ok(false);
500 };
501
502 let driver_path = specs_dir.join(format!("{}.md", driver_id));
504 if !driver_path.exists() {
505 return Ok(false);
506 }
507
508 let mut driver = Spec::load(&driver_path)?;
509
510 if driver.frontmatter.status != SpecStatus::InProgress
513 && driver.frontmatter.status != SpecStatus::Pending
514 {
515 return Ok(false);
516 }
517
518 use crate::spec::TransitionBuilder;
520 TransitionBuilder::new(&mut driver)
521 .force()
522 .to(SpecStatus::Failed)?;
523
524 driver.save(&driver_path)?;
525
526 Ok(true)
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn test_is_member_of() {
535 assert!(is_member_of("2026-01-22-001-x7m.1", "2026-01-22-001-x7m"));
536 assert!(is_member_of("2026-01-22-001-x7m.2.1", "2026-01-22-001-x7m"));
537 assert!(!is_member_of("2026-01-22-001-x7m", "2026-01-22-001-x7m"));
538 assert!(!is_member_of("2026-01-22-002-y8n", "2026-01-22-001-x7m"));
539 }
540
541 #[test]
542 fn test_extract_driver_id() {
543 assert_eq!(
544 extract_driver_id("2026-01-22-001-x7m.1"),
545 Some("2026-01-22-001-x7m".to_string())
546 );
547 assert_eq!(
548 extract_driver_id("2026-01-22-001-x7m.2.1"),
549 Some("2026-01-22-001-x7m".to_string())
550 );
551 assert_eq!(extract_driver_id("2026-01-22-001-x7m"), None);
552 assert_eq!(extract_driver_id("2026-01-22-001-x7m.abc"), None);
553 }
554
555 #[test]
556 fn test_extract_member_number() {
557 assert_eq!(extract_member_number("2026-01-24-001-abc.1"), Some(1));
558 assert_eq!(extract_member_number("2026-01-24-001-abc.3"), Some(3));
559 assert_eq!(extract_member_number("2026-01-24-001-abc.10"), Some(10));
560 assert_eq!(extract_member_number("2026-01-24-001-abc.3.2"), Some(3));
561 assert_eq!(extract_member_number("2026-01-24-001-abc"), None);
562 assert_eq!(extract_member_number("2026-01-24-001-abc.abc"), None);
563 }
564
565 #[test]
566 fn test_all_prior_siblings_completed() {
567 let spec1 = Spec::parse(
569 "2026-01-24-001-abc.1",
570 r#"---
571status: pending
572---
573# Test
574"#,
575 )
576 .unwrap();
577
578 assert!(all_prior_siblings_completed(&spec1.id, &[]));
580
581 let spec_prior_1 = Spec::parse(
583 "2026-01-24-001-abc.1",
584 r#"---
585status: completed
586---
587# Test
588"#,
589 )
590 .unwrap();
591
592 let spec_prior_2 = Spec::parse(
593 "2026-01-24-001-abc.2",
594 r#"---
595status: completed
596---
597# Test
598"#,
599 )
600 .unwrap();
601
602 let spec3 = Spec::parse(
603 "2026-01-24-001-abc.3",
604 r#"---
605status: pending
606---
607# Test
608"#,
609 )
610 .unwrap();
611
612 let all_specs = vec![spec_prior_1, spec_prior_2, spec3.clone()];
613 assert!(all_prior_siblings_completed(&spec3.id, &all_specs));
614 }
615
616 #[test]
617 fn test_all_prior_siblings_completed_missing_skipped() {
618 let spec_prior_1 = Spec::parse(
620 "2026-01-24-001-abc.1",
621 r#"---
622status: completed
623---
624# Test
625"#,
626 )
627 .unwrap();
628
629 let spec3 = Spec::parse(
630 "2026-01-24-001-abc.3",
631 r#"---
632status: pending
633---
634# Test
635"#,
636 )
637 .unwrap();
638
639 let all_specs = vec![spec_prior_1, spec3.clone()];
641 assert!(all_prior_siblings_completed(&spec3.id, &all_specs));
642 }
643
644 #[test]
645 fn test_all_prior_siblings_completed_cancelled() {
646 let spec_prior_1 = Spec::parse(
648 "2026-01-24-001-abc.1",
649 r#"---
650status: cancelled
651---
652# Test
653"#,
654 )
655 .unwrap();
656
657 let spec2 = Spec::parse(
658 "2026-01-24-001-abc.2",
659 r#"---
660status: pending
661---
662# Test
663"#,
664 )
665 .unwrap();
666
667 let all_specs = vec![spec_prior_1, spec2.clone()];
668 assert!(all_prior_siblings_completed(&spec2.id, &all_specs));
669 }
670
671 #[test]
672 fn test_all_prior_siblings_completed_not_completed() {
673 let spec_prior_1 = Spec::parse(
675 "2026-01-24-001-abc.1",
676 r#"---
677status: pending
678---
679# Test
680"#,
681 )
682 .unwrap();
683
684 let spec2 = Spec::parse(
685 "2026-01-24-001-abc.2",
686 r#"---
687status: pending
688---
689# Test
690"#,
691 )
692 .unwrap();
693
694 let all_specs = vec![spec_prior_1, spec2.clone()];
695 assert!(!all_prior_siblings_completed(&spec2.id, &all_specs));
696 }
697
698 #[test]
699 fn test_mark_driver_in_progress_when_member_starts() {
700 use tempfile::TempDir;
701
702 let temp_dir = TempDir::new().unwrap();
703 let specs_dir = temp_dir.path();
704
705 let driver_spec = Spec {
707 id: "2026-01-24-001-abc".to_string(),
708 frontmatter: crate::spec::SpecFrontmatter {
709 status: SpecStatus::Pending,
710 ..Default::default()
711 },
712 title: Some("Driver spec".to_string()),
713 body: "# Driver spec\n\nBody content.".to_string(),
714 };
715
716 let driver_path = specs_dir.join("2026-01-24-001-abc.md");
717 driver_spec.save(&driver_path).unwrap();
718
719 mark_driver_in_progress(specs_dir, "2026-01-24-001-abc.1").unwrap();
721
722 let updated_driver = Spec::load(&driver_path).unwrap();
724 assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
725 }
726
727 #[test]
728 fn test_mark_driver_in_progress_skips_if_already_in_progress() {
729 use tempfile::TempDir;
730
731 let temp_dir = TempDir::new().unwrap();
732 let specs_dir = temp_dir.path();
733
734 let driver_spec = Spec {
736 id: "2026-01-24-002-def".to_string(),
737 frontmatter: crate::spec::SpecFrontmatter {
738 status: SpecStatus::InProgress,
739 ..Default::default()
740 },
741 title: Some("Driver spec".to_string()),
742 body: "# Driver spec\n\nBody content.".to_string(),
743 };
744
745 let driver_path = specs_dir.join("2026-01-24-002-def.md");
746 driver_spec.save(&driver_path).unwrap();
747
748 mark_driver_in_progress(specs_dir, "2026-01-24-002-def.1").unwrap();
750
751 let updated_driver = Spec::load(&driver_path).unwrap();
753 assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
754 }
755
756 #[test]
757 fn test_mark_driver_in_progress_nonexistent_driver() {
758 use tempfile::TempDir;
759
760 let temp_dir = TempDir::new().unwrap();
761 let specs_dir = temp_dir.path();
762
763 mark_driver_in_progress(specs_dir, "2026-01-24-003-ghi.1").unwrap();
766 }
767
768 #[test]
769 fn test_get_incomplete_members() {
770 let driver = Spec::parse(
772 "2026-01-24-005-mno",
773 r#"---
774status: in_progress
775---
776# Driver
777"#,
778 )
779 .unwrap();
780
781 let member1 = Spec::parse(
782 "2026-01-24-005-mno.1",
783 r#"---
784status: completed
785---
786# Member 1
787"#,
788 )
789 .unwrap();
790
791 let member2 = Spec::parse(
792 "2026-01-24-005-mno.2",
793 r#"---
794status: pending
795---
796# Member 2
797"#,
798 )
799 .unwrap();
800
801 let member3 = Spec::parse(
802 "2026-01-24-005-mno.3",
803 r#"---
804status: in_progress
805---
806# Member 3
807"#,
808 )
809 .unwrap();
810
811 let all_specs = vec![driver.clone(), member1, member2, member3];
812 let incomplete = get_incomplete_members(&driver.id, &all_specs);
813 assert_eq!(incomplete.len(), 2);
814 assert!(incomplete.contains(&"2026-01-24-005-mno.2".to_string()));
815 assert!(incomplete.contains(&"2026-01-24-005-mno.3".to_string()));
816 }
817
818 #[test]
819 fn test_auto_complete_driver_not_member_spec() {
820 use tempfile::TempDir;
821
822 let temp_dir = TempDir::new().unwrap();
823 let specs_dir = temp_dir.path();
824
825 let driver_spec = Spec::parse(
827 "2026-01-24-006-pqr",
828 r#"---
829status: in_progress
830---
831# Driver spec
832"#,
833 )
834 .unwrap();
835
836 let result =
837 auto_complete_driver_if_ready("2026-01-24-006-pqr", &[driver_spec], specs_dir).unwrap();
838 assert!(
839 !result,
840 "Non-member spec should not trigger auto-completion"
841 );
842 }
843
844 #[test]
845 fn test_auto_complete_driver_when_already_completed() {
846 use tempfile::TempDir;
847
848 let temp_dir = TempDir::new().unwrap();
849 let specs_dir = temp_dir.path();
850
851 let driver_spec = Spec::parse(
853 "2026-01-24-007-stu",
854 r#"---
855status: completed
856---
857# Driver spec
858"#,
859 )
860 .unwrap();
861
862 let member_spec = Spec::parse(
863 "2026-01-24-007-stu.1",
864 r#"---
865status: completed
866---
867# Member 1
868"#,
869 )
870 .unwrap();
871
872 let all_specs = vec![driver_spec, member_spec];
873 let result =
874 auto_complete_driver_if_ready("2026-01-24-007-stu.1", &all_specs, specs_dir).unwrap();
875 assert!(
876 !result,
877 "Driver already completed should not be re-completed"
878 );
879 }
880
881 #[test]
882 fn test_auto_complete_driver_from_pending() {
883 use std::fs;
884 use tempfile::TempDir;
885
886 let temp_dir = TempDir::new().unwrap();
887 let specs_dir = temp_dir.path();
888
889 let driver_content = r#"---
891status: pending
892---
893# Driver spec
894"#;
895 fs::write(specs_dir.join("2026-01-24-009-xyz.md"), driver_content).unwrap();
896
897 let driver_spec = Spec::parse("2026-01-24-009-xyz", driver_content).unwrap();
899
900 let member_spec = Spec::parse(
901 "2026-01-24-009-xyz.1",
902 r#"---
903status: completed
904---
905# Member 1
906"#,
907 )
908 .unwrap();
909
910 let all_specs = vec![driver_spec, member_spec];
911
912 let result =
914 auto_complete_driver_if_ready("2026-01-24-009-xyz.1", &all_specs, specs_dir).unwrap();
915 assert!(
916 result,
917 "Pending driver should be auto-completed when all members are done (chain mode)"
918 );
919
920 let updated_driver = Spec::load(&specs_dir.join("2026-01-24-009-xyz.md")).unwrap();
922 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
923 }
924
925 #[test]
926 fn test_auto_complete_driver_incomplete_members() {
927 use tempfile::TempDir;
928
929 let temp_dir = TempDir::new().unwrap();
930 let specs_dir = temp_dir.path();
931
932 let driver_spec = Spec {
934 id: "2026-01-24-008-vwx".to_string(),
935 frontmatter: crate::spec::SpecFrontmatter {
936 status: SpecStatus::InProgress,
937 ..Default::default()
938 },
939 title: Some("Driver".to_string()),
940 body: "# Driver\n\nBody.".to_string(),
941 };
942
943 let driver_path = specs_dir.join("2026-01-24-008-vwx.md");
944 driver_spec.save(&driver_path).unwrap();
945
946 let member1 = Spec::parse(
948 "2026-01-24-008-vwx.1",
949 r#"---
950status: completed
951---
952# Member 1
953"#,
954 )
955 .unwrap();
956
957 let member2 = Spec::parse(
958 "2026-01-24-008-vwx.2",
959 r#"---
960status: in_progress
961---
962# Member 2
963"#,
964 )
965 .unwrap();
966
967 let all_specs = vec![driver_spec, member1, member2];
968 let result =
969 auto_complete_driver_if_ready("2026-01-24-008-vwx.1", &all_specs, specs_dir).unwrap();
970 assert!(
971 !result,
972 "Driver should not complete when members are incomplete"
973 );
974 }
975
976 #[test]
977 fn test_auto_complete_driver_success() {
978 use tempfile::TempDir;
979
980 let temp_dir = TempDir::new().unwrap();
981 let specs_dir = temp_dir.path();
982
983 let driver_spec = Spec {
985 id: "2026-01-24-009-yz0".to_string(),
986 frontmatter: crate::spec::SpecFrontmatter {
987 status: SpecStatus::InProgress,
988 ..Default::default()
989 },
990 title: Some("Driver".to_string()),
991 body: "# Driver\n\nBody.".to_string(),
992 };
993
994 let driver_path = specs_dir.join("2026-01-24-009-yz0.md");
995 driver_spec.save(&driver_path).unwrap();
996
997 let member1 = Spec::parse(
999 "2026-01-24-009-yz0.1",
1000 r#"---
1001status: completed
1002---
1003# Member 1
1004"#,
1005 )
1006 .unwrap();
1007
1008 let member2 = Spec::parse(
1009 "2026-01-24-009-yz0.2",
1010 r#"---
1011status: completed
1012---
1013# Member 2
1014"#,
1015 )
1016 .unwrap();
1017
1018 let all_specs = vec![driver_spec, member1, member2];
1019
1020 let result =
1022 auto_complete_driver_if_ready("2026-01-24-009-yz0.2", &all_specs, specs_dir).unwrap();
1023 assert!(
1024 result,
1025 "Driver should be auto-completed when all members are completed"
1026 );
1027
1028 let updated_driver = Spec::load(&driver_path).unwrap();
1030 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
1031 assert_eq!(
1032 updated_driver.frontmatter.model,
1033 Some("auto-completed".to_string())
1034 );
1035 assert!(updated_driver.frontmatter.completed_at.is_some());
1036 }
1037
1038 #[test]
1039 fn test_auto_complete_driver_nonexistent_driver() {
1040 use tempfile::TempDir;
1041
1042 let temp_dir = TempDir::new().unwrap();
1043 let specs_dir = temp_dir.path();
1044
1045 let all_specs = vec![];
1047 let result =
1048 auto_complete_driver_if_ready("2026-01-24-010-abc.1", &all_specs, specs_dir).unwrap();
1049 assert!(
1050 !result,
1051 "Should return false when driver spec doesn't exist"
1052 );
1053 }
1054
1055 #[test]
1056 fn test_auto_complete_driver_single_member() {
1057 use tempfile::TempDir;
1058
1059 let temp_dir = TempDir::new().unwrap();
1060 let specs_dir = temp_dir.path();
1061
1062 let driver_spec = Spec {
1064 id: "2026-01-24-011-def".to_string(),
1065 frontmatter: crate::spec::SpecFrontmatter {
1066 status: SpecStatus::InProgress,
1067 ..Default::default()
1068 },
1069 title: Some("Driver".to_string()),
1070 body: "# Driver\n\nBody.".to_string(),
1071 };
1072
1073 let driver_path = specs_dir.join("2026-01-24-011-def.md");
1074 driver_spec.save(&driver_path).unwrap();
1075
1076 let member = Spec::parse(
1078 "2026-01-24-011-def.1",
1079 r#"---
1080status: completed
1081---
1082# Member 1
1083"#,
1084 )
1085 .unwrap();
1086
1087 let all_specs = vec![driver_spec, member];
1088
1089 let result =
1091 auto_complete_driver_if_ready("2026-01-24-011-def.1", &all_specs, specs_dir).unwrap();
1092 assert!(
1093 result,
1094 "Driver should be auto-completed when single member completes"
1095 );
1096
1097 let updated_driver = Spec::load(&driver_path).unwrap();
1099 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
1100 assert_eq!(
1101 updated_driver.frontmatter.model,
1102 Some("auto-completed".to_string())
1103 );
1104 }
1105
1106 #[test]
1107 fn test_compare_spec_ids_member_numeric_sort() {
1108 use std::cmp::Ordering;
1109
1110 assert_eq!(
1112 compare_spec_ids("2026-01-25-00y-abc.2", "2026-01-25-00y-abc.10"),
1113 Ordering::Less
1114 );
1115 assert_eq!(
1116 compare_spec_ids("2026-01-25-00y-abc.10", "2026-01-25-00y-abc.2"),
1117 Ordering::Greater
1118 );
1119 assert_eq!(
1120 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc.1"),
1121 Ordering::Equal
1122 );
1123
1124 assert_eq!(
1126 compare_spec_ids("2026-01-25-00y-abc.99", "2026-01-25-00y-abc.100"),
1127 Ordering::Less
1128 );
1129 }
1130
1131 #[test]
1132 fn test_compare_spec_ids_different_drivers() {
1133 use std::cmp::Ordering;
1134
1135 assert_eq!(
1137 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-def.1"),
1138 Ordering::Less
1139 );
1140 assert_eq!(
1141 compare_spec_ids("2026-01-25-00y-def.1", "2026-01-25-00y-abc.1"),
1142 Ordering::Greater
1143 );
1144 }
1145
1146 #[test]
1147 fn test_compare_spec_ids_non_member_specs() {
1148 use std::cmp::Ordering;
1149
1150 assert_eq!(
1152 compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-def"),
1153 Ordering::Less
1154 );
1155 assert_eq!(
1156 compare_spec_ids("2026-01-25-00y-def", "2026-01-25-00y-abc"),
1157 Ordering::Greater
1158 );
1159 }
1160
1161 #[test]
1162 fn test_compare_spec_ids_driver_vs_member() {
1163 use std::cmp::Ordering;
1164
1165 assert_eq!(
1167 compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-abc.1"),
1168 Ordering::Less
1169 );
1170 assert_eq!(
1171 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc"),
1172 Ordering::Greater
1173 );
1174 }
1175
1176 #[test]
1177 fn test_compare_spec_ids_sorting_list() {
1178 let mut ids = vec![
1180 "2026-01-25-00y-abc.10",
1181 "2026-01-25-00y-abc.2",
1182 "2026-01-25-00y-abc.1",
1183 "2026-01-25-00y-abc",
1184 "2026-01-25-00y-abc.3",
1185 ];
1186
1187 ids.sort_by(|a, b| compare_spec_ids(a, b));
1188
1189 assert_eq!(
1190 ids,
1191 vec![
1192 "2026-01-25-00y-abc",
1193 "2026-01-25-00y-abc.1",
1194 "2026-01-25-00y-abc.2",
1195 "2026-01-25-00y-abc.3",
1196 "2026-01-25-00y-abc.10",
1197 ]
1198 );
1199 }
1200
1201 #[test]
1202 fn test_compare_spec_ids_base36_sequence_rollover() {
1203 use std::cmp::Ordering;
1204
1205 assert_eq!(
1207 compare_spec_ids("2026-01-25-010-xxx", "2026-01-25-00z-yyy"),
1208 Ordering::Greater
1209 );
1210 assert_eq!(
1211 compare_spec_ids("2026-01-25-00z-yyy", "2026-01-25-010-xxx"),
1212 Ordering::Less
1213 );
1214
1215 let mut ids = vec![
1217 "2026-01-25-010-aaa",
1218 "2026-01-25-00a-bbb",
1219 "2026-01-25-00z-ccc",
1220 "2026-01-25-001-ddd",
1221 "2026-01-25-011-eee",
1222 ];
1223
1224 ids.sort_by(|a, b| compare_spec_ids(a, b));
1225
1226 assert_eq!(
1227 ids,
1228 vec![
1229 "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", ]
1235 );
1236 }
1237
1238 #[test]
1239 fn test_driver_auto_completion_with_two_members() {
1240 use tempfile::TempDir;
1241
1242 let temp_dir = TempDir::new().unwrap();
1243 let specs_dir = temp_dir.path();
1244
1245 let driver_spec = Spec {
1247 id: "2026-01-24-012-ghi".to_string(),
1248 frontmatter: crate::spec::SpecFrontmatter {
1249 status: SpecStatus::Pending,
1250 ..Default::default()
1251 },
1252 title: Some("Driver spec with 2 members".to_string()),
1253 body: "# Driver\n\nBody.".to_string(),
1254 };
1255
1256 let driver_path = specs_dir.join("2026-01-24-012-ghi.md");
1257 driver_spec.save(&driver_path).unwrap();
1258
1259 let _member1 = Spec::parse(
1261 "2026-01-24-012-ghi.1",
1262 r#"---
1263status: pending
1264---
1265# Member 1
1266"#,
1267 )
1268 .unwrap();
1269
1270 let member2 = Spec::parse(
1272 "2026-01-24-012-ghi.2",
1273 r#"---
1274status: pending
1275---
1276# Member 2
1277"#,
1278 )
1279 .unwrap();
1280
1281 mark_driver_in_progress(specs_dir, "2026-01-24-012-ghi.1").unwrap();
1283
1284 let updated_driver = Spec::load(&driver_path).unwrap();
1285 assert_eq!(
1286 updated_driver.frontmatter.status,
1287 SpecStatus::InProgress,
1288 "Driver should be in_progress after first member starts"
1289 );
1290
1291 let member1_completed = Spec::parse(
1293 "2026-01-24-012-ghi.1",
1294 r#"---
1295status: completed
1296---
1297# Member 1
1298"#,
1299 )
1300 .unwrap();
1301
1302 let all_specs = vec![
1303 updated_driver.clone(),
1304 member1_completed.clone(),
1305 member2.clone(),
1306 ];
1307 let result =
1308 auto_complete_driver_if_ready("2026-01-24-012-ghi.1", &all_specs, specs_dir).unwrap();
1309 assert!(
1310 !result,
1311 "Driver should NOT auto-complete when first member is done but second is pending"
1312 );
1313
1314 let still_in_progress = Spec::load(&driver_path).unwrap();
1315 assert_eq!(
1316 still_in_progress.frontmatter.status,
1317 SpecStatus::InProgress,
1318 "Driver should still be in_progress"
1319 );
1320
1321 let member2_completed = Spec::parse(
1323 "2026-01-24-012-ghi.2",
1324 r#"---
1325status: completed
1326---
1327# Member 2
1328"#,
1329 )
1330 .unwrap();
1331
1332 let all_specs = vec![
1333 still_in_progress.clone(),
1334 member1_completed.clone(),
1335 member2_completed.clone(),
1336 ];
1337 let result =
1338 auto_complete_driver_if_ready("2026-01-24-012-ghi.2", &all_specs, specs_dir).unwrap();
1339 assert!(
1340 result,
1341 "Driver should auto-complete when all members are completed"
1342 );
1343
1344 let final_driver = Spec::load(&driver_path).unwrap();
1345 assert_eq!(
1346 final_driver.frontmatter.status,
1347 SpecStatus::Completed,
1348 "Driver should be completed after all members complete"
1349 );
1350 assert_eq!(
1351 final_driver.frontmatter.model,
1352 Some("auto-completed".to_string()),
1353 "Driver should have auto-completed model"
1354 );
1355 assert!(
1356 final_driver.frontmatter.completed_at.is_some(),
1357 "Driver should have completed_at timestamp"
1358 );
1359 }
1360}