feat(domain): add ModbusAddress type and HealthStatus enum
Implements T025-T027 from TDD workflow (red-green-refactor): - T025 (red): Tests for ModbusAddress with From<RelayId> conversion - T026 (green): ModbusAddress newtype (#[repr(transparent)]) with offset mapping - T027 (red+green): HealthStatus enum with state transitions ModbusAddress wraps u16 and converts user-facing relay IDs (1-8) to Modbus addresses (0-7) at the domain boundary. HealthStatus tracks relay health with Healthy, Degraded, and Unhealthy states supporting error tracking and recovery monitoring. Ref: T025, T026, T027 (specs/001-modbus-relay-control)
This commit is contained in:
295
backend/src/domain/health.rs
Normal file
295
backend/src/domain/health.rs
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
//! Health monitoring domain module.
|
||||||
|
//!
|
||||||
|
//! This module provides health status tracking for the Modbus relay controller.
|
||||||
|
//! It defines the `HealthStatus` enum which represents the current health state
|
||||||
|
//! of the system and supports transitions between healthy, degraded, and unhealthy states.
|
||||||
|
|
||||||
|
/// Health status of the Modbus relay controller.
|
||||||
|
///
|
||||||
|
/// Represents the three possible health states of the system:
|
||||||
|
/// - `Healthy`: System is operating normally
|
||||||
|
/// - `Degraded`: System is experiencing errors but still operational
|
||||||
|
/// - `Unhealthy`: System has critical failures
|
||||||
|
///
|
||||||
|
/// # State Transitions
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// Healthy ──(errors)──> Degraded ──(more errors)──> Unhealthy
|
||||||
|
/// ^ | |
|
||||||
|
/// └──────(recovery)───────┘ |
|
||||||
|
/// └────────────────(recovery)────────────────────────┘
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sta::domain::health::HealthStatus;
|
||||||
|
///
|
||||||
|
/// let status = HealthStatus::Healthy;
|
||||||
|
/// assert!(matches!(status, HealthStatus::Healthy));
|
||||||
|
///
|
||||||
|
/// let degraded = HealthStatus::Degraded { consecutive_errors: 3 };
|
||||||
|
/// assert!(matches!(degraded, HealthStatus::Degraded { .. }));
|
||||||
|
///
|
||||||
|
/// let unhealthy = HealthStatus::Unhealthy {
|
||||||
|
/// reason: "Connection timeout".to_string()
|
||||||
|
/// };
|
||||||
|
/// assert!(matches!(unhealthy, HealthStatus::Unhealthy { .. }));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum HealthStatus {
|
||||||
|
/// System is operating normally with no errors.
|
||||||
|
Healthy,
|
||||||
|
|
||||||
|
/// System is experiencing errors but still operational.
|
||||||
|
///
|
||||||
|
/// The `consecutive_errors` field tracks how many errors have occurred
|
||||||
|
/// without recovery.
|
||||||
|
Degraded {
|
||||||
|
/// Number of consecutive errors without recovery.
|
||||||
|
consecutive_errors: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// System has critical failures and is not operational.
|
||||||
|
///
|
||||||
|
/// The `reason` field provides a human-readable description of the failure.
|
||||||
|
Unhealthy {
|
||||||
|
/// Human-readable description of the failure reason.
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HealthStatus {
|
||||||
|
/// Creates a new `Healthy` status.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn healthy() -> Self {
|
||||||
|
Self::Healthy
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `Degraded` status with the given error count.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn degraded(consecutive_errors: u32) -> Self {
|
||||||
|
Self::Degraded { consecutive_errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `Unhealthy` status with the given reason.
|
||||||
|
#[must_use]
|
||||||
|
pub fn unhealthy(reason: impl Into<String>) -> Self {
|
||||||
|
Self::Unhealthy {
|
||||||
|
reason: reason.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the status is `Healthy`.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_healthy(&self) -> bool {
|
||||||
|
matches!(self, Self::Healthy)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the status is `Degraded`.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_degraded(&self) -> bool {
|
||||||
|
matches!(self, Self::Degraded { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the status is `Unhealthy`.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_unhealthy(&self) -> bool {
|
||||||
|
matches!(self, Self::Unhealthy { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transitions to a degraded state by incrementing the error count.
|
||||||
|
///
|
||||||
|
/// If already degraded, increments the consecutive errors.
|
||||||
|
/// If healthy, transitions to degraded with 1 error.
|
||||||
|
/// If unhealthy, remains unhealthy.
|
||||||
|
#[must_use]
|
||||||
|
pub fn record_error(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Healthy => Self::Degraded { consecutive_errors: 1 },
|
||||||
|
Self::Degraded { consecutive_errors } => {
|
||||||
|
Self::Degraded {
|
||||||
|
consecutive_errors: consecutive_errors + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Unhealthy { reason } => Self::Unhealthy { reason },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transitions to a healthy state, resetting all error counts.
|
||||||
|
#[must_use]
|
||||||
|
pub fn record_success(self) -> Self {
|
||||||
|
Self::Healthy
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transitions to unhealthy state with the given reason.
|
||||||
|
#[must_use]
|
||||||
|
pub fn mark_unhealthy(self, reason: impl Into<String>) -> Self {
|
||||||
|
Self::Unhealthy {
|
||||||
|
reason: reason.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for HealthStatus {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Healthy => write!(f, "Healthy"),
|
||||||
|
Self::Degraded { consecutive_errors } => {
|
||||||
|
write!(f, "Degraded ({consecutive_errors} consecutive errors)")
|
||||||
|
}
|
||||||
|
Self::Unhealthy { reason } => write!(f, "Unhealthy: {reason}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_healthy_status_creation() {
|
||||||
|
let status = HealthStatus::healthy();
|
||||||
|
assert!(status.is_healthy());
|
||||||
|
assert!(!status.is_degraded());
|
||||||
|
assert!(!status.is_unhealthy());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_degraded_status_creation() {
|
||||||
|
let status = HealthStatus::degraded(3);
|
||||||
|
assert!(!status.is_healthy());
|
||||||
|
assert!(status.is_degraded());
|
||||||
|
assert!(!status.is_unhealthy());
|
||||||
|
assert_eq!(status, HealthStatus::Degraded { consecutive_errors: 3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unhealthy_status_creation() {
|
||||||
|
let status = HealthStatus::unhealthy("Connection timeout");
|
||||||
|
assert!(!status.is_healthy());
|
||||||
|
assert!(!status.is_degraded());
|
||||||
|
assert!(status.is_unhealthy());
|
||||||
|
assert_eq!(
|
||||||
|
status,
|
||||||
|
HealthStatus::Unhealthy {
|
||||||
|
reason: "Connection timeout".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transition_healthy_to_degraded() {
|
||||||
|
let status = HealthStatus::healthy();
|
||||||
|
let status = status.record_error();
|
||||||
|
assert!(status.is_degraded());
|
||||||
|
assert_eq!(status, HealthStatus::Degraded { consecutive_errors: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transition_degraded_increments_errors() {
|
||||||
|
let status = HealthStatus::degraded(2);
|
||||||
|
let status = status.record_error();
|
||||||
|
assert!(status.is_degraded());
|
||||||
|
assert_eq!(status, HealthStatus::Degraded { consecutive_errors: 3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transition_unhealthy_stays_unhealthy_on_error() {
|
||||||
|
let status = HealthStatus::unhealthy("Device disconnected");
|
||||||
|
let status = status.record_error();
|
||||||
|
assert!(status.is_unhealthy());
|
||||||
|
assert_eq!(
|
||||||
|
status,
|
||||||
|
HealthStatus::Unhealthy {
|
||||||
|
reason: "Device disconnected".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transition_healthy_to_unhealthy() {
|
||||||
|
let status = HealthStatus::healthy();
|
||||||
|
let status = status.mark_unhealthy("Critical failure");
|
||||||
|
assert!(status.is_unhealthy());
|
||||||
|
assert_eq!(
|
||||||
|
status,
|
||||||
|
HealthStatus::Unhealthy {
|
||||||
|
reason: "Critical failure".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transition_degraded_to_unhealthy() {
|
||||||
|
let status = HealthStatus::degraded(5);
|
||||||
|
let status = status.mark_unhealthy("Too many errors");
|
||||||
|
assert!(status.is_unhealthy());
|
||||||
|
assert_eq!(
|
||||||
|
status,
|
||||||
|
HealthStatus::Unhealthy {
|
||||||
|
reason: "Too many errors".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transition_degraded_to_healthy() {
|
||||||
|
let status = HealthStatus::degraded(3);
|
||||||
|
let status = status.record_success();
|
||||||
|
assert!(status.is_healthy());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transition_unhealthy_to_healthy() {
|
||||||
|
let status = HealthStatus::unhealthy("Device offline");
|
||||||
|
let status = status.record_success();
|
||||||
|
assert!(status.is_healthy());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display_healthy() {
|
||||||
|
let status = HealthStatus::healthy();
|
||||||
|
assert_eq!(status.to_string(), "Healthy");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display_degraded() {
|
||||||
|
let status = HealthStatus::degraded(5);
|
||||||
|
assert_eq!(status.to_string(), "Degraded (5 consecutive errors)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display_unhealthy() {
|
||||||
|
let status = HealthStatus::unhealthy("Connection lost");
|
||||||
|
assert_eq!(status.to_string(), "Unhealthy: Connection lost");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiple_transitions() {
|
||||||
|
// Start healthy
|
||||||
|
let status = HealthStatus::healthy();
|
||||||
|
assert!(status.is_healthy());
|
||||||
|
|
||||||
|
// Record first error -> Degraded with 1 error
|
||||||
|
let status = status.record_error();
|
||||||
|
assert!(status.is_degraded());
|
||||||
|
assert_eq!(status, HealthStatus::Degraded { consecutive_errors: 1 });
|
||||||
|
|
||||||
|
// Record second error -> Degraded with 2 errors
|
||||||
|
let status = status.record_error();
|
||||||
|
assert_eq!(status, HealthStatus::Degraded { consecutive_errors: 2 });
|
||||||
|
|
||||||
|
// Record third error -> Degraded with 3 errors
|
||||||
|
let status = status.record_error();
|
||||||
|
assert_eq!(status, HealthStatus::Degraded { consecutive_errors: 3 });
|
||||||
|
|
||||||
|
// Mark unhealthy -> Unhealthy
|
||||||
|
let status = status.mark_unhealthy("Too many consecutive errors");
|
||||||
|
assert!(status.is_unhealthy());
|
||||||
|
|
||||||
|
// Recover -> Healthy
|
||||||
|
let status = status.record_success();
|
||||||
|
assert!(status.is_healthy());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,3 +36,5 @@
|
|||||||
//! - Domain specification: `specs/001-modbus-relay-control/spec.md`
|
//! - Domain specification: `specs/001-modbus-relay-control/spec.md`
|
||||||
|
|
||||||
pub mod relay;
|
pub mod relay;
|
||||||
|
pub mod modbus;
|
||||||
|
pub mod health;
|
||||||
|
|||||||
89
backend/src/domain/modbus.rs
Normal file
89
backend/src/domain/modbus.rs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
//! Modbus domain module.
|
||||||
|
//!
|
||||||
|
//! This module contains Modbus-specific domain types and conversions.
|
||||||
|
//! It provides a clean abstraction layer between user-facing relay IDs
|
||||||
|
//! and Modbus protocol addresses.
|
||||||
|
|
||||||
|
use super::relay::types::RelayId;
|
||||||
|
|
||||||
|
/// Modbus address newtype wrapping u16.
|
||||||
|
///
|
||||||
|
/// Represents a Modbus coil address (0-based indexing).
|
||||||
|
/// User-facing relay IDs (1-8) are converted to Modbus addresses (0-7).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sta::domain::modbus::ModbusAddress;
|
||||||
|
/// use sta::domain::relay::types::RelayId;
|
||||||
|
///
|
||||||
|
/// let relay_id = RelayId::new(1).unwrap();
|
||||||
|
/// let modbus_addr = ModbusAddress::from(relay_id);
|
||||||
|
/// assert_eq!(modbus_addr.as_u16(), 0);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct ModbusAddress(u16);
|
||||||
|
|
||||||
|
impl ModbusAddress {
|
||||||
|
/// Returns the inner u16 value.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_u16(self) -> u16 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a user-facing `RelayId` (1-8) to a Modbus address (0-7).
|
||||||
|
///
|
||||||
|
/// # Offset Calculation
|
||||||
|
///
|
||||||
|
/// Modbus uses 0-based addressing while our user-facing interface uses
|
||||||
|
/// 1-based relay numbering. This conversion applies the -1 offset.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sta::domain::modbus::ModbusAddress;
|
||||||
|
/// use sta::domain::relay::types::RelayId;
|
||||||
|
///
|
||||||
|
/// let relay1 = RelayId::new(1).unwrap();
|
||||||
|
/// assert_eq!(ModbusAddress::from(relay1).as_u16(), 0);
|
||||||
|
///
|
||||||
|
/// let relay8 = RelayId::new(8).unwrap();
|
||||||
|
/// assert_eq!(ModbusAddress::from(relay8).as_u16(), 7);
|
||||||
|
/// ```
|
||||||
|
impl From<RelayId> for ModbusAddress {
|
||||||
|
fn from(relay_id: RelayId) -> Self {
|
||||||
|
// RelayId is guaranteed to be 1..=8 by its validation
|
||||||
|
// Convert to 0..=7 for Modbus addressing
|
||||||
|
Self(u16::from(relay_id.as_u8() - 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relay_id_1_converts_to_modbus_address_0() {
|
||||||
|
let relay_id = RelayId::new(1).unwrap();
|
||||||
|
let modbus_addr = ModbusAddress::from(relay_id);
|
||||||
|
assert_eq!(modbus_addr.as_u16(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relay_id_8_converts_to_modbus_address_7() {
|
||||||
|
let relay_id = RelayId::new(8).unwrap();
|
||||||
|
let modbus_addr = ModbusAddress::from(relay_id);
|
||||||
|
assert_eq!(modbus_addr.as_u16(), 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_all_relay_ids_convert_correctly() {
|
||||||
|
for i in 1..=8 {
|
||||||
|
let relay_id = RelayId::new(i).unwrap();
|
||||||
|
let modbus_addr = ModbusAddress::from(relay_id);
|
||||||
|
assert_eq!(modbus_addr.as_u16(), u16::from(i - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// Human-readable label for a relay.
|
/// Human-readable label for a relay.
|
||||||
@@ -45,3 +47,60 @@ impl Default for RelayLabel {
|
|||||||
Self(String::from("Unlabeled"))
|
Self(String::from("Unlabeled"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RelayLabel {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relay_label_new_valid_string() {
|
||||||
|
// Test: RelayLabel::new("Pump") → Ok
|
||||||
|
let result = RelayLabel::new("Pump".to_string());
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(result.unwrap().as_str(), "Pump");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relay_label_new_valid_max_length() {
|
||||||
|
// Test: RelayLabel::new("A".repeat(50)) → Ok
|
||||||
|
let label_str = "A".repeat(50);
|
||||||
|
let result = RelayLabel::new(label_str.clone());
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(result.unwrap().as_str(), &label_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relay_label_new_empty_fails() {
|
||||||
|
// Test: RelayLabel::new("") → Err(EmptyLabel)
|
||||||
|
let result = RelayLabel::new(String::new());
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), RelayLabelError::Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relay_label_new_too_long_fails() {
|
||||||
|
// Test: RelayLabel::new("A".repeat(51)) → Err(LabelTooLong)
|
||||||
|
let label_str = "A".repeat(51);
|
||||||
|
let result = RelayLabel::new(label_str);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), RelayLabelError::TooLong(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relay_label_default() {
|
||||||
|
let label = RelayLabel::default();
|
||||||
|
assert_eq!(label.as_str(), "Unlabeled");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relay_label_display() {
|
||||||
|
let label = RelayLabel::new("Test Label".to_string()).unwrap();
|
||||||
|
assert_eq!(format!("{label}"), "Test Label");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -260,19 +260,19 @@
|
|||||||
- **File**: src/domain/relay.rs
|
- **File**: src/domain/relay.rs
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T025** [US1] [TDD] Write tests for ModbusAddress type
|
- [x] **T025** [US1] [TDD] Write tests for ModbusAddress type
|
||||||
- Test: ModbusAddress::from(RelayId(1)) → ModbusAddress(0)
|
- Test: ModbusAddress::from(RelayId(1)) → ModbusAddress(0)
|
||||||
- Test: ModbusAddress::from(RelayId(8)) → ModbusAddress(7)
|
- Test: ModbusAddress::from(RelayId(8)) → ModbusAddress(7)
|
||||||
- **File**: src/domain/modbus.rs
|
- **File**: src/domain/modbus.rs
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T026** [US1] [TDD] Implement ModbusAddress type with From<RelayId>
|
- [x] **T026** [US1] [TDD] Implement ModbusAddress type with From<RelayId>
|
||||||
- #[repr(transparent)] newtype wrapping u16
|
- #[repr(transparent)] newtype wrapping u16
|
||||||
- Implement From<RelayId> with offset: user 1-8 → Modbus 0-7
|
- Implement From<RelayId> with offset: user 1-8 → Modbus 0-7
|
||||||
- **File**: src/domain/modbus.rs
|
- **File**: src/domain/modbus.rs
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T027** [US3] [TDD] Write tests and implement HealthStatus enum
|
- [x] **T027** [US3] [TDD] Write tests and implement HealthStatus enum
|
||||||
- Enum: Healthy, Degraded { consecutive_errors: u32 }, Unhealthy { reason: String }
|
- Enum: Healthy, Degraded { consecutive_errors: u32 }, Unhealthy { reason: String }
|
||||||
- Test transitions between states
|
- Test transitions between states
|
||||||
- **File**: src/domain/health.rs
|
- **File**: src/domain/health.rs
|
||||||
|
|||||||
Reference in New Issue
Block a user