Skip to content

Commit 7e2e7e0

Browse files
NomDeTomclaude
andcommitted
TrafficManagement: adapt tests to forwarded throttles and hourly membership
Throttle tests (PSRAM and NodeDB-fallback paths): a request inside the 30 s window now CONTINUEs into normal relay handling instead of being consumed - assert no spoofed TX, no NAK suppression, and (PSRAM path) that nodeinfo_cache_hits counts only replies actually sent. Membership test (renamed reconcileMembershipMarking): the per-minute sweep no longer refreshes isMember, so the test now pins down both halves of the new contract - the very next sweep after a passive NodeDB eviction still shows the stale member bit (the documented up-to-an-hour lag), and a reconcile interval's worth of sweeps clears it. Disabled-module test additionally proves the write-through hooks share the has_traffic_management gate: a key commit while disabled must not land in the NodeInfo cache. Not covered here: a build permutation with TRAFFIC_MANAGEMENT_CACHE_SIZE overridden to 0 (the configuration the maintenance-guard fix protects) would need its own PlatformIO env plus guards on every unified-cache test - left as a follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent cfe1e62 commit 7e2e7e0

1 file changed

Lines changed: 36 additions & 12 deletions

File tree

test/test_traffic_management/test_main.cpp

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,14 @@ static void test_tm_moduleDisabled_doesNothing(void)
364364
TEST_ASSERT_EQUAL_UINT32(0, stats.packets_inspected);
365365
TEST_ASSERT_EQUAL_UINT32(0, stats.unknown_packet_drops);
366366
TEST_ASSERT_FALSE(module.ignoreRequestFlag());
367+
368+
// The write-through hooks share the disabled gate: with maintenance (sweep + reconcile)
369+
// off, NodeDB commits must not fill the NodeInfo cache either.
370+
uint8_t key[32];
371+
memset(key, 0x42, sizeof(key));
372+
module.onNodeKeyCommitted(kRemoteNode, key, false);
373+
uint8_t out[32];
374+
TEST_ASSERT_FALSE(module.copyPublicKey(kRemoteNode, out, nullptr));
367375
}
368376

