use serde::{Deserialize, Serialize};
use super::{InternalVerb, VerbCategory};
pub struct ObservabilityAlertList;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ObservabilityAlertListInput {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub severity: Option<String>,
#[serde(default = "bool_true")]
pub active_only: bool,
}
fn bool_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct AlertEntry {
pub id: String,
pub title: String,
pub severity: String,
pub state: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub started_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ObservabilityAlertListOutput {
pub alerts: Vec<AlertEntry>,
}
impl InternalVerb for ObservabilityAlertList {
type Input = ObservabilityAlertListInput;
type Output = ObservabilityAlertListOutput;
const ID: &'static str = "observability.alert.list";
const CATEGORY: VerbCategory = VerbCategory::Observability;
}
pub struct ObservabilityAlertAck;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ObservabilityAlertAckInput {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ObservabilityAlertAckOutput {
pub acknowledged: bool,
}
impl InternalVerb for ObservabilityAlertAck {
type Input = ObservabilityAlertAckInput;
type Output = ObservabilityAlertAckOutput;
const ID: &'static str = "observability.alert.ack";
const CATEGORY: VerbCategory = VerbCategory::Observability;
}
pub struct ObservabilityIncidentOpen;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ObservabilityIncidentOpenInput {
pub title: String,
pub severity: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ObservabilityIncidentOpenOutput {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
impl InternalVerb for ObservabilityIncidentOpen {
type Input = ObservabilityIncidentOpenInput;
type Output = ObservabilityIncidentOpenOutput;
const ID: &'static str = "observability.incident.open";
const CATEGORY: VerbCategory = VerbCategory::Observability;
}
pub struct ObservabilityIncidentClose;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ObservabilityIncidentCloseInput {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolution: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ObservabilityIncidentCloseOutput {
pub closed: bool,
}
impl InternalVerb for ObservabilityIncidentClose {
type Input = ObservabilityIncidentCloseInput;
type Output = ObservabilityIncidentCloseOutput;
const ID: &'static str = "observability.incident.close";
const CATEGORY: VerbCategory = VerbCategory::Observability;
}
pub struct ObservabilityDashboardSnapshot;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ObservabilityDashboardSnapshotInput {
pub dashboard_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time_range: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct ObservabilityDashboardSnapshotOutput {
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
}
impl InternalVerb for ObservabilityDashboardSnapshot {
type Input = ObservabilityDashboardSnapshotInput;
type Output = ObservabilityDashboardSnapshotOutput;
const ID: &'static str = "observability.dashboard.snapshot";
const CATEGORY: VerbCategory = VerbCategory::Observability;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verb_ids_match_canonical_namespace() {
let ids = [
ObservabilityAlertList::ID,
ObservabilityAlertAck::ID,
ObservabilityIncidentOpen::ID,
ObservabilityIncidentClose::ID,
ObservabilityDashboardSnapshot::ID,
];
for id in ids {
assert!(id.starts_with("observability."), "{id}");
}
}
#[test]
fn verbs_are_under_observability_category() {
assert_eq!(
ObservabilityAlertList::CATEGORY,
VerbCategory::Observability
);
assert_eq!(ObservabilityAlertAck::CATEGORY, VerbCategory::Observability);
assert_eq!(
ObservabilityIncidentOpen::CATEGORY,
VerbCategory::Observability
);
assert_eq!(
ObservabilityIncidentClose::CATEGORY,
VerbCategory::Observability
);
assert_eq!(
ObservabilityDashboardSnapshot::CATEGORY,
VerbCategory::Observability
);
}
#[test]
fn alert_list_input_defaults_active_only_true() {
let wire = r#"{"severity":"critical"}"#;
let parsed: ObservabilityAlertListInput = serde_json::from_str(wire).unwrap();
assert!(parsed.active_only, "active_only should default to true");
assert_eq!(parsed.severity.as_deref(), Some("critical"));
}
#[test]
fn alert_list_input_no_severity_returns_all() {
let wire = r#"{}"#;
let parsed: ObservabilityAlertListInput = serde_json::from_str(wire).unwrap();
assert!(parsed.severity.is_none());
assert!(parsed.active_only);
}
#[test]
fn alert_entry_round_trips() {
let entry = AlertEntry {
id: "a1".into(),
title: "CPU high".into(),
severity: "critical".into(),
state: "firing".into(),
started_at: Some("2026-06-06T00:00:00Z".into()),
detail: None,
};
let wire = serde_json::to_string(&entry).unwrap();
let back: AlertEntry = serde_json::from_str(&wire).unwrap();
assert_eq!(back.severity, "critical");
assert!(back.detail.is_none());
}
#[test]
fn incident_open_output_omits_url_when_absent() {
let out = ObservabilityIncidentOpenOutput {
id: "INC-1".into(),
url: None,
};
let wire = serde_json::to_value(&out).unwrap();
assert!(!wire.as_object().unwrap().contains_key("url"));
}
#[test]
fn dashboard_snapshot_time_range_optional() {
let no_range = r#"{"dashboard_id":"abc123"}"#;
let parsed: ObservabilityDashboardSnapshotInput = serde_json::from_str(no_range).unwrap();
assert!(parsed.time_range.is_none());
let with_range = r#"{"dashboard_id":"abc123","time_range":"last_24h"}"#;
let parsed: ObservabilityDashboardSnapshotInput = serde_json::from_str(with_range).unwrap();
assert_eq!(parsed.time_range.as_deref(), Some("last_24h"));
}
#[cfg(feature = "json-schema")]
#[test]
fn verbs_emit_schemas_via_for_verb() {
use super::super::VerbDescriptor;
let alert_list = VerbDescriptor::for_verb::<ObservabilityAlertList>();
assert_eq!(alert_list.id, "observability.alert.list");
assert!(alert_list.output_schema.to_string().contains("alerts"));
let snapshot = VerbDescriptor::for_verb::<ObservabilityDashboardSnapshot>();
assert_eq!(snapshot.id, "observability.dashboard.snapshot");
assert!(snapshot.output_schema.to_string().contains("url"));
}
}