test: migrate AdminUI/ControlPlane/Runtime/Host tests to xunit.v3 (#756)
v2-ci / unit-tests (push) Failing after 13m22s

Akka.TestKit.Xunit2 -> Akka.TestKit.Xunit 1.5.62 (xunit.v3 TestKit; needs xunit.v3 3.2.2, set via
VersionOverride on the four projects). Removes the xunit 2.9.3 pin. IAsyncLifetime returns ValueTask,
SqlServerFactAttribute gains the source-info ctor, and xUnit1051 call sites pass
TestContext.Current.CancellationToken. Test counts unchanged.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-10 08:57:33 -05:00
parent 2a8e2981fe
commit b211911f98
191 changed files with 2161 additions and 2166 deletions
+4 -7
View File
@@ -13,7 +13,7 @@
<PackageVersion Include="Akka.Remote.Hosting" Version="1.5.62" />
<PackageVersion Include="Akka.Streams" Version="1.5.62" />
<PackageVersion Include="Akka.Streams.TestKit" Version="1.5.62" />
<PackageVersion Include="Akka.TestKit.Xunit2" Version="1.5.62" />
<PackageVersion Include="Akka.TestKit.Xunit" Version="1.5.62" />
<PackageVersion Include="Avalonia" Version="11.2.7" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.2.7" />
<PackageVersion Include="Avalonia.Desktop" Version="11.2.7" />
@@ -212,13 +212,10 @@
Avalonia.X11 (Linux desktop). Pin to the patched 0.21.3 in the same 0.x line.
Applied via a direct PackageReference in the consuming desktop project(s). -->
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
<!-- xunit v2 pin: AdminUI.Tests / ControlPlane.Tests / Runtime.Tests are held on v2 by
Akka.TestKit.Xunit2 (no xunit.v3 TestKit ships as of Akka 1.5.62 — both candidate ids
Akka.TestKit.Xunit3 / Akka.TestKit.Xunit.v3 return BlobNotFound on nuget.org, verified
2026-07-13). All 44 other test projects are on xunit.v3 1.1.0. Remove this pin and migrate
the 3 holdouts when Akka ships an xunit.v3 TestKit. (R2-12 / 07/C-4.) -->
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2" />
<!-- Akka.TestKit.Xunit 1.5.62 (the xunit.v3 TestKit) requires xunit.v3.assert / extensibility.core >= 3.2.2, so
the 4 Akka TestKit test projects (AdminUI/ControlPlane/Runtime/Host.Tests) carry VersionOverride="3.2.2".
Move this pin to 3.2.2 repo-wide (and drop the overrides) once the other projects' analyzer fallout is handled. -->
<PackageVersion Include="xunit.v3" Version="1.1.0" />
<PackageVersion Include="ZB.MOM.WW.Health" Version="0.3.0" />
<!--
@@ -28,28 +28,28 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
var roots = await svc.LoadRootsAsync(AclScopeTestDb.ClusterId);
var roots = await svc.LoadRootsAsync(AclScopeTestDb.ClusterId, TestContext.Current.CancellationToken);
var cluster = roots.Raw.ShouldHaveSingleItem();
cluster.ScopeKind.ShouldBe(NodeAclScopeKind.Cluster);
var folder = (await svc.LoadChildrenAsync(cluster)).ShouldHaveSingleItem();
var folder = (await svc.LoadChildrenAsync(cluster, TestContext.Current.CancellationToken)).ShouldHaveSingleItem();
folder.ScopeKind.ShouldBe(NodeAclScopeKind.RawFolder);
folder.ScopeId.ShouldBe(AclScopeTestDb.FolderId);
var driver = (await svc.LoadChildrenAsync(folder)).ShouldHaveSingleItem();
var driver = (await svc.LoadChildrenAsync(folder, TestContext.Current.CancellationToken)).ShouldHaveSingleItem();
driver.ScopeKind.ShouldBe(NodeAclScopeKind.RawDriver);
driver.ScopeId.ShouldBe(AclScopeTestDb.DriverId);
var device = (await svc.LoadChildrenAsync(driver)).ShouldHaveSingleItem();
var device = (await svc.LoadChildrenAsync(driver, TestContext.Current.CancellationToken)).ShouldHaveSingleItem();
device.ScopeKind.ShouldBe(NodeAclScopeKind.RawDevice);
device.ScopeId.ShouldBe(AclScopeTestDb.DeviceId);
var group = (await svc.LoadChildrenAsync(device)).ShouldHaveSingleItem();
var group = (await svc.LoadChildrenAsync(device, TestContext.Current.CancellationToken)).ShouldHaveSingleItem();
group.ScopeKind.ShouldBe(NodeAclScopeKind.RawTagGroup);
group.ScopeId.ShouldBe(AclScopeTestDb.TagGroupId);
var tag = (await svc.LoadChildrenAsync(group)).ShouldHaveSingleItem();
var tag = (await svc.LoadChildrenAsync(group, TestContext.Current.CancellationToken)).ShouldHaveSingleItem();
tag.ScopeKind.ShouldBe(NodeAclScopeKind.Tag);
tag.ScopeId.ShouldBe(AclScopeTestDb.TagId);
}
@@ -64,10 +64,10 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
var cluster = (await svc.LoadRootsAsync(AclScopeTestDb.ClusterId)).Raw.ShouldHaveSingleItem();
var cluster = (await svc.LoadRootsAsync(AclScopeTestDb.ClusterId, TestContext.Current.CancellationToken)).Raw.ShouldHaveSingleItem();
cluster.ScopeId.ShouldBeNull();
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, cluster.ScopeKind!.Value, cluster.ScopeId))
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, cluster.ScopeKind!.Value, cluster.ScopeId, TestContext.Current.CancellationToken))
.Ok.ShouldBeTrue();
}
@@ -81,13 +81,13 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
var roots = await svc.LoadRootsAsync(AclScopeTestDb.ClusterId);
var roots = await svc.LoadRootsAsync(AclScopeTestDb.ClusterId, TestContext.Current.CancellationToken);
var reachable = await FlattenAsync(svc, roots);
reachable.ShouldNotContain(AclScopeTestDb.OtherDriverId);
// …and it is a real driver, so the absence is the rooting rather than the id being unknown.
var side = await svc.LoadRootsAsync(AclScopeTestDb.OtherClusterId);
var side = await svc.LoadRootsAsync(AclScopeTestDb.OtherClusterId, TestContext.Current.CancellationToken);
(await FlattenAsync(svc, side)).ShouldContain(AclScopeTestDb.OtherDriverId);
}
@@ -97,7 +97,7 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
var area = (await svc.LoadRootsAsync(AclScopeTestDb.ClusterId)).Uns.ShouldHaveSingleItem();
var area = (await svc.LoadRootsAsync(AclScopeTestDb.ClusterId, TestContext.Current.CancellationToken)).Uns.ShouldHaveSingleItem();
area.ScopeKind.ShouldBe(NodeAclScopeKind.UnsArea);
area.ScopeId.ShouldBe(AclScopeTestDb.AreaId);
@@ -120,7 +120,7 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
var roots = await svc.LoadRootsAsync("NO-SUCH-CLUSTER");
var roots = await svc.LoadRootsAsync("NO-SUCH-CLUSTER", TestContext.Current.CancellationToken);
roots.Raw.ShouldBeEmpty();
roots.Uns.ShouldBeEmpty();
@@ -146,7 +146,7 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, kind, id))
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, kind, id, TestContext.Current.CancellationToken))
.Ok.ShouldBeTrue($"{kind} '{id}' names a real entity of that type");
// The same id under a different kind must not resolve. RawDevice is the neighbour for every raw
@@ -156,7 +156,7 @@ public sealed class AclScopeServiceTests
? (kind == NodeAclScopeKind.UnsArea ? NodeAclScopeKind.UnsLine : NodeAclScopeKind.UnsArea)
: (kind == NodeAclScopeKind.RawDevice ? NodeAclScopeKind.RawDriver : NodeAclScopeKind.RawDevice);
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, neighbour, id))
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, neighbour, id, TestContext.Current.CancellationToken))
.Ok.ShouldBeFalse($"'{id}' is not a {neighbour}");
}
@@ -167,7 +167,7 @@ public sealed class AclScopeServiceTests
var svc = AclScopeTestDb.NewService();
var check = await svc.CheckScopeAsync(
AclScopeTestDb.ClusterId, NodeAclScopeKind.RawDevice, "DEV-typo");
AclScopeTestDb.ClusterId, NodeAclScopeKind.RawDevice, "DEV-typo", TestContext.Current.CancellationToken);
check.Ok.ShouldBeFalse();
check.Message.ShouldContain("DEV-typo");
@@ -184,7 +184,7 @@ public sealed class AclScopeServiceTests
var svc = AclScopeTestDb.NewService();
var check = await svc.CheckScopeAsync(
AclScopeTestDb.ClusterId, NodeAclScopeKind.RawDriver, AclScopeTestDb.OtherDriverId);
AclScopeTestDb.ClusterId, NodeAclScopeKind.RawDriver, AclScopeTestDb.OtherDriverId, TestContext.Current.CancellationToken);
check.Ok.ShouldBeFalse();
check.Message.ShouldContain(AclScopeTestDb.OtherClusterId);
@@ -192,7 +192,7 @@ public sealed class AclScopeServiceTests
// Authored under its own cluster, the same row is fine — so the rejection is the boundary, not
// the driver.
(await svc.CheckScopeAsync(
AclScopeTestDb.OtherClusterId, NodeAclScopeKind.RawDriver, AclScopeTestDb.OtherDriverId))
AclScopeTestDb.OtherClusterId, NodeAclScopeKind.RawDriver, AclScopeTestDb.OtherDriverId, TestContext.Current.CancellationToken))
.Ok.ShouldBeTrue();
}
@@ -206,13 +206,13 @@ public sealed class AclScopeServiceTests
var svc = AclScopeTestDb.NewService();
(await svc.CheckScopeAsync(
AclScopeTestDb.ClusterId, NodeAclScopeKind.Equipment, AclScopeTestDb.OrphanEquipmentId))
AclScopeTestDb.ClusterId, NodeAclScopeKind.Equipment, AclScopeTestDb.OrphanEquipmentId, TestContext.Current.CancellationToken))
.Ok.ShouldBeFalse();
// The healthy equipment in the same table passes, so the rejection is the broken line and not the
// Equipment arm being wired to nothing.
(await svc.CheckScopeAsync(
AclScopeTestDb.ClusterId, NodeAclScopeKind.Equipment, AclScopeTestDb.EquipmentId))
AclScopeTestDb.ClusterId, NodeAclScopeKind.Equipment, AclScopeTestDb.EquipmentId, TestContext.Current.CancellationToken))
.Ok.ShouldBeTrue();
}
@@ -226,9 +226,9 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, NodeAclScopeKind.Cluster, AclScopeTestDb.DeviceId))
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, NodeAclScopeKind.Cluster, AclScopeTestDb.DeviceId, TestContext.Current.CancellationToken))
.Ok.ShouldBeFalse();
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, NodeAclScopeKind.Cluster, null))
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, NodeAclScopeKind.Cluster, null, TestContext.Current.CancellationToken))
.Ok.ShouldBeTrue();
}
@@ -238,9 +238,9 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, NodeAclScopeKind.RawDevice, null))
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, NodeAclScopeKind.RawDevice, null, TestContext.Current.CancellationToken))
.Ok.ShouldBeFalse();
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, NodeAclScopeKind.RawDevice, " "))
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, NodeAclScopeKind.RawDevice, " ", TestContext.Current.CancellationToken))
.Ok.ShouldBeFalse();
}
@@ -254,7 +254,7 @@ public sealed class AclScopeServiceTests
var svc = AclScopeTestDb.NewService();
var check = await svc.CheckScopeAsync(
AclScopeTestDb.ClusterId, NodeAclScopeKind.FolderSegment, AclScopeTestDb.FolderId);
AclScopeTestDb.ClusterId, NodeAclScopeKind.FolderSegment, AclScopeTestDb.FolderId, TestContext.Current.CancellationToken);
check.Ok.ShouldBeFalse();
check.Message.ShouldContain("RawFolder");
@@ -272,7 +272,7 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, NodeAclScopeKind.Namespace, scopeId))
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, NodeAclScopeKind.Namespace, scopeId, TestContext.Current.CancellationToken))
.Ok.ShouldBe(expected);
}
@@ -282,7 +282,7 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, (NodeAclScopeKind)999, "anything"))
(await svc.CheckScopeAsync(AclScopeTestDb.ClusterId, (NodeAclScopeKind)999, "anything", TestContext.Current.CancellationToken))
.Ok.ShouldBeFalse();
}
@@ -294,7 +294,7 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
var usage = await svc.CheckPrincipalAsync(OpcUaPrincipals.Anonymous);
var usage = await svc.CheckPrincipalAsync(OpcUaPrincipals.Anonymous, TestContext.Current.CancellationToken);
usage.IsAnonymous.ShouldBeTrue();
usage.ReachesSomething.ShouldBeTrue();
@@ -309,12 +309,12 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService(new FixedClock(AclScopeTestDb.Now));
var live = await svc.CheckPrincipalAsync(AclScopeTestDb.LivePrincipal);
var live = await svc.CheckPrincipalAsync(AclScopeTestDb.LivePrincipal, TestContext.Current.CancellationToken);
live.EnabledKeys.ShouldBe(2);
live.DisabledKeys.ShouldBe(1);
live.ReachesSomething.ShouldBeTrue();
var stale = await svc.CheckPrincipalAsync(AclScopeTestDb.StalePrincipal);
var stale = await svc.CheckPrincipalAsync(AclScopeTestDb.StalePrincipal, TestContext.Current.CancellationToken);
stale.EnabledKeys.ShouldBe(0, "its only key expired yesterday");
stale.DisabledKeys.ShouldBe(1);
stale.ReachesSomething.ShouldBeFalse();
@@ -326,7 +326,7 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
var usage = await svc.CheckPrincipalAsync("scadabridge-dcI"); // capital i, not lowercase L
var usage = await svc.CheckPrincipalAsync("scadabridge-dcI", TestContext.Current.CancellationToken); // capital i, not lowercase L
usage.EnabledKeys.ShouldBe(0);
usage.ReachesSomething.ShouldBeFalse();
@@ -338,7 +338,7 @@ public sealed class AclScopeServiceTests
{
var svc = AclScopeTestDb.NewService();
var known = await svc.LoadKnownPrincipalsAsync();
var known = await svc.LoadKnownPrincipalsAsync(TestContext.Current.CancellationToken);
known.ShouldContain(AclScopeTestDb.LivePrincipal);
known.ShouldContain(AclScopeTestDb.StalePrincipal);
@@ -25,10 +25,8 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
/// </para>
/// <para>
/// The skip is an attribute rather than the <c>Assert.Skip(fixture.SkipReason)</c> call the S7
/// integration suite uses, because AdminUI.Tests is one of the three projects pinned to
/// <b>xunit v2</b> by <c>Akka.TestKit.Xunit2</c> (see <c>Directory.Packages.props</c>) and
/// <c>Assert.Skip</c> is xunit v3 only. Overriding <c>FactAttribute.Skip</c> is the v2 equivalent
/// and reports a genuine "skipped", not a vacuous pass. Both the attribute and this fixture read
/// integration suite uses, so that the skip is decided before the test runs and
/// the <c>FactAttribute.Skip</c> override reports a genuine "skipped", not a vacuous pass. Both the attribute and this fixture read
/// the same one-shot probe in <see cref="AdminUiSqlServer"/>, so a session probes once.
/// </para>
/// <para>
@@ -382,14 +380,17 @@ END";
/// <summary>
/// A <see cref="FactAttribute"/> that skips itself when the shared test SQL Server is unreachable.
/// The xunit v2 equivalent of xunit v3's <c>Assert.Skip</c> — see
/// Attribute-level counterpart of xunit v3's <c>Assert.Skip</c> — see
/// <see cref="AdminUiSqlServerFixture"/>'s remarks for why this project cannot use the latter.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class SqlServerFactAttribute : FactAttribute
{
/// <summary>Initializes a new instance of the <see cref="SqlServerFactAttribute"/> class.</summary>
public SqlServerFactAttribute()
public SqlServerFactAttribute(
[System.Runtime.CompilerServices.CallerFilePath] string? sourceFilePath = null,
[System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = -1)
: base(sourceFilePath, sourceLineNumber)
{
if (AdminUiSqlServer.SkipReason is { } reason) Skip = reason;
}
@@ -1,6 +1,6 @@
using Akka.Actor;
using Akka.Hosting;
using Akka.TestKit.Xunit2;
using Akka.TestKit.Xunit;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Clients;
@@ -59,7 +59,7 @@ public sealed class AlarmsHistorianReaderTests : TestKit
reading.Availability.ShouldBe(HistorianSinkAvailability.Unavailable);
reading.Message!.ShouldContain("did not answer");
// Positive control: the Ask really was delivered — the classification is about the missing reply.
silent.ExpectMsg<HistorianAdapterActor.GetStatus>();
silent.ExpectMsg<HistorianAdapterActor.GetStatus>(cancellationToken: TestContext.Current.CancellationToken);
}
[Fact]
@@ -1,5 +1,5 @@
using Akka.Hosting;
using Akka.TestKit.Xunit2;
using Akka.TestKit.Xunit;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Clients;
@@ -52,7 +52,7 @@ public sealed class AlertsAckPathTests : TestKit
result.Ok.ShouldBeTrue();
result.Message.ShouldBeNull();
var sent = mesh.ExpectMsg<MeshCommand>(TimeSpan.FromSeconds(5));
var sent = mesh.ExpectMsg<MeshCommand>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
sent.Topic.ShouldBe(AlarmCommandsTopic.Name);
var command = sent.Message.ShouldBeOfType<AlarmCommand>();
command.AlarmId.ShouldBe("alarm-42");
@@ -74,7 +74,7 @@ public sealed class AlertsAckPathTests : TestKit
"alarm-7", "operator-kim", ShelveKind.Timed, unshelveAtUtc: until, comment: null, CancellationToken.None);
result.Ok.ShouldBeTrue();
var command = mesh.ExpectMsg<MeshCommand>(TimeSpan.FromSeconds(5)).Message.ShouldBeOfType<AlarmCommand>();
var command = mesh.ExpectMsg<MeshCommand>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).Message.ShouldBeOfType<AlarmCommand>();
command.Operation.ShouldBe("TimedShelve");
command.UnshelveAtUtc.ShouldBe(until);
}
@@ -41,11 +41,11 @@ public sealed class DeploymentStatusEndpointTests
seed.Deployments.Add(new Deployment { DeploymentId = Guid.NewGuid(), RevisionHash = "other", CreatedBy = "x" });
seed.NodeDeploymentStates.Add(new NodeDeploymentState { NodeId = "node-b", DeploymentId = id, Status = NodeDeploymentStatus.Applying });
seed.NodeDeploymentStates.Add(new NodeDeploymentState { NodeId = "node-a", DeploymentId = id, Status = NodeDeploymentStatus.Applied, AppliedAtUtc = DateTime.UtcNow });
await seed.SaveChangesAsync();
await seed.SaveChangesAsync(TestContext.Current.CancellationToken);
}
await using var db = NewDb(name);
var status = await DeployApiEndpoints.GetDeploymentStatusAsync(db, id);
var status = await DeployApiEndpoints.GetDeploymentStatusAsync(db, id, TestContext.Current.CancellationToken);
status.ShouldNotBeNull();
status!.DeploymentId.ShouldBe(id);
@@ -59,7 +59,7 @@ public sealed class DeploymentStatusEndpointTests
public async Task GetDeploymentStatusAsync_returns_null_for_an_unknown_id()
{
await using var db = NewDb(Guid.NewGuid().ToString("N"));
(await DeployApiEndpoints.GetDeploymentStatusAsync(db, Guid.NewGuid())).ShouldBeNull();
(await DeployApiEndpoints.GetDeploymentStatusAsync(db, Guid.NewGuid(), TestContext.Current.CancellationToken)).ShouldBeNull();
}
[Fact]
@@ -28,7 +28,7 @@ public sealed class ApiKeyGalaxyAckIdentityServiceTests
[Fact]
public async Task Set_writes_both_secrets_encrypted_under_the_driver_names()
{
await Service().SetAsync(KeyId, "alice", "CORP", Password, "admin", default);
await Service().SetAsync(KeyId, "alice", "CORP", Password, "admin", TestContext.Current.CancellationToken);
var user = _store.Rows[GalaxyAckIdentitySecrets.UserSecretName(KeyId)];
var pw = _store.Rows[GalaxyAckIdentitySecrets.PasswordSecretName(KeyId)];
@@ -48,11 +48,11 @@ public sealed class ApiKeyGalaxyAckIdentityServiceTests
public async Task List_returns_identity_and_never_decrypts_a_password()
{
var svc = Service();
await svc.SetAsync(KeyId, "alice", "CORP", Password, "admin", default);
await svc.SetAsync("ffff0000ffff0000", "bob", null, "", "admin", default);
await svc.SetAsync(KeyId, "alice", "CORP", Password, "admin", TestContext.Current.CancellationToken);
await svc.SetAsync("ffff0000ffff0000", "bob", null, "", "admin", TestContext.Current.CancellationToken);
_store.GetCalls.Clear();
var list = await svc.ListAsync(default);
var list = await svc.ListAsync(TestContext.Current.CancellationToken);
list[KeyId].ShouldBe("CORP\\alice");
list["ffff0000ffff0000"].ShouldBe("bob");
@@ -63,12 +63,12 @@ public sealed class ApiKeyGalaxyAckIdentityServiceTests
public async Task Clear_removes_both_and_audits_without_the_credential()
{
var svc = Service();
await svc.SetAsync(KeyId, "alice", "CORP", Password, "admin", default);
await svc.SetAsync(KeyId, "alice", "CORP", Password, "admin", TestContext.Current.CancellationToken);
await svc.ClearAsync(KeyId, "admin", default);
await svc.ClearAsync(KeyId, "admin", TestContext.Current.CancellationToken);
_store.Rows.ShouldBeEmpty();
(await svc.ListAsync(default)).ShouldBeEmpty();
(await svc.ListAsync(TestContext.Current.CancellationToken)).ShouldBeEmpty();
_audit.Events.Select(e => e.Action).ShouldBe(["apikey.galaxy-ack-identity.set", "apikey.galaxy-ack-identity.clear"]);
_audit.Events.ShouldAllBe(e => e.Target == KeyId && e.Actor == "admin");
string.Join("|", _audit.Events.Select(e => e.ToString())).ShouldNotContain(Password);
@@ -1,5 +1,5 @@
using Akka.Hosting;
using Akka.TestKit.Xunit2;
using Akka.TestKit.Xunit;
using Shouldly;
using Xunit;
using ZB.MOM.WW.Audit;
@@ -36,9 +36,9 @@ public sealed class ActorAuditWriterTests : TestKit
DetailsJson = "{}",
};
await writer.WriteAsync(evt);
await writer.WriteAsync(evt, TestContext.Current.CancellationToken);
var received = probe.ExpectMsg<AuditEvent>(TimeSpan.FromSeconds(3));
var received = probe.ExpectMsg<AuditEvent>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
received.ShouldBe(evt);
}
}
@@ -57,7 +57,7 @@ public sealed class DeploymentDriftProbeTests
SourceNode = "node-1",
EditedAtUtc = DateTime.UtcNow,
});
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
compose.Result = "a-different-hash";
@@ -27,16 +27,16 @@ public sealed class FleetDiagnosticsLinksTests
DriverInstanceId = "drv-press-1", ClusterId = "MAIN", Name = "Press line Modbus",
DriverType = "Modbus", DriverConfig = "{}",
});
await seed.SaveChangesAsync();
await seed.SaveChangesAsync(TestContext.Current.CancellationToken);
}
await using var db = new OtOpcUaConfigDbContext(options);
var names = await FleetDiagnosticsLinks.LoadDisplayNamesAsync(db, ["drv-press-1", "drv-unknown"]);
var names = await FleetDiagnosticsLinks.LoadDisplayNamesAsync(db, ["drv-press-1", "drv-unknown"], TestContext.Current.CancellationToken);
names["DRV-PRESS-1"].ShouldBe("Press line Modbus"); // the returned map is case-insensitive
names.ContainsKey("drv-unknown").ShouldBeFalse();
(await FleetDiagnosticsLinks.LoadDisplayNamesAsync(db, [])).ShouldBeEmpty();
(await FleetDiagnosticsLinks.LoadDisplayNamesAsync(db, [], TestContext.Current.CancellationToken)).ShouldBeEmpty();
}
[Fact]
@@ -1,5 +1,5 @@
using Akka.Hosting;
using Akka.TestKit.Xunit2;
using Akka.TestKit.Xunit;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -53,7 +53,7 @@ public sealed class CentralCommandGrpcServiceTests : TestKit
response.ShouldNotBeNull();
var received = probe.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(3));
var received = probe.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
received.DeploymentId.ShouldBe(DeploymentId.Parse(request.DeploymentId));
received.NodeId.Value.ShouldBe("site-a-1");
received.Outcome.ShouldBe(ApplyAckOutcome.Failed);
@@ -73,12 +73,12 @@ public sealed class CentralCommandGrpcServiceTests : TestKit
var service = Service(registry);
await service.ReportApply(WellFormed(), new FakeServerCallContext());
probe.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(3)).FailureReason.ShouldBeNull();
probe.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken).FailureReason.ShouldBeNull();
var blank = WellFormed();
blank.FailureReason = "";
await service.ReportApply(blank, new FakeServerCallContext());
probe.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(3)).FailureReason.ShouldBe("");
probe.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken).FailureReason.ShouldBe("");
}
[Fact]
@@ -109,7 +109,7 @@ public sealed class CentralCommandGrpcServiceTests : TestKit
Service(registry).ReportApply(request, new FakeServerCallContext()));
ex.StatusCode.ShouldBe(StatusCode.InvalidArgument);
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
}
[Fact]
@@ -126,7 +126,7 @@ public sealed class CentralCommandGrpcServiceTests : TestKit
Service(registry).ReportApply(request, new FakeServerCallContext()));
ex.StatusCode.ShouldBe(StatusCode.InvalidArgument);
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
}
[Fact]
@@ -145,7 +145,7 @@ public sealed class CentralCommandGrpcServiceTests : TestKit
Service(registry).ReportApply(request, new FakeServerCallContext()));
ex.StatusCode.ShouldBe(StatusCode.InvalidArgument);
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
}
[Fact]
@@ -55,10 +55,10 @@ public sealed class DeployedDriverSetTests
{
db.Deployments.Add(Dep(d1, DeploymentStatus.Sealed, Now.AddHours(-1), Artifact("drv-quiet", "drv-kept")));
db.NodeDeploymentStates.Add(Node("n1", d1, NodeDeploymentStatus.Applied, Now.AddHours(-1)));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var deployed = await new DeployedDriverSet(factory, parseDriverIds: Parse).LoadAsync();
var deployed = await new DeployedDriverSet(factory, parseDriverIds: Parse).LoadAsync(TestContext.Current.CancellationToken);
deployed.ShouldNotBeNull();
var store = new InMemoryDriverStatusSnapshotStore();
@@ -89,10 +89,10 @@ public sealed class DeployedDriverSetTests
db.NodeDeploymentStates.Add(Node("n1", stale, NodeDeploymentStatus.Applied, Now.AddDays(-9)));
db.NodeDeploymentStates.Add(Node("n1", old, NodeDeploymentStatus.Applied, Now.AddHours(-3)));
db.NodeDeploymentStates.Add(Node("n1", failed, NodeDeploymentStatus.Failed, Now.AddHours(-1)));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var deployed = await new DeployedDriverSet(factory, parseDriverIds: Parse).LoadAsync();
var deployed = await new DeployedDriverSet(factory, parseDriverIds: Parse).LoadAsync(TestContext.Current.CancellationToken);
// n1's latest (failed apply, may be partly running), its latest Applied (what it ran before), and the
// latest Sealed deployment — but not a superseded one nobody runs.
@@ -108,13 +108,13 @@ public sealed class DeployedDriverSetTests
await using (var db = factory.CreateDbContext())
{
db.Deployments.Add(Dep(d1, DeploymentStatus.Sealed, Now, Artifact("drv-a")));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var parses = 0;
var set = new DeployedDriverSet(factory, parseDriverIds: b => { parses++; return Parse(b); });
await set.LoadAsync();
await set.LoadAsync();
await set.LoadAsync(TestContext.Current.CancellationToken);
await set.LoadAsync(TestContext.Current.CancellationToken);
parses.ShouldBe(1);
set.CachedDeployments.ShouldBe(1);
@@ -122,10 +122,10 @@ public sealed class DeployedDriverSetTests
await using (var db = factory.CreateDbContext())
{
db.Deployments.Add(Dep(d2, DeploymentStatus.Sealed, Now.AddMinutes(1), Artifact("drv-b")));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
(await set.LoadAsync())!.ShouldBe(["drv-b"]);
(await set.LoadAsync(TestContext.Current.CancellationToken))!.ShouldBe(["drv-b"]);
set.CachedDeployments.ShouldBe(1); // d1 is no longer live, so it is pruned
parses.ShouldBe(2);
}
@@ -138,10 +138,10 @@ public sealed class DeployedDriverSetTests
await using (var db = factory.CreateDbContext())
{
db.Deployments.Add(Dep(d1, DeploymentStatus.Sealed, Now, Array.Empty<byte>())); // retention-emptied
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
(await new DeployedDriverSet(factory, parseDriverIds: Parse).LoadAsync()).ShouldBeNull();
(await new DeployedDriverSet(factory, parseDriverIds: Parse).LoadAsync(TestContext.Current.CancellationToken)).ShouldBeNull();
}
[Fact]
@@ -25,7 +25,7 @@ public sealed class TelemetryModeWiringTests
public async Task Dps_mode_spawns_the_four_telemetry_bridges_and_not_the_dial_supervisor()
{
using var host = BuildBridgeHost(TelemetryDialOptions.ModeDps);
await host.StartAsync();
await host.StartAsync(TestContext.Current.CancellationToken);
try
{
var registry = host.Services.GetRequiredService<ActorRegistry>();
@@ -39,7 +39,7 @@ public sealed class TelemetryModeWiringTests
}
finally
{
await host.StopAsync();
await host.StopAsync(TestContext.Current.CancellationToken);
}
}
@@ -48,7 +48,7 @@ public sealed class TelemetryModeWiringTests
public async Task Grpc_mode_spawns_the_dial_supervisor_and_none_of_the_four_telemetry_bridges()
{
using var host = BuildBridgeHost(TelemetryDialOptions.ModeGrpc);
await host.StartAsync();
await host.StartAsync(TestContext.Current.CancellationToken);
try
{
var registry = host.Services.GetRequiredService<ActorRegistry>();
@@ -62,7 +62,7 @@ public sealed class TelemetryModeWiringTests
}
finally
{
await host.StopAsync();
await host.StopAsync(TestContext.Current.CancellationToken);
}
}
@@ -71,7 +71,7 @@ public sealed class TelemetryModeWiringTests
public async Task Default_mode_is_dps()
{
using var host = BuildBridgeHost(mode: null);
await host.StartAsync();
await host.StartAsync(TestContext.Current.CancellationToken);
try
{
var registry = host.Services.GetRequiredService<ActorRegistry>();
@@ -80,7 +80,7 @@ public sealed class TelemetryModeWiringTests
}
finally
{
await host.StopAsync();
await host.StopAsync(TestContext.Current.CancellationToken);
}
}
@@ -79,7 +79,7 @@ public sealed class RawBrowseRebirthRefreshTests : IDisposable
gate.SetResult(new BrowseOpenResult(true, null, stale));
await open;
await svc.Closed.Task.WaitAsync(TimeSpan.FromSeconds(10));
await svc.Closed.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
svc.ClosedTokens.ShouldBe([stale]);
Get<Guid>(modal, "_token").ShouldBe(Guid.Empty);
}
@@ -68,8 +68,8 @@ public sealed class RawCsvRenderingTests
var dbName = Uns.RawTreeTestDb.SeedFresh();
var reader = new RawTagCsvExportReader(Uns.RawTreeTestDb.Factory(dbName));
var full = await RawTagCsvExport.BuildAsync(reader, Uns.RawTreeTestDb.FolderedDeviceId, null, "Modbus");
var preview = await RawTagCsvExport.BuildAsync(reader, Uns.RawTreeTestDb.FolderedDeviceId, null, "Modbus", maxRows: 1);
var full = await RawTagCsvExport.BuildAsync(reader, Uns.RawTreeTestDb.FolderedDeviceId, null, "Modbus", TestContext.Current.CancellationToken);
var preview = await RawTagCsvExport.BuildAsync(reader, Uns.RawTreeTestDb.FolderedDeviceId, null, "Modbus", ct: TestContext.Current.CancellationToken, maxRows: 1);
full!.RowCount.ShouldBe(2);
Commons.Csv.CsvParser.Parse(full.Csv).Count.ShouldBe(3); // header + 2 (positive control)
@@ -50,9 +50,9 @@ public sealed class AnalysisCompilationReuseTests
// A clean script stays clean and an error stays an error across interleaved analyses on one service
// — a stale tree leaking from the previous call would surface here.
var svc = new ScriptAnalysisService();
svc.Diagnose(new DiagnoseRequest("return ctx.Nope();")).Markers.ShouldContain(m => m.Severity == 8);
svc.Diagnose(new DiagnoseRequest("""return (double)ctx.GetTag("A").Value * 2.0;""")).Markers.ShouldBeEmpty();
svc.Diagnose(new DiagnoseRequest("var a = 1;\nreturn ctx.Nope();")).Markers
svc.Diagnose(new DiagnoseRequest("return ctx.Nope();"), TestContext.Current.CancellationToken).Markers.ShouldContain(m => m.Severity == 8);
svc.Diagnose(new DiagnoseRequest("""return (double)ctx.GetTag("A").Value * 2.0;"""), TestContext.Current.CancellationToken).Markers.ShouldBeEmpty();
svc.Diagnose(new DiagnoseRequest("var a = 1;\nreturn ctx.Nope();"), TestContext.Current.CancellationToken).Markers
.ShouldContain(m => m.Severity == 8 && m.StartLineNumber == 2);
}
}
@@ -23,34 +23,34 @@ public sealed class CtxCompletionGuardTests
[Fact] public async Task Completion_on_ctx_GetTag_literal_offers_catalog_paths()
{
// "return ctx.GetTag(" + '""' + ')' — col 20 lands on the closing quote of the empty string literal.
var res = await Svc.CompleteAsync(new CompletionsRequest("return ctx.GetTag(\"\")", 1, 20));
var res = await Svc.CompleteAsync(new CompletionsRequest("return ctx.GetTag(\"\")", 1, 20), TestContext.Current.CancellationToken);
res.Items.ShouldContain(i => i.InsertText == "Line1.Speed");
}
[Fact] public async Task Completion_on_non_ctx_receiver_does_NOT_offer_catalog_paths()
{
// Same shape but receiver is "foo" — must NOT offer catalog paths.
var res = await Svc.CompleteAsync(new CompletionsRequest("return foo.GetTag(\"\")", 1, 20));
var res = await Svc.CompleteAsync(new CompletionsRequest("return foo.GetTag(\"\")", 1, 20), TestContext.Current.CancellationToken);
res.Items.ShouldNotContain(i => i.InsertText == "Line1.Speed");
}
[Fact] public async Task Hover_on_ctx_SetVirtualTag_literal_warns_write_is_dropped()
{
var md = (await Svc.Hover(new HoverRequest("return ctx.SetVirtualTag(\"V\", 1);", 1, 27))).Markdown;
var md = (await Svc.Hover(new HoverRequest("return ctx.SetVirtualTag(\"V\", 1);", 1, 27), TestContext.Current.CancellationToken)).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("single-tag mode");
}
[Fact] public async Task Hover_on_ctx_GetTag_literal_has_no_dropped_write_note()
{
var md = (await Svc.Hover(new HoverRequest("return ctx.GetTag(\"V\").Value;", 1, 20))).Markdown;
var md = (await Svc.Hover(new HoverRequest("return ctx.GetTag(\"V\").Value;", 1, 20), TestContext.Current.CancellationToken)).Markdown;
md.ShouldNotBeNull();
md!.ShouldNotContain("single-tag mode");
}
[Fact] public async Task Hover_on_non_ctx_receiver_literal_is_not_treated_as_a_tag_path()
{
var md = (await Svc.Hover(new HoverRequest("return bar.GetTag(\"V\");", 1, 20))).Markdown;
var md = (await Svc.Hover(new HoverRequest("return bar.GetTag(\"V\");", 1, 20), TestContext.Current.CancellationToken)).Markdown;
(md is null || !md.Contains("Tag path")).ShouldBeTrue();
}
}
@@ -65,7 +65,7 @@ public sealed class EquipTokenEditorTests
[Fact] public async Task Hover_on_equip_token_literal_notes_equipment_relative()
{
// caret inside ctx.GetTag("{{equip}}/Speed")
var md = (await Svc.Hover(new HoverRequest("""return ctx.GetTag("{{equip}}/Speed").Value;""", 1, 24))).Markdown;
var md = (await Svc.Hover(new HoverRequest("""return ctx.GetTag("{{equip}}/Speed").Value;""", 1, 24), TestContext.Current.CancellationToken)).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("Equipment-relative");
// the {{equip}} token must render literally (non-interpolated markdown segment)
@@ -78,14 +78,14 @@ public sealed class EquipTokenEditorTests
// "Missing" is not one of the equipment's references (Speed/Temp) → a diagnostic, matching the
// deploy gate. Uses the equipment context via the request's EquipmentId.
var resp = await Svc.DiagnoseAsync(
new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Missing").Value;""", "EQ-1"));
new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Missing").Value;""", "EQ-1"), TestContext.Current.CancellationToken);
resp.Markers.ShouldContain(m => m.Code == "OTSCRIPT_EQUIPREF" && m.Message.Contains("Missing"));
}
[Fact] public async Task Diagnostic_clean_for_resolved_equip_reference()
{
var resp = await Svc.DiagnoseAsync(
new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Speed").Value;""", "EQ-1"));
new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Speed").Value;""", "EQ-1"), TestContext.Current.CancellationToken);
resp.Markers.ShouldNotContain(m => m.Code == "OTSCRIPT_EQUIPREF");
}
@@ -94,7 +94,7 @@ public sealed class EquipTokenEditorTests
// No EquipmentId (shared ScriptEdit page) → the equip-ref check is inert (base markers only),
// matching the deploy gate which only resolves per owning equipment.
var resp = await Svc.DiagnoseAsync(
new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Missing").Value;"""));
new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Missing").Value;"""), TestContext.Current.CancellationToken);
resp.Markers.ShouldNotContain(m => m.Code == "OTSCRIPT_EQUIPREF");
}
}
@@ -23,18 +23,18 @@ public sealed class HoverSignatureTests
[Fact] public async Task Hover_on_GetTag_returns_member_markdown()
{
// hover over the "GetTag" identifier in ctx.GetTag("A")
var md = (await Svc.Hover(new HoverRequest("""return ctx.GetTag("A").Value;""", 1, 16))).Markdown;
var md = (await Svc.Hover(new HoverRequest("""return ctx.GetTag("A").Value;""", 1, 16), TestContext.Current.CancellationToken)).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("GetTag");
}
[Fact] public async Task Hover_on_nothing_returns_null()
=> (await Svc.Hover(new HoverRequest(" ", 1, 1))).Markdown.ShouldBeNull();
=> (await Svc.Hover(new HoverRequest(" ", 1, 1), TestContext.Current.CancellationToken)).Markdown.ShouldBeNull();
[Fact] public void SignatureHelp_inside_GetTag_call_shows_the_signature()
{
// caret just after the open paren of ctx.GetTag(
var sh = Svc.SignatureHelp(new SignatureHelpRequest("""return ctx.GetTag();""", 1, 19));
var sh = Svc.SignatureHelp(new SignatureHelpRequest("""return ctx.GetTag();""", 1, 19), TestContext.Current.CancellationToken);
sh.Label.ShouldNotBeNull();
sh.Label!.ShouldContain("GetTag");
sh.Parameters.ShouldNotBeNull();
@@ -44,7 +44,7 @@ public sealed class HoverSignatureTests
[Fact] public async Task Hover_on_a_plain_local_resolves_the_local_not_an_enclosing_member()
{
// hovering the local `x` (not a member) must resolve the LOCAL, and must NOT climb to GetTag/Value.
var md = (await Svc.Hover(new HoverRequest("var x = 1;\nreturn ctx.GetTag(\"A\").Value + x;", 2, 33))).Markdown;
var md = (await Svc.Hover(new HoverRequest("var x = 1;\nreturn ctx.GetTag(\"A\").Value + x;", 2, 33), TestContext.Current.CancellationToken)).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("x");
md.ShouldNotContain("GetTag");
@@ -54,12 +54,12 @@ public sealed class HoverSignatureTests
{
// ctx.Now is a property, not a call; use a parameterless method if one exists, else assert empty-safe behavior.
// Deadband is static on ScriptContext (3 params) — instead verify a 0-arg edge via an empty arg list doesn't throw:
var sh = Svc.SignatureHelp(new SignatureHelpRequest("return ctx.GetTag();", 1, 19));
var sh = Svc.SignatureHelp(new SignatureHelpRequest("return ctx.GetTag();", 1, 19), TestContext.Current.CancellationToken);
sh.ActiveParameter.ShouldBe(0); // clamped; no exception
}
[Fact] public async Task Hover_with_out_of_range_position_returns_null_without_throwing()
=> (await Svc.Hover(new HoverRequest("return 1;", 99, 99))).Markdown.ShouldBeNull();
=> (await Svc.Hover(new HoverRequest("return 1;", 99, 99), TestContext.Current.CancellationToken)).Markdown.ShouldBeNull();
// ── tag-path hover (driven by the catalog) ─────────────────────────────────────────────────
@@ -67,7 +67,7 @@ public sealed class HoverSignatureTests
{
var svc = new ScriptAnalysisService(new FakeCatalog());
// caret on the path literal inside ctx.GetTag("Line1.Speed")
var md = (await svc.Hover(new HoverRequest("""return ctx.GetTag("Line1.Speed").Value;""", 1, 24))).Markdown;
var md = (await svc.Hover(new HoverRequest("""return ctx.GetTag("Line1.Speed").Value;""", 1, 24), TestContext.Current.CancellationToken)).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("Line1.Speed");
md.ShouldContain("Double");
@@ -79,7 +79,7 @@ public sealed class HoverSignatureTests
{
var svc = new ScriptAnalysisService(new FakeCatalog());
// caret on the path literal inside ctx.GetTag("Unknownz")
var md = (await svc.Hover(new HoverRequest("""return ctx.GetTag("Unknownz").Value;""", 1, 22))).Markdown;
var md = (await svc.Hover(new HoverRequest("""return ctx.GetTag("Unknownz").Value;""", 1, 22), TestContext.Current.CancellationToken)).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("Not a known");
}
@@ -26,7 +26,7 @@ public sealed class MarkerPayloadBoundTests
[Fact]
public void A_non_csharp_paste_returns_a_bounded_marker_set_that_still_holds_the_errors()
{
var markers = new ScriptAnalysisService().Diagnose(new DiagnoseRequest(NonCSharpPaste())).Markers;
var markers = new ScriptAnalysisService().Diagnose(new DiagnoseRequest(NonCSharpPaste()), TestContext.Current.CancellationToken).Markers;
markers.Count.ShouldBe(ScriptEditorLimits.MaxDiagnosticMarkers); // positive control: the cap was hit
markers.ShouldAllBe(m => m.Message.Length <= ScriptEditorLimits.MaxDiagnosticMessageChars);
@@ -39,7 +39,7 @@ public sealed class MarkerPayloadBoundTests
public void A_long_quoted_token_has_its_message_truncated()
{
var giant = new string('x', 20_000);
var markers = new ScriptAnalysisService().Diagnose(new DiagnoseRequest($"return {giant};")).Markers;
var markers = new ScriptAnalysisService().Diagnose(new DiagnoseRequest($"return {giant};"), TestContext.Current.CancellationToken).Markers;
markers.ShouldNotBeEmpty();
markers.ShouldAllBe(m => m.Message.Length <= ScriptEditorLimits.MaxDiagnosticMessageChars);
@@ -50,7 +50,7 @@ public sealed class MarkerPayloadBoundTests
public void A_small_script_is_not_truncated()
{
// Positive control for the cap: ordinary diagnostics pass through untouched.
var markers = new ScriptAnalysisService().Diagnose(new DiagnoseRequest("return ctx.Nope();")).Markers;
var markers = new ScriptAnalysisService().Diagnose(new DiagnoseRequest("return ctx.Nope();"), TestContext.Current.CancellationToken).Markers;
markers.ShouldContain(m => m.Severity == 8 && !m.Message.EndsWith("…"));
}
@@ -168,7 +168,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
var paths = await catalog.GetPathsAsync(null, default);
var paths = await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken);
paths.ShouldContain("Plant/Modbus/dev1/Speed"); // directly under the device
paths.ShouldContain("Plant/Modbus/dev1/Motors/Torque"); // nested under a tag group
@@ -190,7 +190,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
var paths = await catalog.GetPathsAsync(null, default);
var paths = await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken);
paths.Distinct(StringComparer.OrdinalIgnoreCase).Count().ShouldBe(paths.Count);
}
@@ -203,7 +203,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
var paths = await catalog.GetPathsAsync("Plant/Modbus/dev1/", default);
var paths = await catalog.GetPathsAsync("Plant/Modbus/dev1/", TestContext.Current.CancellationToken);
paths.ShouldContain("Plant/Modbus/dev1/Speed");
paths.ShouldContain("Plant/Modbus/dev1/Motors/Torque");
@@ -218,7 +218,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
(await catalog.GetPathsAsync("plant/modbus", default)).ShouldContain("Plant/Modbus/dev1/Speed");
(await catalog.GetPathsAsync("plant/modbus", TestContext.Current.CancellationToken)).ShouldContain("Plant/Modbus/dev1/Speed");
}
/// <summary>A tag whose ancestry chain is broken (device row missing) has no RawPath. The runtime could
@@ -302,7 +302,7 @@ public sealed class ScriptTagCatalogTests
db.SaveChanges();
}
(await catalog.GetPathsAsync(null, default)).ShouldContain("RootDrv/d0/Scalar");
(await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken)).ShouldContain("RootDrv/d0/Scalar");
}
/// <summary>An empty database yields an empty list rather than throwing.</summary>
@@ -311,7 +311,7 @@ public sealed class ScriptTagCatalogTests
{
var (catalog, _) = Fresh();
(await catalog.GetPathsAsync(null, default)).ShouldBeEmpty();
(await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken)).ShouldBeEmpty();
}
/// <summary>Hover: an absolute RawPath resolves to Tag-kind info carrying the configured DataType and —
@@ -322,7 +322,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
var info = await catalog.GetTagInfoAsync("Plant/Modbus/dev1/Motors/Torque", default);
var info = await catalog.GetTagInfoAsync("Plant/Modbus/dev1/Motors/Torque", TestContext.Current.CancellationToken);
info.ShouldNotBeNull();
info!.Path.ShouldBe("Plant/Modbus/dev1/Motors/Torque");
@@ -339,8 +339,8 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
(await catalog.GetTagInfoAsync("Computed", default)).ShouldBeNull();
var info = await catalog.GetTagInfoAsync("EQ-1/Computed", default);
(await catalog.GetTagInfoAsync("Computed", TestContext.Current.CancellationToken)).ShouldBeNull();
var info = await catalog.GetTagInfoAsync("EQ-1/Computed", TestContext.Current.CancellationToken);
info.ShouldNotBeNull();
info!.Kind.ShouldBe("Virtual tag");
@@ -356,7 +356,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
(await catalog.GetTagInfoAsync("Motor.Speed", default)).ShouldBeNull();
(await catalog.GetTagInfoAsync("Motor.Speed", TestContext.Current.CancellationToken)).ShouldBeNull();
}
/// <summary>An unknown path resolves to null.</summary>
@@ -366,7 +366,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
(await catalog.GetTagInfoAsync("Plant/Modbus/dev1/NoSuchTag", default)).ShouldBeNull();
(await catalog.GetTagInfoAsync("Plant/Modbus/dev1/NoSuchTag", TestContext.Current.CancellationToken)).ShouldBeNull();
}
/// <summary>The lookup is case-SENSITIVE (Ordinal), mirroring the runtime DependencyMuxActor's
@@ -378,8 +378,8 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
(await catalog.GetTagInfoAsync("plant/modbus/dev1/speed", default)).ShouldBeNull();
(await catalog.GetTagInfoAsync("Plant/Modbus/dev1/Speed", default)).ShouldNotBeNull();
(await catalog.GetTagInfoAsync("plant/modbus/dev1/speed", TestContext.Current.CancellationToken)).ShouldBeNull();
(await catalog.GetTagInfoAsync("Plant/Modbus/dev1/Speed", TestContext.Current.CancellationToken)).ShouldNotBeNull();
}
// -----------------------------------------------------------------------
@@ -396,8 +396,8 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts, factory, _) = FreshInstrumented();
Seed(opts);
var first = await catalog.GetPathsAsync(null, default);
var second = await catalog.GetPathsAsync(null, default);
var first = await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken);
var second = await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken);
factory.Created.ShouldBe(1);
second.ShouldBe(first);
@@ -412,7 +412,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts, factory, time) = FreshInstrumented();
Seed(opts);
(await catalog.GetPathsAsync(null, default)).ShouldNotContain("Plant/Modbus/dev1/Pressure");
(await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken)).ShouldNotContain("Plant/Modbus/dev1/Pressure");
factory.Created.ShouldBe(1);
using (var db = new OtOpcUaConfigDbContext(opts))
@@ -427,7 +427,7 @@ public sealed class ScriptTagCatalogTests
time.Advance(TimeSpan.FromSeconds(6));
var after = await catalog.GetPathsAsync(null, default);
var after = await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken);
factory.Created.ShouldBe(2);
after.ShouldContain("Plant/Modbus/dev1/Pressure");
@@ -445,19 +445,19 @@ public sealed class ScriptTagCatalogTests
factory.Gate = gate;
var first = Task.Run(() => catalog.GetPathsAsync(null, default));
await factory.FirstRequest.Task.WaitAsync(TimeSpan.FromSeconds(10));
await factory.FirstRequest.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
var second = Task.Run(() => catalog.GetPathsAsync(null, default));
// Give the second caller ample time to reach the cache. It must be parked on the rebuild gate,
// NOT scanning: a second scan here is exactly the duplicated work this test falsifies.
for (var i = 0; i < 50 && factory.Created < 2; i++) await Task.Delay(10);
for (var i = 0; i < 50 && factory.Created < 2; i++) await Task.Delay(10, TestContext.Current.CancellationToken);
factory.Created.ShouldBe(1);
gate.SetResult();
var a = await first.WaitAsync(TimeSpan.FromSeconds(10));
var b = await second.WaitAsync(TimeSpan.FromSeconds(10));
var a = await first.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
var b = await second.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
factory.Created.ShouldBe(1);
a.ShouldContain("Plant/Modbus/dev1/Speed");
@@ -473,14 +473,14 @@ public sealed class ScriptTagCatalogTests
var cache = new ScriptTagCatalogSnapshotCache(time);
IReadOnlyList<ScriptTagInfo> good = [new ScriptTagInfo("A/B", "Tag", "Float", "DRV-1")];
(await cache.GetOrBuildAsync(_ => Task.FromResult(good), default)).ShouldBe(good);
(await cache.GetOrBuildAsync(_ => Task.FromResult(good), TestContext.Current.CancellationToken)).ShouldBe(good);
time.Advance(TimeSpan.FromSeconds(6));
await Should.ThrowAsync<InvalidOperationException>(
cache.GetOrBuildAsync(_ => Task.FromException<IReadOnlyList<ScriptTagInfo>>(new InvalidOperationException("boom")), default));
cache.GetOrBuildAsync(_ => Task.FromException<IReadOnlyList<ScriptTagInfo>>(new InvalidOperationException("boom")), TestContext.Current.CancellationToken));
// The gate was released and the old snapshot survived, so the next build still runs and succeeds.
(await cache.GetOrBuildAsync(_ => Task.FromResult(good), default)).ShouldBe(good);
(await cache.GetOrBuildAsync(_ => Task.FromResult(good), TestContext.Current.CancellationToken)).ShouldBe(good);
}
// -----------------------------------------------------------------------
@@ -514,7 +514,7 @@ public sealed class ScriptTagCatalogTests
Seed(opts);
SeedReferences(opts);
var names = await catalog.GetEquipmentReferenceNamesAsync("EQ-1", null, default);
var names = await catalog.GetEquipmentReferenceNamesAsync("EQ-1", null, TestContext.Current.CancellationToken);
names.ShouldContain("Speed"); // TAG-EQ Name, no override
names.ShouldContain("Path"); // TAG-SP under the "Path" override
@@ -529,7 +529,7 @@ public sealed class ScriptTagCatalogTests
Seed(opts);
SeedReferences(opts);
var names = await catalog.GetEquipmentReferenceNamesAsync("EQ-1", "sp", default);
var names = await catalog.GetEquipmentReferenceNamesAsync("EQ-1", "sp", TestContext.Current.CancellationToken);
names.ShouldContain("Speed");
names.ShouldNotContain("Path");
@@ -543,7 +543,7 @@ public sealed class ScriptTagCatalogTests
Seed(opts);
SeedReferences(opts);
(await catalog.GetEquipmentReferenceNamesAsync("EQ-UNKNOWN", null, default)).ShouldBeEmpty();
(await catalog.GetEquipmentReferenceNamesAsync("", null, default)).ShouldBeEmpty();
(await catalog.GetEquipmentReferenceNamesAsync("EQ-UNKNOWN", null, TestContext.Current.CancellationToken)).ShouldBeEmpty();
(await catalog.GetEquipmentReferenceNamesAsync("", null, TestContext.Current.CancellationToken)).ShouldBeEmpty();
}
}
@@ -31,7 +31,7 @@ public sealed class ScriptListQueriesTests
var name = await SeedAsync();
await using var db = NewDb(name);
var rows = await ScriptListQueries.LoadRowsAsync(db);
var rows = await ScriptListQueries.LoadRowsAsync(db, TestContext.Current.CancellationToken);
rows.Select(r => r.ScriptId).ShouldBe(["s-a", "s-b"]);
rows[0].ShouldBe(new ScriptListRow("s-a", "Alpha", "CSharp", "ha"));
@@ -45,8 +45,8 @@ public sealed class ScriptListQueriesTests
var name = await SeedAsync();
await using var db = NewDb(name);
(await ScriptListQueries.LoadSourceAsync(db, "s-b")).ShouldBe("return 2;");
(await ScriptListQueries.LoadSourceAsync(db, "missing")).ShouldBeNull();
(await ScriptListQueries.LoadSourceAsync(db, "s-b", TestContext.Current.CancellationToken)).ShouldBe("return 2;");
(await ScriptListQueries.LoadSourceAsync(db, "missing", TestContext.Current.CancellationToken)).ShouldBeNull();
}
[Fact]
@@ -1,6 +1,6 @@
using System.Collections.Concurrent;
using Akka.Actor;
using Akka.TestKit.Xunit2;
using Akka.TestKit.Xunit;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
@@ -39,7 +39,7 @@ public sealed class TelemetryDialDeadlineTests : TestKit
first.Ct.IsCancellationRequested.ShouldBeFalse("the deadline has not elapsed yet");
// The stalled dial is cut at the deadline...
AwaitCondition(() => first.Ct.IsCancellationRequested, Timeout, PollInterval);
AwaitCondition(() => first.Ct.IsCancellationRequested, Timeout, PollInterval, cancellationToken: TestContext.Current.CancellationToken);
// ...and the supervisor re-dials (a stalled dial is a transport failure, never a silent end).
var invocations = dial.WaitForCount(2, Timeout);
invocations[1].CorrelationId.ShouldBe("central-node-1-2");
@@ -53,10 +53,10 @@ public sealed class TelemetryDialDeadlineTests : TestKit
Spawn(dial, callTimeoutSeconds: 1, sinks);
var first = dial.WaitForCount(1, Timeout).Single();
AwaitCondition(() => sinks.HealthStore.Upserts.Count == 1, Timeout, PollInterval);
AwaitCondition(() => sinks.HealthStore.Upserts.Count == 1, Timeout, PollInterval, cancellationToken: TestContext.Current.CancellationToken);
// Positive control: well past the deadline, the established stream is untouched.
await Task.Delay(TimeSpan.FromSeconds(2.5));
await Task.Delay(TimeSpan.FromSeconds(2.5), TestContext.Current.CancellationToken);
first.Ct.IsCancellationRequested.ShouldBeFalse("an established stream must never be cut by the dial deadline");
dial.Invocations.Count.ShouldBe(1);
}
@@ -1,6 +1,6 @@
using System.Collections.Concurrent;
using Akka.Actor;
using Akka.TestKit.Xunit2;
using Akka.TestKit.Xunit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
@@ -70,14 +70,14 @@ public sealed class TelemetryDialSupervisorTests : TestKit
actor.Tell(new TelemetryDialSupervisor.RefreshNodes());
// node-1's dialer is cancelled and dropped.
AwaitCondition(() => node1.Ct.IsCancellationRequested, Timeout, PollInterval);
AwaitCondition(() => node1.Ct.IsCancellationRequested, Timeout, PollInterval, cancellationToken: TestContext.Current.CancellationToken);
// node-3 gets a fresh dialer.
var invocations = dial.WaitForCount(3, Timeout);
invocations.ShouldContain(i => i.Target.NodeId == "node-3" && i.CorrelationId == "central-node-3-1");
// node-2 is present in BOTH refreshes with an unchanged endpoint: it must NOT be restarted, or
// its live stream would be dropped on every refresh. Exactly one dial invocation for it.
ExpectNoMsg(TimeSpan.FromMilliseconds(200));
ExpectNoMsg(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
dial.Invocations.Count(i => i.Target.NodeId == "node-2").ShouldBe(1);
}
@@ -102,7 +102,7 @@ public sealed class TelemetryDialSupervisorTests : TestKit
actor.Tell(new TelemetryDialSupervisor.RefreshNodes());
// The old node-1 stream is torn down and a fresh dialer opens at the NEW endpoint (gen 2).
AwaitCondition(() => node1Original.Ct.IsCancellationRequested, Timeout, PollInterval);
AwaitCondition(() => node1Original.Ct.IsCancellationRequested, Timeout, PollInterval, cancellationToken: TestContext.Current.CancellationToken);
var invocations = dial.WaitForCount(3, Timeout);
invocations.ShouldContain(i =>
i.Target.NodeId == "node-1"
@@ -110,7 +110,7 @@ public sealed class TelemetryDialSupervisorTests : TestKit
&& i.CorrelationId == "central-node-1-2");
// node-2's endpoint did not change → still exactly one dial invocation (no churn).
ExpectNoMsg(TimeSpan.FromMilliseconds(200));
ExpectNoMsg(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
dial.Invocations.Count(i => i.Target.NodeId == "node-2").ShouldBe(1);
}
@@ -136,7 +136,7 @@ public sealed class TelemetryDialSupervisorTests : TestKit
sinks.Health.ShouldHaveSingleItem().DriverInstanceId.ShouldBe("d1");
sinks.Resilience.ShouldHaveSingleItem().DriverInstanceId.ShouldBe("d1");
},
Timeout);
Timeout, cancellationToken: TestContext.Current.CancellationToken);
}
[Fact]
@@ -173,7 +173,7 @@ public sealed class TelemetryDialSupervisorTests : TestKit
first.OnMapped(Alarm("stale"));
// Give the message a chance to be (wrongly) routed, then assert it was not.
ExpectNoMsg(TimeSpan.FromMilliseconds(300));
ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
sinks.Alarms.ShouldBeEmpty();
}
@@ -191,7 +191,7 @@ public sealed class TelemetryDialSupervisorTests : TestKit
// A duplicate/late failure from gen-1 must be ignored (no third dial).
first.OnError(new InvalidOperationException("late duplicate"));
ExpectNoMsg(TimeSpan.FromMilliseconds(400));
ExpectNoMsg(TimeSpan.FromMilliseconds(400), TestContext.Current.CancellationToken);
dial.Invocations.Count.ShouldBe(2);
}
@@ -215,23 +215,23 @@ public sealed class TelemetryDialSupervisorTests : TestKit
// First node connects → both broadcasters flip to connected.
node1.OnMapped(Alarm("a1"));
AwaitAssert(() => sinks.PillStates.ToArray().ShouldBe(new[] { true }), Timeout);
AwaitAssert(() => sinks.PillStates.ToArray().ShouldBe(new[] { true }), Timeout, cancellationToken: TestContext.Current.CancellationToken);
// Second node connects → still connected, no extra transition.
node2.OnMapped(Alarm("a2"));
ExpectNoMsg(TimeSpan.FromMilliseconds(200));
ExpectNoMsg(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
sinks.PillStates.ToArray().ShouldBe(new[] { true });
// node-1 drops but node-2 is still up → pill stays true.
node1.OnError(new InvalidOperationException("down"));
node1.Complete();
ExpectNoMsg(TimeSpan.FromMilliseconds(200));
ExpectNoMsg(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
sinks.PillStates.ToArray().ShouldBe(new[] { true });
// node-2 drops too → all down → pill flips false.
node2.OnError(new InvalidOperationException("down"));
node2.Complete();
AwaitAssert(() => sinks.PillStates.ToArray().ShouldBe(new[] { true, false }), Timeout);
AwaitAssert(() => sinks.PillStates.ToArray().ShouldBe(new[] { true, false }), Timeout, cancellationToken: TestContext.Current.CancellationToken);
}
[Fact]
@@ -250,7 +250,7 @@ public sealed class TelemetryDialSupervisorTests : TestKit
Sys.Stop(actor);
AwaitCondition(() => inv.All(i => i.Ct.IsCancellationRequested), Timeout, PollInterval);
AwaitCondition(() => inv.All(i => i.Ct.IsCancellationRequested), Timeout, PollInterval, cancellationToken: TestContext.Current.CancellationToken);
}
[Fact]
@@ -264,7 +264,7 @@ public sealed class TelemetryDialSupervisorTests : TestKit
Node("null-port", "h2", grpcPort: null, enabled: true, maintenance: false),
Node("maintenance", "h3", grpcPort: 5300, enabled: true, maintenance: true),
Node("disabled", "h4", grpcPort: 5300, enabled: false, maintenance: false));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var source = TelemetryNodeSource.Create(factory, NullLogger.Instance);
@@ -96,7 +96,7 @@ public sealed class EffectiveNameGuardTests
var guard = Guard(SeedFactory());
var result = await guard.CheckAsync(
EquipmentId, "brand-new-name", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null);
EquipmentId, "brand-new-name", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null, TestContext.Current.CancellationToken);
result.ShouldBeNull();
}
@@ -108,7 +108,7 @@ public sealed class EffectiveNameGuardTests
// A new VirtualTag proposing "speed" collides with reference REF-1's effective name.
var result = await guard.CheckAsync(
EquipmentId, "speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null);
EquipmentId, "speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null, TestContext.Current.CancellationToken);
result.ShouldNotBeNull();
result.ShouldBe($"effective name 'speed' already used by reference '{RefNoOverrideId}' in equipment '{EquipmentId}'");
@@ -121,7 +121,7 @@ public sealed class EffectiveNameGuardTests
// A new reference proposing "temperature" collides with REF-2's OVERRIDE effective name.
var result = await guard.CheckAsync(
EquipmentId, "temperature", EffectiveNameSourceKind.Reference, excludeSourceId: null);
EquipmentId, "temperature", EffectiveNameSourceKind.Reference, excludeSourceId: null, TestContext.Current.CancellationToken);
result.ShouldNotBeNull();
result.ShouldBe($"effective name 'temperature' already used by reference '{RefWithOverrideId}' in equipment '{EquipmentId}'");
@@ -133,7 +133,7 @@ public sealed class EffectiveNameGuardTests
var guard = Guard(SeedFactory());
var result = await guard.CheckAsync(
EquipmentId, "highpress", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null);
EquipmentId, "highpress", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null, TestContext.Current.CancellationToken);
result.ShouldNotBeNull();
result.ShouldBe($"effective name 'highpress' already used by ScriptedAlarm '{ScriptedAlarmId}' in equipment '{EquipmentId}'");
@@ -147,7 +147,7 @@ public sealed class EffectiveNameGuardTests
// Editing REF-2 to change its override to "temperature" (its own current effective name)
// must NOT collide with itself.
var result = await guard.CheckAsync(
EquipmentId, "temperature", EffectiveNameSourceKind.Reference, excludeSourceId: RefWithOverrideId);
EquipmentId, "temperature", EffectiveNameSourceKind.Reference, excludeSourceId: RefWithOverrideId, TestContext.Current.CancellationToken);
result.ShouldBeNull();
}
@@ -160,7 +160,7 @@ public sealed class EffectiveNameGuardTests
// Excluding a VirtualTag whose id happens to equal a reference id must NOT excuse the
// reference collision — the self-row is identified by (kind, id) together.
var result = await guard.CheckAsync(
EquipmentId, "speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: RefNoOverrideId);
EquipmentId, "speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: RefNoOverrideId, TestContext.Current.CancellationToken);
result.ShouldNotBeNull();
result.ShouldBe($"effective name 'speed' already used by reference '{RefNoOverrideId}' in equipment '{EquipmentId}'");
@@ -173,7 +173,7 @@ public sealed class EffectiveNameGuardTests
// "Speed" (capital S) must NOT collide with the existing "speed" (ordinal comparison).
var result = await guard.CheckAsync(
EquipmentId, "Speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null);
EquipmentId, "Speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null, TestContext.Current.CancellationToken);
result.ShouldBeNull();
}
@@ -185,7 +185,7 @@ public sealed class EffectiveNameGuardTests
// Same name, different equipment → no collision (per-equipment NodeId space).
var result = await guard.CheckAsync(
"EQ-000000000002", "speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null);
"EQ-000000000002", "speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null, TestContext.Current.CancellationToken);
result.ShouldBeNull();
}
@@ -210,10 +210,10 @@ public sealed class EffectiveNameGuardTests
var guard = Guard(factory);
var expected = await guard.CheckAsync(
EquipmentId, expectedCollidingName, EffectiveNameSourceKind.Reference, excludeSourceId: null);
EquipmentId, expectedCollidingName, EffectiveNameSourceKind.Reference, excludeSourceId: null, TestContext.Current.CancellationToken);
expected.ShouldNotBeNull();
var actual = await guard.CheckManyAsync(EquipmentId, names, EffectiveNameSourceKind.Reference);
var actual = await guard.CheckManyAsync(EquipmentId, names, EffectiveNameSourceKind.Reference, TestContext.Current.CancellationToken);
actual.ShouldBe(expected);
}
@@ -227,7 +227,7 @@ public sealed class EffectiveNameGuardTests
var result = await guard.CheckManyAsync(
EquipmentId,
["free-a", "free-b", "Speed" /* ordinal: capital S does not collide */, "free-c"],
EffectiveNameSourceKind.Reference);
EffectiveNameSourceKind.Reference, TestContext.Current.CancellationToken);
result.ShouldBeNull();
}
@@ -238,7 +238,7 @@ public sealed class EffectiveNameGuardTests
{
var guard = Guard(SeedFactory());
var result = await guard.CheckManyAsync(EquipmentId, [], EffectiveNameSourceKind.Reference);
var result = await guard.CheckManyAsync(EquipmentId, [], EffectiveNameSourceKind.Reference, TestContext.Current.CancellationToken);
result.ShouldBeNull();
}
@@ -238,7 +238,7 @@ public sealed class RawTagConfigGateTests
new RawTagImportRow(null, Tag("good2", GoodS7)),
};
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows);
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows, TestContext.Current.CancellationToken);
result.Inserted.ShouldBe(0);
result.Errors.ShouldContain(e => e.Contains("Row 2") && e.Contains("TagConfig"));
@@ -256,7 +256,7 @@ public sealed class RawTagConfigGateTests
var result = await service.ImportTagsAsync(
RawTreeTestDb.RootDeviceId,
new[] { new RawTagImportRow(null, Tag("ok1", GoodS7)), new RawTagImportRow(null, Tag("ok2", GoodS7)) });
new[] { new RawTagImportRow(null, Tag("ok1", GoodS7)), new RawTagImportRow(null, Tag("ok2", GoodS7)) }, TestContext.Current.CancellationToken);
result.Errors.ShouldBeEmpty();
result.Inserted.ShouldBe(2);
@@ -273,7 +273,7 @@ public sealed class RawTagConfigGateTests
var (service, _) = Seeded();
var bad = await service.CreateTagAsync(
RawTreeTestDb.RootDeviceId, null, Tag("newTag", """{"address":"DB1.DBW0"}"""));
RawTreeTestDb.RootDeviceId, null, Tag("newTag", """{"address":"DB1.DBW0"}"""), TestContext.Current.CancellationToken);
bad.Ok.ShouldBeFalse();
bad.Error.ShouldNotBeNull();
@@ -285,7 +285,7 @@ public sealed class RawTagConfigGateTests
{
var (service, _) = Seeded();
(await service.CreateTagAsync(RawTreeTestDb.RootDeviceId, null, Tag("newTag", GoodS7)))
(await service.CreateTagAsync(RawTreeTestDb.RootDeviceId, null, Tag("newTag", GoodS7), TestContext.Current.CancellationToken))
.Ok.ShouldBeTrue();
}
@@ -300,7 +300,7 @@ public sealed class RawTagConfigGateTests
}
var bad = await service.UpdateTagAsync(
RawTreeTestDb.ReferencedTagId, Tag("temperature", """{"address":"DB1.DBW0"}"""), rowVersion);
RawTreeTestDb.ReferencedTagId, Tag("temperature", """{"address":"DB1.DBW0"}"""), rowVersion, TestContext.Current.CancellationToken);
bad.Ok.ShouldBeFalse();
bad.Error.ShouldNotBeNull();
@@ -75,7 +75,7 @@ public sealed class RawTreeServiceRenameWarningTests
var (service, dbName) = Seeded();
var rv = DeviceRowVersion(dbName, DevAllId);
var result = await service.RenameDeviceAsync(DevAllId, "Dev-renamed", rv);
var result = await service.RenameDeviceAsync(DevAllId, "Dev-renamed", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Warnings.ShouldContain(w => w.Contains("historian")); // (a)
@@ -90,7 +90,7 @@ public sealed class RawTreeServiceRenameWarningTests
var (service, dbName) = Seeded();
var rv = DeviceRowVersion(dbName, DevPinnedId);
var result = await service.RenameDeviceAsync(DevPinnedId, "Dev-pinned2", rv);
var result = await service.RenameDeviceAsync(DevPinnedId, "Dev-pinned2", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
// A historianTagname override pins the tagname, so its history does NOT fork — no (a) warning; and
@@ -105,7 +105,7 @@ public sealed class RawTreeServiceRenameWarningTests
var (service, dbName) = Seeded();
var rv = DeviceRowVersion(dbName, DevPlainId);
var result = await service.RenameDeviceAsync(DevPlainId, "Dev-plain2", rv);
var result = await service.RenameDeviceAsync(DevPlainId, "Dev-plain2", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Warnings.ShouldBeEmpty();
@@ -119,7 +119,7 @@ public sealed class RawTreeServiceRenameWarningTests
// Renaming the DRIVER (an ancestor of the scripted tag) still computes the tag's unchanged OLD
// RawPath (rename not yet saved) and finds it as a substring of the script body.
var result = await service.RenameDriverAsync(DriverId, "modbus-renamed", rv);
var result = await service.RenameDriverAsync(DriverId, "modbus-renamed", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Warnings.ShouldContain(w => w.Contains("script") && w.Contains(ScriptName));
@@ -146,7 +146,7 @@ public sealed class RawTreeServiceRenameWarningTests
});
var rv = DeviceRowVersion(dbName, DevAllId);
var result = await service.RenameDeviceAsync(DevAllId, "Dev-renamed", rv);
var result = await service.RenameDeviceAsync(DevAllId, "Dev-renamed", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
var scriptWarnings = result.Warnings.Where(w => w.Contains("script")).ToList();
@@ -194,7 +194,7 @@ public sealed class RawTreeServiceRenameWarningTests
});
var rv = DeviceRowVersion(dbName, "DEV-bulk");
var result = await service.RenameDeviceAsync("DEV-bulk", "Dev-bulk2", rv);
var result = await service.RenameDeviceAsync("DEV-bulk", "Dev-bulk2", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
// Ordinal sort: 'S-first' < 'S-last' < 'S-middle'.
@@ -47,11 +47,11 @@ public sealed class RawTreeServiceSecretRefRoundTripTests
"{\"Password\":\"secret:opcua/x/pw\",\"UserCertificatePassword\":\"secret:opcua/x/certpw\"}";
var result = await service.UpdateDriverAsync(
RawTreeTestDb.FolderedDriverId, "modbus-1", config, null, rv);
RawTreeTestDb.FolderedDriverId, "modbus-1", config, null, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
// Reload through the same seam the modal uses.
var reloaded = await service.LoadDriverForEditAsync(RawTreeTestDb.FolderedDriverId);
var reloaded = await service.LoadDriverForEditAsync(RawTreeTestDb.FolderedDriverId, TestContext.Current.CancellationToken);
reloaded.ShouldNotBeNull();
// Byte-identical: nothing on the save path rewrote the string.
@@ -74,11 +74,11 @@ public sealed class RawTreeServiceSecretRefRoundTripTests
const string config = "{\"Gateway\":{\"ApiKeySecretRef\":\"secret:galaxy/x/apikey\"}}";
var created = await service.CreateDriverAsync(
RawTreeTestDb.ClusterId, RawTreeTestDb.RootFolderId, "galaxy-1", "Galaxy", config);
RawTreeTestDb.ClusterId, RawTreeTestDb.RootFolderId, "galaxy-1", "Galaxy", config, TestContext.Current.CancellationToken);
created.Ok.ShouldBeTrue();
created.CreatedId.ShouldNotBeNull();
var reloaded = await service.LoadDriverForEditAsync(created.CreatedId!);
var reloaded = await service.LoadDriverForEditAsync(created.CreatedId!, TestContext.Current.CancellationToken);
reloaded.ShouldNotBeNull();
reloaded!.DriverConfig.ShouldBe(config);
@@ -99,10 +99,10 @@ public sealed class RawTreeServiceSecretRefRoundTripTests
"{\"Password\":\"env:OPCUA_PW\",\"UserCertificatePassword\":\"file:/run/secrets/certpw\"}";
var result = await service.UpdateDriverAsync(
RawTreeTestDb.FolderedDriverId, "modbus-1", config, null, rv);
RawTreeTestDb.FolderedDriverId, "modbus-1", config, null, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
var reloaded = await service.LoadDriverForEditAsync(RawTreeTestDb.FolderedDriverId);
var reloaded = await service.LoadDriverForEditAsync(RawTreeTestDb.FolderedDriverId, TestContext.Current.CancellationToken);
reloaded!.DriverConfig.ShouldBe(config);
reloaded.DriverConfig.ShouldContain("env:OPCUA_PW");
reloaded.DriverConfig.ShouldContain("file:/run/secrets/certpw");
@@ -39,7 +39,7 @@ public sealed class RawTreeServiceTests
{
var (service, _) = Seeded();
var roots = await service.LoadRootsAsync();
var roots = await service.LoadRootsAsync(TestContext.Current.CancellationToken);
var ent = roots.ShouldHaveSingleItem();
ent.Kind.ShouldBe(RawNodeKind.Enterprise);
@@ -57,9 +57,9 @@ public sealed class RawTreeServiceTests
public async Task LoadChildren_of_cluster_yields_root_folders_and_root_drivers()
{
var (service, _) = Seeded();
var cluster = (await service.LoadRootsAsync()).Single().Children.Single();
var cluster = (await service.LoadRootsAsync(TestContext.Current.CancellationToken)).Single().Children.Single();
var children = await service.LoadChildrenAsync(cluster);
var children = await service.LoadChildrenAsync(cluster, TestContext.Current.CancellationToken);
var folder = children.Single(n => n.Kind == RawNodeKind.Folder);
folder.DisplayName.ShouldBe("PLCs");
@@ -77,29 +77,29 @@ public sealed class RawTreeServiceTests
public async Task LoadChildren_propagates_driver_type_down_to_device_group_and_tag()
{
var (service, _) = Seeded();
var cluster = (await service.LoadRootsAsync()).Single().Children.Single();
var folder = (await service.LoadChildrenAsync(cluster)).Single(n => n.Kind == RawNodeKind.Folder);
var cluster = (await service.LoadRootsAsync(TestContext.Current.CancellationToken)).Single().Children.Single();
var folder = (await service.LoadChildrenAsync(cluster, TestContext.Current.CancellationToken)).Single(n => n.Kind == RawNodeKind.Folder);
var driver = (await service.LoadChildrenAsync(folder)).ShouldHaveSingleItem();
var driver = (await service.LoadChildrenAsync(folder, TestContext.Current.CancellationToken)).ShouldHaveSingleItem();
driver.Kind.ShouldBe(RawNodeKind.Driver);
driver.DriverType.ShouldBe("Modbus");
var device = (await service.LoadChildrenAsync(driver)).ShouldHaveSingleItem();
var device = (await service.LoadChildrenAsync(driver, TestContext.Current.CancellationToken)).ShouldHaveSingleItem();
device.Kind.ShouldBe(RawNodeKind.Device);
device.DriverType.ShouldBe("Modbus"); // propagated
device.ChildCount.ShouldBe(1); // one root group (tags live under the group)
var group = (await service.LoadChildrenAsync(device)).ShouldHaveSingleItem();
var group = (await service.LoadChildrenAsync(device, TestContext.Current.CancellationToken)).ShouldHaveSingleItem();
group.Kind.ShouldBe(RawNodeKind.TagGroup);
group.DriverType.ShouldBe("Modbus"); // propagated
group.ChildCount.ShouldBe(2); // two tags
var tags = await service.LoadChildrenAsync(group);
var tags = await service.LoadChildrenAsync(group, TestContext.Current.CancellationToken);
tags.Count.ShouldBe(2);
tags.ShouldAllBe(t => t.Kind == RawNodeKind.Tag);
tags.ShouldAllBe(t => !t.HasLazyChildren);
tags.ShouldAllBe(t => t.DriverType == "Modbus"); // propagated
(await service.LoadChildrenAsync(tags[0])).ShouldBeEmpty(); // leaf
(await service.LoadChildrenAsync(tags[0], TestContext.Current.CancellationToken)).ShouldBeEmpty(); // leaf
}
/// <summary>
@@ -112,24 +112,24 @@ public sealed class RawTreeServiceTests
public async Task LoadChildren_yields_tag_rows_carrying_id_name_and_row_version()
{
var (service, dbName) = Seeded();
var cluster = (await service.LoadRootsAsync()).Single().Children.Single();
var cluster = (await service.LoadRootsAsync(TestContext.Current.CancellationToken)).Single().Children.Single();
// Group-child loader: two tags under "Motors".
var folder = (await service.LoadChildrenAsync(cluster)).Single(n => n.Kind == RawNodeKind.Folder);
var driver = (await service.LoadChildrenAsync(folder)).ShouldHaveSingleItem();
var device = (await service.LoadChildrenAsync(driver)).ShouldHaveSingleItem();
var group = (await service.LoadChildrenAsync(device)).ShouldHaveSingleItem();
var folder = (await service.LoadChildrenAsync(cluster, TestContext.Current.CancellationToken)).Single(n => n.Kind == RawNodeKind.Folder);
var driver = (await service.LoadChildrenAsync(folder, TestContext.Current.CancellationToken)).ShouldHaveSingleItem();
var device = (await service.LoadChildrenAsync(driver, TestContext.Current.CancellationToken)).ShouldHaveSingleItem();
var group = (await service.LoadChildrenAsync(device, TestContext.Current.CancellationToken)).ShouldHaveSingleItem();
var plain = (await service.LoadChildrenAsync(group)).Single(t => t.EntityId == RawTreeTestDb.PlainTagId);
var plain = (await service.LoadChildrenAsync(group, TestContext.Current.CancellationToken)).Single(t => t.EntityId == RawTreeTestDb.PlainTagId);
plain.DisplayName.ShouldBe("state");
plain.RowVersion.ShouldBe(
RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.PlainTagId).RowVersion));
// Device-child loader: the cluster-root device's own tag.
var rootDriver = (await service.LoadChildrenAsync(cluster)).Single(n => n.Kind == RawNodeKind.Driver);
var rootDevice = (await service.LoadChildrenAsync(rootDriver)).ShouldHaveSingleItem();
var rootDriver = (await service.LoadChildrenAsync(cluster, TestContext.Current.CancellationToken)).Single(n => n.Kind == RawNodeKind.Driver);
var rootDevice = (await service.LoadChildrenAsync(rootDriver, TestContext.Current.CancellationToken)).ShouldHaveSingleItem();
var referenced = (await service.LoadChildrenAsync(rootDevice))
var referenced = (await service.LoadChildrenAsync(rootDevice, TestContext.Current.CancellationToken))
.Single(t => t.EntityId == RawTreeTestDb.ReferencedTagId);
referenced.Kind.ShouldBe(RawNodeKind.Tag);
referenced.DisplayName.ShouldBe("temperature");
@@ -144,11 +144,11 @@ public sealed class RawTreeServiceTests
{
var (service, dbName) = Seeded();
var folder = await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "Cell-B");
var folder = await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "Cell-B", TestContext.Current.CancellationToken);
folder.Ok.ShouldBeTrue();
folder.CreatedId.ShouldNotBeNull();
var driver = await service.CreateDriverAsync(RawTreeTestDb.ClusterId, folder.CreatedId, "abcip-1", "AbCip", "{}");
var driver = await service.CreateDriverAsync(RawTreeTestDb.ClusterId, folder.CreatedId, "abcip-1", "AbCip", "{}", TestContext.Current.CancellationToken);
driver.Ok.ShouldBeTrue();
using var db = RawTreeTestDb.CreateNamed(dbName);
@@ -160,7 +160,7 @@ public sealed class RawTreeServiceTests
{
var (service, _) = Seeded();
var dup = await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "PLCs"); // already a root folder
var dup = await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "PLCs", TestContext.Current.CancellationToken); // already a root folder
dup.Ok.ShouldBeFalse();
dup.Error!.ShouldContain("already exists");
}
@@ -170,7 +170,7 @@ public sealed class RawTreeServiceTests
{
var (service, _) = Seeded();
var bad = await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "a/b");
var bad = await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "a/b", TestContext.Current.CancellationToken);
bad.Ok.ShouldBeFalse();
bad.Error!.ShouldContain("/");
}
@@ -180,7 +180,7 @@ public sealed class RawTreeServiceTests
{
var (service, _) = Seeded();
var dup = await service.CreateTagGroupAsync(RawTreeTestDb.FolderedDeviceId, null, "Motors");
var dup = await service.CreateTagGroupAsync(RawTreeTestDb.FolderedDeviceId, null, "Motors", TestContext.Current.CancellationToken);
dup.Ok.ShouldBeFalse();
dup.Error!.ShouldContain("already exists");
}
@@ -193,7 +193,7 @@ public sealed class RawTreeServiceTests
var (service, dbName) = Seeded();
var rv = RowVersionOf(dbName, db => db.RawFolders.Single(f => f.RawFolderId == RawTreeTestDb.RootFolderId).RowVersion);
var result = await service.RenameFolderAsync(RawTreeTestDb.RootFolderId, "PLC", rv);
var result = await service.RenameFolderAsync(RawTreeTestDb.RootFolderId, "PLC", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Warnings.ShouldContain(w => w.Contains("historized"));
@@ -205,7 +205,7 @@ public sealed class RawTreeServiceTests
var (service, dbName) = Seeded();
var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.RootDriverId).RowVersion);
var result = await service.RenameDriverAsync(RawTreeTestDb.RootDriverId, "s7-main", rv);
var result = await service.RenameDriverAsync(RawTreeTestDb.RootDriverId, "s7-main", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Warnings.ShouldContain(w => w.Contains(RawTreeTestDb.EquipmentName));
@@ -215,10 +215,10 @@ public sealed class RawTreeServiceTests
public async Task RenameFolder_rejects_sibling_collision()
{
var (service, dbName) = Seeded();
await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "Spare");
await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "Spare", TestContext.Current.CancellationToken);
var rv = RowVersionOf(dbName, db => db.RawFolders.Single(f => f.RawFolderId == RawTreeTestDb.RootFolderId).RowVersion);
var result = await service.RenameFolderAsync(RawTreeTestDb.RootFolderId, "Spare", rv);
var result = await service.RenameFolderAsync(RawTreeTestDb.RootFolderId, "Spare", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error!.ShouldContain("already exists");
@@ -232,7 +232,7 @@ public sealed class RawTreeServiceTests
var (service, dbName) = Seeded();
var rv = RowVersionOf(dbName, db => db.RawFolders.Single(f => f.RawFolderId == RawTreeTestDb.RootFolderId).RowVersion);
var result = await service.DeleteFolderAsync(RawTreeTestDb.RootFolderId, rv);
var result = await service.DeleteFolderAsync(RawTreeTestDb.RootFolderId, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error!.ShouldContain("driver");
@@ -245,7 +245,7 @@ public sealed class RawTreeServiceTests
var (service, dbName) = Seeded();
var rv = RowVersionOf(dbName, db => db.Devices.Single(d => d.DeviceId == RawTreeTestDb.RootDeviceId).RowVersion);
var result = await service.DeleteDeviceAsync(RawTreeTestDb.RootDeviceId, rv);
var result = await service.DeleteDeviceAsync(RawTreeTestDb.RootDeviceId, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error!.ShouldContain("tags");
@@ -257,7 +257,7 @@ public sealed class RawTreeServiceTests
var (service, dbName) = Seeded();
var rv = RowVersionOf(dbName, db => db.TagGroups.Single(g => g.TagGroupId == RawTreeTestDb.RootGroupId).RowVersion);
var result = await service.DeleteTagGroupAsync(RawTreeTestDb.RootGroupId, rv);
var result = await service.DeleteTagGroupAsync(RawTreeTestDb.RootGroupId, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error!.ShouldContain("tags");
@@ -269,7 +269,7 @@ public sealed class RawTreeServiceTests
var (service, dbName) = Seeded();
var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.RootDriverId).RowVersion);
var result = await service.DeleteDriverAsync(RawTreeTestDb.RootDriverId, rv);
var result = await service.DeleteDriverAsync(RawTreeTestDb.RootDriverId, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error!.ShouldContain("Device1"); // the offending device is named
@@ -281,7 +281,7 @@ public sealed class RawTreeServiceTests
var (service, dbName) = Seeded();
var rv = RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.ReferencedTagId).RowVersion);
var result = await service.DeleteTagAsync(RawTreeTestDb.ReferencedTagId, rv);
var result = await service.DeleteTagAsync(RawTreeTestDb.ReferencedTagId, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error!.ShouldContain(RawTreeTestDb.EquipmentName);
@@ -296,7 +296,7 @@ public sealed class RawTreeServiceTests
var (service, dbName) = Seeded();
var rv = RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.PlainTagId).RowVersion);
var result = await service.DeleteTagAsync(RawTreeTestDb.PlainTagId, rv);
var result = await service.DeleteTagAsync(RawTreeTestDb.PlainTagId, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
using var db = RawTreeTestDb.CreateNamed(dbName);
@@ -307,10 +307,10 @@ public sealed class RawTreeServiceTests
public async Task DeleteFolder_succeeds_when_empty()
{
var (service, dbName) = Seeded();
var created = await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "Empty");
var created = await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "Empty", TestContext.Current.CancellationToken);
var rv = RowVersionOf(dbName, db => db.RawFolders.Single(f => f.RawFolderId == created.CreatedId!).RowVersion);
var result = await service.DeleteFolderAsync(created.CreatedId!, rv);
var result = await service.DeleteFolderAsync(created.CreatedId!, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
}
@@ -323,7 +323,7 @@ public sealed class RawTreeServiceTests
var (service, dbName) = Seeded();
var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.RootDriverId).RowVersion);
var result = await service.MoveDriverToFolderAsync(RawTreeTestDb.RootDriverId, RawTreeTestDb.RootFolderId, rv);
var result = await service.MoveDriverToFolderAsync(RawTreeTestDb.RootDriverId, RawTreeTestDb.RootFolderId, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
using var db = RawTreeTestDb.CreateNamed(dbName);
@@ -337,7 +337,7 @@ public sealed class RawTreeServiceTests
var (service, dbName) = Seeded();
var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId).RowVersion);
var result = await service.MoveDriverToFolderAsync(RawTreeTestDb.FolderedDriverId, null, rv);
var result = await service.MoveDriverToFolderAsync(RawTreeTestDb.FolderedDriverId, null, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
using var db = RawTreeTestDb.CreateNamed(dbName);
@@ -360,7 +360,7 @@ public sealed class RawTreeServiceTests
db.SaveChanges();
}
var result = await service.RenameFolderAsync(RawTreeTestDb.RootFolderId, "Whatever", rv);
var result = await service.RenameFolderAsync(RawTreeTestDb.RootFolderId, "Whatever", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -372,7 +372,7 @@ public sealed class RawTreeServiceTests
{
var (service, _) = Seeded();
var dto = await service.LoadTagForEditAsync(RawTreeTestDb.HistorizedTagId);
var dto = await service.LoadTagForEditAsync(RawTreeTestDb.HistorizedTagId, TestContext.Current.CancellationToken);
dto.ShouldNotBeNull();
dto.Value.Name.ShouldBe("speed");
@@ -62,7 +62,7 @@ public sealed class RawTreeServiceWaveBTests
var (service, dbName) = Seeded();
var result = await service.CreateTagAsync(
RawTreeTestDb.FolderedDeviceId, RawTreeTestDb.RootGroupId, Tag("torque"));
RawTreeTestDb.FolderedDeviceId, RawTreeTestDb.RootGroupId, Tag("torque"), TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.CreatedId.ShouldNotBeNull();
@@ -81,7 +81,7 @@ public sealed class RawTreeServiceWaveBTests
var result = await service.CreateTagAsync(
RawTreeTestDb.FolderedDeviceId, null,
new RawTagInput("flow", "Float", TagAccessLevel.Read, false, " ", ModbusConfig));
new RawTagInput("flow", "Float", TagAccessLevel.Read, false, " ", ModbusConfig), TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
using var db = RawTreeTestDb.CreateNamed(dbName);
@@ -94,7 +94,7 @@ public sealed class RawTreeServiceWaveBTests
var (service, _) = Seeded();
// "speed" already exists under RootGroup.
var dup = await service.CreateTagAsync(RawTreeTestDb.FolderedDeviceId, RawTreeTestDb.RootGroupId, Tag("speed"));
var dup = await service.CreateTagAsync(RawTreeTestDb.FolderedDeviceId, RawTreeTestDb.RootGroupId, Tag("speed"), TestContext.Current.CancellationToken);
dup.Ok.ShouldBeFalse();
dup.Error!.ShouldContain("already exists");
@@ -106,7 +106,7 @@ public sealed class RawTreeServiceWaveBTests
var (service, _) = Seeded();
var bad = await service.CreateTagAsync(
RawTreeTestDb.FolderedDeviceId, null, Tag("pressure", tagConfig: "{not json"));
RawTreeTestDb.FolderedDeviceId, null, Tag("pressure", tagConfig: "{not json"), TestContext.Current.CancellationToken);
bad.Ok.ShouldBeFalse();
bad.Error!.ShouldContain("JSON");
@@ -117,7 +117,7 @@ public sealed class RawTreeServiceWaveBTests
{
var (service, _) = Seeded();
var bad = await service.CreateTagAsync(RawTreeTestDb.FolderedDeviceId, null, Tag("a/b"));
var bad = await service.CreateTagAsync(RawTreeTestDb.FolderedDeviceId, null, Tag("a/b"), TestContext.Current.CancellationToken);
bad.Ok.ShouldBeFalse();
bad.Error!.ShouldContain("/");
@@ -139,7 +139,7 @@ public sealed class RawTreeServiceWaveBTests
new RawTagInput(
"running", "Boolean", TagAccessLevel.ReadWrite, true, "pg1",
"""{"region":"HoldingRegisters","address":1,"scale":2}"""),
rv);
rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
using var db = RawTreeTestDb.CreateNamed(dbName);
@@ -161,7 +161,7 @@ public sealed class RawTreeServiceWaveBTests
// Rename "state" (PlainTag) to "speed" — a sibling under the same group ⇒ collision.
var rv = RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.PlainTagId).RowVersion);
var result = await service.UpdateTagAsync(RawTreeTestDb.PlainTagId, Tag("speed"), rv);
var result = await service.UpdateTagAsync(RawTreeTestDb.PlainTagId, Tag("speed"), rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error!.ShouldContain("already exists");
@@ -174,7 +174,7 @@ public sealed class RawTreeServiceWaveBTests
var rv = RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.PlainTagId).RowVersion);
// Keeping its own name "state" must not trip the excl-self uniqueness check.
var result = await service.UpdateTagAsync(RawTreeTestDb.PlainTagId, Tag("state", "Boolean"), rv);
var result = await service.UpdateTagAsync(RawTreeTestDb.PlainTagId, Tag("state", "Boolean"), rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
}
@@ -191,7 +191,7 @@ public sealed class RawTreeServiceWaveBTests
db.SaveChanges();
}
var result = await service.UpdateTagAsync(RawTreeTestDb.PlainTagId, Tag("whatever", "Boolean"), rv);
var result = await service.UpdateTagAsync(RawTreeTestDb.PlainTagId, Tag("whatever", "Boolean"), rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -211,7 +211,7 @@ public sealed class RawTreeServiceWaveBTests
new RawTagImportRow("Line/B", S7Tag("t3")), // reuses Line, new B
};
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows);
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows, TestContext.Current.CancellationToken);
result.Errors.ShouldBeEmpty();
result.Inserted.ShouldBe(3);
@@ -232,7 +232,7 @@ public sealed class RawTreeServiceWaveBTests
var (service, dbName) = Seeded();
var result = await service.ImportTagsAsync(
RawTreeTestDb.RootDeviceId, new[] { new RawTagImportRow(null, S7Tag("rootTag")) });
RawTreeTestDb.RootDeviceId, new[] { new RawTagImportRow(null, S7Tag("rootTag")) }, TestContext.Current.CancellationToken);
result.Inserted.ShouldBe(1);
using var db = RawTreeTestDb.CreateNamed(dbName);
@@ -251,7 +251,7 @@ public sealed class RawTreeServiceWaveBTests
new RawTagImportRow("Grp", S7Tag("good2")),
};
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows);
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows, TestContext.Current.CancellationToken);
result.Inserted.ShouldBe(0);
result.Errors.ShouldNotBeEmpty();
@@ -274,7 +274,7 @@ public sealed class RawTreeServiceWaveBTests
new RawTagImportRow("Grp", S7Tag("dup")), // same path + name within the batch
};
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows);
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows, TestContext.Current.CancellationToken);
result.Inserted.ShouldBe(0);
result.Errors.ShouldContain(e => e.Contains("Row 2") && e.Contains("already exists"));
@@ -287,7 +287,7 @@ public sealed class RawTreeServiceWaveBTests
// "temperature" already sits at the device root of RootDevice.
var result = await service.ImportTagsAsync(
RawTreeTestDb.RootDeviceId, new[] { new RawTagImportRow(null, S7Tag("temperature")) });
RawTreeTestDb.RootDeviceId, new[] { new RawTagImportRow(null, S7Tag("temperature")) }, TestContext.Current.CancellationToken);
result.Inserted.ShouldBe(0);
result.Errors.ShouldContain(e => e.Contains("already exists"));
@@ -300,7 +300,7 @@ public sealed class RawTreeServiceWaveBTests
{
var (service, _) = Seeded();
var dto = await service.LoadDriverForEditAsync(RawTreeTestDb.FolderedDriverId);
var dto = await service.LoadDriverForEditAsync(RawTreeTestDb.FolderedDriverId, TestContext.Current.CancellationToken);
dto.ShouldNotBeNull();
dto!.Name.ShouldBe("modbus-1");
@@ -312,7 +312,7 @@ public sealed class RawTreeServiceWaveBTests
public async Task LoadDriverForEdit_returns_null_when_missing()
{
var (service, _) = Seeded();
(await service.LoadDriverForEditAsync("DRV-nope")).ShouldBeNull();
(await service.LoadDriverForEditAsync("DRV-nope", TestContext.Current.CancellationToken)).ShouldBeNull();
}
[Fact]
@@ -322,7 +322,7 @@ public sealed class RawTreeServiceWaveBTests
var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId).RowVersion);
var result = await service.UpdateDriverAsync(
RawTreeTestDb.FolderedDriverId, "modbus-main", "{\"Host\":\"10.0.0.1\"}", "{\"x\":1}", rv);
RawTreeTestDb.FolderedDriverId, "modbus-main", "{\"Host\":\"10.0.0.1\"}", "{\"x\":1}", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
using var db = RawTreeTestDb.CreateNamed(dbName);
@@ -338,10 +338,10 @@ public sealed class RawTreeServiceWaveBTests
{
var (service, dbName) = Seeded();
// Two cluster-root vs foldered drivers differ in scope; create a sibling in the same folder to collide.
await service.CreateDriverAsync(RawTreeTestDb.ClusterId, RawTreeTestDb.RootFolderId, "modbus-2", "Modbus", "{}");
await service.CreateDriverAsync(RawTreeTestDb.ClusterId, RawTreeTestDb.RootFolderId, "modbus-2", "Modbus", "{}", TestContext.Current.CancellationToken);
var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId).RowVersion);
var result = await service.UpdateDriverAsync(RawTreeTestDb.FolderedDriverId, "modbus-2", "{}", null, rv);
var result = await service.UpdateDriverAsync(RawTreeTestDb.FolderedDriverId, "modbus-2", "{}", null, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error!.ShouldContain("already exists");
@@ -362,7 +362,7 @@ public sealed class RawTreeServiceWaveBTests
db.SaveChanges();
}
var result = await service.UpdateDriverAsync(RawTreeTestDb.FolderedDriverId, "x", "{}", null, rv);
var result = await service.UpdateDriverAsync(RawTreeTestDb.FolderedDriverId, "x", "{}", null, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -375,7 +375,7 @@ public sealed class RawTreeServiceWaveBTests
{
var (service, _) = Seeded();
var dto = await service.LoadDeviceForEditAsync(RawTreeTestDb.FolderedDeviceId);
var dto = await service.LoadDeviceForEditAsync(RawTreeTestDb.FolderedDeviceId, TestContext.Current.CancellationToken);
dto.ShouldNotBeNull();
dto!.Name.ShouldBe("Device1");
@@ -388,7 +388,7 @@ public sealed class RawTreeServiceWaveBTests
public async Task LoadDeviceForEdit_returns_null_when_missing()
{
var (service, _) = Seeded();
(await service.LoadDeviceForEditAsync("DEV-nope")).ShouldBeNull();
(await service.LoadDeviceForEditAsync("DEV-nope", TestContext.Current.CancellationToken)).ShouldBeNull();
}
[Fact]
@@ -398,7 +398,7 @@ public sealed class RawTreeServiceWaveBTests
var rv = RowVersionOf(dbName, db => db.Devices.Single(d => d.DeviceId == RawTreeTestDb.FolderedDeviceId).RowVersion);
var result = await service.UpdateDeviceAsync(
RawTreeTestDb.FolderedDeviceId, "PLC-A", "{\"Host\":\"10.0.0.9\",\"Port\":502}", enabled: false, rv);
RawTreeTestDb.FolderedDeviceId, "PLC-A", "{\"Host\":\"10.0.0.9\",\"Port\":502}", enabled: false, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
using var db = RawTreeTestDb.CreateNamed(dbName);
@@ -412,10 +412,10 @@ public sealed class RawTreeServiceWaveBTests
public async Task UpdateDevice_rejects_sibling_name_collision()
{
var (service, dbName) = Seeded();
await service.CreateDeviceAsync(RawTreeTestDb.FolderedDriverId, "Device2", "{}");
await service.CreateDeviceAsync(RawTreeTestDb.FolderedDriverId, "Device2", "{}", TestContext.Current.CancellationToken);
var rv = RowVersionOf(dbName, db => db.Devices.Single(d => d.DeviceId == RawTreeTestDb.FolderedDeviceId).RowVersion);
var result = await service.UpdateDeviceAsync(RawTreeTestDb.FolderedDeviceId, "Device2", "{}", true, rv);
var result = await service.UpdateDeviceAsync(RawTreeTestDb.FolderedDeviceId, "Device2", "{}", true, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error!.ShouldContain("already exists");
@@ -425,17 +425,17 @@ public sealed class RawTreeServiceWaveBTests
public async Task CreateDevice_refuses_a_second_device_under_an_MTConnect_driver_but_not_under_others()
{
var (service, dbName) = Seeded();
var mt = await service.CreateDriverAsync(RawTreeTestDb.ClusterId, null, "mt-1", "MTConnect", "{}");
var mt = await service.CreateDriverAsync(RawTreeTestDb.ClusterId, null, "mt-1", "MTConnect", "{}", TestContext.Current.CancellationToken);
mt.Ok.ShouldBeTrue();
// CreateDriver auto-creates Device1, so the very next device is the ambiguous second one (#628).
var refused = await service.CreateDeviceAsync(mt.CreatedId!, "Device2", "{}");
var refused = await service.CreateDeviceAsync(mt.CreatedId!, "Device2", "{}", TestContext.Current.CancellationToken);
refused.Ok.ShouldBeFalse();
refused.Error!.ShouldContain("one MTConnect driver per agent device");
// Positive control: the same call against a non-MTConnect driver still succeeds.
(await service.CreateDeviceAsync(RawTreeTestDb.FolderedDriverId, "Device9", "{}")).Ok.ShouldBeTrue();
(await service.CreateDeviceAsync(RawTreeTestDb.FolderedDriverId, "Device9", "{}", TestContext.Current.CancellationToken)).Ok.ShouldBeTrue();
using var db = RawTreeTestDb.CreateNamed(dbName);
db.Devices.Count(d => d.DriverInstanceId == mt.CreatedId).ShouldBe(1);
}
@@ -454,7 +454,7 @@ public sealed class RawTreeServiceWaveBTests
db.SaveChanges();
}
var probe = await service.LoadMergedProbeConfigAsync(RawTreeTestDb.FolderedDeviceId);
var probe = await service.LoadMergedProbeConfigAsync(RawTreeTestDb.FolderedDeviceId, TestContext.Current.CancellationToken);
probe.ShouldNotBeNull();
probe!.Value.DriverType.ShouldBe("Modbus");
@@ -467,6 +467,6 @@ public sealed class RawTreeServiceWaveBTests
public async Task LoadMergedProbeConfig_returns_null_for_unknown_device()
{
var (service, _) = Seeded();
(await service.LoadMergedProbeConfigAsync("DEV-nope")).ShouldBeNull();
(await service.LoadMergedProbeConfigAsync("DEV-nope", TestContext.Current.CancellationToken)).ShouldBeNull();
}
}
@@ -94,7 +94,7 @@ public sealed class ScriptSourceServiceTests
var (service, dbName) = Fresh();
SeedScriptAndUsages(dbName);
var loaded = await service.GetScriptSourceAsync("SCRIPT-1");
var loaded = await service.GetScriptSourceAsync("SCRIPT-1", TestContext.Current.CancellationToken);
loaded.ShouldNotBeNull();
loaded.Value.SourceCode.ShouldBe("return 1;");
@@ -109,7 +109,7 @@ public sealed class ScriptSourceServiceTests
var (service, dbName) = Fresh();
SeedScriptAndUsages(dbName);
var loaded = await service.GetScriptSourceAsync("SCRIPT-NOPE");
var loaded = await service.GetScriptSourceAsync("SCRIPT-NOPE", TestContext.Current.CancellationToken);
loaded.ShouldBeNull();
}
@@ -123,9 +123,9 @@ public sealed class ScriptSourceServiceTests
var (service, dbName) = Fresh();
SeedScriptAndUsages(dbName);
(await service.CountVirtualTagsUsingScriptAsync("SCRIPT-1")).ShouldBe(2);
(await service.CountVirtualTagsUsingScriptAsync("SCRIPT-2")).ShouldBe(1);
(await service.CountVirtualTagsUsingScriptAsync("SCRIPT-NOPE")).ShouldBe(0);
(await service.CountVirtualTagsUsingScriptAsync("SCRIPT-1", TestContext.Current.CancellationToken)).ShouldBe(2);
(await service.CountVirtualTagsUsingScriptAsync("SCRIPT-2", TestContext.Current.CancellationToken)).ShouldBe(1);
(await service.CountVirtualTagsUsingScriptAsync("SCRIPT-NOPE", TestContext.Current.CancellationToken)).ShouldBe(0);
}
// ----- UpdateScriptSourceAsync -----
@@ -147,7 +147,7 @@ public sealed class ScriptSourceServiceTests
}
const string newSource = "return 42;";
var result = await service.UpdateScriptSourceAsync("SCRIPT-1", newSource, rv);
var result = await service.UpdateScriptSourceAsync("SCRIPT-1", newSource, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -166,7 +166,7 @@ public sealed class ScriptSourceServiceTests
var (service, dbName) = Fresh();
SeedScriptAndUsages(dbName);
var result = await service.UpdateScriptSourceAsync("SCRIPT-NOPE", "x", []);
var result = await service.UpdateScriptSourceAsync("SCRIPT-NOPE", "x", [], TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("Script not found.");
@@ -200,7 +200,7 @@ public sealed class ScriptSourceServiceTests
other.SaveChanges();
}
var result = await service.UpdateScriptSourceAsync("SCRIPT-1", "return 99;", staleRv);
var result = await service.UpdateScriptSourceAsync("SCRIPT-1", "return 99;", staleRv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -76,7 +76,7 @@ public sealed class UnsSegmentGuardTests
{
var (service, _) = Fresh();
var result = await service.CreateAreaAsync("MAIN", "AREA-NEW", name, null);
var result = await service.CreateAreaAsync("MAIN", "AREA-NEW", name, null, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -88,7 +88,7 @@ public sealed class UnsSegmentGuardTests
{
var (service, _) = Fresh();
(await service.CreateAreaAsync("MAIN", "AREA-NEW", name, null)).Ok.ShouldBeTrue();
(await service.CreateAreaAsync("MAIN", "AREA-NEW", name, null, TestContext.Current.CancellationToken)).Ok.ShouldBeTrue();
}
/// <summary>
@@ -99,13 +99,13 @@ public sealed class UnsSegmentGuardTests
public async Task UpdateArea_rejects_a_rename_to_an_invalid_segment_and_leaves_the_row_alone()
{
var (service, dbName) = Fresh();
await service.CreateAreaAsync("MAIN", "AREA-1", "packaging", null);
await service.CreateAreaAsync("MAIN", "AREA-1", "packaging", null, TestContext.Current.CancellationToken);
var rowVersion = Load(dbName).UnsAreas.Single(a => a.UnsAreaId == "AREA-1").RowVersion;
(await service.UpdateAreaAsync("AREA-1", "Packaging", null, "MAIN", rowVersion)).Ok.ShouldBeFalse();
(await service.UpdateAreaAsync("AREA-1", "Packaging", null, "MAIN", rowVersion, TestContext.Current.CancellationToken)).Ok.ShouldBeFalse();
Load(dbName).UnsAreas.Single(a => a.UnsAreaId == "AREA-1").Name.ShouldBe("packaging");
(await service.UpdateAreaAsync("AREA-1", "assembly", null, "MAIN", rowVersion)).Ok.ShouldBeTrue();
(await service.UpdateAreaAsync("AREA-1", "assembly", null, "MAIN", rowVersion, TestContext.Current.CancellationToken)).Ok.ShouldBeTrue();
Load(dbName).UnsAreas.Single(a => a.UnsAreaId == "AREA-1").Name.ShouldBe("assembly");
}
@@ -118,7 +118,7 @@ public sealed class UnsSegmentGuardTests
var (service, dbName) = Fresh();
SeedAreaAndLine(dbName);
var result = await service.CreateLineAsync("AREA-1", "LINE-NEW", name, null);
var result = await service.CreateLineAsync("AREA-1", "LINE-NEW", name, null, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -130,7 +130,7 @@ public sealed class UnsSegmentGuardTests
var (service, dbName) = Fresh();
SeedAreaAndLine(dbName);
(await service.CreateLineAsync("AREA-1", "LINE-NEW", "line-2", null)).Ok.ShouldBeTrue();
(await service.CreateLineAsync("AREA-1", "LINE-NEW", "line-2", null, TestContext.Current.CancellationToken)).Ok.ShouldBeTrue();
}
[Fact]
@@ -140,8 +140,8 @@ public sealed class UnsSegmentGuardTests
SeedAreaAndLine(dbName);
var rowVersion = Load(dbName).UnsLines.Single(l => l.UnsLineId == "LINE-1").RowVersion;
(await service.UpdateLineAsync("LINE-1", "Line 1", null, "AREA-1", rowVersion)).Ok.ShouldBeFalse();
(await service.UpdateLineAsync("LINE-1", "line-1", null, "AREA-1", rowVersion)).Ok.ShouldBeTrue();
(await service.UpdateLineAsync("LINE-1", "Line 1", null, "AREA-1", rowVersion, TestContext.Current.CancellationToken)).Ok.ShouldBeFalse();
(await service.UpdateLineAsync("LINE-1", "line-1", null, "AREA-1", rowVersion, TestContext.Current.CancellationToken)).Ok.ShouldBeTrue();
}
// --- Equipment ----------------------------------------------------------------------------
@@ -153,7 +153,7 @@ public sealed class UnsSegmentGuardTests
var (service, dbName) = Fresh();
SeedAreaAndLine(dbName);
(await service.CreateEquipmentAsync(Input(name, "mc_1"))).Ok.ShouldBeFalse();
(await service.CreateEquipmentAsync(Input(name, "mc_1"), TestContext.Current.CancellationToken)).Ok.ShouldBeFalse();
}
[Fact]
@@ -162,7 +162,7 @@ public sealed class UnsSegmentGuardTests
var (service, dbName) = Fresh();
SeedAreaAndLine(dbName);
(await service.CreateEquipmentAsync(Input("packer-1", "mc_1"))).Ok.ShouldBeTrue();
(await service.CreateEquipmentAsync(Input("packer-1", "mc_1"), TestContext.Current.CancellationToken)).Ok.ShouldBeTrue();
}
[Fact]
@@ -170,14 +170,14 @@ public sealed class UnsSegmentGuardTests
{
var (service, dbName) = Fresh();
SeedAreaAndLine(dbName);
var created = await service.CreateEquipmentAsync(Input("packer-1", "mc_1"));
var created = await service.CreateEquipmentAsync(Input("packer-1", "mc_1"), TestContext.Current.CancellationToken);
created.Ok.ShouldBeTrue();
var row = Load(dbName).Equipment.Single();
(await service.UpdateEquipmentAsync(row.EquipmentId, Input("Packer 1", "mc_1"), row.RowVersion))
(await service.UpdateEquipmentAsync(row.EquipmentId, Input("Packer 1", "mc_1"), row.RowVersion, TestContext.Current.CancellationToken))
.Ok.ShouldBeFalse();
(await service.UpdateEquipmentAsync(row.EquipmentId, Input("packer-2", "mc_1"), row.RowVersion))
(await service.UpdateEquipmentAsync(row.EquipmentId, Input("packer-2", "mc_1"), row.RowVersion, TestContext.Current.CancellationToken))
.Ok.ShouldBeTrue();
}
@@ -202,7 +202,7 @@ public sealed class UnsSegmentGuardTests
Input("packer-1", "mc_1"),
Input("Packer 2", "mc_2"),
Input("packer-3", "mc_3"),
]);
], TestContext.Current.CancellationToken);
result.Inserted.ShouldBe(2);
result.Skipped.ShouldBe(0, "a bad name is not a row the operator already has");
@@ -32,7 +32,7 @@ public sealed class UnsTreeServiceAreaLineTests
{
var (service, dbName) = Fresh();
var result = await service.CreateAreaAsync("MAIN", "AREA-NEW", "assembly", " ");
var result = await service.CreateAreaAsync("MAIN", "AREA-NEW", "assembly", " ", TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -49,9 +49,9 @@ public sealed class UnsTreeServiceAreaLineTests
public async Task CreateArea_duplicate_id_returns_error()
{
var (service, dbName) = Fresh();
await service.CreateAreaAsync("MAIN", "AREA-DUP", "first", null);
await service.CreateAreaAsync("MAIN", "AREA-DUP", "first", null, TestContext.Current.CancellationToken);
var result = await service.CreateAreaAsync("MAIN", "AREA-DUP", "second", null);
var result = await service.CreateAreaAsync("MAIN", "AREA-DUP", "second", null, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("Area 'AREA-DUP' already exists.");
@@ -67,7 +67,7 @@ public sealed class UnsTreeServiceAreaLineTests
public async Task UpdateArea_changes_name_and_notes()
{
var (service, dbName) = Fresh();
await service.CreateAreaAsync("MAIN", "AREA-1", "old", "old notes");
await service.CreateAreaAsync("MAIN", "AREA-1", "old", "old notes", TestContext.Current.CancellationToken);
byte[] rv;
using (var db = UnsTreeTestDb.CreateNamed(dbName))
@@ -75,7 +75,7 @@ public sealed class UnsTreeServiceAreaLineTests
rv = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-1").RowVersion;
}
var result = await service.UpdateAreaAsync("AREA-1", "new", " ", "MAIN", rv);
var result = await service.UpdateAreaAsync("AREA-1", "new", " ", "MAIN", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -116,7 +116,7 @@ public sealed class UnsTreeServiceAreaLineTests
rv = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-X").RowVersion;
}
var result = await service.UpdateAreaAsync("AREA-X", "a", null, "SITE-A", rv);
var result = await service.UpdateAreaAsync("AREA-X", "a", null, "SITE-A", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -132,7 +132,7 @@ public sealed class UnsTreeServiceAreaLineTests
{
var (service, _) = Fresh();
var result = await service.UpdateAreaAsync("NOPE", "n", null, "MAIN", []);
var result = await service.UpdateAreaAsync("NOPE", "n", null, "MAIN", [], TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("Row no longer exists.");
@@ -145,7 +145,7 @@ public sealed class UnsTreeServiceAreaLineTests
public async Task DeleteArea_removes_row()
{
var (service, dbName) = Fresh();
await service.CreateAreaAsync("MAIN", "AREA-DEL", "a", null);
await service.CreateAreaAsync("MAIN", "AREA-DEL", "a", null, TestContext.Current.CancellationToken);
byte[] rv;
using (var db = UnsTreeTestDb.CreateNamed(dbName))
@@ -153,7 +153,7 @@ public sealed class UnsTreeServiceAreaLineTests
rv = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-DEL").RowVersion;
}
var result = await service.DeleteAreaAsync("AREA-DEL", rv);
var result = await service.DeleteAreaAsync("AREA-DEL", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -168,7 +168,7 @@ public sealed class UnsTreeServiceAreaLineTests
{
var (service, _) = Fresh();
var result = await service.DeleteAreaAsync("GHOST", []);
var result = await service.DeleteAreaAsync("GHOST", [], TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -181,9 +181,9 @@ public sealed class UnsTreeServiceAreaLineTests
public async Task CreateLine_then_load_shows_it()
{
var (service, dbName) = Fresh();
await service.CreateAreaAsync("MAIN", "AREA-1", "a", null);
await service.CreateAreaAsync("MAIN", "AREA-1", "a", null, TestContext.Current.CancellationToken);
var result = await service.CreateLineAsync("AREA-1", "LINE-NEW", "line-a", " ");
var result = await service.CreateLineAsync("AREA-1", "LINE-NEW", "line-a", " ", TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -200,9 +200,9 @@ public sealed class UnsTreeServiceAreaLineTests
public async Task CreateLine_duplicate_id_returns_error()
{
var (service, _) = Fresh();
await service.CreateLineAsync("AREA-1", "LINE-DUP", "first", null);
await service.CreateLineAsync("AREA-1", "LINE-DUP", "first", null, TestContext.Current.CancellationToken);
var result = await service.CreateLineAsync("AREA-1", "LINE-DUP", "second", null);
var result = await service.CreateLineAsync("AREA-1", "LINE-DUP", "second", null, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("Line 'LINE-DUP' already exists.");
@@ -215,7 +215,7 @@ public sealed class UnsTreeServiceAreaLineTests
public async Task UpdateLine_changes_area_name_and_notes()
{
var (service, dbName) = Fresh();
await service.CreateLineAsync("AREA-1", "LINE-1", "old", "old notes");
await service.CreateLineAsync("AREA-1", "LINE-1", "old", "old notes", TestContext.Current.CancellationToken);
byte[] rv;
using (var db = UnsTreeTestDb.CreateNamed(dbName))
@@ -223,7 +223,7 @@ public sealed class UnsTreeServiceAreaLineTests
rv = db.UnsLines.Single(l => l.UnsLineId == "LINE-1").RowVersion;
}
var result = await service.UpdateLineAsync("LINE-1", "new", " ", "AREA-2", rv);
var result = await service.UpdateLineAsync("LINE-1", "new", " ", "AREA-2", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -241,7 +241,7 @@ public sealed class UnsTreeServiceAreaLineTests
{
var (service, _) = Fresh();
var result = await service.UpdateLineAsync("NOPE", "n", null, "AREA-1", []);
var result = await service.UpdateLineAsync("NOPE", "n", null, "AREA-1", [], TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("Row no longer exists.");
@@ -275,7 +275,7 @@ public sealed class UnsTreeServiceAreaLineTests
rv = db.UnsLines.Single(l => l.UnsLineId == "LINE-BOUND").RowVersion;
}
var result = await service.UpdateLineAsync("LINE-BOUND", "line", null, "A2-SITE-A", rv);
var result = await service.UpdateLineAsync("LINE-BOUND", "line", null, "A2-SITE-A", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -292,7 +292,7 @@ public sealed class UnsTreeServiceAreaLineTests
public async Task DeleteLine_removes_row()
{
var (service, dbName) = Fresh();
await service.CreateLineAsync("AREA-1", "LINE-DEL", "l", null);
await service.CreateLineAsync("AREA-1", "LINE-DEL", "l", null, TestContext.Current.CancellationToken);
byte[] rv;
using (var db = UnsTreeTestDb.CreateNamed(dbName))
@@ -300,7 +300,7 @@ public sealed class UnsTreeServiceAreaLineTests
rv = db.UnsLines.Single(l => l.UnsLineId == "LINE-DEL").RowVersion;
}
var result = await service.DeleteLineAsync("LINE-DEL", rv);
var result = await service.DeleteLineAsync("LINE-DEL", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -315,7 +315,7 @@ public sealed class UnsTreeServiceAreaLineTests
{
var (service, _) = Fresh();
var result = await service.DeleteLineAsync("GHOST", []);
var result = await service.DeleteLineAsync("GHOST", [], TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -36,7 +36,7 @@ public sealed class UnsTreeServiceCreateScriptTests
{
var (service, dbName) = Fresh();
var result = await service.CreateScriptAsync("my new script");
var result = await service.CreateScriptAsync("my new script", TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -59,7 +59,7 @@ public sealed class UnsTreeServiceCreateScriptTests
{
var (service, dbName) = Fresh();
var result = await service.CreateScriptAsync(" ");
var result = await service.CreateScriptAsync(" ", TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
using var db = UnsTreeTestDb.CreateNamed(dbName);
@@ -72,8 +72,8 @@ public sealed class UnsTreeServiceCreateScriptTests
{
var (service, dbName) = Fresh();
var first = await service.CreateScriptAsync("first");
var second = await service.CreateScriptAsync("second");
var first = await service.CreateScriptAsync("first", TestContext.Current.CancellationToken);
var second = await service.CreateScriptAsync("second", TestContext.Current.CancellationToken);
first.Ok.ShouldBeTrue();
second.Ok.ShouldBeTrue();
@@ -50,7 +50,7 @@ public sealed class UnsTreeServiceDeleteClusterTests
db.SaveChanges();
}
var result = await service.DeleteClusterAsync("CL-EMPTY");
var result = await service.DeleteClusterAsync("CL-EMPTY", TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -65,7 +65,7 @@ public sealed class UnsTreeServiceDeleteClusterTests
{
var (service, _) = Fresh();
var result = await service.DeleteClusterAsync("GHOST-CLUSTER");
var result = await service.DeleteClusterAsync("GHOST-CLUSTER", TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -83,7 +83,7 @@ public sealed class UnsTreeServiceDeleteClusterTests
db.SaveChanges();
}
var result = await service.DeleteClusterAsync("CL-AREA");
var result = await service.DeleteClusterAsync("CL-AREA", TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -112,7 +112,7 @@ public sealed class UnsTreeServiceDeleteClusterTests
db.SaveChanges();
}
var result = await service.DeleteClusterAsync("CL-DRV");
var result = await service.DeleteClusterAsync("CL-DRV", TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -140,7 +140,7 @@ public sealed class UnsTreeServiceDeleteClusterTests
db.SaveChanges();
}
var result = await service.DeleteClusterAsync("CL-NODE");
var result = await service.DeleteClusterAsync("CL-NODE", TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -171,7 +171,7 @@ public sealed class UnsTreeServiceDeleteClusterTests
db.SaveChanges();
}
var result = await service.DeleteEnterpriseAsync("ent-1");
var result = await service.DeleteEnterpriseAsync("ent-1", TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -187,7 +187,7 @@ public sealed class UnsTreeServiceDeleteClusterTests
{
var (service, _) = Fresh();
var result = await service.DeleteEnterpriseAsync("nonexistent-enterprise");
var result = await service.DeleteEnterpriseAsync("nonexistent-enterprise", TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -209,7 +209,7 @@ public sealed class UnsTreeServiceDeleteClusterTests
db.SaveChanges();
}
var result = await service.DeleteEnterpriseAsync("ent-3");
var result = await service.DeleteEnterpriseAsync("ent-3", TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -20,7 +20,7 @@ public sealed class UnsTreeServiceEquipmentChildRowsTests
[Fact]
public async Task LoadVirtualTagsForEquipment_returns_vtags_in_name_order()
{
var rows = await SeededService().LoadVirtualTagsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId);
var rows = await SeededService().LoadVirtualTagsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId, TestContext.Current.CancellationToken);
rows.Count.ShouldBe(1);
rows[0].VirtualTagId.ShouldBe("VTAG-1");
rows[0].Name.ShouldBe("computed");
@@ -22,7 +22,7 @@ public sealed class UnsTreeServiceEquipmentPickContextTests
[Fact]
public async Task ResolvesClusterLines_forKnownLine()
{
var ctx = await SeededService().LoadEquipmentPickContextAsync("LINE-1");
var ctx = await SeededService().LoadEquipmentPickContextAsync("LINE-1", TestContext.Current.CancellationToken);
ctx.Lines.ShouldContain(o => o.Id == "LINE-1");
}
@@ -30,7 +30,7 @@ public sealed class UnsTreeServiceEquipmentPickContextTests
public async Task Empty_for_null_or_unknown_line()
{
var svc = SeededService();
(await svc.LoadEquipmentPickContextAsync(null)).Lines.ShouldBeEmpty();
(await svc.LoadEquipmentPickContextAsync("NOPE")).Lines.ShouldBeEmpty();
(await svc.LoadEquipmentPickContextAsync(null, TestContext.Current.CancellationToken)).Lines.ShouldBeEmpty();
(await svc.LoadEquipmentPickContextAsync("NOPE", TestContext.Current.CancellationToken)).Lines.ShouldBeEmpty();
}
}
@@ -48,7 +48,7 @@ public sealed class UnsTreeServiceEquipmentTests
var (service, dbName) = Fresh();
SeedLine(dbName, "MAIN");
var result = await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1"));
var result = await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1"), TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -68,9 +68,9 @@ public sealed class UnsTreeServiceEquipmentTests
{
var (service, dbName) = Fresh();
SeedLine(dbName, "MAIN");
await service.CreateEquipmentAsync(Input("machine-1", "machine_dup", "LINE-1"));
await service.CreateEquipmentAsync(Input("machine-1", "machine_dup", "LINE-1"), TestContext.Current.CancellationToken);
var result = await service.CreateEquipmentAsync(Input("machine-2", "machine_dup", "LINE-1"));
var result = await service.CreateEquipmentAsync(Input("machine-2", "machine_dup", "LINE-1"), TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("MachineCode 'machine_dup' already exists in this fleet.");
@@ -82,7 +82,7 @@ public sealed class UnsTreeServiceEquipmentTests
{
var (service, _) = Fresh();
var result = await service.CreateEquipmentAsync(Input("machine-1", "machine_001", ""));
var result = await service.CreateEquipmentAsync(Input("machine-1", "machine_001", ""), TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("Pick a UNS line.");
@@ -99,7 +99,7 @@ public sealed class UnsTreeServiceEquipmentTests
var input = new EquipmentInput("machine-2", "machine_002", "LINE-1",
null, null, null, null, null, null, null, null, null, null, null, true);
var result = await service.CreateEquipmentAsync(input);
var result = await service.CreateEquipmentAsync(input, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.CreatedId.ShouldNotBeNull();
@@ -114,7 +114,7 @@ public sealed class UnsTreeServiceEquipmentTests
{
var (service, dbName) = Fresh();
SeedLine(dbName, "MAIN");
await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1"));
await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1"), TestContext.Current.CancellationToken);
string equipmentId;
byte[] rv;
@@ -130,7 +130,7 @@ public sealed class UnsTreeServiceEquipmentTests
HardwareRevision: null, SoftwareRevision: null, YearOfConstruction: (short)2021,
AssetLocation: null, ManufacturerUri: null, DeviceManualUri: null, Enabled: false);
var result = await service.UpdateEquipmentAsync(equipmentId, updated, rv);
var result = await service.UpdateEquipmentAsync(equipmentId, updated, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -150,7 +150,7 @@ public sealed class UnsTreeServiceEquipmentTests
{
var (service, _) = Fresh();
var result = await service.UpdateEquipmentAsync("EQ-nope", Input("n", "mc", "LINE-1"), []);
var result = await service.UpdateEquipmentAsync("EQ-nope", Input("n", "mc", "LINE-1"), [], TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("Row no longer exists.");
@@ -162,8 +162,8 @@ public sealed class UnsTreeServiceEquipmentTests
{
var (service, dbName) = Fresh();
SeedLine(dbName, "MAIN");
await service.CreateEquipmentAsync(Input("machine-a", "mc_a", "LINE-1"));
await service.CreateEquipmentAsync(Input("machine-b", "mc_b", "LINE-1"));
await service.CreateEquipmentAsync(Input("machine-a", "mc_a", "LINE-1"), TestContext.Current.CancellationToken);
await service.CreateEquipmentAsync(Input("machine-b", "mc_b", "LINE-1"), TestContext.Current.CancellationToken);
string equipmentId;
byte[] rv;
@@ -176,7 +176,7 @@ public sealed class UnsTreeServiceEquipmentTests
// Try to rename mc_a → mc_b (which already exists).
var result = await service.UpdateEquipmentAsync(
equipmentId, Input("machine-a", "mc_b", "LINE-1"), rv);
equipmentId, Input("machine-a", "mc_b", "LINE-1"), rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -195,7 +195,7 @@ public sealed class UnsTreeServiceEquipmentTests
{
var (service, dbName) = Fresh();
SeedLine(dbName, "MAIN");
await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1"));
await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1"), TestContext.Current.CancellationToken);
string equipmentId;
byte[] rv;
@@ -206,7 +206,7 @@ public sealed class UnsTreeServiceEquipmentTests
rv = eq.RowVersion;
}
var result = await service.DeleteEquipmentAsync(equipmentId, rv);
var result = await service.DeleteEquipmentAsync(equipmentId, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -221,7 +221,7 @@ public sealed class UnsTreeServiceEquipmentTests
{
var (service, _) = Fresh();
var result = await service.DeleteEquipmentAsync("EQ-ghost", []);
var result = await service.DeleteEquipmentAsync("EQ-ghost", [], TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -46,7 +46,7 @@ public sealed class UnsTreeServiceImportTests
[
Input("machine-1", "mc_1", "LINE-1"),
Input("machine-2", "mc_2", "LINE-1"),
]);
], TestContext.Current.CancellationToken);
result.Inserted.ShouldBe(2);
result.Skipped.ShouldBe(0);
@@ -72,14 +72,14 @@ public sealed class UnsTreeServiceImportTests
var (service, dbName) = Fresh();
SeedLine(dbName, "MAIN");
// Pre-existing equipment with MachineCode "mc_existing".
await service.CreateEquipmentAsync(Input("machine-x", "mc_existing", "LINE-1"));
await service.CreateEquipmentAsync(Input("machine-x", "mc_existing", "LINE-1"), TestContext.Current.CancellationToken);
var result = await service.ImportEquipmentAsync(
[
Input("machine-1", "mc_existing", "LINE-1"), // dup of DB row → skip
Input("machine-2", "mc_new", "LINE-1"), // inserts
Input("machine-3", "mc_new", "LINE-1"), // dup of in-batch row → skip
]);
], TestContext.Current.CancellationToken);
result.Inserted.ShouldBe(1);
result.Skipped.ShouldBe(2);
@@ -101,7 +101,7 @@ public sealed class UnsTreeServiceImportTests
[
Input("machine-1", "mc_1", "LINE-BOGUS"),
Input("machine-2", "mc_2", "LINE-1"),
]);
], TestContext.Current.CancellationToken);
result.Inserted.ShouldBe(1);
result.Skipped.ShouldBe(0);
@@ -26,7 +26,7 @@ public sealed class UnsTreeServiceLoadEditTests
{
var (service, _) = Seeded();
var dto = await service.LoadAreaAsync("AREA-1");
var dto = await service.LoadAreaAsync("AREA-1", TestContext.Current.CancellationToken);
dto.ShouldNotBeNull();
dto.UnsAreaId.ShouldBe("AREA-1");
@@ -41,7 +41,7 @@ public sealed class UnsTreeServiceLoadEditTests
{
var (service, _) = Seeded();
(await service.LoadAreaAsync("NOPE")).ShouldBeNull();
(await service.LoadAreaAsync("NOPE", TestContext.Current.CancellationToken)).ShouldBeNull();
}
/// <summary>Loading a seeded line maps its fields, parent area, and a non-empty RowVersion.</summary>
@@ -50,7 +50,7 @@ public sealed class UnsTreeServiceLoadEditTests
{
var (service, _) = Seeded();
var dto = await service.LoadLineAsync("LINE-1");
var dto = await service.LoadLineAsync("LINE-1", TestContext.Current.CancellationToken);
dto.ShouldNotBeNull();
dto.UnsLineId.ShouldBe("LINE-1");
@@ -65,7 +65,7 @@ public sealed class UnsTreeServiceLoadEditTests
{
var (service, _) = Seeded();
(await service.LoadLineAsync("NOPE")).ShouldBeNull();
(await service.LoadLineAsync("NOPE", TestContext.Current.CancellationToken)).ShouldBeNull();
}
/// <summary>Loading the seeded equipment maps its identity fields, line, and a non-empty RowVersion.</summary>
@@ -74,7 +74,7 @@ public sealed class UnsTreeServiceLoadEditTests
{
var (service, _) = Seeded();
var dto = await service.LoadEquipmentAsync(UnsTreeTestDb.SeededEquipmentId);
var dto = await service.LoadEquipmentAsync(UnsTreeTestDb.SeededEquipmentId, TestContext.Current.CancellationToken);
dto.ShouldNotBeNull();
dto.EquipmentId.ShouldBe(UnsTreeTestDb.SeededEquipmentId);
@@ -90,7 +90,7 @@ public sealed class UnsTreeServiceLoadEditTests
{
var (service, _) = Seeded();
(await service.LoadEquipmentAsync("NOPE")).ShouldBeNull();
(await service.LoadEquipmentAsync("NOPE", TestContext.Current.CancellationToken)).ShouldBeNull();
}
/// <summary>
@@ -129,10 +129,10 @@ public sealed class UnsTreeServiceLoadEditTests
DriverType = "S7",
DriverConfig = "{}",
});
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var drivers = await service.LoadDriversForClusterAsync(UnsTreeTestDb.PopulatedClusterId);
var drivers = await service.LoadDriversForClusterAsync(UnsTreeTestDb.PopulatedClusterId, TestContext.Current.CancellationToken);
drivers.Count.ShouldBe(2);
drivers[0].DriverInstanceId.ShouldBe("DRV-A");
@@ -151,7 +151,7 @@ public sealed class UnsTreeServiceLoadEditTests
{
var (service, _) = Seeded();
var dto = await service.LoadTagAsync("TAG-1");
var dto = await service.LoadTagAsync("TAG-1", TestContext.Current.CancellationToken);
dto.ShouldNotBeNull();
dto.TagId.ShouldBe("TAG-1");
@@ -170,7 +170,7 @@ public sealed class UnsTreeServiceLoadEditTests
{
var (service, _) = Seeded();
(await service.LoadTagAsync("NOPE")).ShouldBeNull();
(await service.LoadTagAsync("NOPE", TestContext.Current.CancellationToken)).ShouldBeNull();
}
/// <summary>Loading a seeded virtual tag maps its fields, owning equipment, and a non-empty RowVersion.</summary>
@@ -179,7 +179,7 @@ public sealed class UnsTreeServiceLoadEditTests
{
var (service, _) = Seeded();
var dto = await service.LoadVirtualTagAsync("VTAG-1");
var dto = await service.LoadVirtualTagAsync("VTAG-1", TestContext.Current.CancellationToken);
dto.ShouldNotBeNull();
dto.VirtualTagId.ShouldBe("VTAG-1");
@@ -196,6 +196,6 @@ public sealed class UnsTreeServiceLoadEditTests
{
var (service, _) = Seeded();
(await service.LoadVirtualTagAsync("NOPE")).ShouldBeNull();
(await service.LoadVirtualTagAsync("NOPE", TestContext.Current.CancellationToken)).ShouldBeNull();
}
}
@@ -108,7 +108,7 @@ public sealed class UnsTreeServiceReferenceTests
Seed(name);
var service = new UnsTreeService(Factory(name));
var result = await service.AddReferencesAsync(EquipmentId, ["TAG-speed", "TAG-running"]);
var result = await service.AddReferencesAsync(EquipmentId, ["TAG-speed", "TAG-running"], TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -127,9 +127,9 @@ public sealed class UnsTreeServiceReferenceTests
var name = $"ref-load-{Guid.NewGuid():N}";
Seed(name);
var service = new UnsTreeService(Factory(name));
await service.AddReferencesAsync(EquipmentId, ["TAG-speed", "TAG-grouped"]);
await service.AddReferencesAsync(EquipmentId, ["TAG-speed", "TAG-grouped"], TestContext.Current.CancellationToken);
var rows = await service.LoadReferencesForEquipmentAsync(EquipmentId);
var rows = await service.LoadReferencesForEquipmentAsync(EquipmentId, TestContext.Current.CancellationToken);
rows.Count.ShouldBe(2);
@@ -151,7 +151,7 @@ public sealed class UnsTreeServiceReferenceTests
Seed(name);
var service = new UnsTreeService(Factory(name));
var result = await service.AddReferencesAsync(EquipmentId, ["TAG-other"]);
var result = await service.AddReferencesAsync(EquipmentId, ["TAG-other"], TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -168,9 +168,9 @@ public sealed class UnsTreeServiceReferenceTests
var name = $"ref-dup-{Guid.NewGuid():N}";
Seed(name);
var service = new UnsTreeService(Factory(name));
await service.AddReferencesAsync(EquipmentId, ["TAG-speed"]);
await service.AddReferencesAsync(EquipmentId, ["TAG-speed"], TestContext.Current.CancellationToken);
var result = await service.AddReferencesAsync(EquipmentId, ["TAG-speed"]);
var result = await service.AddReferencesAsync(EquipmentId, ["TAG-speed"], TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -189,7 +189,7 @@ public sealed class UnsTreeServiceReferenceTests
var guard = new FakeGuard { Result = "'speed' already exists on equipment 'machine-1'." };
var service = new UnsTreeService(Factory(name), guard);
var result = await service.AddReferencesAsync(EquipmentId, ["TAG-speed"]);
var result = await service.AddReferencesAsync(EquipmentId, ["TAG-speed"], TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("'speed' already exists on equipment 'machine-1'.");
@@ -211,7 +211,7 @@ public sealed class UnsTreeServiceReferenceTests
var guard = new FakeGuard();
var service = new UnsTreeService(Factory(name), guard);
var result = await service.AddReferencesAsync(EquipmentId, ["TAG-speed", "TAG-running", "TAG-grouped"]);
var result = await service.AddReferencesAsync(EquipmentId, ["TAG-speed", "TAG-running", "TAG-grouped"], TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
guard.CheckManyCalls.ShouldBe(1);
@@ -230,7 +230,7 @@ public sealed class UnsTreeServiceReferenceTests
var guard = new FakeGuard { Result = "'running' already exists on equipment 'machine-1'." };
var service = new UnsTreeService(Factory(name), guard);
var result = await service.AddReferencesAsync(EquipmentId, ["TAG-speed", "TAG-running"]);
var result = await service.AddReferencesAsync(EquipmentId, ["TAG-speed", "TAG-running"], TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("'running' already exists on equipment 'machine-1'.");
@@ -249,22 +249,22 @@ public sealed class UnsTreeServiceReferenceTests
var name = $"ref-override-{Guid.NewGuid():N}";
Seed(name);
var service = new UnsTreeService(Factory(name));
await service.AddReferencesAsync(EquipmentId, ["TAG-speed"]);
await service.AddReferencesAsync(EquipmentId, ["TAG-speed"], TestContext.Current.CancellationToken);
var row = (await service.LoadReferencesForEquipmentAsync(EquipmentId)).Single();
var row = (await service.LoadReferencesForEquipmentAsync(EquipmentId, TestContext.Current.CancellationToken)).Single();
var set = await service.SetReferenceOverrideAsync(row.UnsTagReferenceId, "line-speed", row.RowVersion);
var set = await service.SetReferenceOverrideAsync(row.UnsTagReferenceId, "line-speed", row.RowVersion, TestContext.Current.CancellationToken);
set.Ok.ShouldBeTrue();
var afterSet = (await service.LoadReferencesForEquipmentAsync(EquipmentId)).Single();
var afterSet = (await service.LoadReferencesForEquipmentAsync(EquipmentId, TestContext.Current.CancellationToken)).Single();
afterSet.EffectiveName.ShouldBe("line-speed");
afterSet.DisplayNameOverride.ShouldBe("line-speed");
afterSet.RawPath.ShouldBe("MAIN/dev1/speed"); // raw path is unchanged by an override
var clear = await service.SetReferenceOverrideAsync(afterSet.UnsTagReferenceId, " ", afterSet.RowVersion);
var clear = await service.SetReferenceOverrideAsync(afterSet.UnsTagReferenceId, " ", afterSet.RowVersion, TestContext.Current.CancellationToken);
clear.Ok.ShouldBeTrue();
var afterClear = (await service.LoadReferencesForEquipmentAsync(EquipmentId)).Single();
var afterClear = (await service.LoadReferencesForEquipmentAsync(EquipmentId, TestContext.Current.CancellationToken)).Single();
afterClear.EffectiveName.ShouldBe("speed");
afterClear.DisplayNameOverride.ShouldBeNull();
}
@@ -277,11 +277,11 @@ public sealed class UnsTreeServiceReferenceTests
Seed(name);
var guard = new FakeGuard();
var service = new UnsTreeService(Factory(name), guard);
await service.AddReferencesAsync(EquipmentId, ["TAG-speed"]);
var row = (await service.LoadReferencesForEquipmentAsync(EquipmentId)).Single();
await service.AddReferencesAsync(EquipmentId, ["TAG-speed"], TestContext.Current.CancellationToken);
var row = (await service.LoadReferencesForEquipmentAsync(EquipmentId, TestContext.Current.CancellationToken)).Single();
guard.Result = "collides with a virtual tag.";
var result = await service.SetReferenceOverrideAsync(row.UnsTagReferenceId, "running", row.RowVersion);
var result = await service.SetReferenceOverrideAsync(row.UnsTagReferenceId, "running", row.RowVersion, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("collides with a virtual tag.");
@@ -297,17 +297,17 @@ public sealed class UnsTreeServiceReferenceTests
var name = $"ref-remove-{Guid.NewGuid():N}";
Seed(name);
var service = new UnsTreeService(Factory(name));
await service.AddReferencesAsync(EquipmentId, ["TAG-speed"]);
var row = (await service.LoadReferencesForEquipmentAsync(EquipmentId)).Single();
await service.AddReferencesAsync(EquipmentId, ["TAG-speed"], TestContext.Current.CancellationToken);
var row = (await service.LoadReferencesForEquipmentAsync(EquipmentId, TestContext.Current.CancellationToken)).Single();
var result = await service.RemoveReferenceAsync(row.UnsTagReferenceId, row.RowVersion);
var result = await service.RemoveReferenceAsync(row.UnsTagReferenceId, row.RowVersion, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
using var db = Db(name);
db.UnsTagReferences.Any(r => r.EquipmentId == EquipmentId).ShouldBeFalse();
// Already gone → idempotent success.
var again = await service.RemoveReferenceAsync(row.UnsTagReferenceId, row.RowVersion);
var again = await service.RemoveReferenceAsync(row.UnsTagReferenceId, row.RowVersion, TestContext.Current.CancellationToken);
again.Ok.ShouldBeTrue();
}
@@ -321,7 +321,7 @@ public sealed class UnsTreeServiceReferenceTests
Seed(name);
var service = new UnsTreeService(Factory(name));
var root = await service.LoadReferencePickerRootAsync(EquipmentId);
var root = await service.LoadReferencePickerRootAsync(EquipmentId, TestContext.Current.CancellationToken);
root.ShouldNotBeNull();
root!.Kind.ShouldBe(RawNodeKind.Cluster);
@@ -347,7 +347,7 @@ public sealed class UnsTreeServiceReferenceTests
EntityId = "DEV-1",
};
var ids = await service.LoadDescendantTagIdsAsync(deviceNode);
var ids = await service.LoadDescendantTagIdsAsync(deviceNode, TestContext.Current.CancellationToken);
ids.ShouldBe(new[] { "TAG-speed", "TAG-running", "TAG-grouped" }, ignoreOrder: true);
}
@@ -369,7 +369,7 @@ public sealed class UnsTreeServiceReferenceTests
EntityId = "TG-1",
};
var ids = await service.LoadDescendantTagIdsAsync(groupNode);
var ids = await service.LoadDescendantTagIdsAsync(groupNode, TestContext.Current.CancellationToken);
ids.ShouldBe(new[] { "TAG-grouped" });
}
@@ -21,16 +21,16 @@ public sealed class UnsTreeServiceScriptedAlarmTests
public async Task Create_then_list_and_load_roundtrips()
{
var svc = SeededService();
var create = await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample());
var create = await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample(), TestContext.Current.CancellationToken);
create.Ok.ShouldBeTrue();
create.CreatedId.ShouldBe("SA-1");
var rows = await svc.LoadAlarmsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId);
var rows = await svc.LoadAlarmsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId, TestContext.Current.CancellationToken);
rows.Count.ShouldBe(1);
rows[0].Name.ShouldBe("Over-temp");
rows[0].Severity.ShouldBe(700);
var dto = await svc.LoadScriptedAlarmAsync("SA-1");
var dto = await svc.LoadScriptedAlarmAsync("SA-1", TestContext.Current.CancellationToken);
dto.ShouldNotBeNull();
dto!.EquipmentId.ShouldBe(UnsTreeTestDb.SeededEquipmentId);
dto.HistorizeToAveva.ShouldBeTrue();
@@ -41,8 +41,8 @@ public sealed class UnsTreeServiceScriptedAlarmTests
public async Task Create_rejects_duplicate_id()
{
var svc = SeededService();
(await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample())).Ok.ShouldBeTrue();
var dup = await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample());
(await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample(), TestContext.Current.CancellationToken)).Ok.ShouldBeTrue();
var dup = await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample(), TestContext.Current.CancellationToken);
dup.Ok.ShouldBeFalse();
dup.Error.ShouldNotBeNull();
}
@@ -51,14 +51,14 @@ public sealed class UnsTreeServiceScriptedAlarmTests
public async Task Update_changes_fields()
{
var svc = SeededService();
await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample());
var dto = await svc.LoadScriptedAlarmAsync("SA-1");
await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample(), TestContext.Current.CancellationToken);
var dto = await svc.LoadScriptedAlarmAsync("SA-1", TestContext.Current.CancellationToken);
var upd = await svc.UpdateScriptedAlarmAsync("SA-1",
Sample() with { Name = "Renamed", Severity = 250, HistorizeToAveva = false }, dto!.RowVersion);
Sample() with { Name = "Renamed", Severity = 250, HistorizeToAveva = false }, dto!.RowVersion, TestContext.Current.CancellationToken);
upd.Ok.ShouldBeTrue();
var after = await svc.LoadScriptedAlarmAsync("SA-1");
var after = await svc.LoadScriptedAlarmAsync("SA-1", TestContext.Current.CancellationToken);
after!.Name.ShouldBe("Renamed");
after.Severity.ShouldBe(250);
after.HistorizeToAveva.ShouldBeFalse();
@@ -68,20 +68,20 @@ public sealed class UnsTreeServiceScriptedAlarmTests
public async Task Delete_removes_row()
{
var svc = SeededService();
await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample());
var dto = await svc.LoadScriptedAlarmAsync("SA-1");
await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample(), TestContext.Current.CancellationToken);
var dto = await svc.LoadScriptedAlarmAsync("SA-1", TestContext.Current.CancellationToken);
(await svc.DeleteScriptedAlarmAsync("SA-1", dto!.RowVersion)).Ok.ShouldBeTrue();
(await svc.LoadAlarmsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId)).ShouldBeEmpty();
(await svc.DeleteScriptedAlarmAsync("SA-1", dto!.RowVersion, TestContext.Current.CancellationToken)).Ok.ShouldBeTrue();
(await svc.LoadAlarmsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId, TestContext.Current.CancellationToken)).ShouldBeEmpty();
}
[Fact]
public async Task Create_rejects_duplicate_name_on_same_equipment()
{
var svc = SeededService();
(await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample("SA-1"))).Ok.ShouldBeTrue();
(await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample("SA-1"), TestContext.Current.CancellationToken)).Ok.ShouldBeTrue();
// Same Name ("Over-temp" from Sample), different id, same equipment → rejected.
var dup = await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample("SA-2"));
var dup = await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample("SA-2"), TestContext.Current.CancellationToken);
dup.Ok.ShouldBeFalse();
dup.Error.ShouldNotBeNull();
dup.Error!.ShouldContain("already exists");
@@ -91,10 +91,10 @@ public sealed class UnsTreeServiceScriptedAlarmTests
public async Task LoadAlarmsForEquipment_excludes_other_equipment()
{
var svc = SeededService();
await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample("SA-MINE"));
await svc.CreateScriptedAlarmAsync("EQ-OTHER", Sample("SA-OTHER") with { Name = "Other-alarm" });
await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample("SA-MINE"), TestContext.Current.CancellationToken);
await svc.CreateScriptedAlarmAsync("EQ-OTHER", Sample("SA-OTHER") with { Name = "Other-alarm" }, TestContext.Current.CancellationToken);
var rows = await svc.LoadAlarmsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId);
var rows = await svc.LoadAlarmsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId, TestContext.Current.CancellationToken);
rows.Count.ShouldBe(1);
rows[0].ScriptedAlarmId.ShouldBe("SA-MINE");
@@ -26,7 +26,7 @@ public sealed class UnsTreeServiceStructureTests
{
var service = SeededService();
var roots = await service.LoadStructureAsync();
var roots = await service.LoadStructureAsync(TestContext.Current.CancellationToken);
var enterprise = roots.ShouldHaveSingleItem();
enterprise.Kind.ShouldBe(UnsNodeKind.Enterprise);
@@ -61,7 +61,7 @@ public sealed class UnsTreeServiceStructureTests
{
var service = SeededService();
var roots = await service.LoadStructureAsync();
var roots = await service.LoadStructureAsync(TestContext.Current.CancellationToken);
var equipment = roots
.Single()
@@ -82,7 +82,7 @@ public sealed class UnsTreeServiceStructureTests
{
var service = SeededService();
var roots = await service.LoadStructureAsync();
var roots = await service.LoadStructureAsync(TestContext.Current.CancellationToken);
var siteA = roots
.Single()
@@ -58,7 +58,7 @@ public sealed class UnsTreeServiceTagDriversTests
var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
var drivers = await service.LoadTagDriversForEquipmentAsync("EQ-1");
var drivers = await service.LoadTagDriversForEquipmentAsync("EQ-1", TestContext.Current.CancellationToken);
drivers.ShouldBeEmpty();
}
@@ -73,7 +73,7 @@ public sealed class UnsTreeServiceTagTests
var (service, dbName) = Fresh();
SeedHierarchy(dbName);
var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed"));
var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed"), TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe(TagAuthoringMovedError);
@@ -89,9 +89,9 @@ public sealed class UnsTreeServiceTagTests
var (service, dbName) = Fresh();
SeedHierarchy(dbName);
(await service.CreateTagAsync("EQ-NOPE", Input("TAG-1", "speed"))).Error
(await service.CreateTagAsync("EQ-NOPE", Input("TAG-1", "speed"), TestContext.Current.CancellationToken)).Error
.ShouldBe(TagAuthoringMovedError);
(await service.CreateTagAsync("EQ-1", Input("TAG-2", "speed", tagConfig: "{ not json"))).Error
(await service.CreateTagAsync("EQ-1", Input("TAG-2", "speed", tagConfig: "{ not json"), TestContext.Current.CancellationToken)).Error
.ShouldBe(TagAuthoringMovedError);
}
@@ -101,7 +101,7 @@ public sealed class UnsTreeServiceTagTests
{
var (service, _) = Fresh();
var result = await service.UpdateTagAsync("TAG-1", Input("TAG-1", "renamed"), []);
var result = await service.UpdateTagAsync("TAG-1", Input("TAG-1", "renamed"), [], TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe(TagAuthoringMovedError);
@@ -130,7 +130,7 @@ public sealed class UnsTreeServiceTagTests
rv = db.Tags.Single(t => t.TagId == "TAG-1").RowVersion;
}
var result = await service.DeleteTagAsync("TAG-1", rv);
var result = await service.DeleteTagAsync("TAG-1", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -145,7 +145,7 @@ public sealed class UnsTreeServiceTagTests
{
var (service, _) = Fresh();
var result = await service.DeleteTagAsync("TAG-ghost", []);
var result = await service.DeleteTagAsync("TAG-ghost", [], TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -160,6 +160,6 @@ public sealed class UnsTreeServiceTagTests
var (service, dbName) = Fresh();
SeedHierarchy(dbName);
(await service.LoadTagDriversForEquipmentAsync("EQ-1")).ShouldBeEmpty();
(await service.LoadTagDriversForEquipmentAsync("EQ-1", TestContext.Current.CancellationToken)).ShouldBeEmpty();
}
}
@@ -71,7 +71,7 @@ public sealed class UnsTreeServiceVirtualTagTests
var (service, dbName) = Fresh();
SeedEquipmentAndScript(dbName);
var result = await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"));
var result = await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"), TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -92,7 +92,7 @@ public sealed class UnsTreeServiceVirtualTagTests
var (service, dbName) = Fresh();
SeedEquipmentAndScript(dbName);
var result = await service.CreateVirtualTagAsync("EQ-NOPE", Input("VTAG-1", "computed"));
var result = await service.CreateVirtualTagAsync("EQ-NOPE", Input("VTAG-1", "computed"), TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("Equipment 'EQ-NOPE' not found.");
@@ -109,7 +109,7 @@ public sealed class UnsTreeServiceVirtualTagTests
SeedEquipmentAndScript(dbName);
var result = await service.CreateVirtualTagAsync(
"EQ-1", Input("VTAG-1", "computed", changeTriggered: false, timerIntervalMs: null));
"EQ-1", Input("VTAG-1", "computed", changeTriggered: false, timerIntervalMs: null), TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("Pick at least one trigger — change or timer.");
@@ -126,7 +126,7 @@ public sealed class UnsTreeServiceVirtualTagTests
SeedEquipmentAndScript(dbName);
var result = await service.CreateVirtualTagAsync(
"EQ-1", Input("VTAG-1", "computed", changeTriggered: false, timerIntervalMs: 10));
"EQ-1", Input("VTAG-1", "computed", changeTriggered: false, timerIntervalMs: 10), TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("TimerIntervalMs must be at least 50 ms.");
@@ -141,9 +141,9 @@ public sealed class UnsTreeServiceVirtualTagTests
{
var (service, dbName) = Fresh();
SeedEquipmentAndScript(dbName);
await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"));
await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"), TestContext.Current.CancellationToken);
var result = await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "another"));
var result = await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "another"), TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("VirtualTag 'VTAG-1' already exists.");
@@ -155,9 +155,9 @@ public sealed class UnsTreeServiceVirtualTagTests
{
var (service, dbName) = Fresh();
SeedEquipmentAndScript(dbName);
await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"));
await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"), TestContext.Current.CancellationToken);
var result = await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-2", "computed"));
var result = await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-2", "computed"), TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("A virtual tag named 'computed' already exists on this equipment.");
@@ -171,7 +171,7 @@ public sealed class UnsTreeServiceVirtualTagTests
SeedEquipmentAndScript(dbName);
var input = Input("VTAG-1", "computed", scriptId: "", changeTriggered: true);
var result = await service.CreateVirtualTagAsync("EQ-1", input);
var result = await service.CreateVirtualTagAsync("EQ-1", input, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -189,7 +189,7 @@ public sealed class UnsTreeServiceVirtualTagTests
{
var (service, dbName) = Fresh();
SeedEquipmentAndScript(dbName);
await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"));
await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"), TestContext.Current.CancellationToken);
byte[] rv;
using (var db = UnsTreeTestDb.CreateNamed(dbName))
@@ -200,7 +200,7 @@ public sealed class UnsTreeServiceVirtualTagTests
var updated = new VirtualTagInput("VTAG-1", "renamed", DataType: "Int32", ScriptId: "SCRIPT-1",
ChangeTriggered: false, TimerIntervalMs: 250, Historize: true, Enabled: false);
var result = await service.UpdateVirtualTagAsync("VTAG-1", updated, rv);
var result = await service.UpdateVirtualTagAsync("VTAG-1", updated, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -223,7 +223,7 @@ public sealed class UnsTreeServiceVirtualTagTests
var (service, dbName) = Fresh();
SeedEquipmentAndScript(dbName);
var result = await service.UpdateVirtualTagAsync("VTAG-nope", Input("VTAG-nope", "x"), []);
var result = await service.UpdateVirtualTagAsync("VTAG-nope", Input("VTAG-nope", "x"), [], TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("Row no longer exists.");
@@ -235,8 +235,8 @@ public sealed class UnsTreeServiceVirtualTagTests
{
var (service, dbName) = Fresh();
SeedEquipmentAndScript(dbName);
await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-A", "vt_a"));
await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-B", "vt_b"));
await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-A", "vt_a"), TestContext.Current.CancellationToken);
await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-B", "vt_b"), TestContext.Current.CancellationToken);
byte[] rv;
using (var db = UnsTreeTestDb.CreateNamed(dbName))
@@ -245,7 +245,7 @@ public sealed class UnsTreeServiceVirtualTagTests
}
var input = Input("VTAG-A", "vt_b"); // try to rename to the name already used by VTAG-B
var result = await service.UpdateVirtualTagAsync("VTAG-A", input, rv);
var result = await service.UpdateVirtualTagAsync("VTAG-A", input, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -260,7 +260,7 @@ public sealed class UnsTreeServiceVirtualTagTests
{
var (service, dbName) = Fresh();
SeedEquipmentAndScript(dbName);
await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"));
await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"), TestContext.Current.CancellationToken);
byte[] rv;
using (var db = UnsTreeTestDb.CreateNamed(dbName))
@@ -268,7 +268,7 @@ public sealed class UnsTreeServiceVirtualTagTests
rv = db.VirtualTags.Single(v => v.VirtualTagId == "VTAG-1").RowVersion;
}
var result = await service.DeleteVirtualTagAsync("VTAG-1", rv);
var result = await service.DeleteVirtualTagAsync("VTAG-1", rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -283,7 +283,7 @@ public sealed class UnsTreeServiceVirtualTagTests
{
var (service, _) = Fresh();
var result = await service.DeleteVirtualTagAsync("VTAG-ghost", []);
var result = await service.DeleteVirtualTagAsync("VTAG-ghost", [], TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -298,7 +298,7 @@ public sealed class UnsTreeServiceVirtualTagTests
var (service, dbName) = Fresh();
SeedEquipmentAndScript(dbName);
var scripts = await service.LoadScriptsAsync();
var scripts = await service.LoadScriptsAsync(TestContext.Current.CancellationToken);
scripts.Count.ShouldBe(1);
scripts[0].ScriptId.ShouldBe("SCRIPT-1");
@@ -104,7 +104,7 @@ public sealed class VirtualTagEquipTokenValidationTests
var (service, dbName) = Fresh();
Seed(dbName, EquipRefScript, referenceName: "Speed");
var result = await service.CreateVirtualTagAsync("EQ-1", Input());
var result = await service.CreateVirtualTagAsync("EQ-1", Input(), TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -117,7 +117,7 @@ public sealed class VirtualTagEquipTokenValidationTests
var (service, dbName) = Fresh();
Seed(dbName, EquipRefScript, referenceName: null);
var result = await service.CreateVirtualTagAsync("EQ-1", Input());
var result = await service.CreateVirtualTagAsync("EQ-1", Input(), TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -135,7 +135,7 @@ public sealed class VirtualTagEquipTokenValidationTests
var (service, dbName) = Fresh();
Seed(dbName, PlainScript, referenceName: null);
var result = await service.CreateVirtualTagAsync("EQ-1", Input());
var result = await service.CreateVirtualTagAsync("EQ-1", Input(), TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -151,7 +151,7 @@ public sealed class VirtualTagEquipTokenValidationTests
Seed(dbName, EquipRefScript, referenceName: "Speed");
var rv = SeedVirtualTagAndRowVersion(dbName);
var result = await service.UpdateVirtualTagAsync("VTAG-1", Input(name: "renamed"), rv);
var result = await service.UpdateVirtualTagAsync("VTAG-1", Input(name: "renamed"), rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -165,7 +165,7 @@ public sealed class VirtualTagEquipTokenValidationTests
Seed(dbName, EquipRefScript, referenceName: null);
var rv = SeedVirtualTagAndRowVersion(dbName);
var result = await service.UpdateVirtualTagAsync("VTAG-1", Input(name: "renamed"), rv);
var result = await service.UpdateVirtualTagAsync("VTAG-1", Input(name: "renamed"), rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -184,7 +184,7 @@ public sealed class VirtualTagEquipTokenValidationTests
Seed(dbName, PlainScript, referenceName: null);
var rv = SeedVirtualTagAndRowVersion(dbName);
var result = await service.UpdateVirtualTagAsync("VTAG-1", Input(name: "renamed"), rv);
var result = await service.UpdateVirtualTagAsync("VTAG-1", Input(name: "renamed"), rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -203,7 +203,7 @@ public sealed class VirtualTagEquipTokenValidationTests
var (service, dbName) = Fresh();
Seed(dbName, EquipRefScript, referenceName: null);
var result = await service.CreateScriptedAlarmAsync("EQ-1", AlarmInput());
var result = await service.CreateScriptedAlarmAsync("EQ-1", AlarmInput(), TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -221,7 +221,7 @@ public sealed class VirtualTagEquipTokenValidationTests
var (service, dbName) = Fresh();
Seed(dbName, PlainScript, referenceName: "Speed"); // predicate has no token; only the template does
var result = await service.CreateScriptedAlarmAsync("EQ-1", AlarmInput(messageTemplate: "bad {{equip}}/Missing here"));
var result = await service.CreateScriptedAlarmAsync("EQ-1", AlarmInput(messageTemplate: "bad {{equip}}/Missing here"), TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
@@ -235,7 +235,7 @@ public sealed class VirtualTagEquipTokenValidationTests
var (service, dbName) = Fresh();
Seed(dbName, EquipRefScript, referenceName: "Speed");
var result = await service.CreateScriptedAlarmAsync("EQ-1", AlarmInput());
var result = await service.CreateScriptedAlarmAsync("EQ-1", AlarmInput(), TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit"/>
<PackageReference Include="xunit.v3" VersionOverride="3.2.2"/>
<PackageReference Include="Shouldly"/>
<PackageReference Include="bunit"/>
<!-- bUnit (test-only) pulls AngleSharp 1.1.1 (GHSA-pgww-w46g-26qg, moderate, HTML parsing of untrusted
@@ -21,10 +21,9 @@
it directly, matching Configuration.Tests. -->
<PackageReference Include="Microsoft.Data.SqlClient"/>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<!-- Akka.TestKit.Xunit2 ties this project to xunit v2 (TestKit isn't ported to v3 yet);
used by the IAuditWriter adapter test to register a TestProbe in an ActorRegistry. -->
<!-- Akka.TestKit.Xunit (xunit.v3 TestKit); used by the IAuditWriter adapter test to register a TestProbe in an ActorRegistry. -->
<PackageReference Include="Akka.Hosting"/>
<PackageReference Include="Akka.TestKit.Xunit2"/>
<PackageReference Include="Akka.TestKit.Xunit"/>
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -33,16 +33,16 @@ public sealed class AdminOperationsActorMailboxTests : ControlPlaneActorTestBase
var connectClient = CreateTestProbe("connect-client");
connectClient.Send(actor, new TestDriverConnect("Blocking", "{}", TimeoutSeconds: 60, Guid.NewGuid()));
probe.Started.Wait(PresenceBudget).ShouldBeTrue("the probe must be in flight before the ack is sent");
probe.Started.Wait(PresenceBudget, TestContext.Current.CancellationToken).ShouldBeTrue("the probe must be in flight before the ack is sent");
actor.Tell(new AcknowledgeAlarmCommand("alarm-1", "op", null, CorrelationId.NewId()));
var ack = ExpectMsg<AcknowledgeAlarmResult>(AckBudget);
var ack = ExpectMsg<AcknowledgeAlarmResult>(AckBudget, cancellationToken: TestContext.Current.CancellationToken);
ack.Ok.ShouldBeTrue();
// Positive control: the probe reply still lands, with the right sender, once the probe returns.
connectClient.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
connectClient.ExpectNoMsg(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
probe.Release(new DriverProbeResult(true, null, TimeSpan.FromMilliseconds(5)));
var result = connectClient.ExpectMsg<TestDriverConnectResult>(PresenceBudget);
var result = connectClient.ExpectMsg<TestDriverConnectResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.LatencyMs.ShouldNotBeNull();
}
@@ -58,7 +58,7 @@ public sealed class AdminOperationsActorMailboxTests : ControlPlaneActorTestBase
actor.Tell(new TestDriverConnect("Blocking", "{}", TimeoutSeconds: 1, Guid.NewGuid()));
var result = ExpectMsg<TestDriverConnectResult>(PresenceBudget);
var result = ExpectMsg<TestDriverConnectResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Message.ShouldNotBeNull();
result.Message.ShouldContain("timed out");
@@ -79,16 +79,16 @@ public sealed class AdminOperationsActorMailboxTests : ControlPlaneActorTestBase
gated.Hold();
deployer.Send(actor, new StartDeployment("joe", CorrelationId.NewId()));
gated.Entered.Wait(PresenceBudget).ShouldBeTrue("the compose must be parked before the ack is sent");
gated.Entered.Wait(PresenceBudget, TestContext.Current.CancellationToken).ShouldBeTrue("the compose must be parked before the ack is sent");
actor.Tell(new AcknowledgeAlarmCommand("alarm-2", "op", null, CorrelationId.NewId()));
ExpectMsg<AcknowledgeAlarmResult>(AckBudget).Ok.ShouldBeTrue();
ExpectMsg<AcknowledgeAlarmResult>(AckBudget, cancellationToken: TestContext.Current.CancellationToken).Ok.ShouldBeTrue();
// Positive control: the deploy completes once released.
deployer.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
deployer.ExpectNoMsg(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
gated.Release();
deployer.ExpectMsg<StartDeploymentResult>(PresenceBudget).Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
coordinator.ExpectMsg<DispatchDeployment>(PresenceBudget);
deployer.ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
coordinator.ExpectMsg<DispatchDeployment>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>
@@ -106,14 +106,14 @@ public sealed class AdminOperationsActorMailboxTests : ControlPlaneActorTestBase
gated.Hold();
first.Send(actor, new StartDeployment("joe", CorrelationId.NewId()));
gated.Entered.Wait(PresenceBudget).ShouldBeTrue();
gated.Entered.Wait(PresenceBudget, TestContext.Current.CancellationToken).ShouldBeTrue();
second.Send(actor, new StartDeployment("kim", CorrelationId.NewId()));
second.ExpectMsg<StartDeploymentResult>(AckBudget).Outcome.ShouldBe(StartDeploymentOutcome.AnotherDeploymentInFlight);
second.ExpectMsg<StartDeploymentResult>(AckBudget, cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(StartDeploymentOutcome.AnotherDeploymentInFlight);
gated.Release();
first.ExpectMsg<StartDeploymentResult>(PresenceBudget).Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
coordinator.ExpectMsg<DispatchDeployment>(PresenceBudget);
first.ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
coordinator.ExpectMsg<DispatchDeployment>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
}
private sealed class BlockingProbe : IDriverProbe
@@ -29,7 +29,7 @@ public sealed class AdminOperationsActorServerFaultMessageTests : ControlPlaneAc
actor.Tell(new StartDeployment("joe", correlationId));
var reply = ExpectMsg<StartDeploymentResult>(PresenceBudget);
var reply = ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Rejected);
reply.Message.ShouldNotBeNull();
reply.Message.ShouldNotContain("hunter2");
@@ -46,7 +46,7 @@ public sealed class AdminOperationsActorServerFaultMessageTests : ControlPlaneAc
actor.Tell(new TestDriverConnect("Throwing", "{}", 5, correlationId));
var reply = ExpectMsg<TestDriverConnectResult>(PresenceBudget);
var reply = ExpectMsg<TestDriverConnectResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
reply.Ok.ShouldBeFalse();
reply.Message.ShouldNotBeNull();
reply.Message.ShouldNotContain("hunter2");
@@ -67,9 +67,9 @@ public sealed class AdminOperationsActorSingleSnapshotTests : ControlPlaneActorT
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
var reply = ExpectMsg<StartDeploymentResult>(PresenceBudget);
var reply = ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Accepted, reply.Message);
coordinator.ExpectMsg<DispatchDeployment>(PresenceBudget);
coordinator.ExpectMsg<DispatchDeployment>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
using (var verify = plain.CreateDbContext())
{
@@ -85,7 +85,7 @@ public sealed class AdminOperationsActorSingleSnapshotTests : ControlPlaneActorT
// Positive control: now that the row is visible to a fresh read, the gate rejects it.
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
var second = ExpectMsg<StartDeploymentResult>(PresenceBudget);
var second = ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
second.Outcome.ShouldBe(StartDeploymentOutcome.Rejected);
second.Message.ShouldNotBeNull();
second.Message.ShouldContain("EquipmentIdNotDerived");
@@ -61,8 +61,8 @@ public sealed class AdminOperationsActorTagConfigGateTests : ControlPlaneActorTe
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectMsg<DispatchDeployment>(PresenceBudget);
var reply = ExpectMsg<StartDeploymentResult>(PresenceBudget);
coordinator.ExpectMsg<DispatchDeployment>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
var reply = ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
reply.Message.ShouldNotBeNull();
reply.Message.ShouldContain("Intt16");
@@ -80,8 +80,8 @@ public sealed class AdminOperationsActorTagConfigGateTests : ControlPlaneActorTe
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
var reply = ExpectMsg<StartDeploymentResult>(PresenceBudget);
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
var reply = ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Rejected);
reply.Message.ShouldNotBeNull();
reply.Message.ShouldContain("TagConfigInvalid");
@@ -102,8 +102,8 @@ public sealed class AdminOperationsActorTagConfigGateTests : ControlPlaneActorTe
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectMsg<DispatchDeployment>(PresenceBudget);
var reply = ExpectMsg<StartDeploymentResult>(PresenceBudget);
coordinator.ExpectMsg<DispatchDeployment>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
var reply = ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
(reply.Message is null || !reply.Message.Contains("Intt16")).ShouldBeTrue();
}
@@ -121,8 +121,8 @@ public sealed class AdminOperationsActorTagConfigGateTests : ControlPlaneActorTe
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectMsg<DispatchDeployment>(PresenceBudget);
ExpectMsg<StartDeploymentResult>(PresenceBudget).Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
coordinator.ExpectMsg<DispatchDeployment>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
}
// lmxopcua#781 item 3 — through the actor, in Error mode too: the writable mismatch is a warning, never a reject.
@@ -46,14 +46,14 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
var correlationId = CorrelationId.NewId();
actor.Tell(new AcknowledgeAlarmCommand("alarm-42", "operator-jo", "looking into it", correlationId));
var published = topicProbe.ExpectMsg<AlarmCommand>(TimeSpan.FromSeconds(3));
var published = topicProbe.ExpectMsg<AlarmCommand>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
published.AlarmId.ShouldBe("alarm-42");
published.Operation.ShouldBe("Acknowledge");
published.User.ShouldBe("operator-jo");
published.Comment.ShouldBe("looking into it");
published.UnshelveAtUtc.ShouldBeNull();
var reply = ExpectMsg<AcknowledgeAlarmResult>(TimeSpan.FromSeconds(3));
var reply = ExpectMsg<AcknowledgeAlarmResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Ok.ShouldBeTrue();
reply.Message.ShouldBeNull();
reply.CorrelationId.ShouldBe(correlationId);
@@ -72,13 +72,13 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
var correlationId = CorrelationId.NewId();
actor.Tell(new ShelveAlarmCommand("alarm-7", "op-kim", ShelveKind.OneShot, UnshelveAtUtc: null, Comment: null, correlationId));
var published = topicProbe.ExpectMsg<AlarmCommand>(TimeSpan.FromSeconds(3));
var published = topicProbe.ExpectMsg<AlarmCommand>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
published.AlarmId.ShouldBe("alarm-7");
published.Operation.ShouldBe("OneShotShelve");
published.User.ShouldBe("op-kim");
published.UnshelveAtUtc.ShouldBeNull();
var reply = ExpectMsg<ShelveAlarmResult>(TimeSpan.FromSeconds(3));
var reply = ExpectMsg<ShelveAlarmResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Ok.ShouldBeTrue();
reply.CorrelationId.ShouldBe(correlationId);
}
@@ -96,13 +96,13 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
var unshelveAt = DateTime.UtcNow.AddMinutes(15);
actor.Tell(new ShelveAlarmCommand("alarm-9", "op-lee", ShelveKind.Timed, unshelveAt, Comment: "maint window", CorrelationId.NewId()));
var published = topicProbe.ExpectMsg<AlarmCommand>(TimeSpan.FromSeconds(3));
var published = topicProbe.ExpectMsg<AlarmCommand>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
published.AlarmId.ShouldBe("alarm-9");
published.Operation.ShouldBe("TimedShelve");
published.UnshelveAtUtc.ShouldBe(unshelveAt);
published.Comment.ShouldBe("maint window");
var reply = ExpectMsg<ShelveAlarmResult>(TimeSpan.FromSeconds(3));
var reply = ExpectMsg<ShelveAlarmResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Ok.ShouldBeTrue();
}
@@ -118,11 +118,11 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
actor.Tell(new ShelveAlarmCommand("alarm-3", "op-sam", ShelveKind.Unshelve, UnshelveAtUtc: null, Comment: null, CorrelationId.NewId()));
var published = topicProbe.ExpectMsg<AlarmCommand>(TimeSpan.FromSeconds(3));
var published = topicProbe.ExpectMsg<AlarmCommand>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
published.Operation.ShouldBe("Unshelve");
published.UnshelveAtUtc.ShouldBeNull();
ExpectMsg<ShelveAlarmResult>(TimeSpan.FromSeconds(3)).Ok.ShouldBeTrue();
ExpectMsg<ShelveAlarmResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken).Ok.ShouldBeTrue();
}
/// <summary>Verifies a <see cref="ShelveKind.Timed"/> shelve with a null UnshelveAtUtc is rejected
@@ -137,13 +137,13 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
actor.Tell(new ShelveAlarmCommand("alarm-1", "op-zoe", ShelveKind.Timed, UnshelveAtUtc: null, Comment: null, CorrelationId.NewId()));
var reply = ExpectMsg<ShelveAlarmResult>(TimeSpan.FromSeconds(3));
var reply = ExpectMsg<ShelveAlarmResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Ok.ShouldBeFalse();
reply.Message.ShouldNotBeNull();
reply.Message.ShouldContain("UnshelveAtUtc");
// No command should have been published.
topicProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
topicProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
}
/// <summary>Verifies that starting a deployment inserts a row and dispatches to the coordinator.</summary>
@@ -156,11 +156,11 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
var dispatch = coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3));
var dispatch = coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
dispatch.DeploymentId.Value.ShouldNotBe(Guid.Empty);
dispatch.RevisionHash.Value.Length.ShouldBe(64);
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
reply.DeploymentId.ShouldBe(dispatch.DeploymentId);
reply.RevisionHash.ShouldBe(dispatch.RevisionHash);
@@ -237,8 +237,8 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Rejected);
reply.Message.ShouldNotBeNull();
reply.Message.ShouldContain("UnsEffectiveNameCollision"); // the v3 rule's error code
@@ -275,8 +275,8 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Rejected);
reply.Message.ShouldNotBeNull();
reply.Message.ShouldContain("EquipmentIdNotDerived");
@@ -321,9 +321,9 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
// Still dispatches — the advisory is non-blocking.
coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3));
coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
reply.Message.ShouldNotBeNull();
reply.Message.ShouldContain("will compile");
@@ -360,9 +360,9 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3));
coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
// No compiled scripts → no advisory emitted.
(reply.Message is null || !reply.Message.Contains("will compile")).ShouldBeTrue();
@@ -396,9 +396,9 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3));
coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
reply.Message.ShouldNotBeNull();
reply.Message.ShouldContain("will compile");
@@ -454,8 +454,8 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Rejected);
reply.Message.ShouldNotBeNull();
reply.Message.ShouldContain("ClusterEnabledNodeCountMismatch");
@@ -511,9 +511,9 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3));
coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
(reply.Message is null || !reply.Message.Contains("ClusterEnabledNodeCountMismatch")).ShouldBeTrue();
(reply.Message is null || !reply.Message.Contains("ClusterRedundancyModeInvalid")).ShouldBeTrue();
@@ -544,8 +544,8 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
reply.Outcome.ShouldBe(StartDeploymentOutcome.AnotherDeploymentInFlight);
reply.DeploymentId.ShouldNotBeNull();
}
@@ -35,7 +35,7 @@ public sealed class AuditWriterActorRejectedRowTests : ControlPlaneActorTestBase
Watch(actor);
actor.Tell(PoisonPill.Instance); // PostStop flushes
ExpectTerminated(actor);
ExpectTerminated(actor, cancellationToken: TestContext.Current.CancellationToken);
using var db = rejecting.CreateDbContext();
var persisted = db.ConfigAuditLogs.Select(r => r.EventId).ToList();
@@ -71,7 +71,7 @@ public sealed class AuditWriterActorRejectedRowTests : ControlPlaneActorTestBase
foreach (var id in ids) actor.Tell(NewEvent(id));
Watch(actor);
actor.Tell(PoisonPill.Instance);
ExpectTerminated(actor);
ExpectTerminated(actor, cancellationToken: TestContext.Current.CancellationToken);
using var db = factory.CreateDbContext();
var persisted = db.ConfigAuditLogs.Select(r => r.EventId).ToList();
@@ -99,7 +99,7 @@ public sealed class AuditWriterActorRejectedRowTests : ControlPlaneActorTestBase
foreach (var id in ids) actor.Tell(NewEvent(id));
Watch(actor);
actor.Tell(PoisonPill.Instance);
ExpectTerminated(actor, PresenceBudget);
ExpectTerminated(actor, PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
using var db = sql.NewContext();
var persisted = db.ConfigAuditLogs.Select(r => r.EventId).ToList();
@@ -120,7 +120,7 @@ public sealed class AuditWriterActorRejectedRowTests : ControlPlaneActorTestBase
for (var i = 0; i < 5; i++) actor.Tell(NewEvent(Guid.NewGuid()));
Watch(actor);
actor.Tell(PoisonPill.Instance);
ExpectTerminated(actor); // terminates ⇒ the retry loop did not spin
ExpectTerminated(actor, cancellationToken: TestContext.Current.CancellationToken); // terminates ⇒ the retry loop did not spin
using var db = rejecting.CreateDbContext();
db.ConfigAuditLogs.Count().ShouldBe(0);
@@ -39,7 +39,7 @@ public sealed class AuditWriterActorTests : ControlPlaneActorTestBase
{
using var db = dbFactory.CreateDbContext();
db.ConfigAuditLogs.Count().ShouldBe(AuditWriterActor.FlushBatchSize);
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>Verifies that duplicate event IDs within a batch are deduplicated in the buffer.</summary>
@@ -57,7 +57,7 @@ public sealed class AuditWriterActorTests : ControlPlaneActorTestBase
// Force a flush — send PoisonPill, which triggers PostStop → FlushBuffer.
Watch(actor);
actor.Tell(PoisonPill.Instance);
ExpectTerminated(actor);
ExpectTerminated(actor, cancellationToken: TestContext.Current.CancellationToken);
using var db = dbFactory.CreateDbContext();
db.ConfigAuditLogs.Count().ShouldBe(100, "in-buffer dedup should collapse duplicate EventIds");
@@ -76,7 +76,7 @@ public sealed class AuditWriterActorTests : ControlPlaneActorTestBase
Watch(actor);
actor.Tell(PoisonPill.Instance);
ExpectTerminated(actor);
ExpectTerminated(actor, cancellationToken: TestContext.Current.CancellationToken);
using var db = dbFactory.CreateDbContext();
db.ConfigAuditLogs.Count().ShouldBe(10);
@@ -94,7 +94,7 @@ public sealed class AuditWriterActorTests : ControlPlaneActorTestBase
Watch(actor);
actor.Tell(PoisonPill.Instance);
ExpectTerminated(actor);
ExpectTerminated(actor, cancellationToken: TestContext.Current.CancellationToken);
using var db = dbFactory.CreateDbContext();
var row = db.ConfigAuditLogs.Single();
@@ -127,7 +127,7 @@ public sealed class AuditWriterActorTests : ControlPlaneActorTestBase
Watch(actor);
actor.Tell(PoisonPill.Instance);
ExpectTerminated(actor);
ExpectTerminated(actor, cancellationToken: TestContext.Current.CancellationToken);
using var db = dbFactory.CreateDbContext();
var row = db.ConfigAuditLogs.Single();
@@ -146,7 +146,7 @@ public sealed class AuditWriterActorTests : ControlPlaneActorTestBase
var testRef = ActorOfAsTestActorRef<AuditWriterActor>(AuditWriterActor.Props(dbFactory));
IAuditWriter writer = testRef.UnderlyingActor;
var task = writer.WriteAsync(NewEvent(Guid.NewGuid(), action: "Published"));
var task = writer.WriteAsync(NewEvent(Guid.NewGuid(), action: "Published"), TestContext.Current.CancellationToken);
task.IsCompletedSuccessfully.ShouldBeTrue("WriteAsync must be best-effort and complete synchronously");
await Should.NotThrowAsync(async () => await task);
}
@@ -165,7 +165,7 @@ public sealed class AuditWriterActorTests : ControlPlaneActorTestBase
Watch(actor);
actor.Tell(PoisonPill.Instance);
ExpectTerminated(actor);
ExpectTerminated(actor, cancellationToken: TestContext.Current.CancellationToken);
using var db = dbFactory.CreateDbContext();
var row = db.ConfigAuditLogs.Single();
@@ -187,7 +187,7 @@ public sealed class AuditWriterActorTests : ControlPlaneActorTestBase
Watch(actor);
actor.Tell(PoisonPill.Instance);
ExpectTerminated(actor);
ExpectTerminated(actor, cancellationToken: TestContext.Current.CancellationToken);
using var db = dbFactory.CreateDbContext();
var row = db.ConfigAuditLogs.Single();
@@ -28,7 +28,7 @@ public sealed class ClusterNodeAddressReconcilerActorTests : ControlPlaneActorTe
SeedNode(dbFactory, "site-a-1:4053", enabled: true);
EventFilter.Warning(contains: "site-a-1:4053").ExpectOne(TimeSpan.FromSeconds(10), () =>
Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(dbFactory)));
Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(dbFactory)), TestContext.Current.CancellationToken);
}
/// <summary>
@@ -49,7 +49,7 @@ public sealed class ClusterNodeAddressReconcilerActorTests : ControlPlaneActorTe
dbFactory, sweepInterval: TimeSpan.FromMilliseconds(400)));
// Hold the filter open past several sweeps rather than returning immediately.
ExpectNoMsg(TimeSpan.FromSeconds(2.5));
});
}, TestContext.Current.CancellationToken);
}
/// <summary>
@@ -67,7 +67,7 @@ public sealed class ClusterNodeAddressReconcilerActorTests : ControlPlaneActorTe
Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(
dbFactory, sweepInterval: TimeSpan.FromMilliseconds(400)));
ExpectNoMsg(TimeSpan.FromSeconds(1.5));
});
}, TestContext.Current.CancellationToken);
}
/// <summary>
@@ -88,7 +88,7 @@ public sealed class ClusterNodeAddressReconcilerActorTests : ControlPlaneActorTe
Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(
dbFactory, ownClusterId: "MAIN", sweepInterval: TimeSpan.FromMilliseconds(400)));
ExpectNoMsg(TimeSpan.FromSeconds(1.5));
});
}, TestContext.Current.CancellationToken);
}
/// <summary>
@@ -103,7 +103,7 @@ public sealed class ClusterNodeAddressReconcilerActorTests : ControlPlaneActorTe
SeedNode(dbFactory, "main-1:4053", enabled: true, clusterId: "MAIN");
EventFilter.Warning(contains: "main-1:4053").ExpectOne(TimeSpan.FromSeconds(10), () =>
Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(dbFactory, ownClusterId: "MAIN")));
Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(dbFactory, ownClusterId: "MAIN")), TestContext.Current.CancellationToken);
}
/// <summary>
@@ -118,7 +118,7 @@ public sealed class ClusterNodeAddressReconcilerActorTests : ControlPlaneActorTe
SeedNode(dbFactory, "site-a-1:4053", enabled: true, clusterId: "SITE-A");
EventFilter.Warning(contains: "site-a-1:4053").ExpectOne(TimeSpan.FromSeconds(10), () =>
Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(dbFactory, ownClusterId: null)));
Sys.ActorOf(ClusterNodeAddressReconcilerActor.Props(dbFactory, ownClusterId: null)), TestContext.Current.CancellationToken);
}
private static void SeedNode(
@@ -3,7 +3,7 @@ using Akka.Actor;
using Akka.Cluster.Tools.PublishSubscribe;
using Akka.Event;
using Akka.TestKit;
using Akka.TestKit.Xunit2;
using Akka.TestKit.Xunit;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
@@ -86,7 +86,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
// without an explicit sender would ack to TestActor and this wait would hang.
DistributedPubSub.Get(Sys).Mediator.Tell(
new Subscribe(DeploymentsTopic, mediatorProbe.Ref), mediatorProbe.Ref);
mediatorProbe.ExpectMsg<SubscribeAck>(TimeSpan.FromSeconds(5));
mediatorProbe.ExpectMsg<SubscribeAck>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
var actor = Spawn(factory, new MeshTransportOptions());
var dispatch = new DispatchDeployment(
@@ -94,7 +94,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
actor.Tell(new MeshCommand(DeploymentsTopic, dispatch));
mediatorProbe.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(5)).ShouldBe(dispatch);
mediatorProbe.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).ShouldBe(dispatch);
}
[Fact]
@@ -114,7 +114,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
actor.Tell(ack, node.Ref);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5)).ShouldBe(ack);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).ShouldBe(ack);
// Forward, not Tell: the coordinator must see the node, not this relay.
coordinator.LastSender.ShouldBe(node.Ref);
}
@@ -133,7 +133,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
new CorrelationId(Guid.NewGuid()));
EventFilter.Warning(contains: "not resolvable").ExpectOne(TimeSpan.FromSeconds(5), () =>
actor.Tell(ack));
actor.Tell(ack), TestContext.Current.CancellationToken);
}
[Fact]
@@ -147,11 +147,11 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
// fail with two matches instead of one.
EventFilter.Warning(contains: "contact set was NOT refreshed").ExpectOne(
TimeSpan.FromSeconds(5),
() => actor.Tell(new Status.Failure(new InvalidOperationException("db down"))));
() => actor.Tell(new Status.Failure(new InvalidOperationException("db down"))), TestContext.Current.CancellationToken);
// Still serving: a transient DB outage must not take the command boundary down with it.
actor.Tell(new MeshCommand(DeploymentsTopic, "anything"));
ExpectNoMsg(TimeSpan.FromMilliseconds(200));
ExpectNoMsg(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
}
// ------------------------------------------------------------------ Mode = Grpc
@@ -177,7 +177,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch()));
AwaitCondition(() => rig.Dialer.Calls.Count == 3, TimeSpan.FromSeconds(5));
AwaitCondition(() => rig.Dialer.Calls.Count == 3, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
rig.Dialer.Calls.Select(c => c.Endpoint).ShouldBe(
new[] { "http://host-a:4056", "http://host-b:4056", "http://host-c:4056" }, ignoreOrder: true);
@@ -198,15 +198,15 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
var mediatorProbe = CreateTestProbe();
DistributedPubSub.Get(Sys).Mediator.Tell(
new Subscribe(DeploymentsTopic, mediatorProbe.Ref), mediatorProbe.Ref);
mediatorProbe.ExpectMsg<SubscribeAck>(TimeSpan.FromSeconds(5));
mediatorProbe.ExpectMsg<SubscribeAck>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
var rig = SpawnGrpcRig(factory);
AwaitContactsLoaded(rig);
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch()));
AwaitCondition(() => rig.Dialer.Calls.Count == 1, TimeSpan.FromSeconds(5));
mediatorProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
AwaitCondition(() => rig.Dialer.Calls.Count == 1, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
mediatorProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
}
[Fact]
@@ -226,11 +226,11 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch()));
AwaitCondition(() => rig.Dialer.Calls.Count == 1, TimeSpan.FromSeconds(5));
AwaitCondition(() => rig.Dialer.Calls.Count == 1, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
rig.Dialer.Calls[0].Endpoint.ShouldContain("host-a");
// And no late arrival from the two excluded rows.
ExpectNoMsg(TimeSpan.FromMilliseconds(300));
ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
rig.Dialer.Calls.Count.ShouldBe(1);
}
@@ -248,13 +248,13 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
GrpcRig? rig = null;
EventFilter.Warning(contains: "has no GrpcPort").ExpectOne(
TimeSpan.FromSeconds(5), () => rig = SpawnGrpcRig(factory));
TimeSpan.FromSeconds(5), () => rig = SpawnGrpcRig(factory), TestContext.Current.CancellationToken);
rig.ShouldNotBeNull();
AwaitContactsLoaded(rig);
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch()));
AwaitCondition(() => rig.Dialer.Calls.Count == 1, TimeSpan.FromSeconds(5));
AwaitCondition(() => rig.Dialer.Calls.Count == 1, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
rig.Dialer.Calls[0].Endpoint.ShouldBe("http://host-b:4056");
}
@@ -273,11 +273,11 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
SetGrpcPort(factory, "host-a:4053", 4056);
rig.Actor.Tell(new CentralCommunicationActor.RefreshContacts());
AwaitCondition(
() => rig.Psk.KnownClusterPublications.Count >= 2, TimeSpan.FromSeconds(5));
() => rig.Psk.KnownClusterPublications.Count >= 2, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
SetGrpcPort(factory, "host-a:4053", null);
EventFilter.Warning(contains: "has no GrpcPort").ExpectOne(TimeSpan.FromSeconds(5), () =>
rig.Actor.Tell(new CentralCommunicationActor.RefreshContacts()));
rig.Actor.Tell(new CentralCommunicationActor.RefreshContacts()), TestContext.Current.CancellationToken);
}
[Fact]
@@ -302,14 +302,14 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
// The node id, not the endpoint: an operator reading the log is looking at ClusterNode rows.
EventFilter.Warning(contains: "host-a:4053").ExpectOne(TimeSpan.FromSeconds(5), () =>
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch())));
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch())), TestContext.Current.CancellationToken);
// Both were attempted; the healthy one is unaffected by its neighbour's failure.
AwaitCondition(() => rig.Dialer.Calls.Count == 2, TimeSpan.FromSeconds(5));
AwaitCondition(() => rig.Dialer.Calls.Count == 2, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
// Still serving after a per-node failure — no supervision restart, no state loss.
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch()));
AwaitCondition(() => rig.Dialer.Calls.Count == 4, TimeSpan.FromSeconds(5));
AwaitCondition(() => rig.Dialer.Calls.Count == 4, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
}
[Fact]
@@ -330,9 +330,9 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
AwaitContactsLoaded(rig);
EventFilter.Warning(contains: "Unauthenticated").ExpectOne(TimeSpan.FromSeconds(5), () =>
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch())));
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch())), TestContext.Current.CancellationToken);
AwaitCondition(() => rig.Dialer.Calls.Count == 2, TimeSpan.FromSeconds(5));
AwaitCondition(() => rig.Dialer.Calls.Count == 2, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
}
[Fact]
@@ -343,7 +343,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
AwaitContactsLoaded(rig);
EventFilter.Warning(contains: "dropping").ExpectOne(TimeSpan.FromSeconds(5), () =>
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch())));
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch())), TestContext.Current.CancellationToken);
rig.Dialer.Calls.ShouldBeEmpty();
}
@@ -359,13 +359,13 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
AwaitContactsLoaded(rig);
EventFilter.Error(contains: "Cannot encode").ExpectOne(TimeSpan.FromSeconds(5), () =>
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, "not a command record")));
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, "not a command record")), TestContext.Current.CancellationToken);
rig.Dialer.Calls.ShouldBeEmpty();
// Still serving.
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch()));
AwaitCondition(() => rig.Dialer.Calls.Count == 1, TimeSpan.FromSeconds(5));
AwaitCondition(() => rig.Dialer.Calls.Count == 1, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
}
[Fact]
@@ -384,7 +384,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, new DispatchDeployment(
new DeploymentId(Guid.NewGuid()), new RevisionHash("abc"), correlationId)));
AwaitCondition(() => rig.Dialer.Calls.Count == 2, TimeSpan.FromSeconds(5));
AwaitCondition(() => rig.Dialer.Calls.Count == 2, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
rig.Dialer.Calls.ShouldAllBe(c => c.Envelope.CorrelationId == correlationId.ToString());
// One envelope object, encoded once and fanned — not re-encoded per node.
rig.Dialer.Calls.Select(c => c.Envelope).Distinct().Count().ShouldBe(1);
@@ -408,7 +408,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
rig.Actor.Tell(new MeshCommand(
"alarms", new AlarmCommand("alarm-1", "Acknowledge", "operator", null, null)));
AwaitCondition(() => rig.Dialer.Calls.Count == 2, TimeSpan.FromSeconds(5));
AwaitCondition(() => rig.Dialer.Calls.Count == 2, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
rig.Dialer.Calls.Select(c => c.Envelope.CorrelationId).Distinct().Count().ShouldBe(1);
rig.Dialer.Calls[0].Envelope.CorrelationId.ShouldNotBeNullOrWhiteSpace();
rig.Dialer.Calls[0].Envelope.AlarmCommand.AlarmId.ShouldBe("alarm-1");
@@ -461,7 +461,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
new CorrelationId(Guid.NewGuid()));
rig.Actor.Tell(ack);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5)).ShouldBe(ack);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).ShouldBe(ack);
}
finally
{
@@ -502,11 +502,11 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
rig.Actor.Tell(new CentralCommunicationActor.RefreshContacts());
AwaitCondition(
() => rig.Psk.KnownClusterPublications.Count >= 2, TimeSpan.FromSeconds(5));
() => rig.Psk.KnownClusterPublications.Count >= 2, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch()));
AwaitCondition(() => rig.Dialer.Calls.Count == 2, TimeSpan.FromSeconds(5));
AwaitCondition(() => rig.Dialer.Calls.Count == 2, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
rig.Dialer.Calls.Select(c => c.Endpoint).ShouldBe(
new[] { "http://host-a:4056", "http://host-b:4056" }, ignoreOrder: true);
@@ -540,7 +540,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
() => psk));
AwaitCondition(
() => psk.KnownClusterPublications.Count >= 1, TimeSpan.FromSeconds(5));
() => psk.KnownClusterPublications.Count >= 1, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
psk.KnownClusterPublications[0].ShouldBe(new[] { "C1" });
psk.InvalidateAllCount.ShouldBeGreaterThanOrEqualTo(1);
}
@@ -565,8 +565,8 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch()));
AwaitCondition(() => rig.Dialer.Calls.Count == 1, TimeSpan.FromSeconds(5));
ExpectNoMsg(TimeSpan.FromMilliseconds(300));
AwaitCondition(() => rig.Dialer.Calls.Count == 1, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
rig.Dialer.Calls.Count.ShouldBe(1);
rig.Dialer.Calls[0].ClusterId.ShouldBe("C1");
}
@@ -589,7 +589,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
// Targets-before-gate, observed the only way it can be from outside the actor: the very next
// message finds the dispatch set already populated.
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch()));
AwaitCondition(() => rig.Dialer.Calls.Count == 1, TimeSpan.FromSeconds(5));
AwaitCondition(() => rig.Dialer.Calls.Count == 1, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
}
[Fact]
@@ -614,7 +614,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
// same absence at two severities.
EventFilter.Error(contains: "no command dialer is wired").ExpectOne(
TimeSpan.FromSeconds(5),
() => actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch())));
() => actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch())), TestContext.Current.CancellationToken);
}
// ------------------------------------------------------------------ PreStart resolve-race logging
@@ -673,7 +673,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
actor!.Tell(new MeshCommand(DeploymentsTopic, NewDispatch()));
return dialer.Calls.Count >= 1;
}, TimeSpan.FromSeconds(5));
});
}, TestContext.Current.CancellationToken);
// Both retries actually happened — this fact is not vacuously true because the factories
// never got a second chance to run.
@@ -731,7 +731,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
Sys.ActorOf(CentralCommunicationActor.Props(
factory, ModeOptions(mode), () => null, () => new RecordingDialer(), PskFactory));
AwaitCondition(() => psk.KnownClusterPublications.Count >= 1, TimeSpan.FromSeconds(10));
});
}, TestContext.Current.CancellationToken);
}
[Fact]
@@ -744,9 +744,9 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
EventFilter.Warning().Expect(2, TimeSpan.FromSeconds(10), () =>
Sys.ActorOf(CentralCommunicationActor.Props(
factory, GrpcMode(), () => null, () => new RecordingDialer(), () => psk)));
factory, GrpcMode(), () => null, () => new RecordingDialer(), () => psk)), TestContext.Current.CancellationToken);
AwaitCondition(() => psk.KnownClusterPublications.Count >= 1, TimeSpan.FromSeconds(10));
AwaitCondition(() => psk.KnownClusterPublications.Count >= 1, TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
}
[Fact]
@@ -778,25 +778,25 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
// A mailbox barrier also waits for PreStart to register the zero-delay periodic tick.
var marker = new object();
actor.Tell(marker);
barrier.ExpectMsg<UnhandledMessage>(TimeSpan.FromSeconds(5)).Message.ShouldBeSameAs(marker);
barrier.ExpectMsg<UnhandledMessage>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).Message.ShouldBeSameAs(marker);
scheduler.Advance(TimeSpan.Zero);
loads.ExpectMsg(1, TimeSpan.FromSeconds(5));
local.AwaitCondition(() => Volatile.Read(ref calls) == 2, TimeSpan.FromSeconds(5));
loads.ExpectMsg(1, TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
local.AwaitCondition(() => Volatile.Read(ref calls) == 2, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
// This mailbox marker is handled after the failed ContactsLoaded handler returns.
// Unlike observing the provider call alone, it proves retry registration is complete.
actor.Tell(marker);
barrier.ExpectMsg<UnhandledMessage>(TimeSpan.FromSeconds(5)).Message.ShouldBeSameAs(marker);
barrier.ExpectMsg<UnhandledMessage>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).Message.ShouldBeSameAs(marker);
local.EventFilter.Info(contains: "Known-cluster gate published").ExpectOne(
TimeSpan.FromSeconds(5), () => actor.Tell(new CentralCommunicationActor.RefreshContacts()));
loads.ExpectMsg(2, TimeSpan.FromSeconds(5));
TimeSpan.FromSeconds(5), () => actor.Tell(new CentralCommunicationActor.RefreshContacts()), TestContext.Current.CancellationToken);
loads.ExpectMsg(2, TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
// Cross the pending retry deadline, leaving the 60-second periodic tick untouched.
// LoadContactsFromDb uses Task.Run, so observe its boundary for the original absence
// budget rather than asserting before a mistakenly retained retry can start its read.
scheduler.Advance(TimeSpan.FromMilliseconds(2500));
loads.ExpectNoMsg(TimeSpan.FromMilliseconds(2500));
loads.ExpectNoMsg(TimeSpan.FromMilliseconds(2500), TestContext.Current.CancellationToken);
factory.Calls.ShouldBe(2);
psk.KnownClusterPublications.Count.ShouldBe(1);
}
@@ -810,7 +810,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
factory, GrpcMode(), () => null, () => new RecordingDialer(), () => new RecordingPskProvider(),
bootstrapRetryInterval: TimeSpan.FromMilliseconds(20)));
EventFilter.Warning().Expect(32, TimeSpan.FromSeconds(10), () => { });
EventFilter.Warning().Expect(32, TimeSpan.FromSeconds(10), () => { }, TestContext.Current.CancellationToken);
Thread.Sleep(1500);
// 1 initial + 15 retries; the 60 s periodic tick has not fired again.
factory.Calls.ShouldBe(16);
@@ -854,7 +854,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
// ran and PublishKnownClusters's property access performed the first-use retry — not
// merely that the actor was constructed.
AwaitCondition(() => pskCalls >= 2, TimeSpan.FromSeconds(5));
});
}, TestContext.Current.CancellationToken);
// The bootstrap retry is BOUNDED: with a 50 ms interval and a budget of 15 it has long
// finished, so the call count plateaus instead of growing with elapsed time.
@@ -873,7 +873,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
{
actor.Tell(new CentralCommunicationActor.RefreshContacts());
AwaitCondition(() => pskCalls > pskCallsAfterFirstLoad, TimeSpan.FromSeconds(5));
});
}, TestContext.Current.CancellationToken);
}
[Fact]
@@ -904,15 +904,15 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
// Proof PreStart's resolve attempt and the first contact load it schedules have
// actually run, so this block's silence is not vacuous.
AwaitCondition(() => psk.KnownClusterPublications.Count >= 1, TimeSpan.FromSeconds(5));
});
}, TestContext.Current.CancellationToken);
EventFilter.Error(contains: "no command dialer is wired").ExpectOne(TimeSpan.FromSeconds(5), () =>
actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch())));
actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch())), TestContext.Current.CancellationToken);
// A SECOND dispatch must ALSO log it: this signal is per-dispatch and unguarded, the
// opposite shape from the PSK provider's once-per-incarnation guard above.
EventFilter.Error(contains: "no command dialer is wired").ExpectOne(TimeSpan.FromSeconds(5), () =>
actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch())));
actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch())), TestContext.Current.CancellationToken);
}
private static void SetGrpcPort(
@@ -943,7 +943,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
AwaitContactsLoaded(rig);
// The dialer is resolved lazily on first use; a dispatch forces that, and the NEXT refresh prunes.
rig.Actor.Tell(new MeshCommand(DeploymentsTopic, NewDispatch()));
AwaitCondition(() => rig.Dialer.Calls.Count == 2, TimeSpan.FromSeconds(5));
AwaitCondition(() => rig.Dialer.Calls.Count == 2, TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
using (var db = factory.CreateDbContext())
{
@@ -954,7 +954,7 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
AwaitCondition(
() => rig.Dialer.RetainCalls.Any(set => set.Count == 1 && set.Single().Contains("host-a")),
TimeSpan.FromSeconds(5));
TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
rig.Dialer.RetainCalls.Last().ShouldBe(new[] { "http://host-a:4056" });
}
@@ -75,7 +75,7 @@ public sealed class ControlPlaneStarterOrderTests
}
finally
{
await host.StopAsync();
await host.StopAsync(TestContext.Current.CancellationToken);
}
}
@@ -101,16 +101,16 @@ public sealed class ControlPlaneStarterOrderTests
// StartAsync at which a synchronous assertion is correct.
using var host = BuildAdminHost(mode, useSignallingProvider: true);
await host.StartAsync();
await host.StartAsync(TestContext.Current.CancellationToken);
try
{
var signal = (SignallingPskProvider)host.Services.GetRequiredService<IMeshPskProvider>();
await signal.Published.WaitAsync(TimeSpan.FromSeconds(20));
await signal.Published.WaitAsync(TimeSpan.FromSeconds(20), TestContext.Current.CancellationToken);
}
finally
{
await host.StopAsync();
await host.StopAsync(TestContext.Current.CancellationToken);
}
}
@@ -51,7 +51,7 @@ public sealed class MeshPskProviderRefreshTests
resolver.Value = "new";
resolver.Hold();
var refresh = provider.RefreshAsync(new[] { "SITE-A" }, CancellationToken.None);
resolver.Entered.Wait(TimeSpan.FromSeconds(5)).ShouldBeTrue();
resolver.Entered.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken).ShouldBeTrue();
// Mid-refresh: the old key, from memory — the store is NOT re-read on this path.
var callsMidRefresh = resolver.Calls;
@@ -307,7 +307,7 @@ public sealed class MeshPskProviderTests
return psk;
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default)).ToArray();
arrived.Wait(TimeSpan.FromSeconds(10)).ShouldBeTrue("all callers should reach the start gate");
arrived.Wait(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken).ShouldBeTrue("all callers should reach the start gate");
start.Set();
var results = await Task.WhenAll(callers);
@@ -38,7 +38,7 @@ public class MeshRouterDispatchTests : ControlPlaneActorTestBase
coordinator.Tell(new Commons.Messages.Deploy.DispatchDeployment(
deploymentId, new RevisionHash("rev-1"), CorrelationId.NewId()));
var cmd = router.ExpectMsg<MeshCommand>(PresenceBudget);
var cmd = router.ExpectMsg<MeshCommand>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
cmd.Topic.ShouldBe(DeploymentsTopic);
cmd.Message.ShouldBeOfType<Commons.Messages.Deploy.DispatchDeployment>();
}
@@ -49,7 +49,7 @@ public class MeshRouterDispatchTests : ControlPlaneActorTestBase
var subscriber = CreateTestProbe();
DistributedPubSub.Get(Sys).Mediator.Tell(
new Subscribe(DeploymentsTopic, subscriber.Ref), subscriber.Ref);
subscriber.ExpectMsg<SubscribeAck>(PresenceBudget);
subscriber.ExpectMsg<SubscribeAck>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
var coordinator = Sys.ActorOf(ConfigPublishCoordinator.Props(NewInMemoryDbFactory()));
@@ -59,7 +59,7 @@ public class MeshRouterDispatchTests : ControlPlaneActorTestBase
// Presence budget, not 5 s: the coordinator's first ConfigDb (EF InMemory) read sits in front
// of the dispatch and is a cold multi-second cost on a loaded CI runner (#606).
subscriber.ExpectMsg<Commons.Messages.Deploy.DispatchDeployment>(PresenceBudget);
subscriber.ExpectMsg<Commons.Messages.Deploy.DispatchDeployment>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
}
[Fact]
@@ -75,7 +75,7 @@ public class MeshRouterDispatchTests : ControlPlaneActorTestBase
admin.Tell(new RestartDriver("C1", "driver-1", "alice", Guid.NewGuid()));
var cmd = router.ExpectMsg<MeshCommand>(PresenceBudget);
var cmd = router.ExpectMsg<MeshCommand>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
cmd.Topic.ShouldBe(DriverControlTopic.Name);
cmd.Message.ShouldBeOfType<RestartDriver>();
}
@@ -93,7 +93,7 @@ public class MeshRouterDispatchTests : ControlPlaneActorTestBase
admin.Tell(new ReconnectDriver("C1", "driver-1", "alice", Guid.NewGuid()));
var cmd = router.ExpectMsg<MeshCommand>(PresenceBudget);
var cmd = router.ExpectMsg<MeshCommand>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
cmd.Topic.ShouldBe(DriverControlTopic.Name);
cmd.Message.ShouldBeOfType<ReconnectDriver>();
}
@@ -111,7 +111,7 @@ public class MeshRouterDispatchTests : ControlPlaneActorTestBase
admin.Tell(new AcknowledgeAlarmCommand("alarm-1", "alice", "c", CorrelationId.NewId()));
var cmd = router.ExpectMsg<MeshCommand>(PresenceBudget);
var cmd = router.ExpectMsg<MeshCommand>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
cmd.Topic.ShouldBe(AlarmCommandsTopic.Name);
cmd.Message.ShouldBeOfType<AlarmCommand>();
}
@@ -129,7 +129,7 @@ public class MeshRouterDispatchTests : ControlPlaneActorTestBase
admin.Tell(new ShelveAlarmCommand("alarm-1", "alice", ShelveKind.OneShot, null, "c", CorrelationId.NewId()));
var cmd = router.ExpectMsg<MeshCommand>(PresenceBudget);
var cmd = router.ExpectMsg<MeshCommand>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
cmd.Topic.ShouldBe(AlarmCommandsTopic.Name);
cmd.Message.ShouldBeOfType<AlarmCommand>();
}
@@ -32,20 +32,20 @@ public sealed class ConfigComposerSqlServerHashStabilityTests
// Collation order a-2, B-1, c-3; ordinal order B-1, a-2, c-3.
foreach (var id in new[] { "c-3", "B-1", "a-2" })
seed.Scripts.Add(new Script { ScriptId = id, Name = id, SourceCode = "return 1;", SourceHash = id });
await seed.SaveChangesAsync();
await seed.SaveChangesAsync(TestContext.Current.CancellationToken);
}
await using var legacyDb = sql.NewContext();
var legacy = await LegacySnapshotAndFlattenAsync(legacyDb);
await using var viaDb = sql.NewContext();
var current = await ConfigComposer.SnapshotAndFlattenAsync(viaDb);
var current = await ConfigComposer.SnapshotAndFlattenAsync(viaDb, TestContext.Current.CancellationToken);
current.Blob.ShouldBe(legacy.Blob);
current.RevisionHash.ShouldBe(legacy.RevisionHash);
// The deploy path's form (validate + compose from ONE snapshot) must agree too.
await using var deployDb = sql.NewContext();
var viaSnapshot = ConfigComposer.Compose(await DraftSnapshotFactory.FromConfigDbAsync(deployDb));
var viaSnapshot = ConfigComposer.Compose(await DraftSnapshotFactory.FromConfigDbAsync(deployDb, TestContext.Current.CancellationToken));
viaSnapshot.RevisionHash.ShouldBe(legacy.RevisionHash);
}
@@ -20,10 +20,10 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase
var f = NewInMemoryDbFactory();
await using var db1 = f.CreateDbContext();
var a1 = await ConfigComposer.SnapshotAndFlattenAsync(db1);
var a1 = await ConfigComposer.SnapshotAndFlattenAsync(db1, TestContext.Current.CancellationToken);
await using var db2 = f.CreateDbContext();
var a2 = await ConfigComposer.SnapshotAndFlattenAsync(db2);
var a2 = await ConfigComposer.SnapshotAndFlattenAsync(db2, TestContext.Current.CancellationToken);
a1.RevisionHash.ShouldBe(a2.RevisionHash);
a1.Blob.ShouldBe(a2.Blob);
@@ -40,9 +40,9 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase
{
db.ServerClusters.Add(NewCluster("cluster-a"));
db.ServerClusters.Add(NewCluster("cluster-b"));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var hashAB = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext())).RevisionHash;
var hashAB = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken)).RevisionHash;
// Fresh DB, same rows in reverse insertion order.
var f2 = NewInMemoryDbFactory();
@@ -50,9 +50,9 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase
{
db.ServerClusters.Add(NewCluster("cluster-b"));
db.ServerClusters.Add(NewCluster("cluster-a"));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var hashBA = (await ConfigComposer.SnapshotAndFlattenAsync(f2.CreateDbContext())).RevisionHash;
var hashBA = (await ConfigComposer.SnapshotAndFlattenAsync(f2.CreateDbContext(), TestContext.Current.CancellationToken)).RevisionHash;
hashAB.ShouldBe(hashBA);
}
@@ -65,16 +65,16 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase
await using (var db = f.CreateDbContext())
{
db.ServerClusters.Add(NewCluster("cluster-a"));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var hashA = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext())).RevisionHash;
var hashA = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken)).RevisionHash;
await using (var db = f.CreateDbContext())
{
db.ServerClusters.Add(NewCluster("cluster-b"));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var hashAB = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext())).RevisionHash;
var hashAB = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken)).RevisionHash;
hashAB.ShouldNotBe(hashA);
}
@@ -84,7 +84,7 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase
public async Task Hash_is_64_lowercase_hex_chars()
{
var f = NewInMemoryDbFactory();
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext());
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken);
artifact.RevisionHash.Length.ShouldBe(64);
artifact.RevisionHash.ShouldMatch("^[0-9a-f]{64}$");
}
@@ -102,10 +102,10 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase
await using (var db = f.CreateDbContext())
{
db.ServerClusters.Add(NewCluster("c1"));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext());
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken);
artifact.Blob[0].ShouldBe((byte)0x1F);
artifact.Blob[1].ShouldBe((byte)0x8B);
@@ -118,7 +118,7 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase
using var inflated = new MemoryStream();
await using (var gz = new GZipStream(new MemoryStream(artifact.Blob), CompressionMode.Decompress))
{
await gz.CopyToAsync(inflated);
await gz.CopyToAsync(inflated, TestContext.Current.CancellationToken);
}
var json = inflated.ToArray();
Encoding.UTF8.GetString(json).ShouldStartWith("{");
@@ -155,11 +155,11 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase
DeviceId = "dev-1", DriverInstanceId = "drv-1",
Name = "dev-1", DeviceConfig = "{\"HostAddress\":\"10.9.9.9:8193\"}",
});
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
await using var readDb = f.CreateDbContext();
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(readDb);
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(readDb, TestContext.Current.CancellationToken);
var specs = DeploymentArtifact.ParseDriverInstances(artifact.Blob);
var spec = specs.ShouldHaveSingleItem();
@@ -208,11 +208,11 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase
DriverInstanceId = "drv-2", ClusterId = "c1",
Name = "Modbus2", DriverType = "Modbus", DriverConfig = "{}",
});
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
await using var readDb = f.CreateDbContext();
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(readDb);
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(readDb, TestContext.Current.CancellationToken);
var specs = DeploymentArtifact.ParseDriverInstances(artifact.Blob);
var withOverride = specs.Single(s => s.DriverInstanceId == "drv-1");
@@ -241,11 +241,11 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase
ApiKeyId = "k1", Name = "scada", SecretHash = "h", Principal = "scadabridge", CreatedUtc = FixedTimestamp,
});
db.Scripts.Add(new Script { ScriptId = "s-1", Name = "s", SourceCode = "return 1;", SourceHash = "x" });
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var viaDb = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext());
var snapshot = await Configuration.Validation.DraftSnapshotFactory.FromConfigDbAsync(f.CreateDbContext());
var viaDb = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken);
var snapshot = await Configuration.Validation.DraftSnapshotFactory.FromConfigDbAsync(f.CreateDbContext(), TestContext.Current.CancellationToken);
var viaSnapshot = ConfigComposer.Compose(snapshot);
viaSnapshot.RevisionHash.ShouldBe(viaDb.RevisionHash);
@@ -60,7 +60,7 @@ public sealed class ConfigPublishCoordinatorDbSourcedAcksTests : ControlPlaneAct
using var db = dbFactory.CreateDbContext();
db.NodeDeploymentStates.Select(s => s.NodeId).ShouldBe(new[] { "site-a-1:4053" });
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.AwaitingApplyAcks);
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
actor.Tell(new ApplyAck(deploymentId, NodeId.Parse("site-a-1:4053"),
ApplyAckOutcome.Applied, null, CorrelationId.NewId()));
@@ -69,7 +69,7 @@ public sealed class ConfigPublishCoordinatorDbSourcedAcksTests : ControlPlaneAct
{
using var db = dbFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed);
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>
@@ -94,7 +94,7 @@ public sealed class ConfigPublishCoordinatorDbSourcedAcksTests : ControlPlaneAct
{
using var db = dbFactory.CreateDbContext();
db.NodeDeploymentStates.Select(s => s.NodeId).ShouldBe(new[] { "site-a-1:4053" });
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
// ...and the enabled node's ack alone seals it. If the disabled row were expected this would
// sit at AwaitingApplyAcks until the deadline.
@@ -105,7 +105,7 @@ public sealed class ConfigPublishCoordinatorDbSourcedAcksTests : ControlPlaneAct
{
using var db = dbFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed);
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>
@@ -126,13 +126,13 @@ public sealed class ConfigPublishCoordinatorDbSourcedAcksTests : ControlPlaneAct
{
using var db = dbFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.AwaitingApplyAcks);
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
// A node nobody registered acks. One expected node, one ack — if it were counted, the
// coordinator would seal here.
actor.Tell(new ApplyAck(deploymentId, NodeId.Parse("ghost-9:4053"),
ApplyAckOutcome.Applied, null, CorrelationId.NewId()));
ExpectNoMsg(TimeSpan.FromMilliseconds(400));
ExpectNoMsg(TimeSpan.FromMilliseconds(400), TestContext.Current.CancellationToken);
using (var db = dbFactory.CreateDbContext())
{
@@ -147,7 +147,7 @@ public sealed class ConfigPublishCoordinatorDbSourcedAcksTests : ControlPlaneAct
{
using var db = dbFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed);
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>
@@ -170,7 +170,7 @@ public sealed class ConfigPublishCoordinatorDbSourcedAcksTests : ControlPlaneAct
using var db = dbFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed);
db.NodeDeploymentStates.ShouldBeEmpty();
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>
@@ -202,7 +202,7 @@ public sealed class ConfigPublishCoordinatorDbSourcedAcksTests : ControlPlaneAct
{
using var db = dbFactory.CreateDbContext();
db.NodeDeploymentStates.Select(s => s.NodeId).ShouldBe(new[] { "site-a-1:4053" });
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
actor.Tell(new ApplyAck(deploymentId, NodeId.Parse("site-a-1:4053"),
ApplyAckOutcome.Applied, null, CorrelationId.NewId()));
@@ -211,7 +211,7 @@ public sealed class ConfigPublishCoordinatorDbSourcedAcksTests : ControlPlaneAct
{
using var db = dbFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed);
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
}
private static void SeedNodes(
@@ -46,7 +46,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
using var db = dbFactory.CreateDbContext();
db.NodeDeploymentStates.Select(s => s.NodeId).ShouldBe(new[] { "site-a-1:4053" });
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.AwaitingApplyAcks);
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
actor.Tell(new ApplyAck(deploymentId, NodeId.Parse("site-a-1:4053"),
ApplyAckOutcome.Applied, null, CorrelationId.NewId()));
@@ -55,7 +55,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
{
using var db = dbFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed);
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>
@@ -79,7 +79,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
row.Status.ShouldBe(DeploymentStatus.TimedOut);
row.FailureReason.ShouldNotBeNull();
row.FailureReason.ShouldContain("lost");
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
// Positive control: the coordinator is not wedged — a fresh deployment runs end to end.
var nextId = SeedDeployment(dbFactory, DeploymentStatus.Dispatching, DateTime.UtcNow);
@@ -88,7 +88,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
{
using var db = dbFactory.CreateDbContext();
db.Deployments.Single(d => d.DeploymentId == nextId.Value).Status.ShouldBe(DeploymentStatus.AwaitingApplyAcks);
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
actor.Tell(new ApplyAck(nextId, NodeId.Parse("site-a-1:4053"),
ApplyAckOutcome.Applied, null, CorrelationId.NewId()));
@@ -96,7 +96,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
{
using var db = dbFactory.CreateDbContext();
db.Deployments.Single(d => d.DeploymentId == nextId.Value).Status.ShouldBe(DeploymentStatus.Sealed);
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>
@@ -116,7 +116,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
using var db = dbFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed);
db.NodeDeploymentStates.ShouldBeEmpty();
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>
@@ -139,10 +139,10 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
// Arm the fault AFTER PreStart has run (it would otherwise fault the recovery read itself and
// the actor would be stopped, not restarted). Identify is processed after creation, so a
// resolved selection proves PreStart completed.
await Sys.ActorSelection(coordinator.Path).ResolveOne(PresenceBudget);
await Sys.ActorSelection(coordinator.Path).ResolveOne(PresenceBudget, TestContext.Current.CancellationToken);
faulting.FailNext();
admin.Tell(new StartDeployment("joe", CorrelationId.NewId()));
var first = ExpectMsg<StartDeploymentResult>(PresenceBudget);
var first = ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
first.Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
first.DeploymentId.ShouldNotBeNull();
@@ -153,7 +153,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
using var db = adminFactory.CreateDbContext();
db.NodeDeploymentStates.Count(s => s.DeploymentId == first.DeploymentId.Value.Value).ShouldBe(1);
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.AwaitingApplyAcks);
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
coordinator.Tell(new ApplyAck(first.DeploymentId.Value, NodeId.Parse("site-a-1:4053"),
ApplyAckOutcome.Applied, null, CorrelationId.NewId()));
@@ -161,11 +161,11 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
{
using var db = adminFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed);
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
// Positive control: nothing is wedged — the next deploy is Accepted and seals too.
admin.Tell(new StartDeployment("joe", CorrelationId.NewId()));
var second = ExpectMsg<StartDeploymentResult>(PresenceBudget);
var second = ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
second.Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
coordinator.Tell(new ApplyAck(second.DeploymentId!.Value, NodeId.Parse("site-a-1:4053"),
ApplyAckOutcome.Applied, null, CorrelationId.NewId()));
@@ -173,7 +173,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
{
using var db = adminFactory.CreateDbContext();
db.Deployments.Count(d => d.Status == DeploymentStatus.Sealed).ShouldBe(2);
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>
@@ -189,14 +189,14 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
var dbFactory = NewInMemoryDbFactory();
SeedNodes(dbFactory, "site-a-1:4053");
var coordinator = Sys.ActorOf(ConfigPublishCoordinator.Props(dbFactory, NoTimeout));
await Sys.ActorSelection(coordinator.Path).ResolveOne(PresenceBudget); // PreStart saw nothing
await Sys.ActorSelection(coordinator.Path).ResolveOne(PresenceBudget, TestContext.Current.CancellationToken); // PreStart saw nothing
var stale = SeedDeployment(dbFactory, DeploymentStatus.Dispatching, DateTime.UtcNow.AddMinutes(-10));
var admin = Sys.ActorOf(AdminOperationsActor.Props(
dbFactory, coordinator, Enumerable.Empty<IDriverProbe>(), applyDeadline: NoTimeout));
admin.Tell(new StartDeployment("joe", CorrelationId.NewId()));
var result = ExpectMsg<StartDeploymentResult>(PresenceBudget);
var result = ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
result.Outcome.ShouldBe(StartDeploymentOutcome.Accepted, result.Message);
using (var db = dbFactory.CreateDbContext())
@@ -214,7 +214,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
using var db = dbFactory.CreateDbContext();
db.Deployments.Single(d => d.DeploymentId == result.DeploymentId.Value.Value).Status
.ShouldBe(DeploymentStatus.Sealed);
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>Control for the closure: the same row INSIDE the deadline is still in flight and still refuses.</summary>
@@ -228,7 +228,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
dbFactory, coordinator.Ref, Enumerable.Empty<IDriverProbe>(), applyDeadline: NoTimeout));
admin.Tell(new StartDeployment("joe", CorrelationId.NewId()));
var result = ExpectMsg<StartDeploymentResult>(PresenceBudget);
var result = ExpectMsg<StartDeploymentResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
result.Outcome.ShouldBe(StartDeploymentOutcome.AnotherDeploymentInFlight);
result.DeploymentId.ShouldBe(pending);
@@ -261,7 +261,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
{
using var db = dbFactory.CreateDbContext();
db.Deployments.Single(d => d.DeploymentId == live.Value).Status.ShouldBe(DeploymentStatus.AwaitingApplyAcks);
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
using var verify = dbFactory.CreateDbContext();
verify.Deployments.Single(d => d.DeploymentId == done.Value).Status.ShouldBe(terminal);
@@ -298,7 +298,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
{
using var db = sql.NewContext();
db.Deployments.AsNoTracking().Single().Status.ShouldBe(DeploymentStatus.AwaitingApplyAcks);
}, duration: PresenceBudget); // PreStart re-drives the un-dispatched row
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken); // PreStart re-drives the un-dispatched row
coordinator.Tell(new ApplyAck(id, NodeId.Parse("site-a-1:4053"), ApplyAckOutcome.Applied, null, CorrelationId.NewId()));
AwaitAssert(() =>
@@ -308,7 +308,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
row.Status.ShouldBe(DeploymentStatus.Sealed);
row.SealedAtUtc.ShouldNotBeNull();
row.ArtifactBlob.ShouldBe(blob);
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>
@@ -327,7 +327,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
SeedNodes(inner, "site-a-1:4053");
var interposer = new InterposingDbFactory(inner);
var coordinator = Sys.ActorOf(ConfigPublishCoordinator.Props(interposer, NoTimeout));
await Sys.ActorSelection(coordinator.Path).ResolveOne(PresenceBudget); // PreStart ran over an empty table
await Sys.ActorSelection(coordinator.Path).ResolveOne(PresenceBudget, TestContext.Current.CancellationToken); // PreStart ran over an empty table
// The race proper: a new Dispatching row whose dispatch is interleaved with an AdminOps-style closure.
var victim = SeedDeployment(inner, DeploymentStatus.Dispatching, DateTime.UtcNow);
@@ -339,7 +339,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
(int)DeploymentStatus.TimedOut, victim.Value);
});
coordinator.Tell(new DispatchDeployment(victim, TestRevision, CorrelationId.NewId()));
AwaitAssert(() => interposer.Fired.ShouldBeTrue(), duration: PresenceBudget);
AwaitAssert(() => interposer.Fired.ShouldBeTrue(), duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
// Positive control + ordering: a live row dispatched after it IS flipped, so the victim's dispatch was processed.
var live = SeedDeployment(inner, DeploymentStatus.Dispatching, DateTime.UtcNow);
@@ -349,7 +349,7 @@ public sealed class ConfigPublishCoordinatorRecoveryTests : ControlPlaneActorTes
using var db = sql.NewContext();
db.Deployments.AsNoTracking().Single(d => d.DeploymentId == live.Value).Status
.ShouldBe(DeploymentStatus.AwaitingApplyAcks);
}, duration: PresenceBudget);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
using var verify = sql.NewContext();
verify.Deployments.AsNoTracking().Single(d => d.DeploymentId == victim.Value).Status
@@ -30,7 +30,7 @@ public sealed class ConfigPublishCoordinatorTests : ControlPlaneActorTestBase
using var db = dbFactory.CreateDbContext();
var status = db.Deployments.Single().Status;
status.ShouldBe(DeploymentStatus.Sealed);
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>Verifies that stale apply acknowledgments arriving after seal are ignored.</summary>
@@ -48,14 +48,14 @@ public sealed class ConfigPublishCoordinatorTests : ControlPlaneActorTestBase
{
using var db = dbFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed);
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
// Now send a late ApplyAck for the just-sealed deployment. Should be a no-op — neither
// crash the actor nor modify the row. We give it a beat and re-check the status.
actor.Tell(new ApplyAck(deploymentId, NodeId.Parse("ghost-node"),
ApplyAckOutcome.Applied, null, CorrelationId.NewId()));
ExpectNoMsg(TimeSpan.FromMilliseconds(250));
ExpectNoMsg(TimeSpan.FromMilliseconds(250), TestContext.Current.CancellationToken);
using var db = dbFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed);
}
@@ -106,7 +106,7 @@ public sealed class ConfigPublishCoordinatorTests : ControlPlaneActorTestBase
ApplyAckOutcome.Applied, null, CorrelationId.NewId()));
// Give it time to process and potentially seal if buggy.
ExpectNoMsg(TimeSpan.FromMilliseconds(400));
ExpectNoMsg(TimeSpan.FromMilliseconds(400), TestContext.Current.CancellationToken);
using var db2 = dbFactory.CreateDbContext();
var status = db2.Deployments.Single().Status;
@@ -53,7 +53,7 @@ public sealed class ConfigPublishCoordinatorTimeoutTests : ControlPlaneActorTest
{
using var db = dbFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.TimedOut);
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>Verifies that stale DeadlineElapsed for other deployments is ignored.</summary>
@@ -68,7 +68,7 @@ public sealed class ConfigPublishCoordinatorTimeoutTests : ControlPlaneActorTest
actor.Tell(new ConfigPublishCoordinator.DeadlineElapsed(DeploymentId.NewId()));
// The seeded one should remain in its starting state (no transition triggered).
ExpectNoMsg(TimeSpan.FromMilliseconds(250));
ExpectNoMsg(TimeSpan.FromMilliseconds(250), TestContext.Current.CancellationToken);
using var db = dbFactory.CreateDbContext();
var status = db.Deployments.Single().Status;
status.ShouldBeOneOf(DeploymentStatus.Dispatching, DeploymentStatus.AwaitingApplyAcks, DeploymentStatus.Sealed);
@@ -108,7 +108,7 @@ public sealed class ConfigPublishCoordinatorTimeoutTests : ControlPlaneActorTest
{
using var db = dbFactory.CreateDbContext();
db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed);
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>
@@ -135,7 +135,7 @@ public sealed class ConfigPublishCoordinatorTimeoutTests : ControlPlaneActorTest
row.FailureReason.ShouldContain("No ack from: site-b-1:4053.");
row.FailureReason.ShouldContain("(1/2 landed)");
row.FailureReason.ShouldNotContain("site-a-1");
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>lmxopcua#784 — the timeout reason stays bounded on a large fleet: it names 20 nodes and counts the rest.</summary>
@@ -159,7 +159,7 @@ public sealed class ConfigPublishCoordinatorTimeoutTests : ControlPlaneActorTest
reason.ShouldNotContain("node-20-");
reason.ShouldContain("(and 5 more)");
reason.Length.ShouldBeLessThanOrEqualTo(2048);
}, duration: TimeSpan.FromSeconds(3));
}, duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
}
private static void SeedNodeRows(
@@ -33,10 +33,10 @@ public sealed class DataPlaneAuthorityArtifactTests : ControlPlaneActorTestBase
{
db.ServerClusters.Add(NewCluster("cluster-a"));
db.ApiKeys.Add(NewKey("abc123", "scadabridge-dcl"));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext());
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken);
var parsed = DeploymentArtifact.ParseDataPlaneAuthority(artifact.Blob, NodeA);
var key = parsed.Keys.ShouldHaveSingleItem();
@@ -58,10 +58,10 @@ public sealed class DataPlaneAuthorityArtifactTests : ControlPlaneActorTestBase
db.ServerClusters.Add(NewCluster("cluster-a"));
db.NodeAcls.Add(NewAcl("acl-1", "cluster-a", "scadabridge-dcl",
NodeAclScopeKind.Cluster, NodePermissions.Read | NodePermissions.WriteOperate));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext());
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken);
var parsed = DeploymentArtifact.ParseDataPlaneAuthority(artifact.Blob, NodeA);
var grant = parsed.Grants.ShouldHaveSingleItem();
@@ -82,10 +82,10 @@ public sealed class DataPlaneAuthorityArtifactTests : ControlPlaneActorTestBase
var acl = NewAcl("acl-1", "cluster-a", "p", NodeAclScopeKind.Equipment, NodePermissions.Read);
acl.ScopeId = "EQ-1";
db.NodeAcls.Add(acl);
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext());
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken);
var parsed = DeploymentArtifact.ParseDataPlaneAuthority(artifact.Blob, NodeA);
var grant = parsed.Grants.ShouldHaveSingleItem();
@@ -110,10 +110,10 @@ public sealed class DataPlaneAuthorityArtifactTests : ControlPlaneActorTestBase
var key = NewKey("abc123", "p");
key.ExpiresUtc = expires;
db.ApiKeys.Add(key);
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext());
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken);
var parsed = DeploymentArtifact.ParseDataPlaneAuthority(artifact.Blob, NodeA);
var parsedKey = parsed.Keys.ShouldHaveSingleItem();
@@ -139,10 +139,10 @@ public sealed class DataPlaneAuthorityArtifactTests : ControlPlaneActorTestBase
db.ApiKeys.Add(NewKey("key-b", "principal-b"));
db.NodeAcls.Add(NewAcl("acl-a", "cluster-a", "principal-a", NodeAclScopeKind.Cluster, NodePermissions.Read));
db.NodeAcls.Add(NewAcl("acl-b", "cluster-b", "principal-b", NodeAclScopeKind.Cluster, NodePermissions.Read));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext());
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken);
var parsed = DeploymentArtifact.ParseDataPlaneAuthority(artifact.Blob, NodeA);
parsed.ClusterId.ShouldBe("cluster-a");
@@ -162,10 +162,10 @@ public sealed class DataPlaneAuthorityArtifactTests : ControlPlaneActorTestBase
db.ServerClusters.Add(NewCluster("cluster-b"));
db.ClusterNodes.Add(NewNode(NodeB, "cluster-b"));
db.NodeAcls.Add(NewAcl("acl-b", "cluster-b", "principal-b", NodeAclScopeKind.Cluster, NodePermissions.Read));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext());
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken);
var parsed = DeploymentArtifact.ParseDataPlaneAuthority(artifact.Blob, "node-nowhere:4840");
parsed.Grants.ShouldBeEmpty();
@@ -184,10 +184,10 @@ public sealed class DataPlaneAuthorityArtifactTests : ControlPlaneActorTestBase
var key = NewKey("abc123", "p");
key.Enabled = false;
db.ApiKeys.Add(key);
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext());
var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken);
var parsed = DeploymentArtifact.ParseDataPlaneAuthority(artifact.Blob, NodeA);
parsed.Keys.ShouldHaveSingleItem().Enabled.ShouldBeFalse();
@@ -211,16 +211,16 @@ public sealed class DataPlaneAuthorityArtifactTests : ControlPlaneActorTestBase
await using (var db = f.CreateDbContext())
{
db.ServerClusters.Add(NewCluster("cluster-a"));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var before = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext())).RevisionHash;
var before = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken)).RevisionHash;
await using (var db = f.CreateDbContext())
{
db.ApiKeys.Add(NewKey("abc123", "p"));
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var after = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext())).RevisionHash;
var after = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext(), TestContext.Current.CancellationToken)).RevisionHash;
after.ShouldNotBe(before);
}
@@ -29,20 +29,20 @@ public sealed class DeploymentRetentionTests : ControlPlaneActorTestBase
int pruned;
await using (var db = f.CreateDbContext())
pruned = await DeploymentRetentionActor.SweepAsync(db, options, SweepNow);
pruned = await DeploymentRetentionActor.SweepAsync(db, options, SweepNow, TestContext.Current.CancellationToken);
pruned.ShouldBe(7);
await using var verify = f.CreateDbContext();
foreach (var id in ids.Take(5))
(await verify.Deployments.SingleAsync(d => d.DeploymentId == id)).ArtifactBlob.Length.ShouldBeGreaterThan(0, "newest 5 keep their blob");
(await verify.Deployments.SingleAsync(d => d.DeploymentId == id, TestContext.Current.CancellationToken)).ArtifactBlob.Length.ShouldBeGreaterThan(0, "newest 5 keep their blob");
foreach (var id in ids.Skip(5))
(await verify.Deployments.SingleAsync(d => d.DeploymentId == id)).ArtifactBlob.ShouldBeEmpty("older rows are emptied");
(await verify.Deployments.SingleAsync(d => d.DeploymentId == id, TestContext.Current.CancellationToken)).ArtifactBlob.ShouldBeEmpty("older rows are emptied");
// Rows survive: the audit record is intact.
(await verify.Deployments.CountAsync()).ShouldBe(12);
(await verify.Deployments.CountAsync(TestContext.Current.CancellationToken)).ShouldBe(12);
// Idempotent: a second sweep finds nothing.
await using var again = f.CreateDbContext();
(await DeploymentRetentionActor.SweepAsync(again, options, SweepNow)).ShouldBe(0);
(await DeploymentRetentionActor.SweepAsync(again, options, SweepNow, TestContext.Current.CancellationToken)).ShouldBe(0);
}
[Fact]
@@ -76,19 +76,19 @@ public sealed class DeploymentRetentionTests : ControlPlaneActorTestBase
DeploymentId = inFlight, RevisionHash = new string('f', 64), Status = DeploymentStatus.AwaitingApplyAcks,
CreatedBy = "test", CreatedAtUtc = SweepNow.AddDays(-400), ArtifactBlob = new byte[] { 1, 2, 3 },
});
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var options = new DeploymentRetentionOptions { KeepSealed = 1, ReferencedDays = 30 };
await using (var db = f.CreateDbContext())
(await DeploymentRetentionActor.SweepAsync(db, options, SweepNow)).ShouldBe(2); // ids[1], ids[2]
(await DeploymentRetentionActor.SweepAsync(db, options, SweepNow, TestContext.Current.CancellationToken)).ShouldBe(2); // ids[1], ids[2]
await using var verify = f.CreateDbContext();
(await verify.Deployments.SingleAsync(d => d.DeploymentId == ids[0])).ArtifactBlob.ShouldNotBeEmpty("newest");
(await verify.Deployments.SingleAsync(d => d.DeploymentId == ids[1])).ArtifactBlob.ShouldBeEmpty();
(await verify.Deployments.SingleAsync(d => d.DeploymentId == ids[2])).ArtifactBlob.ShouldBeEmpty("applied 90 days ago");
(await verify.Deployments.SingleAsync(d => d.DeploymentId == ids[3])).ArtifactBlob.ShouldNotBeEmpty("applied yesterday");
(await verify.Deployments.SingleAsync(d => d.DeploymentId == inFlight)).ArtifactBlob.ShouldNotBeEmpty("in flight");
(await verify.Deployments.SingleAsync(d => d.DeploymentId == ids[0], TestContext.Current.CancellationToken)).ArtifactBlob.ShouldNotBeEmpty("newest");
(await verify.Deployments.SingleAsync(d => d.DeploymentId == ids[1], TestContext.Current.CancellationToken)).ArtifactBlob.ShouldBeEmpty();
(await verify.Deployments.SingleAsync(d => d.DeploymentId == ids[2], TestContext.Current.CancellationToken)).ArtifactBlob.ShouldBeEmpty("applied 90 days ago");
(await verify.Deployments.SingleAsync(d => d.DeploymentId == ids[3], TestContext.Current.CancellationToken)).ArtifactBlob.ShouldNotBeEmpty("applied yesterday");
(await verify.Deployments.SingleAsync(d => d.DeploymentId == inFlight, TestContext.Current.CancellationToken)).ArtifactBlob.ShouldNotBeEmpty("in flight");
}
/// <summary>
@@ -121,17 +121,17 @@ public sealed class DeploymentRetentionTests : ControlPlaneActorTestBase
CreatedBy = "test", CreatedAtUtc = SweepNow.AddHours(-i), ArtifactBlob = new byte[] { 9 },
});
}
await db.SaveChangesAsync();
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
}
var options = new DeploymentRetentionOptions { KeepSealed = 50, ReferencedDays = 30, MaxRowsPerSweep = 200 };
await using (var db = f.CreateDbContext())
(await DeploymentRetentionActor.SweepAsync(db, options, SweepNow)).ShouldBe(60);
(await DeploymentRetentionActor.SweepAsync(db, options, SweepNow, TestContext.Current.CancellationToken)).ShouldBe(60);
await using var verify = f.CreateDbContext();
(await verify.Deployments.SingleAsync(d => d.DeploymentId == sealedId)).ArtifactBlob
(await verify.Deployments.SingleAsync(d => d.DeploymentId == sealedId, TestContext.Current.CancellationToken)).ArtifactBlob
.ShouldNotBeEmpty("the newest Sealed artifact is never pruned");
(await verify.Deployments.CountAsync(d => timedOut.Contains(d.DeploymentId) && d.ArtifactBlob.Length == 0))
(await verify.Deployments.CountAsync(d => timedOut.Contains(d.DeploymentId) && d.ArtifactBlob.Length == 0, TestContext.Current.CancellationToken))
.ShouldBe(60);
}
@@ -143,11 +143,11 @@ public sealed class DeploymentRetentionTests : ControlPlaneActorTestBase
var ids = Seed(f, sealedCount: 3, mixInTimedOut: false);
await using (var db = f.CreateDbContext())
(await DeploymentRetentionActor.SweepAsync(db, new DeploymentRetentionOptions { KeepSealed = 0 }, SweepNow))
(await DeploymentRetentionActor.SweepAsync(db, new DeploymentRetentionOptions { KeepSealed = 0 }, SweepNow, TestContext.Current.CancellationToken))
.ShouldBe(2);
await using var verify = f.CreateDbContext();
(await verify.Deployments.SingleAsync(d => d.DeploymentId == ids[0])).ArtifactBlob.ShouldNotBeEmpty();
(await verify.Deployments.SingleAsync(d => d.DeploymentId == ids[0], TestContext.Current.CancellationToken)).ArtifactBlob.ShouldNotBeEmpty();
}
[Fact]
@@ -158,13 +158,13 @@ public sealed class DeploymentRetentionTests : ControlPlaneActorTestBase
var options = new DeploymentRetentionOptions { KeepSealed = 2, MaxRowsPerSweep = 3 };
await using (var db = f.CreateDbContext())
(await DeploymentRetentionActor.SweepAsync(db, options, SweepNow)).ShouldBe(3);
(await DeploymentRetentionActor.SweepAsync(db, options, SweepNow, TestContext.Current.CancellationToken)).ShouldBe(3);
await using (var db = f.CreateDbContext())
(await DeploymentRetentionActor.SweepAsync(db, options, SweepNow)).ShouldBe(3);
(await DeploymentRetentionActor.SweepAsync(db, options, SweepNow, TestContext.Current.CancellationToken)).ShouldBe(3);
await using (var db = f.CreateDbContext())
(await DeploymentRetentionActor.SweepAsync(db, options, SweepNow)).ShouldBe(2);
(await DeploymentRetentionActor.SweepAsync(db, options, SweepNow, TestContext.Current.CancellationToken)).ShouldBe(2);
await using (var db = f.CreateDbContext())
(await DeploymentRetentionActor.SweepAsync(db, options, SweepNow)).ShouldBe(0);
(await DeploymentRetentionActor.SweepAsync(db, options, SweepNow, TestContext.Current.CancellationToken)).ShouldBe(0);
}
/// <summary>The actor runs the same rule off its thread and reports back.</summary>
@@ -180,7 +180,7 @@ public sealed class DeploymentRetentionTests : ControlPlaneActorTestBase
actor.Tell(DeploymentRetentionActor.Sweep.Instance);
ExpectMsg<DeploymentRetentionActor.SweepCompleted>(PresenceBudget).Pruned.ShouldBe(2);
ExpectMsg<DeploymentRetentionActor.SweepCompleted>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken).Pruned.ShouldBe(2);
using var verify = f.CreateDbContext();
verify.Deployments.Count(d => d.ArtifactBlob.Length == 0).ShouldBe(2);
}
@@ -149,7 +149,7 @@ public sealed class GossipedRoleCaseInsensitivityTests
}),
$"redundancy-{Guid.NewGuid():N}");
var snapshot = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(15));
var snapshot = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(15), TestContext.Current.CancellationToken);
var node = snapshot.Nodes.ShouldHaveSingleItem();
node.Role.ShouldBe(
@@ -1,6 +1,6 @@
using Akka.Cluster;
using Akka.TestKit;
using Akka.TestKit.Xunit2;
using Akka.TestKit.Xunit;
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.OtOpcUa.Configuration;
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness;
/// <summary>
/// Akka TestKit fixture for ControlPlane actor tests. Provides:
/// - A test ActorSystem (xunit2 TestKit) wired with PubSub/Cluster extensions.
/// - A test ActorSystem (xunit.v3 TestKit) wired with PubSub/Cluster extensions.
/// - A fresh in-memory <see cref="OtOpcUaConfigDbContext"/> per harness instance.
/// - An <see cref="IDbContextFactory{TContext}"/> the actors can hold.
///
@@ -58,7 +58,7 @@ public sealed class ManualFailoverServiceTests : IAsyncLifetime
}
""");
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
// Order matters: the seed forms the cluster and is therefore the oldest member.
_oldest = ActorSystem.Create("manual-failover", NodeConfig(OldestPort, "manual-failover", OldestPort));
@@ -69,7 +69,7 @@ public sealed class ManualFailoverServiceTests : IAsyncLifetime
await WaitForUpAsync(_youngest, expectedMembers: 2);
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
if (_youngest is not null) await _youngest.Terminate();
if (_oldest is not null) await _oldest.Terminate();
@@ -128,7 +128,7 @@ public sealed class ManualFailoverServiceTests : IAsyncLifetime
// The leaving node runs CoordinatedShutdown and terminates — that is what makes the process
// exit and be restarted as the youngest member.
await _oldest!.WhenTerminated.WaitAsync(TimeSpan.FromSeconds(30));
await _oldest!.WhenTerminated.WaitAsync(TimeSpan.FromSeconds(30), TestContext.Current.CancellationToken);
// ... and the survivor is now what RedundancyStateActor names Primary.
var snapshot = await CaptureSnapshotAsync(_youngest!);
@@ -192,7 +192,7 @@ public sealed class ManualFailoverServiceTests : IAsyncLifetime
snapshotPrimary.ShouldBe(elected.ToString(), "the UI panel must name the same node the button acts on");
// The dry run really was dry.
await Task.Delay(500);
await Task.Delay(500, TestContext.Current.CancellationToken);
Akka.Cluster.Cluster.Get(_oldest!).State.Members
.Count(m => m.Status == MemberStatus.Up)
.ShouldBe(2, "a dry run must not remove anyone");
@@ -74,7 +74,7 @@ public sealed class RedundancyPrimaryElectionTests : IAsyncLifetime
}
""");
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
// Order matters: the seed forms the cluster and is therefore the oldest member.
_oldest = ActorSystem.Create("redundancy-election", NodeConfig(OldestPort));
@@ -85,7 +85,7 @@ public sealed class RedundancyPrimaryElectionTests : IAsyncLifetime
await WaitForUpAsync(_youngest, expectedMembers: 2);
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
if (_youngest is not null) await _youngest.Terminate();
if (_oldest is not null) await _oldest.Terminate();
@@ -23,7 +23,7 @@ public sealed class RedundancyStateActorTests : ControlPlaneActorTestBase
Sys.ActorOf(RedundancyStateActor.Props(broadcast: msg => probe.Ref.Tell(msg)),
"redundancy-actor");
var msg = probe.ExpectMsg<RedundancyStateChanged>(TimeSpan.FromSeconds(3));
var msg = probe.ExpectMsg<RedundancyStateChanged>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
msg.Nodes.ShouldNotBeNull();
msg.CorrelationId.Value.ShouldNotBe(Guid.Empty);
}
@@ -37,10 +37,10 @@ public sealed class RedundancyStateActorTests : ControlPlaneActorTestBase
"redundancy-debounce");
// First publish should arrive within the debounce window.
probe.ExpectMsg<RedundancyStateChanged>(TimeSpan.FromSeconds(3));
probe.ExpectMsg<RedundancyStateChanged>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
// After debounce settles, no more events are fired by a quiescent cluster.
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
}
/// <summary>
@@ -60,10 +60,10 @@ public sealed class RedundancyStateActorTests : ControlPlaneActorTestBase
"redundancy-heartbeat");
// First publish: the change-driven self-join snapshot.
probe.ExpectMsg<RedundancyStateChanged>(TimeSpan.FromSeconds(2));
probe.ExpectMsg<RedundancyStateChanged>(TimeSpan.FromSeconds(2), cancellationToken: TestContext.Current.CancellationToken);
// Second publish: the periodic heartbeat re-publish, with NO new cluster event sent.
probe.ExpectMsg<RedundancyStateChanged>(TimeSpan.FromSeconds(2));
probe.ExpectMsg<RedundancyStateChanged>(TimeSpan.FromSeconds(2), cancellationToken: TestContext.Current.CancellationToken);
}
/// <summary>
@@ -7,11 +7,10 @@
</PropertyGroup>
<ItemGroup>
<!-- Akka.TestKit.Xunit2 ties this project to xunit v2 (TestKit isn't ported to v3 yet). -->
<PackageReference Include="xunit"/>
<PackageReference Include="xunit.v3" VersionOverride="3.2.2"/>
<PackageReference Include="Shouldly"/>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<PackageReference Include="Akka.TestKit.Xunit2"/>
<PackageReference Include="Akka.TestKit.Xunit"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory"/>
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
@@ -68,7 +68,7 @@ public sealed class DriverResilienceStatusPublisherServiceTests
public async Task ExecuteAsync_emits_Resilience_item_per_published_snapshot()
{
// Plain self-joined single-node cluster (mirrors SecretsReplicationRegistrationTests — the
// xunit.v3 project cannot use Akka.TestKit.Xunit2). public-hostname is load-bearing for the bind.
// xunit.v3 project cannot use Akka.TestKit.Xunit). public-hostname is load-bearing for the bind.
var hocon = ConfigurationFactory.ParseString("""
akka {
loglevel = WARNING
@@ -128,7 +128,7 @@ public sealed class SecretsReplicationRegistrationTests
// the hook, and prove the replication actor exists. On 0.2.0 this fails because the actor
// is never spawned (inert replicator); on 0.2.1 it deadlocks in the resolve (scadaproj#1).
//
// Akka.TestKit.Xunit2 is xunit-v2-only and this project is on xunit.v3 (CS0433), so this
// Akka.TestKit.Xunit is xunit-v2-only and this project is on xunit.v3 (CS0433), so this
// uses a plain self-joined single-node cluster — which is also all the actor needs: its
// constructor gets the DistributedPubSub mediator, no peers required.
ActorSystem system = ActorSystem.Create(
@@ -1,5 +1,5 @@
using Akka.Actor;
using Akka.TestKit.Xunit2;
using Akka.TestKit.Xunit;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Shouldly;
@@ -62,7 +62,7 @@ public sealed class VirtualTagToVirtualTagFlowTests : TestKit
mux.Tell(Driver(5));
publish.FishForMessage<OpcUaPublishActor.AttributeValueUpdate>(
m => m.NodeId == AKey && Equals(m.Value, 10d), TimeSpan.FromMilliseconds(500));
}, Budget, TimeSpan.FromMilliseconds(50));
}, Budget, TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken);
NextFor(publish, BKey, m => Equals(m.Value, 11d) && m.StatusCode == OpcUaStatus.Good);
// A's input changes: B re-evaluates against A's NEW value (ctx.GetTag returns A's current value).
@@ -1,5 +1,5 @@
using Akka.Hosting;
using Akka.TestKit.Xunit2;
using Akka.TestKit.Xunit;
using Grpc.Core;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
@@ -54,16 +54,16 @@ public sealed class NodeCommandGrpcServiceTests : TestKit
var alarm = new AlarmCommand("alarm-7", "Acknowledge", "carol", "looked at it", null);
(await service.Execute(MeshTransportDtoMapper.ToEnvelope(dispatch), Ctx())).ShouldNotBeNull();
probe.ExpectMsg<DispatchDeployment>().ShouldBe(dispatch);
probe.ExpectMsg<DispatchDeployment>(cancellationToken: TestContext.Current.CancellationToken).ShouldBe(dispatch);
await service.Execute(MeshTransportDtoMapper.ToEnvelope(restart), Ctx());
probe.ExpectMsg<RestartDriver>().ShouldBe(restart);
probe.ExpectMsg<RestartDriver>(cancellationToken: TestContext.Current.CancellationToken).ShouldBe(restart);
await service.Execute(MeshTransportDtoMapper.ToEnvelope(reconnect), Ctx());
probe.ExpectMsg<ReconnectDriver>().ShouldBe(reconnect);
probe.ExpectMsg<ReconnectDriver>(cancellationToken: TestContext.Current.CancellationToken).ShouldBe(reconnect);
await service.Execute(MeshTransportDtoMapper.ToEnvelope(alarm, AlarmCorrelation), Ctx());
probe.ExpectMsg<AlarmCommand>().ShouldBe(alarm);
probe.ExpectMsg<AlarmCommand>(cancellationToken: TestContext.Current.CancellationToken).ShouldBe(alarm);
}
[Fact]
@@ -94,7 +94,7 @@ public sealed class NodeCommandGrpcServiceTests : TestKit
new CommandEnvelope { CorrelationId = Guid.NewGuid().ToString("N") }, Ctx()));
ex.StatusCode.ShouldBe(StatusCode.InvalidArgument);
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
}
[Theory]
@@ -109,7 +109,7 @@ public sealed class NodeCommandGrpcServiceTests : TestKit
var ex = await Should.ThrowAsync<RpcException>(() => service.Execute(envelope, Ctx()));
ex.StatusCode.ShouldBe(StatusCode.InvalidArgument);
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken);
}
public static TheoryData<CommandEnvelope> UndecodableEnvelopes() =>
@@ -1,6 +1,6 @@
using Akka.Actor;
using Akka.Configuration;
using Akka.TestKit.Xunit2;
using Akka.TestKit.Xunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
@@ -106,7 +106,7 @@ public sealed class ApiKeyAuthenticationTests
{
var (presented, store) = SetUp(NodePermissions.WriteOperate, NodeAclScopeKind.Cluster);
var result = await Auth(store).AuthenticateUserNameAsync("apikey", presented, default);
var result = await Auth(store).AuthenticateUserNameAsync("apikey", presented, TestContext.Current.CancellationToken);
result.Success.ShouldBeTrue();
result.DisplayName.ShouldBe(Principal);
@@ -117,7 +117,7 @@ public sealed class ApiKeyAuthenticationTests
public async Task The_wrong_username_is_denied()
{
var (presented, store) = SetUp();
var result = await Auth(store).AuthenticateUserNameAsync("alice", presented, default);
var result = await Auth(store).AuthenticateUserNameAsync("alice", presented, TestContext.Current.CancellationToken);
result.Success.ShouldBeFalse();
}
@@ -127,10 +127,10 @@ public sealed class ApiKeyAuthenticationTests
var (presented, store) = SetUp();
var auth = Auth(store);
var wrongSecret = await auth.AuthenticateUserNameAsync("apikey", $"otua_{store.KeyId}_wrong", default);
var noSuchKey = await auth.AuthenticateUserNameAsync("apikey", "otua_ffffffff_whatever", default);
var malformed = await auth.AuthenticateUserNameAsync("apikey", "not-a-key", default);
var empty = await auth.AuthenticateUserNameAsync("apikey", "", default);
var wrongSecret = await auth.AuthenticateUserNameAsync("apikey", $"otua_{store.KeyId}_wrong", TestContext.Current.CancellationToken);
var noSuchKey = await auth.AuthenticateUserNameAsync("apikey", "otua_ffffffff_whatever", TestContext.Current.CancellationToken);
var malformed = await auth.AuthenticateUserNameAsync("apikey", "not-a-key", TestContext.Current.CancellationToken);
var empty = await auth.AuthenticateUserNameAsync("apikey", "", TestContext.Current.CancellationToken);
foreach (var r in new[] { wrongSecret, noSuchKey, malformed, empty })
{
@@ -141,7 +141,7 @@ public sealed class ApiKeyAuthenticationTests
}
// ...and the valid one still works, so the above is not passing because everything is denied.
(await auth.AuthenticateUserNameAsync("apikey", presented, default)).Success.ShouldBeTrue();
(await auth.AuthenticateUserNameAsync("apikey", presented, TestContext.Current.CancellationToken)).Success.ShouldBeTrue();
}
[Fact]
@@ -150,7 +150,7 @@ public sealed class ApiKeyAuthenticationTests
// The interface contract forbids throwing — an exception escapes into the SDK session-activation
// path and surfaces as BadInternalError instead of a clean rejection.
var store = new ThrowingApiKeyStore();
var result = await Auth(store).AuthenticateUserNameAsync("apikey", "otua_aabbccdd_secret", default);
var result = await Auth(store).AuthenticateUserNameAsync("apikey", "otua_aabbccdd_secret", TestContext.Current.CancellationToken);
result.Success.ShouldBeFalse();
result.Error.ShouldBe(ApiKeyOpcUaUserAuthenticator.DenyMessage);
@@ -165,7 +165,7 @@ public sealed class ApiKeyAuthenticationTests
Options.Create(new ApiKeyOptions { Username = "apikey", Pepper = "" }),
NullLogger<ApiKeyOpcUaUserAuthenticator>.Instance);
(await auth.AuthenticateUserNameAsync("apikey", presented, default)).Success.ShouldBeFalse();
(await auth.AuthenticateUserNameAsync("apikey", presented, TestContext.Current.CancellationToken)).Success.ShouldBeFalse();
}
// ---------- anonymous as a normal principal (D3) ----------
@@ -183,7 +183,7 @@ public sealed class ApiKeyAuthenticationTests
PermissionFlags = NodePermissions.WriteOperate,
});
var result = await Auth(store).AuthenticateAnonymousAsync(default);
var result = await Auth(store).AuthenticateAnonymousAsync(TestContext.Current.CancellationToken);
result.Success.ShouldBeTrue();
result.DisplayName.ShouldBe(OpcUaPrincipals.Anonymous);
@@ -197,7 +197,7 @@ public sealed class ApiKeyAuthenticationTests
// Today's behaviour preserved: it connects, and every role-gated operation denies.
var (_, store) = SetUp();
var result = await Auth(store).AuthenticateAnonymousAsync(default);
var result = await Auth(store).AuthenticateAnonymousAsync(TestContext.Current.CancellationToken);
result.Success.ShouldBeTrue();
result.Roles.ShouldBeEmpty();
@@ -208,7 +208,7 @@ public sealed class ApiKeyAuthenticationTests
{
// Deliberately NOT admit-with-no-roles. That is the pre-#520 behaviour, and defaulting to it
// exactly when we cannot read what anonymous may do stops being harmless once reads are gated.
var result = await Auth(new ThrowingApiKeyStore()).AuthenticateAnonymousAsync(default);
var result = await Auth(new ThrowingApiKeyStore()).AuthenticateAnonymousAsync(TestContext.Current.CancellationToken);
result.Success.ShouldBeFalse();
}
@@ -227,7 +227,7 @@ public sealed class ApiKeyAuthenticationTests
CreatedUtc = DateTime.UtcNow,
};
var result = await Auth(new FakeApiKeyStore(key, [])).AuthenticateUserNameAsync("apikey", presented, default);
var result = await Auth(new FakeApiKeyStore(key, [])).AuthenticateUserNameAsync("apikey", presented, TestContext.Current.CancellationToken);
// Otherwise a credentialled client silently inherits whatever anonymous was granted.
result.Success.ShouldBeFalse();
@@ -241,7 +241,7 @@ public sealed class ApiKeyAuthenticationTests
// Interface-typed on purpose: a default interface implementation is only reachable through the
// interface, which is how OpcUaApplicationHost holds it.
IOpcUaUserAuthenticator sut = NullOpcUaUserAuthenticator.Instance;
var result = await sut.AuthenticateAnonymousAsync(default);
var result = await sut.AuthenticateAnonymousAsync(TestContext.Current.CancellationToken);
result.Success.ShouldBeTrue();
result.Roles.ShouldBeEmpty();
@@ -26,7 +26,7 @@ public sealed class ArtifactApiKeyStoreTests
{
var sut = new ArtifactApiKeyStore(new DataPlaneAuthorityState());
(await sut.FindAsync("abc", Now, default)).ShouldBeNull();
(await sut.FindAsync("abc", Now, TestContext.Current.CancellationToken)).ShouldBeNull();
}
[Fact]
@@ -37,7 +37,7 @@ public sealed class ArtifactApiKeyStoreTests
[Key("abc", "scadabridge-dcl")],
[Acl("acl-1", "scadabridge-dcl", NodeAclScopeKind.Cluster, NodePermissions.WriteOperate)], NoScopePaths);
var found = await new ArtifactApiKeyStore(state).FindAsync("abc", Now, default);
var found = await new ArtifactApiKeyStore(state).FindAsync("abc", Now, TestContext.Current.CancellationToken);
found.ShouldNotBeNull();
found!.Key.Principal.ShouldBe("scadabridge-dcl");
@@ -67,7 +67,7 @@ public sealed class ArtifactApiKeyStoreTests
Acl("acl-equipment", "p", NodeAclScopeKind.Equipment, NodePermissions.WriteOperate),
], NoScopePaths);
var found = await new ArtifactApiKeyStore(state).FindAsync("abc", Now, default);
var found = await new ArtifactApiKeyStore(state).FindAsync("abc", Now, TestContext.Current.CancellationToken);
found!.Grants.Select(g => g.NodeAclId).ShouldBe(["acl-cluster", "acl-equipment"], ignoreOrder: true);
}
@@ -91,7 +91,7 @@ public sealed class ArtifactApiKeyStoreTests
[Acl("acl-equipment", "p", NodeAclScopeKind.Equipment, NodePermissions.WriteOperate)],
NoScopePaths);
var found = await new ArtifactApiKeyStore(state).FindAsync("abc", Now, default);
var found = await new ArtifactApiKeyStore(state).FindAsync("abc", Now, TestContext.Current.CancellationToken);
found!.Grants.ShouldHaveSingleItem();
ApiKeyInterimRoleMapper.Map(found.Grants, "p").ShouldBeEmpty();
@@ -106,13 +106,13 @@ public sealed class ArtifactApiKeyStoreTests
var sut = new ArtifactApiKeyStore(state);
state.Apply("cluster-a", "rev-1", [Key("abc", "p")], [], NoScopePaths);
(await sut.FindAsync("abc", Now, default)).ShouldNotBeNull();
(await sut.FindAsync("abc", Now, TestContext.Current.CancellationToken)).ShouldNotBeNull();
var revoked = Key("abc", "p");
revoked.Enabled = false;
state.Apply("cluster-a", "rev-2", [revoked], [], NoScopePaths);
(await sut.FindAsync("abc", Now, default)).ShouldBeNull();
(await sut.FindAsync("abc", Now, TestContext.Current.CancellationToken)).ShouldBeNull();
}
[Fact]
@@ -124,7 +124,7 @@ public sealed class ArtifactApiKeyStoreTests
[Acl("acl-anon", OpcUaPrincipals.Anonymous, NodeAclScopeKind.Cluster, NodePermissions.Read)],
NoScopePaths);
var grants = await new ArtifactApiKeyStore(state).FindGrantsAsync(OpcUaPrincipals.Anonymous, default);
var grants = await new ArtifactApiKeyStore(state).FindGrantsAsync(OpcUaPrincipals.Anonymous, TestContext.Current.CancellationToken);
grants.ShouldHaveSingleItem().PermissionFlags.ShouldBe(NodePermissions.Read);
}
@@ -25,13 +25,13 @@ public sealed class BoundedApiKeyStoreKnownIdTests
var clock = new FakeClock(T0);
var sut = Bounded(inner, clock, cacheSeconds: 5);
(await sut.FindAsync("k-guess-1", T0.UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync("k-guess-2", T0.UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync("k-guess-1", T0.UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
(await sut.FindAsync("k-guess-2", T0.UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.FindCalls.ShouldBe(0, "unknown ids never reach the store");
inner.ListCalls.ShouldBe(1, "one set load serves every unknown id inside the TTL");
// Positive control: the real id reaches the store and is found.
(await sut.FindAsync("k-real", T0.UtcDateTime, default)).ShouldNotBeNull();
(await sut.FindAsync("k-real", T0.UtcDateTime, TestContext.Current.CancellationToken)).ShouldNotBeNull();
inner.FindCalls.ShouldBe(1);
}
@@ -42,15 +42,15 @@ public sealed class BoundedApiKeyStoreKnownIdTests
var clock = new FakeClock(T0);
var sut = Bounded(inner, clock, cacheSeconds: 5);
(await sut.FindAsync("k-new", T0.UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync("k-new", T0.UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.Mint("k-new");
// Still inside the TTL: the stale set refuses it (the documented window).
clock.Advance(TimeSpan.FromSeconds(3));
(await sut.FindAsync("k-new", clock.GetUtcNow().UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync("k-new", clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.FindCalls.ShouldBe(0);
clock.Advance(TimeSpan.FromSeconds(3));
(await sut.FindAsync("k-new", clock.GetUtcNow().UtcDateTime, default)).ShouldNotBeNull();
(await sut.FindAsync("k-new", clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldNotBeNull();
inner.ListCalls.ShouldBe(2);
}
@@ -62,20 +62,20 @@ public sealed class BoundedApiKeyStoreKnownIdTests
var sut = Bounded(inner, clock, cacheSeconds: 5);
// No set yet and the load fails: the per-id path runs (old behaviour), nothing is wedged.
(await sut.FindAsync("k-guess", T0.UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync("k-guess", T0.UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.FindCalls.ShouldBe(1);
sut.KnownKeyIds.ShouldBeNull();
inner.ListThrows = false;
clock.Advance(TimeSpan.FromSeconds(6));
(await sut.FindAsync("k-guess-2", clock.GetUtcNow().UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync("k-guess-2", clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
sut.KnownKeyIds.ShouldNotBeNull();
inner.FindCalls.ShouldBe(1, "once the set loaded, the guess was refused in memory");
// A later failed refresh keeps the previous set in force.
inner.ListThrows = true;
clock.Advance(TimeSpan.FromSeconds(6));
(await sut.FindAsync("k-guess-3", clock.GetUtcNow().UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync("k-guess-3", clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.FindCalls.ShouldBe(1);
}
@@ -88,7 +88,7 @@ public sealed class BoundedApiKeyStoreKnownIdTests
var sut = Bounded(inner, clock, cacheSeconds: 0, missBurst: 3, refillPerSecond: 1);
for (var i = 0; i < 3; i++)
(await sut.FindAsync($"k-guess-{i}", T0.UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync($"k-guess-{i}", T0.UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.FindCalls.ShouldBe(3);
// Budget exhausted: refused without a store read, as a throw the authenticator denies on.
@@ -97,14 +97,14 @@ public sealed class BoundedApiKeyStoreKnownIdTests
// Refill: one second restores one token.
clock.Advance(TimeSpan.FromSeconds(1));
(await sut.FindAsync("k-guess-10", clock.GetUtcNow().UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync("k-guess-10", clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.FindCalls.ShouldBe(4);
// A hit is not a miss: it reaches the store even with the bucket empty? No — it needs a token to
// be attempted, but the token is refunded, so the next attempt still has it. Prove the refund.
clock.Advance(TimeSpan.FromSeconds(1));
(await sut.FindAsync("k-real", clock.GetUtcNow().UtcDateTime, default)).ShouldNotBeNull();
(await sut.FindAsync("k-guess-11", clock.GetUtcNow().UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync("k-real", clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldNotBeNull();
(await sut.FindAsync("k-guess-11", clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.FindCalls.ShouldBe(6);
}
@@ -114,8 +114,8 @@ public sealed class BoundedApiKeyStoreKnownIdTests
var inner = new ListingStore("k-real");
var sut = Bounded(inner, new FakeClock(T0), cacheSeconds: 0, missBurst: 0);
(await sut.FindAsync("k-guess", T0.UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync("k-guess", T0.UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync("k-guess", T0.UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
(await sut.FindAsync("k-guess", T0.UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.FindCalls.ShouldBe(2);
inner.ListCalls.ShouldBe(0);
}
@@ -139,13 +139,13 @@ public sealed class BoundedApiKeyStoreKnownIdTests
var unlisting = new ListingStore("k-real") { SupportsListing = false };
var first = new BoundedApiKeyStore(unlisting, options, NullLogger<BoundedApiKeyStore>.Instance, clock, guard);
(await first.FindAsync("k-guess", T0.UtcDateTime, default)).ShouldBeNull();
(await first.FindAsync("k-guess", T0.UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
unlisting.FindCalls.ShouldBe(1, "no set: the per-id path ran");
guard.KnownIdSet.ShouldBeTrue("the shared options object is never written");
var listing = new ListingStore("k-real");
var second = new BoundedApiKeyStore(listing, options, NullLogger<BoundedApiKeyStore>.Instance, clock, guard);
(await second.FindAsync("k-guess", T0.UtcDateTime, default)).ShouldBeNull();
(await second.FindAsync("k-guess", T0.UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
listing.FindCalls.ShouldBe(0, "the second decorator still refuses unknown ids in memory");
listing.ListCalls.ShouldBe(1);
}
@@ -31,7 +31,7 @@ public sealed class BoundedApiKeyStorePerEndpointTests
var sut = Bounded(inner, new FakeClock(T0), missBurst: 100, perEndpointBurst: 3);
for (var i = 0; i < 3; i++)
(await sut.FindAsync($"k-guess-{i}", T0.UtcDateTime, Noisy, default)).ShouldBeNull();
(await sut.FindAsync($"k-guess-{i}", T0.UtcDateTime, Noisy, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.FindCalls.ShouldBe(3);
var refused = await Should.ThrowAsync<InvalidOperationException>(
@@ -40,7 +40,7 @@ public sealed class BoundedApiKeyStorePerEndpointTests
inner.FindCalls.ShouldBe(3, "the exhausted client is refused without a store read");
// Positive control: another client still reaches the store.
(await sut.FindAsync("k-guess-q", T0.UtcDateTime, Quiet, default)).ShouldBeNull();
(await sut.FindAsync("k-guess-q", T0.UtcDateTime, Quiet, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.FindCalls.ShouldBe(4);
}
@@ -51,8 +51,8 @@ public sealed class BoundedApiKeyStorePerEndpointTests
var inner = new UnlistingStore("k-real");
var sut = Bounded(inner, new FakeClock(T0), missBurst: 2, perEndpointBurst: 10);
(await sut.FindAsync("k-a", T0.UtcDateTime, Noisy, default)).ShouldBeNull();
(await sut.FindAsync("k-b", T0.UtcDateTime, remoteAddress: null, default)).ShouldBeNull();
(await sut.FindAsync("k-a", T0.UtcDateTime, Noisy, TestContext.Current.CancellationToken)).ShouldBeNull();
(await sut.FindAsync("k-b", T0.UtcDateTime, remoteAddress: null, TestContext.Current.CancellationToken)).ShouldBeNull();
var refused = await Should.ThrowAsync<InvalidOperationException>(
() => sut.FindAsync("k-c", T0.UtcDateTime, Quiet, default));
@@ -70,14 +70,14 @@ public sealed class BoundedApiKeyStorePerEndpointTests
var sut = Bounded(inner, clock, missBurst: 100, perEndpointBurst: 2, perEndpointRefill: 1);
for (var i = 0; i < 5; i++)
(await sut.FindAsync("k-real", T0.UtcDateTime, Noisy, default)).ShouldNotBeNull();
(await sut.FindAsync("k-real", T0.UtcDateTime, Noisy, TestContext.Current.CancellationToken)).ShouldNotBeNull();
(await sut.FindAsync("k-g1", T0.UtcDateTime, Noisy, default)).ShouldBeNull();
(await sut.FindAsync("k-g2", T0.UtcDateTime, Noisy, default)).ShouldBeNull();
(await sut.FindAsync("k-g1", T0.UtcDateTime, Noisy, TestContext.Current.CancellationToken)).ShouldBeNull();
(await sut.FindAsync("k-g2", T0.UtcDateTime, Noisy, TestContext.Current.CancellationToken)).ShouldBeNull();
await Should.ThrowAsync<InvalidOperationException>(() => sut.FindAsync("k-g3", T0.UtcDateTime, Noisy, default));
clock.Advance(TimeSpan.FromSeconds(1));
(await sut.FindAsync("k-g4", clock.GetUtcNow().UtcDateTime, Noisy, default)).ShouldBeNull();
(await sut.FindAsync("k-g4", clock.GetUtcNow().UtcDateTime, Noisy, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.FindCalls.ShouldBe(8);
}
@@ -89,7 +89,7 @@ public sealed class BoundedApiKeyStorePerEndpointTests
var sut = Bounded(inner, new FakeClock(T0), missBurst: 100, perEndpointBurst: 0);
for (var i = 0; i < 20; i++)
(await sut.FindAsync($"k-guess-{i}", T0.UtcDateTime, Noisy, default)).ShouldBeNull();
(await sut.FindAsync($"k-guess-{i}", T0.UtcDateTime, Noisy, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.FindCalls.ShouldBe(20);
}
@@ -107,7 +107,7 @@ public sealed class BoundedApiKeyStorePerEndpointTests
NullLogger<ApiKeyOpcUaUserAuthenticator>.Instance);
var result = await authenticator.AuthenticateUserNameAsync(
"apikey", "otua_k-guess_secret", new IPEndPoint(Noisy, 50123), default);
"apikey", "otua_k-guess_secret", new IPEndPoint(Noisy, 50123), TestContext.Current.CancellationToken);
result.Success.ShouldBeFalse();
store.LastAddress.ShouldBe(Noisy);
@@ -54,7 +54,7 @@ public sealed class BoundedApiKeyStoreTests
var auth = Auth(Bounded(inner, timeoutMs: 150, cacheSeconds: 0));
var result = await auth.AuthenticateUserNameAsync("apikey", presented, CancellationToken.None)
.WaitAsync(TimeSpan.FromSeconds(5));
.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
result.Success.ShouldBeFalse();
result.Error.ShouldBe(ApiKeyOpcUaUserAuthenticator.DenyMessage);
@@ -95,10 +95,10 @@ public sealed class BoundedApiKeyStoreTests
var pending = auth.AuthenticateUserNameAsync("apikey", presented, CancellationToken.None);
// Let the deadline pass, THEN hand the inner store its (valid) answer.
await Task.Delay(400);
await Task.Delay(400, TestContext.Current.CancellationToken);
gate.SetResult();
var result = await pending.WaitAsync(TimeSpan.FromSeconds(5));
var result = await pending.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
result.Success.ShouldBeFalse();
result.Error.ShouldBe(ApiKeyOpcUaUserAuthenticator.DenyMessage);
}
@@ -167,10 +167,10 @@ public sealed class BoundedApiKeyStoreTests
// The abandoned inner task now completes, with a perfectly valid key, into nobody's hands.
inner.Release();
await inner.Completed.WaitAsync(TimeSpan.FromSeconds(2));
await inner.Completed.WaitAsync(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken);
// Well inside the 60 s TTL: a cached late answer would make this a hit.
(await sut.FindAsync(inner.KeyId, DateTime.UtcNow, default).WaitAsync(TimeSpan.FromSeconds(2)))
(await sut.FindAsync(inner.KeyId, DateTime.UtcNow, TestContext.Current.CancellationToken).WaitAsync(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken))
.ShouldNotBeNull("the store answers immediately once released");
inner.Calls.ShouldBe(2, "the abandoned lookup must not have populated the cache");
}
@@ -197,12 +197,12 @@ public sealed class BoundedApiKeyStoreTests
.WaitAsync(TimeSpan.FromSeconds(2)));
inner.ReleaseWithFault(new InvalidOperationException("SqlClient gave up"));
await inner.Completed.WaitAsync(TimeSpan.FromSeconds(2));
await inner.Completed.WaitAsync(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken);
// The continuation is a separate task from the one just awaited; give it a moment to run.
var deadline = Stopwatch.StartNew();
while (log.Lines.Count == 0 && deadline.Elapsed < TimeSpan.FromSeconds(2))
await Task.Delay(10);
await Task.Delay(10, TestContext.Current.CancellationToken);
log.Lines.ShouldContain(line => line.Contains("abandoned", StringComparison.OrdinalIgnoreCase));
}
@@ -222,11 +222,11 @@ public sealed class BoundedApiKeyStoreTests
var pending = auth.AuthenticateUserNameAsync("apikey", presented, CancellationToken.None);
// Far longer than any deadline this class configures; an active bound would have denied by now.
await Task.Delay(300);
await Task.Delay(300, TestContext.Current.CancellationToken);
pending.IsCompleted.ShouldBeFalse("a disabled deadline must not cancel a slow lookup");
gate.SetResult();
(await pending.WaitAsync(TimeSpan.FromSeconds(5))).Success.ShouldBeTrue();
(await pending.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken)).Success.ShouldBeTrue();
}
/// <summary>The anonymous activation path gets the same deadline, and the same denial.</summary>
@@ -242,7 +242,7 @@ public sealed class BoundedApiKeyStoreTests
var auth = Auth(Bounded(inner, timeoutMs: 150, cacheSeconds: 0));
var result = await auth.AuthenticateAnonymousAsync(CancellationToken.None)
.WaitAsync(TimeSpan.FromSeconds(5));
.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
result.Success.ShouldBeFalse();
}
@@ -257,9 +257,9 @@ public sealed class BoundedApiKeyStoreTests
var clock = new FakeClock(new DateTimeOffset(2026, 8, 20, 12, 0, 0, TimeSpan.Zero));
var sut = Bounded(inner, timeoutMs: 0, cacheSeconds: 5, clock);
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default)).ShouldNotBeNull();
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldNotBeNull();
clock.Advance(TimeSpan.FromSeconds(2));
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default)).ShouldNotBeNull();
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldNotBeNull();
inner.Calls.ShouldBe(1);
}
@@ -280,18 +280,18 @@ public sealed class BoundedApiKeyStoreTests
var clock = new FakeClock(new DateTimeOffset(2026, 8, 20, 12, 0, 0, TimeSpan.Zero));
var sut = Bounded(inner, timeoutMs: 0, cacheSeconds: 5, clock);
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default))
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken))
.ShouldNotBeNull("non-vacuity: the key really was usable before the revocation");
inner.Revoked = true;
// Still inside the TTL — the documented, accepted staleness window.
clock.Advance(TimeSpan.FromSeconds(3));
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default)).ShouldNotBeNull();
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldNotBeNull();
// Past it — the store is re-read and the revocation lands.
clock.Advance(TimeSpan.FromSeconds(3));
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.Calls.ShouldBe(2);
}
@@ -304,7 +304,7 @@ public sealed class BoundedApiKeyStoreTests
var sut = Bounded(inner, timeoutMs: 0, cacheSeconds: 0, clock);
for (var i = 0; i < 3; i++)
await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default);
await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken);
inner.Calls.ShouldBe(3);
}
@@ -328,7 +328,7 @@ public sealed class BoundedApiKeyStoreTests
() => sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default));
// Same instant — a cached fault would still be "fresh" here, so the clock cannot rescue this.
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default)).ShouldNotBeNull();
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldNotBeNull();
inner.Calls.ShouldBe(2);
}
@@ -340,12 +340,12 @@ public sealed class BoundedApiKeyStoreTests
var clock = new FakeClock(new DateTimeOffset(2026, 8, 20, 12, 0, 0, TimeSpan.Zero));
var sut = Bounded(inner, timeoutMs: 0, cacheSeconds: 5, clock);
await sut.FindGrantsAsync(OpcUaPrincipals.Anonymous, default);
await sut.FindGrantsAsync(OpcUaPrincipals.Anonymous, default);
await sut.FindGrantsAsync(OpcUaPrincipals.Anonymous, TestContext.Current.CancellationToken);
await sut.FindGrantsAsync(OpcUaPrincipals.Anonymous, TestContext.Current.CancellationToken);
inner.Calls.ShouldBe(1);
clock.Advance(TimeSpan.FromSeconds(6));
await sut.FindGrantsAsync(OpcUaPrincipals.Anonymous, default);
await sut.FindGrantsAsync(OpcUaPrincipals.Anonymous, TestContext.Current.CancellationToken);
inner.Calls.ShouldBe(2);
}
@@ -369,15 +369,15 @@ public sealed class BoundedApiKeyStoreTests
guard: new ApiKeyStoreGuardOptions { MissBurst = 0 });
// Seed the real key, then flood past the ceiling with ids that do not exist.
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default)).ShouldNotBeNull();
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldNotBeNull();
for (var i = 0; i < BoundedApiKeyStore.MaxCacheEntries + 1; i++)
await sut.FindAsync($"ghost-{i}", clock.GetUtcNow().UtcDateTime, default);
await sut.FindAsync($"ghost-{i}", clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken);
var callsAfterFlood = inner.Calls;
// Still correct after the clear — and the real key was evicted, so it is re-read rather than
// served from a cache the flood was supposed to have emptied.
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default)).ShouldNotBeNull();
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldNotBeNull();
inner.Calls.ShouldBe(callsAfterFlood + 1);
}
@@ -406,7 +406,7 @@ public sealed class BoundedApiKeyStoreTests
var clock = new FakeClock(start);
var sut = Bounded(inner, timeoutMs: 0, cacheSeconds: 60, clock);
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default))
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken))
.ShouldNotBeNull("non-vacuity: the key was usable and IS now cached");
inner.Revoked = true;
@@ -414,7 +414,7 @@ public sealed class BoundedApiKeyStoreTests
// Past ExpiresUtc (+2 s) but far inside the 60 s TTL — the entry is still "fresh" to the cache.
clock.Advance(TimeSpan.FromSeconds(5));
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default)).ShouldBeNull();
(await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken)).ShouldBeNull();
inner.Calls.ShouldBe(2, "the expired entry must be re-read, not served");
}
@@ -449,10 +449,10 @@ public sealed class BoundedApiKeyStoreTests
using var meter = new MeterRecorder();
var sut = Bounded(inner, timeoutMs: 0, cacheSeconds: 5, clock);
await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default);
await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken);
meter.WithOutcome("cache_hit").ShouldBe(0, "the first call is a miss");
await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, default);
await sut.FindAsync(inner.KeyId, clock.GetUtcNow().UtcDateTime, TestContext.Current.CancellationToken);
meter.WithOutcome("cache_hit").ShouldBe(1);
}
@@ -468,7 +468,7 @@ public sealed class BoundedApiKeyStoreTests
using var meter = new MeterRecorder();
var sut = Bounded(inner, timeoutMs: 2000, cacheSeconds: 0);
await sut.FindAsync(inner.KeyId, DateTime.UtcNow, default);
await sut.FindAsync(inner.KeyId, DateTime.UtcNow, TestContext.Current.CancellationToken);
meter.Total.ShouldBe(0);
}
@@ -52,7 +52,7 @@ public sealed class ConfigDbApiKeyStoreTests
keys: [Key("abc", "scadabridge-dcl")],
acls: [Acl("acl-other", OtherCluster, "scadabridge-dcl", NodeAclScopeKind.Cluster, NodePermissions.WriteOperate)]);
var found = await new ConfigDbApiKeyStore(factory, ThisCluster).FindAsync("abc", Now, default);
var found = await new ConfigDbApiKeyStore(factory, ThisCluster).FindAsync("abc", Now, TestContext.Current.CancellationToken);
// The KEY still resolves — keys are fleet-wide credentials (ApiKey carries no ClusterId); it is
// the grants that are cluster-bound. Asserting a null lookup here would pass for the wrong reason.
@@ -76,7 +76,7 @@ public sealed class ConfigDbApiKeyStoreTests
Acl("acl-other", OtherCluster, "scadabridge-dcl", NodeAclScopeKind.Cluster, NodePermissions.WriteOperate),
]);
var found = await new ConfigDbApiKeyStore(factory, ThisCluster).FindAsync("abc", Now, default);
var found = await new ConfigDbApiKeyStore(factory, ThisCluster).FindAsync("abc", Now, TestContext.Current.CancellationToken);
found!.Grants.ShouldHaveSingleItem().NodeAclId.ShouldBe("acl-mine");
}
@@ -98,7 +98,7 @@ public sealed class ConfigDbApiKeyStoreTests
Acl("acl-b", OtherCluster, "p", NodeAclScopeKind.Cluster, NodePermissions.WriteOperate),
]);
var found = await new ConfigDbApiKeyStore(factory, clusterId: null).FindAsync("abc", Now, default);
var found = await new ConfigDbApiKeyStore(factory, clusterId: null).FindAsync("abc", Now, TestContext.Current.CancellationToken);
found!.Grants.Select(g => g.NodeAclId).ShouldBe(["acl-a", "acl-b"], ignoreOrder: true);
}
@@ -119,7 +119,7 @@ public sealed class ConfigDbApiKeyStoreTests
keys: [Key("abc", "p")],
acls: [Acl("acl-mine", "SITE-A", "p", NodeAclScopeKind.Cluster, NodePermissions.Read)]);
var found = await new ConfigDbApiKeyStore(factory, "site-a").FindAsync("abc", Now, default);
var found = await new ConfigDbApiKeyStore(factory, "site-a").FindAsync("abc", Now, TestContext.Current.CancellationToken);
found!.Grants.ShouldHaveSingleItem().NodeAclId.ShouldBe("acl-mine");
}
@@ -141,7 +141,7 @@ public sealed class ConfigDbApiKeyStoreTests
]);
var grants = await new ConfigDbApiKeyStore(factory, ThisCluster)
.FindGrantsAsync(OpcUaPrincipals.Anonymous, default);
.FindGrantsAsync(OpcUaPrincipals.Anonymous, TestContext.Current.CancellationToken);
grants.ShouldHaveSingleItem().NodeAclId.ShouldBe("acl-anon-mine");
}
@@ -168,7 +168,7 @@ public sealed class ConfigDbApiKeyStoreTests
Acl("acl-equipment", ThisCluster, "p", NodeAclScopeKind.Equipment, NodePermissions.WriteOperate),
]);
var found = await new ConfigDbApiKeyStore(factory, ThisCluster).FindAsync("abc", Now, default);
var found = await new ConfigDbApiKeyStore(factory, ThisCluster).FindAsync("abc", Now, TestContext.Current.CancellationToken);
found!.Grants.Select(g => g.NodeAclId).ShouldBe(["acl-cluster", "acl-equipment"], ignoreOrder: true);
@@ -54,7 +54,7 @@ public sealed class OtOpcUaServerHostedServiceStartFailureTests
state.Status.ShouldBe(OpcUaEndpointStatus.Failed);
state.FailureReason.ShouldNotBeNullOrWhiteSpace();
var probe = await new OpcUaEndpointHealthCheck(state).CheckHealthAsync(new HealthCheckContext());
var probe = await new OpcUaEndpointHealthCheck(state).CheckHealthAsync(new HealthCheckContext(), TestContext.Current.CancellationToken);
probe.Status.ShouldBe(HealthStatus.Unhealthy);
probe.Description.ShouldNotBeNull();
probe.Description.ShouldContain("failed to start");
@@ -29,8 +29,8 @@ public sealed class FailStartExitTests
psi.Environment["Cluster__SeedNodes__0"] = "not-a-valid-address";
using var proc = Process.Start(psi)!;
var stdout = proc.StandardOutput.ReadToEndAsync();
var stderr = proc.StandardError.ReadToEndAsync();
var stdout = proc.StandardOutput.ReadToEndAsync(TestContext.Current.CancellationToken);
var stderr = proc.StandardError.ReadToEndAsync(TestContext.Current.CancellationToken);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
try { await proc.WaitForExitAsync(cts.Token); }
@@ -7,11 +7,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit"/>
<PackageReference Include="xunit.v3" VersionOverride="3.2.2"/>
<PackageReference Include="Shouldly"/>
<!-- NodeCommandGrpcServiceTests registers a TestProbe under NodeCommunicationActorKey, so the
service is asserted against the same ActorRegistry seam the Host wires in production. -->
<PackageReference Include="Akka.TestKit.Xunit2"/>
<PackageReference Include="Akka.TestKit.Xunit"/>
<!-- ConfigDbApiKeyStoreTests drives the store against a real EF query, the same in-memory
provider convention the ControlPlane/Runtime test projects use. No central SQL Server. -->
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory"/>
@@ -1,5 +1,5 @@
using Akka.Actor;
using Akka.TestKit.Xunit2;
using Akka.TestKit.Xunit;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
@@ -56,7 +56,7 @@ public class EventStreamCommandDeliveryTests : RuntimeActorTestBase
// EventStream subscription. Without this the relay below races the subscription.
var warmup = NewDispatch();
driverHost.Tell(warmup);
ackRouter.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(15)).DeploymentId.ShouldBe(warmup.DeploymentId);
ackRouter.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(15), cancellationToken: TestContext.Current.CancellationToken).DeploymentId.ShouldBe(warmup.DeploymentId);
// Now the real assertion — relayed exactly as central would send it. The comm actor only
// publishes node-locally, so the host must be subscribed for this to arrive at all.
@@ -66,7 +66,7 @@ public class EventStreamCommandDeliveryTests : RuntimeActorTestBase
// The deployment id does not exist, so the host answers Failed. That is the stronger
// assertion, not a weaker one: an ack at all proves the command was received and handled,
// and only PreStart's EventStream subscription can have delivered it.
ackRouter.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(15)).DeploymentId.ShouldBe(relayed.DeploymentId);
ackRouter.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(15), cancellationToken: TestContext.Current.CancellationToken).DeploymentId.ShouldBe(relayed.DeploymentId);
}
[Fact]
@@ -83,7 +83,7 @@ public class EventStreamCommandDeliveryTests : RuntimeActorTestBase
var dispatch = NewDispatch();
driverHost.Tell(dispatch);
ackRouter.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(15)).DeploymentId.ShouldBe(dispatch.DeploymentId);
ackRouter.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(15), cancellationToken: TestContext.Current.CancellationToken).DeploymentId.ShouldBe(dispatch.DeploymentId);
}
}
@@ -150,12 +150,12 @@ akka {
// Tell INSIDE the assertion window alongside the relay, which made the test vacuous — the
// direct Tell alone produced the log the assertion was waiting for.
EventFilter.Debug(contains: "ignoring AlarmCommand")
.ExpectOne(TimeSpan.FromSeconds(15), () => host.Tell(cmd));
.ExpectOne(TimeSpan.FromSeconds(15), () => host.Tell(cmd), TestContext.Current.CancellationToken);
// The real assertion: relayed ONLY through the comm actor, which publishes node-locally.
// The host owns no alarms, so it logs and stops — still a positive delivery signal, because
// that line is only reached if the message arrived.
EventFilter.Debug(contains: "ignoring AlarmCommand")
.ExpectOne(TimeSpan.FromSeconds(15), () => comm.Tell(cmd));
.ExpectOne(TimeSpan.FromSeconds(15), () => comm.Tell(cmd), TestContext.Current.CancellationToken);
}
}
@@ -73,7 +73,7 @@ public sealed class GrpcApplyAckSenderTests
return (CentralCommandService.CentralCommandServiceClient)new RecordingClient(calls);
};
await Sender(["http://central-1:4055", "http://central-2:4055"], factory).SendAsync(Ack());
await Sender(["http://central-1:4055", "http://central-2:4055"], factory).SendAsync(Ack(), TestContext.Current.CancellationToken);
consulted.ShouldBe(["http://central-1:4055"]);
calls.Count.ShouldBe(1);
@@ -90,7 +90,7 @@ public sealed class GrpcApplyAckSenderTests
"disk full",
new CorrelationId(Guid.NewGuid()));
await Sender(["http://central-1:4055"], _ => new RecordingClient(calls)).SendAsync(ack);
await Sender(["http://central-1:4055"], _ => new RecordingClient(calls)).SendAsync(ack, TestContext.Current.CancellationToken);
var sent = calls.Single().Request;
sent.DeploymentId.ShouldBe(ack.DeploymentId.ToString());
@@ -109,7 +109,7 @@ public sealed class GrpcApplyAckSenderTests
var calls = new List<(ApplyAckMessage Request, CallOptions Options)>();
var before = DateTime.UtcNow;
await Sender(["http://central-1:4055"], _ => new RecordingClient(calls)).SendAsync(Ack());
await Sender(["http://central-1:4055"], _ => new RecordingClient(calls)).SendAsync(Ack(), TestContext.Current.CancellationToken);
var options = calls.Single().Options;
options.Headers.ShouldNotBeNull();
@@ -138,7 +138,7 @@ public sealed class GrpcApplyAckSenderTests
? (CentralCommandService.CentralCommandServiceClient)new SlowThrowingClient(calls, TimeSpan.FromMilliseconds(50))
: new RecordingClient(calls);
await Sender(["http://central-1:4055", "http://central-2:4055"], factory).SendAsync(Ack());
await Sender(["http://central-1:4055", "http://central-2:4055"], factory).SendAsync(Ack(), TestContext.Current.CancellationToken);
calls.Count.ShouldBe(2);
calls[1].Options.Deadline!.Value.ShouldBeGreaterThan(calls[0].Options.Deadline!.Value);
@@ -158,7 +158,7 @@ public sealed class GrpcApplyAckSenderTests
: new RecordingClient(calls);
};
await Sender(["http://central-1:4055", "http://central-2:4055"], factory).SendAsync(Ack());
await Sender(["http://central-1:4055", "http://central-2:4055"], factory).SendAsync(Ack(), TestContext.Current.CancellationToken);
consulted.ShouldBe(["http://central-1:4055", "http://central-2:4055"]);
calls.Count.ShouldBe(1);
@@ -185,7 +185,7 @@ public sealed class GrpcApplyAckSenderTests
};
var ack = Ack();
await Sender(["http://central-1:4055", "http://central-2:4055"], factory, log).SendAsync(ack);
await Sender(["http://central-1:4055", "http://central-2:4055"], factory, log).SendAsync(ack, TestContext.Current.CancellationToken);
attempts.ShouldBe(2);
var warning = log.Entries.Single(e => e.Level == LogLevel.Warning);
@@ -213,7 +213,7 @@ public sealed class GrpcApplyAckSenderTests
: new RecordingClient(calls);
};
await Sender(["http://c1:4055", "http://c2:4055", "http://c3:4055"], factory).SendAsync(Ack());
await Sender(["http://c1:4055", "http://c2:4055", "http://c3:4055"], factory).SendAsync(Ack(), TestContext.Current.CancellationToken);
// c3 is never dialled: the pass stops at the first success, and it does not go back to c1.
consulted.ShouldBe(["http://c1:4055", "http://c2:4055"]);
@@ -230,7 +230,7 @@ public sealed class GrpcApplyAckSenderTests
var factory = (string _) => (CentralCommandService.CentralCommandServiceClient)
new ThrowingClient(new RpcException(new Status(StatusCode.Unavailable, "down")));
await Sender(["http://central-1:4055"], factory, log).SendAsync(Ack());
await Sender(["http://central-1:4055"], factory, log).SendAsync(Ack(), TestContext.Current.CancellationToken);
log.Entries.ShouldNotBeEmpty();
log.Entries.Any(e => e.Message.Contains(Psk, StringComparison.Ordinal)).ShouldBeFalse();
@@ -251,7 +251,7 @@ public sealed class GrpcApplyAckSenderTests
return (CentralCommandService.CentralCommandServiceClient)new RecordingClient([]);
};
await Sender([], factory, log).SendAsync(Ack());
await Sender([], factory, log).SendAsync(Ack(), TestContext.Current.CancellationToken);
dialled.ShouldBeFalse();
log.Entries.Single(e => e.Level == LogLevel.Warning).Message
@@ -269,7 +269,7 @@ public sealed class GrpcApplyAckSenderTests
var calls = new List<(ApplyAckMessage Request, CallOptions Options)>();
await Sender(["http://central-1:4055"], _ => new RecordingClient(calls), log, roles: ["driver"])
.SendAsync(Ack());
.SendAsync(Ack(), TestContext.Current.CancellationToken);
calls.Single().Options.Headers!.Any(h =>
h.Key.Equals("x-otopcua-cluster", StringComparison.Ordinal)).ShouldBeFalse();
@@ -311,7 +311,7 @@ public sealed class GrpcApplyAckSenderTests
Options.Create(new AkkaClusterOptions { Roles = ["driver", "cluster-SITE-A"] }),
log);
await sender.SendAsync(Ack());
await sender.SendAsync(Ack(), TestContext.Current.CancellationToken);
// The post must have failed (nothing is listening) — which is what put a channel in the cache.
log.Entries.Any(e => e.Level == LogLevel.Warning).ShouldBeTrue();
@@ -28,7 +28,7 @@ public class NodeCommunicationActorTests : RuntimeActorTestBase
var dispatch = NewDispatch();
actor.Tell(dispatch);
subscriber.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(5)).ShouldBe(dispatch);
subscriber.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).ShouldBe(dispatch);
}
[Fact]
@@ -43,8 +43,8 @@ public class NodeCommunicationActorTests : RuntimeActorTestBase
actor.Tell(NewDispatch());
subscriber.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(5));
subscriber.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
subscriber.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
subscriber.ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
}
[Fact]
@@ -57,7 +57,7 @@ public class NodeCommunicationActorTests : RuntimeActorTestBase
var msg = new RestartDriver("C1", "driver-1", "alice", Guid.NewGuid());
actor.Tell(msg);
subscriber.ExpectMsg<RestartDriver>(TimeSpan.FromSeconds(5)).ShouldBe(msg);
subscriber.ExpectMsg<RestartDriver>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).ShouldBe(msg);
}
[Fact]
@@ -70,7 +70,7 @@ public class NodeCommunicationActorTests : RuntimeActorTestBase
var msg = new ReconnectDriver("C1", "driver-1", "alice", Guid.NewGuid());
actor.Tell(msg);
subscriber.ExpectMsg<ReconnectDriver>(TimeSpan.FromSeconds(5)).ShouldBe(msg);
subscriber.ExpectMsg<ReconnectDriver>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).ShouldBe(msg);
}
[Fact]
@@ -86,7 +86,7 @@ public class NodeCommunicationActorTests : RuntimeActorTestBase
var msg = new AlarmCommand("alarm-1", "Acknowledge", "alice", "comment", null);
actor.Tell(msg);
subscriber.ExpectMsg<AlarmCommand>(TimeSpan.FromSeconds(5)).ShouldBe(msg);
subscriber.ExpectMsg<AlarmCommand>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).ShouldBe(msg);
}
[Fact]
@@ -104,7 +104,7 @@ public class NodeCommunicationActorTests : RuntimeActorTestBase
new CorrelationId(Guid.NewGuid()));
EventFilter.Warning(contains: "dropping ApplyAck").ExpectOne(
TimeSpan.FromSeconds(5), () => actor.Tell(ack));
TimeSpan.FromSeconds(5), () => actor.Tell(ack), TestContext.Current.CancellationToken);
}
// ---- Mode=Grpc (the cross-mesh ack leg) -----------------------------------------------------
@@ -129,9 +129,9 @@ public class NodeCommunicationActorTests : RuntimeActorTestBase
var ack = NewAck();
actor.Tell(ack);
AwaitAssert(() => sender.Sent.Count.ShouldBe(1), TimeSpan.FromSeconds(5));
AwaitAssert(() => sender.Sent.Count.ShouldBe(1), TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
sender.Sent[0].ShouldBe(ack);
ExpectNoMsg(TimeSpan.FromMilliseconds(300));
ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
sender.Sent.Count.ShouldBe(1);
}
@@ -148,7 +148,7 @@ public class NodeCommunicationActorTests : RuntimeActorTestBase
var dispatch = NewDispatch();
actor.Tell(dispatch);
subscriber.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(5)).ShouldBe(dispatch);
subscriber.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).ShouldBe(dispatch);
sender.Sent.ShouldBeEmpty();
}
@@ -119,9 +119,9 @@ public sealed class LocalDbDeploymentArtifactCacheTests : IDisposable
var artifact = RandomArtifact(300 * 1024);
var cache = CreateCache();
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", artifact);
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", artifact, TestContext.Current.CancellationToken);
var cached = await cache.GetCurrentAsync(ClusterA);
var cached = await cache.GetCurrentAsync(ClusterA, TestContext.Current.CancellationToken);
cached.ShouldNotBeNull();
cached.DeploymentId.ShouldBe("dep-1");
@@ -135,15 +135,15 @@ public sealed class LocalDbDeploymentArtifactCacheTests : IDisposable
{
var cache = CreateCache();
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", RandomArtifact(64));
await cache.StoreAsync(ClusterA, "dep-2", "rev-2", RandomArtifact(64));
await cache.StoreAsync(ClusterA, "dep-3", "rev-3", RandomArtifact(64));
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", RandomArtifact(64), TestContext.Current.CancellationToken);
await cache.StoreAsync(ClusterA, "dep-2", "rev-2", RandomArtifact(64), TestContext.Current.CancellationToken);
await cache.StoreAsync(ClusterA, "dep-3", "rev-3", RandomArtifact(64), TestContext.Current.CancellationToken);
// Bounded retention is what stops a node that redeploys daily from filling its disk with
// address spaces nobody will ever roll back to.
(await DistinctDeploymentIdsAsync()).ShouldBe(["dep-2", "dep-3"]);
var cached = await cache.GetCurrentAsync(ClusterA);
var cached = await cache.GetCurrentAsync(ClusterA, TestContext.Current.CancellationToken);
cached.ShouldNotBeNull();
cached.DeploymentId.ShouldBe("dep-3");
}
@@ -163,20 +163,20 @@ public sealed class LocalDbDeploymentArtifactCacheTests : IDisposable
var cache = CreateCache();
var newest = RandomArtifact(1024);
await cache.StoreAsync(ClusterA, "dep-old-1", "rev-1", RandomArtifact(1024));
await cache.StoreAsync(ClusterA, "dep-old-2", "rev-2", RandomArtifact(1024));
await cache.StoreAsync(ClusterA, "dep-old-1", "rev-1", RandomArtifact(1024), TestContext.Current.CancellationToken);
await cache.StoreAsync(ClusterA, "dep-old-2", "rev-2", RandomArtifact(1024), TestContext.Current.CancellationToken);
await _db.ExecuteAsync(
"UPDATE deployment_artifacts SET cached_at_utc = @Future",
new { Future = "2099-01-01T00:00:00.0000000Z" });
new { Future = "2099-01-01T00:00:00.0000000Z" }, TestContext.Current.CancellationToken);
await cache.StoreAsync(ClusterA, "dep-new", "rev-3", newest);
await cache.StoreAsync(ClusterA, "dep-new", "rev-3", newest, TestContext.Current.CancellationToken);
// The pointer names dep-new, so dep-new's chunks must still be there...
(await CountChunksAsync("dep-new")).ShouldBeGreaterThan(0);
// ...and it must actually read back, which is the behaviour that matters.
var cached = await cache.GetCurrentAsync(ClusterA);
var cached = await cache.GetCurrentAsync(ClusterA, TestContext.Current.CancellationToken);
cached.ShouldNotBeNull();
cached.DeploymentId.ShouldBe("dep-new");
cached.Artifact.ShouldBe(newest);
@@ -186,31 +186,31 @@ public sealed class LocalDbDeploymentArtifactCacheTests : IDisposable
public async Task GetCurrent_ReturnsNullWhenAChunkIsCorrupt()
{
var cache = CreateCache();
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", RandomArtifact(300 * 1024));
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", RandomArtifact(300 * 1024), TestContext.Current.CancellationToken);
// Valid base64 of the wrong bytes: this passes decoding and the chunk-count check, so only
// the SHA-256 comparison can catch it. That is the check being pinned.
await _db.ExecuteAsync(
"UPDATE deployment_artifacts SET chunk_base64 = @Chunk WHERE deployment_id = @DeploymentId AND chunk_index = 1",
new { Chunk = Convert.ToBase64String(RandomArtifact(128 * 1024)), DeploymentId = "dep-1" });
new { Chunk = Convert.ToBase64String(RandomArtifact(128 * 1024)), DeploymentId = "dep-1" }, TestContext.Current.CancellationToken);
(await cache.GetCurrentAsync(ClusterA)).ShouldBeNull();
(await cache.GetCurrentAsync(ClusterA, TestContext.Current.CancellationToken)).ShouldBeNull();
}
[Fact]
public async Task GetCurrent_ReturnsNullWhenAChunkIsMissing()
{
var cache = CreateCache();
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", RandomArtifact(300 * 1024));
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", RandomArtifact(300 * 1024), TestContext.Current.CancellationToken);
// A partially replicated artifact is the realistic version of this: the pointer row arrives
// before the last chunk does. Reassembling what is present would yield a plausible-looking
// but silently truncated address space.
await _db.ExecuteAsync(
"DELETE FROM deployment_artifacts WHERE deployment_id = @DeploymentId AND chunk_index = 2",
new { DeploymentId = "dep-1" });
new { DeploymentId = "dep-1" }, TestContext.Current.CancellationToken);
(await cache.GetCurrentAsync(ClusterA)).ShouldBeNull();
(await cache.GetCurrentAsync(ClusterA, TestContext.Current.CancellationToken)).ShouldBeNull();
}
[Fact]
@@ -218,7 +218,7 @@ public sealed class LocalDbDeploymentArtifactCacheTests : IDisposable
{
var cache = CreateCache();
(await cache.GetCurrentAsync(ClusterA)).ShouldBeNull();
(await cache.GetCurrentAsync(ClusterA, TestContext.Current.CancellationToken)).ShouldBeNull();
}
[Fact]
@@ -226,17 +226,17 @@ public sealed class LocalDbDeploymentArtifactCacheTests : IDisposable
{
var cache = CreateCache();
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", RandomArtifact(300 * 1024));
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", RandomArtifact(300 * 1024), TestContext.Current.CancellationToken);
// The re-store is smaller, so a delete-before-insert that did not happen would leave the
// tail chunks of the first artifact behind — orphans that also break the chunk-count check.
var second = RandomArtifact(200 * 1024);
await cache.StoreAsync(ClusterA, "dep-1", "rev-1b", second);
await cache.StoreAsync(ClusterA, "dep-1", "rev-1b", second, TestContext.Current.CancellationToken);
(await CountChunksAsync("dep-1")).ShouldBe(2);
(await CountChunksAsync()).ShouldBe(2);
var cached = await cache.GetCurrentAsync(ClusterA);
var cached = await cache.GetCurrentAsync(ClusterA, TestContext.Current.CancellationToken);
cached.ShouldNotBeNull();
cached.RevisionHash.ShouldBe("rev-1b");
cached.Artifact.ShouldBe(second);
@@ -253,18 +253,18 @@ public sealed class LocalDbDeploymentArtifactCacheTests : IDisposable
var cache = CreateCache();
var artifact = RandomArtifact(300 * 1024);
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", artifact);
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", artifact, TestContext.Current.CancellationToken);
var oplogDepth = await OplogDepthAsync();
var rowVersions = await RowVersionsAsync();
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", artifact);
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", artifact, TestContext.Current.CancellationToken);
(await OplogDepthAsync()).ShouldBe(oplogDepth);
(await RowVersionsAsync()).ShouldBe(rowVersions);
// Skipping must not cost readability: the artifact still reads back intact.
var cached = await cache.GetCurrentAsync(ClusterA);
var cached = await cache.GetCurrentAsync(ClusterA, TestContext.Current.CancellationToken);
cached.ShouldNotBeNull();
cached.DeploymentId.ShouldBe("dep-1");
cached.Artifact.ShouldBe(artifact);
@@ -280,16 +280,16 @@ public sealed class LocalDbDeploymentArtifactCacheTests : IDisposable
var cache = CreateCache();
var artifact = RandomArtifact(300 * 1024);
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", artifact);
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", artifact, TestContext.Current.CancellationToken);
await _db.ExecuteAsync(
"DELETE FROM deployment_artifacts WHERE deployment_id = @DeploymentId AND chunk_index = 2",
new { DeploymentId = "dep-1" });
(await cache.GetCurrentAsync(ClusterA)).ShouldBeNull();
new { DeploymentId = "dep-1" }, TestContext.Current.CancellationToken);
(await cache.GetCurrentAsync(ClusterA, TestContext.Current.CancellationToken)).ShouldBeNull();
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", artifact);
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", artifact, TestContext.Current.CancellationToken);
(await CountChunksAsync("dep-1")).ShouldBe(3);
var cached = await cache.GetCurrentAsync(ClusterA);
var cached = await cache.GetCurrentAsync(ClusterA, TestContext.Current.CancellationToken);
cached.ShouldNotBeNull();
cached.Artifact.ShouldBe(artifact);
}
@@ -303,12 +303,12 @@ public sealed class LocalDbDeploymentArtifactCacheTests : IDisposable
// space with every id-shaped signal saying it was correct.
var cache = CreateCache();
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", RandomArtifact(1024));
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", RandomArtifact(1024), TestContext.Current.CancellationToken);
var rebuilt = RandomArtifact(1024);
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", rebuilt);
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", rebuilt, TestContext.Current.CancellationToken);
var cached = await cache.GetCurrentAsync(ClusterA);
var cached = await cache.GetCurrentAsync(ClusterA, TestContext.Current.CancellationToken);
cached.ShouldNotBeNull();
cached.Artifact.ShouldBe(rebuilt);
}
@@ -319,9 +319,9 @@ public sealed class LocalDbDeploymentArtifactCacheTests : IDisposable
var artifact = RandomArtifact(300 * 1024);
var cache = CreateCache();
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", artifact);
await cache.StoreAsync(ClusterA, "dep-1", "rev-1", artifact, TestContext.Current.CancellationToken);
var cached = await cache.GetCurrentUnkeyedAsync();
var cached = await cache.GetCurrentUnkeyedAsync(TestContext.Current.CancellationToken);
cached.ShouldNotBeNull();
cached.DeploymentId.ShouldBe("dep-1");
@@ -335,10 +335,10 @@ public sealed class LocalDbDeploymentArtifactCacheTests : IDisposable
var logger = new CapturingLogger();
var cache = CreateCache(logger);
await cache.StoreAsync(ClusterA, "dep-a", "rev-a", RandomArtifact(1024));
await cache.StoreAsync(ClusterB, "dep-b", "rev-b", newest);
await cache.StoreAsync(ClusterA, "dep-a", "rev-a", RandomArtifact(1024), TestContext.Current.CancellationToken);
await cache.StoreAsync(ClusterB, "dep-b", "rev-b", newest, TestContext.Current.CancellationToken);
var cached = await cache.GetCurrentUnkeyedAsync();
var cached = await cache.GetCurrentUnkeyedAsync(TestContext.Current.CancellationToken);
cached.ShouldNotBeNull();
cached.DeploymentId.ShouldBe("dep-b");
@@ -100,7 +100,7 @@ public sealed class AbCipAlmaNativeAlarmEndToEndTests : RuntimeActorTestBase
drop.State.Message.ShouldContain("H alarm");
drop.State.Message.ShouldNotContain("HH");
drop.State.Severity.ShouldBe((ushort)600);
alerts.ExpectNoMsg(Quiet);
alerts.ExpectNoMsg(Quiet, TestContext.Current.CancellationToken);
// The PLC acknowledges every declared limit — ONE "Acknowledged" row.
foreach (var a in new[] { "HHAcked", "HAcked", "LAcked", "LLAcked" }) plc.Set(a, true);
@@ -116,7 +116,7 @@ public sealed class AbCipAlmaNativeAlarmEndToEndTests : RuntimeActorTestBase
plc.Set("HHInAlarm", false);
var back = NextUpdate(publish);
back.State.Acknowledged.ShouldBeFalse("the unacknowledged HH excursion keeps the condition unacknowledged");
alerts.ExpectNoMsg(Quiet);
alerts.ExpectNoMsg(Quiet, TestContext.Current.CancellationToken);
// Positive control: the clear MUST write its row (the absence checks above would pass on a dead host).
plc.Set("HInAlarm", false);
@@ -27,7 +27,7 @@ public sealed class ActorNodeWriteGatewayTests : RuntimeActorTestBase
var writeTask = gateway.WriteAsync("eq-1/speed", 123.0, AddressSpaceRealm.Uns, CancellationToken.None);
var routed = probe.ExpectMsg<DriverHostActor.RouteNodeWrite>(TimeSpan.FromSeconds(5));
var routed = probe.ExpectMsg<DriverHostActor.RouteNodeWrite>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
routed.NodeId.ShouldBe("eq-1/speed");
routed.Value.ShouldBe(123.0);
probe.Reply(new DriverHostActor.NodeWriteResult(true, null));
@@ -46,7 +46,7 @@ public sealed class ActorNodeWriteGatewayTests : RuntimeActorTestBase
var writeTask = gateway.WriteAsync("eq-1/speed", 123.0, AddressSpaceRealm.Uns, CancellationToken.None);
probe.ExpectMsg<DriverHostActor.RouteNodeWrite>(TimeSpan.FromSeconds(5));
probe.ExpectMsg<DriverHostActor.RouteNodeWrite>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
probe.Reply(new DriverHostActor.NodeWriteResult(false, "not primary"));
var outcome = await writeTask;
@@ -82,7 +82,7 @@ public sealed class CalculationDependencyFlowTests : RuntimeActorTestBase
published.ShouldContain(e => e.FullReference == "calc/engine/out" && Equals(e.Snapshot.Value, 42.0));
// Leg 2 — calc-of-calc: A's 42 re-entered the mux → B computes 43 on calc/engine/b.
published.ShouldContain(e => e.FullReference == "calc/engine/b" && Equals(e.Snapshot.Value, 43.0));
}, PresenceBudget, TimeSpan.FromMilliseconds(100));
}, PresenceBudget, TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken);
}
/// <summary>
@@ -128,7 +128,7 @@ public sealed class CalculationDependencyFlowTests : RuntimeActorTestBase
mux.Tell(ValuePublishes.Single("src-drv", "src/a", 2.0, OpcUaStatus.Good, Ts));
published.ShouldContain(e => e.FullReference == "calc/keep" && Equals(e.Snapshot.Value, 20.0));
published.ShouldContain(e => e.FullReference == "calc/remove" && Equals(e.Snapshot.Value, 7.0));
}, PresenceBudget, TimeSpan.FromMilliseconds(100));
}, PresenceBudget, TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken);
// ---- IN-PLACE RE-BIND (driver ReinitializeAsync) ----
// New authored set: "keep" script EDITED to src/a*1000; "add" is a NEW tag reading a NEW dep src/b;
@@ -157,11 +157,11 @@ public sealed class CalculationDependencyFlowTests : RuntimeActorTestBase
published.ShouldContain(e => e.FullReference == "calc/add" && Equals(e.Snapshot.Value, 150.0));
// (c) edited script produces the NEW result: 3 * 1000 = 3000 (NOT the pre-edit 3 * 10 = 30).
published.ShouldContain(e => e.FullReference == "calc/keep" && Equals(e.Snapshot.Value, 3000.0));
}, PresenceBudget, TimeSpan.FromMilliseconds(100));
}, PresenceBudget, TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken);
// (b) the removed tag never recomputes on the post-redeploy src/a=3 (it would be 3 + 5 = 8) and the
// edited "keep" never serves the stale *10 result (3 * 10 = 30). Give any errant compute a moment to land.
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
published.ShouldNotContain(e => e.FullReference == "calc/remove" && Equals(e.Snapshot.Value, 8.0));
published.ShouldNotContain(e => e.FullReference == "calc/keep" && Equals(e.Snapshot.Value, 30.0));
}
@@ -67,7 +67,7 @@ public sealed class DriverHostActorApplyingStashTests()
{
// A is in flight: the fetch has started and its task will not complete until released.
actor.Tell(new DispatchDeployment(deploymentA, RevA, CorrelationId.NewId()));
AwaitAssert(() => fetcher.Calls.ShouldBe(1), duration: PresenceBudget);
AwaitAssert(() => fetcher.Calls.ShouldBe(1), duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
var logs = SubscribeToLogs("deferring");
@@ -86,8 +86,8 @@ public sealed class DriverHostActorApplyingStashTests()
// Nothing was lost: B re-enters exactly once, after A's apply transitions back to Steady.
fetcher.Release();
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10), cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(ApplyAckOutcome.Applied);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10), cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(ApplyAckOutcome.Applied);
fetcher.Calls.ShouldBe(2);
}
finally
@@ -137,7 +137,7 @@ public sealed class DriverHostActorApplyingStashTests()
try
{
actor.Tell(new DispatchDeployment(deploymentA, RevA, CorrelationId.NewId()));
AwaitAssert(() => fetcher.Calls.ShouldBe(1), duration: PresenceBudget);
AwaitAssert(() => fetcher.Calls.ShouldBe(1), duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
var logs = SubscribeToLogs("deferring", "already deferred");
@@ -152,9 +152,9 @@ public sealed class DriverHostActorApplyingStashTests()
// No second stash, therefore no second re-delivery: exactly one Applied ack for B.
fetcher.Release();
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10), cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(ApplyAckOutcome.Applied);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10), cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(ApplyAckOutcome.Applied);
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
fetcher.Calls.ShouldBe(2);
}
finally
@@ -194,24 +194,24 @@ public sealed class DriverHostActorApplyingStashTests()
try
{
actor.Tell(new DispatchDeployment(deploymentA, RevA, CorrelationId.NewId()));
AwaitAssert(() => fetcher.Calls.ShouldBe(1), duration: PresenceBudget);
AwaitAssert(() => fetcher.Calls.ShouldBe(1), duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
actor.Tell(new DispatchDeployment(deploymentB, RevB, CorrelationId.NewId()));
// A completes; B unstashes and starts its own (still gated) fetch.
fetcher.Release(1);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10)).DeploymentId.ShouldBe(deploymentA);
AwaitAssert(() => fetcher.Calls.ShouldBe(2), duration: PresenceBudget);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10), cancellationToken: TestContext.Current.CancellationToken).DeploymentId.ShouldBe(deploymentA);
AwaitAssert(() => fetcher.Calls.ShouldBe(2), duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
// C arrives mid-B — a fresh stash cycle, on a set that was cleared when B was released.
actor.Tell(new DispatchDeployment(deploymentC, RevC, CorrelationId.NewId()));
fetcher.Release(2);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10)).DeploymentId.ShouldBe(deploymentB);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10)).DeploymentId.ShouldBe(deploymentC);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10), cancellationToken: TestContext.Current.CancellationToken).DeploymentId.ShouldBe(deploymentB);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10), cancellationToken: TestContext.Current.CancellationToken).DeploymentId.ShouldBe(deploymentC);
actor.Tell(new GetDiagnostics(CorrelationId.NewId()), coordinator.Ref);
coordinator.ExpectMsg<NodeDiagnosticsSnapshot>(TimeSpan.FromSeconds(5))
coordinator.ExpectMsg<NodeDiagnosticsSnapshot>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken)
.CurrentRevision.ShouldBe(RevC);
}
finally
@@ -251,7 +251,7 @@ public sealed class DriverHostActorApplyingStashTests()
try
{
EventFilter.Warning(contains: deploymentA.ToString()).ExpectOne(TimeSpan.FromSeconds(10), () =>
actor.Tell(new DispatchDeployment(deploymentA, RevA, CorrelationId.NewId())));
actor.Tell(new DispatchDeployment(deploymentA, RevA, CorrelationId.NewId())), TestContext.Current.CancellationToken);
}
finally
{
@@ -285,7 +285,7 @@ public sealed class DriverHostActorApplyingStashTests()
var logs = SubscribeToLogs("appears WEDGED");
actor.Tell(new DispatchDeployment(deploymentA, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(10), cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(ApplyAckOutcome.Applied);
// Well past the (200 ms) delay — a live timer would have fired several times over.
logs.Drain(SpinWindow);
@@ -42,9 +42,9 @@ public sealed class DriverHostActorArtifactCacheTests : RuntimeActorTestBase
deploymentArtifactCache: cache));
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(ApplyAckOutcome.Applied);
AwaitAssert(() => cache.Stores.Count.ShouldBe(1), duration: TimeSpan.FromSeconds(3));
AwaitAssert(() => cache.Stores.Count.ShouldBe(1), duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
var store = cache.Stores[0];
store.DeploymentId.ShouldBe(deploymentId.ToString());
@@ -69,9 +69,9 @@ public sealed class DriverHostActorArtifactCacheTests : RuntimeActorTestBase
deploymentArtifactCache: cache));
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5));
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
AwaitAssert(() => cache.Stores.Count.ShouldBe(1), duration: TimeSpan.FromSeconds(3));
AwaitAssert(() => cache.Stores.Count.ShouldBe(1), duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
cache.Stores[0].ClusterId.ShouldBe("SITE-A");
}
@@ -89,9 +89,9 @@ public sealed class DriverHostActorArtifactCacheTests : RuntimeActorTestBase
deploymentArtifactCache: cache));
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5));
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
AwaitAssert(() => cache.Stores.Count.ShouldBe(1), duration: TimeSpan.FromSeconds(3));
AwaitAssert(() => cache.Stores.Count.ShouldBe(1), duration: TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
cache.Stores[0].ClusterId.ShouldBe("__single");
}
@@ -132,7 +132,7 @@ public sealed class DriverHostActorArtifactCacheTests : RuntimeActorTestBase
// Since #486 the apply FAILS rather than reporting a success it did not achieve: empty bytes are
// what an unreadable artifact looks like, not a legitimate no-op deployment. (A configuration that
// genuinely declares nothing still serialises to a JSON document with empty arrays.)
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Failed);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(ApplyAckOutcome.Failed);
// The point of this test is unchanged, and now holds for two independent reasons: nothing is cached.
Thread.Sleep(500);
@@ -156,11 +156,11 @@ public sealed class DriverHostActorArtifactCacheTests : RuntimeActorTestBase
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(ApplyAckOutcome.Applied);
// Exactly one ACK, and it says Applied. A second Failed ACK arriving here would mean the
// cache failure had corrupted the deployment protocol.
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
}
[Fact]
@@ -177,7 +177,7 @@ public sealed class DriverHostActorArtifactCacheTests : RuntimeActorTestBase
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(ApplyAckOutcome.Applied);
}
/// <summary>
@@ -56,21 +56,21 @@ public sealed class DriverHostActorAsyncCacheWriteTests()
try
{
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken).Outcome.ShouldBe(ApplyAckOutcome.Applied);
// The store was entered and has NOT completed — the gate is still closed.
AwaitAssert(() => cache.StoresEntered.ShouldBe(1), duration: PresenceBudget);
AwaitAssert(() => cache.StoresEntered.ShouldBe(1), duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
cache.StoresCompleted.ShouldBe(0);
// THE assertion: with the store still in flight the host is still processing its mailbox.
// A blocking `.GetAwaiter().GetResult()` parks the actor thread here and this times out.
actor.Tell(new GetDiagnostics(CorrelationId.NewId()), coordinator.Ref);
coordinator.ExpectMsg<NodeDiagnosticsSnapshot>(TimeSpan.FromSeconds(3));
coordinator.ExpectMsg<NodeDiagnosticsSnapshot>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);
// Completion is observed rather than fire-and-forgotten: the store pipes back to Self and
// the handler logs it, so an operator can tell a cached apply from an uncached one.
EventFilter.Info(contains: "cached applied deployment")
.ExpectOne(TimeSpan.FromSeconds(5), () => cache.Release());
.ExpectOne(TimeSpan.FromSeconds(5), () => cache.Release(), TestContext.Current.CancellationToken);
}
finally
{
@@ -107,9 +107,9 @@ public sealed class DriverHostActorAsyncCacheWriteTests()
{
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
});
}, TestContext.Current.CancellationToken);
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
}
/// <summary>Seeds a sealed, parseable deployment and returns its id.</summary>
@@ -57,7 +57,7 @@ public sealed class DriverHostActorBatchFanOutTests : RuntimeActorTestBase
new DriverInstanceActor.AttributeValueItem("Plant/Modbus/dev1/temp", 7.5, OpcUaStatus.BadNoCommunication, TsB),
}));
var batch = publish.ExpectMsg<OpcUaPublishActor.AttributeValueBatchUpdate>(PresenceBudget);
var batch = publish.ExpectMsg<OpcUaPublishActor.AttributeValueBatchUpdate>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
batch.Items.Count.ShouldBe(3);
var byId = batch.Items.ToDictionary(w => w.NodeId);
@@ -85,7 +85,7 @@ public sealed class DriverHostActorBatchFanOutTests : RuntimeActorTestBase
byId["Plant/Modbus/dev1/temp"].Realm.ShouldBe(AddressSpaceRealm.Raw);
// ONE message, not one per value.
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
}
/// <summary>An item whose ref is bound to no NodeId is dropped; the rest of the burst still lands in the
@@ -106,7 +106,7 @@ public sealed class DriverHostActorBatchFanOutTests : RuntimeActorTestBase
new DriverInstanceActor.AttributeValueItem("Plant/Modbus/dev1/speed", 42.0, OpcUaStatus.Good, TsA),
}));
var batch = publish.ExpectMsg<OpcUaPublishActor.AttributeValueBatchUpdate>(PresenceBudget);
var batch = publish.ExpectMsg<OpcUaPublishActor.AttributeValueBatchUpdate>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
batch.Items.Count.ShouldBe(1);
batch.Items[0].NodeId.ShouldBe("Plant/Modbus/dev1/speed");
batch.Items[0].Value.ShouldBe(42.0);
@@ -116,7 +116,7 @@ public sealed class DriverHostActorBatchFanOutTests : RuntimeActorTestBase
{
new DriverInstanceActor.AttributeValueItem("Plant/Modbus/dev1/speed", 43.0, OpcUaStatus.Good, TsB),
}));
publish.ExpectMsg<OpcUaPublishActor.AttributeValueBatchUpdate>(PresenceBudget)
publish.ExpectMsg<OpcUaPublishActor.AttributeValueBatchUpdate>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken)
.Items[0].Value.ShouldBe(43.0);
}
@@ -142,11 +142,11 @@ public sealed class DriverHostActorBatchFanOutTests : RuntimeActorTestBase
new DriverInstanceActor.AttributeValueItem("Plant/Modbus/dev1/temp", 7.5, OpcUaStatus.Good, TsB),
}));
var fwd = mux.ExpectMsg<DriverInstanceActor.AttributeValueBatchPublished>(PresenceBudget);
var fwd = mux.ExpectMsg<DriverInstanceActor.AttributeValueBatchPublished>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
fwd.DriverInstanceId.ShouldBe("drv-1");
fwd.Items.Select(i => i.FullReference).ShouldBe(
new[] { "Plant/Modbus/dev1/speed", "Plant/Modbus/dev1/temp" });
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
}
/// <summary>A burst in which NOTHING resolves sends no publish message at all — an empty batch would be
@@ -168,9 +168,9 @@ public sealed class DriverHostActorBatchFanOutTests : RuntimeActorTestBase
new DriverInstanceActor.AttributeValueItem("Plant/Modbus/dev1/alsonope", 2.0, OpcUaStatus.Good, TsB),
}));
mux.ExpectMsg<DriverInstanceActor.AttributeValueBatchPublished>(PresenceBudget)
mux.ExpectMsg<DriverInstanceActor.AttributeValueBatchPublished>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken)
.Items.Count.ShouldBe(2);
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
}
/// <summary>A host with NO OPC UA publish actor wired (the ctor default — the dev/Mac shape) still
@@ -191,7 +191,7 @@ public sealed class DriverHostActorBatchFanOutTests : RuntimeActorTestBase
new DriverInstanceActor.AttributeValueItem("Plant/Modbus/dev1/speed", 42.0, OpcUaStatus.Good, TsA),
}));
var fwd = mux.ExpectMsg<DriverInstanceActor.AttributeValueBatchPublished>(PresenceBudget);
var fwd = mux.ExpectMsg<DriverInstanceActor.AttributeValueBatchPublished>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
fwd.DriverInstanceId.ShouldBe("drv-1");
fwd.Items.Single().FullReference.ShouldBe("Plant/Modbus/dev1/speed");
}

Some files were not shown because too many files have changed in this diff Show More