Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/OpenClaw.Connection/GatewayClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 8 additions & 2 deletions tests/OpenClaw.Connection.Tests/GatewayClientFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ namespace OpenClaw.Connection.Tests;

public sealed class GatewayClientFactoryTests
{
/// <summary>
/// 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.
/// </summary>
[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);
Expand All @@ -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
{
Expand Down
25 changes: 25 additions & 0 deletions tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,31 @@ public void BootstrapNodeHandoff_FreshDevice_RequestsNodeRoleWithoutScopes()
Assert.False(auth.ContainsKey("deviceToken"));
}

/// <summary>
/// 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.
/// </summary>
[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()
{
Expand Down