@@ -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