fix(siteruntime): decode List value to typed array before DCL write (OPC UA array write path)
This commit is contained in:
@@ -660,6 +660,94 @@ public class InstanceActorTests : TestKit, IDisposable
|
||||
Assert.Equal(1450, response.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MV (C1 fix): a WRITE to a data-sourced <c>DataType.List</c> attribute must
|
||||
/// send the DCL a TYPED collection (so OPC UA writes an array node), NOT the
|
||||
/// canonical JSON string the script layer produced. The script path encodes
|
||||
/// <c>List<int></c> to <c>"[10,20,30]"</c>; HandleSetDataAttribute must
|
||||
/// decode that back to a typed <c>List<int></c> before building the
|
||||
/// WriteTagRequest. We assert the captured WriteTagRequest.Value is the typed
|
||||
/// list {10,20,30} — never the string "[10,20,30]".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InstanceActor_DataSourcedListWrite_SendsTypedArrayToDcl_NotJsonString()
|
||||
{
|
||||
const string tag = "ns=3;s=Pump.Setpoints";
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Pump-ListWrite",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute
|
||||
{
|
||||
CanonicalName = "Setpoints", Value = null,
|
||||
DataType = "List", ElementDataType = "Int32",
|
||||
DataSourceReference = tag, BoundDataConnectionName = "PLC"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var dcl = CreateTestProbe();
|
||||
var actor = CreateInstanceActorWithDcl("Pump-ListWrite", config, dcl);
|
||||
|
||||
// Script-style write: ScopeAccessors (AttributeValueCodec.Encode) has
|
||||
// already encoded the script's List<int> to the canonical JSON array string,
|
||||
// which is an array of element STRINGS (not raw JSON numbers).
|
||||
actor.Tell(new SetStaticAttributeCommand(
|
||||
"corr-write", "Pump-ListWrite", "Setpoints", "[\"10\",\"20\",\"30\"]", DateTimeOffset.UtcNow));
|
||||
|
||||
// The DCL must receive a WriteTagRequest carrying a TYPED collection.
|
||||
var write = dcl.ExpectMsg<WriteTagRequest>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("PLC", write.ConnectionName);
|
||||
Assert.Equal(tag, write.TagPath);
|
||||
Assert.IsNotType<string>(write.Value);
|
||||
var list = Assert.IsType<List<int>>(write.Value);
|
||||
Assert.Equal(new[] { 10, 20, 30 }, list);
|
||||
|
||||
// Complete the Ask so the actor replies success to the caller.
|
||||
dcl.Reply(new WriteTagResponse("corr-write", true, null, DateTimeOffset.UtcNow));
|
||||
var response = ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(5));
|
||||
Assert.True(response.Success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MV (C1 fix): a malformed value written to a data-sourced List attribute
|
||||
/// must be REJECTED before reaching the DCL — Success=false and NO
|
||||
/// WriteTagRequest is forwarded (mirrors the static-path malformed rejection).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InstanceActor_DataSourcedListWrite_Malformed_Rejected_NoDclWrite()
|
||||
{
|
||||
const string tag = "ns=3;s=Pump.Bad";
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Pump-ListWriteBad",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute
|
||||
{
|
||||
CanonicalName = "Setpoints", Value = null,
|
||||
DataType = "List", ElementDataType = "Int32",
|
||||
DataSourceReference = tag, BoundDataConnectionName = "PLC"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var dcl = CreateTestProbe();
|
||||
var actor = CreateInstanceActorWithDcl("Pump-ListWriteBad", config, dcl);
|
||||
|
||||
// Malformed JSON (unterminated array, non-int element) → reject the write.
|
||||
actor.Tell(new SetStaticAttributeCommand(
|
||||
"corr-bad-write", "Pump-ListWriteBad", "Setpoints", "[\"a\"", DateTimeOffset.UtcNow));
|
||||
|
||||
var response = ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(5));
|
||||
Assert.False(response.Success);
|
||||
Assert.NotNull(response.ErrorMessage);
|
||||
|
||||
// No write must reach the DCL.
|
||||
dcl.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
private void ExpectNoTerminated(IActorRef actor, TimeSpan within)
|
||||
{
|
||||
// The actor is Watch()ed; assert no Terminated arrives in the window.
|
||||
|
||||
Reference in New Issue
Block a user