feat(server): route OPC UA Part 9 AddComment to ScriptedAlarmEngine
RouteScriptedAlarmMethodCalls now handles ConditionType.AddComment alongside Acknowledge/Confirm, dispatching to engine.AddCommentAsync. An empty comment is rejected by the Part 9 state machine and surfaced as BadInvalidArgument. MapCallOperation gates AddComment at the AlarmAcknowledge tier — there is no dedicated AddComment permission bit. Closes phase-7-status.md Gap 1: all Part 9 alarm methods now route to the engine. Adds 3 unit tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,14 @@ public sealed class CallGatingTests
|
||||
.ShouldBe(OpcUaOperation.AlarmConfirm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapCallOperation_AddComment_maps_to_AlarmAcknowledge()
|
||||
{
|
||||
// AddComment has no dedicated permission bit; it gates at the Acknowledge tier.
|
||||
DriverNodeManager.MapCallOperation(MethodIds.ConditionType_AddComment)
|
||||
.ShouldBe(OpcUaOperation.AlarmAcknowledge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapCallOperation_generic_method_maps_to_Call()
|
||||
{
|
||||
|
||||
@@ -161,6 +161,18 @@ public sealed class ScriptedAlarmMethodRoutingTests
|
||||
},
|
||||
};
|
||||
|
||||
private static CallMethodRequest AddCommentRequest(string conditionNodeId, string comment)
|
||||
=> new()
|
||||
{
|
||||
ObjectId = new NodeId(conditionNodeId, 2),
|
||||
MethodId = MethodIds.ConditionType_AddComment,
|
||||
InputArguments = new VariantCollection
|
||||
{
|
||||
new Variant(new byte[] { 1, 2, 3 }), // EventId (ByteString) — ignored
|
||||
new Variant(new LocalizedText(comment)),
|
||||
},
|
||||
};
|
||||
|
||||
private static CallMethodRequest GenericRequest(string objectNodeId)
|
||||
=> new()
|
||||
{
|
||||
@@ -357,6 +369,47 @@ public sealed class ScriptedAlarmMethodRoutingTests
|
||||
engine.GetState("confirm-alarm")!.LastConfirmUser.ShouldBe("ops-user");
|
||||
}
|
||||
|
||||
// ---- AddComment --------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void AddComment_appends_comment_to_engine_state()
|
||||
{
|
||||
using var engine = BuildEngine("al-1");
|
||||
|
||||
var calls = new List<CallMethodRequest> { AddCommentRequest("al-1.Condition", "checked the line") };
|
||||
var results = new List<CallMethodResult> { new CallMethodResult() };
|
||||
var errors = new List<ServiceResult> { null! };
|
||||
var index = Index(("al-1.Condition", "al-1"));
|
||||
|
||||
DriverNodeManager.RouteScriptedAlarmMethodCalls(
|
||||
MakeIdentity("ops-user"), calls, results, errors, engine, index);
|
||||
|
||||
ServiceResult.IsBad(errors[0]).ShouldBeFalse("AddComment handled");
|
||||
results[0].StatusCode.ShouldBe((StatusCode)StatusCodes.Good);
|
||||
var last = engine.GetState("al-1")!.Comments[^1];
|
||||
last.Kind.ShouldBe("AddComment");
|
||||
last.Text.ShouldBe("checked the line");
|
||||
last.User.ShouldBe("ops-user");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddComment_with_empty_text_returns_BadInvalidArgument()
|
||||
{
|
||||
using var engine = BuildEngine("al-1");
|
||||
|
||||
// The Part 9 state machine rejects an empty comment — surfaced as BadInvalidArgument.
|
||||
var calls = new List<CallMethodRequest> { AddCommentRequest("al-1.Condition", "") };
|
||||
var results = new List<CallMethodResult> { new CallMethodResult() };
|
||||
var errors = new List<ServiceResult> { null! };
|
||||
var index = Index(("al-1.Condition", "al-1"));
|
||||
|
||||
DriverNodeManager.RouteScriptedAlarmMethodCalls(
|
||||
MakeIdentity("ops-user"), calls, results, errors, engine, index);
|
||||
|
||||
ServiceResult.IsBad(errors[0]).ShouldBeTrue();
|
||||
errors[0].StatusCode.ShouldBe((StatusCode)StatusCodes.BadInvalidArgument);
|
||||
}
|
||||
|
||||
// ---- Mixed batches -----------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user