test: add HandleRequestError coverage for auth/scope/unknown-method paths (#202)

Add 13 new tests to OpenClawGatewayClientTests covering the
HandleRequestError private method via reflection:

Pairing required (3 tests):
- SetsPairingBlockFlag: verifies _pairingRequiredAwaitingApproval = true
- LogsWarning: verifies auto-reconnect-paused warning is emitted
- RaisesErrorStatus: verifies ConnectionStatus.Error is raised

Device signature invalid (2 tests):
- CyclesSignatureMode: verifies mode steps V3AuthToken -> V3EmptyToken
- LogsWarningWithMode: verifies rejection warning is logged

Missing operator.read scope (1 Theory x 4 cases):
- sessions.list, usage.status, usage.cost, node.list all set
  _operatorReadScopeUnavailable = true

Unknown method fallbacks (4 tests):
- usage.status, usage.cost, sessions.preview, node.list each set
  their respective _*Unsupported flags

Also adds GatewayClientTestHelper overloads:
- Constructor accepting IOpenClawLogger (for log capture)
- TrackPendingRequest, GetPairingRequiredFlag, GetSignatureTokenMode,
  GetOperatorReadScopeUnavailable, CaptureStatusChanges helpers

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
github-actions[bot] 2026-04-23 10:25:20 -07:00 committed by GitHub
parent 1c251dd9c7
commit 2c29677dc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -19,6 +19,11 @@ public class OpenClawGatewayClientTests
_client = new OpenClawGatewayClient("ws://localhost:18789", "test-token", new TestLogger());
}
public GatewayClientTestHelper(IOpenClawLogger logger)
{
_client = new OpenClawGatewayClient("ws://localhost:18789", "test-token", logger);
}
public string ClassifyNotification(string text)
{
var (_, type) = NotificationCategorizer.ClassifyByKeywords(text);
@ -255,6 +260,36 @@ public class OpenClawGatewayClientTests
var deviceIdProp = identity.GetType().GetProperty("DeviceId");
return (string)deviceIdProp!.GetValue(identity)!;
}
/// <summary>Pre-register a pending request so ProcessRawMessage can resolve the method.</summary>
public void TrackPendingRequest(string requestId, string method)
{
var methodInfo = typeof(OpenClawGatewayClient).GetMethod(
"TrackPendingRequest",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
methodInfo!.Invoke(_client, new object[] { requestId, method });
}
public bool GetPairingRequiredFlag() =>
GetPrivateField<bool>("_pairingRequiredAwaitingApproval");
public string GetSignatureTokenMode()
{
var field = typeof(OpenClawGatewayClient).GetField(
"_signatureTokenMode",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
return field!.GetValue(_client)!.ToString()!;
}
public bool GetOperatorReadScopeUnavailable() =>
GetPrivateField<bool>("_operatorReadScopeUnavailable");
public List<ConnectionStatus> CaptureStatusChanges()
{
var changes = new List<ConnectionStatus>();
_client.StatusChanged += (_, s) => changes.Add(s);
return changes;
}
}
private class TestLogger : IOpenClawLogger
@ -1344,4 +1379,208 @@ public class OpenClawGatewayClientTests
Assert.Contains("pairing required", output);
Assert.Contains("Approve this Windows tray device ID", output);
}
// --- HandleRequestError: pairing required ---
[Fact]
public void HandleRequestError_PairingRequired_SetsPairingBlockFlag()
{
var helper = new GatewayClientTestHelper();
helper.TrackPendingRequest("req-pairing-1", "connect");
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-pairing-1",
"ok": false,
"error": "pairing required for this device"
}
""");
Assert.True(helper.GetPairingRequiredFlag());
}
[Fact]
public void HandleRequestError_PairingRequired_LogsWarning()
{
var helper = new GatewayClientTestHelper();
helper.TrackPendingRequest("req-pairing-2", "connect");
var logger = new TestLogger();
var helperWithLogger = new GatewayClientTestHelper(logger);
helperWithLogger.TrackPendingRequest("req-pairing-2", "connect");
helperWithLogger.ProcessRawMessage("""
{
"type": "res",
"id": "req-pairing-2",
"ok": false,
"error": "pairing required for this device"
}
""");
Assert.Contains(logger.Logs, l => l.Contains("auto-reconnect paused", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void HandleRequestError_PairingRequired_RaisesErrorStatus()
{
var helper = new GatewayClientTestHelper();
helper.TrackPendingRequest("req-pairing-3", "connect");
var statusChanges = helper.CaptureStatusChanges();
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-pairing-3",
"ok": false,
"error": "pairing required for this device"
}
""");
Assert.Contains(ConnectionStatus.Error, statusChanges);
}
// --- HandleRequestError: device signature invalid ---
[Fact]
public void HandleRequestError_DeviceSignatureInvalid_CyclesSignatureMode()
{
var helper = new GatewayClientTestHelper();
// Starting mode is V3AuthToken; first rejection should move it to V3EmptyToken
Assert.Equal("V3AuthToken", helper.GetSignatureTokenMode());
helper.TrackPendingRequest("req-sig-1", "connect");
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-sig-1",
"ok": false,
"error": "device signature invalid"
}
""");
Assert.Equal("V3EmptyToken", helper.GetSignatureTokenMode());
}
[Fact]
public void HandleRequestError_DeviceSignatureInvalid_LogsWarningWithMode()
{
var logger = new TestLogger();
var helper = new GatewayClientTestHelper(logger);
helper.TrackPendingRequest("req-sig-log", "connect");
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-sig-log",
"ok": false,
"error": "device signature invalid"
}
""");
Assert.Contains(logger.Logs, l => l.Contains("device signature", StringComparison.OrdinalIgnoreCase));
}
// --- HandleRequestError: missing scope ---
[Theory]
[InlineData("sessions.list")]
[InlineData("usage.status")]
[InlineData("usage.cost")]
[InlineData("node.list")]
public void HandleRequestError_MissingOperatorReadScope_SetsUnavailableFlag(string method)
{
var helper = new GatewayClientTestHelper();
var reqId = $"req-scope-{method}";
helper.TrackPendingRequest(reqId, method);
helper.ProcessRawMessage($$"""
{
"type": "res",
"id": "{{reqId}}",
"ok": false,
"error": "missing scope: operator.read"
}
""");
Assert.True(helper.GetOperatorReadScopeUnavailable());
}
// --- HandleRequestError: unknown method fallbacks ---
[Fact]
public void HandleRequestError_UnknownMethod_UsageStatus_SetsUnsupportedFlag()
{
var helper = new GatewayClientTestHelper();
helper.TrackPendingRequest("req-um-us", "usage.status");
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-um-us",
"ok": false,
"error": "unknown method: usage.status"
}
""");
var flags = helper.GetUnsupportedMethodFlags();
Assert.True(flags.UsageStatus);
}
[Fact]
public void HandleRequestError_UnknownMethod_UsageCost_SetsUnsupportedFlag()
{
var helper = new GatewayClientTestHelper();
helper.TrackPendingRequest("req-um-uc", "usage.cost");
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-um-uc",
"ok": false,
"error": "unknown method: usage.cost"
}
""");
var flags = helper.GetUnsupportedMethodFlags();
Assert.True(flags.UsageCost);
}
[Fact]
public void HandleRequestError_UnknownMethod_SessionsPreview_SetsUnsupportedFlag()
{
var helper = new GatewayClientTestHelper();
helper.TrackPendingRequest("req-um-sp", "sessions.preview");
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-um-sp",
"ok": false,
"error": "unknown method: sessions.preview"
}
""");
var flags = helper.GetUnsupportedMethodFlags();
Assert.True(flags.SessionPreview);
}
[Fact]
public void HandleRequestError_UnknownMethod_NodeList_SetsUnsupportedFlag()
{
var helper = new GatewayClientTestHelper();
helper.TrackPendingRequest("req-um-nl", "node.list");
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-um-nl",
"ok": false,
"error": "unknown method: node.list"
}
""");
var flags = helper.GetUnsupportedMethodFlags();
Assert.True(flags.NodeList);
}
}