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 return false;
335 }
336 } else {
337 return false;
339 }
340 }
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() {
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_not_completed() {
646 let spec_prior_1 = Spec::parse(
648 "2026-01-24-001-abc.1",
649 r#"---
650status: pending
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_mark_driver_in_progress_when_member_starts() {
673 use tempfile::TempDir;
674
675 let temp_dir = TempDir::new().unwrap();
676 let specs_dir = temp_dir.path();
677
678 let driver_spec = Spec {
680 id: "2026-01-24-001-abc".to_string(),
681 frontmatter: crate::spec::SpecFrontmatter {
682 status: SpecStatus::Pending,
683 ..Default::default()
684 },
685 title: Some("Driver spec".to_string()),
686 body: "# Driver spec\n\nBody content.".to_string(),
687 };
688
689 let driver_path = specs_dir.join("2026-01-24-001-abc.md");
690 driver_spec.save(&driver_path).unwrap();
691
692 mark_driver_in_progress(specs_dir, "2026-01-24-001-abc.1").unwrap();
694
695 let updated_driver = Spec::load(&driver_path).unwrap();
697 assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
698 }
699
700 #[test]
701 fn test_mark_driver_in_progress_skips_if_already_in_progress() {
702 use tempfile::TempDir;
703
704 let temp_dir = TempDir::new().unwrap();
705 let specs_dir = temp_dir.path();
706
707 let driver_spec = Spec {
709 id: "2026-01-24-002-def".to_string(),
710 frontmatter: crate::spec::SpecFrontmatter {
711 status: SpecStatus::InProgress,
712 ..Default::default()
713 },
714 title: Some("Driver spec".to_string()),
715 body: "# Driver spec\n\nBody content.".to_string(),
716 };
717
718 let driver_path = specs_dir.join("2026-01-24-002-def.md");
719 driver_spec.save(&driver_path).unwrap();
720
721 mark_driver_in_progress(specs_dir, "2026-01-24-002-def.1").unwrap();
723
724 let updated_driver = Spec::load(&driver_path).unwrap();
726 assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
727 }
728
729 #[test]
730 fn test_mark_driver_in_progress_nonexistent_driver() {
731 use tempfile::TempDir;
732
733 let temp_dir = TempDir::new().unwrap();
734 let specs_dir = temp_dir.path();
735
736 mark_driver_in_progress(specs_dir, "2026-01-24-003-ghi.1").unwrap();
739 }
740
741 #[test]
742 fn test_get_incomplete_members() {
743 let driver = Spec::parse(
745 "2026-01-24-005-mno",
746 r#"---
747status: in_progress
748---
749# Driver
750"#,
751 )
752 .unwrap();
753
754 let member1 = Spec::parse(
755 "2026-01-24-005-mno.1",
756 r#"---
757status: completed
758---
759# Member 1
760"#,
761 )
762 .unwrap();
763
764 let member2 = Spec::parse(
765 "2026-01-24-005-mno.2",
766 r#"---
767status: pending
768---
769# Member 2
770"#,
771 )
772 .unwrap();
773
774 let member3 = Spec::parse(
775 "2026-01-24-005-mno.3",
776 r#"---
777status: in_progress
778---
779# Member 3
780"#,
781 )
782 .unwrap();
783
784 let all_specs = vec![driver.clone(), member1, member2, member3];
785 let incomplete = get_incomplete_members(&driver.id, &all_specs);
786 assert_eq!(incomplete.len(), 2);
787 assert!(incomplete.contains(&"2026-01-24-005-mno.2".to_string()));
788 assert!(incomplete.contains(&"2026-01-24-005-mno.3".to_string()));
789 }
790
791 #[test]
792 fn test_auto_complete_driver_not_member_spec() {
793 use tempfile::TempDir;
794
795 let temp_dir = TempDir::new().unwrap();
796 let specs_dir = temp_dir.path();
797
798 let driver_spec = Spec::parse(
800 "2026-01-24-006-pqr",
801 r#"---
802status: in_progress
803---
804# Driver spec
805"#,
806 )
807 .unwrap();
808
809 let result =
810 auto_complete_driver_if_ready("2026-01-24-006-pqr", &[driver_spec], specs_dir).unwrap();
811 assert!(
812 !result,
813 "Non-member spec should not trigger auto-completion"
814 );
815 }
816
817 #[test]
818 fn test_auto_complete_driver_when_already_completed() {
819 use tempfile::TempDir;
820
821 let temp_dir = TempDir::new().unwrap();
822 let specs_dir = temp_dir.path();
823
824 let driver_spec = Spec::parse(
826 "2026-01-24-007-stu",
827 r#"---
828status: completed
829---
830# Driver spec
831"#,
832 )
833 .unwrap();
834
835 let member_spec = Spec::parse(
836 "2026-01-24-007-stu.1",
837 r#"---
838status: completed
839---
840# Member 1
841"#,
842 )
843 .unwrap();
844
845 let all_specs = vec![driver_spec, member_spec];
846 let result =
847 auto_complete_driver_if_ready("2026-01-24-007-stu.1", &all_specs, specs_dir).unwrap();
848 assert!(
849 !result,
850 "Driver already completed should not be re-completed"
851 );
852 }
853
854 #[test]
855 fn test_auto_complete_driver_from_pending() {
856 use std::fs;
857 use tempfile::TempDir;
858
859 let temp_dir = TempDir::new().unwrap();
860 let specs_dir = temp_dir.path();
861
862 let driver_content = r#"---
864status: pending
865---
866# Driver spec
867"#;
868 fs::write(specs_dir.join("2026-01-24-009-xyz.md"), driver_content).unwrap();
869
870 let driver_spec = Spec::parse("2026-01-24-009-xyz", driver_content).unwrap();
872
873 let member_spec = Spec::parse(
874 "2026-01-24-009-xyz.1",
875 r#"---
876status: completed
877---
878# Member 1
879"#,
880 )
881 .unwrap();
882
883 let all_specs = vec![driver_spec, member_spec];
884
885 let result =
887 auto_complete_driver_if_ready("2026-01-24-009-xyz.1", &all_specs, specs_dir).unwrap();
888 assert!(
889 result,
890 "Pending driver should be auto-completed when all members are done (chain mode)"
891 );
892
893 let updated_driver = Spec::load(&specs_dir.join("2026-01-24-009-xyz.md")).unwrap();
895 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
896 }
897
898 #[test]
899 fn test_auto_complete_driver_incomplete_members() {
900 use tempfile::TempDir;
901
902 let temp_dir = TempDir::new().unwrap();
903 let specs_dir = temp_dir.path();
904
905 let driver_spec = Spec {
907 id: "2026-01-24-008-vwx".to_string(),
908 frontmatter: crate::spec::SpecFrontmatter {
909 status: SpecStatus::InProgress,
910 ..Default::default()
911 },
912 title: Some("Driver".to_string()),
913 body: "# Driver\n\nBody.".to_string(),
914 };
915
916 let driver_path = specs_dir.join("2026-01-24-008-vwx.md");
917 driver_spec.save(&driver_path).unwrap();
918
919 let member1 = Spec::parse(
921 "2026-01-24-008-vwx.1",
922 r#"---
923status: completed
924---
925# Member 1
926"#,
927 )
928 .unwrap();
929
930 let member2 = Spec::parse(
931 "2026-01-24-008-vwx.2",
932 r#"---
933status: in_progress
934---
935# Member 2
936"#,
937 )
938 .unwrap();
939
940 let all_specs = vec![driver_spec, member1, member2];
941 let result =
942 auto_complete_driver_if_ready("2026-01-24-008-vwx.1", &all_specs, specs_dir).unwrap();
943 assert!(
944 !result,
945 "Driver should not complete when members are incomplete"
946 );
947 }
948
949 #[test]
950 fn test_auto_complete_driver_success() {
951 use tempfile::TempDir;
952
953 let temp_dir = TempDir::new().unwrap();
954 let specs_dir = temp_dir.path();
955
956 let driver_spec = Spec {
958 id: "2026-01-24-009-yz0".to_string(),
959 frontmatter: crate::spec::SpecFrontmatter {
960 status: SpecStatus::InProgress,
961 ..Default::default()
962 },
963 title: Some("Driver".to_string()),
964 body: "# Driver\n\nBody.".to_string(),
965 };
966
967 let driver_path = specs_dir.join("2026-01-24-009-yz0.md");
968 driver_spec.save(&driver_path).unwrap();
969
970 let member1 = Spec::parse(
972 "2026-01-24-009-yz0.1",
973 r#"---
974status: completed
975---
976# Member 1
977"#,
978 )
979 .unwrap();
980
981 let member2 = Spec::parse(
982 "2026-01-24-009-yz0.2",
983 r#"---
984status: completed
985---
986# Member 2
987"#,
988 )
989 .unwrap();
990
991 let all_specs = vec![driver_spec, member1, member2];
992
993 let result =
995 auto_complete_driver_if_ready("2026-01-24-009-yz0.2", &all_specs, specs_dir).unwrap();
996 assert!(
997 result,
998 "Driver should be auto-completed when all members are completed"
999 );
1000
1001 let updated_driver = Spec::load(&driver_path).unwrap();
1003 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
1004 assert_eq!(
1005 updated_driver.frontmatter.model,
1006 Some("auto-completed".to_string())
1007 );
1008 assert!(updated_driver.frontmatter.completed_at.is_some());
1009 }
1010
1011 #[test]
1012 fn test_auto_complete_driver_nonexistent_driver() {
1013 use tempfile::TempDir;
1014
1015 let temp_dir = TempDir::new().unwrap();
1016 let specs_dir = temp_dir.path();
1017
1018 let all_specs = vec![];
1020 let result =
1021 auto_complete_driver_if_ready("2026-01-24-010-abc.1", &all_specs, specs_dir).unwrap();
1022 assert!(
1023 !result,
1024 "Should return false when driver spec doesn't exist"
1025 );
1026 }
1027
1028 #[test]
1029 fn test_auto_complete_driver_single_member() {
1030 use tempfile::TempDir;
1031
1032 let temp_dir = TempDir::new().unwrap();
1033 let specs_dir = temp_dir.path();
1034
1035 let driver_spec = Spec {
1037 id: "2026-01-24-011-def".to_string(),
1038 frontmatter: crate::spec::SpecFrontmatter {
1039 status: SpecStatus::InProgress,
1040 ..Default::default()
1041 },
1042 title: Some("Driver".to_string()),
1043 body: "# Driver\n\nBody.".to_string(),
1044 };
1045
1046 let driver_path = specs_dir.join("2026-01-24-011-def.md");
1047 driver_spec.save(&driver_path).unwrap();
1048
1049 let member = Spec::parse(
1051 "2026-01-24-011-def.1",
1052 r#"---
1053status: completed
1054---
1055# Member 1
1056"#,
1057 )
1058 .unwrap();
1059
1060 let all_specs = vec![driver_spec, member];
1061
1062 let result =
1064 auto_complete_driver_if_ready("2026-01-24-011-def.1", &all_specs, specs_dir).unwrap();
1065 assert!(
1066 result,
1067 "Driver should be auto-completed when single member completes"
1068 );
1069
1070 let updated_driver = Spec::load(&driver_path).unwrap();
1072 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
1073 assert_eq!(
1074 updated_driver.frontmatter.model,
1075 Some("auto-completed".to_string())
1076 );
1077 }
1078
1079 #[test]
1080 fn test_compare_spec_ids_member_numeric_sort() {
1081 use std::cmp::Ordering;
1082
1083 assert_eq!(
1085 compare_spec_ids("2026-01-25-00y-abc.2", "2026-01-25-00y-abc.10"),
1086 Ordering::Less
1087 );
1088 assert_eq!(
1089 compare_spec_ids("2026-01-25-00y-abc.10", "2026-01-25-00y-abc.2"),
1090 Ordering::Greater
1091 );
1092 assert_eq!(
1093 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc.1"),
1094 Ordering::Equal
1095 );
1096
1097 assert_eq!(
1099 compare_spec_ids("2026-01-25-00y-abc.99", "2026-01-25-00y-abc.100"),
1100 Ordering::Less
1101 );
1102 }
1103
1104 #[test]
1105 fn test_compare_spec_ids_different_drivers() {
1106 use std::cmp::Ordering;
1107
1108 assert_eq!(
1110 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-def.1"),
1111 Ordering::Less
1112 );
1113 assert_eq!(
1114 compare_spec_ids("2026-01-25-00y-def.1", "2026-01-25-00y-abc.1"),
1115 Ordering::Greater
1116 );
1117 }
1118
1119 #[test]
1120 fn test_compare_spec_ids_non_member_specs() {
1121 use std::cmp::Ordering;
1122
1123 assert_eq!(
1125 compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-def"),
1126 Ordering::Less
1127 );
1128 assert_eq!(
1129 compare_spec_ids("2026-01-25-00y-def", "2026-01-25-00y-abc"),
1130 Ordering::Greater
1131 );
1132 }
1133
1134 #[test]
1135 fn test_compare_spec_ids_driver_vs_member() {
1136 use std::cmp::Ordering;
1137
1138 assert_eq!(
1140 compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-abc.1"),
1141 Ordering::Less
1142 );
1143 assert_eq!(
1144 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc"),
1145 Ordering::Greater
1146 );
1147 }
1148
1149 #[test]
1150 fn test_compare_spec_ids_sorting_list() {
1151 let mut ids = vec![
1153 "2026-01-25-00y-abc.10",
1154 "2026-01-25-00y-abc.2",
1155 "2026-01-25-00y-abc.1",
1156 "2026-01-25-00y-abc",
1157 "2026-01-25-00y-abc.3",
1158 ];
1159
1160 ids.sort_by(|a, b| compare_spec_ids(a, b));
1161
1162 assert_eq!(
1163 ids,
1164 vec![
1165 "2026-01-25-00y-abc",
1166 "2026-01-25-00y-abc.1",
1167 "2026-01-25-00y-abc.2",
1168 "2026-01-25-00y-abc.3",
1169 "2026-01-25-00y-abc.10",
1170 ]
1171 );
1172 }
1173
1174 #[test]
1175 fn test_compare_spec_ids_base36_sequence_rollover() {
1176 use std::cmp::Ordering;
1177
1178 assert_eq!(
1180 compare_spec_ids("2026-01-25-010-xxx", "2026-01-25-00z-yyy"),
1181 Ordering::Greater
1182 );
1183 assert_eq!(
1184 compare_spec_ids("2026-01-25-00z-yyy", "2026-01-25-010-xxx"),
1185 Ordering::Less
1186 );
1187
1188 let mut ids = vec![
1190 "2026-01-25-010-aaa",
1191 "2026-01-25-00a-bbb",
1192 "2026-01-25-00z-ccc",
1193 "2026-01-25-001-ddd",
1194 "2026-01-25-011-eee",
1195 ];
1196
1197 ids.sort_by(|a, b| compare_spec_ids(a, b));
1198
1199 assert_eq!(
1200 ids,
1201 vec![
1202 "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", ]
1208 );
1209 }
1210
1211 #[test]
1212 fn test_driver_auto_completion_with_two_members() {
1213 use tempfile::TempDir;
1214
1215 let temp_dir = TempDir::new().unwrap();
1216 let specs_dir = temp_dir.path();
1217
1218 let driver_spec = Spec {
1220 id: "2026-01-24-012-ghi".to_string(),
1221 frontmatter: crate::spec::SpecFrontmatter {
1222 status: SpecStatus::Pending,
1223 ..Default::default()
1224 },
1225 title: Some("Driver spec with 2 members".to_string()),
1226 body: "# Driver\n\nBody.".to_string(),
1227 };
1228
1229 let driver_path = specs_dir.join("2026-01-24-012-ghi.md");
1230 driver_spec.save(&driver_path).unwrap();
1231
1232 let _member1 = Spec::parse(
1234 "2026-01-24-012-ghi.1",
1235 r#"---
1236status: pending
1237---
1238# Member 1
1239"#,
1240 )
1241 .unwrap();
1242
1243 let member2 = Spec::parse(
1245 "2026-01-24-012-ghi.2",
1246 r#"---
1247status: pending
1248---
1249# Member 2
1250"#,
1251 )
1252 .unwrap();
1253
1254 mark_driver_in_progress(specs_dir, "2026-01-24-012-ghi.1").unwrap();
1256
1257 let updated_driver = Spec::load(&driver_path).unwrap();
1258 assert_eq!(
1259 updated_driver.frontmatter.status,
1260 SpecStatus::InProgress,
1261 "Driver should be in_progress after first member starts"
1262 );
1263
1264 let member1_completed = Spec::parse(
1266 "2026-01-24-012-ghi.1",
1267 r#"---
1268status: completed
1269---
1270# Member 1
1271"#,
1272 )
1273 .unwrap();
1274
1275 let all_specs = vec![
1276 updated_driver.clone(),
1277 member1_completed.clone(),
1278 member2.clone(),
1279 ];
1280 let result =
1281 auto_complete_driver_if_ready("2026-01-24-012-ghi.1", &all_specs, specs_dir).unwrap();
1282 assert!(
1283 !result,
1284 "Driver should NOT auto-complete when first member is done but second is pending"
1285 );
1286
1287 let still_in_progress = Spec::load(&driver_path).unwrap();
1288 assert_eq!(
1289 still_in_progress.frontmatter.status,
1290 SpecStatus::InProgress,
1291 "Driver should still be in_progress"
1292 );
1293
1294 let member2_completed = Spec::parse(
1296 "2026-01-24-012-ghi.2",
1297 r#"---
1298status: completed
1299---
1300# Member 2
1301"#,
1302 )
1303 .unwrap();
1304
1305 let all_specs = vec![
1306 still_in_progress.clone(),
1307 member1_completed.clone(),
1308 member2_completed.clone(),
1309 ];
1310 let result =
1311 auto_complete_driver_if_ready("2026-01-24-012-ghi.2", &all_specs, specs_dir).unwrap();
1312 assert!(
1313 result,
1314 "Driver should auto-complete when all members are completed"
1315 );
1316
1317 let final_driver = Spec::load(&driver_path).unwrap();
1318 assert_eq!(
1319 final_driver.frontmatter.status,
1320 SpecStatus::Completed,
1321 "Driver should be completed after all members complete"
1322 );
1323 assert_eq!(
1324 final_driver.frontmatter.model,
1325 Some("auto-completed".to_string()),
1326 "Driver should have auto-completed model"
1327 );
1328 assert!(
1329 final_driver.frontmatter.completed_at.is_some(),
1330 "Driver should have completed_at timestamp"
1331 );
1332 }
1333}