//! Notifications service — pipeline-event fan-out to chat transports. //! //! Subscribes to [`WatcherEvent`] broadcasts and posts human-readable messages //! to all configured chat rooms whenever a work item moves through the pipeline. //! //! Follows service-module conventions: //! - `mod.rs` (this file) — public API, typed [`Error`] type, orchestration //! - `io/` — the ONLY place that performs side effects (DB reads, config //! loads, `tokio::spawn`) //! - `format.rs` — pure: message formatting functions //! - `filter.rs` — pure: debounce constants and suppression predicates //! - `events.rs` — pure: WatcherEvent classification / event mapping //! - `route.rs` — pure: room-routing decisions pub(super) mod events; pub(super) mod filter; pub(super) mod format; pub(super) mod io; pub(super) mod route; pub use format::{ format_blocked_notification, format_disk_recovery_notification, format_disk_warning_notification, format_error_notification, format_stage_notification, }; pub use io::resolve_project_display_name; pub use io::spawn_notification_listener; pub use io::spawn_stage_notification_subscriber; // ── Error type ──────────────────────────────────────────────────────────────── /// Typed errors returned by `service::notifications` operations. /// /// HTTP handlers and bot commands may map these to user-facing messages. #[derive(Debug)] #[allow(dead_code)] pub enum Error { /// The incoming event type is not recognised or not supported. UnknownEvent(String), /// A message could not be formatted for delivery (e.g. malformed input). RenderFailure(String), /// The underlying chat transport rejected the send operation. TransportSendFailure(String), /// Required configuration (room IDs, credentials) is absent. ConfigMissing(String), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::UnknownEvent(msg) => write!(f, "Unknown event: {msg}"), Self::RenderFailure(msg) => write!(f, "Render failure: {msg}"), Self::TransportSendFailure(msg) => write!(f, "Transport send failure: {msg}"), Self::ConfigMissing(msg) => write!(f, "Config missing: {msg}"), } } } #[cfg(test)] mod tests { use super::*; // ── Error Display ───────────────────────────────────────────────────────── #[test] fn error_unknown_event_display() { let e = Error::UnknownEvent("bad_event_type".to_string()); assert!(e.to_string().contains("Unknown event")); assert!(e.to_string().contains("bad_event_type")); } #[test] fn error_render_failure_display() { let e = Error::RenderFailure("malformed input".to_string()); assert!(e.to_string().contains("Render failure")); assert!(e.to_string().contains("malformed input")); } #[test] fn error_transport_send_failure_display() { let e = Error::TransportSendFailure("connection refused".to_string()); assert!(e.to_string().contains("Transport send failure")); assert!(e.to_string().contains("connection refused")); } #[test] fn error_config_missing_display() { let e = Error::ConfigMissing("room_id not set".to_string()); assert!(e.to_string().contains("Config missing")); assert!(e.to_string().contains("room_id not set")); } }