#include #include "fgc/ChannelHistory.h" #include "fgc/ChannelTopics.h" #include using namespace fgc; TEST_CASE("ChannelHistory counts each delivery outcome separately") { ChannelHistory h; h.record(true, Delivery::Ok, "t/a", "1"); h.record(true, Delivery::Ok, "t/a", "2"); h.record(true, Delivery::Dropped, "t/a", "3"); h.record(true, Delivery::Failed, "t/a", "4"); h.record(false, Delivery::Ok, "t/b", "in"); ChannelStats s = h.stats(); CHECK(s.published_ok == 2); CHECK(s.published_dropped == 1); // the silent-drop path: the number that matters CHECK(s.published_failed == 1); CHECK(s.received == 1); // inbound never touches the publish counters } TEST_CASE("ChannelHistory returns messages oldest first and keeps their fields") { ChannelHistory h; h.record(true, Delivery::Dropped, "t/first", "a"); h.record(false, Delivery::Ok, "t/second", "b"); auto r = h.recent(); REQUIRE(r.size() == 2); CHECK(r[0].topic == "t/first"); CHECK(r[0].outbound); CHECK(r[0].delivery == Delivery::Dropped); CHECK(r[0].payload == "a"); CHECK(r[1].topic == "t/second"); CHECK_FALSE(r[1].outbound); CHECK(r[1].timestamp_ms > 0); } TEST_CASE("ChannelHistory evicts the oldest beyond its capacity") { ChannelHistory h; const int n = static_cast(ChannelHistory::kCapacity) + 10; for (int i = 0; i < n; ++i) h.record(true, Delivery::Ok, "t", std::to_string(i)); auto r = h.recent(); REQUIRE(r.size() == ChannelHistory::kCapacity); CHECK(r.front().payload == std::to_string(n - static_cast(ChannelHistory::kCapacity))); CHECK(r.back().payload == std::to_string(n - 1)); // Counters are cumulative; they must not be bounded by the ring. CHECK(h.stats().published_ok == n); } TEST_CASE("ChannelHistory tracks connects, losses and the last status sent") { ChannelHistory h; CHECK(h.stats().connects == 0); CHECK(h.stats().last_connect_ms == 0); h.noteConnect(); h.noteLoss(); h.noteConnect(); h.noteStatus("1"); ChannelStats s = h.stats(); CHECK(s.connects == 2); CHECK(s.last_connect_ms > 0); CHECK(s.last_loss_ms > 0); CHECK(s.last_status_payload == "1"); } TEST_CASE("topic and payload builders match the documented wire format") { CHECK(towerTopic("Tower", kTopicCamEvent) == "GGS/FWT/Tower/CamEvent"); CamEvent c; c.tower = "Tower"; c.camera = "RGB"; c.heading_decideg = 1373; c.pitch_decideg = 300; c.timestamp_ms = 1719312345678LL; const std::string j = camEventJson(c); CHECK(j.find("\"fwt\":\"Tower\"") != std::string::npos); CHECK(j.find("\"cam\":\"RGB\"") != std::string::npos); CHECK(j.find("\"hdg\":1373") != std::string::npos); CHECK(j.find("\"pit\":300") != std::string::npos); CHECK(j.find("\"time\":1719312345678") != std::string::npos); // The quality gate is gone; nothing should still be publishing a verdict. CHECK(j.find("degraded") == std::string::npos); EnvEvent e; e.tower = "Tower"; e.temp_c = 21.5f; e.humidity_pct = 48.0f; e.timestamp_ms = 42; const std::string ej = envJson(e); CHECK(ej.find("\"fwt\":\"Tower\"") != std::string::npos); CHECK(ej.find("\"temp_c\":21.5") != std::string::npos); CHECK(ej.find("\"time\":42") != std::string::npos); }