#include #include #include "secsgem/gem/exception_state.hpp" #include "secsgem/gem/store/exceptions.hpp" using namespace secsgem::gem; TEST_CASE("ExceptionStateMachine: default starts Posted") { ExceptionStateMachine fsm; CHECK(fsm.state() == ExceptionState::Posted); } TEST_CASE("ExceptionStateMachine: Posted -> Recovering via host Recover") { ExceptionStateMachine fsm; CHECK(fsm.on_host_command(ExceptionEvent::Recover) == AlarmAck::Accept); CHECK(fsm.state() == ExceptionState::Recovering); } TEST_CASE("ExceptionStateMachine: Recovering -> Cleared on RecoveryComplete") { ExceptionStateMachine fsm; fsm.on_host_command(ExceptionEvent::Recover); REQUIRE(fsm.state() == ExceptionState::Recovering); CHECK(fsm.on_internal(ExceptionEvent::RecoveryComplete)); CHECK(fsm.state() == ExceptionState::Cleared); } TEST_CASE("ExceptionStateMachine: Recovering -> RecoverFailed -> Recovering retry") { ExceptionStateMachine fsm; fsm.on_host_command(ExceptionEvent::Recover); CHECK(fsm.on_internal(ExceptionEvent::RecoveryFailed)); CHECK(fsm.state() == ExceptionState::RecoverFailed); CHECK(fsm.on_host_command(ExceptionEvent::Recover) == AlarmAck::Accept); CHECK(fsm.state() == ExceptionState::Recovering); } TEST_CASE("ExceptionStateMachine: RecoveryAbort returns to Posted") { ExceptionStateMachine fsm; fsm.on_host_command(ExceptionEvent::Recover); REQUIRE(fsm.state() == ExceptionState::Recovering); CHECK(fsm.on_host_command(ExceptionEvent::RecoveryAbort) == AlarmAck::Accept); CHECK(fsm.state() == ExceptionState::Posted); } TEST_CASE("ExceptionStateMachine: autonomous Clear from Posted -> Cleared") { ExceptionStateMachine fsm; CHECK(fsm.on_internal(ExceptionEvent::Clear)); CHECK(fsm.state() == ExceptionState::Cleared); } TEST_CASE("ExceptionStateMachine: illegal host event from Cleared returns Error") { ExceptionStateMachine fsm(ExceptionTransitionTable::default_table(), ExceptionState::Cleared); CHECK(fsm.on_host_command(ExceptionEvent::Recover) == AlarmAck::Error); } // ---- ExceptionStore ----------------------------------------------------- TEST_CASE("ExceptionStore: post rejects duplicate EXID") { ExceptionStore store; CHECK(store.post(42, "FATAL", "vacuum lost", {"RETRY"}) == ExceptionStore::PostResult::Posted); CHECK(store.post(42, "FATAL", "vacuum lost", {"RETRY"}) == ExceptionStore::PostResult::Denied_AlreadyExists); CHECK(store.size() == 1); } TEST_CASE("ExceptionStore: on_recover validates EXRECVRA against post list") { ExceptionStore store; store.post(1, "WARN", "fan stalled", {"RETRY", "RESTART"}); CHECK(store.on_recover(1, "RETRY") == AlarmAck::Accept); // Same EXID, different action — must come from the original list. store.post(2, "WARN", "x", {"ONLY"}); CHECK(store.on_recover(2, "BOGUS") == AlarmAck::Error); CHECK(store.on_recover(99, "RETRY") == AlarmAck::Error); // unknown EXID } TEST_CASE("ExceptionStore: Cleared removes the entry") { ExceptionStore store; store.post(7, "INFO", "x", {"OK"}); REQUIRE(store.has(7)); store.on_recover(7, "OK"); store.fire_internal(7, ExceptionEvent::RecoveryComplete); CHECK_FALSE(store.has(7)); CHECK(store.size() == 0); } TEST_CASE("ExceptionStore: state-change handler observes Created + transitions") { ExceptionStore store; std::vector> log; store.set_state_change_handler( [&](uint32_t exid, ExceptionState f, ExceptionState t, ExceptionEvent) { log.emplace_back(exid, f, t); }); store.post(42, "FATAL", "x", {"R"}); store.on_recover(42, "R"); store.fire_internal(42, ExceptionEvent::RecoveryComplete); REQUIRE(log.size() == 3); CHECK(std::get<1>(log[0]) == ExceptionState::NoState); CHECK(std::get<2>(log[0]) == ExceptionState::Posted); CHECK(std::get<2>(log[1]) == ExceptionState::Recovering); CHECK(std::get<2>(log[2]) == ExceptionState::Cleared); }