1use crate::claude::run_improve_session;
8use crate::error::{Autom8Error, Result};
9use crate::gh::find_spec_for_branch;
10use crate::git::{self, CommitInfo, DiffEntry};
11use crate::knowledge::ProjectKnowledge;
12use crate::output::improve::print_context_summary;
13use crate::spec::Spec;
14use crate::state::StateManager;
15use std::path::PathBuf;
16
17pub fn improve_command(_verbose: bool) -> Result<()> {
46 if !git::is_git_repo() {
48 return Err(Autom8Error::NotInGitRepo);
49 }
50
51 let context = load_follow_up_context()?;
53
54 print_context_summary(&context);
56
57 let prompt = build_improve_prompt(&context);
59
60 run_improve_session(&prompt)?;
62
63 Ok(())
64}
65
66#[derive(Debug, Clone)]
75pub struct GitContext {
76 pub branch_name: String,
78 pub base_branch: String,
80 pub commits: Vec<CommitInfo>,
82 pub diff_entries: Vec<DiffEntry>,
84 pub merge_base_commit: Option<String>,
86}
87
88impl GitContext {
89 pub fn is_feature_branch(&self) -> bool {
91 self.branch_name != "main" && self.branch_name != "master"
92 }
93
94 pub fn commit_count(&self) -> usize {
96 self.commits.len()
97 }
98
99 pub fn files_changed_count(&self) -> usize {
101 self.diff_entries.len()
102 }
103
104 pub fn total_additions(&self) -> u32 {
106 self.diff_entries.iter().map(|e| e.additions).sum()
107 }
108
109 pub fn total_deletions(&self) -> u32 {
111 self.diff_entries.iter().map(|e| e.deletions).sum()
112 }
113}
114
115pub fn gather_git_context() -> Result<GitContext> {
130 let branch_name = git::current_branch()?;
132
133 let base_branch = git::detect_base_branch().unwrap_or_else(|_| "main".to_string());
135
136 let merge_base_commit = git::get_merge_base(&base_branch).ok();
138
139 let commits = git::get_branch_commits(&base_branch).unwrap_or_default();
142
143 let diff_entries = if let Some(ref merge_base) = merge_base_commit {
146 git::get_diff_since(merge_base).unwrap_or_default()
147 } else {
148 git::get_diff_since(&base_branch).unwrap_or_default()
150 };
151
152 Ok(GitContext {
153 branch_name,
154 base_branch,
155 commits,
156 diff_entries,
157 merge_base_commit,
158 })
159}
160
161pub fn build_improve_prompt(context: &FollowUpContext) -> String {
176 let mut sections: Vec<String> = Vec::new();
177
178 let opening = build_opening_statement(context);
180 sections.push(opening);
181
182 if let Some(ref spec) = context.spec {
184 sections.push(build_spec_summary(spec));
185 }
186
187 if let Some(ref knowledge) = context.knowledge {
189 if !knowledge.decisions.is_empty() {
190 sections.push(build_decisions_summary(&knowledge.decisions));
191 }
192 }
193
194 if let Some(ref knowledge) = context.knowledge {
196 if !knowledge.story_changes.is_empty() {
197 if let Some(files_section) = build_files_summary(&knowledge.story_changes) {
198 sections.push(files_section);
199 }
200 }
201 }
202
203 if !context.work_summaries.is_empty() {
205 sections.push(build_work_summaries(&context.work_summaries));
206 }
207
208 sections.push("What would you like to work on?".to_string());
210
211 sections.join("\n\n")
212}
213
214fn build_opening_statement(context: &FollowUpContext) -> String {
216 let branch = &context.git.branch_name;
217 let level = context.richness_level();
218
219 match level {
220 3 => format!(
221 "You're on branch `{}`. I've loaded the spec, session knowledge, and git history.",
222 branch
223 ),
224 2 => {
225 if context.has_spec() {
226 format!(
227 "You're on branch `{}`. I've loaded the spec and git history.",
228 branch
229 )
230 } else {
231 format!(
232 "You're on branch `{}`. I've loaded session knowledge and git history.",
233 branch
234 )
235 }
236 }
237 _ => format!(
238 "You're on branch `{}`. I've loaded the git history.",
239 branch
240 ),
241 }
242}
243
244fn build_spec_summary(spec: &Spec) -> String {
246 let (completed, total) = spec.progress();
247 let status = if spec.all_complete() {
248 "all complete".to_string()
249 } else {
250 format!("{}/{} stories complete", completed, total)
251 };
252
253 format!("**Feature:** {} ({})", spec.project, status)
254}
255
256fn build_decisions_summary(decisions: &[crate::knowledge::Decision]) -> String {
258 let mut lines = vec!["**Key decisions:**".to_string()];
259
260 for decision in decisions.iter().take(5) {
262 lines.push(format!("- {}: {}", decision.topic, decision.choice));
263 }
264
265 if decisions.len() > 5 {
266 lines.push(format!("- ...and {} more", decisions.len() - 5));
267 }
268
269 lines.join("\n")
270}
271
272fn build_files_summary(story_changes: &[crate::knowledge::StoryChanges]) -> Option<String> {
274 use std::collections::HashSet;
275
276 let mut created: HashSet<&std::path::Path> = HashSet::new();
277 let mut modified: HashSet<&std::path::Path> = HashSet::new();
278
279 for changes in story_changes {
280 for file in &changes.files_created {
281 created.insert(&file.path);
282 }
283 for file in &changes.files_modified {
284 if !created.contains(file.path.as_path()) {
286 modified.insert(&file.path);
287 }
288 }
289 }
290
291 if created.is_empty() && modified.is_empty() {
292 return None;
293 }
294
295 let mut lines = vec!["**Files touched:**".to_string()];
296
297 let mut created_vec: Vec<_> = created.iter().collect();
299 created_vec.sort();
300
301 let mut modified_vec: Vec<_> = modified.iter().collect();
302 modified_vec.sort();
303
304 if !created_vec.is_empty() {
306 lines.push("Created:".to_string());
307 for path in created_vec.iter().take(8) {
308 lines.push(format!("- {}", path.display()));
309 }
310 if created_vec.len() > 8 {
311 lines.push(format!("- ...and {} more", created_vec.len() - 8));
312 }
313 }
314
315 if !modified_vec.is_empty() {
317 lines.push("Modified:".to_string());
318 for path in modified_vec.iter().take(8) {
319 lines.push(format!("- {}", path.display()));
320 }
321 if modified_vec.len() > 8 {
322 lines.push(format!("- ...and {} more", modified_vec.len() - 8));
323 }
324 }
325
326 Some(lines.join("\n"))
327}
328
329fn build_work_summaries(summaries: &[String]) -> String {
331 let mut lines = vec!["**Work completed:**".to_string()];
332
333 for summary in summaries.iter().take(5) {
335 let truncated = if summary.len() > 100 {
337 format!("{}...", &summary[..97])
338 } else {
339 summary.clone()
340 };
341 lines.push(format!("- {}", truncated));
342 }
343
344 if summaries.len() > 5 {
345 lines.push(format!("- ...and {} more iterations", summaries.len() - 5));
346 }
347
348 lines.join("\n")
349}
350
351#[derive(Debug, Clone)]
364pub struct FollowUpContext {
365 pub git: GitContext,
367
368 pub spec: Option<Spec>,
370
371 pub spec_path: Option<PathBuf>,
373
374 pub knowledge: Option<ProjectKnowledge>,
376
377 pub work_summaries: Vec<String>,
380
381 pub session_id: Option<String>,
383}
384
385impl FollowUpContext {
386 pub fn has_spec(&self) -> bool {
388 self.spec.is_some()
389 }
390
391 pub fn has_knowledge(&self) -> bool {
393 self.knowledge.is_some()
394 }
395
396 pub fn has_work_summaries(&self) -> bool {
398 !self.work_summaries.is_empty()
399 }
400
401 pub fn work_summary_count(&self) -> usize {
403 self.work_summaries.len()
404 }
405
406 pub fn richness_level(&self) -> u8 {
412 let mut level = 1; if self.has_spec() {
414 level += 1;
415 }
416 if self.has_knowledge() {
417 level += 1;
418 }
419 level
420 }
421}
422
423pub fn load_follow_up_context() -> Result<FollowUpContext> {
436 let git = gather_git_context()?;
438
439 let state_manager = StateManager::new()?;
441 let session_metadata = state_manager
442 .find_session_for_branch(&git.branch_name)
443 .ok()
444 .flatten();
445
446 let mut spec: Option<Spec> = None;
447 let mut spec_path: Option<PathBuf> = None;
448 let mut knowledge: Option<ProjectKnowledge> = None;
449 let mut work_summaries: Vec<String> = Vec::new();
450 let mut session_id: Option<String> = None;
451
452 if let Some(ref metadata) = session_metadata {
453 session_id = Some(metadata.session_id.clone());
454
455 if let Some(ref path) = metadata.spec_json_path {
457 if path.exists() {
458 if let Ok(loaded_spec) = Spec::load(path) {
459 spec = Some(loaded_spec);
460 spec_path = Some(path.clone());
461 }
462 }
463 }
464
465 let session_state_manager = StateManager::with_session(metadata.session_id.clone())?;
467 if let Ok(Some(run_state)) = session_state_manager.load_current() {
468 knowledge = Some(run_state.knowledge.clone());
470
471 work_summaries = run_state
473 .iterations
474 .iter()
475 .filter_map(|iter| iter.work_summary.clone())
476 .collect();
477 }
478 }
479
480 if spec.is_none() {
482 if let Ok(Some((found_spec, found_path))) = find_spec_for_branch(&git.branch_name) {
483 spec = Some(found_spec);
484 spec_path = Some(found_path);
485 }
486 }
487
488 Ok(FollowUpContext {
489 git,
490 spec,
491 spec_path,
492 knowledge,
493 work_summaries,
494 session_id,
495 })
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use crate::git::{DiffEntry, DiffStatus};
502 use crate::knowledge::{Decision, Pattern};
503 use crate::spec::UserStory;
504 use std::path::PathBuf;
505
506 #[test]
511 fn test_git_context_is_feature_branch_true() {
512 let context = GitContext {
513 branch_name: "feature/improve-command".to_string(),
514 base_branch: "main".to_string(),
515 commits: vec![],
516 diff_entries: vec![],
517 merge_base_commit: None,
518 };
519
520 assert!(context.is_feature_branch());
521 }
522
523 #[test]
524 fn test_git_context_is_feature_branch_false_main() {
525 let context = GitContext {
526 branch_name: "main".to_string(),
527 base_branch: "main".to_string(),
528 commits: vec![],
529 diff_entries: vec![],
530 merge_base_commit: None,
531 };
532
533 assert!(!context.is_feature_branch());
534 }
535
536 #[test]
537 fn test_git_context_is_feature_branch_false_master() {
538 let context = GitContext {
539 branch_name: "master".to_string(),
540 base_branch: "master".to_string(),
541 commits: vec![],
542 diff_entries: vec![],
543 merge_base_commit: None,
544 };
545
546 assert!(!context.is_feature_branch());
547 }
548
549 #[test]
550 fn test_git_context_commit_count() {
551 let context = GitContext {
552 branch_name: "feature/test".to_string(),
553 base_branch: "main".to_string(),
554 commits: vec![
555 crate::git::CommitInfo {
556 short_hash: "abc1234".to_string(),
557 full_hash: "abc1234567890".to_string(),
558 message: "First commit".to_string(),
559 author: "Test".to_string(),
560 date: "2024-01-15".to_string(),
561 },
562 crate::git::CommitInfo {
563 short_hash: "def5678".to_string(),
564 full_hash: "def5678901234".to_string(),
565 message: "Second commit".to_string(),
566 author: "Test".to_string(),
567 date: "2024-01-16".to_string(),
568 },
569 ],
570 diff_entries: vec![],
571 merge_base_commit: Some("basehash".to_string()),
572 };
573
574 assert_eq!(context.commit_count(), 2);
575 }
576
577 #[test]
578 fn test_git_context_files_changed_count() {
579 let context = GitContext {
580 branch_name: "feature/test".to_string(),
581 base_branch: "main".to_string(),
582 commits: vec![],
583 diff_entries: vec![
584 DiffEntry {
585 path: PathBuf::from("src/lib.rs"),
586 additions: 10,
587 deletions: 5,
588 status: DiffStatus::Modified,
589 },
590 DiffEntry {
591 path: PathBuf::from("src/main.rs"),
592 additions: 20,
593 deletions: 0,
594 status: DiffStatus::Added,
595 },
596 ],
597 merge_base_commit: None,
598 };
599
600 assert_eq!(context.files_changed_count(), 2);
601 }
602
603 #[test]
604 fn test_git_context_total_additions() {
605 let context = GitContext {
606 branch_name: "feature/test".to_string(),
607 base_branch: "main".to_string(),
608 commits: vec![],
609 diff_entries: vec![
610 DiffEntry {
611 path: PathBuf::from("src/lib.rs"),
612 additions: 10,
613 deletions: 5,
614 status: DiffStatus::Modified,
615 },
616 DiffEntry {
617 path: PathBuf::from("src/main.rs"),
618 additions: 20,
619 deletions: 3,
620 status: DiffStatus::Modified,
621 },
622 ],
623 merge_base_commit: None,
624 };
625
626 assert_eq!(context.total_additions(), 30);
627 }
628
629 #[test]
630 fn test_git_context_total_deletions() {
631 let context = GitContext {
632 branch_name: "feature/test".to_string(),
633 base_branch: "main".to_string(),
634 commits: vec![],
635 diff_entries: vec![
636 DiffEntry {
637 path: PathBuf::from("src/lib.rs"),
638 additions: 10,
639 deletions: 5,
640 status: DiffStatus::Modified,
641 },
642 DiffEntry {
643 path: PathBuf::from("src/main.rs"),
644 additions: 20,
645 deletions: 3,
646 status: DiffStatus::Modified,
647 },
648 ],
649 merge_base_commit: None,
650 };
651
652 assert_eq!(context.total_deletions(), 8);
653 }
654
655 #[test]
656 fn test_git_context_empty() {
657 let context = GitContext {
658 branch_name: "feature/empty".to_string(),
659 base_branch: "main".to_string(),
660 commits: vec![],
661 diff_entries: vec![],
662 merge_base_commit: None,
663 };
664
665 assert_eq!(context.commit_count(), 0);
666 assert_eq!(context.files_changed_count(), 0);
667 assert_eq!(context.total_additions(), 0);
668 assert_eq!(context.total_deletions(), 0);
669 }
670
671 #[test]
672 fn test_git_context_clone() {
673 let context = GitContext {
674 branch_name: "feature/test".to_string(),
675 base_branch: "main".to_string(),
676 commits: vec![],
677 diff_entries: vec![],
678 merge_base_commit: Some("abc123".to_string()),
679 };
680
681 let cloned = context.clone();
682 assert_eq!(cloned.branch_name, context.branch_name);
683 assert_eq!(cloned.merge_base_commit, context.merge_base_commit);
684 }
685
686 #[test]
687 fn test_git_context_debug() {
688 let context = GitContext {
689 branch_name: "feature/test".to_string(),
690 base_branch: "main".to_string(),
691 commits: vec![],
692 diff_entries: vec![],
693 merge_base_commit: None,
694 };
695
696 let debug = format!("{:?}", context);
697 assert!(debug.contains("GitContext"));
698 assert!(debug.contains("feature/test"));
699 }
700
701 #[test]
706 fn test_gather_git_context_returns_valid_context() {
707 let result = gather_git_context();
709 assert!(result.is_ok());
710
711 let context = result.unwrap();
712 assert!(!context.branch_name.is_empty());
714 assert!(!context.base_branch.is_empty());
716 }
717
718 #[test]
719 fn test_gather_git_context_has_base_branch() {
720 let result = gather_git_context();
721 assert!(result.is_ok());
722
723 let context = result.unwrap();
724 assert!(
726 context.base_branch == "main" || context.base_branch == "master",
727 "Expected 'main' or 'master', got '{}'",
728 context.base_branch
729 );
730 }
731
732 fn make_git_context() -> GitContext {
737 GitContext {
738 branch_name: "feature/test".to_string(),
739 base_branch: "main".to_string(),
740 commits: vec![],
741 diff_entries: vec![],
742 merge_base_commit: None,
743 }
744 }
745
746 fn make_spec() -> Spec {
747 Spec {
748 project: "TestProject".to_string(),
749 branch_name: "feature/test".to_string(),
750 description: "Test description".to_string(),
751 user_stories: vec![UserStory {
752 id: "US-001".to_string(),
753 title: "Test Story".to_string(),
754 description: "Test".to_string(),
755 acceptance_criteria: vec![],
756 priority: 1,
757 passes: false,
758 notes: String::new(),
759 }],
760 }
761 }
762
763 fn make_knowledge() -> ProjectKnowledge {
764 let mut knowledge = ProjectKnowledge::default();
765 knowledge.decisions.push(Decision {
766 story_id: "US-001".to_string(),
767 topic: "Architecture".to_string(),
768 choice: "Use modules".to_string(),
769 rationale: "Better organization".to_string(),
770 });
771 knowledge.patterns.push(Pattern {
772 story_id: "US-001".to_string(),
773 description: "Use Result for errors".to_string(),
774 example_file: None,
775 });
776 knowledge
777 }
778
779 #[test]
780 fn test_follow_up_context_git_only() {
781 let context = FollowUpContext {
782 git: make_git_context(),
783 spec: None,
784 spec_path: None,
785 knowledge: None,
786 work_summaries: vec![],
787 session_id: None,
788 };
789
790 assert!(!context.has_spec());
791 assert!(!context.has_knowledge());
792 assert!(!context.has_work_summaries());
793 assert_eq!(context.richness_level(), 1);
794 }
795
796 #[test]
797 fn test_follow_up_context_with_spec() {
798 let context = FollowUpContext {
799 git: make_git_context(),
800 spec: Some(make_spec()),
801 spec_path: Some(PathBuf::from("/path/to/spec.json")),
802 knowledge: None,
803 work_summaries: vec![],
804 session_id: None,
805 };
806
807 assert!(context.has_spec());
808 assert!(!context.has_knowledge());
809 assert_eq!(context.richness_level(), 2);
810 }
811
812 #[test]
813 fn test_follow_up_context_full() {
814 let context = FollowUpContext {
815 git: make_git_context(),
816 spec: Some(make_spec()),
817 spec_path: Some(PathBuf::from("/path/to/spec.json")),
818 knowledge: Some(make_knowledge()),
819 work_summaries: vec![
820 "Implemented feature A".to_string(),
821 "Fixed bug in module B".to_string(),
822 ],
823 session_id: Some("session-123".to_string()),
824 };
825
826 assert!(context.has_spec());
827 assert!(context.has_knowledge());
828 assert!(context.has_work_summaries());
829 assert_eq!(context.work_summary_count(), 2);
830 assert_eq!(context.richness_level(), 3);
831 }
832
833 #[test]
834 fn test_follow_up_context_richness_level_spec_only() {
835 let context = FollowUpContext {
837 git: make_git_context(),
838 spec: Some(make_spec()),
839 spec_path: None,
840 knowledge: None,
841 work_summaries: vec![],
842 session_id: None,
843 };
844
845 assert_eq!(context.richness_level(), 2);
846 }
847
848 #[test]
849 fn test_follow_up_context_richness_level_knowledge_only() {
850 let context = FollowUpContext {
852 git: make_git_context(),
853 spec: None,
854 spec_path: None,
855 knowledge: Some(make_knowledge()),
856 work_summaries: vec![],
857 session_id: None,
858 };
859
860 assert_eq!(context.richness_level(), 2);
861 }
862
863 #[test]
864 fn test_follow_up_context_work_summaries_empty() {
865 let context = FollowUpContext {
866 git: make_git_context(),
867 spec: None,
868 spec_path: None,
869 knowledge: None,
870 work_summaries: vec![],
871 session_id: None,
872 };
873
874 assert!(!context.has_work_summaries());
875 assert_eq!(context.work_summary_count(), 0);
876 }
877
878 #[test]
879 fn test_follow_up_context_work_summaries_with_entries() {
880 let context = FollowUpContext {
881 git: make_git_context(),
882 spec: None,
883 spec_path: None,
884 knowledge: None,
885 work_summaries: vec![
886 "Summary 1".to_string(),
887 "Summary 2".to_string(),
888 "Summary 3".to_string(),
889 ],
890 session_id: None,
891 };
892
893 assert!(context.has_work_summaries());
894 assert_eq!(context.work_summary_count(), 3);
895 }
896
897 #[test]
898 fn test_follow_up_context_clone() {
899 let context = FollowUpContext {
900 git: make_git_context(),
901 spec: Some(make_spec()),
902 spec_path: Some(PathBuf::from("/path/to/spec.json")),
903 knowledge: Some(make_knowledge()),
904 work_summaries: vec!["Summary".to_string()],
905 session_id: Some("session-id".to_string()),
906 };
907
908 let cloned = context.clone();
909 assert_eq!(cloned.git.branch_name, context.git.branch_name);
910 assert_eq!(cloned.spec_path, context.spec_path);
911 assert_eq!(cloned.work_summaries.len(), context.work_summaries.len());
912 assert_eq!(cloned.session_id, context.session_id);
913 }
914
915 #[test]
916 fn test_follow_up_context_debug() {
917 let context = FollowUpContext {
918 git: make_git_context(),
919 spec: None,
920 spec_path: None,
921 knowledge: None,
922 work_summaries: vec![],
923 session_id: None,
924 };
925
926 let debug = format!("{:?}", context);
927 assert!(debug.contains("FollowUpContext"));
928 assert!(debug.contains("git"));
929 }
930
931 use crate::knowledge::{FileChange, StoryChanges};
936
937 fn make_knowledge_with_files() -> ProjectKnowledge {
938 let mut knowledge = ProjectKnowledge::default();
939 knowledge.decisions.push(Decision {
940 story_id: "US-001".to_string(),
941 topic: "Architecture".to_string(),
942 choice: "Use modules".to_string(),
943 rationale: "Better organization".to_string(),
944 });
945 knowledge.decisions.push(Decision {
946 story_id: "US-002".to_string(),
947 topic: "Error handling".to_string(),
948 choice: "Use thiserror".to_string(),
949 rationale: "Clean error types".to_string(),
950 });
951 knowledge.story_changes.push(StoryChanges {
952 story_id: "US-001".to_string(),
953 files_created: vec![FileChange {
954 path: PathBuf::from("src/new_module.rs"),
955 additions: 100,
956 deletions: 0,
957 purpose: Some("New module".to_string()),
958 key_symbols: vec![],
959 }],
960 files_modified: vec![FileChange {
961 path: PathBuf::from("src/lib.rs"),
962 additions: 5,
963 deletions: 0,
964 purpose: None,
965 key_symbols: vec![],
966 }],
967 files_deleted: vec![],
968 commit_hash: None,
969 });
970 knowledge
971 }
972
973 fn make_complete_spec() -> Spec {
974 Spec {
975 project: "TestProject".to_string(),
976 branch_name: "feature/test".to_string(),
977 description: "Test description".to_string(),
978 user_stories: vec![
979 UserStory {
980 id: "US-001".to_string(),
981 title: "Test Story".to_string(),
982 description: "Test".to_string(),
983 acceptance_criteria: vec![],
984 priority: 1,
985 passes: true,
986 notes: String::new(),
987 },
988 UserStory {
989 id: "US-002".to_string(),
990 title: "Test Story 2".to_string(),
991 description: "Test 2".to_string(),
992 acceptance_criteria: vec![],
993 priority: 2,
994 passes: true,
995 notes: String::new(),
996 },
997 ],
998 }
999 }
1000
1001 #[test]
1002 fn test_build_improve_prompt_git_only() {
1003 let context = FollowUpContext {
1004 git: make_git_context(),
1005 spec: None,
1006 spec_path: None,
1007 knowledge: None,
1008 work_summaries: vec![],
1009 session_id: None,
1010 };
1011
1012 let prompt = build_improve_prompt(&context);
1013
1014 assert!(prompt.contains("feature/test"));
1016 assert!(prompt.contains("git history"));
1018 assert!(prompt.contains("What would you like to work on?"));
1020 assert!(!prompt.contains("**Feature:**"));
1022 assert!(!prompt.contains("**Key decisions:**"));
1023 }
1024
1025 #[test]
1026 fn test_build_improve_prompt_with_spec() {
1027 let context = FollowUpContext {
1028 git: make_git_context(),
1029 spec: Some(make_spec()),
1030 spec_path: None,
1031 knowledge: None,
1032 work_summaries: vec![],
1033 session_id: None,
1034 };
1035
1036 let prompt = build_improve_prompt(&context);
1037
1038 assert!(prompt.contains("feature/test"));
1040 assert!(prompt.contains("**Feature:**"));
1041 assert!(prompt.contains("TestProject"));
1042 assert!(prompt.contains("0/1 stories complete"));
1044 assert!(prompt.contains("What would you like to work on?"));
1046 }
1047
1048 #[test]
1049 fn test_build_improve_prompt_with_complete_spec() {
1050 let context = FollowUpContext {
1051 git: make_git_context(),
1052 spec: Some(make_complete_spec()),
1053 spec_path: None,
1054 knowledge: None,
1055 work_summaries: vec![],
1056 session_id: None,
1057 };
1058
1059 let prompt = build_improve_prompt(&context);
1060
1061 assert!(prompt.contains("all complete"));
1063 }
1064
1065 #[test]
1066 fn test_build_improve_prompt_with_decisions() {
1067 let context = FollowUpContext {
1068 git: make_git_context(),
1069 spec: None,
1070 spec_path: None,
1071 knowledge: Some(make_knowledge_with_files()),
1072 work_summaries: vec![],
1073 session_id: None,
1074 };
1075
1076 let prompt = build_improve_prompt(&context);
1077
1078 assert!(prompt.contains("**Key decisions:**"));
1080 assert!(prompt.contains("Architecture: Use modules"));
1082 assert!(prompt.contains("Error handling: Use thiserror"));
1083 }
1084
1085 #[test]
1086 fn test_build_improve_prompt_with_files() {
1087 let context = FollowUpContext {
1088 git: make_git_context(),
1089 spec: None,
1090 spec_path: None,
1091 knowledge: Some(make_knowledge_with_files()),
1092 work_summaries: vec![],
1093 session_id: None,
1094 };
1095
1096 let prompt = build_improve_prompt(&context);
1097
1098 assert!(prompt.contains("**Files touched:**"));
1100 assert!(prompt.contains("Created:"));
1102 assert!(prompt.contains("src/new_module.rs"));
1103 assert!(prompt.contains("Modified:"));
1104 assert!(prompt.contains("src/lib.rs"));
1105 }
1106
1107 #[test]
1108 fn test_build_improve_prompt_with_work_summaries() {
1109 let context = FollowUpContext {
1110 git: make_git_context(),
1111 spec: None,
1112 spec_path: None,
1113 knowledge: None,
1114 work_summaries: vec![
1115 "Implemented user authentication".to_string(),
1116 "Fixed login validation bug".to_string(),
1117 ],
1118 session_id: None,
1119 };
1120
1121 let prompt = build_improve_prompt(&context);
1122
1123 assert!(prompt.contains("**Work completed:**"));
1125 assert!(prompt.contains("Implemented user authentication"));
1126 assert!(prompt.contains("Fixed login validation bug"));
1127 }
1128
1129 #[test]
1130 fn test_build_improve_prompt_full_context() {
1131 let context = FollowUpContext {
1132 git: make_git_context(),
1133 spec: Some(make_spec()),
1134 spec_path: None,
1135 knowledge: Some(make_knowledge_with_files()),
1136 work_summaries: vec!["Completed initial setup".to_string()],
1137 session_id: Some("session-123".to_string()),
1138 };
1139
1140 let prompt = build_improve_prompt(&context);
1141
1142 assert!(prompt.contains("spec, session knowledge, and git history"));
1144 assert!(prompt.contains("**Feature:**"));
1146 assert!(prompt.contains("**Key decisions:**"));
1147 assert!(prompt.contains("**Files touched:**"));
1148 assert!(prompt.contains("**Work completed:**"));
1149 assert!(prompt.ends_with("What would you like to work on?"));
1151 }
1152
1153 #[test]
1154 fn test_build_improve_prompt_limits_decisions() {
1155 let mut knowledge = ProjectKnowledge::default();
1156 for i in 1..=7 {
1158 knowledge.decisions.push(Decision {
1159 story_id: format!("US-{:03}", i),
1160 topic: format!("Topic {}", i),
1161 choice: format!("Choice {}", i),
1162 rationale: "Rationale".to_string(),
1163 });
1164 }
1165
1166 let context = FollowUpContext {
1167 git: make_git_context(),
1168 spec: None,
1169 spec_path: None,
1170 knowledge: Some(knowledge),
1171 work_summaries: vec![],
1172 session_id: None,
1173 };
1174
1175 let prompt = build_improve_prompt(&context);
1176
1177 assert!(prompt.contains("Topic 1: Choice 1"));
1179 assert!(prompt.contains("Topic 5: Choice 5"));
1180 assert!(prompt.contains("...and 2 more"));
1181 assert!(!prompt.contains("Topic 6: Choice 6"));
1183 }
1184
1185 #[test]
1186 fn test_build_improve_prompt_truncates_long_summaries() {
1187 let long_summary = "A".repeat(150);
1188 let context = FollowUpContext {
1189 git: make_git_context(),
1190 spec: None,
1191 spec_path: None,
1192 knowledge: None,
1193 work_summaries: vec![long_summary],
1194 session_id: None,
1195 };
1196
1197 let prompt = build_improve_prompt(&context);
1198
1199 assert!(prompt.contains("..."));
1201 assert!(!prompt.contains(&"A".repeat(150)));
1203 }
1204
1205 #[test]
1206 fn test_build_improve_prompt_limits_work_summaries() {
1207 let summaries: Vec<String> = (1..=8).map(|i| format!("Summary {}", i)).collect();
1208
1209 let context = FollowUpContext {
1210 git: make_git_context(),
1211 spec: None,
1212 spec_path: None,
1213 knowledge: None,
1214 work_summaries: summaries,
1215 session_id: None,
1216 };
1217
1218 let prompt = build_improve_prompt(&context);
1219
1220 assert!(prompt.contains("Summary 1"));
1222 assert!(prompt.contains("Summary 5"));
1223 assert!(prompt.contains("...and 3 more iterations"));
1224 }
1225
1226 #[test]
1227 fn test_build_improve_prompt_empty_knowledge_no_files_section() {
1228 let mut knowledge = ProjectKnowledge::default();
1229 knowledge.decisions.push(Decision {
1230 story_id: "US-001".to_string(),
1231 topic: "Test".to_string(),
1232 choice: "Test".to_string(),
1233 rationale: "Test".to_string(),
1234 });
1235 let context = FollowUpContext {
1238 git: make_git_context(),
1239 spec: None,
1240 spec_path: None,
1241 knowledge: Some(knowledge),
1242 work_summaries: vec![],
1243 session_id: None,
1244 };
1245
1246 let prompt = build_improve_prompt(&context);
1247
1248 assert!(prompt.contains("**Key decisions:**"));
1250 assert!(!prompt.contains("**Files touched:**"));
1251 }
1252
1253 #[test]
1254 fn test_build_improve_prompt_opening_level_1() {
1255 let context = FollowUpContext {
1256 git: make_git_context(),
1257 spec: None,
1258 spec_path: None,
1259 knowledge: None,
1260 work_summaries: vec![],
1261 session_id: None,
1262 };
1263
1264 let prompt = build_improve_prompt(&context);
1265 assert!(prompt.contains("I've loaded the git history"));
1266 }
1267
1268 #[test]
1269 fn test_build_improve_prompt_opening_level_2_with_spec() {
1270 let context = FollowUpContext {
1271 git: make_git_context(),
1272 spec: Some(make_spec()),
1273 spec_path: None,
1274 knowledge: None,
1275 work_summaries: vec![],
1276 session_id: None,
1277 };
1278
1279 let prompt = build_improve_prompt(&context);
1280 assert!(prompt.contains("I've loaded the spec and git history"));
1281 }
1282
1283 #[test]
1284 fn test_build_improve_prompt_opening_level_2_with_knowledge() {
1285 let context = FollowUpContext {
1286 git: make_git_context(),
1287 spec: None,
1288 spec_path: None,
1289 knowledge: Some(make_knowledge()),
1290 work_summaries: vec![],
1291 session_id: None,
1292 };
1293
1294 let prompt = build_improve_prompt(&context);
1295 assert!(prompt.contains("I've loaded session knowledge and git history"));
1296 }
1297
1298 #[test]
1299 fn test_build_improve_prompt_opening_level_3() {
1300 let context = FollowUpContext {
1301 git: make_git_context(),
1302 spec: Some(make_spec()),
1303 spec_path: None,
1304 knowledge: Some(make_knowledge()),
1305 work_summaries: vec![],
1306 session_id: None,
1307 };
1308
1309 let prompt = build_improve_prompt(&context);
1310 assert!(prompt.contains("I've loaded the spec, session knowledge, and git history"));
1311 }
1312
1313 #[test]
1318 fn test_load_follow_up_context_succeeds() {
1319 let result = load_follow_up_context();
1321 assert!(result.is_ok());
1322
1323 let context = result.unwrap();
1324 assert!(!context.git.branch_name.is_empty());
1326 assert!(context.richness_level() >= 1);
1328 }
1329
1330 #[test]
1331 fn test_load_follow_up_context_git_always_present() {
1332 let result = load_follow_up_context();
1333 assert!(result.is_ok());
1334
1335 let context = result.unwrap();
1336 assert!(!context.git.branch_name.is_empty());
1338 assert!(!context.git.base_branch.is_empty());
1339 let _ = context.git.commit_count();
1341 let _ = context.git.files_changed_count();
1342 }
1343}