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(specs_dir: &Path, member_id: &str) -> Result<()> {
368 if let Some(driver_id) = extract_driver_id(member_id) {
369 let driver_path = specs_dir.join(format!("{}.md", driver_id));
371 if driver_path.exists() {
372 let mut driver = Spec::load(&driver_path)?;
373 if driver.frontmatter.status == SpecStatus::Pending {
374 driver.frontmatter.status = SpecStatus::InProgress;
375 driver.save(&driver_path)?;
376 }
377 }
378 }
379 Ok(())
380}
381
382pub fn auto_complete_driver_if_ready(
408 member_id: &str,
409 all_specs: &[Spec],
410 specs_dir: &Path,
411) -> Result<bool> {
412 let Some(driver_id) = extract_driver_id(member_id) else {
414 return Ok(false);
415 };
416
417 let Some(driver_spec) = all_specs.iter().find(|s| s.id == driver_id) else {
419 return Ok(false);
420 };
421
422 if driver_spec.frontmatter.status != SpecStatus::InProgress {
424 return Ok(false);
425 }
426
427 if !all_members_completed(&driver_id, all_specs) {
429 return Ok(false);
430 }
431
432 let driver_path = specs_dir.join(format!("{}.md", driver_id));
434 let mut driver = Spec::load(&driver_path)?;
435
436 driver.frontmatter.status = SpecStatus::Completed;
437 driver.frontmatter.completed_at = Some(
438 chrono::Local::now()
439 .format("%Y-%m-%dT%H:%M:%SZ")
440 .to_string(),
441 );
442 driver.frontmatter.model = Some("auto-completed".to_string());
443
444 driver.save(&driver_path)?;
445
446 Ok(true)
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 #[test]
454 fn test_is_member_of() {
455 assert!(is_member_of("2026-01-22-001-x7m.1", "2026-01-22-001-x7m"));
456 assert!(is_member_of("2026-01-22-001-x7m.2.1", "2026-01-22-001-x7m"));
457 assert!(!is_member_of("2026-01-22-001-x7m", "2026-01-22-001-x7m"));
458 assert!(!is_member_of("2026-01-22-002-y8n", "2026-01-22-001-x7m"));
459 }
460
461 #[test]
462 fn test_extract_driver_id() {
463 assert_eq!(
464 extract_driver_id("2026-01-22-001-x7m.1"),
465 Some("2026-01-22-001-x7m".to_string())
466 );
467 assert_eq!(
468 extract_driver_id("2026-01-22-001-x7m.2.1"),
469 Some("2026-01-22-001-x7m".to_string())
470 );
471 assert_eq!(extract_driver_id("2026-01-22-001-x7m"), None);
472 assert_eq!(extract_driver_id("2026-01-22-001-x7m.abc"), None);
473 }
474
475 #[test]
476 fn test_extract_member_number() {
477 assert_eq!(extract_member_number("2026-01-24-001-abc.1"), Some(1));
478 assert_eq!(extract_member_number("2026-01-24-001-abc.3"), Some(3));
479 assert_eq!(extract_member_number("2026-01-24-001-abc.10"), Some(10));
480 assert_eq!(extract_member_number("2026-01-24-001-abc.3.2"), Some(3));
481 assert_eq!(extract_member_number("2026-01-24-001-abc"), None);
482 assert_eq!(extract_member_number("2026-01-24-001-abc.abc"), None);
483 }
484
485 #[test]
486 fn test_all_prior_siblings_completed() {
487 let spec1 = Spec::parse(
489 "2026-01-24-001-abc.1",
490 r#"---
491status: pending
492---
493# Test
494"#,
495 )
496 .unwrap();
497
498 assert!(all_prior_siblings_completed(&spec1.id, &[]));
500
501 let spec_prior_1 = Spec::parse(
503 "2026-01-24-001-abc.1",
504 r#"---
505status: completed
506---
507# Test
508"#,
509 )
510 .unwrap();
511
512 let spec_prior_2 = Spec::parse(
513 "2026-01-24-001-abc.2",
514 r#"---
515status: completed
516---
517# Test
518"#,
519 )
520 .unwrap();
521
522 let spec3 = Spec::parse(
523 "2026-01-24-001-abc.3",
524 r#"---
525status: pending
526---
527# Test
528"#,
529 )
530 .unwrap();
531
532 let all_specs = vec![spec_prior_1, spec_prior_2, spec3.clone()];
533 assert!(all_prior_siblings_completed(&spec3.id, &all_specs));
534 }
535
536 #[test]
537 fn test_all_prior_siblings_completed_missing() {
538 let spec_prior_1 = Spec::parse(
540 "2026-01-24-001-abc.1",
541 r#"---
542status: completed
543---
544# Test
545"#,
546 )
547 .unwrap();
548
549 let spec3 = Spec::parse(
550 "2026-01-24-001-abc.3",
551 r#"---
552status: pending
553---
554# Test
555"#,
556 )
557 .unwrap();
558
559 let all_specs = vec![spec_prior_1, spec3.clone()];
561 assert!(!all_prior_siblings_completed(&spec3.id, &all_specs));
562 }
563
564 #[test]
565 fn test_all_prior_siblings_completed_not_completed() {
566 let spec_prior_1 = Spec::parse(
568 "2026-01-24-001-abc.1",
569 r#"---
570status: pending
571---
572# Test
573"#,
574 )
575 .unwrap();
576
577 let spec2 = Spec::parse(
578 "2026-01-24-001-abc.2",
579 r#"---
580status: pending
581---
582# Test
583"#,
584 )
585 .unwrap();
586
587 let all_specs = vec![spec_prior_1, spec2.clone()];
588 assert!(!all_prior_siblings_completed(&spec2.id, &all_specs));
589 }
590
591 #[test]
592 fn test_mark_driver_in_progress_when_member_starts() {
593 use tempfile::TempDir;
594
595 let temp_dir = TempDir::new().unwrap();
596 let specs_dir = temp_dir.path();
597
598 let driver_spec = Spec {
600 id: "2026-01-24-001-abc".to_string(),
601 frontmatter: crate::spec::SpecFrontmatter {
602 status: SpecStatus::Pending,
603 ..Default::default()
604 },
605 title: Some("Driver spec".to_string()),
606 body: "# Driver spec\n\nBody content.".to_string(),
607 };
608
609 let driver_path = specs_dir.join("2026-01-24-001-abc.md");
610 driver_spec.save(&driver_path).unwrap();
611
612 mark_driver_in_progress(specs_dir, "2026-01-24-001-abc.1").unwrap();
614
615 let updated_driver = Spec::load(&driver_path).unwrap();
617 assert_eq!(updated_driver.frontmatter.status, SpecStatus::InProgress);
618 }
619
620 #[test]
621 fn test_mark_driver_in_progress_skips_if_already_in_progress() {
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-002-def".to_string(),
630 frontmatter: crate::spec::SpecFrontmatter {
631 status: SpecStatus::InProgress,
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-002-def.md");
639 driver_spec.save(&driver_path).unwrap();
640
641 mark_driver_in_progress(specs_dir, "2026-01-24-002-def.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_nonexistent_driver() {
651 use tempfile::TempDir;
652
653 let temp_dir = TempDir::new().unwrap();
654 let specs_dir = temp_dir.path();
655
656 mark_driver_in_progress(specs_dir, "2026-01-24-003-ghi.1").unwrap();
659 }
660
661 #[test]
662 fn test_get_incomplete_members() {
663 let driver = Spec::parse(
665 "2026-01-24-005-mno",
666 r#"---
667status: in_progress
668---
669# Driver
670"#,
671 )
672 .unwrap();
673
674 let member1 = Spec::parse(
675 "2026-01-24-005-mno.1",
676 r#"---
677status: completed
678---
679# Member 1
680"#,
681 )
682 .unwrap();
683
684 let member2 = Spec::parse(
685 "2026-01-24-005-mno.2",
686 r#"---
687status: pending
688---
689# Member 2
690"#,
691 )
692 .unwrap();
693
694 let member3 = Spec::parse(
695 "2026-01-24-005-mno.3",
696 r#"---
697status: in_progress
698---
699# Member 3
700"#,
701 )
702 .unwrap();
703
704 let all_specs = vec![driver.clone(), member1, member2, member3];
705 let incomplete = get_incomplete_members(&driver.id, &all_specs);
706 assert_eq!(incomplete.len(), 2);
707 assert!(incomplete.contains(&"2026-01-24-005-mno.2".to_string()));
708 assert!(incomplete.contains(&"2026-01-24-005-mno.3".to_string()));
709 }
710
711 #[test]
712 fn test_auto_complete_driver_not_member_spec() {
713 use tempfile::TempDir;
714
715 let temp_dir = TempDir::new().unwrap();
716 let specs_dir = temp_dir.path();
717
718 let driver_spec = Spec::parse(
720 "2026-01-24-006-pqr",
721 r#"---
722status: in_progress
723---
724# Driver spec
725"#,
726 )
727 .unwrap();
728
729 let result =
730 auto_complete_driver_if_ready("2026-01-24-006-pqr", &[driver_spec], specs_dir).unwrap();
731 assert!(
732 !result,
733 "Non-member spec should not trigger auto-completion"
734 );
735 }
736
737 #[test]
738 fn test_auto_complete_driver_driver_not_in_progress() {
739 use tempfile::TempDir;
740
741 let temp_dir = TempDir::new().unwrap();
742 let specs_dir = temp_dir.path();
743
744 let driver_spec = Spec::parse(
746 "2026-01-24-007-stu",
747 r#"---
748status: pending
749---
750# Driver spec
751"#,
752 )
753 .unwrap();
754
755 let member_spec = Spec::parse(
756 "2026-01-24-007-stu.1",
757 r#"---
758status: completed
759---
760# Member 1
761"#,
762 )
763 .unwrap();
764
765 let all_specs = vec![driver_spec, member_spec];
766 let result =
767 auto_complete_driver_if_ready("2026-01-24-007-stu.1", &all_specs, specs_dir).unwrap();
768 assert!(
769 !result,
770 "Driver not in progress should not be auto-completed"
771 );
772 }
773
774 #[test]
775 fn test_auto_complete_driver_incomplete_members() {
776 use tempfile::TempDir;
777
778 let temp_dir = TempDir::new().unwrap();
779 let specs_dir = temp_dir.path();
780
781 let driver_spec = Spec {
783 id: "2026-01-24-008-vwx".to_string(),
784 frontmatter: crate::spec::SpecFrontmatter {
785 status: SpecStatus::InProgress,
786 ..Default::default()
787 },
788 title: Some("Driver".to_string()),
789 body: "# Driver\n\nBody.".to_string(),
790 };
791
792 let driver_path = specs_dir.join("2026-01-24-008-vwx.md");
793 driver_spec.save(&driver_path).unwrap();
794
795 let member1 = Spec::parse(
797 "2026-01-24-008-vwx.1",
798 r#"---
799status: completed
800---
801# Member 1
802"#,
803 )
804 .unwrap();
805
806 let member2 = Spec::parse(
807 "2026-01-24-008-vwx.2",
808 r#"---
809status: in_progress
810---
811# Member 2
812"#,
813 )
814 .unwrap();
815
816 let all_specs = vec![driver_spec, member1, member2];
817 let result =
818 auto_complete_driver_if_ready("2026-01-24-008-vwx.1", &all_specs, specs_dir).unwrap();
819 assert!(
820 !result,
821 "Driver should not complete when members are incomplete"
822 );
823 }
824
825 #[test]
826 fn test_auto_complete_driver_success() {
827 use tempfile::TempDir;
828
829 let temp_dir = TempDir::new().unwrap();
830 let specs_dir = temp_dir.path();
831
832 let driver_spec = Spec {
834 id: "2026-01-24-009-yz0".to_string(),
835 frontmatter: crate::spec::SpecFrontmatter {
836 status: SpecStatus::InProgress,
837 ..Default::default()
838 },
839 title: Some("Driver".to_string()),
840 body: "# Driver\n\nBody.".to_string(),
841 };
842
843 let driver_path = specs_dir.join("2026-01-24-009-yz0.md");
844 driver_spec.save(&driver_path).unwrap();
845
846 let member1 = Spec::parse(
848 "2026-01-24-009-yz0.1",
849 r#"---
850status: completed
851---
852# Member 1
853"#,
854 )
855 .unwrap();
856
857 let member2 = Spec::parse(
858 "2026-01-24-009-yz0.2",
859 r#"---
860status: completed
861---
862# Member 2
863"#,
864 )
865 .unwrap();
866
867 let all_specs = vec![driver_spec, member1, member2];
868
869 let result =
871 auto_complete_driver_if_ready("2026-01-24-009-yz0.2", &all_specs, specs_dir).unwrap();
872 assert!(
873 result,
874 "Driver should be auto-completed when all members are completed"
875 );
876
877 let updated_driver = Spec::load(&driver_path).unwrap();
879 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
880 assert_eq!(
881 updated_driver.frontmatter.model,
882 Some("auto-completed".to_string())
883 );
884 assert!(updated_driver.frontmatter.completed_at.is_some());
885 }
886
887 #[test]
888 fn test_auto_complete_driver_nonexistent_driver() {
889 use tempfile::TempDir;
890
891 let temp_dir = TempDir::new().unwrap();
892 let specs_dir = temp_dir.path();
893
894 let all_specs = vec![];
896 let result =
897 auto_complete_driver_if_ready("2026-01-24-010-abc.1", &all_specs, specs_dir).unwrap();
898 assert!(
899 !result,
900 "Should return false when driver spec doesn't exist"
901 );
902 }
903
904 #[test]
905 fn test_auto_complete_driver_single_member() {
906 use tempfile::TempDir;
907
908 let temp_dir = TempDir::new().unwrap();
909 let specs_dir = temp_dir.path();
910
911 let driver_spec = Spec {
913 id: "2026-01-24-011-def".to_string(),
914 frontmatter: crate::spec::SpecFrontmatter {
915 status: SpecStatus::InProgress,
916 ..Default::default()
917 },
918 title: Some("Driver".to_string()),
919 body: "# Driver\n\nBody.".to_string(),
920 };
921
922 let driver_path = specs_dir.join("2026-01-24-011-def.md");
923 driver_spec.save(&driver_path).unwrap();
924
925 let member = Spec::parse(
927 "2026-01-24-011-def.1",
928 r#"---
929status: completed
930---
931# Member 1
932"#,
933 )
934 .unwrap();
935
936 let all_specs = vec![driver_spec, member];
937
938 let result =
940 auto_complete_driver_if_ready("2026-01-24-011-def.1", &all_specs, specs_dir).unwrap();
941 assert!(
942 result,
943 "Driver should be auto-completed when single member completes"
944 );
945
946 let updated_driver = Spec::load(&driver_path).unwrap();
948 assert_eq!(updated_driver.frontmatter.status, SpecStatus::Completed);
949 assert_eq!(
950 updated_driver.frontmatter.model,
951 Some("auto-completed".to_string())
952 );
953 }
954
955 #[test]
956 fn test_compare_spec_ids_member_numeric_sort() {
957 use std::cmp::Ordering;
958
959 assert_eq!(
961 compare_spec_ids("2026-01-25-00y-abc.2", "2026-01-25-00y-abc.10"),
962 Ordering::Less
963 );
964 assert_eq!(
965 compare_spec_ids("2026-01-25-00y-abc.10", "2026-01-25-00y-abc.2"),
966 Ordering::Greater
967 );
968 assert_eq!(
969 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc.1"),
970 Ordering::Equal
971 );
972
973 assert_eq!(
975 compare_spec_ids("2026-01-25-00y-abc.99", "2026-01-25-00y-abc.100"),
976 Ordering::Less
977 );
978 }
979
980 #[test]
981 fn test_compare_spec_ids_different_drivers() {
982 use std::cmp::Ordering;
983
984 assert_eq!(
986 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-def.1"),
987 Ordering::Less
988 );
989 assert_eq!(
990 compare_spec_ids("2026-01-25-00y-def.1", "2026-01-25-00y-abc.1"),
991 Ordering::Greater
992 );
993 }
994
995 #[test]
996 fn test_compare_spec_ids_non_member_specs() {
997 use std::cmp::Ordering;
998
999 assert_eq!(
1001 compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-def"),
1002 Ordering::Less
1003 );
1004 assert_eq!(
1005 compare_spec_ids("2026-01-25-00y-def", "2026-01-25-00y-abc"),
1006 Ordering::Greater
1007 );
1008 }
1009
1010 #[test]
1011 fn test_compare_spec_ids_driver_vs_member() {
1012 use std::cmp::Ordering;
1013
1014 assert_eq!(
1016 compare_spec_ids("2026-01-25-00y-abc", "2026-01-25-00y-abc.1"),
1017 Ordering::Less
1018 );
1019 assert_eq!(
1020 compare_spec_ids("2026-01-25-00y-abc.1", "2026-01-25-00y-abc"),
1021 Ordering::Greater
1022 );
1023 }
1024
1025 #[test]
1026 fn test_compare_spec_ids_sorting_list() {
1027 let mut ids = vec![
1029 "2026-01-25-00y-abc.10",
1030 "2026-01-25-00y-abc.2",
1031 "2026-01-25-00y-abc.1",
1032 "2026-01-25-00y-abc",
1033 "2026-01-25-00y-abc.3",
1034 ];
1035
1036 ids.sort_by(|a, b| compare_spec_ids(a, b));
1037
1038 assert_eq!(
1039 ids,
1040 vec![
1041 "2026-01-25-00y-abc",
1042 "2026-01-25-00y-abc.1",
1043 "2026-01-25-00y-abc.2",
1044 "2026-01-25-00y-abc.3",
1045 "2026-01-25-00y-abc.10",
1046 ]
1047 );
1048 }
1049
1050 #[test]
1051 fn test_compare_spec_ids_base36_sequence_rollover() {
1052 use std::cmp::Ordering;
1053
1054 assert_eq!(
1056 compare_spec_ids("2026-01-25-010-xxx", "2026-01-25-00z-yyy"),
1057 Ordering::Greater
1058 );
1059 assert_eq!(
1060 compare_spec_ids("2026-01-25-00z-yyy", "2026-01-25-010-xxx"),
1061 Ordering::Less
1062 );
1063
1064 let mut ids = vec![
1066 "2026-01-25-010-aaa",
1067 "2026-01-25-00a-bbb",
1068 "2026-01-25-00z-ccc",
1069 "2026-01-25-001-ddd",
1070 "2026-01-25-011-eee",
1071 ];
1072
1073 ids.sort_by(|a, b| compare_spec_ids(a, b));
1074
1075 assert_eq!(
1076 ids,
1077 vec![
1078 "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", ]
1084 );
1085 }
1086
1087 #[test]
1088 fn test_driver_auto_completion_with_two_members() {
1089 use tempfile::TempDir;
1090
1091 let temp_dir = TempDir::new().unwrap();
1092 let specs_dir = temp_dir.path();
1093
1094 let driver_spec = Spec {
1096 id: "2026-01-24-012-ghi".to_string(),
1097 frontmatter: crate::spec::SpecFrontmatter {
1098 status: SpecStatus::Pending,
1099 ..Default::default()
1100 },
1101 title: Some("Driver spec with 2 members".to_string()),
1102 body: "# Driver\n\nBody.".to_string(),
1103 };
1104
1105 let driver_path = specs_dir.join("2026-01-24-012-ghi.md");
1106 driver_spec.save(&driver_path).unwrap();
1107
1108 let _member1 = Spec::parse(
1110 "2026-01-24-012-ghi.1",
1111 r#"---
1112status: pending
1113---
1114# Member 1
1115"#,
1116 )
1117 .unwrap();
1118
1119 let member2 = Spec::parse(
1121 "2026-01-24-012-ghi.2",
1122 r#"---
1123status: pending
1124---
1125# Member 2
1126"#,
1127 )
1128 .unwrap();
1129
1130 mark_driver_in_progress(specs_dir, "2026-01-24-012-ghi.1").unwrap();
1132
1133 let updated_driver = Spec::load(&driver_path).unwrap();
1134 assert_eq!(
1135 updated_driver.frontmatter.status,
1136 SpecStatus::InProgress,
1137 "Driver should be in_progress after first member starts"
1138 );
1139
1140 let member1_completed = Spec::parse(
1142 "2026-01-24-012-ghi.1",
1143 r#"---
1144status: completed
1145---
1146# Member 1
1147"#,
1148 )
1149 .unwrap();
1150
1151 let all_specs = vec![
1152 updated_driver.clone(),
1153 member1_completed.clone(),
1154 member2.clone(),
1155 ];
1156 let result =
1157 auto_complete_driver_if_ready("2026-01-24-012-ghi.1", &all_specs, specs_dir).unwrap();
1158 assert!(
1159 !result,
1160 "Driver should NOT auto-complete when first member is done but second is pending"
1161 );
1162
1163 let still_in_progress = Spec::load(&driver_path).unwrap();
1164 assert_eq!(
1165 still_in_progress.frontmatter.status,
1166 SpecStatus::InProgress,
1167 "Driver should still be in_progress"
1168 );
1169
1170 let member2_completed = Spec::parse(
1172 "2026-01-24-012-ghi.2",
1173 r#"---
1174status: completed
1175---
1176# Member 2
1177"#,
1178 )
1179 .unwrap();
1180
1181 let all_specs = vec![
1182 still_in_progress.clone(),
1183 member1_completed.clone(),
1184 member2_completed.clone(),
1185 ];
1186 let result =
1187 auto_complete_driver_if_ready("2026-01-24-012-ghi.2", &all_specs, specs_dir).unwrap();
1188 assert!(
1189 result,
1190 "Driver should auto-complete when all members are completed"
1191 );
1192
1193 let final_driver = Spec::load(&driver_path).unwrap();
1194 assert_eq!(
1195 final_driver.frontmatter.status,
1196 SpecStatus::Completed,
1197 "Driver should be completed after all members complete"
1198 );
1199 assert_eq!(
1200 final_driver.frontmatter.model,
1201 Some("auto-completed".to_string()),
1202 "Driver should have auto-completed model"
1203 );
1204 assert!(
1205 final_driver.frontmatter.completed_at.is_some(),
1206 "Driver should have completed_at timestamp"
1207 );
1208 }
1209}