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 if skip {
383 return Ok(());
384 }
385
386 if let Some(driver_id) = extract_driver_id(member_id) {
387 let driver_path = specs_dir.join(format!("{}.md", driver_id));
389 if driver_path.exists() {
390 let mut driver = Spec::load(&driver_path)?;
391 if driver.frontmatter.status == SpecStatus::Pending {
392 driver.frontmatter.status = SpecStatus::InProgress;
393 driver.save(&driver_path)?;
394 }
395 }
396 }
397 Ok(())
398}
399
400pub fn mark_driver_in_progress(specs_dir: &Path, member_id: &str) -> Result<()> {
405 mark_driver_in_progress_conditional(specs_dir, member_id, false)
406}
407
408pub fn auto_complete_driver_if_ready(
434 member_id: &str,
435 all_specs: &[Spec],
436 specs_dir: &Path,
437) -> Result<bool> {
438 let Some(driver_id) = extract_driver_id(member_id) else {
440 return Ok(false);
441 };
442
443 let Some(driver_spec) = all_specs.iter().find(|s| s.id == driver_id) else {
445 return Ok(false);
446 };
447
448 if driver_spec.frontmatter.status != SpecStatus::InProgress
451 && driver_spec.frontmatter.status != SpecStatus::Pending
452 {
453 return Ok(false);
454 }
455
456 if !all_members_completed(&driver_id, all_specs) {
458 return Ok(false);
459 }
460
461 let driver_path = specs_dir.join(format!("{}.md", driver_id));
463 let mut driver = Spec::load(&driver_path)?;
464
465 driver.frontmatter.status = SpecStatus::Completed;
466 driver.frontmatter.completed_at = Some(
467 chrono::Local::now()
468 .format("%Y-%m-%dT%H:%M:%SZ")
469 .to_string(),
470 );
471 driver.frontmatter.model = Some("auto-completed".to_string());
472
473 driver.save(&driver_path)?;
474
475 Ok(true)
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn test_is_member_of() {
484 assert!(is_member_of("2026-01-22-001-x7m.1", "2026-01-22-001-x7m"));
485 assert!(is_member_of("2026-01-22-001-x7m.2.1", "2026-01-22-001-x7m"));
486 assert!(!is_member_of("2026-01-22-001-x7m", "2026-01-22-001-x7m"));
487 assert!(!is_member_of("2026-01-22-002-y8n", "2026-01-22-001-x7m"));
488 }
489
490 #[test]
491 fn test_extract_driver_id() {
492 assert_eq!(
493 extract_driver_id("2026-01-22-001-x7m.1"),
494 Some("2026-01-22-001-x7m".to_string())
495 );
496 assert_eq!(
497 extract_driver_id("2026-01-22-001-x7m.2.1"),
498 Some("2026-01-22-001-x7m".to_string())
499 );
500 assert_eq!(extract_driver_id("2026-01-22-001-x7m"), None);
501 assert_eq!(extract_driver_id("2026-01-22-001-x7m.abc"), None);
502 }
503
504 #[test]
505 fn test_extract_member_number() {
506 assert_eq!(extract_member_number("2026-01-24-001-abc.1"), Some(1));
507 assert_eq!(extract_member_number("2026-01-24-001-abc.3"), Some(3));
508 assert_eq!(extract_member_number("2026-01-24-001-abc.10"), Some(10));
509 assert_eq!(extract_member_number("2026-01-24-001-abc.3.2"), Some(3));
510 assert_eq!(extract_member_number("2026-01-24-001-abc"), None);
511 assert_eq!(extract_member_number("2026-01-24-001-abc.abc"), None);
512 }
513
514 #[test]
515 fn test_all_prior_siblings_completed() {
516 let spec1 = Spec::parse(
518 "2026-01-24-001-abc.1",
519 r#"---
520status: pending
521---
522# Test
523"#,
524 )
525 .unwrap();
526
527 assert!(all_prior_siblings_completed(&spec1.id, &[]));
529
530 let spec_prior_1 = Spec::parse(
532 "2026-01-24-001-abc.1",
533 r#"---
534status: completed
535---
536# Test
537"#,
538 )
539 .unwrap();
540
541 let spec_prior_2 = Spec::parse(
542 "2026-01-24-001-abc.2",
543 r#"---
544status: completed
545---
546# Test
547"#,
548 )
549 .unwrap();
550
551 let spec3 = Spec::parse(
552 "2026-01-24-001-abc.3",
553 r#"---
554status: pending
555---
556# Test
557"#,
558 )
559 .unwrap();
560
561 let all_specs = vec![spec_prior_1, spec_prior_2, spec3.clone()];
562 assert!(all_prior_siblings_completed(&spec3.id, &all_specs));
563 }
564
565 #[test]
566 fn test_all_prior_siblings_completed_missing() {
567 let spec_prior_1 = Spec::parse(
569 "2026-01-24-001-abc.1",
570 r#"---
571status: completed
572---
573# Test
574"#,
575 )
576 .unwrap();
577
578 let spec3 = Spec::parse(
579 "2026-01-24-001-abc.3",
580 r#"---
581status: pending
582---
583# Test
584"#,
585 )
586 .unwrap();
587
588 let all_specs = vec![spec_prior_1, spec3.clone()];
590 assert!(!all_prior_siblings_completed(&spec3.id, &all_specs));
591 }
592
593 #[test]
594 fn test_all_prior_siblings_completed_not_completed() {
595 let spec_prior_1 = Spec::parse(
597 "2026-01-24-001-abc.1",
598 r#"---
599status: pending
600---
601# Test
602"#,
603 )
604 .unwrap();
605
606 let spec2 = Spec::parse(
607 "2026-01-24-001-abc.2",
608 r#"---
609status: pending
610---
611# Test
612"#,
613 )
614 .unwrap();
615
616 let all_specs = vec![spec_prior_1, spec2.clone()];
617 assert!(!all_prior_siblings_completed(&spec2.id, &all_specs));
618 }
619
620 #[test]
621 fn test_mark_driver_in_progress_when_member_starts() {
622 use tempfile::TempDir;
623
624 let temp_dir = TempDir::new().unwrap();
625 let specs_dir = temp_dir.path();
626
627 let driver_spec = Spec {
629 id: "2026-01-24-001-abc".to_string(),
630 frontmatter: crate::spec::SpecFrontmatter {
631 status: SpecStatus::Pending,
632 ..Default::default()
633 },
634 title: Some("Driver spec".to_string()),
635 body: "# Driver spec\n\nBody content.".to_string(),
636 };
637
638 let driver_path = specs_dir.join("2026-01-24-001-abc.md");
639 driver_spec.save(&driver_path).unwrap();
640
641 mark_driver_in_progress(specs_dir, "2026-01-24-001-abc.1").unwrap();
643
644 let updated_driver = Spec::load(&driver_path).unwrap();
646 assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
647 }
648
649 #[test]
650 fn test_mark_driver_in_progress_skips_if_already_in_progress() {
651 use tempfile::TempDir;
652
653 let temp_dir = TempDir::new().unwrap();
654 let specs_dir = temp_dir.path();
655
656 let driver_spec = Spec {
658 id: "2026-01-24-002-def".to_string(),
659 frontmatter: crate::spec::SpecFrontmatter {
660 status: SpecStatus::InProgress,
661 ..Default::default()
662 },
663 title: Some("Driver spec".to_string()),
664 body: "# Driver spec\n\nBody content.".to_string(),
665 };
666
667 let driver_path = specs_dir.join("2026-01-24-002-def.md");
668 driver_spec.save(&driver_path).unwrap();
669
670 mark_driver_in_progress(specs_dir, "2026-01-24-002-def.1").unwrap();
672
673 let updated_driver = Spec::load(&driver_path).unwrap();
675 assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
676 }
677
678 #[test]
679 fn test_mark_driver_in_progress_nonexistent_driver() {
680 use tempfile::TempDir;
681
682 let temp_dir = TempDir::new().unwrap();
683 let specs_dir = temp_dir.path();
684
685 mark_driver_in_progress(specs_dir, "2026-01-24-003-ghi.1").unwrap();
688 }
689
690 #[test]
691 fn test_get_incomplete_members() {
692 let driver = Spec::parse(
694 "2026-01-24-005-mno",
695 r#"---
696status: in_progress
697---
698# Driver
699"#,
700 )
701 .unwrap();
702
703 let member1 = Spec::parse(
704 "2026-01-24-005-mno.1",
705 r#"---
706status: completed
707---
708# Member 1
709"#,
710 )
711 .unwrap();
712
713 let member2 = Spec::parse(
714 "2026-01-24-005-mno.2",
715 r#"---
716status: pending
717---
718# Member 2
719"#,
720 )
721 .unwrap();
722
723 let member3 = Spec::parse(
724 "2026-01-24-005-mno.3",
725 r#"---
726status: in_progress
727---
728# Member 3
729"#,
730 )
731 .unwrap();
732
733 let all_specs = vec![driver.clone(), member1, member2, member3];
734 let incomplete = get_incomplete_members(&driver.id, &all_specs);
735 assert_eq!(incomplete.len(), 2);
736 assert!(incomplete.contains(&"2026-01-24-005-mno.2".to_string()));
737 assert!(incomplete.contains(&"2026-01-24-005-mno.3".to_string()));
738 }
739
740 #[test]
741 fn test_auto_complete_driver_not_member_spec() {
742 use tempfile::TempDir;
743
744 let temp_dir = TempDir::new().unwrap();
745 let specs_dir = temp_dir.path();
746
747 let driver_spec = Spec::parse(
749 "2026-01-24-006-pqr",
750 r#"---
751status: in_progress
752---
753# Driver spec
754"#,
755 )
756 .unwrap();
757
758 let result =
759 auto_complete_driver_if_ready("2026-01-24-006-pqr", &[driver_spec], specs_dir).unwrap();
760 assert!(
761 !result,
762 "Non-member spec should not trigger auto-completion"
763 );
764 }
765
766 #[test]
767 fn test_auto_complete_driver_when_already_completed() {
768 use tempfile::TempDir;
769
770 let temp_dir = TempDir::new().unwrap();
771 let specs_dir = temp_dir.path();
772
773 let driver_spec = Spec::parse(
775 "2026-01-24-007-stu",
776 r#"---
777status: completed
778---
779# Driver spec
780"#,
781 )
782 .unwrap();
783
784 let member_spec = Spec::parse(
785 "2026-01-24-007-stu.1",
786 r#"---
787status: completed
788---
789# Member 1
790"#,
791 )
792 .unwrap();
793
794 let all_specs = vec![driver_spec, member_spec];
795 let result =
796 auto_complete_driver_if_ready("2026-01-24-007-stu.1", &all_specs, specs_dir).unwrap();
797 assert!(
798 !result,
799 "Driver already completed should not be re-completed"
800 );
801 }
802
803 #[test]
804 fn test_auto_complete_driver_from_pending() {
805 use std::fs;
806 use tempfile::TempDir;
807
808 let temp_dir = TempDir::new().unwrap();
809 let specs_dir = temp_dir.path();
810
811 let driver_content = r#"---
813status: pending
814---
815# Driver spec
816"#;
817 fs::write(specs_dir.join("2026-01-24-009-xyz.md"), driver_content).unwrap();
818
819 let driver_spec = Spec::parse("2026-01-24-009-xyz", driver_content).unwrap();
821
822 let member_spec = Spec::parse(
823 "2026-01-24-009-xyz.1",
824 r#"---
825status: completed
826---
827# Member 1
828"#,
829 )
830 .unwrap();
831
832 let all_specs = vec![driver_spec, member_spec];
833
834 let result =
836 auto_complete_driver_if_ready("2026-01-24-009-xyz.1", &all_specs, specs_dir).unwrap();
837 assert!(
838 result,
839 "Pending driver should be auto-completed when all members are done (chain mode)"
840 );
841
842 let updated_driver = Spec::load(&specs_dir.join("2026-01-24-009-xyz.md")).unwrap();
844 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
845 }
846
847 #[test]
848 fn test_auto_complete_driver_incomplete_members() {
849 use tempfile::TempDir;
850
851 let temp_dir = TempDir::new().unwrap();
852 let specs_dir = temp_dir.path();
853
854 let driver_spec = Spec {
856 id: "2026-01-24-008-vwx".to_string(),
857 frontmatter: crate::spec::SpecFrontmatter {
858 status: SpecStatus::InProgress,
859 ..Default::default()
860 },
861 title: Some("Driver".to_string()),
862 body: "# Driver\n\nBody.".to_string(),
863 };
864
865 let driver_path = specs_dir.join("2026-01-24-008-vwx.md");
866 driver_spec.save(&driver_path).unwrap();
867
868 let member1 = Spec::parse(
870 "2026-01-24-008-vwx.1",
871 r#"---
872status: completed
873---
874# Member 1
875"#,
876 )
877 .unwrap();
878
879 let member2 = Spec::parse(
880 "2026-01-24-008-vwx.2",
881 r#"---
882status: in_progress
883---
884# Member 2
885"#,
886 )
887 .unwrap();
888
889 let all_specs = vec![driver_spec, member1, member2];
890 let result =
891 auto_complete_driver_if_ready("2026-01-24-008-vwx.1", &all_specs, specs_dir).unwrap();
892 assert!(
893 !result,
894 "Driver should not complete when members are incomplete"
895 );
896 }
897
898 #[test]
899 fn test_auto_complete_driver_success() {
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-009-yz0".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-009-yz0.md");
917 driver_spec.save(&driver_path).unwrap();
918
919 let member1 = Spec::parse(
921 "2026-01-24-009-yz0.1",
922 r#"---
923status: completed
924---
925# Member 1
926"#,
927 )
928 .unwrap();
929
930 let member2 = Spec::parse(
931 "2026-01-24-009-yz0.2",
932 r#"---
933status: completed
934---
935# Member 2
936"#,
937 )
938 .unwrap();
939
940 let all_specs = vec![driver_spec, member1, member2];
941
942 let result =
944 auto_complete_driver_if_ready("2026-01-24-009-yz0.2", &all_specs, specs_dir).unwrap();
945 assert!(
946 result,
947 "Driver should be auto-completed when all members are completed"
948 );
949
950 let updated_driver = Spec::load(&driver_path).unwrap();
952 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
953 assert_eq!(
954 updated_driver.frontmatter.model,
955 Some("auto-completed".to_string())
956 );
957 assert!(updated_driver.frontmatter.completed_at.is_some());
958 }
959
960 #[test]
961 fn test_auto_complete_driver_nonexistent_driver() {
962 use tempfile::TempDir;
963
964 let temp_dir = TempDir::new().unwrap();
965 let specs_dir = temp_dir.path();
966
967 let all_specs = vec![];
969 let result =
970 auto_complete_driver_if_ready("2026-01-24-010-abc.1", &all_specs, specs_dir).unwrap();
971 assert!(
972 !result,
973 "Should return false when driver spec doesn't exist"
974 );
975 }
976
977 #[test]
978 fn test_auto_complete_driver_single_member() {
979 use tempfile::TempDir;
980
981 let temp_dir = TempDir::new().unwrap();
982 let specs_dir = temp_dir.path();
983
984 let driver_spec = Spec {
986 id: "2026-01-24-011-def".to_string(),
987 frontmatter: crate::spec::SpecFrontmatter {
988 status: SpecStatus::InProgress,
989 ..Default::default()
990 },
991 title: Some("Driver".to_string()),
992 body: "# Driver\n\nBody.".to_string(),
993 };
994
995 let driver_path = specs_dir.join("2026-01-24-011-def.md");
996 driver_spec.save(&driver_path).unwrap();
997
998 let member = Spec::parse(
1000 "2026-01-24-011-def.1",
1001 r#"---
1002status: completed
1003---
1004# Member 1
1005"#,
1006 )
1007 .unwrap();
1008
1009 let all_specs = vec![driver_spec, member];
1010
1011 let result =
1013 auto_complete_driver_if_ready("2026-01-24-011-def.1", &all_specs, specs_dir).unwrap();
1014 assert!(
1015 result,
1016 "Driver should be auto-completed when single member completes"
1017 );
1018
1019 let updated_driver = Spec::load(&driver_path).unwrap();
1021 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
1022 assert_eq!(
1023 updated_driver.frontmatter.model,
1024 Some("auto-completed".to_string())
1025 );
1026 }
1027
1028 #[test]
1029 fn test_compare_spec_ids_member_numeric_sort() {
1030 use std::cmp::Ordering;
1031
1032 assert_eq!(
1034 compare_spec_ids("2026-01-25-00y-abc.2", "2026-01-25-00y-abc.10"),
1035 Ordering::Less
1036 );
1037 assert_eq!(
1038 compare_spec_ids("2026-01-25-00y-abc.10", "2026-01-25-00y-abc.2"),
1039 Ordering::Greater
1040 );
1041 assert_eq!(
1042 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc.1"),
1043 Ordering::Equal
1044 );
1045
1046 assert_eq!(
1048 compare_spec_ids("2026-01-25-00y-abc.99", "2026-01-25-00y-abc.100"),
1049 Ordering::Less
1050 );
1051 }
1052
1053 #[test]
1054 fn test_compare_spec_ids_different_drivers() {
1055 use std::cmp::Ordering;
1056
1057 assert_eq!(
1059 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-def.1"),
1060 Ordering::Less
1061 );
1062 assert_eq!(
1063 compare_spec_ids("2026-01-25-00y-def.1", "2026-01-25-00y-abc.1"),
1064 Ordering::Greater
1065 );
1066 }
1067
1068 #[test]
1069 fn test_compare_spec_ids_non_member_specs() {
1070 use std::cmp::Ordering;
1071
1072 assert_eq!(
1074 compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-def"),
1075 Ordering::Less
1076 );
1077 assert_eq!(
1078 compare_spec_ids("2026-01-25-00y-def", "2026-01-25-00y-abc"),
1079 Ordering::Greater
1080 );
1081 }
1082
1083 #[test]
1084 fn test_compare_spec_ids_driver_vs_member() {
1085 use std::cmp::Ordering;
1086
1087 assert_eq!(
1089 compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-abc.1"),
1090 Ordering::Less
1091 );
1092 assert_eq!(
1093 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc"),
1094 Ordering::Greater
1095 );
1096 }
1097
1098 #[test]
1099 fn test_compare_spec_ids_sorting_list() {
1100 let mut ids = vec![
1102 "2026-01-25-00y-abc.10",
1103 "2026-01-25-00y-abc.2",
1104 "2026-01-25-00y-abc.1",
1105 "2026-01-25-00y-abc",
1106 "2026-01-25-00y-abc.3",
1107 ];
1108
1109 ids.sort_by(|a, b| compare_spec_ids(a, b));
1110
1111 assert_eq!(
1112 ids,
1113 vec![
1114 "2026-01-25-00y-abc",
1115 "2026-01-25-00y-abc.1",
1116 "2026-01-25-00y-abc.2",
1117 "2026-01-25-00y-abc.3",
1118 "2026-01-25-00y-abc.10",
1119 ]
1120 );
1121 }
1122
1123 #[test]
1124 fn test_compare_spec_ids_base36_sequence_rollover() {
1125 use std::cmp::Ordering;
1126
1127 assert_eq!(
1129 compare_spec_ids("2026-01-25-010-xxx", "2026-01-25-00z-yyy"),
1130 Ordering::Greater
1131 );
1132 assert_eq!(
1133 compare_spec_ids("2026-01-25-00z-yyy", "2026-01-25-010-xxx"),
1134 Ordering::Less
1135 );
1136
1137 let mut ids = vec![
1139 "2026-01-25-010-aaa",
1140 "2026-01-25-00a-bbb",
1141 "2026-01-25-00z-ccc",
1142 "2026-01-25-001-ddd",
1143 "2026-01-25-011-eee",
1144 ];
1145
1146 ids.sort_by(|a, b| compare_spec_ids(a, b));
1147
1148 assert_eq!(
1149 ids,
1150 vec![
1151 "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", ]
1157 );
1158 }
1159
1160 #[test]
1161 fn test_driver_auto_completion_with_two_members() {
1162 use tempfile::TempDir;
1163
1164 let temp_dir = TempDir::new().unwrap();
1165 let specs_dir = temp_dir.path();
1166
1167 let driver_spec = Spec {
1169 id: "2026-01-24-012-ghi".to_string(),
1170 frontmatter: crate::spec::SpecFrontmatter {
1171 status: SpecStatus::Pending,
1172 ..Default::default()
1173 },
1174 title: Some("Driver spec with 2 members".to_string()),
1175 body: "# Driver\n\nBody.".to_string(),
1176 };
1177
1178 let driver_path = specs_dir.join("2026-01-24-012-ghi.md");
1179 driver_spec.save(&driver_path).unwrap();
1180
1181 let _member1 = Spec::parse(
1183 "2026-01-24-012-ghi.1",
1184 r#"---
1185status: pending
1186---
1187# Member 1
1188"#,
1189 )
1190 .unwrap();
1191
1192 let member2 = Spec::parse(
1194 "2026-01-24-012-ghi.2",
1195 r#"---
1196status: pending
1197---
1198# Member 2
1199"#,
1200 )
1201 .unwrap();
1202
1203 mark_driver_in_progress(specs_dir, "2026-01-24-012-ghi.1").unwrap();
1205
1206 let updated_driver = Spec::load(&driver_path).unwrap();
1207 assert_eq!(
1208 updated_driver.frontmatter.status,
1209 SpecStatus::InProgress,
1210 "Driver should be in_progress after first member starts"
1211 );
1212
1213 let member1_completed = Spec::parse(
1215 "2026-01-24-012-ghi.1",
1216 r#"---
1217status: completed
1218---
1219# Member 1
1220"#,
1221 )
1222 .unwrap();
1223
1224 let all_specs = vec![
1225 updated_driver.clone(),
1226 member1_completed.clone(),
1227 member2.clone(),
1228 ];
1229 let result =
1230 auto_complete_driver_if_ready("2026-01-24-012-ghi.1", &all_specs, specs_dir).unwrap();
1231 assert!(
1232 !result,
1233 "Driver should NOT auto-complete when first member is done but second is pending"
1234 );
1235
1236 let still_in_progress = Spec::load(&driver_path).unwrap();
1237 assert_eq!(
1238 still_in_progress.frontmatter.status,
1239 SpecStatus::InProgress,
1240 "Driver should still be in_progress"
1241 );
1242
1243 let member2_completed = Spec::parse(
1245 "2026-01-24-012-ghi.2",
1246 r#"---
1247status: completed
1248---
1249# Member 2
1250"#,
1251 )
1252 .unwrap();
1253
1254 let all_specs = vec![
1255 still_in_progress.clone(),
1256 member1_completed.clone(),
1257 member2_completed.clone(),
1258 ];
1259 let result =
1260 auto_complete_driver_if_ready("2026-01-24-012-ghi.2", &all_specs, specs_dir).unwrap();
1261 assert!(
1262 result,
1263 "Driver should auto-complete when all members are completed"
1264 );
1265
1266 let final_driver = Spec::load(&driver_path).unwrap();
1267 assert_eq!(
1268 final_driver.frontmatter.status,
1269 SpecStatus::Completed,
1270 "Driver should be completed after all members complete"
1271 );
1272 assert_eq!(
1273 final_driver.frontmatter.model,
1274 Some("auto-completed".to_string()),
1275 "Driver should have auto-completed model"
1276 );
1277 assert!(
1278 final_driver.frontmatter.completed_at.is_some(),
1279 "Driver should have completed_at timestamp"
1280 );
1281 }
1282}