@@ -5,6 +5,7 @@ use crate::utils::loop_heartbeats::LoopHeartbeats;
55use alloy:: primitives:: Address ;
66use anyhow:: Error ;
77use anyhow:: Result ;
8+ use chrono:: Utc ;
89use log:: { error, info} ;
910use serde_json;
1011use shared:: models:: api:: ApiResponse ;
@@ -225,7 +226,8 @@ impl DiscoveryMonitor {
225226 }
226227 Ok ( None ) => {
227228 info ! ( "Discovered new validated node: {}" , node_address) ;
228- let node = OrchestratorNode :: from ( discovery_node. clone ( ) ) ;
229+ let mut node = OrchestratorNode :: from ( discovery_node. clone ( ) ) ;
230+ node. first_seen = Some ( Utc :: now ( ) ) ;
229231 let _ = self . store_context . node_store . add_node ( node. clone ( ) ) . await ;
230232 }
231233 Err ( e) => {
@@ -292,6 +294,7 @@ mod tests {
292294 let mut orchestrator_node = OrchestratorNode :: from ( discovery_node. clone ( ) ) ;
293295 orchestrator_node. status = NodeStatus :: Ejected ;
294296 orchestrator_node. address = discovery_node. node . id . parse :: < Address > ( ) . unwrap ( ) ;
297+ orchestrator_node. first_seen = Some ( Utc :: now ( ) ) ;
295298 orchestrator_node. compute_specs = Some ( ComputeSpecs {
296299 gpu : None ,
297300 cpu : None ,
@@ -364,4 +367,133 @@ mod tests {
364367 assert_eq ! ( node. status, NodeStatus :: Dead ) ;
365368 }
366369 }
370+
371+ #[ tokio:: test]
372+ async fn test_first_seen_timestamp_set_on_new_node ( ) {
373+ let node_address = "0x2234567890123456789012345678901234567890" ;
374+ let discovery_node = DiscoveryNode {
375+ is_validated : true ,
376+ is_provider_whitelisted : true ,
377+ is_active : true ,
378+ node : Node {
379+ id : node_address. to_string ( ) ,
380+ provider_address : node_address. to_string ( ) ,
381+ ip_address : "192.168.1.100" . to_string ( ) ,
382+ port : 8080 ,
383+ compute_pool_id : 1 ,
384+ compute_specs : None ,
385+ } ,
386+ is_blacklisted : false ,
387+ last_updated : None ,
388+ created_at : None ,
389+ } ;
390+
391+ let store = Arc :: new ( RedisStore :: new_test ( ) ) ;
392+ let mut con = store
393+ . client
394+ . get_connection ( )
395+ . expect ( "Should connect to test Redis instance" ) ;
396+
397+ redis:: cmd ( "PING" )
398+ . query :: < String > ( & mut con)
399+ . expect ( "Redis should be responsive" ) ;
400+ redis:: cmd ( "FLUSHALL" )
401+ . query :: < String > ( & mut con)
402+ . expect ( "Redis should be flushed" ) ;
403+
404+ let store_context = Arc :: new ( StoreContext :: new ( store. clone ( ) ) ) ;
405+
406+ let fake_wallet = Wallet :: new (
407+ "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97" ,
408+ Url :: parse ( "http://localhost:8545" ) . unwrap ( ) ,
409+ )
410+ . unwrap ( ) ;
411+
412+ let mode = ServerMode :: Full ;
413+
414+ let discovery_monitor = DiscoveryMonitor :: new (
415+ fake_wallet,
416+ 1 ,
417+ 10 ,
418+ "http://localhost:8080" . to_string ( ) ,
419+ store_context. clone ( ) ,
420+ Arc :: new ( LoopHeartbeats :: new ( & mode) ) ,
421+ ) ;
422+
423+ let time_before = Utc :: now ( ) ;
424+
425+ // Sync a new node that doesn't exist in the store
426+ discovery_monitor
427+ . sync_single_node_with_discovery ( & discovery_node)
428+ . await
429+ . unwrap ( ) ;
430+
431+ let time_after = Utc :: now ( ) ;
432+
433+ // Verify the node was added with first_seen timestamp
434+ let node_from_store = store_context
435+ . node_store
436+ . get_node ( & discovery_node. node . id . parse :: < Address > ( ) . unwrap ( ) )
437+ . await
438+ . unwrap ( ) ;
439+
440+ assert ! ( node_from_store. is_some( ) ) ;
441+ let node = node_from_store. unwrap ( ) ;
442+
443+ // Verify first_seen is set
444+ assert ! ( node. first_seen. is_some( ) ) ;
445+ let first_seen = node. first_seen . unwrap ( ) ;
446+
447+ // Verify the timestamp is within the expected range
448+ assert ! ( first_seen >= time_before && first_seen <= time_after) ;
449+
450+ // Verify other fields are set correctly
451+ assert_eq ! ( node. status, NodeStatus :: Discovered ) ;
452+ assert_eq ! ( node. ip_address, "192.168.1.100" ) ;
453+
454+ // Test case: Sync the same node again to verify first_seen is preserved
455+ // Simulate some time passing
456+ tokio:: time:: sleep ( tokio:: time:: Duration :: from_millis ( 100 ) ) . await ;
457+
458+ // Update discovery data to simulate a change (e.g., IP address change)
459+ let updated_discovery_node = DiscoveryNode {
460+ is_validated : true ,
461+ is_provider_whitelisted : true ,
462+ is_active : true ,
463+ node : Node {
464+ id : node_address. to_string ( ) ,
465+ provider_address : node_address. to_string ( ) ,
466+ ip_address : "192.168.1.101" . to_string ( ) , // Changed IP
467+ port : 8080 ,
468+ compute_pool_id : 1 ,
469+ compute_specs : None ,
470+ } ,
471+ is_blacklisted : false ,
472+ last_updated : Some ( Utc :: now ( ) ) ,
473+ created_at : None ,
474+ } ;
475+
476+ // Sync the node again
477+ discovery_monitor
478+ . sync_single_node_with_discovery ( & updated_discovery_node)
479+ . await
480+ . unwrap ( ) ;
481+
482+ // Verify the node was updated but first_seen is preserved
483+ let node_after_resync = store_context
484+ . node_store
485+ . get_node ( & discovery_node. node . id . parse :: < Address > ( ) . unwrap ( ) )
486+ . await
487+ . unwrap ( )
488+ . unwrap ( ) ;
489+
490+ // Verify first_seen is still the same (preserved)
491+ assert_eq ! ( node_after_resync. first_seen, Some ( first_seen) ) ;
492+
493+ // Verify IP was updated
494+ assert_eq ! ( node_after_resync. ip_address, "192.168.1.101" ) ;
495+
496+ // Status should remain the same
497+ assert_eq ! ( node_after_resync. status, NodeStatus :: Discovered ) ;
498+ }
367499}
0 commit comments