diff --git a/src/OpenClaw.Connection/GatewayClientFactory.cs b/src/OpenClaw.Connection/GatewayClientFactory.cs index 5ea0c59d..9802579a 100644 --- a/src/OpenClaw.Connection/GatewayClientFactory.cs +++ b/src/OpenClaw.Connection/GatewayClientFactory.cs @@ -14,12 +14,16 @@ public IGatewayClientLifecycle Create( string identityPath, IOpenClawLogger logger) { + // bootstrapPairAsNode is always false for the operator (chat) client: the operator + // client must connect as "operator" role regardless of credential type so that chat + // works immediately after QR-code or setup-code pairing. The node-platform client + // has its own creation path (NodeConnector) which sets bootstrapPairAsNode as needed. var client = new OpenClawGatewayClient( gatewayUrl, credential.Token, logger, tokenIsBootstrapToken: credential.IsBootstrapToken, - bootstrapPairAsNode: credential.IsBootstrapToken, + bootstrapPairAsNode: false, identityPath: identityPath); return new GatewayClientLifecycleAdapter(client); diff --git a/tests/OpenClaw.Connection.Tests/GatewayClientFactoryTests.cs b/tests/OpenClaw.Connection.Tests/GatewayClientFactoryTests.cs index 73b6a2b8..318d31e4 100644 --- a/tests/OpenClaw.Connection.Tests/GatewayClientFactoryTests.cs +++ b/tests/OpenClaw.Connection.Tests/GatewayClientFactoryTests.cs @@ -6,8 +6,13 @@ namespace OpenClaw.Connection.Tests; public sealed class GatewayClientFactoryTests { + /// + /// Regression test for issue #720: the operator (chat) client must connect as "operator" + /// role even when the credential is a bootstrap token. Setting bootstrapPairAsNode: false + /// for the operator client ensures chat works immediately after QR-code or setup-code pairing. + /// [Fact] - public void Create_BootstrapCredential_PairsAsNode() + public void Create_BootstrapCredential_PairsAsOperator() { var tempDir = Path.Combine(Path.GetTempPath(), "openclaw-gateway-factory-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); @@ -20,7 +25,8 @@ public void Create_BootstrapCredential_PairsAsNode() tempDir, NullLogger.Instance); - Assert.Equal("node", GetConnectRole(lifecycle.DataClient)); + // Operator client always connects as "operator", even with a bootstrap token. + Assert.Equal("operator", GetConnectRole(lifecycle.DataClient)); } finally { diff --git a/tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs b/tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs index ea9d62bc..8dba7368 100644 --- a/tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs +++ b/tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs @@ -598,6 +598,31 @@ public void BootstrapNodeHandoff_FreshDevice_RequestsNodeRoleWithoutScopes() Assert.False(auth.ContainsKey("deviceToken")); } + /// + /// Regression test for issue #720: the operator (chat) client must connect as "operator" + /// role even when the credential is a bootstrap token (QR-code or setup-code pairing). + /// GatewayClientFactory always passes bootstrapPairAsNode: false for the operator client. + /// + [Fact] + public void OperatorClient_WithBootstrapToken_ConnectsAsOperatorRole() + { + // bootstrapPairAsNode: false — this is what GatewayClientFactory now passes for the operator client. + var helper = new GatewayClientTestHelper( + tokenIsBootstrapToken: true, + bootstrapPairAsNode: false, + identityPath: CreateTempIdentityPath()); + helper.SetDeviceTokenForTest(null); + + var auth = helper.BuildAuthPayload(); + + // Must request "operator" role so chat works immediately after QR-code pairing. + Assert.Equal("operator", helper.GetConnectRole()); + // Must send bootstrapToken (not plain token) so the gateway performs bootstrap pairing. + Assert.Equal("test-token", auth["bootstrapToken"]); + Assert.False(auth.ContainsKey("token")); + Assert.False(auth.ContainsKey("deviceToken")); + } + [Fact] public void BootstrapNodeHandoff_HelloOkWithNodeRole_DoesNotStorePrimaryNodeTokenAsOperator() {