Skip to main content

coding_agent_search/ui/
mod.rs

1//! FTUI application surface and supporting UI modules.
2//!
3//! [`app`] is the canonical runtime entrypoint. [`tui`] is a retained
4//! comment-only legacy shell kept in-tree by policy until file deletion is
5//! explicitly authorized.
6pub mod analytics_charts;
7pub mod app;
8pub mod components;
9pub mod data;
10pub mod ftui_adapter;
11pub mod shortcuts;
12pub mod style_system;
13pub mod theme;
14pub mod time_parser;
15pub mod trace;
16pub mod tui;
17
18#[cfg(test)]
19mod legacy_shell_tests {
20    use super::app::CassApp;
21    use super::ftui_adapter::Rect;
22    use super::theme::CassTheme;
23
24    #[test]
25    fn canonical_ui_runtime_types_live_outside_legacy_tui_shell() {
26        let _ = std::mem::size_of::<CassApp>();
27        let _ = std::mem::size_of::<CassTheme>();
28        let _ = std::mem::size_of::<Rect>();
29    }
30}
31
32/// Structured test logging for unit/E2E scenario diagnostics (2dccg.11.6).
33///
34/// Provides a lightweight, in-crate test logger with JSON-structured events
35/// so that any test failure includes enough context to diagnose without rerunning.
36///
37/// Schema version: 1 (stable, backwards-compatible additions only).
38#[cfg(test)]
39pub mod test_log {
40    use std::cell::RefCell;
41    use std::time::Instant;
42
43    /// Schema version for structured test log events.
44    pub const SCHEMA_VERSION: u32 = 1;
45
46    /// Category of test event.
47    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
48    pub enum Category {
49        Style,
50        Render,
51        Interaction,
52        Degradation,
53        Theme,
54        Layout,
55    }
56
57    impl Category {
58        pub fn as_str(self) -> &'static str {
59            match self {
60                Self::Style => "style",
61                Self::Render => "render",
62                Self::Interaction => "interaction",
63                Self::Degradation => "degradation",
64                Self::Theme => "theme",
65                Self::Layout => "layout",
66            }
67        }
68    }
69
70    /// Kind of test event.
71    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
72    pub enum Event {
73        AssertPass,
74        AssertFail,
75        StepStart,
76        StepEnd,
77        StateSnapshot,
78    }
79
80    impl Event {
81        pub fn as_str(self) -> &'static str {
82            match self {
83                Self::AssertPass => "assert_pass",
84                Self::AssertFail => "assert_fail",
85                Self::StepStart => "step_start",
86                Self::StepEnd => "step_end",
87                Self::StateSnapshot => "state_snapshot",
88            }
89        }
90    }
91
92    /// A single structured test log entry.
93    #[derive(Debug, Clone)]
94    pub struct LogEntry {
95        pub test_id: String,
96        pub elapsed_us: u64,
97        pub category: Category,
98        pub event: Event,
99        pub detail: String,
100    }
101
102    impl LogEntry {
103        /// Serialize to a single-line JSON string.
104        pub fn to_json(&self) -> String {
105            format!(
106                r#"{{"schema_v":{},"test_id":"{}","elapsed_us":{},"category":"{}","event":"{}","detail":{}}}"#,
107                SCHEMA_VERSION,
108                self.test_id.replace('"', r#"\""#),
109                self.elapsed_us,
110                self.category.as_str(),
111                self.event.as_str(),
112                self.detail,
113            )
114        }
115    }
116
117    /// Lightweight per-test structured logger.
118    ///
119    /// Collects events in memory; on failure, emits them as a diagnostic dump.
120    /// Zero-cost when tests pass and output is not captured.
121    pub struct TestLogger {
122        test_id: String,
123        start: Instant,
124        entries: RefCell<Vec<LogEntry>>,
125    }
126
127    impl TestLogger {
128        /// Create a new logger for the given test scenario.
129        pub fn new(test_id: impl Into<String>) -> Self {
130            Self {
131                test_id: test_id.into(),
132                start: Instant::now(),
133                entries: RefCell::new(Vec::new()),
134            }
135        }
136
137        /// Log a structured event.
138        pub fn log(&self, category: Category, event: Event, detail: impl Into<String>) {
139            let elapsed_us = self.start.elapsed().as_micros() as u64;
140            self.entries.borrow_mut().push(LogEntry {
141                test_id: self.test_id.clone(),
142                elapsed_us,
143                category,
144                event,
145                detail: detail.into(),
146            });
147        }
148
149        /// Log an assertion pass.
150        pub fn pass(&self, category: Category, detail: impl Into<String>) {
151            self.log(category, Event::AssertPass, detail);
152        }
153
154        /// Log an assertion failure (call before the actual assert! so the log is captured).
155        pub fn fail(&self, category: Category, detail: impl Into<String>) {
156            self.log(category, Event::AssertFail, detail);
157        }
158
159        /// Log a step start.
160        pub fn step_start(&self, category: Category, detail: impl Into<String>) {
161            self.log(category, Event::StepStart, detail);
162        }
163
164        /// Log a step end.
165        pub fn step_end(&self, category: Category, detail: impl Into<String>) {
166            self.log(category, Event::StepEnd, detail);
167        }
168
169        /// Emit a state snapshot (theme, degradation, viewport, etc.).
170        pub fn snapshot(&self, category: Category, detail: impl Into<String>) {
171            self.log(category, Event::StateSnapshot, detail);
172        }
173
174        /// Return all entries as JSONL.
175        pub fn to_jsonl(&self) -> String {
176            self.entries
177                .borrow()
178                .iter()
179                .map(|e| e.to_json())
180                .collect::<Vec<_>>()
181                .join("\n")
182        }
183
184        /// Return pass/fail/total summary.
185        pub fn summary(&self) -> (usize, usize, usize) {
186            let entries = self.entries.borrow();
187            let pass = entries
188                .iter()
189                .filter(|e| e.event == Event::AssertPass)
190                .count();
191            let fail = entries
192                .iter()
193                .filter(|e| e.event == Event::AssertFail)
194                .count();
195            (pass, fail, entries.len())
196        }
197
198        /// Dump all events to stderr (useful on test failure).
199        pub fn dump_on_failure(&self) {
200            let (pass, fail, total) = self.summary();
201            if fail > 0 {
202                eprintln!(
203                    "--- TestLogger dump for '{}' ({} pass, {} fail, {} total) ---",
204                    self.test_id, pass, fail, total
205                );
206                eprintln!("{}", self.to_jsonl());
207                eprintln!("--- end dump ---");
208            }
209        }
210    }
211
212    impl Drop for TestLogger {
213        fn drop(&mut self) {
214            // Auto-dump on panic (test failure)
215            if std::thread::panicking() {
216                let (pass, fail, total) = self.summary();
217                eprintln!(
218                    "\n--- TestLogger auto-dump for '{}' ({} pass, {} fail, {} total) ---",
219                    self.test_id, pass, fail, total
220                );
221                eprintln!("{}", self.to_jsonl());
222                eprintln!("--- end auto-dump ---\n");
223            }
224        }
225    }
226
227    /// Assert two styles are equal, logging pass/fail with full context.
228    #[macro_export]
229    macro_rules! assert_style_eq {
230        ($logger:expr, $left:expr, $right:expr, $category:expr, $msg:expr) => {{
231            let left_val = &$left;
232            let right_val = &$right;
233            if left_val == right_val {
234                $logger.pass($category, format!(r#""{}""#, $msg));
235            } else {
236                $logger.fail(
237                    $category,
238                    format!(
239                        r#"{{"msg":"{}","left":"{:?}","right":"{:?}"}}"#,
240                        $msg, left_val, right_val
241                    ),
242                );
243                panic!(
244                    "assert_style_eq failed: {}\n  left: {:?}\n  right: {:?}",
245                    $msg, left_val, right_val
246                );
247            }
248        }};
249    }
250
251    /// Assert a condition, logging pass/fail with context.
252    #[macro_export]
253    macro_rules! assert_logged {
254        ($logger:expr, $cond:expr, $category:expr, $msg:expr) => {{
255            if $cond {
256                $logger.pass($category, format!(r#""{}""#, $msg));
257            } else {
258                $logger.fail(
259                    $category,
260                    format!(r#"{{"msg":"{}","condition":"false"}}"#, $msg),
261                );
262                panic!("assert_logged failed: {}", $msg);
263            }
264        }};
265    }
266
267    #[cfg(test)]
268    mod tests {
269        use super::*;
270
271        #[test]
272        fn test_logger_basic_lifecycle() {
273            let log = TestLogger::new("test_logger_basic");
274            log.step_start(Category::Style, r#""begin style check""#.to_string());
275            log.pass(Category::Style, r#""token resolved""#.to_string());
276            log.step_end(Category::Style, r#""style check done""#.to_string());
277
278            let (pass, fail, total) = log.summary();
279            assert_eq!(pass, 1);
280            assert_eq!(fail, 0);
281            assert_eq!(total, 3);
282        }
283
284        #[test]
285        fn test_logger_jsonl_output() {
286            let log = TestLogger::new("jsonl_test");
287            log.pass(Category::Render, r#""rendered ok""#.to_string());
288            let jsonl = log.to_jsonl();
289            assert!(jsonl.contains(r#""schema_v":1"#));
290            assert!(jsonl.contains(r#""test_id":"jsonl_test""#));
291            assert!(jsonl.contains(r#""category":"render""#));
292            assert!(jsonl.contains(r#""event":"assert_pass""#));
293        }
294
295        #[test]
296        fn test_logger_summary_counts_correctly() {
297            let log = TestLogger::new("summary_test");
298            log.pass(Category::Style, r#""a""#.to_string());
299            log.pass(Category::Theme, r#""b""#.to_string());
300            log.fail(Category::Degradation, r#""c""#.to_string());
301            log.snapshot(Category::Layout, r#""d""#.to_string());
302
303            let (pass, fail, total) = log.summary();
304            assert_eq!(pass, 2);
305            assert_eq!(fail, 1);
306            assert_eq!(total, 4);
307        }
308
309        #[test]
310        fn test_logger_schema_version_stable() {
311            assert_eq!(
312                SCHEMA_VERSION, 1,
313                "schema version must not change without migration"
314            );
315        }
316
317        #[test]
318        fn category_all_variants_have_str() {
319            let cats = [
320                Category::Style,
321                Category::Render,
322                Category::Interaction,
323                Category::Degradation,
324                Category::Theme,
325                Category::Layout,
326            ];
327            for cat in cats {
328                assert!(!cat.as_str().is_empty());
329            }
330        }
331
332        #[test]
333        fn event_all_variants_have_str() {
334            let events = [
335                Event::AssertPass,
336                Event::AssertFail,
337                Event::StepStart,
338                Event::StepEnd,
339                Event::StateSnapshot,
340            ];
341            for ev in events {
342                assert!(!ev.as_str().is_empty());
343            }
344        }
345
346        #[test]
347        fn assert_style_eq_macro_passes() {
348            let log = TestLogger::new("macro_test");
349            let a = 42u32;
350            let b = 42u32;
351            assert_style_eq!(log, a, b, Category::Style, "values should match");
352            let (pass, _, _) = log.summary();
353            assert_eq!(pass, 1);
354        }
355
356        #[test]
357        fn assert_logged_macro_passes() {
358            let log = TestLogger::new("logged_test");
359            assert_logged!(log, true, Category::Render, "condition holds");
360            let (pass, _, _) = log.summary();
361            assert_eq!(pass, 1);
362        }
363    }
364}