369377
/**
@@ -893,12 +901,16 @@ static void test_tm_nodeinfo_directResponse_psramThrottlesWithinWindow(void)
893901
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(module.handleReceived(request)));
894902
TEST_ASSERT_EQUAL_UINT32(1, static_cast<uint32_t>(mockRouter.sentPackets.size()));
895903

896-
// Second request within the throttle window: suppressed (STOP) but NOT transmitted again.
904+
// Second request within the throttle window: our spoofed TX is suppressed, but the request
905+
// is NOT consumed - it CONTINUEs into normal relay handling so the genuine target can answer
906+
// itself. (Previously it was black-holed: STOPped with nothing sent.) The cache-hit stat
907+
// counts only replies actually sent.
897908
TrafficManagementModule::s_testNowMs += 5000; // 5 s < 30 s window
898909
request.id = 0xAAAA0002;
899-
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(module.handleReceived(request)));
900-
TEST_ASSERT_TRUE(module.ignoreRequestFlag());
910+
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(module.handleReceived(request)));
911+
TEST_ASSERT_FALSE(module.ignoreRequestFlag());
901912
TEST_ASSERT_EQUAL_UINT32(1, static_cast<uint32_t>(mockRouter.sentPackets.size()));
913+
TEST_ASSERT_EQUAL_UINT32(1, module.getStats().nodeinfo_cache_hits);
902914

903915
// Past the throttle window: served again.
904916
TrafficManagementModule::s_testNowMs += 30000; // now > 30 s since first reply
@@ -1542,11 +1554,13 @@ static void test_tm_nodeinfo_tickSaturation_sweepClearsObserved(void)
15421554
}
15431555

15441556
/**
1545-
* Membership marking (ported from tmm-fix-superset): the sweep marks entries whose node
1546-
* exists in NodeDB and clears the mark when the node is gone; the entry itself persists
1547-
* (no TTL) and reconciliation-seeded identities stay unservable.
1557+
* Membership marking: the hourly reconcile marks entries whose node exists in NodeDB and
1558+
* clears the mark when the node is gone. A plain 60 s sweep does NOT refresh membership
1559+
* (that per-entry NodeDB scan was moved into the reconcile), so a passive NodeDB eviction
1560+
* lags by up to an hour; the entry itself persists (no TTL) and reconciliation-seeded
1561+
* identities stay unservable.
15481562
*/
1549-
static void test_tm_nodeinfo_sweepMembershipMarking(void)
1563+
static void test_tm_nodeinfo_reconcileMembershipMarking(void)
15501564
{
15511565
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
15521566
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
@@ -1568,10 +1582,19 @@ static void test_tm_nodeinfo_sweepMembershipMarking(void)
15681582
TEST_ASSERT_TRUE(flags & kFlagFullUser);
15691583
TEST_ASSERT_FALSE(flags & kFlagObserved);
15701584

1571-
// Node drops out of NodeDB entirely: the next sweep clears the membership mark.
1585+
// Node drops out of NodeDB entirely. Membership is refreshed by the hourly reconcile,
1586+
// not the per-minute sweep, so the very next sweep still shows the stale member bit -
1587+
// the documented up-to-an-hour lag for passive evictions.
15721588
mockNodeDB->rollHotStore();
15731589
module.runOnce();
15741590
flags = module.peekNodeInfoFlagsForTest(kTargetNode);
1591+
TEST_ASSERT_TRUE(flags >= 0);
1592+
TEST_ASSERT_TRUE(flags & kFlagMember); // stale by design between reconciles
1593+
1594+
// After a reconcile interval's worth of sweeps, the mark is cleared.
1595+
for (int i = 0; i < 60; i++)
1596+
module.runOnce();
1597+
flags = module.peekNodeInfoFlagsForTest(kTargetNode);
15751598
TEST_ASSERT_TRUE(flags >= 0); // entry persists (no TTL) ...
15761599
TEST_ASSERT_FALSE(flags & kFlagMember); // ... but is no longer pinned as a member
15771600
}
@@ -1694,11 +1717,12 @@ static void test_tm_nodeinfo_directResponse_fallbackThrottlesWithinWindow(void)
16941717
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(module.handleReceived(request)));
16951718
TEST_ASSERT_EQUAL_UINT32(1, static_cast<uint32_t>(mockRouter.sentPackets.size()));
16961719

1697-
// Second request within the throttle window: suppressed (STOP) but NOT transmitted again.
1720+
// Second request within the throttle window: our spoofed TX is suppressed, but the request
1721+
// is NOT consumed - it CONTINUEs so the genuine target (or another cache-holder) can answer.
16981722
TrafficManagementModule::s_testNowMs += 5000; // 5 s < 30 s window
16991723
request.id = 0xBBBB0002;
1700-
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(module.handleReceived(request)));
1701-
TEST_ASSERT_TRUE(module.ignoreRequestFlag());
1724+
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(module.handleReceived(request)));
1725+
TEST_ASSERT_FALSE(module.ignoreRequestFlag());
17021726
TEST_ASSERT_EQUAL_UINT32(1, static_cast<uint32_t>(mockRouter.sentPackets.size()));
17031727

17041728
// Past the throttle window: served again.
@@ -2765,7 +2789,7 @@ TM_TEST_ENTRY void setup()
27652789
#endif
27662790
RUN_TEST(test_tm_nodeinfo_keyHook_upsertsAndGovernsProvenance);
27672791
RUN_TEST(test_tm_nodeinfo_tickSaturation_sweepClearsObserved);
2768-
RUN_TEST(test_tm_nodeinfo_sweepMembershipMarking);
2792+
RUN_TEST(test_tm_nodeinfo_reconcileMembershipMarking);
27692793
#endif
27702794
RUN_TEST(test_tm_alterReceived_telemetryBroadcast_hopLimitUnchanged);
27712795
RUN_TEST(test_tm_alterReceived_skipsLocalAndUnicast);

0 commit comments

Comments
 (0)