Merge b640-619alm: OpcUaClient native alarms route to RawPath conditions + ConditionRefresh; docker-dev opc-plc seed (#619)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+62
@@ -255,6 +255,68 @@ public sealed class OpcUaClientDriverWireTests
|
||||
"the writable variable and the read-only one must classify differently");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #619 — ConditionRefresh over the wire: a condition already active+unacked BEFORE the driver subscribes is
|
||||
/// reported (Raise, keyed by the RawPath) right after subscribe, not only on its next transition. A second
|
||||
/// subscribe of the same set refreshes again and must emit nothing (no re-raise); a positive control then
|
||||
/// proves the feed is still live by acknowledging upstream and seeing exactly the Acknowledge arrive.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task A_condition_already_active_before_subscribe_is_reported_by_ConditionRefresh()
|
||||
{
|
||||
const string AlarmRef = "Sim/dev1/TempHi";
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await using var rig = await StartRigAsync("drv-alarm-refresh", ct);
|
||||
rig.MaterialiseNative();
|
||||
rig.Project(native: true, new AlarmConditionSnapshot(Active: true, Acknowledged: false, Confirmed: true,
|
||||
Enabled: true, AlarmShelvingKind.Unshelved, 800, "already hot"));
|
||||
|
||||
await using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions
|
||||
{
|
||||
EndpointUrl = rig.EndpointUrl,
|
||||
RawTags = [Tag(AlarmRef, ServerEventRig.RawNsu(ServerEventRig.RawAlarmPath))],
|
||||
SecurityPolicy = OpcUaSecurityPolicy.None,
|
||||
SecurityMode = OpcUaSecurityMode.None,
|
||||
AuthType = OpcUaAuthType.Username,
|
||||
Username = "apikey",
|
||||
Password = ServerEventRig.KeyPassword,
|
||||
AutoAcceptCertificates = true,
|
||||
TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform,
|
||||
Timeout = TimeSpan.FromSeconds(10),
|
||||
SessionTimeout = TimeSpan.FromSeconds(30),
|
||||
KeepAliveInterval = TimeSpan.FromSeconds(1),
|
||||
}, "wire-alarm-refresh");
|
||||
await drv.InitializeAsync("{}", ct);
|
||||
|
||||
var seen = new List<AlarmEventArgs>();
|
||||
drv.OnAlarmEvent += (_, e) => { lock (seen) seen.Add(e); };
|
||||
AlarmEventArgs[] Seen() { lock (seen) return [.. seen]; }
|
||||
|
||||
var first = await drv.SubscribeAlarmsAsync([AlarmRef], ct);
|
||||
await RecordedEvents.WaitUntilAsync(() => Seen().Any(e => e.Kind == AlarmTransitionKind.Raise));
|
||||
|
||||
var raise = Seen().Single(e => e.Kind == AlarmTransitionKind.Raise);
|
||||
raise.ConditionId.ShouldBe(AlarmRef);
|
||||
raise.Message.ShouldBe("already hot");
|
||||
raise.OpcUaSeverity.ShouldBe(800);
|
||||
Seen().ShouldAllBe(e => e.ConditionId == AlarmRef, "refresh markers must never surface as alarms");
|
||||
|
||||
// Same set again, released first exactly as DriverInstanceActor does → a fresh subscription and another
|
||||
// ConditionRefresh; the identical state must not re-raise.
|
||||
var before = Seen().Length;
|
||||
await drv.UnsubscribeAlarmsAsync(first, ct);
|
||||
await drv.SubscribeAlarmsAsync([AlarmRef], ct);
|
||||
await Task.Delay(1500, ct); // let the second refresh's events land before the control transition
|
||||
|
||||
// Positive control: an upstream acknowledge arrives as exactly one Acknowledge, after which nothing else.
|
||||
rig.Project(native: true, new AlarmConditionSnapshot(Active: true, Acknowledged: true, Confirmed: true,
|
||||
Enabled: true, AlarmShelvingKind.Unshelved, 800, "already hot"));
|
||||
await RecordedEvents.WaitUntilAsync(() => Seen().Any(e => e.Kind == AlarmTransitionKind.Acknowledge));
|
||||
await Task.Delay(500, ct);
|
||||
Seen().Skip(before).Select(e => e.Kind).ShouldBe([AlarmTransitionKind.Acknowledge],
|
||||
"the second refresh re-delivered an unchanged state and must have emitted nothing");
|
||||
}
|
||||
|
||||
private sealed class RecordingBuilder : IAddressSpaceBuilder
|
||||
{
|
||||
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = [];
|
||||
|
||||
Reference in New Issue
Block a user