146 lines
5.2 KiB
C#
146 lines
5.2 KiB
C#
using System.Text.Json.Nodes;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Nexus.Api.Models;
|
|
using Nexus.Api.Services;
|
|
using Xunit;
|
|
|
|
namespace Nexus.Api.Tests;
|
|
|
|
public sealed class OpenClawWizardServiceTests
|
|
{
|
|
private static readonly OpenClawInvocationContext Invocation =
|
|
OpenClawInvocationContext.Create(
|
|
actor: "owner-1",
|
|
idempotencyKey: "wizard-test",
|
|
correlationId: "wizard-correlation",
|
|
traceParent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
|
|
|
|
[Fact]
|
|
public async Task Start_RequiresExplicitConfirmationAndLocalManagement()
|
|
{
|
|
var gateway = ReadyGateway();
|
|
var state = new OpenClawManagementState();
|
|
var service = CreateService(gateway, state);
|
|
|
|
var unconfirmed = await service.StartAsync(
|
|
new StartOpenClawWizardRequest(Confirmed: false),
|
|
Invocation);
|
|
var disabled = await service.StartAsync(
|
|
new StartOpenClawWizardRequest(Confirmed: true),
|
|
Invocation);
|
|
|
|
Assert.Equal("confirmation_required", unconfirmed.State);
|
|
Assert.Equal("management_disabled", disabled.State);
|
|
Assert.Empty(gateway.Invocations);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Start_UsesOfficialSafeParametersAndProjectsInteractiveStep()
|
|
{
|
|
var gateway = ReadyGateway();
|
|
gateway.Handler = (method, _) => method == "wizard.start"
|
|
? JsonNode.Parse(
|
|
"""
|
|
{
|
|
"sessionId": "wizard-1",
|
|
"done": false,
|
|
"status": "running",
|
|
"step": {
|
|
"id": "mode",
|
|
"type": "select",
|
|
"title": "Choose mode",
|
|
"options": [
|
|
{ "value": "safe", "label": "Safe", "hint": "Recommended" }
|
|
]
|
|
}
|
|
}
|
|
""")
|
|
: null;
|
|
var state = new OpenClawManagementState();
|
|
state.SetEnabled(true);
|
|
var service = CreateService(gateway, state);
|
|
|
|
var result = await service.StartAsync(
|
|
new StartOpenClawWizardRequest("remote", Confirmed: true),
|
|
Invocation);
|
|
|
|
Assert.True(result.Ok);
|
|
Assert.Equal("wizard-1", result.SessionId);
|
|
Assert.Equal("select", result.Step?.Type);
|
|
var invocation = Assert.Single(gateway.Invocations);
|
|
Assert.Equal("wizard.start", invocation.Method);
|
|
Assert.Equal("remote", invocation.Parameters?["mode"]?.GetValue<string>());
|
|
Assert.False(invocation.Parameters?["installDaemon"]?.GetValue<bool>());
|
|
Assert.Equal("setup", invocation.Parameters?["flow"]?.GetValue<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SensitiveStep_IsRedactedAndCannotBeAnsweredFromBrowser()
|
|
{
|
|
var gateway = ReadyGateway();
|
|
gateway.Handler = (method, _) => method == "wizard.start"
|
|
? JsonNode.Parse(
|
|
"""
|
|
{
|
|
"sessionId": "wizard-secret",
|
|
"done": false,
|
|
"step": {
|
|
"id": "provider-token",
|
|
"type": "text",
|
|
"title": "Provider token",
|
|
"sensitive": true,
|
|
"initialValue": "must-not-leak",
|
|
"placeholder": "must-not-leak"
|
|
}
|
|
}
|
|
""")
|
|
: null;
|
|
var state = new OpenClawManagementState();
|
|
state.SetEnabled(true);
|
|
var service = CreateService(gateway, state);
|
|
|
|
var started = await service.StartAsync(
|
|
new StartOpenClawWizardRequest(Confirmed: true),
|
|
Invocation);
|
|
var advanced = await service.NextAsync(
|
|
new AdvanceOpenClawWizardRequest(
|
|
"wizard-secret",
|
|
"provider-token",
|
|
JsonValue.Create("browser-secret"),
|
|
HasAnswer: true),
|
|
Invocation);
|
|
|
|
Assert.True(started.Step?.Sensitive);
|
|
Assert.Null(started.Step?.InitialValue);
|
|
Assert.Null(started.Step?.Placeholder);
|
|
Assert.False(started.Step?.CanAnswer);
|
|
Assert.Equal("server_secret_required", advanced.State);
|
|
Assert.Single(gateway.Invocations);
|
|
Assert.DoesNotContain("must-not-leak", started.ToString(), StringComparison.Ordinal);
|
|
Assert.DoesNotContain("browser-secret", advanced.ToString(), StringComparison.Ordinal);
|
|
}
|
|
|
|
private static OpenClawWizardService CreateService(
|
|
SetupGatewayConnector gateway,
|
|
IOpenClawManagementState managementState)
|
|
=> new(
|
|
gateway,
|
|
NullLogger<OpenClawWizardService>.Instance,
|
|
managementState);
|
|
|
|
private static SetupGatewayConnector ReadyGateway()
|
|
{
|
|
var gateway = new SetupGatewayConnector
|
|
{
|
|
ConnectionState = GatewayConnectionState.Connected,
|
|
GrantedScopes = new HashSet<string>(
|
|
["operator.read", "operator.admin"],
|
|
StringComparer.Ordinal),
|
|
AdvertisedMethods = new HashSet<string>(
|
|
["wizard.start", "wizard.next", "wizard.status", "wizard.cancel"],
|
|
StringComparer.Ordinal)
|
|
};
|
|
return gateway;
|
|
}
|
|
}
|