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:
2026-01-03 23:44:38 +01:00
parent 6fc1fb834c
commit 7e10823714
5 changed files with 448 additions and 3 deletions

View File

@@ -1,3 +1,5 @@
use std::fmt::Display;
use thiserror::Error;
/// Human-readable label for a relay.
@@ -45,3 +47,60 @@ impl Default for RelayLabel {
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");
}
}