diff --git a/DevPack.Tests/Skyline.DataMiner.Dev.Utils.Solutions.MediaOps.Plan.Tests.csproj b/DevPack.Tests/Skyline.DataMiner.Dev.Utils.Solutions.MediaOps.Plan.Tests.csproj
index bb7a3b9e..1ec5510c 100644
--- a/DevPack.Tests/Skyline.DataMiner.Dev.Utils.Solutions.MediaOps.Plan.Tests.csproj
+++ b/DevPack.Tests/Skyline.DataMiner.Dev.Utils.Solutions.MediaOps.Plan.Tests.csproj
@@ -15,7 +15,6 @@
- allruntime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/DevPack.Tests/Workflow/Jobs/ResourceManagerTraceDataHandlerTests.cs b/DevPack.Tests/Workflow/Jobs/ResourceManagerTraceDataHandlerTests.cs
new file mode 100644
index 00000000..de62bddb
--- /dev/null
+++ b/DevPack.Tests/Workflow/Jobs/ResourceManagerTraceDataHandlerTests.cs
@@ -0,0 +1,215 @@
+namespace RT_MediaOps.Plan.Workflow.Jobs
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ using Skyline.DataMiner.Net.Messages;
+ using Skyline.DataMiner.Net.ResourceManager.Helpers;
+ using Skyline.DataMiner.Net.ResponseErrorData;
+ using Skyline.DataMiner.Net.SRM.Capabilities;
+ using Skyline.DataMiner.Net.SRM.Capacities;
+ using Skyline.DataMiner.Net.SRM.Quarantine;
+ using Skyline.DataMiner.Solutions.MediaOps.Plan.API;
+ using Skyline.DataMiner.Solutions.MediaOps.Plan.Exceptions;
+ using Skyline.DataMiner.Solutions.MediaOps.Plan.UnitTesting.Simulation;
+
+ using ResourcePool = Skyline.DataMiner.Solutions.MediaOps.Plan.API.ResourcePool;
+ using PlanResource = Skyline.DataMiner.Solutions.MediaOps.Plan.API.Resource;
+
+ ///
+ /// Deterministic, simulation-backed tests for translating core ResourceManager errors into DevPack job resource errors.
+ /// A real Resource Studio resource is created so the handler can resolve the core resource id to its DOM counterpart.
+ ///
+ [TestClass]
+ public sealed class ResourceManagerTraceDataHandlerTests
+ {
+ private static (IMediaOpsPlanApi Api, PlanResource Resource) CreateContextWithResource()
+ {
+ var dms = MediaOpsPlanSimulation.Create();
+ var connection = dms.CreateConnection();
+ var api = connection.GetMediaOpsPlanApi();
+
+ var prefix = Guid.NewGuid();
+
+ var pool = api.ResourcePools.Create(new ResourcePool { Name = $"{prefix}_Pool" });
+ pool = api.ResourcePools.Complete(pool);
+
+ var resource = new UnmanagedResource { Name = $"{prefix}_Resource" }.AssignToPool(pool);
+ resource = api.Resources.Create(resource);
+ resource = api.Resources.Complete(resource);
+
+ return (api, resource);
+ }
+
+ [TestMethod]
+ public void Translate_QuarantineError_EmitsResourceNotAvailableWithDomResourceId()
+ {
+ var (api, resource) = CreateContextWithResource();
+ var reservationId = Guid.NewGuid();
+
+ var error = new ResourceManagerErrorData(
+ ResourceManagerErrorData.Reason.ReservationUpdateCausedReservationsToGoToQuarantine,
+ reservationId,
+ (Guid?)null,
+ new List())
+ {
+ MustBeMovedToQuarantine = new List
+ {
+ new QuarantinedUsagesOnSingleReservation
+ {
+ QuarantinedUsages = new List
+ {
+ new QuarantinedResourceUsageDefinition
+ {
+ QuarantinedResourceUsage = new ResourceUsageDefinition(resource.CoreResourceId),
+ },
+ },
+ },
+ },
+ };
+
+ var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api);
+
+ var result = handler.Translate(new[] { error });
+
+ Assert.IsTrue(result.ContainsKey(reservationId), "Expected the translated errors to be keyed by the reservation id.");
+ var notAvailable = result[reservationId].ErrorData.OfType().Single();
+ Assert.AreEqual(resource.Id, notAvailable.ResourceId, "Expected the DOM resource id to be reported.");
+ }
+
+ [TestMethod]
+ public void Translate_ResourceCapacityInvalid_EmitsInvalidCapacityWithIds()
+ {
+ var (api, resource) = CreateContextWithResource();
+ var reservationId = Guid.NewGuid();
+ var capacityId = Guid.NewGuid();
+
+ var error = new ResourceManagerErrorData(
+ ResourceManagerErrorData.Reason.ResourceCapacityInvalid,
+ reservationId,
+ resource.CoreResourceId,
+ new MultiResourceCapacityUsage { CapacityProfileID = capacityId });
+
+ var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api);
+
+ var result = handler.Translate(new[] { error });
+
+ Assert.IsTrue(result.ContainsKey(reservationId), "Expected the translated errors to be keyed by the reservation id.");
+ var capacityError = result[reservationId].ErrorData.OfType().Single();
+ Assert.AreEqual(resource.Id, capacityError.ResourceId, "Expected the DOM resource id to be reported.");
+ Assert.AreEqual(capacityId, capacityError.CapacityId, "Expected the capacity profile id to be reported.");
+ }
+
+ [TestMethod]
+ public void Translate_ResourceCapabilityInvalid_EmitsInvalidCapabilityWithIds()
+ {
+ var (api, resource) = CreateContextWithResource();
+ var reservationId = Guid.NewGuid();
+ var capabilityId = Guid.NewGuid();
+
+ var error = new ResourceManagerErrorData(
+ ResourceManagerErrorData.Reason.ResourceCapabilityInvalid,
+ reservationId,
+ resource.CoreResourceId,
+ new ResourceCapabilityUsage { CapabilityProfileID = capabilityId },
+ "Capability not available.");
+
+ var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api);
+
+ var result = handler.Translate(new[] { error });
+
+ Assert.IsTrue(result.ContainsKey(reservationId), "Expected the translated errors to be keyed by the reservation id.");
+ var capabilityError = result[reservationId].ErrorData.OfType().Single();
+ Assert.AreEqual(resource.Id, capabilityError.ResourceId, "Expected the DOM resource id to be reported.");
+ Assert.AreEqual(capabilityId, capabilityError.CapabilityId, "Expected the capability profile id to be reported.");
+ }
+
+ [TestMethod]
+ public void Translate_UncategorizedError_FallsBackToRawMessage()
+ {
+ var (api, _) = CreateContextWithResource();
+ var reservationId = Guid.NewGuid();
+
+ var error = new ResourceManagerErrorData(
+ ResourceManagerErrorData.Reason.UnknownError,
+ reservationId,
+ (Guid?)null,
+ new List());
+
+ var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api);
+
+ var result = handler.Translate(new[] { error });
+
+ Assert.IsTrue(result.ContainsKey(reservationId), "Expected the raw fallback to be keyed by the reservation id.");
+ var errorData = result[reservationId].ErrorData.ToList();
+ Assert.IsFalse(
+ errorData.OfType().Any(),
+ "Expected no typed job resource error for an uncategorized reason.");
+ Assert.IsTrue(
+ errorData.Any(x => x.GetType() == typeof(MediaOpsErrorData)),
+ "Expected a raw MediaOpsErrorData fallback for an uncategorized reason.");
+ }
+
+ [TestMethod]
+ public void Translate_KnownAndUnknownErrors_MapsKnownAndAddsRawForUnknown()
+ {
+ var (api, resource) = CreateContextWithResource();
+ var reservationId = Guid.NewGuid();
+ var capacityId = Guid.NewGuid();
+
+ var capacityError = new ResourceManagerErrorData(
+ ResourceManagerErrorData.Reason.ResourceCapacityInvalid,
+ reservationId,
+ resource.CoreResourceId,
+ new MultiResourceCapacityUsage { CapacityProfileID = capacityId });
+
+ var unknownError = new ResourceManagerErrorData(
+ ResourceManagerErrorData.Reason.UnknownError,
+ reservationId,
+ (Guid?)null,
+ new List());
+
+ var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api);
+
+ var result = handler.Translate(new[] { capacityError, unknownError });
+
+ Assert.IsTrue(result.ContainsKey(reservationId), "Expected the translated errors to be keyed by the reservation id.");
+ var errorData = result[reservationId].ErrorData.ToList();
+ Assert.AreEqual(
+ capacityId,
+ errorData.OfType().Single().CapacityId,
+ "Expected the known capacity error to be translated.");
+ Assert.IsTrue(
+ errorData.Any(x => x.GetType() == typeof(MediaOpsErrorData)),
+ "Expected the unknown error to be added as a raw default alongside the translated one.");
+ }
+ [TestMethod]
+ public void Translate_WhenCalledMultipleTimes_DoesNotReturnPreviousResults()
+ {
+ var (api, resource) = CreateContextWithResource();
+ var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api);
+
+ var reservationId1 = Guid.NewGuid();
+ var reservationId2 = Guid.NewGuid();
+
+ var first = new ResourceManagerErrorData(
+ ResourceManagerErrorData.Reason.ResourceCapacityInvalid,
+ reservationId1,
+ resource.CoreResourceId,
+ new MultiResourceCapacityUsage { CapacityProfileID = Guid.NewGuid() });
+
+ var second = new ResourceManagerErrorData(
+ ResourceManagerErrorData.Reason.UnknownError,
+ reservationId2,
+ (Guid?)null,
+ new List());
+
+ Assert.IsTrue(handler.Translate(new[] { first }).ContainsKey(reservationId1));
+
+ var secondResult = handler.Translate(new[] { second });
+ Assert.IsFalse(secondResult.ContainsKey(reservationId1), "Expected previous translation output not to leak into subsequent calls.");
+ Assert.IsTrue(secondResult.ContainsKey(reservationId2));
+ }
+ }
+}
diff --git a/DevPack.UnitTesting/Simulation/SimulatedDms.cs b/DevPack.UnitTesting/Simulation/SimulatedDms.cs
index 9c6f3486..34206964 100644
--- a/DevPack.UnitTesting/Simulation/SimulatedDms.cs
+++ b/DevPack.UnitTesting/Simulation/SimulatedDms.cs
@@ -264,6 +264,14 @@ internal bool TryHandleMessage(DMSMessage message, out IEnumerable r
throw new ArgumentNullException(nameof(message));
}
+ // A DOM select read only asks for specific fields. The DOM message handler does not support it,
+ // so it is translated into a regular read and then reduced to the requested fields.
+ if (DomInstanceSelectStore.TryHandleMessage(message, _domSlNetMessageHandler, out var selectResponse))
+ {
+ responses = new[] { selectResponse };
+ return true;
+ }
+
if (_domSlNetMessageHandler.TryHandleMessage(message, out var domResponse))
{
responses = new[] { domResponse };
diff --git a/DevPack.UnitTesting/Stores/DomInstanceSelectStore.cs b/DevPack.UnitTesting/Stores/DomInstanceSelectStore.cs
new file mode 100644
index 00000000..8b79a4c1
--- /dev/null
+++ b/DevPack.UnitTesting/Stores/DomInstanceSelectStore.cs
@@ -0,0 +1,101 @@
+namespace Skyline.DataMiner.Solutions.MediaOps.Plan.UnitTesting.Stores
+{
+ using System;
+ using System.Collections.Generic;
+
+ using Skyline.DataMiner.Net.Apps.DataMinerObjectModel;
+ using Skyline.DataMiner.Net.Apps.DataMinerObjectModel.Select;
+ using Skyline.DataMiner.Net.Apps.ManagerStore.Select;
+ using Skyline.DataMiner.Net.Messages;
+ using Skyline.DataMiner.Utils.DOM.UnitTesting;
+
+ ///
+ /// Handles DOM instance select (partial object) read requests, mirroring how a real DataMiner Agent
+ /// answers a .
+ ///
+ ///
+ /// A select read returns only the requested fields instead of full objects. The agent replies with a
+ /// whose custom response data holds a .
+ /// does not support these requests and returns ,
+ /// so the request is translated into a regular read whose results are reduced to the requested fields.
+ ///
+ internal static class DomInstanceSelectStore
+ {
+ public static bool TryHandleMessage(DMSMessage message, DomSLNetMessageHandler domHandler, out DMSMessage response)
+ {
+ if (domHandler is null)
+ {
+ throw new ArgumentNullException(nameof(domHandler));
+ }
+
+ response = null;
+
+ if (!(message is ManagerStoreSelectReadRequest request))
+ {
+ return false;
+ }
+
+ var readRequest = new ManagerStoreReadRequest(request.Query)
+ {
+ ModuleId = request.ModuleId,
+ };
+
+ if (!domHandler.TryHandleMessage(readRequest, out var readResponse)
+ || !(readResponse is ManagerStoreCrudResponse crudResponse))
+ {
+ return false;
+ }
+
+ response = CreateResponse(request, crudResponse.Objects ?? new List());
+ return true;
+ }
+
+ private static DMSMessage CreateResponse(ManagerStoreSelectReadRequest request, IEnumerable instances)
+ {
+ if (request is null)
+ {
+ throw new ArgumentNullException(nameof(request));
+ }
+
+ var selectedFields = request.SelectedFields ?? new List();
+ var objects = new List();
+
+ foreach (var instance in instances)
+ {
+ if (instance?.ID is null)
+ {
+ continue;
+ }
+
+ var factory = new PartialDomInstanceFactory(instance.ID.ModuleId);
+ var values = new List(selectedFields.Count);
+
+ foreach (var selectedField in selectedFields)
+ {
+ values.Add(new PartialObjectValue