feat: ship agent-first mission control v0.2.57
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user