Skip to main content

topo_render/
lib.rs

1//! JSONL v0.3, JSON, compact, and human-readable output rendering.
2
3mod compact;
4mod jsonl;
5
6pub use compact::CompactWriter;
7pub use jsonl::JsonlWriter;
8
9#[cfg(test)]
10mod tests {
11    use super::*;
12    use topo_core::{FileRole, Language, ScoredFile, SignalBreakdown};
13
14    fn sample_files() -> Vec<ScoredFile> {
15        vec![
16            ScoredFile {
17                path: "src/auth/middleware.rs".to_string(),
18                score: 0.95,
19                signals: SignalBreakdown {
20                    bm25f: 0.8,
21                    heuristic: 0.7,
22                    ..Default::default()
23                },
24                tokens: 1200,
25                language: Language::Rust,
26                role: FileRole::Implementation,
27            },
28            ScoredFile {
29                path: "src/auth/handler.rs".to_string(),
30                score: 0.72,
31                signals: SignalBreakdown {
32                    bm25f: 0.5,
33                    heuristic: 0.6,
34                    ..Default::default()
35                },
36                tokens: 800,
37                language: Language::Rust,
38                role: FileRole::Implementation,
39            },
40        ]
41    }
42
43    #[test]
44    fn jsonl_output_has_three_lines() {
45        let files = sample_files();
46        let output = JsonlWriter::new("auth middleware", "balanced")
47            .max_bytes(Some(100_000))
48            .min_score(0.01)
49            .render(&files, 358)
50            .unwrap();
51
52        let lines: Vec<&str> = output.trim().lines().collect();
53        assert_eq!(lines.len(), 4); // header + 2 files + footer
54    }
55
56    #[test]
57    fn jsonl_header_contains_version() {
58        let files = sample_files();
59        let output = JsonlWriter::new("test query", "balanced")
60            .render(&files, 100)
61            .unwrap();
62
63        let first_line = output.lines().next().unwrap();
64        let header: serde_json::Value = serde_json::from_str(first_line).unwrap();
65        assert_eq!(header["Version"], "0.3");
66    }
67
68    #[test]
69    fn jsonl_header_contains_query() {
70        let files = sample_files();
71        let output = JsonlWriter::new("auth middleware", "balanced")
72            .render(&files, 100)
73            .unwrap();
74
75        let first_line = output.lines().next().unwrap();
76        let header: serde_json::Value = serde_json::from_str(first_line).unwrap();
77        assert_eq!(header["Query"], "auth middleware");
78    }
79
80    #[test]
81    fn jsonl_file_entries_have_required_fields() {
82        let files = sample_files();
83        let output = JsonlWriter::new("test", "balanced")
84            .render(&files, 100)
85            .unwrap();
86
87        let lines: Vec<&str> = output.trim().lines().collect();
88        let file_entry: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
89
90        assert!(file_entry["Path"].is_string());
91        assert!(file_entry["Score"].is_number());
92        assert!(file_entry["Tokens"].is_number());
93        assert!(file_entry["Language"].is_string());
94        assert!(file_entry["Role"].is_string());
95    }
96
97    #[test]
98    fn jsonl_footer_has_totals() {
99        let files = sample_files();
100        let output = JsonlWriter::new("test", "balanced")
101            .render(&files, 358)
102            .unwrap();
103
104        let last_line = output.trim().lines().last().unwrap();
105        let footer: serde_json::Value = serde_json::from_str(last_line).unwrap();
106
107        assert_eq!(footer["TotalFiles"], 2);
108        assert_eq!(footer["TotalTokens"], 2000); // 1200 + 800
109        assert_eq!(footer["ScannedFiles"], 358);
110    }
111
112    #[test]
113    fn jsonl_empty_files_produces_header_and_footer() {
114        let output = JsonlWriter::new("test", "balanced").render(&[], 0).unwrap();
115
116        let lines: Vec<&str> = output.trim().lines().collect();
117        assert_eq!(lines.len(), 2); // header + footer
118    }
119
120    #[test]
121    fn jsonl_each_line_is_valid_json() {
122        let files = sample_files();
123        let output = JsonlWriter::new("test", "balanced")
124            .render(&files, 100)
125            .unwrap();
126
127        for line in output.trim().lines() {
128            let parsed: Result<serde_json::Value, _> = serde_json::from_str(line);
129            assert!(parsed.is_ok(), "Invalid JSON line: {line}");
130        }
131    }
132
133    #[test]
134    fn jsonl_max_bytes_in_header() {
135        let output = JsonlWriter::new("test", "balanced")
136            .max_bytes(Some(50_000))
137            .render(&[], 0)
138            .unwrap();
139
140        let first_line = output.lines().next().unwrap();
141        let header: serde_json::Value = serde_json::from_str(first_line).unwrap();
142        assert_eq!(header["Budget"]["MaxBytes"], 50_000);
143    }
144
145    #[test]
146    fn jsonl_preset_in_header() {
147        let output = JsonlWriter::new("test", "deep").render(&[], 0).unwrap();
148
149        let first_line = output.lines().next().unwrap();
150        let header: serde_json::Value = serde_json::from_str(first_line).unwrap();
151        assert_eq!(header["Preset"], "deep");
152    }
153}