Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Devolutions.Now.Policy.Api" Version="2026.7.28" />
<PackageReference Include="Devolutions.Now.Policy.Client" Version="2026.7.28" />
<PackageReference Include="Devolutions.Now.Policy.Api" Version="2026.8.4" />
<PackageReference Include="Devolutions.Now.Policy.Client" Version="2026.8.4" />
</ItemGroup>

<ItemGroup>
Expand Down
9 changes: 9 additions & 0 deletions src/UniGetUI.PackageEngine.Operations/PackageOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,15 @@ private async Task<OperationVeredict> PerformBrokerOperation()
}
var output = DisplayBrokerOutput(status.Stdout);

if (status.Status == BrokerOperationStatus.Canceled)
{
string canceledReason = string.IsNullOrWhiteSpace(status.Message)
? CoreTools.Translate("Operation canceled by user")
: status.Message;
Line($"Operation canceled via broker: {canceledReason}", LineType.Error);
return OperationVeredict.Canceled;
}

if (status.Status == BrokerOperationStatus.Completed)
{
var veredict = await GetProcessVeredict(status.ExitCode ?? -1, output);
Expand Down
122 changes: 121 additions & 1 deletion src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using UniGetUI.Core.Logging;
using UniGetUI.Core.SettingsEngine;
using UniGetUI.Core.Tools;
Expand All @@ -14,8 +15,16 @@
using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
using UniGetUI.PackageEngine.Tests.Infrastructure.Fakes;
using UniGetUI.PackageOperations;
using BrokerCapabilitiesResponse = Devolutions.Now.Policy.Api.CapabilitiesResponse;
using BrokerClientErrorKind = Devolutions.Now.Policy.Client.BrokerClientErrorKind;
using BrokerClientException = Devolutions.Now.Policy.Client.BrokerClientException;
using BrokerExecutionResponse = Devolutions.Now.Policy.Api.ExecutionResponse;
using BrokerManagerCapability = Devolutions.Now.Policy.Api.ManagerCapability;
using BrokerManagerName = Devolutions.Now.Policy.Api.ManagerName;
using BrokerOperationKind = Devolutions.Now.Policy.Api.Operation;
using BrokerOperationStatus = Devolutions.Now.Policy.Api.OperationStatus;
using BrokerOperationSubmission = Devolutions.Now.Policy.Api.OperationSubmission;
using BrokerStatusResponse = Devolutions.Now.Policy.Api.StatusResponse;
using BrokerTransportKind = Devolutions.Now.Policy.Api.Transport;
using BrokerTransportRequest = Devolutions.Now.Policy.Client.BrokerTransportRequest;
using BrokerTransportResponse = Devolutions.Now.Policy.Client.BrokerTransportResponse;
Expand Down Expand Up @@ -544,6 +553,49 @@ public async Task BrokerOperationFailsWithoutLocalFallbackWhenBrokerDropsAfterPr
Assert.Contains(transport.RequestedPaths, path => path != "/v1/health");
}

[Fact]
public async Task BrokerOperationReportedCanceledReturnsCanceledVeredict()
{
bool originalSetting = Settings.Get(Settings.K.UseAgentBroker);
bool localExecutionPrepared = false;
var manager = new PackageManagerBuilder()
.WithName("Chocolatey")
.ConfigureManager(m =>
{
m.ExecutablePath = "C:\\test-tools\\choco.exe";
m.ExecutableArguments = "--test";
})
.ConfigureOperation(helper =>
helper.ParametersFactory = (package, _, operation) =>
{
localExecutionPrepared = true;
return [operation.ToString().ToLowerInvariant(), package.Id];
})
.Build();
var package = new PackageBuilder().WithManager(manager).Build();
var transport = new FakeBrokerTransport(
healthy: true,
FakeBrokerTransportScenario.CanceledOperation);
PackageOperation.BrokerTransportFactory = () => transport;
Settings.Set(Settings.K.UseAgentBroker, true);
try
{
using var operation = new BrokerProbingInstallPackageOperation(package, new InstallOptions());

var veredict = await operation.InvokePerformOperationForTests();

Assert.Equal(OperationVeredict.Canceled, veredict);
Assert.Contains("/v1/package-operations/execute", transport.RequestedPaths);
Assert.Contains("/v1/package-operations/get-status", transport.RequestedPaths);
Assert.False(localExecutionPrepared);
}
finally
{
Settings.Set(Settings.K.UseAgentBroker, originalSetting);
PackageOperation.BrokerTransportFactory = null;
}
}

/// <summary>
/// Runs an install operation against a broker whose transport simulates an outage and
/// asserts the policy-enforcement contract: the operation fails with the
Expand Down Expand Up @@ -659,7 +711,16 @@ public BrokerProbingInstallPackageOperation(IPackage package, InstallOptions opt
public Task<OperationVeredict> InvokePerformOperationForTests() => PerformOperation();
}

private sealed class FakeBrokerTransport(bool healthy) : IBrokerTransport
private enum FakeBrokerTransportScenario
{
UnavailableAfterHealth,
CanceledOperation,
}

private sealed class FakeBrokerTransport(
bool healthy,
FakeBrokerTransportScenario scenario = FakeBrokerTransportScenario.UnavailableAfterHealth
) : IBrokerTransport
{
public List<string> RequestedPaths { get; } = [];

Expand All @@ -676,13 +737,72 @@ public Task<BrokerTransportResponse> Send(
return Task.FromResult(new BrokerTransportResponse { StatusCode = 200, Body = "{}" });
}

if (healthy && scenario is FakeBrokerTransportScenario.CanceledOperation)
{
return Task.FromResult(new BrokerTransportResponse
{
StatusCode = 200,
Body = CreateCanceledOperationResponse(request.Path),
});
}

throw new BrokerClientException(
BrokerClientErrorKind.BrokerUnavailable,
"Simulated broker outage",
request.Path);
}

public void Dispose() { }

private static string CreateCanceledOperationResponse(string path)
{
if (path == "/v1/capabilities")
{
return JsonSerializer.Serialize(new BrokerCapabilitiesResponse
{
Transports = [BrokerTransportKind.HttpNamedPipe],
Managers =
[
new BrokerManagerCapability
{
Manager = BrokerManagerName.Chocolatey,
Operations = [BrokerOperationKind.Install],
SupportsCaptureOutput = true,
},
],
MaxRequestBodyBytes = 1_000_000,
});
}

if (path == "/v1/package-operations/execute")
{
return JsonSerializer.Serialize(new BrokerExecutionResponse
{
Operation = new BrokerOperationSubmission
{
OperationId = "operation-1",
Status = BrokerOperationStatus.Running,
SubmittedAt = DateTimeOffset.UtcNow,
},
});
}

if (path == "/v1/package-operations/get-status")
{
return JsonSerializer.Serialize(new BrokerStatusResponse
{
OperationId = "operation-1",
Status = BrokerOperationStatus.Canceled,
Message = "Canceled by broker",
Stdout = "broker output",
});
}

throw new BrokerClientException(
BrokerClientErrorKind.BrokerUnavailable,
"Unexpected broker test path",
path);
}
}

private class InspectableInstallPackageOperation : InstallPackageOperation
Expand Down
Loading