From a1d464b50d42556b7c78d57aa5a65100015a980c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 16 Jun 2026 15:38:00 -0400 Subject: [PATCH] fix(siteruntime): encode list attribute writes via AttributeValueCodec (was .ToString()) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace value?.ToString() with AttributeValueCodec.Encode(value) in AttributeAccessor indexer set and SetAsync, so a List{"a","b"} encodes to ["a","b"] instead of the garbage ToString representation. Add using ZB.MOM.WW.ScadaBridge.Commons.Types. Tests verify the codec contract (list→JSON array, scalar passthrough, null); full round-trip through the accessor is not viable without a live Akka ActorSystem — noted in-test with explanation. --- .../Scripts/ScopeAccessors.cs | 6 ++- .../Scripts/ScopeAccessorTests.cs | 51 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs index 95d4911e..bd78a5b5 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs @@ -1,3 +1,5 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Types; + namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// @@ -53,7 +55,7 @@ public class AttributeAccessor // on the DCL round-trip for data-connected attributes. The async // variants (GetAsync/SetAsync) are preferred where awaiting is possible. get => _ctx.GetAttribute(Resolve(key)).GetAwaiter().GetResult(); - set => _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty).GetAwaiter().GetResult(); + set => _ctx.SetAttribute(Resolve(key), AttributeValueCodec.Encode(value) ?? string.Empty).GetAwaiter().GetResult(); } /// @@ -70,7 +72,7 @@ public class AttributeAccessor /// The value to set. /// A task that represents the asynchronous operation. public Task SetAsync(string key, object? value) - => _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty); + => _ctx.SetAttribute(Resolve(key), AttributeValueCodec.Encode(value) ?? string.Empty); } /// diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScopeAccessorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScopeAccessorTests.cs index 9492ee27..73458ab8 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScopeAccessorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScopeAccessorTests.cs @@ -1,3 +1,4 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Types; using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; @@ -84,4 +85,54 @@ public class ScopeAccessorTests var temp = children["TempSensor"]; Assert.Equal("Motor.TempSensor", temp.Path); } + + // --- AttributeAccessor encoding contract ---------------------------------- + // + // AttributeAccessor.this[key].set and SetAsync both route through + // ScriptRuntimeContext.SetAttribute(name, encodedString), which requires + // a live Akka IActorRef; ScriptRuntimeContext has no virtual members and + // its constructor cannot be satisfied without a real ActorSystem, so a + // full-round-trip unit test through the accessor+context is not viable + // without a heavy Akka harness. + // + // Instead we test the encoding decision directly: AttributeAccessor is now + // documented to delegate value serialisation to AttributeValueCodec.Encode. + // These tests verify that contract at the codec level, which is exactly what + // the fix makes the accessor invoke. + + [Fact] + public void AttributeValueCodec_Encode_List_ProducesJsonArray() + { + // A List must encode to a JSON array, not the garbage + // "System.Collections.Generic.List`1[System.String]" that .ToString() produced. + var list = new List { "a", "b" }; + var encoded = AttributeValueCodec.Encode(list); + Assert.Equal("[\"a\",\"b\"]", encoded); + } + + [Fact] + public void AttributeValueCodec_Encode_Scalar_PassesThrough() + { + // A plain string scalar must be returned unchanged (byte-identical to + // the historical value?.ToString() path for strings). + var encoded = AttributeValueCodec.Encode("x"); + Assert.Equal("x", encoded); + } + + [Fact] + public void AttributeValueCodec_Encode_Null_ReturnsNull() + { + // AttributeAccessor coalesces null → "" at the call site, + // but the codec itself must return null for null input. + Assert.Null(AttributeValueCodec.Encode(null)); + } + + [Fact] + public void AttributeValueCodec_Encode_IntList_ProducesJsonArray() + { + // Integer list elements encode via InvariantCulture IFormattable. + var list = new List { 1, 2, 3 }; + var encoded = AttributeValueCodec.Encode(list); + Assert.Equal("[\"1\",\"2\",\"3\"]", encoded); + } }