feat(adminui): author MTConnect write-back on the driver form (Driver.MTConnect-004)
IWritable shipped behind write.enabled and docs/drivers/MTConnect.md told the operator to set it, but MTConnectDriverForm — the only editor DriverFormMaps maps for this type, with no raw-JSON escape hatch and no driver-instance API — exposed no write section, so the documented enablement path was a hand edit of DriverInstance.DriverConfig in the config DB. - Write-back section: switch + form text stating it is observation injection, that the Agent's AllowPut is an independent second gate, and that it does not by itself make a node writable over OPC UA (Tag.AccessLevel still composes CurrentWrite). - Write added to the mirrored MTConnectConfigDto and to the form model, written in BOTH states — an omitted block would leave a previously-authored true standing, so the switch could never turn writing off again. - The false 'MTConnect is read-only' header comment is gone; the doc names the new section. No bUnit here, so the model/serialization seam is unit-tested both sides: the AdminUI test pins the emitted bytes, and MTConnectFactoryTests parses that same document and asserts MTConnectDriverOptions.Write.Enabled. Live page verification remains a rig step.
This commit is contained in:
@@ -128,6 +128,95 @@ public sealed class MTConnectDriverFormModelTests
|
||||
o["reconnect"]!["minBackoffMs"]!.GetValue<int>().ShouldBe(0);
|
||||
}
|
||||
|
||||
// ---- write-back (Driver.MTConnect-004) -----------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Write_back_is_off_by_default_and_is_written_explicitly()
|
||||
{
|
||||
// The capability shipped 2026-08-05 behind write.enabled and docs/drivers/MTConnect.md tells
|
||||
// the operator to set it — but the form authored no write block at all, so the documented
|
||||
// enablement path existed only as a hand edit of DriverInstance.DriverConfig in the config DB.
|
||||
//
|
||||
// The default is written rather than omitted: the driver reads an absent block as disabled
|
||||
// either way, and an explicit "enabled": false is what makes a config an operator opened and
|
||||
// saved say what it does. It is also what lets the form TURN THE FLAG OFF again — an omitted
|
||||
// key would leave a previously-authored true standing.
|
||||
var o = Emit(new MTConnectDriverForm.MTConnectFormModel { AgentUri = "http://agent:5000" });
|
||||
|
||||
o["write"]!["enabled"]!.GetValue<bool>().ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_back_enabled_reaches_the_key_the_driver_binds()
|
||||
{
|
||||
var o = Emit(new MTConnectDriverForm.MTConnectFormModel
|
||||
{
|
||||
AgentUri = "http://agent:5000",
|
||||
WriteEnabled = true,
|
||||
});
|
||||
|
||||
// MTConnectDriver.ParseOptions binds write.enabled into MTConnectDriverOptions.Write.Enabled
|
||||
// (pinned driver-side by MTConnectFactoryTests). This is the AdminUI half of that contract.
|
||||
o["write"]!["enabled"]!.GetValue<bool>().ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_back_round_trips_and_a_hand_authored_block_loads_into_the_form()
|
||||
{
|
||||
// The mitigating property the finding recorded — BuildConfigJson preserves unknown TOP-LEVEL
|
||||
// keys, so a hand-authored write block survived an edit through the form — is not enough on
|
||||
// its own: unless the form READS it, opening the modal shows the switch off over a config
|
||||
// that has writing on, and the operator's next save is the one that finally clears it.
|
||||
const string handAuthored = """{"agentUri":"http://agent:5000","write":{"enabled":true}}""";
|
||||
|
||||
var form = MTConnectDriverForm.MTConnectFormModel.FromJson(handAuthored);
|
||||
form.WriteEnabled.ShouldBeTrue();
|
||||
|
||||
var again = MTConnectDriverForm.MTConnectFormModel.FromJson(
|
||||
MTConnectDriverForm.BuildConfigJson(form, handAuthored));
|
||||
again.WriteEnabled.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Turning_write_back_off_clears_a_previously_authored_true()
|
||||
{
|
||||
// The falsification control for the "always write the block" decision above: a form that
|
||||
// omitted the key when false would leave the stored true in place, so the switch would appear
|
||||
// to turn writing off while the deployed driver went on accepting writes.
|
||||
const string existing = """{"agentUri":"http://agent:5000","write":{"enabled":true}}""";
|
||||
|
||||
var form = MTConnectDriverForm.MTConnectFormModel.FromJson(existing);
|
||||
form.WriteEnabled = false;
|
||||
|
||||
Emit(form, existing)["write"]!["enabled"]!.GetValue<bool>().ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromJson_reads_a_pascal_case_write_block()
|
||||
{
|
||||
var form = MTConnectDriverForm.MTConnectFormModel.FromJson(
|
||||
"""{"AgentUri":"http://agent:5000","Write":{"Enabled":true}}""");
|
||||
|
||||
form.WriteEnabled.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_write_enabled_document_is_the_literal_the_driver_test_parses()
|
||||
{
|
||||
// The AdminUI cannot be referenced from a driver test project, so the driver-side leg of this
|
||||
// contract (MTConnectFactoryTests.The_document_the_AdminUI_form_emits_arms_write_back) parses a
|
||||
// literal copy of this form's output. Asserting the bytes here is what stops the two drifting:
|
||||
// a rename or a reordering on this side fails HERE, naming the literal that must be updated.
|
||||
MTConnectDriverForm.BuildConfigJson(
|
||||
new MTConnectDriverForm.MTConnectFormModel { AgentUri = "http://agent:5000", WriteEnabled = true },
|
||||
null)
|
||||
.ShouldBe(WriteEnabledDocument);
|
||||
}
|
||||
|
||||
/// <summary>The form's output for a write-enabled instance, mirrored in the driver test suite.</summary>
|
||||
private const string WriteEnabledDocument =
|
||||
"""{"agentUri":"http://agent:5000","requestTimeoutMs":5000,"sampleIntervalMs":1000,"sampleCount":1000,"heartbeatMs":10000,"probe":{"enabled":true,"intervalMs":5000,"timeoutMs":2000},"reconnect":{"minBackoffMs":0,"maxBackoffMs":30000,"backoffMultiplier":2},"write":{"enabled":true}}""";
|
||||
|
||||
// ---- Validate ------------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
@@ -292,6 +381,8 @@ public sealed class MTConnectDriverFormModelTests
|
||||
dto.Reconnect!.MinBackoffMs.ShouldBe(0);
|
||||
dto.Reconnect.MaxBackoffMs.ShouldBe(30000);
|
||||
dto.Reconnect.BackoffMultiplier.ShouldBe(2.0);
|
||||
dto.Write.ShouldNotBeNull();
|
||||
dto.Write!.Enabled.ShouldBe(false);
|
||||
}
|
||||
|
||||
private sealed class WireDto
|
||||
@@ -304,6 +395,12 @@ public sealed class MTConnectDriverFormModelTests
|
||||
public int? HeartbeatMs { get; init; }
|
||||
public WireProbe? Probe { get; init; }
|
||||
public WireReconnect? Reconnect { get; init; }
|
||||
public WireWrite? Write { get; init; }
|
||||
}
|
||||
|
||||
private sealed class WireWrite
|
||||
{
|
||||
public bool? Enabled { get; init; }
|
||||
}
|
||||
|
||||
private sealed class WireProbe
|
||||
|
||||
Reference in New Issue
Block a user