diff --git a/include/secsgem/gem/store/host_commands.hpp b/include/secsgem/gem/store/host_commands.hpp index e2bf10b..4d1867d 100644 --- a/include/secsgem/gem/store/host_commands.hpp +++ b/include/secsgem/gem/store/host_commands.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -48,19 +49,41 @@ class HostCommandRegistry { std::optional force_spool; }; + // Behaviour hook: vendor code that actually *does* something when the host + // sends a command (run a recipe, move hardware, validate the parameters). + // The handler receives the live parameter list and returns + // the HCACK to send back; returning anything other than Accept suppresses + // the Spec's declarative side effects, since the router only applies + // emit_ceid / set_alarm / force_spool on Accept. Without a handler a + // command stays purely declarative (the YAML Spec's static ack), which is + // the pre-existing behaviour. + using Handler = std::function& params)>; + void register_command(std::string rcmd, Spec spec) { by_rcmd_.insert_or_assign(std::move(rcmd), std::move(spec)); } + // Attach behaviour to an already-registered command. Independent of + // register_command so the data dictionary (YAML) and the behaviour + // (application code) can be wired separately. + void set_handler(std::string rcmd, Handler h) { + handlers_.insert_or_assign(std::move(rcmd), std::move(h)); + } bool has(const std::string& rcmd) const { return by_rcmd_.count(rcmd) > 0; } + bool has_handler(const std::string& rcmd) const { return handlers_.count(rcmd) > 0; } Result dispatch(const std::string& rcmd, - const std::vector& /*params*/) const { + const std::vector& params) const { auto it = by_rcmd_.find(rcmd); if (it == by_rcmd_.end()) return {HostCmdAck::InvalidCommand, std::nullopt, std::nullopt, std::nullopt}; - return {it->second.ack, it->second.emit_ceid, it->second.set_alarm, it->second.force_spool}; + HostCmdAck ack = it->second.ack; + if (auto hit = handlers_.find(rcmd); hit != handlers_.end() && hit->second) + ack = hit->second(rcmd, params); // vendor behaviour decides the outcome + return {ack, it->second.emit_ceid, it->second.set_alarm, it->second.force_spool}; } private: std::map by_rcmd_; + std::map handlers_; }; } // namespace secsgem::gem diff --git a/tests/test_data_model.cpp b/tests/test_data_model.cpp index 36b8ef1..17833f6 100644 --- a/tests/test_data_model.cpp +++ b/tests/test_data_model.cpp @@ -117,6 +117,73 @@ TEST_CASE("host command registry returns spec + result") { CHECK(r.dispatch("UNKNOWN", {}).ack == HostCmdAck::InvalidCommand); } +TEST_CASE("host command handler runs vendor behaviour and decides the ack") { + HostCommandRegistry r; + r.register_command("START", {HostCmdAck::Accept, 300, std::nullopt}); + + // Behaviour: record that we ran, read a parameter, and choose the ack. + bool ran = false; + std::string seen_ppid; + r.set_handler("START", [&](const std::string& rcmd, + const std::vector& params) { + ran = true; + CHECK(rcmd == "START"); + for (const auto& p : params) + if (p.name == "PPID") seen_ppid = p.value.as_ascii(); + return HostCmdAck::Accept; + }); + + CHECK(r.has_handler("START")); + CHECK_FALSE(r.has_handler("STOP")); + + std::vector params{{"PPID", s2::Item::ascii("RECIPE-A")}}; + auto res = r.dispatch("START", params); + + CHECK(ran); + CHECK(seen_ppid == "RECIPE-A"); + CHECK(res.ack == HostCmdAck::Accept); + // Declarative side effects from the Spec still travel with the result. + CHECK(res.emit_ceid.value_or(0) == 300); +} + +TEST_CASE("host command handler can reject, overriding the declarative ack") { + HostCommandRegistry r; + // YAML default says Accept; the handler decides otherwise at runtime. + r.register_command("START", {HostCmdAck::Accept, 300, std::nullopt}); + r.set_handler("START", [](const std::string&, + const std::vector&) { + return HostCmdAck::CannotDoNow; // e.g. equipment busy + }); + + auto res = r.dispatch("START", {}); + CHECK(res.ack == HostCmdAck::CannotDoNow); + // emit_ceid is still carried, but the router only applies it on Accept, + // so a rejecting handler naturally suppresses the event. + CHECK(res.emit_ceid.value_or(0) == 300); +} + +TEST_CASE("host command with no handler stays purely declarative") { + HostCommandRegistry r; + r.register_command("STOP", {HostCmdAck::Accept, std::nullopt, std::nullopt}); + // No set_handler call: behaviour is exactly the pre-existing YAML default. + CHECK_FALSE(r.has_handler("STOP")); + CHECK(r.dispatch("STOP", {}).ack == HostCmdAck::Accept); +} + +TEST_CASE("host command handler on an unregistered command is never reached") { + HostCommandRegistry r; + bool ran = false; + // Handler attached but the command was never registered in the dictionary. + r.set_handler("GHOST", [&](const std::string&, + const std::vector&) { + ran = true; + return HostCmdAck::Accept; + }); + auto res = r.dispatch("GHOST", {}); + CHECK(res.ack == HostCmdAck::InvalidCommand); + CHECK_FALSE(ran); // unknown commands short-circuit before the handler +} + // ---- Event reports ------------------------------------------------------- TEST_CASE("define reports rejects unknown VID") {