@@ -12,6 +12,8 @@ public class SetupStepsTests : IDisposable
1212 private readonly string _localTempDir ;
1313 private readonly string ? _prevDataDir ;
1414 private readonly string ? _prevLocalDataDir ;
15+ private const string DevicePairPluginNotFoundOutput = "plugins.entries.device-pair: plugin not found: device-pair" ;
16+ private const string OtherPluginNotFoundOutput = "plugins.entries.other-plugin: plugin not found: other-plugin" ;
1517
1618 public SetupStepsTests ( )
1719 {
@@ -1180,6 +1182,75 @@ public void IsKeepaliveCommandLine_RequiresDistroAndSleepInfinity()
11801182 "OpenClawGateway" ) ) ;
11811183 }
11821184
1185+ [ Fact ]
1186+ public async Task AutoApprovePairing_ReturnsTerminalForDevicePairPluginNotFound ( )
1187+ {
1188+ var ctx = CreatePairingContext ( DevicePairPluginNotFoundOutput ) ;
1189+
1190+ var result = await PairOperatorStep . AutoApprovePairing ( ctx , "device-req-1" , CancellationToken . None ) ;
1191+
1192+ Assert . Equal ( StepOutcome . FailedTerminal , result . Outcome ) ;
1193+ Assert . Equal ( ApprovalRequestHelper . PluginNotFoundMessage , result . Message ) ;
1194+ }
1195+
1196+ [ Fact ]
1197+ public async Task AutoApprovePairing_KeepsOtherMissingPluginRetriable ( )
1198+ {
1199+ var ctx = CreatePairingContext ( OtherPluginNotFoundOutput ) ;
1200+
1201+ var result = await PairOperatorStep . AutoApprovePairing ( ctx , "device-req-1" , CancellationToken . None ) ;
1202+
1203+ Assert . Equal ( StepOutcome . Failed , result . Outcome ) ;
1204+ Assert . Contains ( "Device approval failed" , result . Message ) ;
1205+ Assert . DoesNotContain ( ApprovalRequestHelper . PluginNotFoundMessage , result . Message ) ;
1206+ }
1207+
1208+ [ Fact ]
1209+ public async Task AutoApproveNodePairing_ReturnsTerminalWhenPendingListReportsDevicePairPluginNotFound ( )
1210+ {
1211+ var ctx = CreatePairingContext ( DevicePairPluginNotFoundOutput ) ;
1212+
1213+ var result = await PairNodeStep . AutoApproveNodePairing ( ctx , requestId : null , CancellationToken . None ) ;
1214+
1215+ Assert . Equal ( StepOutcome . FailedTerminal , result . Outcome ) ;
1216+ Assert . Equal ( ApprovalRequestHelper . PluginNotFoundMessage , result . Message ) ;
1217+ }
1218+
1219+ [ Fact ]
1220+ public async Task AutoApproveNodePairing_KeepsOtherPendingListMissingPluginRetriable ( )
1221+ {
1222+ var ctx = CreatePairingContext ( OtherPluginNotFoundOutput ) ;
1223+
1224+ var result = await PairNodeStep . AutoApproveNodePairing ( ctx , requestId : null , CancellationToken . None ) ;
1225+
1226+ Assert . Equal ( StepOutcome . Failed , result . Outcome ) ;
1227+ Assert . Contains ( "Could not list pending node pairing requests" , result . Message ) ;
1228+ Assert . DoesNotContain ( ApprovalRequestHelper . PluginNotFoundMessage , result . Message ) ;
1229+ }
1230+
1231+ [ Fact ]
1232+ public async Task AutoApproveNodePairing_ReturnsTerminalWhenApproveReportsDevicePairPluginNotFound ( )
1233+ {
1234+ var ctx = CreatePairingContext ( DevicePairPluginNotFoundOutput ) ;
1235+
1236+ var result = await PairNodeStep . AutoApproveNodePairing ( ctx , "node-req-1" , CancellationToken . None ) ;
1237+
1238+ Assert . Equal ( StepOutcome . FailedTerminal , result . Outcome ) ;
1239+ Assert . Equal ( ApprovalRequestHelper . PluginNotFoundMessage , result . Message ) ;
1240+ }
1241+
1242+ [ Fact ]
1243+ public async Task AutoApproveNodePairing_KeepsOtherApproveMissingPluginRetriable ( )
1244+ {
1245+ var ctx = CreatePairingContext ( OtherPluginNotFoundOutput ) ;
1246+
1247+ var result = await PairNodeStep . AutoApproveNodePairing ( ctx , "node-req-1" , CancellationToken . None ) ;
1248+
1249+ Assert . Equal ( StepOutcome . Failed , result . Outcome ) ;
1250+ Assert . Contains ( "Node approval failed" , result . Message ) ;
1251+ Assert . DoesNotContain ( ApprovalRequestHelper . PluginNotFoundMessage , result . Message ) ;
1252+ }
1253+
11831254 // ─── Bind validation ───
11841255
11851256 [ Fact ]
@@ -1298,9 +1369,23 @@ private static CommandResult Ok(string stdout = "", string stderr = "")
12981369 private static CommandResult Fail ( string stderr = "" )
12991370 => new ( 1 , "" , stderr , TimeSpan . Zero , TimedOut : false ) ;
13001371
1372+ private static CommandResult FailWithStdout ( string stdout )
1373+ => new ( 1 , stdout , "" , TimeSpan . Zero , TimedOut : false ) ;
1374+
13011375 private static CommandResult TimedOut ( )
13021376 => new ( - 1 , "" , "" , TimeSpan . FromSeconds ( 30 ) , TimedOut : true ) ;
13031377
1378+ private SetupContext CreatePairingContext ( string failureStdout )
1379+ {
1380+ var commands = new FakeCommandRunner (
1381+ _ => Ok ( ) ,
1382+ ( _ , _ , _ ) => FailWithStdout ( failureStdout ) ) ;
1383+ var ctx = CreateContext ( commands : commands ) ;
1384+ ctx . DistroName = "test-distro" ;
1385+ ctx . SharedGatewayToken = "shared-token" ;
1386+ return ctx ;
1387+ }
1388+
13041389 private sealed class FakeCommandRunner (
13051390 Func < string [ ] , CommandResult > run ,
13061391 Func < string , string , TimeSpan , CommandResult > ? runInWsl = null ) : ICommandRunner
0 commit comments