Skip to main content

git_slop/
report.rs

1mod assembly;
2mod render;
3mod support;
4mod write;
5
6pub use render::render_terminal;
7pub use write::{
8    load_report, load_report_with_legacy, migrate_legacy_report, schema, validation_violations,
9    write_json_atomically, write_report_bundle,
10};
11
12#[cfg(test)]
13mod tests {
14    use std::fs;
15    use std::path::Path;
16
17    use serde_json::json;
18    use tempfile::tempdir;
19
20    use super::*;
21    use crate::config;
22    use crate::model::{
23        Analysis, HealthRollup, OrganizationAnalysis, RepoMetadata, ScopeIdentity, SkippedCounts,
24    };
25
26    fn analysis(root: &Path) -> Analysis {
27        Analysis {
28            output_root: root.join(".slop"),
29            report_profile: "standard".to_string(),
30            compression: "none".to_string(),
31            repo: RepoMetadata {
32                repo_name: "fixture".to_string(),
33                repo_root: root.display().to_string(),
34                repository_id: Some("remote:github.com/example/fixture".to_string()),
35                repository_identity_source: Some("normalized_remote".to_string()),
36                branch: Some("main".to_string()),
37                head_commit: Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()),
38                head_commit_timestamp: Some("2026-07-29T08:00:00Z".to_string()),
39                git_remote_url: Some("git@github.com:example/fixture.git".to_string()),
40                is_shallow: false,
41                detached_head: false,
42                worktree_clean: true,
43                staged_change_count: 0,
44                modified_tracked_file_count: 0,
45                untracked_file_count: 0,
46                worktree_state_digest:
47                    "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
48                analyzed_content_digest: None,
49            },
50            config: crate::config::default_config(),
51            generated_at: "2026-07-30T10:11:12Z".to_string(),
52            analyzed_revision_at: Some("2026-07-29T08:00:00Z".to_string()),
53            skipped: SkippedCounts::default(),
54            tracked_file_count: 0,
55            scope: ScopeIdentity {
56                mode: "repository".to_string(),
57                path: None,
58                selected_path_count: 0,
59                selected_path_digest:
60                    "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(),
61            },
62            files: vec![],
63            folders: vec![],
64            organization: OrganizationAnalysis::default(),
65            action_queue: vec![],
66            observation_feed: vec![],
67            diagnostics: json!({}),
68        }
69    }
70
71    #[test]
72    fn report_keeps_generation_and_revision_timestamps_distinct() {
73        let root = tempdir().expect("temporary directory");
74        let analysis = analysis(root.path());
75        let report = assembly::assemble_report(&analysis, &HealthRollup::default());
76        assert_eq!(report["generated_at"], "2026-07-30T10:11:12Z");
77        assert_eq!(report["analyzed_revision_at"], "2026-07-29T08:00:00Z");
78        assert!(report["repo"].get("head_commit").is_none());
79        assert_eq!(
80            report["repo"]["head_sha"],
81            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
82        );
83        assert_eq!(
84            report["repo"]["remote_url"],
85            "git@github.com:example/fixture.git"
86        );
87    }
88
89    #[test]
90    fn bundle_writes_compatibility_and_health_surfaces_atomically() {
91        let root = tempdir().expect("temporary directory");
92        let analysis = analysis(root.path());
93        let result =
94            write_report_bundle(&analysis, &HealthRollup::default()).expect("report bundle");
95        assert!(result.report_json.is_file());
96        assert!(!result.report_yaml.exists());
97        assert!(result.summary_md.is_file());
98        assert!(result.health_md.is_file());
99        assert!(
100            fs::read_to_string(&result.summary_md)
101                .expect("summary")
102                .starts_with("# Git Slop Summary")
103        );
104        assert!(
105            fs::read_to_string(&result.health_md)
106                .expect("health")
107                .starts_with("# Repository Health")
108        );
109        assert_eq!(
110            result.report,
111            load_report(&result.report_json).expect("report")
112        );
113        let run_directories = fs::read_dir(config::runs_dir(root.path()))
114            .expect("runs")
115            .count();
116        assert_eq!(run_directories, 1);
117    }
118
119    #[test]
120    fn compatibility_summary_is_bounded_and_actionable() {
121        let mut analysis = analysis(Path::new("/tmp/fixture"));
122        analysis.action_queue = (0..40)
123            .map(|index| {
124                json!({
125                    "path": format!("src/file_{index}.rs"),
126                    "slop_band": "moderate",
127                    "context_band": "healthy",
128                    "slop_score": 50.0,
129                    "tokens": 4_000,
130                    "age_days": 1,
131                    "revisions_window": 2,
132                    "churn_pressure": 0.5,
133                    "reason_codes": ["high_revision_frequency"],
134                    "is_pure_context_hotspot": false
135                })
136            })
137            .collect();
138        let report = assembly::assemble_report(&analysis, &HealthRollup::default());
139        let summary = render::render_compatibility_summary(&report);
140        assert_eq!(
141            summary.matches("| [src/file_").count(),
142            super::render::DEFAULT_SUMMARY_LIMIT
143        );
144        assert!(summary.contains("git slop explain --path"));
145        assert!(summary.contains("changes frequently"));
146    }
147
148    #[test]
149    fn published_report_schema_matches_the_runtime_contract() {
150        let published: serde_json::Value =
151            serde_json::from_str(include_str!("../schemas/report-5.json"))
152                .expect("published report schema");
153        assert_eq!(published, super::write::schema());
154        assert_eq!(
155            published.pointer("/$defs/folder/properties/health_band/$ref"),
156            Some(&json!("#/$defs/health_band"))
157        );
158        assert_eq!(
159            published.pointer("/$defs/health_band/enum"),
160            Some(&json!(["compact", "healthy", "warning", "budget_exceeded"]))
161        );
162    }
163}