heddle_cli_render/cli/render/
query.rs1use anyhow::{Context, Result};
3use chrono::{TimeZone, Utc};
4use verbs::QueryReport;
5
6use crate::cli::{render::write_stdout, style};
7
8pub fn query_json(report: &QueryReport) -> Result<()> {
9 let mut text = serde_json::to_string(report).context("serialize query output")?;
10 text.push('\n');
11 write_stdout(&text)
12}
13
14pub fn query_text(report: &QueryReport) -> Result<()> {
15 write_stdout(&format_query_text(report))
16}
17
18fn format_query_text(report: &QueryReport) -> String {
19 let mut text = String::new();
20 if report.hits.is_empty() {
21 text.push_str("(no matches)\n");
22 } else {
23 for hit in &report.hits {
24 let ts = Utc
25 .timestamp_opt(hit.timestamp_secs, 0)
26 .single()
27 .map(|d| d.to_rfc3339())
28 .unwrap_or_else(|| hit.timestamp_secs.to_string());
29 text.push_str(&format!(
30 "#{} {} {} <{}>",
31 hit.seq,
32 ts,
33 style::human_text(&hit.verb),
34 style::human_text(&hit.actor_email)
35 ));
36 if let Some(thread) = &hit.thread {
37 text.push_str(&format!(" thread={}", style::thread_label(thread, None)));
38 }
39 if let Some(state_id) = &hit.state_id {
40 text.push_str(&format!(" -> {}", style::human_text(state_id)));
41 }
42 text.push('\n');
43 }
44 }
45 text
46}
47
48#[cfg(test)]
49mod tests {
50 use verbs::QueryHit;
51
52 use super::*;
53
54 #[test]
55 fn text_renderer_consumes_the_typed_query_report() {
56 let report = QueryReport {
57 output_kind: "query",
58 hits: vec![QueryHit {
59 seq: 7,
60 timestamp_secs: 0,
61 verb: "snapshot".to_string(),
62 actor_email: "agent@example.com".to_string(),
63 operation_id: None,
64 thread: Some("agent/facade".to_string()),
65 symbols: Vec::new(),
66 signal_kinds: Vec::new(),
67 state_id: Some("hs-123".to_string()),
68 }],
69 };
70
71 assert_eq!(
72 format_query_text(&report),
73 "#7 1970-01-01T00:00:00+00:00 snapshot <agent@example.com> thread=agent/facade -> hs-123\n"
74 );
75 }
76
77 #[test]
78 fn text_renderer_has_a_stable_empty_report() {
79 let report = QueryReport {
80 output_kind: "query",
81 hits: Vec::new(),
82 };
83 assert_eq!(format_query_text(&report), "(no matches)\n");
84 }
85
86 #[test]
87 fn text_renderer_hides_full_thread_and_state_ids() {
88 let id = "a".repeat(64);
89 let report = QueryReport {
90 output_kind: "query",
91 hits: vec![QueryHit {
92 seq: 1,
93 timestamp_secs: 0,
94 verb: "capture".to_string(),
95 actor_email: "agent@example.com".to_string(),
96 operation_id: None,
97 thread: Some(id.clone()),
98 symbols: Vec::new(),
99 signal_kinds: Vec::new(),
100 state_id: Some(id.clone()),
101 }],
102 };
103 let text = format_query_text(&report);
104 assert!(!text.contains(&id), "{text}");
105 assert!(serde_json::to_string(&report).unwrap().contains(&id));
106 }
107}