using System.Text;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Hosts;
using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Hosts;
///
/// Review D-b r1, I1: /hosts keys eviction on authored ∪ DEPLOYED drivers, because a running driver
/// whose health is unchanged never re-publishes (fingerprint dedup) — staleness is not a liveness proof.
///
[Trait("Category", "Unit")]
public sealed class DeployedDriverSetTests
{
private static readonly DateTime Now = new(2026, 10, 22, 12, 0, 0, DateTimeKind.Utc);
// Test artifacts are a plain comma list; the real parser is DeploymentArtifact.ParseDriverInstances.
private static IEnumerable Parse(byte[] blob) =>
Encoding.UTF8.GetString(blob).Split(',', StringSplitOptions.RemoveEmptyEntries);
private static byte[] Artifact(params string[] driverIds) => Encoding.UTF8.GetBytes(string.Join(",", driverIds));
private sealed class Factory(string name) : IDbContextFactory
{
public int Created;
public OtOpcUaConfigDbContext CreateDbContext()
{
Created++;
return new(new DbContextOptionsBuilder().UseInMemoryDatabase(name).Options);
}
public Task CreateDbContextAsync(CancellationToken ct = default) => Task.FromResult(CreateDbContext());
}
private static Deployment Dep(Guid id, DeploymentStatus status, DateTime created, byte[] blob) =>
new() { DeploymentId = id, RevisionHash = id.ToString("N"), CreatedBy = "t", Status = status, CreatedAtUtc = created, ArtifactBlob = blob };
private static NodeDeploymentState Node(string node, Guid dep, NodeDeploymentStatus status, DateTime started) =>
new() { NodeId = node, DeploymentId = dep, Status = status, StartedAtUtc = started };
[Fact]
public async Task Quiet_running_driver_deleted_in_raw_survives_the_sweep()
{
// drv-quiet is running (in the deployed artifact), deleted in /raw (not authored), and published
// once 5 minutes ago — a Faulted driver with a steady error never re-publishes.
var factory = new Factory(Guid.NewGuid().ToString("N"));
var d1 = Guid.NewGuid();
await using (var db = factory.CreateDbContext())
{
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();
}
var deployed = await new DeployedDriverSet(factory, parseDriverIds: Parse).LoadAsync();
deployed.ShouldNotBeNull();
var store = new InMemoryDriverStatusSnapshotStore();
store.Upsert(new DriverHealthChanged("MAIN", "drv-quiet", "Faulted", null, "refused", 3, Now.AddMinutes(-5)));
store.Upsert(new DriverHealthChanged("MAIN", "drv-gone", "Faulted", null, "refused", 3, Now.AddMinutes(-5)));
var configured = new HashSet(["drv-kept"], StringComparer.OrdinalIgnoreCase); // authored
configured.UnionWith(deployed!);
var removed = store.EvictUnconfiguredStale(configured, Now.AddSeconds(-60));
store.TryGet("drv-quiet", out _).ShouldBeTrue();
// Positive control: an undeployed, unconfigured, stale driver is still evicted.
removed.ShouldBe(1);
store.TryGet("drv-gone", out _).ShouldBeFalse();
}
[Fact]
public async Task Union_covers_each_nodes_latest_and_latest_applied_and_the_latest_sealed()
{
var factory = new Factory(Guid.NewGuid().ToString("N"));
Guid old = Guid.NewGuid(), failed = Guid.NewGuid(), sealedOther = Guid.NewGuid(), stale = Guid.NewGuid();
await using (var db = factory.CreateDbContext())
{
db.Deployments.Add(Dep(stale, DeploymentStatus.Sealed, Now.AddDays(-9), Artifact("drv-ancient")));
db.Deployments.Add(Dep(old, DeploymentStatus.Sealed, Now.AddHours(-3), Artifact("drv-old")));
db.Deployments.Add(Dep(failed, DeploymentStatus.PartiallyFailed, Now.AddHours(-1), Artifact("drv-new")));
db.Deployments.Add(Dep(sealedOther, DeploymentStatus.Sealed, Now.AddHours(-2), Artifact("drv-sealed")));
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();
}
var deployed = await new DeployedDriverSet(factory, parseDriverIds: Parse).LoadAsync();
// 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.
deployed!.ShouldBe(["drv-new", "drv-old", "drv-sealed"], ignoreOrder: true);
deployed!.ShouldNotContain("drv-ancient");
}
[Fact]
public async Task Artifacts_are_parsed_once_per_deployment_and_pruned()
{
var factory = new Factory(Guid.NewGuid().ToString("N"));
var d1 = Guid.NewGuid();
await using (var db = factory.CreateDbContext())
{
db.Deployments.Add(Dep(d1, DeploymentStatus.Sealed, Now, Artifact("drv-a")));
await db.SaveChangesAsync();
}
var parses = 0;
var set = new DeployedDriverSet(factory, parseDriverIds: b => { parses++; return Parse(b); });
await set.LoadAsync();
await set.LoadAsync();
parses.ShouldBe(1);
set.CachedDeployments.ShouldBe(1);
var d2 = Guid.NewGuid();
await using (var db = factory.CreateDbContext())
{
db.Deployments.Add(Dep(d2, DeploymentStatus.Sealed, Now.AddMinutes(1), Artifact("drv-b")));
await db.SaveChangesAsync();
}
(await set.LoadAsync())!.ShouldBe(["drv-b"]);
set.CachedDeployments.ShouldBe(1); // d1 is no longer live, so it is pruned
parses.ShouldBe(2);
}
[Fact]
public async Task An_unreadable_artifact_means_unknown_so_nothing_is_evicted()
{
var factory = new Factory(Guid.NewGuid().ToString("N"));
var d1 = Guid.NewGuid();
await using (var db = factory.CreateDbContext())
{
db.Deployments.Add(Dep(d1, DeploymentStatus.Sealed, Now, Array.Empty())); // retention-emptied
await db.SaveChangesAsync();
}
(await new DeployedDriverSet(factory, parseDriverIds: Parse).LoadAsync()).ShouldBeNull();
}
[Fact]
public void Hosts_skips_eviction_when_the_deployed_set_is_unknown()
{
var src = AdminUiSource.Read("Components", "Pages", "Hosts.razor");
src.ShouldContain("var deployed = await Deployed.LoadAsync(_cts.Token);");
src.ShouldContain("configured.UnionWith(deployed);");
src.IndexOf("if (deployed is not null)", StringComparison.Ordinal)
.ShouldBeLessThan(src.IndexOf("DriverStore.EvictUnconfiguredStale(", StringComparison.Ordinal));
src.ShouldNotContain("≈ 12 health publishes");
}
}