feat(application): HealthMonitor service and hardware integration test

Add HealthMonitor service for tracking system health status with
comprehensive state transition logic and thread-safe operations.
Includes 16 unit tests covering all functionality including concurrent
access scenarios.

Add optional Modbus hardware integration tests with 7 test cases for
real device testing. Tests are marked as ignored and can be run with

running 21 tests
test infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_error_on_failure ... FAILED
test infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_timeout_on_slow_device ... FAILED
test infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_timeout_on_slow_response ... FAILED
test infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_modbus_exception_on_protocol_error ... FAILED
test infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_on_when_coil_is_true ... FAILED
test infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_propagates_controller_error ... FAILED
test infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_off_when_coil_is_false ... FAILED
test infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_connection_error_on_io_error ... FAILED
test infrastructure::modbus::client::tests::t025a_connection_setup_tests::test_new_with_valid_config_connects_successfully ... ok
test infrastructure::modbus::client::tests::t025a_connection_setup_tests::test_new_stores_correct_timeout_duration ... ok
test infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_coil_values_on_success ... ok
test infrastructure::modbus::client::tests::write_all_states_validation_tests::test_write_all_states_with_9_states_returns_invalid_input ... ok
test infrastructure::modbus::client::tests::write_all_states_validation_tests::test_write_all_states_with_empty_vector_returns_invalid_input ... ok
test infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_can_toggle_relay_multiple_times ... ok
test infrastructure::modbus::client::tests::write_all_states_validation_tests::test_write_all_states_with_8_states_succeeds ... ok
test infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_succeeds_for_valid_write ... ok
test infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_off_writes_false_to_coil ... FAILED
test infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_correctly_maps_relay_id_to_modbus_address ... ok
test infrastructure::modbus::client::tests::write_all_states_validation_tests::test_write_all_states_with_7_states_returns_invalid_input ... ok
test infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_on_writes_true_to_coil ... ok
test infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_correctly_maps_relay_id_to_modbus_address ... ok

failures:

---- infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_error_on_failure stdout ----

thread 'infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_error_on_failure' (1157113) panicked at backend/src/infrastructure/modbus/client_test.rs:320:14:
Failed to connect: ConnectionError("Connection refused (os error 111)")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_timeout_on_slow_device stdout ----

thread 'infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_timeout_on_slow_device' (1157114) panicked at backend/src/infrastructure/modbus/client_test.rs:293:14:
Failed to connect: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_timeout_on_slow_response stdout ----

thread 'infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_timeout_on_slow_response' (1157112) panicked at backend/src/infrastructure/modbus/client_test.rs:176:14:
Failed to connect: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_modbus_exception_on_protocol_error stdout ----

thread 'infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_modbus_exception_on_protocol_error' (1157111) panicked at backend/src/infrastructure/modbus/client_test.rs:227:14:
Failed to connect: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_on_when_coil_is_true stdout ----

thread 'infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_on_when_coil_is_true' (1157119) panicked at backend/src/infrastructure/modbus/client_test.rs:354:14:
Failed to connect to test server: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_propagates_controller_error stdout ----

thread 'infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_propagates_controller_error' (1157117) panicked at backend/src/infrastructure/modbus/client_test.rs:396:14:
Failed to connect to test server: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_off_when_coil_is_false stdout ----

thread 'infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_off_when_coil_is_false' (1157118) panicked at backend/src/infrastructure/modbus/client_test.rs:375:14:
Failed to connect to test server: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_connection_error_on_io_error stdout ----

thread 'infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_connection_error_on_io_error' (1157110) panicked at backend/src/infrastructure/modbus/client_test.rs:202:14:
Failed to connect: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_off_writes_false_to_coil stdout ----

thread 'infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_off_writes_false_to_coil' (1157122) panicked at backend/src/infrastructure/modbus/client_test.rs:508:9:
assertion `left == right` failed: Relay should be Off after writing Off state
  left: On
 right: Off


failures:
    infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_connection_error_on_io_error
    infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_modbus_exception_on_protocol_error
    infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_timeout_on_slow_response
    infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_error_on_failure
    infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_timeout_on_slow_device
    infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_propagates_controller_error
    infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_off_when_coil_is_false
    infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_on_when_coil_is_true
    infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_off_writes_false_to_coil

test result: FAILED. 12 passed; 9 failed; 0 ignored; 0 measured; 128 filtered out; finished in 3.27s.

Ref: T034, T039, T040 (specs/001-modbus-relay-control/tasks.org)
This commit is contained in:
2026-01-21 20:43:06 +01:00
parent 1cb4d5f3fc
commit ce186095fa
10 changed files with 664 additions and 41 deletions

View File

