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()); Assert.False(invocation.Parameters?["installDaemon"]?.GetValue()); Assert.Equal("setup", invocation.Parameters?["flow"]?.GetValue()); } [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.Instance, managementState); private static SetupGatewayConnector ReadyGateway() { var gateway = new SetupGatewayConnector { ConnectionState = GatewayConnectionState.Connected, GrantedScopes = new HashSet( ["operator.read", "operator.admin"], StringComparer.Ordinal), AdvertisedMethods = new HashSet( ["wizard.start", "wizard.next", "wizard.status", "wizard.cancel"], StringComparer.Ordinal) }; return gateway; } }