Skip to main content

trusty_console/
connector.rs

1//! ServiceConnector trait — the extensibility seam for all per-service adapters.
2//!
3//! Why: P0 only needs read-only detection (`detect()`), but P1+ must add
4//! `spawn()` and typed tool-call methods. Defining the trait now with those
5//! method stubs keeps the architecture clean and avoids large breaking
6//! refactors when those phases land.
7//! What: Defines `ServiceStatus`, the `ServiceConnector` trait, and a
8//! `ServiceInfo` result struct that the API layer serialises.
9//! Test: Each concrete connector implements `#[cfg(test)]` unit tests that
10//! exercise `detect()` against fake data-dir fixtures; see `detect.rs`.
11
12use serde::{Deserialize, Serialize};
13
14/// Runtime status of one detected service.
15///
16/// Why: Four states capture everything the console needs — whether the binary
17/// exists, whether a daemon is running, whether the MCP handshake succeeded but
18/// the expected tool is missing (Degraded), and whether the binary is absent.
19/// What: Serialises to a lowercase string for the JSON API.
20/// Test: Asserted by unit tests in `detect.rs` and `server.rs`.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum ServiceStatus {
24    /// Daemon is reachable and health-checked OK.
25    Running,
26    /// Binary found on PATH but no daemon discovery file / TCP probe.
27    Available,
28    /// Binary not found on PATH.
29    Absent,
30    /// Process is reachable (MCP handshake succeeded) but the expected
31    /// `console_metrics` tool was not listed in `tools/list` — check that
32    /// the daemon is started in `serve --stdio` mode with the correct wiring.
33    Degraded,
34}
35
36/// All facts gathered about a service in one detection pass.
37///
38/// Why: The API handler turns this struct directly into JSON for
39/// `GET /api/console/services`, so callers get a stable shape to render cards.
40/// What: `id` is the stable machine identifier; `display_name` is human-
41/// readable; `status` is the current runtime state; `version` is the version
42/// string from `/health` when the daemon is running (absent otherwise);
43/// `url` is the daemon base URL when reachable; `hint` is an optional
44/// actionable remediation message surfaced when `status` is `Degraded`.
45/// Test: Tested via the server integration test in `server.rs`.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ServiceInfo {
48    /// Stable machine identifier (e.g. `"trusty-search"`).
49    pub id: String,
50    /// Human-readable display name (e.g. `"Trusty Search"`).
51    pub display_name: String,
52    /// Current runtime state.
53    pub status: ServiceStatus,
54    /// Version string reported by the running daemon's `/health` endpoint.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub version: Option<String>,
57    /// Base URL of the running daemon (e.g. `"http://127.0.0.1:7879"`).
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub url: Option<String>,
60    /// Actionable remediation hint — present when `status` is `Degraded`.
61    /// Example: "reachable but `console_metrics` tool not registered — check
62    /// `serve --stdio` wiring / restart the daemon".
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub hint: Option<String>,
65}
66
67/// Per-service adapter contract.
68///
69/// Why: Decouples the orchestration loop from service-specific knowledge.
70/// P0 only calls `detect()`; P1+ will add `spawn()` and typed MCP/tool-call
71/// methods without modifying existing code paths.
72/// What: A synchronous detect() that returns a `ServiceInfo`. The trait is
73/// object-safe so connectors can be boxed (`Box<dyn ServiceConnector>`).
74/// Test: Each impl has unit tests in `detect.rs` exercising the three status
75/// outcomes.
76pub trait ServiceConnector: Send + Sync {
77    /// Stable machine identifier for this service (must be unique per instance).
78    ///
79    /// Why: Used as the `id` field in `ServiceInfo` and as a log tag.
80    /// What: Returns a `'static str` reference so no allocation is needed.
81    /// Test: Compared against expected strings in tests.
82    fn id(&self) -> &'static str;
83
84    /// Human-readable name shown in the console UI.
85    ///
86    /// Why: Separates the stable ID from the display label so either can
87    /// change independently.
88    /// What: Returns a `'static str`.
89    /// Test: Asserted by the connector construction tests.
90    fn display_name(&self) -> &'static str;
91
92    /// Detect the current runtime status of this service.
93    ///
94    /// Why: P0 detection is purely read-only — reads files, probes TCP, looks
95    /// for binaries — so it never mutates any service state.
96    /// What: Returns a `ServiceInfo` with `status`, optional `version`, and
97    /// optional `url`. Never returns an error; degrades gracefully to `Absent`.
98    /// Test: Unit tests in `detect.rs` inject a temp home dir and fake files.
99    fn detect(&self) -> ServiceInfo;
100}
101
102// ─── tests ────────────────────────────────────────────────────────────────────
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    /// Why: The JSON API contract requires `ServiceStatus` variants to serialise
109    /// to exact lowercase strings. A missing or mismatched `#[serde(rename_all)]`
110    /// attribute would silently change the wire format and break the Svelte UI.
111    /// What: Serialises each variant via `serde_json::to_value` and asserts the
112    /// exact expected string so any future rename is caught at test time.
113    /// Test: This test.
114    #[test]
115    fn service_status_serialises_to_lowercase_strings() {
116        assert_eq!(
117            serde_json::to_value(ServiceStatus::Running).unwrap(),
118            serde_json::json!("running"),
119            "Running must serialise to \"running\""
120        );
121        assert_eq!(
122            serde_json::to_value(ServiceStatus::Available).unwrap(),
123            serde_json::json!("available"),
124            "Available must serialise to \"available\""
125        );
126        assert_eq!(
127            serde_json::to_value(ServiceStatus::Absent).unwrap(),
128            serde_json::json!("absent"),
129            "Absent must serialise to \"absent\""
130        );
131        assert_eq!(
132            serde_json::to_value(ServiceStatus::Degraded).unwrap(),
133            serde_json::json!("degraded"),
134            "Degraded must serialise to \"degraded\""
135        );
136    }
137}