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