11using System ;
22using System . Collections . Generic ;
3+ using System . IO ;
34using System . Linq ;
45using System . Text . Json ;
56using Xunit ;
@@ -16,14 +17,19 @@ private class GatewayClientTestHelper
1617
1718 public OpenClawGatewayClient Client => _client ;
1819
19- public GatewayClientTestHelper ( bool tokenIsBootstrapToken = false , bool bootstrapPairAsNode = false , string gatewayUrl = "ws://localhost:18789" )
20+ public GatewayClientTestHelper (
21+ bool tokenIsBootstrapToken = false ,
22+ bool bootstrapPairAsNode = false ,
23+ string gatewayUrl = "ws://localhost:18789" ,
24+ string ? identityPath = null )
2025 {
2126 _client = new OpenClawGatewayClient (
2227 gatewayUrl ,
2328 "test-token" ,
2429 new TestLogger ( ) ,
2530 tokenIsBootstrapToken ,
26- bootstrapPairAsNode ) ;
31+ bootstrapPairAsNode ,
32+ identityPath ) ;
2733 }
2834
2935 public GatewayClientTestHelper ( IOpenClawLogger logger )
@@ -343,6 +349,24 @@ public void SetDeviceTokenForTest(string? token, string[]? scopes = null)
343349 SetPrivateField ( "_connectAuthToken" , token ?? "test-token" ) ;
344350 }
345351
352+ public string ? GetStoredOperatorDeviceToken ( )
353+ {
354+ var identityField = typeof ( OpenClawGatewayClient ) . GetField (
355+ "_deviceIdentity" ,
356+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
357+ var identity = identityField ! . GetValue ( _client ) ! ;
358+ return ( string ? ) identity . GetType ( ) . GetProperty ( "DeviceToken" ) ! . GetValue ( identity ) ;
359+ }
360+
361+ public string ? GetStoredNodeDeviceToken ( )
362+ {
363+ var identityField = typeof ( OpenClawGatewayClient ) . GetField (
364+ "_deviceIdentity" ,
365+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
366+ var identity = identityField ! . GetValue ( _client ) ! ;
367+ return ( string ? ) identity . GetType ( ) . GetProperty ( "NodeDeviceToken" ) ! . GetValue ( identity ) ;
368+ }
369+
346370 public string GetFallbackDeviceId ( )
347371 {
348372 var identityField = typeof ( OpenClawGatewayClient ) . GetField (
@@ -412,6 +436,9 @@ public List<string> CaptureAuthenticationFailedEvents()
412436 }
413437 }
414438
439+ private static string CreateTempIdentityPath ( ) =>
440+ Path . Combine ( Path . GetTempPath ( ) , "OpenClawGatewayClientTests" , Guid . NewGuid ( ) . ToString ( "N" ) ) ;
441+
415442 [ Fact ]
416443 public void OperatorConnect_FreshDevice_RequestsBootstrapHandoffScopes ( )
417444 {
@@ -536,7 +563,10 @@ public void OperatorConnect_PairedDeviceWithStoredScopes_RequestsStoredScopes()
536563 [ Fact ]
537564 public void BootstrapNodeHandoff_FreshDevice_RequestsNodeRoleWithoutScopes ( )
538565 {
539- var helper = new GatewayClientTestHelper ( tokenIsBootstrapToken : true , bootstrapPairAsNode : true ) ;
566+ var helper = new GatewayClientTestHelper (
567+ tokenIsBootstrapToken : true ,
568+ bootstrapPairAsNode : true ,
569+ identityPath : CreateTempIdentityPath ( ) ) ;
540570 helper . SetDeviceTokenForTest ( null ) ;
541571
542572 var auth = helper . BuildAuthPayload ( ) ;
@@ -548,6 +578,69 @@ public void BootstrapNodeHandoff_FreshDevice_RequestsNodeRoleWithoutScopes()
548578 Assert . False ( auth . ContainsKey ( "deviceToken" ) ) ;
549579 }
550580
581+ [ Fact ]
582+ public void BootstrapNodeHandoff_HelloOkWithNodeRole_DoesNotStorePrimaryNodeTokenAsOperator ( )
583+ {
584+ var helper = new GatewayClientTestHelper (
585+ tokenIsBootstrapToken : true ,
586+ bootstrapPairAsNode : true ,
587+ identityPath : CreateTempIdentityPath ( ) ) ;
588+ helper . SetDeviceTokenForTest ( null ) ;
589+
590+ helper . ProcessRawMessage ( """
591+ {
592+ "type": "res",
593+ "id": "req-hello-node",
594+ "payload": {
595+ "type": "hello-ok",
596+ "auth": {
597+ "deviceToken": "node-token",
598+ "role": "node",
599+ "scopes": []
600+ }
601+ }
602+ }
603+ """ ) ;
604+
605+ Assert . Equal ( "node-token" , helper . GetStoredNodeDeviceToken ( ) ) ;
606+ Assert . Null ( helper . GetStoredOperatorDeviceToken ( ) ) ;
607+ }
608+
609+ [ Fact ]
610+ public void BootstrapNodeHandoff_HelloOkWithOperatorHandoffToken_StoresOperatorToken ( )
611+ {
612+ var helper = new GatewayClientTestHelper (
613+ tokenIsBootstrapToken : true ,
614+ bootstrapPairAsNode : true ,
615+ identityPath : CreateTempIdentityPath ( ) ) ;
616+ helper . SetDeviceTokenForTest ( null ) ;
617+
618+ helper . ProcessRawMessage ( """
619+ {
620+ "type": "res",
621+ "id": "req-hello-node",
622+ "payload": {
623+ "type": "hello-ok",
624+ "auth": {
625+ "deviceToken": "node-token",
626+ "role": "node",
627+ "scopes": [],
628+ "deviceTokens": [
629+ {
630+ "deviceToken": "operator-token",
631+ "role": "operator",
632+ "scopes": ["operator.read"]
633+ }
634+ ]
635+ }
636+ }
637+ }
638+ """ ) ;
639+
640+ Assert . Equal ( "node-token" , helper . GetStoredNodeDeviceToken ( ) ) ;
641+ Assert . Equal ( "operator-token" , helper . GetStoredOperatorDeviceToken ( ) ) ;
642+ }
643+
551644 [ Fact ]
552645 public void BootstrapNodeHandoff_PrefersOperatorTokenFromAdditionalDeviceTokens ( )
553646 {
0 commit comments