v3 B1-WP6: migrate Core + Server test projects to greenfield schema

Fix TESTS only (production already green) so the four in-scope test projects
compile + pass under the v3 dark address space:

- Core.Abstractions.Tests: drop AllowedNamespaceKinds/NamespaceKindCompatibility
  from DriverTypeMetadata; rewrite EquipmentTagRefResolver tests to the single-func
  RawPath-lookup contract (no parseRef/transient cache).
- Core.Tests: rewrite EquipmentNodeWalkerTests to EquipmentNamespaceContent(Areas,
  Lines,Equipment,VirtualTags?,ScriptedAlarms?) — folders + VirtualTags +
  ScriptedAlarms only; raw-tag variables dark (skipped Batch-4 placeholder). Drop
  retired Equipment.DriverInstanceId.
- Runtime.Tests: rewrite golden corpus + TagConfigCorpusParityTests to the v3
  RawPath/RawTagEntry round-trip (byte + TagConfigIntent parity; EquipmentTags dark).
  Salvage VirtualTag + ScriptedAlarm artifact parity to the new Compose signature.
  Retire the equipment-tag-materialization parity files (Array/Historize/Alias) and
  the equipment-device-binding DeviceHost parity to documented skipped placeholders.
  Skip 34 dark equipment-tag routing/value/alarm/write/discovery actor tests with a
  shared DarkAddressSpaceReasons constant. Re-key cluster-scoped tests to v3 UNS
  line->area attribution.
- ControlPlane.Tests: ConfigComposer round-trips re-keyed to RawFolder/Device (no
  Namespaces); tag-config gate resolves driver via Device; deploy-gate collision test
  re-keyed to UnsEffectiveNameCollision; drop retired BadCrossClusterNamespaceBinding
  case.
