test: add terminal auth-failure coverage for PR #206 fix

Tests for the auth-failure backoff and terminal error detection added
in PR #206 (fix: stop aggressive retry on auth failures):

- HandleRequestError_TerminalAuthError_SetsAuthFailedFlag (theory):
  token mismatch / origin not allowed / too many failed → _authFailed set
- HandleRequestError_TerminalAuthError_RaisesAuthenticationFailedEvent:
  AuthenticationFailed event fired with message text
- HandleRequestError_TerminalAuthError_RaisesErrorStatus:
  ConnectionStatus.Error raised on terminal auth error
- HandleRequestError_TerminalAuthError_OnNonConnectMethod_DoesNotSetAuthFailed:
  terminal-auth guard is scoped to 'connect' method only
- HandleHelloOk_AfterAuthFailed_ClearsAuthFailedFlag:
  hello-ok response resets the flag so reconnect is re-enabled
- HandleRequestError_AllDeviceSignatureModesExhausted_SetsAuthFailed:
  cycling through all 4 signature modes fires AuthenticationFailed once

660 Shared pass (+8), 20 skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
github-actions[bot] 2026-04-24 01:16:41 +00:00 committed by GitHub
parent 8283eaa794
commit 7c69a2bea5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -288,6 +288,16 @@ public class OpenClawGatewayClientTests
_client.StatusChanged += (_, s) => changes.Add(s);
return changes;
}
public bool GetAuthFailedFlag() =>
GetPrivateField<bool>("_authFailed");
public List<string> CaptureAuthenticationFailedEvents()
{
var events = new List<string>();
_client.AuthenticationFailed += (_, msg) => events.Add(msg);
return events;
}
}
private class TestLogger : IOpenClawLogger
@ -1626,4 +1636,142 @@ public class OpenClawGatewayClientTests
var flags = helper.GetUnsupportedMethodFlags();
Assert.True(flags.NodeList);
}
// --- HandleRequestError: terminal auth errors (PR #206 fix) ---
[Theory]
[InlineData("token mismatch")]
[InlineData("origin not allowed")]
[InlineData("too many failed attempts")]
public void HandleRequestError_TerminalAuthError_SetsAuthFailedFlag(string errorMessage)
{
var helper = new GatewayClientTestHelper();
helper.TrackPendingRequest("req-auth-1", "connect");
helper.ProcessRawMessage($$"""
{
"type": "res",
"id": "req-auth-1",
"ok": false,
"error": "{{errorMessage}}"
}
""");
Assert.True(helper.GetAuthFailedFlag());
}
[Fact]
public void HandleRequestError_TerminalAuthError_RaisesAuthenticationFailedEvent()
{
var helper = new GatewayClientTestHelper();
var authEvents = helper.CaptureAuthenticationFailedEvents();
helper.TrackPendingRequest("req-auth-2", "connect");
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-auth-2",
"ok": false,
"error": "token mismatch — reconnect rejected"
}
""");
Assert.Single(authEvents);
Assert.Contains("token mismatch", authEvents[0], StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void HandleRequestError_TerminalAuthError_RaisesErrorStatus()
{
var helper = new GatewayClientTestHelper();
var statusChanges = helper.CaptureStatusChanges();
helper.TrackPendingRequest("req-auth-3", "connect");
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-auth-3",
"ok": false,
"error": "origin not allowed"
}
""");
Assert.Contains(ConnectionStatus.Error, statusChanges);
}
[Fact]
public void HandleRequestError_TerminalAuthError_OnNonConnectMethod_DoesNotSetAuthFailed()
{
// Terminal auth check only applies to "connect" method — other methods must not set the flag
var helper = new GatewayClientTestHelper();
helper.TrackPendingRequest("req-auth-4", "sessions.list");
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-auth-4",
"ok": false,
"error": "token mismatch"
}
""");
Assert.False(helper.GetAuthFailedFlag());
}
[Fact]
public void HandleHelloOk_AfterAuthFailed_ClearsAuthFailedFlag()
{
var helper = new GatewayClientTestHelper();
// First, trigger auth failure
helper.TrackPendingRequest("req-auth-5", "connect");
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-auth-5",
"ok": false,
"error": "token mismatch"
}
""");
Assert.True(helper.GetAuthFailedFlag());
// Now receive hello-ok — flag must be cleared
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-hello-1",
"payload": {
"type": "hello-ok"
}
}
""");
Assert.False(helper.GetAuthFailedFlag());
}
[Fact]
public void HandleRequestError_AllDeviceSignatureModesExhausted_SetsAuthFailed()
{
var logger = new TestLogger();
var helper = new GatewayClientTestHelper(logger);
var authEvents = helper.CaptureAuthenticationFailedEvents();
// Cycle through all 4 signature modes by sending 4 successive rejections
for (int i = 1; i <= 4; i++)
{
helper.TrackPendingRequest($"req-sig-exhaust-{i}", "connect");
helper.ProcessRawMessage($$"""
{
"type": "res",
"id": "req-sig-exhaust-{{i}}",
"ok": false,
"error": "device signature invalid"
}
""");
}
Assert.True(helper.GetAuthFailedFlag());
Assert.Single(authEvents);
Assert.Contains("device signature", authEvents[0], StringComparison.OrdinalIgnoreCase);
}
}