1use crate::git::DiffEntry;
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, HashSet};
11use std::path::PathBuf;
12
13#[derive(Debug, Clone, Serialize, Deserialize, Default)]
19#[serde(rename_all = "camelCase")]
20pub struct ProjectKnowledge {
21 pub files: HashMap<PathBuf, FileInfo>,
23
24 pub decisions: Vec<Decision>,
26
27 pub patterns: Vec<Pattern>,
29
30 pub story_changes: Vec<StoryChanges>,
32
33 pub baseline_commit: Option<String>,
35}
36
37impl ProjectKnowledge {
38 pub fn our_files(&self) -> HashSet<&PathBuf> {
43 let mut files = HashSet::new();
44
45 for story in &self.story_changes {
46 for change in &story.files_created {
48 files.insert(&change.path);
49 }
50
51 for change in &story.files_modified {
53 files.insert(&change.path);
54 }
55
56 for path in &story.files_deleted {
58 files.insert(path);
59 }
60 }
61
62 files
63 }
64
65 pub fn filter_our_changes(&self, all_changes: &[DiffEntry]) -> Vec<DiffEntry> {
78 let our_files = self.our_files();
79
80 all_changes
81 .iter()
82 .filter(|entry| {
83 if entry.status == crate::git::DiffStatus::Added {
85 return true;
86 }
87
88 our_files.contains(&entry.path)
90 })
91 .cloned()
92 .collect()
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct FileInfo {
100 pub purpose: String,
102
103 pub key_symbols: Vec<String>,
105
106 pub touched_by: Vec<String>,
108
109 pub line_count: u32,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct Decision {
117 pub story_id: String,
119
120 pub topic: String,
122
123 pub choice: String,
125
126 pub rationale: String,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct Pattern {
134 pub story_id: String,
136
137 pub description: String,
139
140 pub example_file: Option<PathBuf>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct StoryChanges {
148 pub story_id: String,
150
151 pub files_created: Vec<FileChange>,
153
154 pub files_modified: Vec<FileChange>,
156
157 pub files_deleted: Vec<PathBuf>,
159
160 pub commit_hash: Option<String>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct FileChange {
168 pub path: PathBuf,
170
171 pub additions: u32,
173
174 pub deletions: u32,
176
177 pub purpose: Option<String>,
179
180 pub key_symbols: Vec<String>,
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
193 fn test_project_knowledge_default() {
194 let knowledge = ProjectKnowledge::default();
195 assert!(knowledge.files.is_empty());
196 assert!(knowledge.decisions.is_empty());
197 assert!(knowledge.patterns.is_empty());
198 assert!(knowledge.story_changes.is_empty());
199 assert!(knowledge.baseline_commit.is_none());
200 }
201
202 #[test]
203 fn test_project_knowledge_debug_impl() {
204 let knowledge = ProjectKnowledge::default();
205 let debug_str = format!("{:?}", knowledge);
206 assert!(debug_str.contains("ProjectKnowledge"));
207 }
208
209 #[test]
210 fn test_project_knowledge_clone() {
211 let mut knowledge = ProjectKnowledge::default();
212 knowledge.baseline_commit = Some("abc123".to_string());
213 let cloned = knowledge.clone();
214 assert_eq!(cloned.baseline_commit, Some("abc123".to_string()));
215 }
216
217 #[test]
218 fn test_project_knowledge_serialization_roundtrip() {
219 let mut knowledge = ProjectKnowledge::default();
220 knowledge.baseline_commit = Some("abc123".to_string());
221 knowledge.decisions.push(Decision {
222 story_id: "US-001".to_string(),
223 topic: "Architecture".to_string(),
224 choice: "Use modules".to_string(),
225 rationale: "Better organization".to_string(),
226 });
227
228 let json = serde_json::to_string(&knowledge).unwrap();
229 let deserialized: ProjectKnowledge = serde_json::from_str(&json).unwrap();
230
231 assert_eq!(deserialized.baseline_commit, Some("abc123".to_string()));
232 assert_eq!(deserialized.decisions.len(), 1);
233 assert_eq!(deserialized.decisions[0].story_id, "US-001");
234 }
235
236 #[test]
237 fn test_project_knowledge_camel_case_serialization() {
238 let knowledge = ProjectKnowledge {
239 baseline_commit: Some("abc".to_string()),
240 story_changes: vec![StoryChanges {
241 story_id: "US-001".to_string(),
242 files_created: vec![],
243 files_modified: vec![],
244 files_deleted: vec![],
245 commit_hash: None,
246 }],
247 ..Default::default()
248 };
249
250 let json = serde_json::to_string(&knowledge).unwrap();
251 assert!(json.contains("baselineCommit"));
252 assert!(json.contains("storyChanges"));
253 assert!(json.contains("storyId"));
254 assert!(json.contains("filesCreated"));
255 assert!(json.contains("filesModified"));
256 assert!(json.contains("filesDeleted"));
257 assert!(json.contains("commitHash"));
258 }
259
260 #[test]
265 fn test_file_info_creation() {
266 let file_info = FileInfo {
267 purpose: "Main entry point".to_string(),
268 key_symbols: vec!["main".to_string(), "run".to_string()],
269 touched_by: vec!["US-001".to_string()],
270 line_count: 150,
271 };
272
273 assert_eq!(file_info.purpose, "Main entry point");
274 assert_eq!(file_info.key_symbols.len(), 2);
275 assert_eq!(file_info.touched_by.len(), 1);
276 assert_eq!(file_info.line_count, 150);
277 }
278
279 #[test]
280 fn test_file_info_serialization() {
281 let file_info = FileInfo {
282 purpose: "Test file".to_string(),
283 key_symbols: vec!["test_fn".to_string()],
284 touched_by: vec!["US-001".to_string()],
285 line_count: 50,
286 };
287
288 let json = serde_json::to_string(&file_info).unwrap();
289 assert!(json.contains("keySymbols"));
290 assert!(json.contains("touchedBy"));
291 assert!(json.contains("lineCount"));
292
293 let deserialized: FileInfo = serde_json::from_str(&json).unwrap();
294 assert_eq!(deserialized.purpose, "Test file");
295 assert_eq!(deserialized.line_count, 50);
296 }
297
298 #[test]
303 fn test_decision_creation() {
304 let decision = Decision {
305 story_id: "US-001".to_string(),
306 topic: "Database".to_string(),
307 choice: "SQLite".to_string(),
308 rationale: "Simple, embedded, no setup".to_string(),
309 };
310
311 assert_eq!(decision.story_id, "US-001");
312 assert_eq!(decision.topic, "Database");
313 assert_eq!(decision.choice, "SQLite");
314 assert_eq!(decision.rationale, "Simple, embedded, no setup");
315 }
316
317 #[test]
318 fn test_decision_serialization() {
319 let decision = Decision {
320 story_id: "US-002".to_string(),
321 topic: "Auth".to_string(),
322 choice: "JWT".to_string(),
323 rationale: "Stateless".to_string(),
324 };
325
326 let json = serde_json::to_string(&decision).unwrap();
327 assert!(json.contains("storyId"));
328
329 let deserialized: Decision = serde_json::from_str(&json).unwrap();
330 assert_eq!(deserialized.story_id, "US-002");
331 }
332
333 #[test]
338 fn test_pattern_creation_with_example() {
339 let pattern = Pattern {
340 story_id: "US-001".to_string(),
341 description: "Use Result<T, Error> for all fallible operations".to_string(),
342 example_file: Some(PathBuf::from("src/runner.rs")),
343 };
344
345 assert_eq!(pattern.story_id, "US-001");
346 assert!(pattern.example_file.is_some());
347 }
348
349 #[test]
350 fn test_pattern_creation_without_example() {
351 let pattern = Pattern {
352 story_id: "US-001".to_string(),
353 description: "Use snake_case for function names".to_string(),
354 example_file: None,
355 };
356
357 assert!(pattern.example_file.is_none());
358 }
359
360 #[test]
361 fn test_pattern_serialization() {
362 let pattern = Pattern {
363 story_id: "US-001".to_string(),
364 description: "Test pattern".to_string(),
365 example_file: Some(PathBuf::from("src/lib.rs")),
366 };
367
368 let json = serde_json::to_string(&pattern).unwrap();
369 assert!(json.contains("storyId"));
370 assert!(json.contains("exampleFile"));
371
372 let deserialized: Pattern = serde_json::from_str(&json).unwrap();
373 assert_eq!(deserialized.example_file, Some(PathBuf::from("src/lib.rs")));
374 }
375
376 #[test]
381 fn test_story_changes_creation() {
382 let changes = StoryChanges {
383 story_id: "US-001".to_string(),
384 files_created: vec![FileChange {
385 path: PathBuf::from("src/new.rs"),
386 additions: 100,
387 deletions: 0,
388 purpose: Some("New module".to_string()),
389 key_symbols: vec!["NewStruct".to_string()],
390 }],
391 files_modified: vec![FileChange {
392 path: PathBuf::from("src/lib.rs"),
393 additions: 5,
394 deletions: 0,
395 purpose: None,
396 key_symbols: vec![],
397 }],
398 files_deleted: vec![PathBuf::from("src/old.rs")],
399 commit_hash: Some("def456".to_string()),
400 };
401
402 assert_eq!(changes.story_id, "US-001");
403 assert_eq!(changes.files_created.len(), 1);
404 assert_eq!(changes.files_modified.len(), 1);
405 assert_eq!(changes.files_deleted.len(), 1);
406 assert!(changes.commit_hash.is_some());
407 }
408
409 #[test]
410 fn test_story_changes_without_commit() {
411 let changes = StoryChanges {
412 story_id: "US-001".to_string(),
413 files_created: vec![],
414 files_modified: vec![],
415 files_deleted: vec![],
416 commit_hash: None,
417 };
418
419 assert!(changes.commit_hash.is_none());
420 }
421
422 #[test]
423 fn test_story_changes_serialization() {
424 let changes = StoryChanges {
425 story_id: "US-001".to_string(),
426 files_created: vec![],
427 files_modified: vec![],
428 files_deleted: vec![],
429 commit_hash: Some("abc".to_string()),
430 };
431
432 let json = serde_json::to_string(&changes).unwrap();
433 assert!(json.contains("storyId"));
434 assert!(json.contains("filesCreated"));
435 assert!(json.contains("filesModified"));
436 assert!(json.contains("filesDeleted"));
437 assert!(json.contains("commitHash"));
438 }
439
440 #[test]
445 fn test_file_change_creation() {
446 let change = FileChange {
447 path: PathBuf::from("src/test.rs"),
448 additions: 50,
449 deletions: 10,
450 purpose: Some("Test utilities".to_string()),
451 key_symbols: vec!["test_helper".to_string(), "setup".to_string()],
452 };
453
454 assert_eq!(change.path, PathBuf::from("src/test.rs"));
455 assert_eq!(change.additions, 50);
456 assert_eq!(change.deletions, 10);
457 assert!(change.purpose.is_some());
458 assert_eq!(change.key_symbols.len(), 2);
459 }
460
461 #[test]
462 fn test_file_change_minimal() {
463 let change = FileChange {
464 path: PathBuf::from("src/lib.rs"),
465 additions: 1,
466 deletions: 0,
467 purpose: None,
468 key_symbols: vec![],
469 };
470
471 assert!(change.purpose.is_none());
472 assert!(change.key_symbols.is_empty());
473 }
474
475 #[test]
476 fn test_file_change_serialization() {
477 let change = FileChange {
478 path: PathBuf::from("src/test.rs"),
479 additions: 10,
480 deletions: 5,
481 purpose: Some("Test".to_string()),
482 key_symbols: vec!["sym".to_string()],
483 };
484
485 let json = serde_json::to_string(&change).unwrap();
486 assert!(json.contains("keySymbols"));
487
488 let deserialized: FileChange = serde_json::from_str(&json).unwrap();
489 assert_eq!(deserialized.additions, 10);
490 assert_eq!(deserialized.deletions, 5);
491 }
492
493 #[test]
498 fn test_project_knowledge_with_files() {
499 let mut knowledge = ProjectKnowledge::default();
500
501 knowledge.files.insert(
502 PathBuf::from("src/main.rs"),
503 FileInfo {
504 purpose: "Application entry point".to_string(),
505 key_symbols: vec!["main".to_string()],
506 touched_by: vec!["US-001".to_string()],
507 line_count: 100,
508 },
509 );
510
511 assert_eq!(knowledge.files.len(), 1);
512 let file_info = knowledge.files.get(&PathBuf::from("src/main.rs")).unwrap();
513 assert_eq!(file_info.purpose, "Application entry point");
514 }
515
516 #[test]
517 fn test_full_knowledge_serialization_roundtrip() {
518 let mut knowledge = ProjectKnowledge::default();
519 knowledge.baseline_commit = Some("baseline123".to_string());
520
521 knowledge.files.insert(
522 PathBuf::from("src/lib.rs"),
523 FileInfo {
524 purpose: "Library root".to_string(),
525 key_symbols: vec!["mod".to_string()],
526 touched_by: vec!["US-001".to_string(), "US-002".to_string()],
527 line_count: 50,
528 },
529 );
530
531 knowledge.decisions.push(Decision {
532 story_id: "US-001".to_string(),
533 topic: "Error handling".to_string(),
534 choice: "thiserror crate".to_string(),
535 rationale: "Clean error types".to_string(),
536 });
537
538 knowledge.patterns.push(Pattern {
539 story_id: "US-001".to_string(),
540 description: "Use ? operator for error propagation".to_string(),
541 example_file: Some(PathBuf::from("src/runner.rs")),
542 });
543
544 knowledge.story_changes.push(StoryChanges {
545 story_id: "US-001".to_string(),
546 files_created: vec![FileChange {
547 path: PathBuf::from("src/knowledge.rs"),
548 additions: 200,
549 deletions: 0,
550 purpose: Some("Knowledge tracking".to_string()),
551 key_symbols: vec!["ProjectKnowledge".to_string()],
552 }],
553 files_modified: vec![FileChange {
554 path: PathBuf::from("src/lib.rs"),
555 additions: 1,
556 deletions: 0,
557 purpose: None,
558 key_symbols: vec![],
559 }],
560 files_deleted: vec![],
561 commit_hash: Some("commit123".to_string()),
562 });
563
564 let json = serde_json::to_string_pretty(&knowledge).unwrap();
566
567 let deserialized: ProjectKnowledge = serde_json::from_str(&json).unwrap();
569
570 assert_eq!(
572 deserialized.baseline_commit,
573 Some("baseline123".to_string())
574 );
575 assert_eq!(deserialized.files.len(), 1);
576 assert_eq!(deserialized.decisions.len(), 1);
577 assert_eq!(deserialized.patterns.len(), 1);
578 assert_eq!(deserialized.story_changes.len(), 1);
579
580 let file_info = deserialized
582 .files
583 .get(&PathBuf::from("src/lib.rs"))
584 .unwrap();
585 assert_eq!(file_info.touched_by.len(), 2);
586
587 let story_changes = &deserialized.story_changes[0];
588 assert_eq!(story_changes.files_created.len(), 1);
589 assert_eq!(story_changes.files_created[0].additions, 200);
590 }
591
592 #[test]
597 fn test_our_files_empty_knowledge() {
598 let knowledge = ProjectKnowledge::default();
599 let files = knowledge.our_files();
600 assert!(files.is_empty());
601 }
602
603 #[test]
604 fn test_our_files_with_created_files() {
605 let mut knowledge = ProjectKnowledge::default();
606 knowledge.story_changes.push(StoryChanges {
607 story_id: "US-001".to_string(),
608 files_created: vec![FileChange {
609 path: PathBuf::from("src/new.rs"),
610 additions: 100,
611 deletions: 0,
612 purpose: None,
613 key_symbols: vec![],
614 }],
615 files_modified: vec![],
616 files_deleted: vec![],
617 commit_hash: None,
618 });
619
620 let files = knowledge.our_files();
621 assert_eq!(files.len(), 1);
622 assert!(files.contains(&PathBuf::from("src/new.rs")));
623 }
624
625 #[test]
626 fn test_our_files_with_modified_files() {
627 let mut knowledge = ProjectKnowledge::default();
628 knowledge.story_changes.push(StoryChanges {
629 story_id: "US-001".to_string(),
630 files_created: vec![],
631 files_modified: vec![FileChange {
632 path: PathBuf::from("src/lib.rs"),
633 additions: 10,
634 deletions: 5,
635 purpose: None,
636 key_symbols: vec![],
637 }],
638 files_deleted: vec![],
639 commit_hash: None,
640 });
641
642 let files = knowledge.our_files();
643 assert_eq!(files.len(), 1);
644 assert!(files.contains(&PathBuf::from("src/lib.rs")));
645 }
646
647 #[test]
648 fn test_our_files_with_deleted_files() {
649 let mut knowledge = ProjectKnowledge::default();
650 knowledge.story_changes.push(StoryChanges {
651 story_id: "US-001".to_string(),
652 files_created: vec![],
653 files_modified: vec![],
654 files_deleted: vec![PathBuf::from("src/old.rs")],
655 commit_hash: None,
656 });
657
658 let files = knowledge.our_files();
659 assert_eq!(files.len(), 1);
660 assert!(files.contains(&PathBuf::from("src/old.rs")));
661 }
662
663 #[test]
664 fn test_our_files_multiple_stories() {
665 let mut knowledge = ProjectKnowledge::default();
666
667 knowledge.story_changes.push(StoryChanges {
669 story_id: "US-001".to_string(),
670 files_created: vec![FileChange {
671 path: PathBuf::from("src/a.rs"),
672 additions: 50,
673 deletions: 0,
674 purpose: None,
675 key_symbols: vec![],
676 }],
677 files_modified: vec![FileChange {
678 path: PathBuf::from("src/lib.rs"),
679 additions: 1,
680 deletions: 0,
681 purpose: None,
682 key_symbols: vec![],
683 }],
684 files_deleted: vec![],
685 commit_hash: None,
686 });
687
688 knowledge.story_changes.push(StoryChanges {
690 story_id: "US-002".to_string(),
691 files_created: vec![FileChange {
692 path: PathBuf::from("src/b.rs"),
693 additions: 30,
694 deletions: 0,
695 purpose: None,
696 key_symbols: vec![],
697 }],
698 files_modified: vec![FileChange {
699 path: PathBuf::from("src/lib.rs"),
700 additions: 2,
701 deletions: 0,
702 purpose: None,
703 key_symbols: vec![],
704 }],
705 files_deleted: vec![PathBuf::from("src/old.rs")],
706 commit_hash: None,
707 });
708
709 let files = knowledge.our_files();
710 assert_eq!(files.len(), 4);
713 assert!(files.contains(&PathBuf::from("src/a.rs")));
714 assert!(files.contains(&PathBuf::from("src/b.rs")));
715 assert!(files.contains(&PathBuf::from("src/lib.rs")));
716 assert!(files.contains(&PathBuf::from("src/old.rs")));
717 }
718
719 #[test]
720 fn test_our_files_deduplicates() {
721 let mut knowledge = ProjectKnowledge::default();
722
723 knowledge.story_changes.push(StoryChanges {
725 story_id: "US-001".to_string(),
726 files_created: vec![],
727 files_modified: vec![FileChange {
728 path: PathBuf::from("src/lib.rs"),
729 additions: 10,
730 deletions: 0,
731 purpose: None,
732 key_symbols: vec![],
733 }],
734 files_deleted: vec![],
735 commit_hash: None,
736 });
737
738 knowledge.story_changes.push(StoryChanges {
739 story_id: "US-002".to_string(),
740 files_created: vec![],
741 files_modified: vec![FileChange {
742 path: PathBuf::from("src/lib.rs"),
743 additions: 5,
744 deletions: 2,
745 purpose: None,
746 key_symbols: vec![],
747 }],
748 files_deleted: vec![],
749 commit_hash: None,
750 });
751
752 let files = knowledge.our_files();
753 assert_eq!(files.len(), 1);
755 assert!(files.contains(&PathBuf::from("src/lib.rs")));
756 }
757
758 #[test]
759 fn test_filter_our_changes_empty_knowledge() {
760 use crate::git::{DiffEntry, DiffStatus};
761
762 let knowledge = ProjectKnowledge::default();
763 let all_changes = vec![
764 DiffEntry {
765 path: PathBuf::from("src/external.rs"),
766 additions: 10,
767 deletions: 0,
768 status: DiffStatus::Modified,
769 },
770 DiffEntry {
771 path: PathBuf::from("src/new.rs"),
772 additions: 50,
773 deletions: 0,
774 status: DiffStatus::Added,
775 },
776 ];
777
778 let filtered = knowledge.filter_our_changes(&all_changes);
779
780 assert_eq!(filtered.len(), 1);
782 assert_eq!(filtered[0].path, PathBuf::from("src/new.rs"));
783 }
784
785 #[test]
786 fn test_filter_our_changes_includes_our_modified_files() {
787 use crate::git::{DiffEntry, DiffStatus};
788
789 let mut knowledge = ProjectKnowledge::default();
790 knowledge.story_changes.push(StoryChanges {
791 story_id: "US-001".to_string(),
792 files_created: vec![FileChange {
793 path: PathBuf::from("src/our_file.rs"),
794 additions: 100,
795 deletions: 0,
796 purpose: None,
797 key_symbols: vec![],
798 }],
799 files_modified: vec![],
800 files_deleted: vec![],
801 commit_hash: None,
802 });
803
804 let all_changes = vec![
805 DiffEntry {
806 path: PathBuf::from("src/our_file.rs"),
807 additions: 10,
808 deletions: 5,
809 status: DiffStatus::Modified,
810 },
811 DiffEntry {
812 path: PathBuf::from("src/external.rs"),
813 additions: 20,
814 deletions: 0,
815 status: DiffStatus::Modified,
816 },
817 ];
818
819 let filtered = knowledge.filter_our_changes(&all_changes);
820
821 assert_eq!(filtered.len(), 1);
823 assert_eq!(filtered[0].path, PathBuf::from("src/our_file.rs"));
824 }
825
826 #[test]
827 fn test_filter_our_changes_includes_new_files() {
828 use crate::git::{DiffEntry, DiffStatus};
829
830 let mut knowledge = ProjectKnowledge::default();
831 knowledge.story_changes.push(StoryChanges {
832 story_id: "US-001".to_string(),
833 files_created: vec![FileChange {
834 path: PathBuf::from("src/old_new.rs"),
835 additions: 50,
836 deletions: 0,
837 purpose: None,
838 key_symbols: vec![],
839 }],
840 files_modified: vec![],
841 files_deleted: vec![],
842 commit_hash: None,
843 });
844
845 let all_changes = vec![
846 DiffEntry {
847 path: PathBuf::from("src/brand_new.rs"),
848 additions: 100,
849 deletions: 0,
850 status: DiffStatus::Added,
851 },
852 DiffEntry {
853 path: PathBuf::from("src/external.rs"),
854 additions: 20,
855 deletions: 10,
856 status: DiffStatus::Modified,
857 },
858 ];
859
860 let filtered = knowledge.filter_our_changes(&all_changes);
861
862 assert_eq!(filtered.len(), 1);
865 assert_eq!(filtered[0].path, PathBuf::from("src/brand_new.rs"));
866 }
867
868 #[test]
869 fn test_filter_our_changes_excludes_external_modifications() {
870 use crate::git::{DiffEntry, DiffStatus};
871
872 let mut knowledge = ProjectKnowledge::default();
873 knowledge.story_changes.push(StoryChanges {
874 story_id: "US-001".to_string(),
875 files_created: vec![FileChange {
876 path: PathBuf::from("src/our.rs"),
877 additions: 50,
878 deletions: 0,
879 purpose: None,
880 key_symbols: vec![],
881 }],
882 files_modified: vec![],
883 files_deleted: vec![],
884 commit_hash: None,
885 });
886
887 let all_changes = vec![
888 DiffEntry {
889 path: PathBuf::from("package-lock.json"),
890 additions: 1000,
891 deletions: 500,
892 status: DiffStatus::Modified,
893 },
894 DiffEntry {
895 path: PathBuf::from(".env"),
896 additions: 1,
897 deletions: 0,
898 status: DiffStatus::Modified,
899 },
900 ];
901
902 let filtered = knowledge.filter_our_changes(&all_changes);
903
904 assert!(filtered.is_empty());
906 }
907
908 #[test]
909 fn test_filter_our_changes_complex_scenario() {
910 use crate::git::{DiffEntry, DiffStatus};
911
912 let mut knowledge = ProjectKnowledge::default();
913
914 knowledge.story_changes.push(StoryChanges {
916 story_id: "US-001".to_string(),
917 files_created: vec![FileChange {
918 path: PathBuf::from("src/a.rs"),
919 additions: 50,
920 deletions: 0,
921 purpose: None,
922 key_symbols: vec![],
923 }],
924 files_modified: vec![FileChange {
925 path: PathBuf::from("src/lib.rs"),
926 additions: 1,
927 deletions: 0,
928 purpose: None,
929 key_symbols: vec![],
930 }],
931 files_deleted: vec![],
932 commit_hash: None,
933 });
934
935 let all_changes = vec![
936 DiffEntry {
938 path: PathBuf::from("src/a.rs"),
939 additions: 10,
940 deletions: 5,
941 status: DiffStatus::Modified,
942 },
943 DiffEntry {
945 path: PathBuf::from("src/lib.rs"),
946 additions: 3,
947 deletions: 1,
948 status: DiffStatus::Modified,
949 },
950 DiffEntry {
952 path: PathBuf::from("src/b.rs"),
953 additions: 100,
954 deletions: 0,
955 status: DiffStatus::Added,
956 },
957 DiffEntry {
959 path: PathBuf::from("Cargo.lock"),
960 additions: 500,
961 deletions: 100,
962 status: DiffStatus::Modified,
963 },
964 DiffEntry {
966 path: PathBuf::from(".github/workflows/ci.yml"),
967 additions: 50,
968 deletions: 0,
969 status: DiffStatus::Added,
970 },
971 ];
972
973 let filtered = knowledge.filter_our_changes(&all_changes);
974
975 assert_eq!(filtered.len(), 4);
983
984 let paths: Vec<_> = filtered.iter().map(|e| e.path.clone()).collect();
985 assert!(paths.contains(&PathBuf::from("src/a.rs")));
986 assert!(paths.contains(&PathBuf::from("src/lib.rs")));
987 assert!(paths.contains(&PathBuf::from("src/b.rs")));
988 assert!(paths.contains(&PathBuf::from(".github/workflows/ci.yml")));
989 assert!(!paths.contains(&PathBuf::from("Cargo.lock")));
990 }
991
992 #[test]
993 fn test_filter_our_changes_with_deleted_file() {
994 use crate::git::{DiffEntry, DiffStatus};
995
996 let mut knowledge = ProjectKnowledge::default();
997 knowledge.story_changes.push(StoryChanges {
998 story_id: "US-001".to_string(),
999 files_created: vec![],
1000 files_modified: vec![FileChange {
1001 path: PathBuf::from("src/to_delete.rs"),
1002 additions: 5,
1003 deletions: 0,
1004 purpose: None,
1005 key_symbols: vec![],
1006 }],
1007 files_deleted: vec![],
1008 commit_hash: None,
1009 });
1010
1011 let all_changes = vec![DiffEntry {
1012 path: PathBuf::from("src/to_delete.rs"),
1013 additions: 0,
1014 deletions: 50,
1015 status: DiffStatus::Deleted,
1016 }];
1017
1018 let filtered = knowledge.filter_our_changes(&all_changes);
1019
1020 assert_eq!(filtered.len(), 1);
1022 assert_eq!(filtered[0].path, PathBuf::from("src/to_delete.rs"));
1023 assert_eq!(filtered[0].status, DiffStatus::Deleted);
1024 }
1025
1026 #[test]
1027 fn test_filter_our_changes_empty_input() {
1028 let knowledge = ProjectKnowledge::default();
1029 let filtered = knowledge.filter_our_changes(&[]);
1030 assert!(filtered.is_empty());
1031 }
1032}