@@ -331,13 +331,13 @@
--------------
*** STARTED T025: ModbusRelayController Implementation (DECOMPOSED) [9/13]
*** STARTED T025: ModbusRelayController Implementation (DECOMPOSED) [12/13]
- Complexity :: High → Broken into 6 sub-tasks
- Uncertainty :: High
- Rationale :: Nested Result handling, =Arc<Mutex>= synchronization, timeout wrapping
- Protocol :: Native Modbus TCP (MBAP header, no CRC16 validation)
- [X] *T025a* [US1] [TDD] Implement =ModbusRelayController= connection setup
- [X] *T025a* [US1] [TDD] Implement =ModbusRelayController= connection setup [3/3]
- Struct: =ModbusRelayController { ctx: Arc<Mutex<Context>>, timeout_duration: Duration }=
- Constructor: =new(host, port, slave_id, timeout_secs) → Result<Self, ControllerError>=
@@ -382,7 +382,7 @@
- [X] Test: =new()= with invalid host returns =ConnectionError=
- [X] Test: =new()= stores correct timeout_duration
- [X] *T025b* [US1] [TDD] Implement timeout-wrapped =read_coils= helper
- [X] *T025b* [US1] [TDD] Implement timeout-wrapped =read_coils= helper [4/4]
- Private method: =read_coils_with_timeout(addr: u16, count: u16) → Result<Vec<bool>, ControllerError>=
- Wrap =ctx.read_coils()= with =tokio::time::timeout()=
@@ -421,7 +421,7 @@
- [X] Test: =read_coils_with_timeout()= returns =ConnectionError= on =io::Error=
- [X] Test: =read_coils_with_timeout()= returns =ModbusException= on protocol error
- [X] *T025c* [US1] [TDD] Implement timeout-wrapped =write_single_coil= helper
- [X] *T025c* [US1] [TDD] Implement timeout-wrapped =write_single_coil= helper [3/3]
- Private method: =write_single_coil_with_timeout(addr: u16, value: bool) → Result<(), ControllerError>=
- Similar nested Result handling as T025b
@@ -454,7 +454,7 @@
- [X] Test: =write_single_coil_with_timeout()= returns Timeout on slow device
- [X] Test: =write_single_coil_with_timeout()= returns appropriate error on failure
- [X] *T025d* [US1] [TDD] Implement =RelayController::read_state()= using helpers
- [X] *T025d* [US1] [TDD] Implement =RelayController::read_state()= using helpers [3/3]
- Convert =RelayId==ModbusAddress= (0-based)
- Call =read_coils_with_timeout(addr, 1)=
@@ -482,7 +482,7 @@
- [X] Test: =read_state(RelayId(1))= returns =Off= when coil is false
- [X] Test: =read_state()= propagates =ControllerError= from helper
- [X] *T025e* [US1] [TDD] Implement =RelayController::write_state()= using helpers
- [X] *T025e* [US1] [TDD] Implement =RelayController::write_state()= using helpers [2/2]
- Convert =RelayId==ModbusAddress=
- Convert =RelayState= → bool (On=true, Off=false)
@@ -505,7 +505,7 @@
- [X] Test: =write_state(RelayId(1), RelayState::On)= writes true to coil
- [X] Test: =write_state(RelayId(1), RelayState::Off)= writes false to coil
- [X] *T025f* [US1] [TDD] Implement =RelayController::read_all()= and =write_all()=
- [X] *T025f* [US1] [TDD] Implement =RelayController::read_all()= and =write_all()= [3/3]
- =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
@@ -545,7 +545,7 @@
--------------
- [ ] *T034* [US1] [TDD] Integration test with real hardware (optional)
- [X] *T034* [US1] [TDD] Integration test with real hardware (optional)
- *REQUIRES PHYSICAL DEVICE*: Test against actual Modbus relay at configured IP
- Skip if device unavailable, rely on =MockRelayController= for CI
- *File*: =tests/integration/modbus_hardware_test.rs=
@@ -570,12 +570,12 @@
- HashMap-based implementation
- *File*: =src/infrastructure/persistence/mock_label_repository.rs=
- *Complexity*: Low | *Uncertainty*: Low
- [ ] *T039* [US3] [TDD] Write tests for =HealthMonitor= service
- [X] *T039* [US3] [TDD] Write tests for =HealthMonitor= service
- Test: =track_success()= transitions =Degraded==Healthy=
- Test: =track_failure()= transitions =Healthy==Degraded==Unhealthy=
- *File*: =src/application/health_monitor.rs=
- *Complexity*: Medium | *Uncertainty*: Low
- [ ] *T040* [US3] [TDD] Implement =HealthMonitor= service
- [X] *T040* [US3] [TDD] Implement =HealthMonitor= service
- Track consecutive errors, transition states per FR-020, FR-021
- *File*: =src/application/health_monitor.rs=
- *Complexity*: Medium | *Uncertainty*: Low