@@ -227,6 +227,26 @@ private T GetPrivateField<T>(string fieldName)
227227 System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
228228 return ( T ) ( field ! . GetValue ( _client ) ?? throw new InvalidOperationException ( $ "Missing field value: { fieldName } ") ) ;
229229 }
230+
231+ public void SetGrantedScopes ( string [ ] scopes ) => SetPrivateField ( "_grantedOperatorScopes" , scopes ) ;
232+
233+ public void SetOperatorDeviceId ( string ? id ) => SetPrivateField ( "_operatorDeviceId" , id ) ;
234+
235+ public string CallBuildMissingScopeFixCommands ( string missingScope ) =>
236+ _client . BuildMissingScopeFixCommands ( missingScope ) ;
237+
238+ public string CallBuildPairingApprovalFixCommands ( ) =>
239+ _client . BuildPairingApprovalFixCommands ( ) ;
240+
241+ public string GetFallbackDeviceId ( )
242+ {
243+ var identityField = typeof ( OpenClawGatewayClient ) . GetField (
244+ "_deviceIdentity" ,
245+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
246+ var identity = identityField ! . GetValue ( _client ) ! ;
247+ var deviceIdProp = identity . GetType ( ) . GetProperty ( "DeviceId" ) ;
248+ return ( string ) deviceIdProp ! . GetValue ( identity ) ! ;
249+ }
230250 }
231251
232252 private class TestLogger : IOpenClawLogger
@@ -813,4 +833,172 @@ public void ParseChannelHealth_StatusField_TakesPriorityOverDerivedStatus()
813833 Assert . Single ( channels ) ;
814834 Assert . Equal ( "degraded" , channels [ 0 ] . Status ) ;
815835 }
836+
837+ // ── BuildMissingScopeFixCommands tests ─────────────────────────────────────
838+
839+ [ Fact ]
840+ public void BuildMissingScopeFixCommands_NullOrEmptyScope_DefaultsToOperatorWrite ( )
841+ {
842+ var helper = new GatewayClientTestHelper ( ) ;
843+
844+ var output = helper . CallBuildMissingScopeFixCommands ( "" ) ;
845+
846+ Assert . Contains ( "Missing scope: operator.write" , output ) ;
847+ }
848+
849+ [ Fact ]
850+ public void BuildMissingScopeFixCommands_WhitespaceScope_DefaultsToOperatorWrite ( )
851+ {
852+ var helper = new GatewayClientTestHelper ( ) ;
853+
854+ var output = helper . CallBuildMissingScopeFixCommands ( " " ) ;
855+
856+ Assert . Contains ( "Missing scope: operator.write" , output ) ;
857+ }
858+
859+ [ Fact ]
860+ public void BuildMissingScopeFixCommands_WithSpecificScope_IncludesItInOutput ( )
861+ {
862+ var helper = new GatewayClientTestHelper ( ) ;
863+
864+ var output = helper . CallBuildMissingScopeFixCommands ( "operator.approvals" ) ;
865+
866+ Assert . Contains ( "Missing scope: operator.approvals" , output ) ;
867+ }
868+
869+ [ Fact ]
870+ public void BuildMissingScopeFixCommands_EmptyGrantedScopes_ShowsNoneReportedPlaceholder ( )
871+ {
872+ var helper = new GatewayClientTestHelper ( ) ;
873+ // _grantedOperatorScopes is empty by default
874+
875+ var output = helper . CallBuildMissingScopeFixCommands ( "operator.write" ) ;
876+
877+ Assert . Contains ( "(none reported by gateway)" , output ) ;
878+ }
879+
880+ [ Fact ]
881+ public void BuildMissingScopeFixCommands_WithGrantedScopes_ListsScopesInOutput ( )
882+ {
883+ var helper = new GatewayClientTestHelper ( ) ;
884+ helper . SetGrantedScopes ( [ "operator.read" , "operator.admin" ] ) ;
885+
886+ var output = helper . CallBuildMissingScopeFixCommands ( "operator.write" ) ;
887+
888+ Assert . Contains ( "operator.read, operator.admin" , output ) ;
889+ }
890+
891+ [ Fact ]
892+ public void BuildMissingScopeFixCommands_WithOperatorDeviceId_IncludesItInOutput ( )
893+ {
894+ var helper = new GatewayClientTestHelper ( ) ;
895+ helper . SetOperatorDeviceId ( "test-device-id-abc123" ) ;
896+
897+ var output = helper . CallBuildMissingScopeFixCommands ( "operator.write" ) ;
898+
899+ Assert . Contains ( "test-device-id-abc123" , output ) ;
900+ }
901+
902+ [ Fact ]
903+ public void BuildMissingScopeFixCommands_NoOperatorDeviceId_ShowsNotReportedPlaceholder ( )
904+ {
905+ var helper = new GatewayClientTestHelper ( ) ;
906+ // _operatorDeviceId is null by default
907+
908+ var output = helper . CallBuildMissingScopeFixCommands ( "operator.write" ) ;
909+
910+ Assert . Contains ( "(not reported for this operator connection)" , output ) ;
911+ }
912+
913+ [ Fact ]
914+ public void BuildMissingScopeFixCommands_WithNodeScopes_ShowsNodeTokenWarning ( )
915+ {
916+ var helper = new GatewayClientTestHelper ( ) ;
917+ helper . SetGrantedScopes ( [ "node.read" , "node.write" ] ) ;
918+
919+ var output = helper . CallBuildMissingScopeFixCommands ( "operator.write" ) ;
920+
921+ Assert . Contains ( "Detected node.* scopes" , output ) ;
922+ Assert . Contains ( "node token" , output ) ;
923+ }
924+
925+ [ Fact ]
926+ public void BuildMissingScopeFixCommands_WithOnlyOperatorScopes_NoNodeTokenWarning ( )
927+ {
928+ var helper = new GatewayClientTestHelper ( ) ;
929+ helper . SetGrantedScopes ( [ "operator.read" , "operator.write" ] ) ;
930+
931+ var output = helper . CallBuildMissingScopeFixCommands ( "operator.write" ) ;
932+
933+ Assert . DoesNotContain ( "node token" , output ) ;
934+ }
935+
936+ [ Fact ]
937+ public void BuildMissingScopeFixCommands_NodeScopeIsCaseInsensitive ( )
938+ {
939+ var helper = new GatewayClientTestHelper ( ) ;
940+ helper . SetGrantedScopes ( [ "NODE.read" ] ) ;
941+
942+ var output = helper . CallBuildMissingScopeFixCommands ( "operator.write" ) ;
943+
944+ Assert . Contains ( "Detected node.* scopes" , output ) ;
945+ }
946+
947+ // ── BuildPairingApprovalFixCommands tests ──────────────────────────────────
948+
949+ [ Fact ]
950+ public void BuildPairingApprovalFixCommands_WithOperatorDeviceId_UsesItInOutput ( )
951+ {
952+ var helper = new GatewayClientTestHelper ( ) ;
953+ helper . SetOperatorDeviceId ( "operator-device-abc" ) ;
954+
955+ var output = helper . CallBuildPairingApprovalFixCommands ( ) ;
956+
957+ Assert . Contains ( "operator-device-abc" , output ) ;
958+ }
959+
960+ [ Fact ]
961+ public void BuildPairingApprovalFixCommands_NoOperatorDeviceId_FallsBackToDeviceIdentity ( )
962+ {
963+ var helper = new GatewayClientTestHelper ( ) ;
964+ // _operatorDeviceId is null by default
965+
966+ var fallbackId = helper . GetFallbackDeviceId ( ) ;
967+ var output = helper . CallBuildPairingApprovalFixCommands ( ) ;
968+
969+ Assert . Contains ( fallbackId , output ) ;
970+ }
971+
972+ [ Fact ]
973+ public void BuildPairingApprovalFixCommands_EmptyGrantedScopes_ShowsNoneYetPlaceholder ( )
974+ {
975+ var helper = new GatewayClientTestHelper ( ) ;
976+ // _grantedOperatorScopes is empty by default
977+
978+ var output = helper . CallBuildPairingApprovalFixCommands ( ) ;
979+
980+ Assert . Contains ( "(none reported by gateway yet)" , output ) ;
981+ }
982+
983+ [ Fact ]
984+ public void BuildPairingApprovalFixCommands_WithGrantedScopes_ListsThemInOutput ( )
985+ {
986+ var helper = new GatewayClientTestHelper ( ) ;
987+ helper . SetGrantedScopes ( [ "operator.read" , "operator.pairing" ] ) ;
988+
989+ var output = helper . CallBuildPairingApprovalFixCommands ( ) ;
990+
991+ Assert . Contains ( "operator.read, operator.pairing" , output ) ;
992+ }
993+
994+ [ Fact ]
995+ public void BuildPairingApprovalFixCommands_ContainsApprovalInstructions ( )
996+ {
997+ var helper = new GatewayClientTestHelper ( ) ;
998+
999+ var output = helper . CallBuildPairingApprovalFixCommands ( ) ;
1000+
1001+ Assert . Contains ( "pairing required" , output ) ;
1002+ Assert . Contains ( "Approve this Windows tray device ID" , output ) ;
1003+ }
8161004}
0 commit comments