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