trusty_console/detect/review.rs
1//! `ServiceConnector` implementation for `trusty-review`.
2//!
3//! Why: trusty-review writes its bound address to `~/.trusty-review/http_addr`
4//! on successful bind. This connector reads that file and probes the TCP port.
5//! The console previously excluded trusty-review per decision #1069; issue #1163
6//! lifts that exclusion now that the Review dashboard tab is implemented.
7//! What: `ReviewConnector` implements `ServiceConnector::detect()` using
8//! `~/.trusty-review/http_addr` as the discovery file and `trusty-review` as
9//! the binary name. Falls back to probing the default port (7880) when no
10//! discovery file is found, matching the AnalyzeConnector pattern.
11//! Test: `test_review_connector_with_stale_addr_file` and
12//! `test_review_connector_no_addr_file` below.
13
14use std::path::PathBuf;
15
16use crate::connector::{ServiceConnector, ServiceInfo, ServiceStatus};
17
18use super::helpers::{binary_on_path, fetch_health_version, read_addr_file, tcp_probe};
19
20/// ServiceConnector for `trusty-review`.
21///
22/// Why: trusty-review stores its data under `~/.trusty-review/`. An `http_addr`
23/// file there (if written by the running daemon) gives the exact address;
24/// otherwise we probe the default port 7880.
25/// What: Implements `detect()` checking `~/.trusty-review/http_addr`, then
26/// falling back to `http://127.0.0.1:7880` TCP probe when the file is absent.
27/// Test: `test_review_connector_with_stale_addr_file`,
28/// `test_review_connector_no_addr_file` below.
29pub struct ReviewConnector {
30 /// Override for the home directory (used in tests).
31 home_dir: Option<PathBuf>,
32}
33
34impl ReviewConnector {
35 /// Create a new `ReviewConnector`.
36 ///
37 /// Why: Production callers use `new()`; tests use `with_home()`.
38 /// What: Stores no state except the optional home override.
39 /// Test: Created in `all_connectors()` and in unit tests.
40 pub fn new() -> Self {
41 Self { home_dir: None }
42 }
43
44 /// Create a connector that uses `home_dir` instead of the real home.
45 ///
46 /// Why: Unit tests must not read or write the real user's `~/.trusty-*`
47 /// directories. Injecting a temp dir keeps tests hermetic.
48 /// What: Stores `home_dir` for use in `addr_file_path()`.
49 /// Test: `test_review_connector_with_stale_addr_file`,
50 /// `test_review_connector_no_addr_file`.
51 #[cfg(test)]
52 pub fn with_home(home_dir: PathBuf) -> Self {
53 Self {
54 home_dir: Some(home_dir),
55 }
56 }
57
58 fn addr_file_path(&self) -> PathBuf {
59 let home = self
60 .home_dir
61 .clone()
62 .or_else(dirs::home_dir)
63 .unwrap_or_else(|| PathBuf::from("/tmp"));
64 home.join(".trusty-review").join("http_addr")
65 }
66
67 /// Default address string for trusty-review.
68 ///
69 /// Why: trusty-review's fixed default port is 7880. Used as a fallback
70 /// when no http_addr file is found.
71 /// What: Returns the address string `"127.0.0.1:7880"`.
72 /// Test: Covered by the detect() fallback path.
73 pub(crate) fn default_addr() -> &'static str {
74 "127.0.0.1:7880"
75 }
76}
77
78impl Default for ReviewConnector {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl ServiceConnector for ReviewConnector {
85 fn id(&self) -> &'static str {
86 "trusty-review"
87 }
88
89 fn display_name(&self) -> &'static str {
90 "Trusty Review"
91 }
92
93 /// Detect trusty-review status.
94 ///
95 /// Why: Reads `~/.trusty-review/http_addr` — the file the daemon writes
96 /// immediately after successfully binding its port. Falls back to probing
97 /// the default port 7880 when no discovery file is present (matches the
98 /// AnalyzeConnector pattern).
99 /// What: Binary check → addr file + TCP probe → fallback default-port TCP → status.
100 /// Test: `test_review_connector_with_stale_addr_file`,
101 /// `test_review_connector_no_addr_file`.
102 fn detect(&self) -> ServiceInfo {
103 if !binary_on_path("trusty-review") {
104 return ServiceInfo {
105 id: self.id().to_string(),
106 display_name: self.display_name().to_string(),
107 status: ServiceStatus::Absent,
108 version: None,
109 url: None,
110 hint: None,
111 };
112 }
113
114 // Try the discovery file first.
115 if let Some(addr) = read_addr_file(&self.addr_file_path())
116 && tcp_probe(&addr)
117 {
118 let base_url = format!("http://{addr}");
119 let version = fetch_health_version(&addr);
120 return ServiceInfo {
121 id: self.id().to_string(),
122 display_name: self.display_name().to_string(),
123 status: ServiceStatus::Running,
124 version,
125 url: Some(base_url),
126 hint: None,
127 };
128 }
129
130 // Fallback: probe the well-known default port.
131 let default_addr = Self::default_addr();
132 if tcp_probe(default_addr) {
133 let base_url = format!("http://{default_addr}");
134 let version = fetch_health_version(default_addr);
135 return ServiceInfo {
136 id: self.id().to_string(),
137 display_name: self.display_name().to_string(),
138 status: ServiceStatus::Running,
139 version,
140 url: Some(base_url),
141 hint: None,
142 };
143 }
144
145 ServiceInfo {
146 id: self.id().to_string(),
147 display_name: self.display_name().to_string(),
148 status: ServiceStatus::Available,
149 version: None,
150 url: None,
151 hint: None,
152 }
153 }
154}
155
156// ─── tests ────────────────────────────────────────────────────────────────────
157
158#[cfg(test)]
159mod tests {
160 use super::super::helpers::tcp_probe;
161 use super::*;
162 use std::fs;
163 use tempfile::TempDir;
164
165 fn make_home_with_addr(rel_path: &str, content: &str) -> TempDir {
166 let tmp = TempDir::new().expect("tempdir");
167 let path = tmp.path().join(rel_path);
168 fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
169 fs::write(&path, content).expect("write addr file");
170 tmp
171 }
172
173 /// Why: stale addr file must yield Available (not Running) because TCP on
174 /// port 14997 fails. However, the ReviewConnector also probes the default
175 /// port 7880 as a fallback. If `trusty-review` is running on 7880 in the
176 /// test environment this test returns Running — which is correct behaviour.
177 /// What: creates `.trusty-review/http_addr = 127.0.0.1:14997` and calls
178 /// detect(); branches on whether the binary is present and the default port
179 /// is reachable, producing a deterministic assertion regardless of environment.
180 /// Test: this test itself.
181 #[test]
182 fn test_review_connector_with_stale_addr_file() {
183 let tmp = make_home_with_addr(".trusty-review/http_addr", "127.0.0.1:14997");
184 let connector = ReviewConnector::with_home(tmp.path().to_path_buf());
185 let info = connector.detect();
186 let binary_present = which::which("trusty-review").is_ok();
187 let default_running = tcp_probe(ReviewConnector::default_addr());
188 if !binary_present {
189 assert_eq!(info.status, ServiceStatus::Absent, "binary absent → Absent");
190 } else if default_running {
191 assert_eq!(
192 info.status,
193 ServiceStatus::Running,
194 "binary present, default port running → Running"
195 );
196 } else {
197 assert_eq!(
198 info.status,
199 ServiceStatus::Available,
200 "binary present, no running daemon → Available"
201 );
202 }
203 assert_eq!(info.id, "trusty-review");
204 assert_eq!(info.display_name, "Trusty Review");
205 }
206
207 /// Why: no addr file; result depends on whether the binary is present and
208 /// the daemon is running on the default port.
209 /// What: empty temp HOME; branches deterministically on
210 /// `which::which("trusty-review")` and a probe of the default port.
211 /// Test: this test itself.
212 #[test]
213 fn test_review_connector_no_addr_file() {
214 let tmp = TempDir::new().expect("tempdir");
215 let connector = ReviewConnector::with_home(tmp.path().to_path_buf());
216 let info = connector.detect();
217 let binary_present = which::which("trusty-review").is_ok();
218 let default_running = tcp_probe(ReviewConnector::default_addr());
219 if !binary_present {
220 assert_eq!(info.status, ServiceStatus::Absent, "binary absent → Absent");
221 } else if default_running {
222 assert_eq!(
223 info.status,
224 ServiceStatus::Running,
225 "binary present, default port running → Running"
226 );
227 } else {
228 assert_eq!(
229 info.status,
230 ServiceStatus::Available,
231 "binary present, no running daemon → Available"
232 );
233 }
234 assert!(info.url.is_none() || info.status == ServiceStatus::Running);
235 }
236}