1mod assembly;
2mod render;
3mod support;
4mod write;
5
6pub use assembly::assemble_report;
7pub use render::{render_compatibility_summary, render_terminal, render_terminal_output};
8pub use write::{load_report, write_report_bundle};
9
10#[cfg(test)]
11mod tests {
12 use std::fs;
13 use std::path::Path;
14
15 use serde_json::{Value, json};
16 use tempfile::tempdir;
17
18 use super::*;
19 use crate::config;
20 use crate::model::{Analysis, HealthRollup, OrganizationAnalysis, RepoMetadata, SkippedCounts};
21
22 fn analysis(root: &Path) -> Analysis {
23 Analysis {
24 repo_root: root.to_path_buf(),
25 repo: RepoMetadata {
26 repo_name: "fixture".to_string(),
27 repo_root: root.display().to_string(),
28 branch: Some("main".to_string()),
29 head_commit: Some("abc123".to_string()),
30 head_commit_timestamp: Some("2026-07-29T08:00:00Z".to_string()),
31 git_remote_url: Some("git@github.com:example/fixture.git".to_string()),
32 is_shallow: false,
33 },
34 config: crate::config::default_config(),
35 generated_at: "2026-07-30T10:11:12Z".to_string(),
36 analyzed_revision_at: Some("2026-07-29T08:00:00Z".to_string()),
37 skipped: SkippedCounts::default(),
38 tracked_file_count: 0,
39 files: vec![],
40 folders: vec![],
41 commits: vec![],
42 organization: OrganizationAnalysis::default(),
43 action_queue: vec![],
44 report: Value::Null,
45 }
46 }
47
48 #[test]
49 fn report_keeps_generation_and_revision_timestamps_distinct() {
50 let root = tempdir().expect("temporary directory");
51 let analysis = analysis(root.path());
52 let report = assemble_report(&analysis, &HealthRollup::default());
53 assert_eq!(report["generated_at"], "2026-07-30T10:11:12Z");
54 assert_eq!(report["analyzed_revision_at"], "2026-07-29T08:00:00Z");
55 assert_eq!(report["repo"]["head_commit"], "abc123");
56 assert_eq!(report["repo"]["head_sha"], "abc123");
57 assert_eq!(
58 report["repo"]["remote_url"],
59 "git@github.com:example/fixture.git"
60 );
61 }
62
63 #[test]
64 fn bundle_writes_compatibility_and_health_surfaces_atomically() {
65 let root = tempdir().expect("temporary directory");
66 let analysis = analysis(root.path());
67 let result =
68 write_report_bundle(&analysis, &HealthRollup::default()).expect("report bundle");
69 assert!(result.report_json.is_file());
70 assert!(result.report_yaml.is_file());
71 assert!(result.summary_md.is_file());
72 assert!(result.health_md.is_file());
73 assert!(
74 fs::read_to_string(&result.summary_md)
75 .expect("summary")
76 .starts_with("# Git Slop Summary")
77 );
78 assert!(
79 fs::read_to_string(&result.health_md)
80 .expect("health")
81 .starts_with("# Repository Health")
82 );
83 assert_eq!(
84 result.report,
85 load_report(&result.report_json).expect("report")
86 );
87 let run_directories = fs::read_dir(config::runs_dir(root.path()))
88 .expect("runs")
89 .count();
90 assert_eq!(run_directories, 1);
91 }
92
93 #[test]
94 fn compatibility_summary_is_bounded_and_actionable() {
95 let mut analysis = analysis(Path::new("/tmp/fixture"));
96 analysis.action_queue = (0..40)
97 .map(|index| {
98 json!({
99 "path": format!("src/file_{index}.rs"),
100 "slop_band": "moderate",
101 "context_band": "healthy",
102 "slop_score": 50.0,
103 "tokens": 4_000,
104 "age_days": 1,
105 "revisions_window": 2,
106 "churn_pressure": 0.5,
107 "reason_codes": ["high_revision_frequency"],
108 "is_pure_context_hotspot": false
109 })
110 })
111 .collect();
112 let report = assemble_report(&analysis, &HealthRollup::default());
113 let summary = render_compatibility_summary(&report);
114 assert_eq!(
115 summary.matches("| [src/file_").count(),
116 super::render::DEFAULT_SUMMARY_LIMIT
117 );
118 assert!(summary.contains("git-slop explain --path"));
119 assert!(summary.contains("changes frequently"));
120 }
121}