test(runtime): raw-blob routing test uses a no-FullName protocol blob (genuine #4d case)

This commit is contained in:
Joseph Doherty
2026-06-14 00:26:10 -04:00
parent 46f559f5f9
commit 99eea0b455
@@ -125,19 +125,21 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
recorder.Writes.ShouldBeEmpty(); recorder.Writes.ShouldBeEmpty();
} }
/// <summary>The router keys purely on NodeId — the tag's TagConfig blob shape is irrelevant. A tag /// <summary>A protocol TagConfig blob with no <c>FullName</c> key routes by the equipment NodeId, and
/// seeded with a RAW protocol-driver config blob (Modbus-shaped, no <c>FullName</c> key) routes the /// the forwarded wire-ref is the raw blob verbatim. <c>ExtractTagFullName</c> falls back to the raw
/// write to its owning child exactly like the Galaxy-style <c>{FullName}</c> blob does, because the /// blob string when no top-level <c>FullName</c> property is present, so the reverse map keys on
/// reverse map is built from the resolved <c>FullName</c> the composer projects, not the raw blob.</summary> /// <c>(DriverInstanceId, &lt;raw-blob&gt;)</c> and the driver receives that exact string as its
/// <c>WriteRequest.FullReference</c> — not a FullName value extracted from the blob.</summary>
[Fact] [Fact]
public void Primary_routes_write_for_raw_protocol_blob_tag() public void Primary_routes_write_for_raw_protocol_blob_tag()
{ {
var db = NewInMemoryDbFactory(); var db = NewInMemoryDbFactory();
var recorder = new RecordingDriverFactory("Modbus"); var recorder = new RecordingDriverFactory("Modbus");
// Seed the tag with a RAW protocol blob ({region/address/dataType}) instead of {FullName}; the // Seed the tag with a genuine protocol blob that has NO FullName key (pure Modbus wire config).
// composer still resolves a FullName, so the reverse map keys on that and the blob never matters. // ExtractTagFullName falls back to returning the raw blob string verbatim, so that string IS the
var deploymentId = SeedDeploymentWithRawBlobTag(db, RevA, // wire-ref the reverse map keys on and the driver receives as WriteRequest.FullReference.
equip: "eq-2", driver: "drv-2", fullName: "40002", name: "torque"); var (deploymentId, rawBlobString) = SeedDeploymentWithRawBlobTag(db, RevA,
equip: "eq-2", driver: "drv-2", name: "torque");
var actor = SpawnHostAndApply(db, deploymentId, recorder); var actor = SpawnHostAndApply(db, deploymentId, recorder);
@@ -148,11 +150,12 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
var result = asker.ExpectMsg<DriverHostActor.NodeWriteResult>(Timeout); var result = asker.ExpectMsg<DriverHostActor.NodeWriteResult>(Timeout);
result.Success.ShouldBeTrue(); result.Success.ShouldBeTrue();
// The write was forwarded to the owning child keyed by the resolved FullName, not the blob. // The forwarded wire-ref is the raw blob string verbatim — ExtractTagFullName fell back because
// there is no top-level "FullName" key in the blob.
AwaitAssert(() => AwaitAssert(() =>
{ {
recorder.Writes.Count.ShouldBe(1); recorder.Writes.Count.ShouldBe(1);
recorder.Writes[0].FullReference.ShouldBe("40002"); recorder.Writes[0].FullReference.ShouldBe(rawBlobString);
recorder.Writes[0].Value.ShouldBe(456.0); recorder.Writes[0].Value.ShouldBe(456.0);
}, duration: Timeout); }, duration: Timeout);
} }
@@ -256,17 +259,23 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
} }
/// <summary> /// <summary>
/// Seeds a single-tag Sealed deployment exactly like <see cref="SeedDeploymentWithEquipmentTags"/>, /// Seeds a single-tag Sealed deployment whose tag's <c>TagConfig</c> is a genuine protocol-driver
/// except the tag's <c>TagConfig</c> is a RAW protocol-driver blob (Modbus-shaped: /// blob with <strong>no <c>FullName</c> key</strong> (pure Modbus wire config:
/// <c>{FullName, region, address, dataType}</c>) instead of the bare Galaxy-style /// <c>{"region":"HoldingRegister","address":200,"dataType":"UInt16"}</c>). Because
/// <c>{FullName}</c> blob. The composer keys the reverse map purely on the blob's <c>FullName</c> /// <c>ExtractTagFullName</c> finds no top-level <c>FullName</c> property, it falls back to
/// (<c>ExtractTagFullName</c> reads only that field), so the extra raw protocol keys alongside it /// returning the raw blob string verbatim — that raw string becomes the
/// are irrelevant — proving routing is independent of the blob's broader shape. /// <c>(DriverInstanceId, &lt;raw-blob&gt;)</c> reverse-map key, and the driver receives it as
/// <c>WriteRequest.FullReference</c>. Returns both the <see cref="DeploymentId"/> and the exact
/// raw blob string so the caller can assert the forwarded wire-ref precisely.
/// </summary> /// </summary>
private static DeploymentId SeedDeploymentWithRawBlobTag( private static (DeploymentId DeploymentId, string RawBlobString) SeedDeploymentWithRawBlobTag(
IDbContextFactory<OtOpcUaConfigDbContext> db, RevisionHash rev, IDbContextFactory<OtOpcUaConfigDbContext> db, RevisionHash rev,
string equip, string driver, string fullName, string name) string equip, string driver, string name)
{ {
// Serialize the blob with NO FullName field — ExtractTagFullName will fall back to this verbatim.
var rawBlobString = JsonSerializer.Serialize(
new { region = "HoldingRegister", address = 200, dataType = "UInt16" });
var artifact = JsonSerializer.SerializeToUtf8Bytes(new var artifact = JsonSerializer.SerializeToUtf8Bytes(new
{ {
Namespaces = new[] Namespaces = new[]
@@ -296,11 +305,8 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
Name = name, Name = name,
FolderPath = (string?)null, FolderPath = (string?)null,
DataType = "Double", DataType = "Double",
// RAW protocol-driver TagConfig: FullName alongside the actual Modbus wire fields // Pure protocol blob: no FullName key. ExtractTagFullName falls back to raw blob.
// (region/address/dataType), NOT the bare Galaxy {FullName} blob. The composer extracts TagConfig = rawBlobString,
// only FullName, proving the extra protocol keys don't change routing.
TagConfig = JsonSerializer.Serialize(
new { FullName = fullName, region = "HoldingRegister", address = 200, dataType = "UInt16" }),
}, },
}, },
}); });
@@ -317,7 +323,7 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
ArtifactBlob = artifact, ArtifactBlob = artifact,
}); });
ctx.SaveChanges(); ctx.SaveChanges();
return id; return (id, rawBlobString);
} }
/// <summary>An <see cref="IDbContextFactory{TContext}"/> whose <c>CreateDbContext</c> always throws, /// <summary>An <see cref="IDbContextFactory{TContext}"/> whose <c>CreateDbContext</c> always throws,