gobby_core/
degradation.rs1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub enum ServiceState {
12 Available,
14 NotConfigured,
16 Unreachable {
18 message: String,
20 },
21}
22
23impl ServiceState {
24 pub fn is_available(&self) -> bool {
26 matches!(self, Self::Available)
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct SetupIssue {
33 pub object_name: String,
35 pub store: String,
37 pub guidance: Guidance,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Guidance {
46 pub problem: String,
48 pub action: String,
50 pub command_hint: Option<String>,
52}
53
54#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
56pub enum CoreError {
57 #[error("invalid configuration: {0}")]
59 InvalidConfig(String),
60 #[error("required service unavailable: {service} — {message}")]
62 RequiredServiceUnavailable {
63 service: String,
65 message: String,
67 },
68 #[error("write failed: {0}")]
70 WriteFailed(String),
71 #[error("corrupted input: {0}")]
73 CorruptedInput(String),
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub enum DegradationKind {
79 ServiceUnavailable {
81 service: String,
83 state: ServiceState,
85 },
86 PartialSearch {
88 available: Vec<String>,
90 unavailable: Vec<String>,
92 },
93 StaleIndex {
95 paths: Vec<String>,
97 },
98 SkippedArtifacts {
100 count: usize,
102 reason: String,
104 },
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn optional_service_degradation_is_not_fatal() {
113 let unconfigured = ServiceState::NotConfigured;
114 let unreachable = ServiceState::Unreachable {
115 message: "connection refused".to_string(),
116 };
117
118 assert!(!unconfigured.is_available());
119 assert!(!unreachable.is_available());
120
121 let degradation = DegradationKind::ServiceUnavailable {
122 service: "qdrant".to_string(),
123 state: unconfigured,
124 };
125 let fatal = CoreError::RequiredServiceUnavailable {
126 service: "postgres".to_string(),
127 message: "hub is required for writes".to_string(),
128 };
129
130 assert!(matches!(
131 degradation,
132 DegradationKind::ServiceUnavailable {
133 service,
134 state: ServiceState::NotConfigured
135 } if service == "qdrant"
136 ));
137 assert_eq!(
138 fatal.to_string(),
139 "required service unavailable: postgres — hub is required for writes"
140 );
141 }
142
143 #[test]
144 fn guidance_is_structured() {
145 let guidance = Guidance {
146 problem: "BM25 index missing".to_string(),
147 action: "run attached setup validation".to_string(),
148 command_hint: Some("gobby setup validate".to_string()),
149 };
150
151 assert_eq!(guidance.problem, "BM25 index missing");
152 assert_eq!(guidance.action, "run attached setup validation");
153 assert_eq!(
154 guidance.command_hint.as_deref(),
155 Some("gobby setup validate")
156 );
157 }
158
159 #[test]
160 fn core_error_serialization_roundtrip() {
161 let invalid_config = CoreError::InvalidConfig("missing project id".to_string());
162 let encoded = serde_json::to_string(&invalid_config).expect("serialize invalid config");
163 let decoded: CoreError =
164 serde_json::from_str(&encoded).expect("deserialize invalid config");
165 assert!(matches!(
166 decoded,
167 CoreError::InvalidConfig(message) if message == "missing project id"
168 ));
169
170 let unavailable = CoreError::RequiredServiceUnavailable {
171 service: "postgres".to_string(),
172 message: "connection refused".to_string(),
173 };
174 let encoded = serde_json::to_string(&unavailable).expect("serialize unavailable");
175 let decoded: CoreError = serde_json::from_str(&encoded).expect("deserialize unavailable");
176 assert!(matches!(
177 decoded,
178 CoreError::RequiredServiceUnavailable { service, message }
179 if service == "postgres" && message == "connection refused"
180 ));
181 }
182}