7f53bd96ec
Five MQTT tracker items — four AdminUI/authoring defects plus the .Browser layering exception. No runtime driver behaviour changes. #513 payloadFormat in a Sparkplug blob. MqttTagConfigModel.ToJson wrote the key unconditionally. Sharper than the issue states: PayloadFormat is the one Plain field with a NON-NULLABLE typed backing, so the write did not preserve a retyped tag's leftover — it INVENTED "payloadFormat":"Json" in a Sparkplug tag that never had one. Now Plain-only; preserve-unknown-keys is pinned unchanged. #509 slashed Sparkplug metric names. The mapper already carried metricName verbatim; only the tag Name (a RawPath segment) tripped RawPaths.ValidateSegment, which refused the batch whole — including Node Control/Rebirth, mandatory on every Sparkplug edge node. ToRawSegment coerces the Name and leaves the address untouched. Coercion is not injective, so DescribeNameCollisions refuses a colliding set BY NAME: silently merging two metrics into one tag would be worse than the refusal it replaces. Applies to every driver, not just MQTT — the argument is about what a Name is, not about who produced it. The sanitiser is pinned against RawPaths.ValidateSegment rather than a copied charset. #510 stale rebirth affordance. _canRebirth was probed once at browse-open and sessions are reaped for idleness, so a modal left open offered an action whose token no longer resolved. Re-probed on arm AND on confirm (the reap can land between the two, and confirm is the one path that publishes to a live broker); the affordance retires with an explanation instead of vanishing or erroring. #514 rebirth on a quiet plant. The tree is birth-driven and births are never retained, so a plant that birthed before the window opened showed an empty tree — and the scope came only from a tree selection, so the one action that would fill the tree could not be armed. The session already accepted a bare {group}/{edgeNode} ("the prime rebirth target"); this adds the affordance. Edge-node scope only, not a group broadcast. The rule is extracted to SparkplugRebirthScope so it is testable — this repo has no bUnit — and is advisory: the session re-resolves and refuses before publishing. #512 .Browser -> .Driver layering. TWO extractions were needed, not the one the issue anticipated: it costed this as "the client-options builder", but the browse session decodes births itself, so moving only that would have left the reference exactly where it was. - MqttClientOptionsFactory (TLS, PEM CA-pin validator, credentials, protocol mapping) -> new leaf .Transport project. NOT .Contracts: that is deliberately transport-free and MqttClientOptions is an MQTTnet type, so .Contracts would hand MQTTnet to ControlPlane. This is the fix the .Browser csproj comment had already prescribed, against the issue's suggestion. - The Sparkplug protocol layer (SparkplugTopic/MessageType, SparkplugCodec, RebirthRequester) -> .Contracts, beside the Tahu wire schema it encodes. Still exactly one copy of the TLS/CA-pin logic; MqttBrowserLayeringTests guards the graph both ways. Measured, not assumed: Driver.Mqtt.dll, Polly.Core and Serilog are now absent from the AdminUI's output. Core.dll remains, because Driver.Sql.Browser has the SAME violation, undocumented — the old comment blamed this browser for Core and was wrong. Filed separately rather than fixed in passing. Full suite green apart from the 7 known pre-existing failures (4 AbLegacy integration, compose profiles not up; 3 Host.IntegrationTests). Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
86 lines
4.0 KiB
C#
86 lines
4.0 KiB
C#
using System.Reflection;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests;
|
|
|
|
/// <summary>
|
|
/// Pins the MQTT project layering that Gitea #512 restored: <c>.Browser</c> depends on the shared
|
|
/// <c>.Contracts</c> + <c>.Transport</c> leaves, never on the runtime <c>.Driver</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Why this is worth a test.</b> <c>.Browser</c> used to reference the runtime driver, and the
|
|
/// AdminUI references <c>.Browser</c> — so the AdminUI's build/deploy graph transitively contained
|
|
/// the whole MQTT driver runtime plus Core, Polly.Core and Serilog. Nothing failed; the cost was
|
|
/// invisible. A reference re-added out of convenience would be invisible in exactly the same way,
|
|
/// which is precisely what a guard is for.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>What it can and cannot catch.</b> It asserts on emitted assembly references, so it fires when
|
|
/// the browser actually <em>uses</em> a driver-runtime type — the case that matters. A
|
|
/// <c>ProjectReference</c> re-added but never used would not trip it (the compiler emits no
|
|
/// reference), so the csproj comment remains the primary record of the decision.
|
|
/// </para>
|
|
/// </remarks>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class MqttBrowserLayeringTests
|
|
{
|
|
private const string BrowserAssembly = "ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser";
|
|
private const string DriverRuntimeAssembly = "ZB.MOM.WW.OtOpcUa.Driver.Mqtt";
|
|
private const string ContractsAssembly = "ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts";
|
|
private const string TransportAssembly = "ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Transport";
|
|
|
|
private static string[] BrowserReferences()
|
|
{
|
|
var browser = Assembly.Load(BrowserAssembly);
|
|
return [.. browser.GetReferencedAssemblies().Select(a => a.Name!)];
|
|
}
|
|
|
|
[Fact]
|
|
public void Browser_does_not_reference_the_runtime_driver()
|
|
=> BrowserReferences().ShouldNotContain(
|
|
DriverRuntimeAssembly,
|
|
"the MQTT browser is back to referencing the runtime driver — every other bespoke browser " +
|
|
"in this repo (OpcUaClient, Galaxy) depends on its .Contracts leaf only, and this reference " +
|
|
"reaches the AdminUI. If shared connection policy is needed, put it in .Transport; if shared " +
|
|
"protocol/codec is needed, put it in .Contracts. Do NOT copy the TLS/CA-pin logic.");
|
|
|
|
[Fact]
|
|
public void Browser_reaches_the_shared_leaves_instead()
|
|
{
|
|
// The positive control. Asserting only the absence above would also pass if the browser stopped
|
|
// referencing anything MQTT at all — e.g. if this test loaded the wrong assembly, or the type
|
|
// names drifted — so pin what it SHOULD depend on in the same breath.
|
|
var references = BrowserReferences();
|
|
|
|
references.ShouldContain(ContractsAssembly);
|
|
references.ShouldContain(TransportAssembly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Transport_stays_a_leaf_that_does_not_reach_back_into_the_runtime_driver()
|
|
{
|
|
// .Transport holds connection POLICY only. If it ever referenced the runtime driver, routing the
|
|
// browser through it would restore the coupling this whole change removed, one hop further out.
|
|
var references = Assembly.Load(TransportAssembly)
|
|
.GetReferencedAssemblies()
|
|
.Select(a => a.Name!)
|
|
.ToArray();
|
|
|
|
references.ShouldNotContain(DriverRuntimeAssembly);
|
|
references.ShouldNotContain(BrowserAssembly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Contracts_stays_transport_free()
|
|
=> Assembly.Load(ContractsAssembly)
|
|
.GetReferencedAssemblies()
|
|
.Select(a => a.Name!)
|
|
.ShouldNotContain(
|
|
"MQTTnet",
|
|
"Driver.Mqtt.Contracts is deliberately transport-free — it carries the Sparkplug wire " +
|
|
"schema and codec, and is referenced by ControlPlane, which has no business with a " +
|
|
"broker client. MQTTnet-typed connection policy belongs in .Transport.");
|
|
}
|