This commit is contained in:
Joseph Doherty
2026-07-15 21:26:00 -04:00
parent 604928b29d
commit 329144b1aa
26 changed files with 425 additions and 1445 deletions
@@ -8,9 +8,8 @@ public sealed class DriverTypeRegistryTests
{
private static DriverTypeMetadata SampleMetadata(
string typeName = "Modbus",
NamespaceKindCompatibility allowed = NamespaceKindCompatibility.Equipment,
DriverTier tier = DriverTier.B) =>
new(typeName, allowed,
new(typeName,
DriverConfigJsonSchema: "{\"type\": \"object\"}",
DeviceConfigJsonSchema: "{\"type\": \"object\"}",
TagConfigJsonSchema: "{\"type\": \"object\"}",
@@ -118,7 +117,7 @@ public sealed class DriverTypeRegistryTests
var registry = new DriverTypeRegistry();
registry.Register(SampleMetadata("Modbus"));
registry.Register(SampleMetadata("S7"));
registry.Register(SampleMetadata("Galaxy", NamespaceKindCompatibility.Equipment));
registry.Register(SampleMetadata("Galaxy"));
var all = registry.All();
@@ -126,22 +125,6 @@ public sealed class DriverTypeRegistryTests
all.Select(m => m.TypeName).ShouldBe(new[] { "Modbus", "S7", "Galaxy" }, ignoreOrder: true);
}
/// <summary>
/// Verifies that NamespaceKindCompatibility flags are implemented as a bitmask.
/// </summary>
[Fact]
public void NamespaceKindCompatibility_FlagsAreBitmask()
{
// Per decision #111 — driver types may be valid for multiple namespace kinds.
var both = NamespaceKindCompatibility.Equipment | NamespaceKindCompatibility.Simulated;
both.HasFlag(NamespaceKindCompatibility.Equipment).ShouldBeTrue();
both.HasFlag(NamespaceKindCompatibility.Simulated).ShouldBeTrue();
// A single-flag value does not carry the other flag — proves these are real bitmask bits.
NamespaceKindCompatibility.Equipment.HasFlag(NamespaceKindCompatibility.Simulated).ShouldBeFalse();
}
/// <summary>
/// Verifies that Get rejects empty or null type names.
/// </summary>
@@ -4,54 +4,65 @@ using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
/// <summary>
/// v3: <see cref="EquipmentTagRefResolver{TDef}"/> is now a thin wrapper over the driver's
/// authored <c>RawPath → TDef</c> lookup. The pre-v3 two-arg ctor (byName + a JSON-blob parse
/// fallback with a transient cache) is retired — a reference is always a RawPath, and a miss is a
/// miss (no parse attempt). These tests pin the single-func RawPath-lookup contract.
/// </summary>
public class EquipmentTagRefResolverTests
{
private sealed record Def(string Id);
private static EquipmentTagRefResolver<Def> Make(
Dictionary<string, Def> byName, Func<string, Def?> parse)
=> new(r => byName.TryGetValue(r, out var d) ? d : null, parse);
private static EquipmentTagRefResolver<Def> Make(Dictionary<string, Def> byRawPath)
=> new(r => byRawPath.TryGetValue(r, out var d) ? d : null);
[Fact]
public void Legacy_name_resolves_via_byName()
public void RawPath_resolves_via_lookup()
{
var r = Make(new() { ["Temp"] = new Def("Temp") }, _ => null);
r.TryResolve("Temp", out var def).ShouldBeTrue();
var r = Make(new() { ["Area/Modbus/Dev/Temp"] = new Def("Temp") });
r.TryResolve("Area/Modbus/Dev/Temp", out var def).ShouldBeTrue();
def!.Id.ShouldBe("Temp");
}
[Fact]
public void Equipment_ref_resolves_via_parse_and_is_cached()
public void Unknown_rawPath_returns_false()
{
var calls = 0;
var r = Make(new(), reference => { calls++; return reference.StartsWith("{") ? new Def(reference) : null; });
r.TryResolve("{\"a\":1}", out var d1).ShouldBeTrue();
r.TryResolve("{\"a\":1}", out var d2).ShouldBeTrue();
d1!.Id.ShouldBe("{\"a\":1}");
calls.ShouldBe(1);
var r = Make(new() { ["Area/Modbus/Dev/Temp"] = new Def("Temp") });
r.TryResolve("Area/Modbus/Dev/Missing", out _).ShouldBeFalse();
}
[Fact]
public void Garbage_ref_returns_false_and_caches_negative()
public void Lookup_is_consulted_each_call()
{
// v3 holds no transient parse cache — the resolver reads the driver's live table by closure,
// so every TryResolve consults the underlying lookup (a table rebuild is immediately visible).
var calls = 0;
var r = Make(new(), _ => { calls++; return null; });
r.TryResolve("nope", out _).ShouldBeFalse();
r.TryResolve("nope", out _).ShouldBeFalse();
calls.ShouldBe(1);
}
var table = new Dictionary<string, Def>();
var r = new EquipmentTagRefResolver<Def>(reference =>
{
calls++;
return table.TryGetValue(reference, out var d) ? d : null;
});
[Fact]
public void Clear_drops_transient_cache()
{
var calls = 0;
var r = Make(new(), reference => { calls++; return new Def(reference); });
r.TryResolve("{\"a\":1}", out _).ShouldBeTrue();
r.Clear();
r.TryResolve("{\"a\":1}", out _).ShouldBeTrue();
r.TryResolve("Area/Dev/Temp", out _).ShouldBeFalse();
table["Area/Dev/Temp"] = new Def("Temp");
r.TryResolve("Area/Dev/Temp", out var def).ShouldBeTrue();
def!.Id.ShouldBe("Temp");
calls.ShouldBe(2);
}
[Fact]
public void Clear_is_a_noop_and_lookup_still_works()
{
// Clear() is retained for call-site compatibility only — there is no transient cache to drop.
var r = Make(new() { ["ref"] = new Def("ref") });
r.TryResolve("ref", out _).ShouldBeTrue();
r.Clear();
r.TryResolve("ref", out var def).ShouldBeTrue();
def!.Id.ShouldBe("ref");
}
/// <summary>
/// Core.Abstractions-009: <see cref="EquipmentTagRefResolver{TDef}.TryResolve"/> carries
/// <c>[MaybeNullWhen(false)]</c> on its <c>out</c> parameter so NRT-enabled callers do
@@ -62,7 +73,7 @@ public class EquipmentTagRefResolverTests
[Fact]
public void TryResolve_false_path_sets_def_to_default()
{
var r = Make(new(), _ => null);
var r = Make(new());
var returned = r.TryResolve("no-such-ref", out var def);
returned.ShouldBeFalse();
// The [MaybeNullWhen(false)] attribute means callers must treat def as potentially null