feat(sitestream): add central SubscribeSiteAsync alarm-only client (plan #10 T3)
Add SiteStreamGrpcClient.SubscribeSiteAsync mirroring SubscribeAsync but for the site-wide, alarm-only SubscribeSite RPC: builds a SiteStreamRequest, opens the server stream, and delivers each event via a typed Action<AlarmStateChanged> callback (this stream is alarm-only by contract, so Task 4's per-site cache consumes an alarm delta with no downstream type test). Reuses the shared enrichment mapping via a new internal ConvertToAlarmEvent helper that returns null for any non-alarm event, defensively filtering anything that should never appear on the stream. Factory unchanged - it already caches a client per (site, endpoint). Adds focused unit tests for the alarm-only filter and the test-only-client guard. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -208,6 +208,78 @@ public class SiteStreamGrpcClientTests
|
||||
Assert.True(cts2.IsCancellationRequested);
|
||||
}
|
||||
|
||||
// --- Site-wide (SubscribeSite) alarm-only stream (plan #10 T3) ---
|
||||
|
||||
[Fact]
|
||||
public void ConvertToAlarmEvent_AlarmChanged_ReturnsMappedAlarm()
|
||||
{
|
||||
// An alarm event on the site-wide stream is delivered to onAlarmEvent with full enrichment.
|
||||
var ts = DateTimeOffset.UtcNow;
|
||||
var evt = new SiteStreamEvent
|
||||
{
|
||||
CorrelationId = "site-corr",
|
||||
AlarmChanged = new AlarmStateUpdate
|
||||
{
|
||||
InstanceUniqueName = "Site1.Motor01",
|
||||
AlarmName = "T01.Hi",
|
||||
State = AlarmStateEnum.AlarmStateActive,
|
||||
Priority = 700,
|
||||
Timestamp = Timestamp.FromDateTimeOffset(ts),
|
||||
Kind = "NativeOpcUa",
|
||||
Active = true,
|
||||
SourceReference = "T01.Hi"
|
||||
}
|
||||
};
|
||||
|
||||
var alarm = SiteStreamGrpcClient.ConvertToAlarmEvent(evt);
|
||||
|
||||
Assert.NotNull(alarm);
|
||||
Assert.Equal("Site1.Motor01", alarm!.InstanceUniqueName);
|
||||
Assert.Equal("T01.Hi", alarm.AlarmName);
|
||||
Assert.Equal(AlarmState.Active, alarm.State);
|
||||
Assert.Equal(AlarmKind.NativeOpcUa, alarm.Kind);
|
||||
Assert.Equal("T01.Hi", alarm.SourceReference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertToAlarmEvent_AttributeChanged_ReturnsNull()
|
||||
{
|
||||
// Attribute events must never appear on the alarm-only site-wide stream; if one
|
||||
// somehow arrives it is defensively filtered out rather than delivered or thrown.
|
||||
var evt = new SiteStreamEvent
|
||||
{
|
||||
CorrelationId = "site-corr",
|
||||
AttributeChanged = new AttributeValueUpdate
|
||||
{
|
||||
InstanceUniqueName = "Site1.Pump01",
|
||||
AttributePath = "Modules.IO",
|
||||
AttributeName = "Temperature",
|
||||
Value = "42.5",
|
||||
Quality = Quality.Good,
|
||||
Timestamp = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow)
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Null(SiteStreamGrpcClient.ConvertToAlarmEvent(evt));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertToAlarmEvent_UnknownEvent_ReturnsNull()
|
||||
{
|
||||
var evt = new SiteStreamEvent { CorrelationId = "site-corr" };
|
||||
Assert.Null(SiteStreamGrpcClient.ConvertToAlarmEvent(evt));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubscribeSiteAsync_OnTestOnlyClient_Throws()
|
||||
{
|
||||
// Guards against subscribing on a channel-less test double, mirroring SubscribeAsync.
|
||||
var client = SiteStreamGrpcClient.CreateForTesting();
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
client.SubscribeSiteAsync("corr", _ => { }, _ => { }, CancellationToken.None));
|
||||
}
|
||||
|
||||
// --- Communication-003 regression tests ---
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user