#include "TestUtil.h" #include #if defined(ARCH_PORTDUINO) #define TM_TEST_ENTRY extern "C" #else #define TM_TEST_ENTRY #endif #if HAS_TRAFFIC_MANAGEMENT #include "mesh/CryptoEngine.h" #include "mesh/MeshService.h" #include "mesh/NodeDB.h" #include "mesh/Router.h" #include "modules/TrafficManagementModule.h" #include #include #include #include #include namespace { constexpr NodeNum kLocalNode = 0x11111111; constexpr NodeNum kRemoteNode = 0x22222222; constexpr NodeNum kTargetNode = 0x33333333; class MockNodeDB : public NodeDB { public: meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override { if (hasCachedNode && n == cachedNodeNum) return &cachedNode; return NodeDB::getMeshNode(n); } void clearCachedNode() { hasCachedNode = false; cachedNodeNum = 0; cachedNode = meshtastic_NodeInfoLite_init_zero; } void setCachedNode(NodeNum n) { clearCachedNode(); hasCachedNode = true; cachedNodeNum = n; cachedNode.num = n; cachedNode.has_user = true; } private: bool hasCachedNode = false; NodeNum cachedNodeNum = 0; meshtastic_NodeInfoLite cachedNode = meshtastic_NodeInfoLite_init_zero; }; class MockRadioInterface : public RadioInterface { public: ErrorCode send(meshtastic_MeshPacket *p) override { packetPool.release(p); return ERRNO_OK; } uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) override { (void)totalPacketLen; (void)received; return 0; } }; class MockRouter : public Router { public: ~MockRouter() { // Router allocates a global crypt lock in its constructor. // Clean it up here so each test can build a fresh mock router. delete cryptLock; cryptLock = nullptr; } ErrorCode send(meshtastic_MeshPacket *p) override { sentPackets.push_back(*p); packetPool.release(p); return ERRNO_OK; } std::vector sentPackets; }; class TrafficManagementModuleTestShim : public TrafficManagementModule { public: using TrafficManagementModule::alterReceived; using TrafficManagementModule::handleReceived; using TrafficManagementModule::resetEpoch; using TrafficManagementModule::runOnce; bool ignoreRequestFlag() const { return ignoreRequest; } }; MockNodeDB *mockNodeDB = nullptr; static void resetTrafficConfig() { moduleConfig = meshtastic_LocalModuleConfig_init_zero; moduleConfig.has_traffic_management = true; moduleConfig.traffic_management = meshtastic_ModuleConfig_TrafficManagementConfig_init_zero; moduleConfig.traffic_management.enabled = true; config = meshtastic_LocalConfig_init_zero; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; myNodeInfo.my_node_num = kLocalNode; router = nullptr; service = nullptr; mockNodeDB->resetNodes(); mockNodeDB->clearCachedNode(); nodeDB = mockNodeDB; } static meshtastic_MeshPacket makeDecodedPacket(meshtastic_PortNum port, NodeNum from, NodeNum to = NODENUM_BROADCAST) { meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; packet.from = from; packet.to = to; packet.id = 0x1001; packet.channel = 0; packet.hop_start = 3; packet.hop_limit = 3; packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag; packet.decoded.portnum = port; packet.decoded.has_bitfield = true; packet.decoded.bitfield = 0; return packet; } static meshtastic_MeshPacket makeUnknownPacket(NodeNum from, NodeNum to = NODENUM_BROADCAST) { meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; packet.from = from; packet.to = to; packet.id = 0x2001; packet.channel = 0; packet.hop_start = 3; packet.hop_limit = 3; packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag; packet.encrypted.size = 0; return packet; } static meshtastic_MeshPacket makePositionPacket(NodeNum from, int32_t lat, int32_t lon, NodeNum to = NODENUM_BROADCAST) { meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_POSITION_APP, from, to); meshtastic_Position pos = meshtastic_Position_init_zero; pos.has_latitude_i = true; pos.has_longitude_i = true; pos.latitude_i = lat; pos.longitude_i = lon; packet.decoded.payload.size = pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes), &meshtastic_Position_msg, &pos); return packet; } static meshtastic_MeshPacket makeNodeInfoPacket(NodeNum from, const char *longName, const char *shortName) { meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, from, NODENUM_BROADCAST); meshtastic_User user = meshtastic_User_init_zero; snprintf(user.id, sizeof(user.id), "!%08x", from); strncpy(user.long_name, longName, sizeof(user.long_name) - 1); strncpy(user.short_name, shortName, sizeof(user.short_name) - 1); packet.decoded.payload.size = pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes), &meshtastic_User_msg, &user); return packet; } /** * Verify the module is a no-op when traffic management is disabled. * Important so config toggles cannot accidentally change routing behavior. */ static void test_tm_moduleDisabled_doesNothing(void) { moduleConfig.has_traffic_management = false; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); ProcessMessage result = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); TEST_ASSERT_EQUAL_UINT32(0, stats.packets_inspected); TEST_ASSERT_EQUAL_UINT32(0, stats.unknown_packet_drops); TEST_ASSERT_FALSE(module.ignoreRequestFlag()); } /** * Verify unknown-packet dropping uses N+1 threshold semantics. * Important to catch off-by-one regressions in drop decisions. */ static void test_tm_unknownPackets_dropOnNPlusOne(void) { moduleConfig.traffic_management.drop_unknown_enabled = true; moduleConfig.traffic_management.unknown_packet_threshold = 2; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode); ProcessMessage r1 = module.handleReceived(packet); ProcessMessage r2 = module.handleReceived(packet); ProcessMessage r3 = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r3)); TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops); TEST_ASSERT_EQUAL_UINT32(3, stats.packets_inspected); TEST_ASSERT_TRUE(module.ignoreRequestFlag()); } /** * Verify duplicate position broadcasts inside the dedup window are dropped. * Important because this is the primary airtime-saving behavior. */ static void test_tm_positionDedup_dropsDuplicateWithinWindow(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(second); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_GREATER_THAN_UINT32(0, first.decoded.payload.size); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); TEST_ASSERT_TRUE(module.ignoreRequestFlag()); } /** * Verify changed coordinates are forwarded even with dedup enabled. * Important so real movement updates are never suppressed as duplicates. */ static void test_tm_positionDedup_allowsMovedPosition(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket moved = makePositionPacket(kRemoteNode, 384221234, -1210845678); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(moved); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); } /** * Verify rate limiting drops only after exceeding the configured threshold. * Important to protect threshold semantics from off-by-one regressions. */ static void test_tm_rateLimit_dropsOnlyAfterThreshold(void) { moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 60; moduleConfig.traffic_management.rate_limit_max_packets = 3; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); ProcessMessage r1 = module.handleReceived(packet); ProcessMessage r2 = module.handleReceived(packet); ProcessMessage r3 = module.handleReceived(packet); ProcessMessage r4 = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r4)); TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops); TEST_ASSERT_TRUE(module.ignoreRequestFlag()); } /** * Verify routing/admin traffic is exempt from rate limiting. * Important because throttling control traffic can destabilize the mesh. */ static void test_tm_rateLimit_skipsRoutingAndAdminPorts(void) { moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 60; moduleConfig.traffic_management.rate_limit_max_packets = 1; TrafficManagementModuleTestShim module; meshtastic_MeshPacket routingPacket = makeDecodedPacket(meshtastic_PortNum_ROUTING_APP, kRemoteNode); meshtastic_MeshPacket adminPacket = makeDecodedPacket(meshtastic_PortNum_ADMIN_APP, kRemoteNode); for (int i = 0; i < 4; i++) { ProcessMessage rr = module.handleReceived(routingPacket); ProcessMessage ar = module.handleReceived(adminPacket); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(rr)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(ar)); } meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops); } /** * Verify packets sourced from this node bypass dedup and rate limiting. * Important so local transmissions are not accidentally self-throttled. */ static void test_tm_fromUs_bypassesPositionAndRateFilters(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 300; moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 60; moduleConfig.traffic_management.rate_limit_max_packets = 1; TrafficManagementModuleTestShim module; meshtastic_MeshPacket positionPacket = makePositionPacket(kLocalNode, 374221234, -1220845678); meshtastic_MeshPacket textPacket = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode); ProcessMessage p1 = module.handleReceived(positionPacket); ProcessMessage p2 = module.handleReceived(positionPacket); ProcessMessage t1 = module.handleReceived(textPacket); ProcessMessage t2 = module.handleReceived(textPacket); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops); } /** * Verify locally addressed packets are never dropped by transit shaping. * Important so dedup/rate limiting do not suppress end-user delivery. */ static void test_tm_localDestination_bypassesTransitFilters(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 300; moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 60; moduleConfig.traffic_management.rate_limit_max_packets = 1; TrafficManagementModuleTestShim module; meshtastic_MeshPacket position1 = makePositionPacket(kRemoteNode, 374221234, -1220845678, kLocalNode); meshtastic_MeshPacket position2 = makePositionPacket(kRemoteNode, 374221234, -1220845678, kLocalNode); meshtastic_MeshPacket text1 = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode); meshtastic_MeshPacket text2 = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode); ProcessMessage p1 = module.handleReceived(position1); ProcessMessage p2 = module.handleReceived(position2); ProcessMessage t1 = module.handleReceived(text1); ProcessMessage t2 = module.handleReceived(text2); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops); } /** * Verify router role clamps NodeInfo response hops to router-safe maximum. * Important so large config values cannot widen response scope unexpectedly. */ static void test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER; mockNodeDB->setCachedNode(kTargetNode); TrafficManagementModuleTestShim module; meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); request.decoded.want_response = true; request.hop_start = 5; request.hop_limit = 1; // 4 hops away; router clamp should cap max at 3 ProcessMessage result = module.handleReceived(request); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); TEST_ASSERT_FALSE(module.ignoreRequestFlag()); } /** * Verify NodeInfo direct-response success path and reply packet fields. * Important because this path consumes the request and generates a spoofed cached reply. */ static void test_tm_nodeinfo_directResponse_respondsFromCache(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; config.lora.config_ok_to_mqtt = true; mockNodeDB->setCachedNode(kTargetNode); MockRouter mockRouter; mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); MeshService mockService; router = &mockRouter; service = &mockService; TrafficManagementModuleTestShim module; meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); request.decoded.want_response = true; request.id = 0x13572468; request.hop_start = 3; request.hop_limit = 3; // direct request (0 hops away) ProcessMessage result = module.handleReceived(request); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); TEST_ASSERT_TRUE(module.ignoreRequestFlag()); TEST_ASSERT_EQUAL_UINT32(1, stats.nodeinfo_cache_hits); TEST_ASSERT_EQUAL_UINT32(1, static_cast(mockRouter.sentPackets.size())); const meshtastic_MeshPacket &reply = mockRouter.sentPackets.front(); TEST_ASSERT_EQUAL_INT(meshtastic_PortNum_NODEINFO_APP, reply.decoded.portnum); TEST_ASSERT_EQUAL_UINT32(kTargetNode, reply.from); TEST_ASSERT_EQUAL_UINT32(kRemoteNode, reply.to); TEST_ASSERT_EQUAL_UINT32(request.id, reply.decoded.request_id); TEST_ASSERT_FALSE(reply.decoded.want_response); TEST_ASSERT_EQUAL_UINT8(0, reply.hop_limit); TEST_ASSERT_EQUAL_UINT8(0, reply.hop_start); TEST_ASSERT_EQUAL_UINT8(mockNodeDB->getLastByteOfNodeNum(kRemoteNode), reply.next_hop); TEST_ASSERT_TRUE(reply.decoded.has_bitfield); TEST_ASSERT_EQUAL_UINT8(BITFIELD_OK_TO_MQTT_MASK, reply.decoded.bitfield); } /** * Verify cached direct replies still preserve requester NodeInfo learning. * Important so consuming the request does not skip NodeDB refresh for observers. */ static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; mockNodeDB->setCachedNode(kTargetNode); MockRouter mockRouter; mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); MeshService mockService; router = &mockRouter; service = &mockService; TrafficManagementModuleTestShim module; meshtastic_MeshPacket request = makeNodeInfoPacket(kRemoteNode, "requester-long", "rq"); request.to = kTargetNode; request.decoded.want_response = true; request.id = 0x01020304; request.hop_start = 3; request.hop_limit = 3; ProcessMessage result = module.handleReceived(request); meshtastic_NodeInfoLite *requestor = mockNodeDB->getMeshNode(kRemoteNode); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); TEST_ASSERT_NOT_NULL(requestor); TEST_ASSERT_TRUE(requestor->has_user); TEST_ASSERT_EQUAL_STRING("requester-long", requestor->user.long_name); TEST_ASSERT_EQUAL_STRING("rq", requestor->user.short_name); TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel); } /** * Verify client role only answers direct (0-hop) NodeInfo requests. * Important so clients do not answer relayed requests outside intended scope. */ static void test_tm_nodeinfo_clientClamp_skipsWhenNotDirect(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; mockNodeDB->setCachedNode(kTargetNode); TrafficManagementModuleTestShim module; meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); request.decoded.want_response = true; request.hop_start = 2; request.hop_limit = 1; // 1 hop away; clients are clamped to max 0 ProcessMessage result = module.handleReceived(request); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); TEST_ASSERT_FALSE(module.ignoreRequestFlag()); } #if !(defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)) /** * Verify non-PSRAM builds require NodeDB for direct NodeInfo responses. * Important because fallback should only happen through node-wide data when * the dedicated PSRAM cache does not exist. */ static void test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; mockNodeDB->clearCachedNode(); MockRouter mockRouter; mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); MeshService mockService; router = &mockRouter; service = &mockService; TrafficManagementModuleTestShim module; meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); request.decoded.want_response = true; request.hop_start = 3; request.hop_limit = 3; ProcessMessage result = module.handleReceived(request); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); TEST_ASSERT_FALSE(module.ignoreRequestFlag()); TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); TEST_ASSERT_EQUAL_UINT32(0, static_cast(mockRouter.sentPackets.size())); } #endif #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) /** * Verify PSRAM NodeInfo cache can answer requests without NodeDB and that * shouldRespondToNodeInfo() uses cached bitfield metadata. */ static void test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfield(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; config.lora.config_ok_to_mqtt = true; mockNodeDB->clearCachedNode(); MockRouter mockRouter; mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); MeshService mockService; router = &mockRouter; service = &mockService; TrafficManagementModuleTestShim module; meshtastic_MeshPacket observed = makeNodeInfoPacket(kTargetNode, "target-long", "tg"); observed.decoded.has_bitfield = true; observed.decoded.bitfield = BITFIELD_WANT_RESPONSE_MASK; observed.channel = 2; observed.rx_time = 123456; ProcessMessage observedResult = module.handleReceived(observed); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(observedResult)); meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); request.decoded.want_response = true; request.id = 0x24681357; request.channel = 1; request.hop_start = 3; request.hop_limit = 3; ProcessMessage result = module.handleReceived(request); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); TEST_ASSERT_TRUE(module.ignoreRequestFlag()); TEST_ASSERT_EQUAL_UINT32(1, stats.nodeinfo_cache_hits); TEST_ASSERT_EQUAL_UINT32(1, static_cast(mockRouter.sentPackets.size())); const meshtastic_MeshPacket &reply = mockRouter.sentPackets.front(); TEST_ASSERT_TRUE(reply.decoded.has_bitfield); TEST_ASSERT_EQUAL_UINT8(static_cast(BITFIELD_WANT_RESPONSE_MASK | BITFIELD_OK_TO_MQTT_MASK), reply.decoded.bitfield); TEST_ASSERT_EQUAL_UINT32(kTargetNode, reply.from); TEST_ASSERT_EQUAL_UINT32(kRemoteNode, reply.to); TEST_ASSERT_EQUAL_UINT8(request.channel, reply.channel); TEST_ASSERT_EQUAL_UINT32(request.id, reply.decoded.request_id); } /** * Verify PSRAM cache misses do not fall back to NodeDB. * Important so the dedicated PSRAM index stays logically separate from * NodeInfoModule/NodeDB when PSRAM is available. */ static void test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; mockNodeDB->setCachedNode(kTargetNode); MockRouter mockRouter; mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); MeshService mockService; router = &mockRouter; service = &mockService; TrafficManagementModuleTestShim module; meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); request.decoded.want_response = true; request.hop_start = 3; request.hop_limit = 3; ProcessMessage result = module.handleReceived(request); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); TEST_ASSERT_FALSE(module.ignoreRequestFlag()); TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); TEST_ASSERT_EQUAL_UINT32(0, static_cast(mockRouter.sentPackets.size())); } #endif /** * Verify relayed telemetry broadcasts are hop-exhausted when enabled. * Important to prevent further mesh propagation while still allowing one relay step. */ static void test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast(void) { moduleConfig.traffic_management.exhaust_hop_telemetry = true; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST); packet.hop_start = 5; packet.hop_limit = 3; module.alterReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_UINT8(0, packet.hop_limit); TEST_ASSERT_EQUAL_UINT8(3, packet.hop_start); TEST_ASSERT_TRUE(module.shouldExhaustHops(packet)); TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets); } /** * Verify hop exhaustion skips unicast and local-origin packets. * Important to avoid mutating traffic that should retain normal forwarding behavior. */ static void test_tm_alterReceived_skipsLocalAndUnicast(void) { moduleConfig.traffic_management.exhaust_hop_telemetry = true; TrafficManagementModuleTestShim module; meshtastic_MeshPacket unicast = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kTargetNode); unicast.hop_start = 5; unicast.hop_limit = 3; module.alterReceived(unicast); TEST_ASSERT_EQUAL_UINT8(3, unicast.hop_limit); TEST_ASSERT_FALSE(module.shouldExhaustHops(unicast)); meshtastic_MeshPacket fromUs = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kLocalNode, NODENUM_BROADCAST); fromUs.hop_start = 5; fromUs.hop_limit = 3; module.alterReceived(fromUs); TEST_ASSERT_EQUAL_UINT8(3, fromUs.hop_limit); TEST_ASSERT_FALSE(module.shouldExhaustHops(fromUs)); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets); } /** * Verify position dedup window expires and later duplicates are allowed. * Important so periodic identical reports can resume after cooldown. */ static void test_tm_positionDedup_allowsDuplicateAfterIntervalExpires(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 1; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket third = makePositionPacket(kRemoteNode, 374221234, -1220845678); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(second); testDelay(1200); ProcessMessage r3 = module.handleReceived(third); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); } /** * Verify interval=0 disables position deduplication. * Important because this is an explicit configuration escape hatch. */ static void test_tm_positionDedup_intervalZero_neverDrops(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 0; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(second); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); } /** * Verify precision values above 32 fall back to default precision. * Important so invalid config uses the documented default behavior. */ static void test_tm_positionDedup_precisionAbove32_usesDefaultPrecision(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 99; moduleConfig.traffic_management.position_min_interval_secs = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 384221234, -1210845678); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(second); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); } /** * Verify precision=32 does not collapse all positions to one fingerprint. * Important to prevent false duplicate drops at the full-precision boundary. */ static void test_tm_positionDedup_precision32_allowsDistinctPositions(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 32; moduleConfig.traffic_management.position_min_interval_secs = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221235, -1220845677); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(second); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); } /** * Verify invalid precision=0 is treated as full precision. * Important so invalid config does not collapse all positions into one fingerprint. */ static void test_tm_positionDedup_precisionZero_allowsDistinctPositions(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 0; moduleConfig.traffic_management.position_min_interval_secs = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221235, -1220845677); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(second); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); } /** * Verify epoch reset invalidates stale position identity for dedup. * Important so reset paths cannot leak prior packet identity into new windows. */ static void test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket afterReset = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket duplicate = makePositionPacket(kRemoteNode, 374221234, -1220845678); ProcessMessage r1 = module.handleReceived(first); module.resetEpoch(millis()); ProcessMessage r2 = module.handleReceived(afterReset); ProcessMessage r3 = module.handleReceived(duplicate); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r3)); TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); } /** * Verify non-position cache state does not make the first fingerprint-0 position look duplicated. * Important so unified cache entries from other features cannot leak into dedup decisions. */ static void test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 300; moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 60; moduleConfig.traffic_management.rate_limit_max_packets = 10; TrafficManagementModuleTestShim module; meshtastic_MeshPacket text = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 0x12300000, 0x45600000); meshtastic_MeshPacket duplicate = makePositionPacket(kRemoteNode, 0x12300000, 0x45600000); ProcessMessage seeded = module.handleReceived(text); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(duplicate); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(seeded)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); } /** * Verify rate-limit counters reset after the window expires. * Important so temporary bursts do not cause persistent throttling. */ static void test_tm_rateLimit_resetsAfterWindowExpires(void) { moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 1; moduleConfig.traffic_management.rate_limit_max_packets = 1; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); ProcessMessage r1 = module.handleReceived(packet); ProcessMessage r2 = module.handleReceived(packet); testDelay(1200); ProcessMessage r3 = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops); } /** * Verify rate-limit thresholds above 255 effectively clamp to 255. * Important because counters are uint8_t and must not overflow behavior. */ static void test_tm_rateLimit_thresholdAbove255_clamps(void) { moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 60; moduleConfig.traffic_management.rate_limit_max_packets = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); for (int i = 0; i < 255; i++) { ProcessMessage result = module.handleReceived(packet); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); } ProcessMessage dropped = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(dropped)); TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops); } /** * Verify unknown-packet tracking resets after its active window expires. * Important so old unknown traffic does not trigger delayed drops. */ static void test_tm_unknownPackets_resetAfterWindowExpires(void) { moduleConfig.traffic_management.drop_unknown_enabled = true; moduleConfig.traffic_management.unknown_packet_threshold = 1; moduleConfig.traffic_management.rate_limit_window_secs = 1; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode); ProcessMessage r1 = module.handleReceived(packet); ProcessMessage r2 = module.handleReceived(packet); testDelay(1200); ProcessMessage r3 = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops); } /** * Verify unknown threshold values above 255 clamp to the counter ceiling. * Important to align config semantics with saturating counter storage. */ static void test_tm_unknownPackets_thresholdAbove255_clamps(void) { moduleConfig.traffic_management.drop_unknown_enabled = true; moduleConfig.traffic_management.unknown_packet_threshold = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode); for (int i = 0; i < 255; i++) { ProcessMessage result = module.handleReceived(packet); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); } ProcessMessage dropped = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(dropped)); TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops); } /** * Verify relayed position broadcasts can also be hop-exhausted. * Important because telemetry and position use separate exhaust flags. */ static void test_tm_alterReceived_exhaustsRelayedPositionBroadcast(void) { moduleConfig.traffic_management.exhaust_hop_position = true; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makePositionPacket(kRemoteNode, 374221234, -1220845678, NODENUM_BROADCAST); packet.hop_start = 5; packet.hop_limit = 2; module.alterReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_UINT8(0, packet.hop_limit); TEST_ASSERT_EQUAL_UINT8(4, packet.hop_start); TEST_ASSERT_TRUE(module.shouldExhaustHops(packet)); TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets); } /** * Verify hop exhaustion ignores undecoded/encrypted packets. * Important so we never mutate packets that were not decoded by this module. */ static void test_tm_alterReceived_skipsUndecodedPackets(void) { moduleConfig.traffic_management.exhaust_hop_telemetry = true; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode, NODENUM_BROADCAST); packet.hop_start = 5; packet.hop_limit = 3; module.alterReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_UINT8(5, packet.hop_start); TEST_ASSERT_EQUAL_UINT8(3, packet.hop_limit); TEST_ASSERT_FALSE(module.shouldExhaustHops(packet)); TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets); } /** * Verify exhaustRequested is per-packet and resets on next handleReceived(). * Important so a prior packet cannot leak hop-exhaust state into later packets. */ static void test_tm_alterReceived_resetExhaustFlagOnNextPacket(void) { moduleConfig.traffic_management.exhaust_hop_telemetry = true; TrafficManagementModuleTestShim module; meshtastic_MeshPacket telemetry = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST); telemetry.hop_start = 5; telemetry.hop_limit = 3; module.alterReceived(telemetry); TEST_ASSERT_TRUE(module.shouldExhaustHops(telemetry)); meshtastic_MeshPacket text = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); ProcessMessage result = module.handleReceived(text); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); TEST_ASSERT_FALSE(module.shouldExhaustHops(telemetry)); TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets); } /** * Verify exhaust requests are packet-scoped (from + id). * Important so stale state from one packet cannot influence unrelated packets * that pass through duplicate/rebroadcast paths before handleReceived(). */ static void test_tm_alterReceived_exhaustFlag_isPacketScoped(void) { moduleConfig.traffic_management.exhaust_hop_telemetry = true; TrafficManagementModuleTestShim module; meshtastic_MeshPacket exhausted = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST); exhausted.id = 0x1010; exhausted.hop_start = 5; exhausted.hop_limit = 3; module.alterReceived(exhausted); meshtastic_MeshPacket unrelated = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kTargetNode, NODENUM_BROADCAST); unrelated.id = 0x2020; unrelated.hop_start = 4; unrelated.hop_limit = 0; TEST_ASSERT_TRUE(module.shouldExhaustHops(exhausted)); TEST_ASSERT_FALSE(module.shouldExhaustHops(unrelated)); } /** * Verify runOnce() returns sleep-forever interval when module is disabled. * Important to ensure the maintenance thread is effectively inert when off. */ static void test_tm_runOnce_disabledReturnsMaxInterval(void) { moduleConfig.traffic_management.enabled = false; TrafficManagementModuleTestShim module; int32_t interval = module.runOnce(); TEST_ASSERT_EQUAL_INT32(INT32_MAX, interval); } /** * Verify runOnce() returns the maintenance cadence when enabled. * Important so periodic cache housekeeping continues at expected interval. */ static void test_tm_runOnce_enabledReturnsMaintenanceInterval(void) { TrafficManagementModuleTestShim module; int32_t interval = module.runOnce(); TEST_ASSERT_EQUAL_INT32(60 * 1000, interval); } } // namespace void setUp(void) { resetTrafficConfig(); } void tearDown(void) {} TM_TEST_ENTRY void setup() { delay(10); delay(2000); initializeTestEnvironment(); mockNodeDB = new MockNodeDB(); nodeDB = mockNodeDB; UNITY_BEGIN(); RUN_TEST(test_tm_moduleDisabled_doesNothing); RUN_TEST(test_tm_unknownPackets_dropOnNPlusOne); RUN_TEST(test_tm_positionDedup_dropsDuplicateWithinWindow); RUN_TEST(test_tm_positionDedup_allowsMovedPosition); RUN_TEST(test_tm_rateLimit_dropsOnlyAfterThreshold); RUN_TEST(test_tm_rateLimit_skipsRoutingAndAdminPorts); RUN_TEST(test_tm_fromUs_bypassesPositionAndRateFilters); RUN_TEST(test_tm_localDestination_bypassesTransitFilters); RUN_TEST(test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops); RUN_TEST(test_tm_nodeinfo_directResponse_respondsFromCache); RUN_TEST(test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo); RUN_TEST(test_tm_nodeinfo_clientClamp_skipsWhenNotDirect); #if !(defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)) RUN_TEST(test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips); #endif #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) RUN_TEST(test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfield); RUN_TEST(test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb); #endif RUN_TEST(test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast); RUN_TEST(test_tm_alterReceived_skipsLocalAndUnicast); RUN_TEST(test_tm_positionDedup_allowsDuplicateAfterIntervalExpires); RUN_TEST(test_tm_positionDedup_intervalZero_neverDrops); RUN_TEST(test_tm_positionDedup_precisionAbove32_usesDefaultPrecision); RUN_TEST(test_tm_positionDedup_precision32_allowsDistinctPositions); RUN_TEST(test_tm_positionDedup_precisionZero_allowsDistinctPositions); RUN_TEST(test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset); RUN_TEST(test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero); RUN_TEST(test_tm_rateLimit_resetsAfterWindowExpires); RUN_TEST(test_tm_rateLimit_thresholdAbove255_clamps); RUN_TEST(test_tm_unknownPackets_resetAfterWindowExpires); RUN_TEST(test_tm_unknownPackets_thresholdAbove255_clamps); RUN_TEST(test_tm_alterReceived_exhaustsRelayedPositionBroadcast); RUN_TEST(test_tm_alterReceived_skipsUndecodedPackets); RUN_TEST(test_tm_alterReceived_resetExhaustFlagOnNextPacket); RUN_TEST(test_tm_alterReceived_exhaustFlag_isPacketScoped); RUN_TEST(test_tm_runOnce_disabledReturnsMaxInterval); RUN_TEST(test_tm_runOnce_enabledReturnsMaintenanceInterval); exit(UNITY_END()); } TM_TEST_ENTRY void loop() {} #else void setUp(void) {} void tearDown(void) {} TM_TEST_ENTRY void setup() { initializeTestEnvironment(); UNITY_BEGIN(); exit(UNITY_END()); } TM_TEST_ENTRY void loop() {} #endif