1use serde::{Deserialize, Serialize};
17
18use super::{InternalVerb, VerbCategory};
19
20pub struct ObservabilityAlertList;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
28pub struct ObservabilityAlertListInput {
29 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub severity: Option<String>,
33 #[serde(default = "bool_true")]
36 pub active_only: bool,
37}
38
39fn bool_true() -> bool {
40 true
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
46pub struct AlertEntry {
47 pub id: String,
49 pub title: String,
51 pub severity: String,
53 pub state: String,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub started_at: Option<String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub detail: Option<String>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
67pub struct ObservabilityAlertListOutput {
68 pub alerts: Vec<AlertEntry>,
69}
70
71impl InternalVerb for ObservabilityAlertList {
72 type Input = ObservabilityAlertListInput;
73 type Output = ObservabilityAlertListOutput;
74 const ID: &'static str = "observability.alert.list";
75 const CATEGORY: VerbCategory = VerbCategory::Observability;
76}
77
78pub struct ObservabilityAlertAck;
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
86pub struct ObservabilityAlertAckInput {
87 pub id: String,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub message: Option<String>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
97pub struct ObservabilityAlertAckOutput {
98 pub acknowledged: bool,
101}
102
103impl InternalVerb for ObservabilityAlertAck {
104 type Input = ObservabilityAlertAckInput;
105 type Output = ObservabilityAlertAckOutput;
106 const ID: &'static str = "observability.alert.ack";
107 const CATEGORY: VerbCategory = VerbCategory::Observability;
108}
109
110pub struct ObservabilityIncidentOpen;
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
118pub struct ObservabilityIncidentOpenInput {
119 pub title: String,
120 pub severity: String,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub body: Option<String>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
131pub struct ObservabilityIncidentOpenOutput {
132 pub id: String,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub url: Option<String>,
138}
139
140impl InternalVerb for ObservabilityIncidentOpen {
141 type Input = ObservabilityIncidentOpenInput;
142 type Output = ObservabilityIncidentOpenOutput;
143 const ID: &'static str = "observability.incident.open";
144 const CATEGORY: VerbCategory = VerbCategory::Observability;
145}
146
147pub struct ObservabilityIncidentClose;
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
155pub struct ObservabilityIncidentCloseInput {
156 pub id: String,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub resolution: Option<String>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
166pub struct ObservabilityIncidentCloseOutput {
167 pub closed: bool,
170}
171
172impl InternalVerb for ObservabilityIncidentClose {
173 type Input = ObservabilityIncidentCloseInput;
174 type Output = ObservabilityIncidentCloseOutput;
175 const ID: &'static str = "observability.incident.close";
176 const CATEGORY: VerbCategory = VerbCategory::Observability;
177}
178
179pub struct ObservabilityDashboardSnapshot;
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
187pub struct ObservabilityDashboardSnapshotInput {
188 pub dashboard_id: String,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub time_range: Option<String>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
200pub struct ObservabilityDashboardSnapshotOutput {
201 pub url: String,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub expires_at: Option<String>,
207}
208
209impl InternalVerb for ObservabilityDashboardSnapshot {
210 type Input = ObservabilityDashboardSnapshotInput;
211 type Output = ObservabilityDashboardSnapshotOutput;
212 const ID: &'static str = "observability.dashboard.snapshot";
213 const CATEGORY: VerbCategory = VerbCategory::Observability;
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn verb_ids_match_canonical_namespace() {
222 let ids = [
223 ObservabilityAlertList::ID,
224 ObservabilityAlertAck::ID,
225 ObservabilityIncidentOpen::ID,
226 ObservabilityIncidentClose::ID,
227 ObservabilityDashboardSnapshot::ID,
228 ];
229 for id in ids {
230 assert!(id.starts_with("observability."), "{id}");
231 }
232 }
233
234 #[test]
235 fn verbs_are_under_observability_category() {
236 assert_eq!(
237 ObservabilityAlertList::CATEGORY,
238 VerbCategory::Observability
239 );
240 assert_eq!(ObservabilityAlertAck::CATEGORY, VerbCategory::Observability);
241 assert_eq!(
242 ObservabilityIncidentOpen::CATEGORY,
243 VerbCategory::Observability
244 );
245 assert_eq!(
246 ObservabilityIncidentClose::CATEGORY,
247 VerbCategory::Observability
248 );
249 assert_eq!(
250 ObservabilityDashboardSnapshot::CATEGORY,
251 VerbCategory::Observability
252 );
253 }
254
255 #[test]
256 fn alert_list_input_defaults_active_only_true() {
257 let wire = r#"{"severity":"critical"}"#;
258 let parsed: ObservabilityAlertListInput = serde_json::from_str(wire).unwrap();
259 assert!(parsed.active_only, "active_only should default to true");
260 assert_eq!(parsed.severity.as_deref(), Some("critical"));
261 }
262
263 #[test]
264 fn alert_list_input_no_severity_returns_all() {
265 let wire = r#"{}"#;
266 let parsed: ObservabilityAlertListInput = serde_json::from_str(wire).unwrap();
267 assert!(parsed.severity.is_none());
268 assert!(parsed.active_only);
269 }
270
271 #[test]
272 fn alert_entry_round_trips() {
273 let entry = AlertEntry {
274 id: "a1".into(),
275 title: "CPU high".into(),
276 severity: "critical".into(),
277 state: "firing".into(),
278 started_at: Some("2026-06-06T00:00:00Z".into()),
279 detail: None,
280 };
281 let wire = serde_json::to_string(&entry).unwrap();
282 let back: AlertEntry = serde_json::from_str(&wire).unwrap();
283 assert_eq!(back.severity, "critical");
284 assert!(back.detail.is_none());
285 }
286
287 #[test]
288 fn incident_open_output_omits_url_when_absent() {
289 let out = ObservabilityIncidentOpenOutput {
290 id: "INC-1".into(),
291 url: None,
292 };
293 let wire = serde_json::to_value(&out).unwrap();
294 assert!(!wire.as_object().unwrap().contains_key("url"));
295 }
296
297 #[test]
298 fn dashboard_snapshot_time_range_optional() {
299 let no_range = r#"{"dashboard_id":"abc123"}"#;
300 let parsed: ObservabilityDashboardSnapshotInput = serde_json::from_str(no_range).unwrap();
301 assert!(parsed.time_range.is_none());
302
303 let with_range = r#"{"dashboard_id":"abc123","time_range":"last_24h"}"#;
304 let parsed: ObservabilityDashboardSnapshotInput = serde_json::from_str(with_range).unwrap();
305 assert_eq!(parsed.time_range.as_deref(), Some("last_24h"));
306 }
307
308 #[cfg(feature = "json-schema")]
309 #[test]
310 fn verbs_emit_schemas_via_for_verb() {
311 use super::super::VerbDescriptor;
312
313 let alert_list = VerbDescriptor::for_verb::<ObservabilityAlertList>();
314 assert_eq!(alert_list.id, "observability.alert.list");
315 assert!(alert_list.output_schema.to_string().contains("alerts"));
316
317 let snapshot = VerbDescriptor::for_verb::<ObservabilityDashboardSnapshot>();
318 assert_eq!(snapshot.id, "observability.dashboard.snapshot");
319 assert!(snapshot.output_schema.to_string().contains("url"));
320 }
321}