test(infrastructure): implement MockRelayLabelRepository for testing
Create in-memory mock implementation of RelayLabelRepository trait using HashMap with Arc<Mutex<>> for thread-safe concurrent access. Includes 8 comprehensive tests covering all trait methods and edge cases. Also refactor domain repository module structure to support multiple repository types (repository.rs → repository/label.rs + mod.rs). TDD phase: Combined red-green (tests + implementation) Ref: T037, T038
This commit is contained in:
@@ -1,20 +1,8 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use super::types::{RelayId, RelayLabel};
|
use crate::domain::relay::types::{RelayId, RelayLabel};
|
||||||
|
|
||||||
/// Errors that can occur during repository operations.
|
use super::RepositoryError;
|
||||||
///
|
|
||||||
/// This enum provides structured error handling for all data persistence
|
|
||||||
/// operations related to relay management.
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum RepositoryError {
|
|
||||||
/// A database operation failed with the given error message.
|
|
||||||
#[error("Database error: {0}")]
|
|
||||||
DatabaseError(String),
|
|
||||||
/// The requested relay was not found in the repository.
|
|
||||||
#[error("Relay not found: {0}")]
|
|
||||||
NotFound(RelayId),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Repository trait for persisting and retrieving relay labels.
|
/// Repository trait for persisting and retrieving relay labels.
|
||||||
///
|
///
|
||||||
18
backend/src/domain/relay/repository/mod.rs
Normal file
18
backend/src/domain/relay/repository/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
mod label;
|
||||||
|
pub use label::RelayLabelRepository;
|
||||||
|
|
||||||
|
use super::types::RelayId;
|
||||||
|
|
||||||
|
/// Errors that can occur during repository operations.
|
||||||
|
///
|
||||||
|
/// This enum provides structured error handling for all data persistence
|
||||||
|
/// operations related to relay management.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum RepositoryError {
|
||||||
|
/// A database operation failed with the given error message.
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
DatabaseError(String),
|
||||||
|
/// The requested relay was not found in the repository.
|
||||||
|
#[error("Relay not found: {0}")]
|
||||||
|
NotFound(RelayId),
|
||||||
|
}
|
||||||
237
backend/src/infrastructure/persistence/label_repository.rs
Normal file
237
backend/src/infrastructure/persistence/label_repository.rs
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
//! Mock implementation and tests for `RelayLabelRepository` trait.
|
||||||
|
//!
|
||||||
|
//! This module provides a simple in-memory mock implementation of the
|
||||||
|
//! `RelayLabelRepository` trait for testing purposes, along with comprehensive
|
||||||
|
//! tests that verify the trait's contract.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use tokio::sync::{Mutex, MutexGuard};
|
||||||
|
|
||||||
|
use crate::domain::relay::{
|
||||||
|
repository::{RelayLabelRepository, RepositoryError},
|
||||||
|
types::{RelayId, RelayLabel},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// In-memory mock implementation of `RelayLabelRepository` for testing.
|
||||||
|
///
|
||||||
|
/// This implementation uses a `HashMap` wrapped in `Arc<Mutex<_>>` to provide
|
||||||
|
/// thread-safe concurrent access to relay labels. It's useful for testing
|
||||||
|
/// application logic without requiring a database connection.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MockRelayLabelRepository {
|
||||||
|
/// Internal storage for relay labels, protected by a mutex for thread safety.
|
||||||
|
labels: Arc<Mutex<HashMap<u8, RelayLabel>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockRelayLabelRepository {
|
||||||
|
/// Creates a new empty mock repository.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
labels: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn labels(&self) -> MutexGuard<'_, HashMap<u8, RelayLabel>> {
|
||||||
|
self.labels.lock().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MockRelayLabelRepository {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RelayLabelRepository for MockRelayLabelRepository {
|
||||||
|
async fn get_label(&self, id: RelayId) -> Result<Option<RelayLabel>, RepositoryError> {
|
||||||
|
Ok(self.labels().await.get(&id.as_u8()).cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_label(&self, id: RelayId, label: RelayLabel) -> Result<(), RepositoryError> {
|
||||||
|
self.labels().await.insert(id.as_u8(), label);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_all_labels(&self) -> Result<Vec<(RelayId, RelayLabel)>, RepositoryError> {
|
||||||
|
let mut result: Vec<(RelayId, RelayLabel)> = self
|
||||||
|
.labels()
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(&id, label)| {
|
||||||
|
RelayId::new(id).map_or(None, |relay_id| Some((relay_id, label.clone())))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Sort by relay ID for consistent ordering
|
||||||
|
result.sort_by_key(|(id, _)| id.as_u8());
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_label_returns_none_for_non_existent_relay() {
|
||||||
|
// Test: get_label(RelayId(1)) → None when no label is set
|
||||||
|
let repo = MockRelayLabelRepository::new();
|
||||||
|
let relay_id = RelayId::new(1).unwrap();
|
||||||
|
|
||||||
|
let result = repo.get_label(relay_id).await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(result.unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_save_label_and_get_label_returns_saved_label() {
|
||||||
|
// Test: save_label(RelayId(1), "Pump") then get_label(RelayId(1)) → Some("Pump")
|
||||||
|
let repo = MockRelayLabelRepository::new();
|
||||||
|
let relay_id = RelayId::new(1).unwrap();
|
||||||
|
let label = RelayLabel::new("Pump".to_string()).unwrap();
|
||||||
|
|
||||||
|
// Save the label
|
||||||
|
let save_result = repo.save_label(relay_id, label.clone()).await;
|
||||||
|
assert!(save_result.is_ok());
|
||||||
|
|
||||||
|
// Retrieve the label
|
||||||
|
let get_result = repo.get_label(relay_id).await;
|
||||||
|
assert!(get_result.is_ok());
|
||||||
|
|
||||||
|
let retrieved_label = get_result.unwrap();
|
||||||
|
assert!(retrieved_label.is_some());
|
||||||
|
assert_eq!(retrieved_label.unwrap().as_str(), "Pump");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_save_label_overwrites_existing_label() {
|
||||||
|
// Test: save_label twice with different values, get_label returns the latest
|
||||||
|
let repo = MockRelayLabelRepository::new();
|
||||||
|
let relay_id = RelayId::new(1).unwrap();
|
||||||
|
let label1 = RelayLabel::new("First".to_string()).unwrap();
|
||||||
|
let label2 = RelayLabel::new("Second".to_string()).unwrap();
|
||||||
|
|
||||||
|
// Save first label
|
||||||
|
repo.save_label(relay_id, label1).await.unwrap();
|
||||||
|
|
||||||
|
// Overwrite with second label
|
||||||
|
repo.save_label(relay_id, label2).await.unwrap();
|
||||||
|
|
||||||
|
// Retrieve should return the second label
|
||||||
|
let retrieved_label = repo.get_label(relay_id).await.unwrap();
|
||||||
|
assert!(retrieved_label.is_some());
|
||||||
|
assert_eq!(retrieved_label.unwrap().as_str(), "Second");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_all_labels_returns_empty_vec_when_no_labels() {
|
||||||
|
// Test: get_all_labels() → [] when repository is empty
|
||||||
|
let repo = MockRelayLabelRepository::new();
|
||||||
|
|
||||||
|
let result = repo.get_all_labels().await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(result.unwrap().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_all_labels_returns_all_saved_labels() {
|
||||||
|
// Test: save multiple labels, get_all_labels() returns all of them
|
||||||
|
let repo = MockRelayLabelRepository::new();
|
||||||
|
|
||||||
|
let relay1 = RelayId::new(1).unwrap();
|
||||||
|
let relay3 = RelayId::new(3).unwrap();
|
||||||
|
let relay5 = RelayId::new(5).unwrap();
|
||||||
|
|
||||||
|
let label1 = RelayLabel::new("Pump".to_string()).unwrap();
|
||||||
|
let label3 = RelayLabel::new("Heater".to_string()).unwrap();
|
||||||
|
let label5 = RelayLabel::new("Fan".to_string()).unwrap();
|
||||||
|
|
||||||
|
// Save labels
|
||||||
|
repo.save_label(relay1, label1.clone()).await.unwrap();
|
||||||
|
repo.save_label(relay3, label3.clone()).await.unwrap();
|
||||||
|
repo.save_label(relay5, label5.clone()).await.unwrap();
|
||||||
|
|
||||||
|
// Retrieve all labels
|
||||||
|
let result = repo.get_all_labels().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 3);
|
||||||
|
|
||||||
|
// Verify labels are sorted by relay ID
|
||||||
|
assert_eq!(result[0].0.as_u8(), 1);
|
||||||
|
assert_eq!(result[0].1.as_str(), "Pump");
|
||||||
|
|
||||||
|
assert_eq!(result[1].0.as_u8(), 3);
|
||||||
|
assert_eq!(result[1].1.as_str(), "Heater");
|
||||||
|
|
||||||
|
assert_eq!(result[2].0.as_u8(), 5);
|
||||||
|
assert_eq!(result[2].1.as_str(), "Fan");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_all_labels_excludes_relays_without_labels() {
|
||||||
|
// Test: Only relays with labels are returned, not all possible relay IDs
|
||||||
|
let repo = MockRelayLabelRepository::new();
|
||||||
|
|
||||||
|
let relay2 = RelayId::new(2).unwrap();
|
||||||
|
let label2 = RelayLabel::new("Only This One".to_string()).unwrap();
|
||||||
|
|
||||||
|
repo.save_label(relay2, label2).await.unwrap();
|
||||||
|
|
||||||
|
let result = repo.get_all_labels().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
assert_eq!(result[0].0.as_u8(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_save_label_for_all_valid_relay_ids() {
|
||||||
|
// Test: All relay IDs (1-8) can have labels saved
|
||||||
|
let repo = MockRelayLabelRepository::new();
|
||||||
|
|
||||||
|
for id in 1..=8 {
|
||||||
|
let relay_id = RelayId::new(id).unwrap();
|
||||||
|
let label = RelayLabel::new(format!("Relay {id}")).unwrap();
|
||||||
|
|
||||||
|
let result = repo.save_label(relay_id, label).await;
|
||||||
|
assert!(result.is_ok(), "Failed to save label for relay {id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all labels were saved
|
||||||
|
let all_labels = repo.get_all_labels().await.unwrap();
|
||||||
|
assert_eq!(all_labels.len(), 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_concurrent_access_is_thread_safe() {
|
||||||
|
// Test: Multiple concurrent operations don't cause data races
|
||||||
|
let repo = MockRelayLabelRepository::new();
|
||||||
|
|
||||||
|
let handles: Vec<_> = (1..=8)
|
||||||
|
.map(|id| {
|
||||||
|
let repo = repo.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let relay_id = RelayId::new(id).unwrap();
|
||||||
|
let label = RelayLabel::new(format!("Relay {id}")).unwrap();
|
||||||
|
repo.save_label(relay_id, label).await.unwrap();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Wait for all tasks to complete
|
||||||
|
for handle in handles {
|
||||||
|
handle.await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all labels were saved correctly
|
||||||
|
let all_labels = repo.get_all_labels().await.unwrap();
|
||||||
|
assert_eq!(all_labels.len(), 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,5 +3,8 @@
|
|||||||
//! This module contains the concrete implementations of repository traits
|
//! This module contains the concrete implementations of repository traits
|
||||||
//! for data persistence, including SQLite-based storage for relay labels.
|
//! for data persistence, including SQLite-based storage for relay labels.
|
||||||
|
|
||||||
|
/// Mock repository implementation for testing.
|
||||||
|
pub mod label_repository;
|
||||||
|
|
||||||
/// `SQLite` repository implementation for relay labels.
|
/// `SQLite` repository implementation for relay labels.
|
||||||
pub mod sqlite_repository;
|
pub mod sqlite_repository;
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
- Test: CorsSettings with wildcard origin deserializes correctly ✓
|
- Test: CorsSettings with wildcard origin deserializes correctly ✓
|
||||||
- Test: Settings::new() loads cors section from development.yaml ✓
|
- Test: Settings::new() loads cors section from development.yaml ✓
|
||||||
- Test: CorsSettings with partial fields uses defaults ✓
|
- Test: CorsSettings with partial fields uses defaults ✓
|
||||||
- **File**: backend/src/settings.rs (in tests module)
|
- **File**: `backend/src/settings.rs (in tests module)`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
- **Tests Written**: 5 tests (cors_settings_deserialize_from_yaml, cors_settings_default_has_empty_origins, cors_settings_with_wildcard_deserializes, settings_loads_cors_section_from_yaml, cors_settings_deserialize_with_defaults)
|
- **Tests Written**: 5 tests (cors_settings_deserialize_from_yaml, cors_settings_default_has_empty_origins, cors_settings_with_wildcard_deserializes, settings_loads_cors_section_from_yaml, cors_settings_deserialize_with_defaults)
|
||||||
|
|
||||||
@@ -86,14 +86,14 @@
|
|||||||
- Add `#[derive(Debug, serde::Deserialize, Clone)]` to struct
|
- Add `#[derive(Debug, serde::Deserialize, Clone)]` to struct
|
||||||
- Add `#[serde(default)]` attribute to Settings.cors field
|
- Add `#[serde(default)]` attribute to Settings.cors field
|
||||||
- Update Settings struct to include `pub cors: CorsSettings`
|
- Update Settings struct to include `pub cors: CorsSettings`
|
||||||
- **File**: backend/src/settings.rs
|
- **File**: `backend/src/settings.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T011** [Setup] [TDD] Update development.yaml with permissive CORS settings
|
- [x] **T011** [Setup] [TDD] Update development.yaml with permissive CORS settings
|
||||||
- Add cors section with `allowed_origins: ["*"]`, `allow_credentials: false`, `max_age_secs: 3600`
|
- Add cors section with `allowed_origins: ["*"]`, `allow_credentials: false`, `max_age_secs: 3600`
|
||||||
- Update `frontend_url` from `http://localhost:3000` to `http://localhost:5173` (Vite default port)
|
- Update `frontend_url` from `http://localhost:3000` to `http://localhost:5173` (Vite default port)
|
||||||
- **Test**: cargo run loads development config without errors
|
- **Test**: cargo run loads development config without errors
|
||||||
- **File**: backend/settings/development.yaml
|
- **File**: `backend/settings/development.yaml`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T012** [P] [Setup] [TDD] Create production.yaml with restrictive CORS settings
|
- [x] **T012** [P] [Setup] [TDD] Create production.yaml with restrictive CORS settings
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
- Add `frontend_url: "https://REDACTED"`
|
- Add `frontend_url: "https://REDACTED"`
|
||||||
- Add production-specific application settings (protocol: https, host: 0.0.0.0)
|
- Add production-specific application settings (protocol: https, host: 0.0.0.0)
|
||||||
- **Test**: Settings::new() with APP_ENVIRONMENT=production loads config
|
- **Test**: Settings::new() with APP_ENVIRONMENT=production loads config
|
||||||
- **File**: backend/settings/production.yaml
|
- **File**: `backend/settings/production.yaml`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T013** [Setup] [TDD] Write tests for build_cors() function
|
- [x] **T013** [Setup] [TDD] Write tests for build_cors() function
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
- Test: build_cors() sets correct methods (GET, POST, PUT, PATCH, DELETE, OPTIONS) ✓
|
- Test: build_cors() sets correct methods (GET, POST, PUT, PATCH, DELETE, OPTIONS) ✓
|
||||||
- Test: build_cors() sets correct headers (content-type, authorization) ✓
|
- Test: build_cors() sets correct headers (content-type, authorization) ✓
|
||||||
- Test: build_cors() sets max_age from settings ✓
|
- Test: build_cors() sets max_age from settings ✓
|
||||||
- **File**: backend/src/startup.rs (in tests module)
|
- **File**: `backend/src/startup.rs (in tests module)`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T014** [Setup] [TDD] Implement build_cors() free function in startup.rs
|
- [x] **T014** [Setup] [TDD] Implement build_cors() free function in startup.rs
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
- Set `allow_credentials` from settings
|
- Set `allow_credentials` from settings
|
||||||
- Set `max_age` from settings.max_age_secs
|
- Set `max_age` from settings.max_age_secs
|
||||||
- Add structured logging: `tracing::info!(allowed_origins = ?settings.allowed_origins, allow_credentials = settings.allow_credentials, "CORS middleware configured")`
|
- Add structured logging: `tracing::info!(allowed_origins = ?settings.allowed_origins, allow_credentials = settings.allow_credentials, "CORS middleware configured")`
|
||||||
- **File**: backend/src/startup.rs
|
- **File**: `backend/src/startup.rs`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
- In `From<Application> for RunnableApplication`, replace `.with(Cors::new())` with `.with(Cors::from(value.settings.cors.clone()))` ✓
|
- In `From<Application> for RunnableApplication`, replace `.with(Cors::new())` with `.with(Cors::from(value.settings.cors.clone()))` ✓
|
||||||
- CORS is applied after rate limiting (order: RateLimit → CORS → Data) ✓
|
- CORS is applied after rate limiting (order: RateLimit → CORS → Data) ✓
|
||||||
- **Test**: Unit test verifies CORS middleware uses settings ✓
|
- **Test**: Unit test verifies CORS middleware uses settings ✓
|
||||||
- **File**: backend/src/startup.rs (line 84)
|
- **File**: `backend/src/startup.rs (line 84)`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
- **Note**: Used `From<CorsSettings> for Cors` trait instead of `build_cors()` function (better design pattern)
|
- **Note**: Used `From<CorsSettings> for Cors` trait instead of `build_cors()` function (better design pattern)
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
- Test: Multiple origins are supported ✓
|
- Test: Multiple origins are supported ✓
|
||||||
- Test: Unauthorized origins are rejected with 403 ✓
|
- Test: Unauthorized origins are rejected with 403 ✓
|
||||||
- Test: Credentials disabled by default ✓
|
- Test: Credentials disabled by default ✓
|
||||||
- **File**: backend/tests/cors_test.rs (9 integration tests)
|
- **File**: `backend/tests/cors_test.rs (9 integration tests)`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
- **Tests Written**: 9 comprehensive integration tests covering all CORS scenarios
|
- **Tests Written**: 9 comprehensive integration tests covering all CORS scenarios
|
||||||
|
|
||||||
@@ -208,27 +208,27 @@
|
|||||||
- Test: RelayId::new(0) → Err(InvalidRelayId)
|
- Test: RelayId::new(0) → Err(InvalidRelayId)
|
||||||
- Test: RelayId::new(9) → Err(InvalidRelayId)
|
- Test: RelayId::new(9) → Err(InvalidRelayId)
|
||||||
- Test: RelayId::as_u8() returns inner value
|
- Test: RelayId::as_u8() returns inner value
|
||||||
- **File**: src/domain/relay.rs
|
- **File**: `src/domain/relay.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T018** [US1] [TDD] Implement RelayId newtype with validation
|
- [x] **T018** [US1] [TDD] Implement RelayId newtype with validation
|
||||||
- #[repr(transparent)] newtype wrapping u8
|
- #[repr(transparent)] newtype wrapping u8
|
||||||
- Constructor validates 1..=8 range
|
- Constructor validates 1..=8 range
|
||||||
- Implement Display, Debug, Clone, Copy, PartialEq, Eq
|
- Implement Display, Debug, Clone, Copy, PartialEq, Eq
|
||||||
- **File**: src/domain/relay.rs
|
- **File**: `src/domain/relay.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T019** [P] [US1] [TDD] Write tests for RelayState enum
|
- [x] **T019** [P] [US1] [TDD] Write tests for RelayState enum
|
||||||
- Test: RelayState::On → serializes to "on"
|
- Test: RelayState::On → serializes to "on"
|
||||||
- Test: RelayState::Off → serializes to "off"
|
- Test: RelayState::Off → serializes to "off"
|
||||||
- Test: Parse "on"/"off" from strings
|
- Test: Parse "on"/"off" from strings
|
||||||
- **File**: src/domain/relay.rs
|
- **File**: `src/domain/relay.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T020** [P] [US1] [TDD] Implement RelayState enum
|
- [x] **T020** [P] [US1] [TDD] Implement RelayState enum
|
||||||
- Enum: On, Off
|
- Enum: On, Off
|
||||||
- Implement Display, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize/Deserialize
|
- Implement Display, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize/Deserialize
|
||||||
- **File**: src/domain/relay.rs
|
- **File**: `src/domain/relay.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T021** [US1] [TDD] Write tests for Relay aggregate
|
- [x] **T021** [US1] [TDD] Write tests for Relay aggregate
|
||||||
@@ -236,13 +236,13 @@
|
|||||||
- Test: relay.toggle() flips state
|
- Test: relay.toggle() flips state
|
||||||
- Test: relay.turn_on() sets state to On
|
- Test: relay.turn_on() sets state to On
|
||||||
- Test: relay.turn_off() sets state to Off
|
- Test: relay.turn_off() sets state to Off
|
||||||
- **File**: src/domain/relay.rs
|
- **File**: `src/domain/relay.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T022** [US1] [TDD] Implement Relay aggregate
|
- [x] **T022** [US1] [TDD] Implement Relay aggregate
|
||||||
- Struct: `Relay { id: RelayId, state: RelayState, label: Option<RelayLabel> }`
|
- Struct: `Relay { id: RelayId, state: RelayState, label: Option<RelayLabel> }`
|
||||||
- Methods: `new()` `toggle()` `turn_on()` `turn_off()` `state()` `label()`
|
- Methods: `new()` `toggle()` `turn_on()` `turn_off()` `state()` `label()`
|
||||||
- **File**: src/domain/relay.rs
|
- **File**: `src/domain/relay.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T023** [P] [US4] [TDD] Write tests for RelayLabel newtype
|
- [x] **T023** [P] [US4] [TDD] Write tests for RelayLabel newtype
|
||||||
@@ -250,32 +250,32 @@
|
|||||||
- Test: RelayLabel::new("A".repeat(50)) → Ok
|
- Test: RelayLabel::new("A".repeat(50)) → Ok
|
||||||
- Test: RelayLabel::new("") → Err(EmptyLabel)
|
- Test: RelayLabel::new("") → Err(EmptyLabel)
|
||||||
- Test: RelayLabel::new("A".repeat(51)) → Err(LabelTooLong)
|
- Test: RelayLabel::new("A".repeat(51)) → Err(LabelTooLong)
|
||||||
- **File**: src/domain/relay.rs
|
- **File**: `src/domain/relay.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T024** [P] [US4] [TDD] Implement RelayLabel newtype
|
- [x] **T024** [P] [US4] [TDD] Implement RelayLabel newtype
|
||||||
- #[repr(transparent)] newtype wrapping String
|
- #[repr(transparent)] newtype wrapping String
|
||||||
- Constructor validates 1..=50 length
|
- Constructor validates 1..=50 length
|
||||||
- Implement Display, Debug, Clone, PartialEq, Eq
|
- Implement Display, Debug, Clone, PartialEq, Eq
|
||||||
- **File**: src/domain/relay.rs
|
- **File**: `src/domain/relay.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **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
|
||||||
|
|
||||||
- [x] **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
|
||||||
|
|
||||||
- [x] **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`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
**Checkpoint**: Domain types complete with 100% test coverage
|
**Checkpoint**: Domain types complete with 100% test coverage
|
||||||
@@ -290,13 +290,13 @@
|
|||||||
- Test: read_state() returns mocked state
|
- Test: read_state() returns mocked state
|
||||||
- Test: write_state() updates mocked state
|
- Test: write_state() updates mocked state
|
||||||
- Test: read_all() returns 8 relays in known state
|
- Test: read_all() returns 8 relays in known state
|
||||||
- **File**: src/infrastructure/modbus/mock_controller.rs
|
- **File**: `src/infrastructure/modbus/mock_controller.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T029** [P] [US1] [TDD] Implement MockRelayController
|
- [x] **T029** [P] [US1] [TDD] Implement MockRelayController
|
||||||
- Struct with Arc<Mutex<HashMap<RelayId, RelayState>>>
|
- Struct with Arc<Mutex<HashMap<RelayId, RelayState>>>
|
||||||
- Implement RelayController trait with in-memory state
|
- Implement RelayController trait with in-memory state
|
||||||
- **File**: src/infrastructure/modbus/mock_controller.rs
|
- **File**: `src/infrastructure/modbus/mock_controller.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T030** [US1] [TDD] Define RelayController trait
|
- [x] **T030** [US1] [TDD] Define RelayController trait
|
||||||
@@ -304,14 +304,14 @@
|
|||||||
- async fn write_state(&self, id: RelayId, state: RelayState) → Result<(), ControllerError>
|
- async fn write_state(&self, id: RelayId, state: RelayState) → Result<(), ControllerError>
|
||||||
- async fn read_all(&self) → Result<Vec<(RelayId, RelayState)>, ControllerError>
|
- async fn read_all(&self) → Result<Vec<(RelayId, RelayState)>, ControllerError>
|
||||||
- async fn write_all(&self, state: RelayState) → Result<(), ControllerError>
|
- async fn write_all(&self, state: RelayState) → Result<(), ControllerError>
|
||||||
- **File**: src/infrastructure/modbus/controller.rs
|
- **File**: `src/infrastructure/modbus/controller.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T031** [P] [US1] [TDD] Define ControllerError enum
|
- [x] **T031** [P] [US1] [TDD] Define ControllerError enum
|
||||||
- Variants: ConnectionError(String), Timeout(u64), ModbusException(String), InvalidRelayId(u8)
|
- Variants: ConnectionError(String), Timeout(u64), ModbusException(String), InvalidRelayId(u8)
|
||||||
- Implement std::error::Error, Display, Debug
|
- Implement std::error::Error, Display, Debug
|
||||||
- Use thiserror derive macros
|
- Use thiserror derive macros
|
||||||
- **File**: src/infrastructure/modbus/error.rs
|
- **File**: `src/infrastructure/modbus/error.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [x] **T032** [US1] [TDD] Write tests for MockRelayController
|
- [x] **T032** [US1] [TDD] Write tests for MockRelayController
|
||||||
@@ -321,7 +321,7 @@
|
|||||||
- Test: write_relay_state() for all 8 relays independently ✓
|
- Test: write_relay_state() for all 8 relays independently ✓
|
||||||
- Test: read_relay_state() with invalid relay ID (type system prevents) ✓
|
- Test: read_relay_state() with invalid relay ID (type system prevents) ✓
|
||||||
- Test: concurrent access is thread-safe ✓
|
- Test: concurrent access is thread-safe ✓
|
||||||
- **File**: src/infrastructure/modbus/mock_controller.rs
|
- **File**: `src/infrastructure/modbus/mock_controller.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
- **Tests Written**: 6 comprehensive tests covering all mock controller scenarios
|
- **Tests Written**: 6 comprehensive tests covering all mock controller scenarios
|
||||||
|
|
||||||
@@ -338,7 +338,7 @@
|
|||||||
- Struct: `ModbusRelayController { ctx: Arc<Mutex<Context>>, timeout_duration: Duration }`
|
- Struct: `ModbusRelayController { ctx: Arc<Mutex<Context>>, timeout_duration: Duration }`
|
||||||
- Constructor: `new(host, port, slave_id, timeout_secs) → Result<Self, ControllerError>`
|
- Constructor: `new(host, port, slave_id, timeout_secs) → Result<Self, ControllerError>`
|
||||||
- Use `tokio_modbus::client::tcp::connect_slave()`
|
- Use `tokio_modbus::client::tcp::connect_slave()`
|
||||||
- **File**: src/infrastructure/modbus/modbus_controller.rs
|
- **File**: `src/infrastructure/modbus/modbus_controller.rs`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Medium
|
- **Complexity**: Medium | **Uncertainty**: Medium
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -381,7 +381,7 @@
|
|||||||
- Wrap `ctx.read_coils()` with `tokio::time::timeout()`
|
- Wrap `ctx.read_coils()` with `tokio::time::timeout()`
|
||||||
- Handle nested Result: timeout → io::Error → Modbus Exception
|
- Handle nested Result: timeout → io::Error → Modbus Exception
|
||||||
- **Note**: Modbus TCP uses MBAP header (no CRC validation needed)
|
- **Note**: Modbus TCP uses MBAP header (no CRC validation needed)
|
||||||
- **File**: src/infrastructure/modbus/modbus_controller.rs
|
- **File**: `src/infrastructure/modbus/modbus_controller.rs`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Medium
|
- **Complexity**: Medium | **Uncertainty**: Medium
|
||||||
|
|
||||||
**Pseudocode** (CRITICAL PATTERN):
|
**Pseudocode** (CRITICAL PATTERN):
|
||||||
@@ -415,7 +415,7 @@
|
|||||||
- [x] **T025c** [US1] [TDD] Implement timeout-wrapped `write_single_coil` helper
|
- [x] **T025c** [US1] [TDD] Implement timeout-wrapped `write_single_coil` helper
|
||||||
- Private method: `write_single_coil_with_timeout(addr: u16, value: bool) → Result<(), ControllerError>`
|
- Private method: `write_single_coil_with_timeout(addr: u16, value: bool) → Result<(), ControllerError>`
|
||||||
- Similar nested Result handling as T025b
|
- Similar nested Result handling as T025b
|
||||||
- **File**: src/infrastructure/modbus/modbus_controller.rs
|
- **File**: `src/infrastructure/modbus/modbus_controller.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
- Convert RelayId → ModbusAddress (0-based)
|
- Convert RelayId → ModbusAddress (0-based)
|
||||||
- Call `read_coils_with_timeout(addr, 1)`
|
- Call `read_coils_with_timeout(addr, 1)`
|
||||||
- Convert bool → RelayState
|
- Convert bool → RelayState
|
||||||
- **File**: src/infrastructure/modbus/modbus_controller.rs
|
- **File**: `src/infrastructure/modbus/modbus_controller.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -471,7 +471,7 @@
|
|||||||
- Convert RelayId → ModbusAddress
|
- Convert RelayId → ModbusAddress
|
||||||
- Convert RelayState → bool (On=true, Off=false)
|
- Convert RelayState → bool (On=true, Off=false)
|
||||||
- Call `write_single_coil_with_timeout()`
|
- Call `write_single_coil_with_timeout()`
|
||||||
- **File**: src/infrastructure/modbus/modbus_controller.rs
|
- **File**: `src/infrastructure/modbus/modbus_controller.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -491,7 +491,7 @@
|
|||||||
- `read_all()`: Call `read_coils_with_timeout(0, 8)`, map to `Vec<(RelayId, RelayState)>`
|
- `read_all()`: Call `read_coils_with_timeout(0, 8)`, map to `Vec<(RelayId, RelayState)>`
|
||||||
- `write_all()`: Loop over RelayId 1-8, call `write_state()` for each
|
- `write_all()`: Loop over RelayId 1-8, call `write_state()` for each
|
||||||
- Add `firmware_version()` method (read holding register 0x9999, optional)
|
- Add `firmware_version()` method (read holding register 0x9999, optional)
|
||||||
- **File**: src/infrastructure/modbus/modbus_controller.rs
|
- **File**: `src/infrastructure/modbus/modbus_controller.rs`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -527,42 +527,42 @@
|
|||||||
- [ ] **T034** [US1] [TDD] Integration test with real hardware (optional)
|
- [ ] **T034** [US1] [TDD] Integration test with real hardware (optional)
|
||||||
- **REQUIRES PHYSICAL DEVICE**: Test against actual Modbus relay at configured IP
|
- **REQUIRES PHYSICAL DEVICE**: Test against actual Modbus relay at configured IP
|
||||||
- Skip if device unavailable, rely on MockRelayController for CI
|
- Skip if device unavailable, rely on MockRelayController for CI
|
||||||
- **File**: tests/integration/modbus_hardware_test.rs
|
- **File**: `tests/integration/modbus_hardware_test.rs`
|
||||||
- **Complexity**: Medium | **Uncertainty**: High
|
- **Complexity**: Medium | **Uncertainty**: High
|
||||||
- **Note**: Use #[ignore] attribute, run with cargo test -- --ignored
|
- **Note**: Use #[ignore] attribute, run with `cargo test -- --ignored`
|
||||||
|
|
||||||
- [ ] **T035** [P] [US4] [TDD] Write tests for RelayLabelRepository trait
|
- [ ] **T035** [P] [US4] [TDD] Write tests for RelayLabelRepository trait
|
||||||
- Test: get_label(RelayId(1)) → Option<RelayLabel>
|
- Test: `get_label(RelayId(1)) → Option<RelayLabel>`
|
||||||
- Test: set_label(RelayId(1), label) → Result<(), RepositoryError>
|
- Test: `set_label(RelayId(1), label) → Result<(), RepositoryError>`
|
||||||
- Test: delete_label(RelayId(1)) → Result<(), RepositoryError>
|
- Test: `delete_label(RelayId(1)) → Result<(), RepositoryError>`
|
||||||
- **File**: src/infrastructure/persistence/label_repository.rs
|
- **File**: `src/infrastructure/persistence/label_repository.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T036** [P] [US4] [TDD] Implement SQLite RelayLabelRepository
|
- [ ] **T036** [P] [US4] [TDD] Implement SQLite RelayLabelRepository
|
||||||
- Implement get_label(), set_label(), delete_label() using SQLx
|
- Implement `get_label()`, `set_label()`, `delete_label()` using SQLx
|
||||||
- Use sqlx::query! macros for compile-time SQL verification
|
- Use `sqlx::query!` macros for compile-time SQL verification
|
||||||
- **File**: src/infrastructure/persistence/sqlite_label_repository.rs
|
- **File**: `src/infrastructure/persistence/sqlite_label_repository.rs`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T037** [US4] [TDD] Write tests for in-memory mock LabelRepository
|
- [x] **T037** [US4] [TDD] Write tests for in-memory mock LabelRepository
|
||||||
- For testing without SQLite dependency
|
- For testing without SQLite dependency
|
||||||
- **File**: src/infrastructure/persistence/mock_label_repository.rs
|
- **File**: `src/infrastructure/persistence/mock_label_repository.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T038** [US4] [TDD] Implement in-memory mock LabelRepository
|
- [x] **T038** [US4] [TDD] Implement in-memory mock LabelRepository
|
||||||
- HashMap-based implementation
|
- HashMap-based implementation
|
||||||
- **File**: src/infrastructure/persistence/mock_label_repository.rs
|
- **File**: `src/infrastructure/persistence/mock_label_repository.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T039** [US3] [TDD] Write tests for HealthMonitor service
|
- [ ] **T039** [US3] [TDD] Write tests for HealthMonitor service
|
||||||
- Test: track_success() transitions Degraded → Healthy
|
- Test: `track_success()` transitions Degraded → Healthy
|
||||||
- Test: track_failure() transitions Healthy → Degraded → Unhealthy
|
- Test: `track_failure()` transitions Healthy → Degraded → Unhealthy
|
||||||
- **File**: src/application/health_monitor.rs
|
- **File**: `src/application/health_monitor.rs`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T040** [US3] [TDD] Implement HealthMonitor service
|
- [ ] **T040** [US3] [TDD] Implement HealthMonitor service
|
||||||
- Track consecutive errors, transition states per FR-020, FR-021
|
- Track consecutive errors, transition states per FR-020, FR-021
|
||||||
- **File**: src/application/health_monitor.rs
|
- **File**: `src/application/health_monitor.rs`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
**Checkpoint**: Infrastructure layer complete with trait abstractions
|
**Checkpoint**: Infrastructure layer complete with trait abstractions
|
||||||
@@ -580,22 +580,22 @@
|
|||||||
- [ ] **T041** [US1] [TDD] Write tests for ToggleRelayUseCase
|
- [ ] **T041** [US1] [TDD] Write tests for ToggleRelayUseCase
|
||||||
- Test: execute(RelayId(1)) toggles relay state via controller
|
- Test: execute(RelayId(1)) toggles relay state via controller
|
||||||
- Test: execute() returns error if controller fails
|
- Test: execute() returns error if controller fails
|
||||||
- **File**: src/application/use_cases/toggle_relay.rs
|
- **File**: `src/application/use_cases/toggle_relay.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T042** [US1] [TDD] Implement ToggleRelayUseCase
|
- [ ] **T042** [US1] [TDD] Implement ToggleRelayUseCase
|
||||||
- Orchestrate: read current state → toggle → write new state
|
- Orchestrate: read current state → toggle → write new state
|
||||||
- **File**: src/application/use_cases/toggle_relay.rs
|
- **File**: `src/application/use_cases/toggle_relay.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T043** [P] [US1] [TDD] Write tests for GetAllRelaysUseCase
|
- [ ] **T043** [P] [US1] [TDD] Write tests for GetAllRelaysUseCase
|
||||||
- Test: execute() returns all 8 relays with states
|
- Test: execute() returns all 8 relays with states
|
||||||
- **File**: src/application/use_cases/get_all_relays.rs
|
- **File**: `src/application/use_cases/get_all_relays.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T044** [P] [US1] [TDD] Implement GetAllRelaysUseCase
|
- [ ] **T044** [P] [US1] [TDD] Implement GetAllRelaysUseCase
|
||||||
- Call controller.read_all(), map to domain Relay objects
|
- Call controller.read_all(), map to domain Relay objects
|
||||||
- **File**: src/application/use_cases/get_all_relays.rs
|
- **File**: `src/application/use_cases/get_all_relays.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
### Presentation Layer (Backend API)
|
### Presentation Layer (Backend API)
|
||||||
@@ -603,13 +603,13 @@
|
|||||||
- [ ] **T045** [US1] [TDD] Define RelayDto in presentation layer
|
- [ ] **T045** [US1] [TDD] Define RelayDto in presentation layer
|
||||||
- Fields: id (u8), state ("on"/"off"), label (Option<String>)
|
- Fields: id (u8), state ("on"/"off"), label (Option<String>)
|
||||||
- Implement From<Relay> for RelayDto
|
- Implement From<Relay> for RelayDto
|
||||||
- **File**: src/presentation/dto/relay_dto.rs
|
- **File**: `src/presentation/dto/relay_dto.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T046** [US1] [TDD] Define API error responses
|
- [ ] **T046** [US1] [TDD] Define API error responses
|
||||||
- ApiError enum with status codes and messages
|
- ApiError enum with status codes and messages
|
||||||
- Implement poem::error::ResponseError
|
- Implement poem::error::ResponseError
|
||||||
- **File**: src/presentation/error.rs
|
- **File**: `src/presentation/error.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -624,7 +624,7 @@
|
|||||||
- Factory function: create_relay_controller(settings, use_mock) → Arc<dyn RelayController>
|
- Factory function: create_relay_controller(settings, use_mock) → Arc<dyn RelayController>
|
||||||
- Retry 3 times with 2s backoff on connection failure
|
- Retry 3 times with 2s backoff on connection failure
|
||||||
- Graceful degradation: fallback to MockRelayController if all retries fail (FR-023)
|
- Graceful degradation: fallback to MockRelayController if all retries fail (FR-023)
|
||||||
- **File**: src/infrastructure/modbus/factory.rs
|
- **File**: `src/infrastructure/modbus/factory.rs`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Medium
|
- **Complexity**: Medium | **Uncertainty**: Medium
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -683,7 +683,7 @@
|
|||||||
- Factory function: create_label_repository(db_path, use_mock) → Arc<dyn RelayLabelRepository>
|
- Factory function: create_label_repository(db_path, use_mock) → Arc<dyn RelayLabelRepository>
|
||||||
- If use_mock: return MockLabelRepository
|
- If use_mock: return MockLabelRepository
|
||||||
- Else: return SQLiteLabelRepository connected to db_path
|
- Else: return SQLiteLabelRepository connected to db_path
|
||||||
- **File**: src/infrastructure/persistence/factory.rs
|
- **File**: `src/infrastructure/persistence/factory.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -711,7 +711,7 @@
|
|||||||
- Determine test mode: cfg!(test) || env::var("CI").is_ok()
|
- Determine test mode: cfg!(test) || env::var("CI").is_ok()
|
||||||
- Call create_relay_controller() and create_label_repository()
|
- Call create_relay_controller() and create_label_repository()
|
||||||
- Pass dependencies to RelayApi::new()
|
- Pass dependencies to RelayApi::new()
|
||||||
- **File**: src/startup.rs
|
- **File**: `src/startup.rs`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -752,7 +752,7 @@
|
|||||||
- [ ] **T039d** [US1] [TDD] Register RelayApi in route aggregator
|
- [ ] **T039d** [US1] [TDD] Register RelayApi in route aggregator
|
||||||
- Add RelayApi to OpenAPI service
|
- Add RelayApi to OpenAPI service
|
||||||
- Tag: "Relays"
|
- Tag: "Relays"
|
||||||
- **File**: src/startup.rs
|
- **File**: `src/startup.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
**TDD Checklist**:
|
**TDD Checklist**:
|
||||||
@@ -764,39 +764,39 @@
|
|||||||
- [ ] **T048** [US1] [TDD] Write contract tests for GET /api/relays
|
- [ ] **T048** [US1] [TDD] Write contract tests for GET /api/relays
|
||||||
- Test: Returns 200 with array of 8 RelayDto
|
- Test: Returns 200 with array of 8 RelayDto
|
||||||
- Test: Each relay has id 1-8, state, and optional label
|
- Test: Each relay has id 1-8, state, and optional label
|
||||||
- **File**: tests/contract/test_relay_api.rs
|
- **File**: `tests/contract/test_relay_api.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T049** [US1] [TDD] Implement GET /api/relays endpoint
|
- [ ] **T049** [US1] [TDD] Implement GET /api/relays endpoint
|
||||||
- #[oai(path = "/relays", method = "get")]
|
- #[oai(path = "/relays", method = "get")]
|
||||||
- Call GetAllRelaysUseCase, map to RelayDto
|
- Call GetAllRelaysUseCase, map to RelayDto
|
||||||
- **File**: src/presentation/api/relay_api.rs
|
- **File**: `src/presentation/api/relay_api.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T050** [US1] [TDD] Write contract tests for POST /api/relays/{id}/toggle
|
- [ ] **T050** [US1] [TDD] Write contract tests for POST /api/relays/{id}/toggle
|
||||||
- Test: Returns 200 with updated RelayDto
|
- Test: Returns 200 with updated RelayDto
|
||||||
- Test: Returns 404 for id < 1 or id > 8
|
- Test: Returns 404 for id < 1 or id > 8
|
||||||
- Test: State actually changes in controller
|
- Test: State actually changes in controller
|
||||||
- **File**: tests/contract/test_relay_api.rs
|
- **File**: `tests/contract/test_relay_api.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T051** [US1] [TDD] Implement POST /api/relays/{id}/toggle endpoint
|
- [ ] **T051** [US1] [TDD] Implement POST /api/relays/{id}/toggle endpoint
|
||||||
- #[oai(path = "/relays/:id/toggle", method = "post")]
|
- #[oai(path = "/relays/:id/toggle", method = "post")]
|
||||||
- Parse id, call ToggleRelayUseCase, return updated state
|
- Parse id, call ToggleRelayUseCase, return updated state
|
||||||
- **File**: src/presentation/api/relay_api.rs
|
- **File**: `src/presentation/api/relay_api.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
### Frontend Implementation
|
### Frontend Implementation
|
||||||
|
|
||||||
- [ ] **T052** [P] [US1] [TDD] Create RelayDto TypeScript interface
|
- [ ] **T052** [P] [US1] [TDD] Create RelayDto TypeScript interface
|
||||||
- Generate from OpenAPI spec or manually define
|
- Generate from OpenAPI spec or manually define
|
||||||
- **File**: frontend/src/types/relay.ts
|
- **File**: `frontend/src/types/relay.ts`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T053** [P] [US1] [TDD] Create API client service
|
- [ ] **T053** [P] [US1] [TDD] Create API client service
|
||||||
- getAllRelays(): Promise<RelayDto[]>
|
- getAllRelays(): Promise<RelayDto[]>
|
||||||
- toggleRelay(id: number): Promise<RelayDto>
|
- toggleRelay(id: number): Promise<RelayDto>
|
||||||
- **File**: frontend/src/api/relayApi.ts
|
- **File**: `frontend/src/api/relayApi.ts`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -810,7 +810,7 @@
|
|||||||
- [ ] **T046a** [US1] [TDD] Create useRelayPolling composable structure
|
- [ ] **T046a** [US1] [TDD] Create useRelayPolling composable structure
|
||||||
- Setup reactive refs: relays, isLoading, error, lastFetchTime
|
- Setup reactive refs: relays, isLoading, error, lastFetchTime
|
||||||
- Define interval variable and fetch function signature
|
- Define interval variable and fetch function signature
|
||||||
- **File**: frontend/src/composables/useRelayPolling.ts
|
- **File**: `frontend/src/composables/useRelayPolling.ts`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -850,7 +850,7 @@
|
|||||||
- Fetch relays and health status in parallel using Promise.all
|
- Fetch relays and health status in parallel using Promise.all
|
||||||
- Update reactive state on success
|
- Update reactive state on success
|
||||||
- Handle errors gracefully, set isConnected based on success
|
- Handle errors gracefully, set isConnected based on success
|
||||||
- **File**: frontend/src/composables/useRelayPolling.ts
|
- **File**: `frontend/src/composables/useRelayPolling.ts`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -886,7 +886,7 @@
|
|||||||
- startPolling(): Fetch immediately, then setInterval
|
- startPolling(): Fetch immediately, then setInterval
|
||||||
- stopPolling(): clearInterval and cleanup
|
- stopPolling(): clearInterval and cleanup
|
||||||
- Use onMounted/onUnmounted for automatic lifecycle management
|
- Use onMounted/onUnmounted for automatic lifecycle management
|
||||||
- **File**: frontend/src/composables/useRelayPolling.ts
|
- **File**: `frontend/src/composables/useRelayPolling.ts`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -926,7 +926,7 @@
|
|||||||
- [ ] **T046d** [US1] [TDD] Add connection status tracking
|
- [ ] **T046d** [US1] [TDD] Add connection status tracking
|
||||||
- Track isConnected based on fetch success/failure
|
- Track isConnected based on fetch success/failure
|
||||||
- Display connection status in UI
|
- Display connection status in UI
|
||||||
- **File**: frontend/src/composables/useRelayPolling.ts
|
- **File**: `frontend/src/composables/useRelayPolling.ts`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
**Pseudocode**:
|
**Pseudocode**:
|
||||||
@@ -954,7 +954,7 @@
|
|||||||
- Props: relay (RelayDto)
|
- Props: relay (RelayDto)
|
||||||
- Display relay ID, state, label
|
- Display relay ID, state, label
|
||||||
- Emit toggle event on button click
|
- Emit toggle event on button click
|
||||||
- **File**: frontend/src/components/RelayCard.vue
|
- **File**: `frontend/src/components/RelayCard.vue`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T056** [US1] [TDD] Create RelayGrid component
|
- [ ] **T056** [US1] [TDD] Create RelayGrid component
|
||||||
@@ -962,13 +962,13 @@
|
|||||||
- Render 8 RelayCard components
|
- Render 8 RelayCard components
|
||||||
- Handle toggle events by calling API
|
- Handle toggle events by calling API
|
||||||
- Display loading/error states
|
- Display loading/error states
|
||||||
- **File**: frontend/src/components/RelayGrid.vue
|
- **File**: `frontend/src/components/RelayGrid.vue`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T057** [US1] [TDD] Integration test for US1
|
- [ ] **T057** [US1] [TDD] Integration test for US1
|
||||||
- End-to-end test: Load page → see 8 relays → toggle relay 1 → verify state change
|
- End-to-end test: Load page → see 8 relays → toggle relay 1 → verify state change
|
||||||
- Use Playwright or Cypress
|
- Use Playwright or Cypress
|
||||||
- **File**: frontend/tests/e2e/relay-control.spec.ts
|
- **File**: `frontend/tests/e2e/relay-control.spec.ts`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Medium
|
- **Complexity**: Medium | **Uncertainty**: Medium
|
||||||
|
|
||||||
**Checkpoint**: US1 MVP complete - users can view and toggle individual relays
|
**Checkpoint**: US1 MVP complete - users can view and toggle individual relays
|
||||||
@@ -984,49 +984,49 @@
|
|||||||
- [ ] **T058** [US2] [TDD] Write tests for BulkControlUseCase
|
- [ ] **T058** [US2] [TDD] Write tests for BulkControlUseCase
|
||||||
- Test: execute(BulkOperation::AllOn) turns all relays on
|
- Test: execute(BulkOperation::AllOn) turns all relays on
|
||||||
- Test: execute(BulkOperation::AllOff) turns all relays off
|
- Test: execute(BulkOperation::AllOff) turns all relays off
|
||||||
- **File**: src/application/use_cases/bulk_control.rs
|
- **File**: `src/application/use_cases/bulk_control.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T059** [US2] [TDD] Implement BulkControlUseCase
|
- [ ] **T059** [US2] [TDD] Implement BulkControlUseCase
|
||||||
- Call controller.write_all(state)
|
- Call controller.write_all(state)
|
||||||
- **File**: src/application/use_cases/bulk_control.rs
|
- **File**: `src/application/use_cases/bulk_control.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T060** [US2] [TDD] Define BulkOperation enum
|
- [ ] **T060** [US2] [TDD] Define BulkOperation enum
|
||||||
- Variants: AllOn, AllOff
|
- Variants: AllOn, AllOff
|
||||||
- **File**: src/domain/relay.rs
|
- **File**: `src/domain/relay.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T061** [US2] [TDD] Write contract tests for POST /api/relays/all/on
|
- [ ] **T061** [US2] [TDD] Write contract tests for POST /api/relays/all/on
|
||||||
- Test: Returns 200, all relays turn on
|
- Test: Returns 200, all relays turn on
|
||||||
- **File**: tests/contract/test_relay_api.rs
|
- **File**: `tests/contract/test_relay_api.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T062** [US2] [TDD] Implement POST /api/relays/all/on endpoint
|
- [ ] **T062** [US2] [TDD] Implement POST /api/relays/all/on endpoint
|
||||||
- Call BulkControlUseCase with AllOn
|
- Call BulkControlUseCase with AllOn
|
||||||
- **File**: src/presentation/api/relay_api.rs
|
- **File**: `src/presentation/api/relay_api.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T063** [P] [US2] [TDD] Write contract tests for POST /api/relays/all/off
|
- [ ] **T063** [P] [US2] [TDD] Write contract tests for POST /api/relays/all/off
|
||||||
- Test: Returns 200, all relays turn off
|
- Test: Returns 200, all relays turn off
|
||||||
- **File**: tests/contract/test_relay_api.rs
|
- **File**: `tests/contract/test_relay_api.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T064** [P] [US2] [TDD] Implement POST /api/relays/all/off endpoint
|
- [ ] **T064** [P] [US2] [TDD] Implement POST /api/relays/all/off endpoint
|
||||||
- Call BulkControlUseCase with AllOff
|
- Call BulkControlUseCase with AllOff
|
||||||
- **File**: src/presentation/api/relay_api.rs
|
- **File**: `src/presentation/api/relay_api.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T065** [US2] [TDD] Add bulk control buttons to frontend
|
- [ ] **T065** [US2] [TDD] Add bulk control buttons to frontend
|
||||||
- Add "All On" and "All Off" buttons to RelayGrid component
|
- Add "All On" and "All Off" buttons to RelayGrid component
|
||||||
- Call API endpoints and refresh relay states
|
- Call API endpoints and refresh relay states
|
||||||
- **File**: frontend/src/components/RelayGrid.vue
|
- **File**: `frontend/src/components/RelayGrid.vue`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T066** [US2] [TDD] Integration test for US2
|
- [ ] **T066** [US2] [TDD] Integration test for US2
|
||||||
- Click "All On" → verify all 8 relays turn on
|
- Click "All On" → verify all 8 relays turn on
|
||||||
- Click "All Off" → verify all 8 relays turn off
|
- Click "All Off" → verify all 8 relays turn off
|
||||||
- **File**: frontend/tests/e2e/bulk-control.spec.ts
|
- **File**: `frontend/tests/e2e/bulk-control.spec.ts`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
**Checkpoint**: US2 complete - bulk controls functional
|
**Checkpoint**: US2 complete - bulk controls functional
|
||||||
@@ -1043,46 +1043,46 @@
|
|||||||
- Test: Returns Healthy when controller is responsive
|
- Test: Returns Healthy when controller is responsive
|
||||||
- Test: Returns Degraded after 3 consecutive errors
|
- Test: Returns Degraded after 3 consecutive errors
|
||||||
- Test: Returns Unhealthy after 10 consecutive errors
|
- Test: Returns Unhealthy after 10 consecutive errors
|
||||||
- **File**: src/application/use_cases/get_health.rs
|
- **File**: `src/application/use_cases/get_health.rs`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T068** [US3] [TDD] Implement GetHealthUseCase
|
- [ ] **T068** [US3] [TDD] Implement GetHealthUseCase
|
||||||
- Use HealthMonitor to track controller status
|
- Use HealthMonitor to track controller status
|
||||||
- Return current HealthStatus
|
- Return current HealthStatus
|
||||||
- **File**: src/application/use_cases/get_health.rs
|
- **File**: `src/application/use_cases/get_health.rs`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T069** [US3] [TDD] Define HealthDto
|
- [ ] **T069** [US3] [TDD] Define HealthDto
|
||||||
- Fields: status ("healthy"/"degraded"/"unhealthy"), consecutive_errors (optional), reason (optional)
|
- Fields: status ("healthy"/"degraded"/"unhealthy"), consecutive_errors (optional), reason (optional)
|
||||||
- **File**: src/presentation/dto/health_dto.rs
|
- **File**: `src/presentation/dto/health_dto.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T070** [US3] [TDD] Write contract tests for GET /api/health
|
- [ ] **T070** [US3] [TDD] Write contract tests for GET /api/health
|
||||||
- Test: Returns 200 with HealthDto
|
- Test: Returns 200 with HealthDto
|
||||||
- **File**: tests/contract/test_health_api.rs
|
- **File**: `tests/contract/test_health_api.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T071** [US3] [TDD] Implement GET /api/health endpoint
|
- [ ] **T071** [US3] [TDD] Implement GET /api/health endpoint
|
||||||
- Call GetHealthUseCase, map to HealthDto
|
- Call GetHealthUseCase, map to HealthDto
|
||||||
- **File**: src/presentation/api/health_api.rs
|
- **File**: `src/presentation/api/health_api.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T072** [P] [US3] [TDD] Add firmware version display (optional)
|
- [ ] **T072** [P] [US3] [TDD] Add firmware version display (optional)
|
||||||
- If controller supports firmware_version(), display in UI
|
- If controller supports firmware_version(), display in UI
|
||||||
- **File**: frontend/src/components/DeviceInfo.vue
|
- **File**: `frontend/src/components/DeviceInfo.vue`
|
||||||
- **Complexity**: Low | **Uncertainty**: Medium
|
- **Complexity**: Low | **Uncertainty**: Medium
|
||||||
- **Note**: Device may not support this feature
|
- **Note**: Device may not support this feature
|
||||||
|
|
||||||
- [ ] **T073** [US3] [TDD] Create HealthIndicator component
|
- [ ] **T073** [US3] [TDD] Create HealthIndicator component
|
||||||
- Display connection status with color-coded indicator
|
- Display connection status with color-coded indicator
|
||||||
- Show firmware version if available
|
- Show firmware version if available
|
||||||
- **File**: frontend/src/components/HealthIndicator.vue
|
- **File**: `frontend/src/components/HealthIndicator.vue`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T074** [US3] [TDD] Integrate HealthIndicator in RelayGrid
|
- [ ] **T074** [US3] [TDD] Integrate HealthIndicator in RelayGrid
|
||||||
- Fetch health status in useRelayPolling composable
|
- Fetch health status in useRelayPolling composable
|
||||||
- Pass to HealthIndicator component
|
- Pass to HealthIndicator component
|
||||||
- **File**: frontend/src/components/RelayGrid.vue
|
- **File**: `frontend/src/components/RelayGrid.vue`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
**Checkpoint**: US3 complete - health monitoring visible
|
**Checkpoint**: US3 complete - health monitoring visible
|
||||||
@@ -1099,35 +1099,35 @@
|
|||||||
- Test: execute(RelayId(1), "Pump") sets label
|
- Test: execute(RelayId(1), "Pump") sets label
|
||||||
- Test: execute with empty label returns error
|
- Test: execute with empty label returns error
|
||||||
- Test: execute with 51-char label returns error
|
- Test: execute with 51-char label returns error
|
||||||
- **File**: src/application/use_cases/set_label.rs
|
- **File**: `src/application/use_cases/set_label.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T076** [US4] [TDD] Implement SetLabelUseCase
|
- [ ] **T076** [US4] [TDD] Implement SetLabelUseCase
|
||||||
- Validate label with RelayLabel::new()
|
- Validate label with RelayLabel::new()
|
||||||
- Call label_repository.set_label()
|
- Call label_repository.set_label()
|
||||||
- **File**: src/application/use_cases/set_label.rs
|
- **File**: `src/application/use_cases/set_label.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T077** [US4] [TDD] Write contract tests for PUT /api/relays/{id}/label
|
- [ ] **T077** [US4] [TDD] Write contract tests for PUT /api/relays/{id}/label
|
||||||
- Test: Returns 200, label is persisted
|
- Test: Returns 200, label is persisted
|
||||||
- Test: Returns 400 for invalid label
|
- Test: Returns 400 for invalid label
|
||||||
- **File**: tests/contract/test_relay_api.rs
|
- **File**: `tests/contract/test_relay_api.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T078** [US4] [TDD] Implement PUT /api/relays/{id}/label endpoint
|
- [ ] **T078** [US4] [TDD] Implement PUT /api/relays/{id}/label endpoint
|
||||||
- Parse id and label, call SetLabelUseCase
|
- Parse id and label, call SetLabelUseCase
|
||||||
- **File**: src/presentation/api/relay_api.rs
|
- **File**: `src/presentation/api/relay_api.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T079** [US4] [TDD] Add label editing to RelayCard component
|
- [ ] **T079** [US4] [TDD] Add label editing to RelayCard component
|
||||||
- Click label → show input field
|
- Click label → show input field
|
||||||
- Submit → call PUT /api/relays/{id}/label
|
- Submit → call PUT /api/relays/{id}/label
|
||||||
- **File**: frontend/src/components/RelayCard.vue
|
- **File**: `frontend/src/components/RelayCard.vue`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T080** [US4] [TDD] Integration test for US4
|
- [ ] **T080** [US4] [TDD] Integration test for US4
|
||||||
- Set label for relay 1 → refresh → verify label persists
|
- Set label for relay 1 → refresh → verify label persists
|
||||||
- **File**: frontend/tests/e2e/relay-labeling.spec.ts
|
- **File**: `frontend/tests/e2e/relay-labeling.spec.ts`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
**Checkpoint**: US4 complete - relay labeling functional
|
**Checkpoint**: US4 complete - relay labeling functional
|
||||||
@@ -1149,7 +1149,7 @@
|
|||||||
- Document request/response schemas
|
- Document request/response schemas
|
||||||
- Add example values
|
- Add example values
|
||||||
- Tag endpoints appropriately
|
- Tag endpoints appropriately
|
||||||
- **File**: src/presentation/api/*.rs
|
- **File**: `src/presentation/api/*.rs`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T083** [P] Run cargo clippy and fix all warnings
|
- [ ] **T083** [P] Run cargo clippy and fix all warnings
|
||||||
@@ -1172,19 +1172,19 @@
|
|||||||
- Document environment variables
|
- Document environment variables
|
||||||
- Document Modbus device configuration
|
- Document Modbus device configuration
|
||||||
- Add quickstart guide
|
- Add quickstart guide
|
||||||
- **File**: README.md
|
- **File**: `README.md`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T088** [P] Create Docker image for backend
|
- [ ] **T088** [P] Create Docker image for backend
|
||||||
- Multi-stage build with Rust
|
- Multi-stage build with Rust
|
||||||
- Include SQLite database setup
|
- Include SQLite database setup
|
||||||
- **File**: Dockerfile
|
- **File**: `Dockerfile`
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T089** [P] Create production settings/production.yaml
|
- [ ] **T089** [P] Create production settings/production.yaml
|
||||||
- Configure for actual device IP
|
- Configure for actual device IP
|
||||||
- Set appropriate timeouts and retry settings
|
- Set appropriate timeouts and retry settings
|
||||||
- **File**: settings/production.yaml
|
- **File**: `settings/production.yaml`
|
||||||
- **Complexity**: Low | **Uncertainty**: Low
|
- **Complexity**: Low | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T090** Deploy to production environment
|
- [ ] **T090** Deploy to production environment
|
||||||
|
|||||||
Reference in New Issue
Block a user