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}