#include #include #include "secsgem/gem/store/substrates.hpp" #include "secsgem/gem/substrate_state.hpp" using namespace secsgem::gem; TEST_CASE("SubstrateStateMachine: initial AtSource / NeedsProcessing") { SubstrateStateMachine s; CHECK(s.location_state() == SubstrateState::AtSource); CHECK(s.processing_state() == SubstrateProcessingState::NeedsProcessing); } TEST_CASE("Substrate location: AtSource -> AtWork -> AtDestination") { SubstrateStateMachine s; CHECK(s.on_location_event(SubstrateEvent::Acquire)); CHECK(s.location_state() == SubstrateState::AtWork); CHECK(s.on_location_event(SubstrateEvent::Release)); CHECK(s.location_state() == SubstrateState::AtDestination); } TEST_CASE("Substrate location: Return from AtWork sends back to AtSource") { SubstrateStateMachine s; s.on_location_event(SubstrateEvent::Acquire); CHECK(s.on_location_event(SubstrateEvent::Return)); CHECK(s.location_state() == SubstrateState::AtSource); } TEST_CASE("Substrate processing: happy path Start -> End -> Processed") { SubstrateStateMachine s; CHECK(s.on_processing_event(SubstrateProcessingEvent::StartProcessing)); CHECK(s.processing_state() == SubstrateProcessingState::InProcess); CHECK(s.on_processing_event(SubstrateProcessingEvent::EndProcessing)); CHECK(s.processing_state() == SubstrateProcessingState::Processed); } TEST_CASE("Substrate processing: Abort/Stop/Reject/Lost are terminal") { for (auto ev : {SubstrateProcessingEvent::Abort, SubstrateProcessingEvent::Stop, SubstrateProcessingEvent::Reject, SubstrateProcessingEvent::ReportLost}) { SubstrateStateMachine s; s.on_processing_event(SubstrateProcessingEvent::StartProcessing); CHECK(s.on_processing_event(ev)); // Terminal: no further transitions accepted. CHECK_FALSE(s.on_processing_event(SubstrateProcessingEvent::EndProcessing)); } } TEST_CASE("Substrate processing: Skip from NeedsProcessing") { SubstrateStateMachine s; CHECK(s.on_processing_event(SubstrateProcessingEvent::Skip)); CHECK(s.processing_state() == SubstrateProcessingState::Skipped); } // ---- SubstrateStore ----------------------------------------------------- TEST_CASE("SubstrateStore: create + dedup + location update") { SubstrateStore s; CHECK(s.create("W-001", "CAR-A", /*slot=*/1) == SubstrateStore::CreateResult::Created); CHECK(s.create("W-001") == SubstrateStore::CreateResult::Denied_AlreadyExists); REQUIRE(s.has("W-001")); CHECK(s.get("W-001")->carrierid == "CAR-A"); CHECK(s.get("W-001")->slot == 1); CHECK(s.fire_location_event("W-001", SubstrateEvent::Acquire, "MOD-A")); CHECK(s.get("W-001")->location == "MOD-A"); CHECK(s.get("W-001")->fsm->location_state() == SubstrateState::AtWork); } // ---- Substrate ID Status (E90 ยง6.4.6) ----------------------------------- TEST_CASE("Substrate IDS: NotConfirmed -> WaitingForHost -> Confirmed") { SubstrateStateMachine s; CHECK(s.id_state() == SubstrateIDStatus::NotConfirmed); CHECK(s.on_id_event(SubstrateIDEvent::Read)); CHECK(s.id_state() == SubstrateIDStatus::WaitingForHost); CHECK(s.on_id_event(SubstrateIDEvent::Confirm)); CHECK(s.id_state() == SubstrateIDStatus::Confirmed); } TEST_CASE("Substrate IDS: Mismatch + Bind recovers") { SubstrateStateMachine s; s.on_id_event(SubstrateIDEvent::Read); CHECK(s.on_id_event(SubstrateIDEvent::Mismatch)); CHECK(s.id_state() == SubstrateIDStatus::Mismatched); CHECK(s.on_id_event(SubstrateIDEvent::Bind)); CHECK(s.id_state() == SubstrateIDStatus::Confirmed); } TEST_CASE("Substrate IDS: Reset from any state returns to NotConfirmed") { SubstrateStateMachine s; s.on_id_event(SubstrateIDEvent::Read); s.on_id_event(SubstrateIDEvent::Confirm); CHECK(s.on_id_event(SubstrateIDEvent::Reset)); CHECK(s.id_state() == SubstrateIDStatus::NotConfirmed); } TEST_CASE("Substrate IDS: change handler observes transitions only") { SubstrateStateMachine s; int calls = 0; s.set_id_handler([&](SubstrateIDStatus, SubstrateIDStatus, SubstrateIDEvent) { ++calls; }); s.on_id_event(SubstrateIDEvent::Read); s.on_id_event(SubstrateIDEvent::Confirm); CHECK(calls == 2); // Re-confirming from Confirmed: no row, no call. s.on_id_event(SubstrateIDEvent::Confirm); CHECK(calls == 2); } TEST_CASE("SubstrateStore: history records every transition") { SubstrateStore s; s.create("W-H", "CAR-X", 3); s.fire_location_event("W-H", SubstrateEvent::Acquire, "MOD-A"); s.fire_processing_event("W-H", SubstrateProcessingEvent::StartProcessing); s.fire_processing_event("W-H", SubstrateProcessingEvent::EndProcessing); s.fire_location_event("W-H", SubstrateEvent::Release, "DEST"); const auto* hist = s.history("W-H"); REQUIRE(hist != nullptr); CHECK(hist->size() == 4); // First entry is a location transition to AtWork. CHECK((*hist)[0].location_to == SubstrateState::AtWork); CHECK((*hist)[0].processing_to == SubstrateProcessingState::NoState); CHECK((*hist)[0].location_label == "MOD-A"); // Third entry is a processing transition to Processed. CHECK((*hist)[2].processing_to == SubstrateProcessingState::Processed); CHECK((*hist)[2].location_to == SubstrateState::NoState); } TEST_CASE("SubstrateStore: history_limit caps retention") { SubstrateStore s; s.set_history_limit(3); s.create("W-CAP"); // Generate more transitions than the cap. s.fire_location_event("W-CAP", SubstrateEvent::Acquire); s.fire_location_event("W-CAP", SubstrateEvent::Return); s.fire_location_event("W-CAP", SubstrateEvent::Acquire); s.fire_location_event("W-CAP", SubstrateEvent::Release); const auto* hist = s.history("W-CAP"); REQUIRE(hist != nullptr); CHECK(hist->size() == 3); // Oldest entry (Acquire to AtWork) should be evicted; first remaining // entry is the Return. CHECK((*hist)[0].location_to == SubstrateState::AtSource); } TEST_CASE("SubstrateStore: change handlers observe both axes independently") { SubstrateStore s; std::vector> loc_log; std::vector> proc_log; s.set_location_handler([&](const std::string& id, SubstrateState, SubstrateState to, SubstrateEvent) { loc_log.emplace_back(id, to); }); s.set_processing_handler([&](const std::string& id, SubstrateProcessingState, SubstrateProcessingState to, SubstrateProcessingEvent) { proc_log.emplace_back(id, to); }); s.create("W-X"); s.fire_location_event("W-X", SubstrateEvent::Acquire); s.fire_processing_event("W-X", SubstrateProcessingEvent::StartProcessing); s.fire_processing_event("W-X", SubstrateProcessingEvent::EndProcessing); s.fire_location_event("W-X", SubstrateEvent::Release); CHECK(loc_log.size() == 2); CHECK(proc_log.size() == 2); CHECK(loc_log.back().second == SubstrateState::AtDestination); CHECK(proc_log.back().second == SubstrateProcessingState::Processed); }