fix(server): propagate watch-list cancellation; doc + test gaps (Server-051..053)
This commit is contained in:
@@ -306,6 +306,56 @@ public sealed class AlarmWatchListResolverTests
|
||||
Assert.Contains("Tank01", target.ActiveSubtag, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-051: a cancellation triggered while Galaxy Repository discovery is
|
||||
/// awaiting must propagate as <see cref="OperationCanceledException"/>, not be
|
||||
/// swallowed into a config-only watch-list, per the <see cref="IAlarmWatchListResolver"/>
|
||||
/// contract.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ResolveAsync_RepositoryCancelled_PropagatesOperationCanceled()
|
||||
{
|
||||
using CancellationTokenSource cts = new();
|
||||
// Repository observes the token, cancels the source, then throws the matching
|
||||
// OperationCanceledException — exactly what the live SQL path does on shutdown.
|
||||
CancellingGalaxyRepository repo = new(cts);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
||||
resolver.ResolveAsync(
|
||||
Options(include: ["Tank01.Level.HiHi"]),
|
||||
cts.Token));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-052 item 2 / Server-053: an entry that appears in both
|
||||
/// <c>IncludeAttributes</c> and <c>ExcludeAttributes</c> is removed — excludes
|
||||
/// win over explicit includes (the documented "excludes also suppress matching
|
||||
/// explicit includes" behaviour).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ExcludeAlsoSuppressesMatchingExplicitInclude()
|
||||
{
|
||||
StubGalaxyRepository repo = new(
|
||||
[
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "TestArea" },
|
||||
]);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
// Pump01.Fault is an explicit include AND an exclude (case-insensitive). It must
|
||||
// be dropped; the GR row Tank01 survives.
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(
|
||||
useGalaxyRepository: true,
|
||||
include: ["Pump01.Fault"],
|
||||
exclude: ["pump01.fault"]));
|
||||
|
||||
AlarmSubtagTarget target = Assert.Single(result);
|
||||
Assert.Contains("Tank01", target.ActiveSubtag, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(result, t => t.ActiveSubtag.Contains("Pump01", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>In-memory <see cref="IGalaxyRepository"/> returning a fixed alarm rowset.</summary>
|
||||
private sealed class StubGalaxyRepository(List<GalaxyAlarmAttributeRow> rows) : IGalaxyRepository
|
||||
{
|
||||
@@ -357,4 +407,35 @@ public sealed class AlarmWatchListResolverTests
|
||||
public Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default) =>
|
||||
Task.FromException<List<GalaxyAlarmAttributeRow>>(toThrow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IGalaxyRepository"/> whose alarm-attribute query cancels the
|
||||
/// supplied source and throws a token-bound <see cref="OperationCanceledException"/>,
|
||||
/// mirroring the live SQL path being cancelled mid-await.
|
||||
/// </summary>
|
||||
private sealed class CancellingGalaxyRepository(CancellationTokenSource source) : IGalaxyRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<bool> TestConnectionAsync(CancellationToken ct = default) => Task.FromResult(true);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult<DateTime?>(null);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult(new List<GalaxyHierarchyRow>());
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult(new List<GalaxyAttributeRow>());
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default)
|
||||
{
|
||||
source.Cancel();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(new List<GalaxyAlarmAttributeRow>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user