fix(driver-galaxy): resolve Low code-review findings (Driver.Galaxy-005,010,012,013)
- Driver.Galaxy-005: rewrite the EventPump BoundedChannelOptions comment to honestly describe the Wait+TryWrite pattern. - Driver.Galaxy-010: ResolveApiKey now warns when a literal API key is used in production wiring; added an explicit dev: prefix for known cleartext-in-dev cases and rewrote the GalaxyGatewayOptions doc. - Driver.Galaxy-012: O(1) reverse-lookup for SubscriptionRegistry dispatch via per-entry FullRefByItemHandle map; immutable hash-set for the cross-binding reverse map; SubscribeAsync / ReadViaSubscribeOnce use BuildResultIndex for per-reference correlation. - Driver.Galaxy-013: ReinitializeAsync now validates the incoming JSON against the running options; ReplayOnSessionLost honoured by the Replay path; class summary rewritten to describe the shipped surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,121 @@ public sealed class GalaxyDriverInfrastructureTests
|
||||
await Should.NotThrowAsync(async () => await driver.DisposeAsync());
|
||||
}
|
||||
|
||||
// ===== Driver.Galaxy-013 regression: ReplayOnSessionLost gates the replay step =====
|
||||
|
||||
[Fact]
|
||||
public async Task ReplayOnSessionLost_False_SkipsResubscribeBulk()
|
||||
{
|
||||
// ReplayOnSessionLost was a dangling option — defined + documented but never
|
||||
// read. After the fix, setting it to false makes the reconnect replay path
|
||||
// skip SubscribeBulk (operator opts out of replay; the gateway's session-level
|
||||
// ReplaySubscriptions handles state restoration).
|
||||
var sub = new ReplayCountingSubscriber();
|
||||
var opts = new GalaxyDriverOptions(
|
||||
new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
|
||||
new GalaxyMxAccessOptions("InfraTest"),
|
||||
new GalaxyRepositoryOptions(WatchDeployEvents: false),
|
||||
new GalaxyReconnectOptions(ReplayOnSessionLost: false));
|
||||
|
||||
using var driver = new GalaxyDriver("drv-1", opts, null, null, null, sub);
|
||||
|
||||
// Establish a subscription so the replay path has something to walk.
|
||||
await driver.SubscribeAsync(["Tag.A", "Tag.B"], TimeSpan.Zero, CancellationToken.None);
|
||||
sub.SubscribeCalls.ShouldBe(1);
|
||||
|
||||
// Invoke the replay path directly via the internal test seam — the supervisor's
|
||||
// ReportTransportFailure spins it up async; for a deterministic assertion we
|
||||
// call the helper that ReplayAsync is wired against.
|
||||
await driver.InvokeReplayForTestAsync(CancellationToken.None);
|
||||
|
||||
sub.SubscribeCalls.ShouldBe(1,
|
||||
"ReplayOnSessionLost=false must skip the re-SubscribeBulk fan-out on reconnect");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReplayOnSessionLost_True_RunsResubscribeBulk()
|
||||
{
|
||||
var sub = new ReplayCountingSubscriber();
|
||||
var opts = new GalaxyDriverOptions(
|
||||
new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
|
||||
new GalaxyMxAccessOptions("InfraTest"),
|
||||
new GalaxyRepositoryOptions(WatchDeployEvents: false),
|
||||
new GalaxyReconnectOptions(ReplayOnSessionLost: true));
|
||||
|
||||
using var driver = new GalaxyDriver("drv-1", opts, null, null, null, sub);
|
||||
|
||||
await driver.SubscribeAsync(["Tag.A"], TimeSpan.Zero, CancellationToken.None);
|
||||
sub.SubscribeCalls.ShouldBe(1);
|
||||
|
||||
await driver.InvokeReplayForTestAsync(CancellationToken.None);
|
||||
|
||||
sub.SubscribeCalls.ShouldBe(2,
|
||||
"default ReplayOnSessionLost=true must re-issue SubscribeBulk after a transport drop");
|
||||
}
|
||||
|
||||
private sealed class ReplayCountingSubscriber : IGalaxySubscriber
|
||||
{
|
||||
private readonly Channel<MxEvent> _stream = Channel.CreateUnbounded<MxEvent>();
|
||||
private int _nextHandle = 1;
|
||||
public int SubscribeCalls;
|
||||
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref SubscribeCalls);
|
||||
var results = fullReferences.Select(r => new SubscribeResult
|
||||
{
|
||||
TagAddress = r,
|
||||
ItemHandle = Interlocked.Increment(ref _nextHandle),
|
||||
WasSuccessful = true,
|
||||
}).ToList();
|
||||
return Task.FromResult<IReadOnlyList<SubscribeResult>>(results);
|
||||
}
|
||||
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> _stream.Reader.ReadAllAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// ===== Driver.Galaxy-013 regression: ReinitializeAsync rejects unsupported reapply =====
|
||||
|
||||
[Fact]
|
||||
public async Task ReinitializeAsync_RejectsNonEquivalentConfigChange()
|
||||
{
|
||||
// ReinitializeAsync was previously a silent no-op that ignored driverConfigJson.
|
||||
// After the fix it either applies an equivalent config (no-op) or throws
|
||||
// NotSupportedException so a config change isn't silently dropped.
|
||||
var sub = new NoOpSubscriber();
|
||||
using var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub);
|
||||
|
||||
const string newConfig = "{\"Gateway\":{\"Endpoint\":\"https://other.test:5001\",\"ApiKeySecretRef\":\"dev:other\"}}";
|
||||
|
||||
// The driver must NOT pretend the change was applied — either no-op equivalence
|
||||
// or an explicit rejection is acceptable. Silently dropping the new config
|
||||
// (the previous behaviour) is not.
|
||||
await Should.ThrowAsync<NotSupportedException>(async () =>
|
||||
await driver.ReinitializeAsync(newConfig, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReinitializeAsync_AcceptsEquivalentConfig()
|
||||
{
|
||||
var sub = new NoOpSubscriber();
|
||||
using var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub);
|
||||
|
||||
// An empty / null-equivalent config reapply (no field changes) must not throw —
|
||||
// it's a legitimate "refresh health" path. Pass a JSON object that round-trips
|
||||
// to the driver's current options.
|
||||
var json = "{\"Gateway\":{\"Endpoint\":\"https://mxgw.test:5001\",\"ApiKeySecretRef\":\"key\"}," +
|
||||
"\"MxAccess\":{\"ClientName\":\"InfraTest\"}," +
|
||||
"\"Repository\":{\"WatchDeployEvents\":false}," +
|
||||
"\"Reconnect\":{}}";
|
||||
await Should.NotThrowAsync(async () =>
|
||||
await driver.ReinitializeAsync(json, CancellationToken.None));
|
||||
}
|
||||
|
||||
// ===== Minimal IGalaxySubscriber fake that returns empty results for subscribe calls =====
|
||||
|
||||
private sealed class NoOpSubscriber : IGalaxySubscriber
|
||||
|
||||
Reference in New Issue
Block a user