Files
nexus/backend-tests/OpenClawRunGatewayTests.cs
AzuTear f5552218bc
CI - Build & Test / Backend (.NET) (push) Successful in 42s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m46s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Successful in 56s
feat: ship agent-first mission control v0.2.57
2026-07-31 22:39:47 +02:00

206 lines
7.7 KiB
C#

using System.Text.Json;
using System.Text.Json.Nodes;
using Nexus.Api.Data;
using Nexus.Api.Models;
using Nexus.Api.Services;
using Xunit;
namespace Nexus.Api.Tests;
public sealed class OpenClawRunGatewayTests
{
[Fact]
public async Task Start_UsesOfficialChatSendShape_AndForwardsInvocationContext()
{
var connector = new CapturingConnector(
["chat.send"],
new JsonObject
{
["runId"] = "oc-run-42",
["status"] = "started"
});
var gateway = new OpenClawRunGateway(
connector,
new StubOpenClawWriteGate());
var invocation = Invocation();
var result = await gateway.StartAsync(Run(), invocation);
Assert.True(result.Ok);
Assert.Equal("oc-run-42", result.OpenClawRunId);
var call = Assert.Single(connector.Calls);
Assert.Equal("chat.send", call.Method);
Assert.Equal("agent:iris:main", call.Parameters?["sessionKey"]?.GetValue<string>());
Assert.Equal("iris", call.Parameters?["agentId"]?.GetValue<string>());
Assert.Equal("Do the work", call.Parameters?["message"]?.GetValue<string>());
Assert.False(call.Parameters?["deliver"]?.GetValue<bool>());
Assert.Equal("run-idempotency", call.Parameters?["idempotencyKey"]?.GetValue<string>());
Assert.Equal("run-idempotency", call.Invocation?.IdempotencyKey);
Assert.Equal("run-correlation", call.Invocation?.CorrelationId);
Assert.Equal("owner-subject", call.Invocation?.Actor);
Assert.True(call.Invocation?.IncludeIdempotencyParameter);
}
[Fact]
public async Task Start_DoesNotInvokeGateway_WhenWriteBoundaryIsBlocked()
{
var connector = new CapturingConnector(
["chat.send"],
new JsonObject
{
["runId"] = "must-not-be-used",
["status"] = "started"
});
var gate = new StubOpenClawWriteGate(
OpenClawWriteGateDecision.Block(
"endpoint_trust_mismatch",
"The active Gateway endpoint no longer matches the adopted profile.",
"Verify and adopt the connection again."));
var gateway = new OpenClawRunGateway(connector, gate);
var result = await gateway.StartAsync(Run(), Invocation());
Assert.False(result.Ok);
Assert.Equal(OpenClawRunStates.Blocked, result.State);
Assert.Empty(connector.Calls);
Assert.Equal(("chat.send", "operator.write"), Assert.Single(gate.Evaluations));
}
[Fact]
public async Task Stop_RefusesSessionWideAbortWithoutExactOpenClawRunId()
{
var connector = new CapturingConnector(["chat.abort"], new JsonObject());
var gateway = new OpenClawRunGateway(
connector,
new StubOpenClawWriteGate());
var run = Run();
run.OpenClawRunId = null;
var result = await gateway.StopAsync(run, Invocation());
Assert.False(result.Ok);
Assert.Equal(OpenClawRunStates.Blocked, result.State);
Assert.Empty(connector.Calls);
}
[Fact]
public async Task Stop_UsesExactChatAbort_WithoutUnsupportedWireFields()
{
var connector = new CapturingConnector(["chat.abort"], new JsonObject { ["ok"] = true });
var gateway = new OpenClawRunGateway(
connector,
new StubOpenClawWriteGate());
var result = await gateway.StopAsync(Run(), Invocation());
Assert.True(result.Ok);
var call = Assert.Single(connector.Calls);
Assert.Equal("chat.abort", call.Method);
Assert.Equal("oc-run-1", call.Parameters?["runId"]?.GetValue<string>());
Assert.Null(call.Parameters?["idempotencyKey"]);
Assert.False(call.Invocation?.IncludeIdempotencyParameter);
}
[Fact]
public async Task History_IsDisplayNormalizedGatewayData_WithSecretsRedacted()
{
var connector = new CapturingConnector(
["chat.history"],
new JsonObject
{
["messages"] = new JsonArray(new JsonObject { ["text"] = "done" }),
["accessToken"] = "secret-value",
["inputTokens"] = 12
});
var gateway = new OpenClawRunGateway(
connector,
new StubOpenClawWriteGate());
var result = await gateway.GetHistoryAsync(Run(), 50);
Assert.True(result.Ok);
Assert.Equal("[redacted]", result.Data?["accessToken"]?.GetValue<string>());
Assert.Equal(12, result.Data?["inputTokens"]?.GetValue<int>());
var call = Assert.Single(connector.Calls);
Assert.Equal("chat.history", call.Method);
Assert.Equal(50, call.Parameters?["limit"]?.GetValue<int>());
Assert.Null(call.Invocation);
}
private static OpenClawRun Run()
=> new()
{
Title = "Run",
Prompt = "Do the work",
AgentId = "iris",
SessionKey = "agent:iris:main",
Status = OpenClawRunStates.Running,
OpenClawRunId = "oc-run-1",
StartIdempotencyKey = "run-idempotency",
CorrelationId = "run-correlation",
Actor = "owner-subject"
};
private static OpenClawInvocationMetadata Invocation()
=> new(
"run-idempotency",
"run-correlation",
"owner-subject",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
private sealed class CapturingConnector : IGatewayConnector
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
private readonly JsonNode? _response;
public CapturingConnector(IEnumerable<string> methods, JsonNode? response)
{
AdvertisedMethods = new HashSet<string>(methods, StringComparer.Ordinal);
_response = response;
}
public List<InvocationCall> Calls { get; } = [];
public GatewayConnectionState ConnectionState => GatewayConnectionState.Connected;
public string? GatewayVersion => "2026.7.0";
public string? RequiredVersion => "2026.7.0";
public DateTimeOffset? LastConnectedAt => DateTimeOffset.UtcNow;
public int ReconnectAttempts => 0;
public string? StatusMessage => "Connected";
public string? DeviceId => "nexus-tests";
public bool DeviceTokenConfigured => true;
public bool PairingRequired => false;
public string? PairingRequestId => null;
public int? ProtocolVersion => 4;
public IReadOnlySet<string> AdvertisedMethods { get; }
public IReadOnlySet<string> AdvertisedEvents { get; } = new HashSet<string>();
public IReadOnlySet<string> GrantedScopes { get; } = new HashSet<string>();
public DateTimeOffset? LastEventAt => null;
public bool Supports(string method) => AdvertisedMethods.Contains(method);
public Task<JsonNode?> InvokeAsync(
string method,
object? parameters = null,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default,
OpenClawInvocationContext? invocationContext = null)
{
var node = parameters switch
{
null => null,
JsonNode jsonNode => jsonNode.DeepClone(),
_ => JsonSerializer.SerializeToNode(parameters, JsonOptions)
};
Calls.Add(new InvocationCall(method, node, invocationContext));
return Task.FromResult(_response?.DeepClone());
}
public IReadOnlyList<GatewayEventEnvelope> GetRecentEvents(int limit = 100) => [];
}
private sealed record InvocationCall(
string Method,
JsonNode? Parameters,
OpenClawInvocationContext? Invocation);
}