docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)

Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
This commit is contained in:
Joseph Doherty
2026-10-07 15:05:49 -04:00
parent 0ffd4bdbea
commit 513fa1aa16
998 changed files with 17896 additions and 2395 deletions
@@ -11,14 +11,14 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;
/// <summary>
/// WP-6: Tests for DataConnectionActor Become/Stash state machine.
/// WP-9: Auto-reconnect and bad quality tests.
/// WP-10: Transparent re-subscribe tests.
/// WP-11: Write-back support tests.
/// WP-12: Tag path resolution with retry tests.
/// WP-13: Health reporting tests.
/// WP-14: Subscription lifecycle tests.
/// Task-4: Failover state machine tests.
/// Tests for DataConnectionActor Become/Stash state machine.
/// Auto-reconnect and bad quality tests.
/// Transparent re-subscribe tests.
/// Write-back support tests.
/// Tag path resolution with retry tests.
/// Health reporting tests.
/// Subscription lifecycle tests.
/// Failover state machine tests.
/// </summary>
public class DataConnectionActorTests : TestKit
{
@@ -27,6 +27,7 @@ public class DataConnectionActorTests : TestKit
private readonly ISiteHealthCollector _mockHealthCollector;
private readonly IDataConnectionFactory _mockFactory;
/// <summary>Initializes a new instance of <c>DataConnectionActorTests</c>.</summary>
public DataConnectionActorTests()
: base(@"akka.loglevel = DEBUG")
{
@@ -39,7 +40,7 @@ public class DataConnectionActorTests : TestKit
TagResolutionRetryInterval = TimeSpan.FromMilliseconds(200),
WriteTimeout = TimeSpan.FromSeconds(5),
// Default to a single seed-read attempt so existing tests behave exactly as
// before the DCL-027 retry was added; the retry-specific tests opt in.
// before the seed-read retry was added; the retry-specific tests opt in.
SeedReadMaxAttempts = 1,
SeedReadRetryDelay = TimeSpan.FromMilliseconds(10)
};
@@ -93,6 +94,7 @@ public class DataConnectionActorTests : TestKit
c.GetArguments() is [string name, ConnectionHealth.Connected, ..] &&
name == connectionName);
/// <summary>Verifies that starts in connecting state attempts connect.</summary>
[Fact]
public void WP6_StartsInConnectingState_AttemptsConnect()
{
@@ -107,6 +109,7 @@ public class DataConnectionActorTests : TestKit
TimeSpan.FromSeconds(2));
}
/// <summary>Verifies that connecting state stashes subscribe requests.</summary>
[Fact]
public void WP6_ConnectingState_StashesSubscribeRequests()
{
@@ -134,6 +137,8 @@ public class DataConnectionActorTests : TestKit
ExpectMsg<SubscribeTagsResponse>(TimeSpan.FromSeconds(2));
}
/// <summary>Verifies that connected state write returns result.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WP11_ConnectedState_Write_ReturnsResult()
{
@@ -158,6 +163,8 @@ public class DataConnectionActorTests : TestKit
Assert.True(response.Success);
}
/// <summary>Verifies that write failure returned synchronously.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WP11_Write_Failure_ReturnedSynchronously()
{
@@ -177,6 +184,8 @@ public class DataConnectionActorTests : TestKit
Assert.Equal("Device offline", response.ErrorMessage);
}
/// <summary>Verifies that health report returns connection status.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WP13_HealthReport_ReturnsConnectionStatus()
{
@@ -195,8 +204,10 @@ public class DataConnectionActorTests : TestKit
Assert.Equal(ConnectionHealth.Connected, report.Status);
}
// ── Task-4: Failover state machine tests ──
// ── Failover state machine tests ──
/// <summary>Verifies that failover after n retries switches to backup.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Task4_FailoverAfterNRetries_SwitchesToBackup()
{
@@ -242,6 +253,8 @@ public class DataConnectionActorTests : TestKit
TimeSpan.FromSeconds(5));
}
/// <summary>Verifies that single endpoint retries indefinitely no failover.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Task4_SingleEndpoint_RetriesIndefinitely_NoFailover()
{
@@ -275,6 +288,8 @@ public class DataConnectionActorTests : TestKit
_mockFactory.DidNotReceive().Create(Arg.Any<string>(), Arg.Any<IDictionary<string, string>>());
}
/// <summary>Verifies that round robin back to primary after backup fails.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Task4_RoundRobin_BackToPrimary_AfterBackupFails()
{
@@ -352,6 +367,8 @@ public class DataConnectionActorTests : TestKit
TimeSpan.FromSeconds(5));
}
/// <summary>Verifies that successful reconnect resets failure counter.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Task4_SuccessfulReconnect_ResetsFailureCounter()
{
@@ -406,6 +423,8 @@ public class DataConnectionActorTests : TestKit
_mockFactory.DidNotReceive().Create(Arg.Any<string>(), Arg.Any<IDictionary<string, string>>());
}
/// <summary>Verifies that re subscribe all called after failover reconnect.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Task4_ReSubscribeAll_CalledAfterFailoverReconnect()
{
@@ -485,8 +504,10 @@ public class DataConnectionActorTests : TestKit
"sensor/temp", Arg.Any<SubscriptionCallback>(), Arg.Any<CancellationToken>());
}
// ── PLAN-03 Task 16 (S7): background tasks must use the adapter that started them ──
// ── Background tasks must use the adapter that started them ──
/// <summary>Verifies that in flight subscribe continues on original adapter after failover.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Task16_InFlightSubscribe_ContinuesOnOriginalAdapter_AfterFailover()
{
@@ -545,7 +566,7 @@ public class DataConnectionActorTests : TestKit
backupAdapter.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ConnectAsync"),
TimeSpan.FromSeconds(5));
// Demand is registered before I/O (DC-1), so failover independently recovers
// Demand is registered before I/O, so failover independently recovers
// both tags on backup even while the original primary operation is parked.
AwaitCondition(() => backupAdapter.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "SubscribeAsync") == 2,
TimeSpan.FromSeconds(5));
@@ -562,7 +583,7 @@ public class DataConnectionActorTests : TestKit
Assert.Equal(2, backupAdapter.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "SubscribeAsync"));
}
// ── PLAN-03 Task 17 (P3): tag→instances reverse index for the value fan-out hot path ──
// ── Tag→instances reverse index for the value fan-out hot path ──
private IActorRef CreateInstantSubscribeActor(string name)
{
@@ -578,6 +599,8 @@ public class DataConnectionActorTests : TestKit
return CreateConnectionActor(name);
}
/// <summary>Verifies that tag fan out after one instance unsubscribes only remaining subscriber receives.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Task17_TagFanOut_AfterOneInstanceUnsubscribes_OnlyRemainingSubscriberReceives()
{
@@ -608,6 +631,8 @@ public class DataConnectionActorTests : TestKit
probeA.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
}
/// <summary>Verifies that tag fan out two instances disjoint tags each receives only its own.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Task17_TagFanOut_TwoInstancesDisjointTags_EachReceivesOnlyItsOwn()
{
@@ -631,7 +656,7 @@ public class DataConnectionActorTests : TestKit
probeB.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
}
// ── DataConnectionLayer-001: subscribe must not mutate actor state off-thread ──
// ── Subscribe must not mutate actor state off-thread ──
private static async Task<string> DelayedSubscribeAsync()
{
@@ -641,10 +666,12 @@ public class DataConnectionActorTests : TestKit
return "sub-" + Guid.NewGuid().ToString("N");
}
/// <summary>Verifies that concurrent subscribes do not corrupt subscription counters.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL001_ConcurrentSubscribes_DoNotCorruptSubscriptionCounters()
{
// Regression test for DataConnectionLayer-001. HandleSubscribe used to mutate
// Regression test. HandleSubscribe used to mutate
// actor state (_subscriptionIds, _totalSubscribed, _resolvedTags, the per-instance
// HashSet) from a Task.Run background thread. Many concurrent subscribes then race
// on non-thread-safe Dictionary/HashSet and on non-atomic int++ — losing increments
@@ -684,12 +711,14 @@ public class DataConnectionActorTests : TestKit
Assert.Equal(instances * tagsPerInstance, report.ResolvedTags);
}
// ── DataConnectionLayer-004: subscribe-time failure classification ──
// ── Subscribe-time failure classification ──
/// <summary>Verifies that genuine tag resolution failure pushes bad quality to subscriber.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL004_GenuineTagResolutionFailure_PushesBadQualityToSubscriber()
{
// Regression test for DataConnectionLayer-004. When a tag genuinely fails to
// Regression test. When a tag genuinely fails to
// resolve at subscribe time, the design doc (Tag Path Resolution, step 2)
// requires the attribute to be marked quality `bad`. The pre-fix code only
// logged and added the tag to _unresolvedTags — the Instance Actor never got
@@ -718,10 +747,12 @@ public class DataConnectionActorTests : TestKit
Assert.True(ack.Success);
}
/// <summary>Verifies that static tag seed value is delivered after registration.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL026_StaticTagSeedValue_IsDeliveredAfterRegistration()
{
// Regression test for DataConnectionLayer-026. The initial-read seed value used to
// Regression test. The initial-read seed value used to
// be emitted (TagValueReceived) from HandleSubscribe's background task BEFORE
// HandleSubscribeCompleted registered the instance's tags in
// _subscriptionsByInstance. HandleTagValueReceived's fan-out then found no
@@ -755,8 +786,10 @@ public class DataConnectionActorTests : TestKit
Assert.Equal("Left54321", update.Value);
}
// ── DataConnectionLayer-027: robust seeding for STATIC tags ──
// ── Robust seeding for STATIC tags ──
/// <summary>Verifies that seed read empty then good seeds tag within retry budget.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL027_SeedRead_EmptyThenGood_SeedsTagWithinRetryBudget()
{
@@ -792,6 +825,8 @@ public class DataConnectionActorTests : TestKit
Assert.Equal("HY12333", update.Value);
}
/// <summary>Verifies that seed read never returns value logs warning and still acks.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL027_SeedRead_NeverReturnsValue_LogsWarningAndStillAcks()
{
@@ -823,10 +858,12 @@ public class DataConnectionActorTests : TestKit
Assert.True(ack.Success);
}
/// <summary>Verifies that reconnect re seeds static tag.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL027_Reconnect_ReSeedsStaticTag()
{
// Mechanism #3: ReSubscribeAll re-advises after a reconnect but pre-fix did NO
// Mechanism three: ReSubscribeAll re-advises after a reconnect but pre-fix did NO
// seed read, so a STATIC tag (no further OnDataChange) could not self-heal across
// a reconnect/failover even though the source reads Good. After the fix the
// reconnect re-seeds the re-advised tags, so the post-reconnect value reaches the
@@ -873,10 +910,12 @@ public class DataConnectionActorTests : TestKit
Assert.Equal(99.0, reseeded.Value);
}
/// <summary>Verifies that connection level subscribe failure triggers reconnect not tag retry.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL004_ConnectionLevelSubscribeFailure_TriggersReconnect_NotTagRetry()
{
// Regression test for DataConnectionLayer-004. A subscribe failing because the
// Regression test. A subscribe failing because the
// adapter is not connected (InvalidOperationException from EnsureConnected) is
// a connection problem, not a bad tag path. The pre-fix code misclassified it
// as an unresolved tag and retried it on the 10s tag-resolution timer. After
@@ -904,12 +943,14 @@ public class DataConnectionActorTests : TestKit
TimeSpan.FromSeconds(5));
}
// ── DataConnectionLayer-005: WriteTimeout must bound a hung write ──
// ── WriteTimeout must bound a hung write ──
/// <summary>Verifies that write that hangs times out and returns failure synchronously.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL005_Write_ThatHangs_TimesOutAndReturnsFailureSynchronously()
{
// Regression test for DataConnectionLayer-005. HandleWrite called WriteAsync
// Regression test. HandleWrite called WriteAsync
// with no CancellationToken and no timeout, so a hung device write never
// produced a WriteTagResponse. The calling script would block until its own
// Ask-timeout with no DCL-level error. After the fix, _options.WriteTimeout
@@ -939,12 +980,14 @@ public class DataConnectionActorTests : TestKit
Assert.Contains("timeout", response.ErrorMessage, StringComparison.OrdinalIgnoreCase);
}
// ── DataConnectionLayer-006: quality counters must not drift after unsubscribe/reconnect ──
// ── Quality counters must not drift after unsubscribe/reconnect ──
/// <summary>Verifies that disconnect after unsubscribe bad quality count matches remaining tags.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL006_DisconnectAfterUnsubscribe_BadQualityCountMatchesRemainingTags()
{
// Regression test for DataConnectionLayer-006. _lastTagQuality and the three
// Regression test. _lastTagQuality and the three
// quality counters were never cleaned up on unsubscribe, so a tag removed via
// HandleUnsubscribe lingered in _lastTagQuality. PushBadQualityForAllTags then
// set _tagsBadQuality = _lastTagQuality.Count, counting the dropped tag and
@@ -1008,12 +1051,14 @@ public class DataConnectionActorTests : TestKit
reconnectGate.SetCanceled();
}
// ── DataConnectionLayer-010: tag-resolution retry must not double-dispatch ──
// ── Tag-resolution retry must not double-dispatch ──
/// <summary>Verifies that tag resolution retry does not issue duplicate concurrent subscribes.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL010_TagResolutionRetry_DoesNotIssueDuplicateConcurrentSubscribes()
{
// Regression test for DataConnectionLayer-010. HandleRetryTagResolution fired a
// Regression test. HandleRetryTagResolution fired a
// SubscribeAsync for every unresolved tag without removing it from _unresolvedTags
// first. A slow SubscribeAsync overlapping the next retry tick produced duplicate
// concurrent subscribe attempts for the same tag, leaking the first monitored
@@ -1057,12 +1102,14 @@ public class DataConnectionActorTests : TestKit
subscribeGate.SetCanceled();
}
// ── DataConnectionLayer-018: concurrent same-tag subscribes must not orphan adapter handles ──
// ── Concurrent same-tag subscribes must not orphan adapter handles ──
/// <summary>Verifies that concurrent subscribes same tag different instances issue one adapter subscribe.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL018_ConcurrentSubscribes_SameTag_DifferentInstances_IssueOneAdapterSubscribe()
{
// Regression test for DataConnectionLayer-018. Before the fix, HandleSubscribe
// Regression test. Before the fix, HandleSubscribe
// snapshotted _subscriptionIds.Keys on the actor thread BEFORE the Task.Run
// I/O. Two SubscribeTagsRequest messages for different instances sharing a tag
// would both observe "not subscribed" (the first request's SubscribeCompleted
@@ -1130,17 +1177,19 @@ public class DataConnectionActorTests : TestKit
actor.Tell(new DataConnectionActor.GetHealthReport());
var report = ExpectMsg<DataConnectionHealthReport>(TimeSpan.FromSeconds(5));
// One tag shared across two instances must count as exactly one subscription.
// DCL-020 (#28) tracks the related "previously-unresolved tag promoted via
// A separate scenario covers the related "previously-unresolved tag promoted via
// another instance" double-count case; here the tag was never unresolved.
Assert.Equal(1, report.ResolvedTags);
}
// ── DataConnectionLayer-020: previously-unresolved tag, resolved via different instance, must not double-count ──
// ── Previously-unresolved tag, resolved via different instance, must not double-count ──
/// <summary>Verifies that unresolved tag promoted by different instance does not double count total subscribed.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL020_UnresolvedTagPromoted_ByDifferentInstance_DoesNotDoubleCountTotalSubscribed()
{
// Regression test for DataConnectionLayer-020. The first SubscribeTagsRequest
// Regression test. The first SubscribeTagsRequest
// (instance A, tag "promote/tag") fails at the resolution layer — the tag is
// added to _unresolvedTags AND _totalSubscribed is bumped to 1. The second
// SubscribeTagsRequest (instance B, same tag) succeeds the adapter call. Before
@@ -1185,10 +1234,12 @@ public class DataConnectionActorTests : TestKit
Assert.Equal(1, report.ResolvedTags);
}
/// <summary>Verifies that two instances failing same tag only counts tag once in total.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL020_TwoInstancesFailingSameTag_OnlyCountsTagOnceInTotal()
{
// Regression test for DataConnectionLayer-020's symmetric failure branch.
// Regression test for the symmetric failure branch of the same scenario.
// Two instances both fail to resolve the same tag — _unresolvedTags must hold
// a single entry and _totalSubscribed must be 1, not 2. Pre-fix the failure
// branch always ran _totalSubscribed++, double-counting on the second
@@ -1221,12 +1272,14 @@ public class DataConnectionActorTests : TestKit
Assert.Equal(0, report.ResolvedTags);
}
// ── DataConnectionLayer-021: mid-flight unsubscribe must release adapter handle and drop state ──
// ── Mid-flight unsubscribe must release adapter handle and drop state ──
/// <summary>Verifies that unsubscribe during in flight subscribe releases adapter handle and keeps state clean.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL021_UnsubscribeDuringInFlightSubscribe_ReleasesAdapterHandle_AndKeepsStateClean()
{
// Regression test for DataConnectionLayer-021. Previously HandleSubscribeCompleted
// Regression test. Previously HandleSubscribeCompleted
// re-created _subscriptionsByInstance[instanceName] when the instance had been
// unsubscribed while the subscribe I/O was in flight, and then ran the same
// counter/handle mutations as the happy path. The leak permanently inflated
@@ -1280,12 +1333,14 @@ public class DataConnectionActorTests : TestKit
Assert.Equal(0, report.ResolvedTags);
}
// ── DataConnectionLayer-022: tag-resolution retry timer must not reset on every failure ──
// ── Tag-resolution retry timer must not reset on every failure ──
/// <summary>Verifies that bursted failed subscribes do not reset retry timer.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL022_BurstedFailedSubscribes_DoNotResetRetryTimer()
{
// Regression test for DataConnectionLayer-022. Both HandleSubscribeCompleted
// Regression test. Both HandleSubscribeCompleted
// and HandleTagResolutionFailed previously called Timers.StartPeriodicTimer
// unconditionally — StartPeriodicTimer with an existing key cancels and
// replaces the prior timer, so a burst of SubscribeTagsRequests arriving
@@ -1357,12 +1412,14 @@ public class DataConnectionActorTests : TestKit
$"This is the DCL-022 reset-on-every-call starvation regression.");
}
// ── DataConnectionLayer-011: stale callbacks from a disposed adapter must be dropped ──
// ── Stale callbacks from a disposed adapter must be dropped ──
/// <summary>Verifies that stale tag value from old adapter is not forwarded after failover.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL011_StaleTagValueFromOldAdapter_IsNotForwardedAfterFailover()
{
// Regression test for DataConnectionLayer-011. On failover the old adapter is
// Regression test. On failover the old adapter is
// disposed and a fresh one created, but the old adapter's subscription callbacks
// captured Self and keep Telling TagValueReceived. With no per-adapter generation
// tag, a value from the disposed adapter delivered after the actor is Connected
@@ -1430,12 +1487,14 @@ public class DataConnectionActorTests : TestKit
ExpectNoMsg(TimeSpan.FromSeconds(1));
}
// ── DataConnectionLayer-015: initial-connect failures must trigger failover ──
// ── Initial-connect failures must trigger failover ──
/// <summary>Verifies that primary down at startup fails over to backup.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL015_PrimaryDownAtStartup_FailsOverToBackup()
{
// Regression test for DataConnectionLayer-015. HandleConnectResult — the handler
// Regression test. HandleConnectResult — the handler
// for the INITIAL connection attempt in the Connecting state — only logged and
// re-armed the reconnect timer. It never incremented _consecutiveFailures and
// never switched endpoint, so a primary that is unreachable when the actor first
@@ -1470,6 +1529,8 @@ public class DataConnectionActorTests : TestKit
TimeSpan.FromSeconds(5));
}
/// <summary>Verifies that single endpoint down at startup retries indefinitely no failover.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL015_SingleEndpointDownAtStartup_RetriesIndefinitely_NoFailover()
{
@@ -1495,12 +1556,14 @@ public class DataConnectionActorTests : TestKit
_mockFactory.DidNotReceive().Create(Arg.Any<string>(), Arg.Any<IDictionary<string, string>>());
}
// ── DataConnectionLayer-016: subscribe response must reflect a connection-level failure ──
// ── Subscribe response must reflect a connection-level failure ──
/// <summary>Verifies that connection level subscribe failure replies with unsuccessful response.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL016_ConnectionLevelSubscribeFailure_RepliesWithUnsuccessfulResponse()
{
// Regression test for DataConnectionLayer-016. When a subscribe arrives while the
// Regression test. When a subscribe arrives while the
// adapter is silently down, HandleSubscribeCompleted drove the actor into
// Reconnecting (a connection-level failure) but still replied to the caller with
// SubscribeTagsResponse(Success: true, Error: null). The Instance Actor was told
@@ -1530,6 +1593,8 @@ public class DataConnectionActorTests : TestKit
Assert.NotNull(response.ErrorMessage);
}
/// <summary>Verifies that genuine resolution failure still replies success.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL016_GenuineResolutionFailure_StillRepliesSuccess()
{
@@ -1556,6 +1621,8 @@ public class DataConnectionActorTests : TestKit
Assert.Null(ack.ErrorMessage);
}
/// <summary>Verifies that subscribe with failed tags counts resolved and unresolved separately.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL001_SubscribeWithFailedTags_CountsResolvedAndUnresolvedSeparately()
{
@@ -1579,7 +1646,7 @@ public class DataConnectionActorTests : TestKit
["good/a", "good/b", "good/c", "bad/x", "bad/y"], DateTimeOffset.UtcNow));
// Two genuine resolution failures now also push a bad-quality TagValueUpdate
// to the subscriber (DataConnectionLayer-004); skip past those to the ack.
// to the subscriber; skip past those to the ack.
var ack = FishForMessage<SubscribeTagsResponse>(_ => true, TimeSpan.FromSeconds(5));
Assert.True(ack.Success);
@@ -1590,12 +1657,14 @@ public class DataConnectionActorTests : TestKit
Assert.Equal(3, report.ResolvedTags); // only the 3 good ones resolved
}
// ── DataConnectionLayer-008: HandleUnsubscribe shared-tag reference counting ──
// ── HandleUnsubscribe shared-tag reference counting ──
/// <summary>Verifies that unsubscribe only releases tag when last subscriber leaves.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL008_Unsubscribe_OnlyReleasesTagWhenLastSubscriberLeaves()
{
// Regression test for DataConnectionLayer-008. HandleUnsubscribe must release a
// Regression test. HandleUnsubscribe must release a
// tag at the adapter only when no other instance still subscribes to it. The
// O(n) per-tag scan over every instance was replaced with an O(1) reference
// count; this guards that the reference count tracks shared subscriptions
@@ -1649,14 +1718,16 @@ public class DataConnectionActorTests : TestKit
Assert.Equal(0, report2.ResolvedTags);
}
// ── DataConnectionLayer-027: a reconnect re-seed read that lands after a SECOND
// ── A reconnect re-seed read that lands after a SECOND
// failover (generation bumped twice) must be DROPPED, not applied ──
/// <summary>Verifies that stale generation reseed after second failover is dropped.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DCL027_StaleGenerationReseed_AfterSecondFailover_IsDropped()
{
// Regression/invariant pin for DataConnectionLayer-027 (the reconnect re-seed in
// ReSubscribeAll). The re-seed captures the adapter generation at kickoff and
// Regression/invariant pin for the reconnect re-seed in
// ReSubscribeAll. The re-seed captures the adapter generation at kickoff and
// delivers the seed value through the generation-guarded TagValueReceived path:
//
// var generation = _adapterGeneration; // captured at reseed kickoff
@@ -1712,7 +1783,7 @@ public class DataConnectionActorTests : TestKit
backupAdapter.ReadAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(_ =>
{
// Signal the test that the generation-1 re-seed read has been entered
// Signal the test that the generation 1 re-seed read has been entered
// (so generation 1 is definitely captured), then hand back the gated task.
backupReadEntered.TrySetResult();
return staleSeedReadGate.Task;
@@ -1720,7 +1791,7 @@ public class DataConnectionActorTests : TestKit
// ── Primary-again (generation 2): the round-robin target of the SECOND failover.
// Connects cleanly; its re-seed read returns a DISTINCT fresh value (77.0) so we
// can prove the FRESH generation-2 value IS accepted while the stale gen-1 is not. ──
// can prove the FRESH generation 2 value IS accepted while the stale gen 1 is not. ──
primaryAgainAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
primaryAgainAdapter.Status.Returns(ConnectionHealth.Connected);
@@ -1755,12 +1826,12 @@ public class DataConnectionActorTests : TestKit
RaiseDisconnected(primaryAdapter);
ExpectMsg<ConnectionQualityChanged>(TimeSpan.FromSeconds(3)); // bad-quality push on disconnect
// Wait until the generation-1 re-seed read has been ENTERED (generation 1 captured,
// Wait until the generation 1 re-seed read has been ENTERED (generation 1 captured,
// read pending on the gate). This anchors "generation N captured at reseed kickoff".
// WaitAsync throws TimeoutException (failing the test) if the read is never entered.
await backupReadEntered.Task.WaitAsync(TimeSpan.FromSeconds(5));
// ── SECOND failover (while the gen-1 re-seed read is still blocked): disconnect
// ── SECOND failover (while the gen 1 re-seed read is still blocked): disconnect
// backup → unstable → failover round-robin back to primary config (generation
// bumps 1 → 2). After this the actor's current generation is 2. ──
RaiseDisconnected(backupAdapter);
@@ -1774,16 +1845,16 @@ public class DataConnectionActorTests : TestKit
TimeSpan.FromSeconds(5));
await Task.Delay(200); // actor Connected on primary-again (generation 2)
// The FRESH generation-2 re-seed (read 77.0 on primary-again) is the only value
// The FRESH generation 2 re-seed (read 77.0 on primary-again) is the only value
// that should be applied. It may arrive before or after we release the stale seed;
// fish for it specifically and assert the stale value is never among the updates.
// ── Now release the gated generation-1 backup re-seed read. The continuation
// ── Now release the gated generation 1 backup re-seed read. The continuation
// Tells TagValueReceived(generation = 1); the actor's generation is now 2, so
// HandleTagValueReceived's guard MUST drop it. ──
staleSeedReadGate.SetResult(
new ReadResult(true, new TagValue(staleValue, QualityCode.Good, DateTimeOffset.UtcNow), null));
// The fresh generation-2 value (77.0) is accepted and reaches the subscriber;
// The fresh generation 2 value (77.0) is accepted and reaches the subscriber;
// FishForMessage will fail the test outright if it ever sees the stale 11.0.
var fresh = FishForMessage<TagValueUpdate>(
m =>
@@ -1801,6 +1872,8 @@ public class DataConnectionActorTests : TestKit
// ── Deferred register row 55: first-subscribe resolution counts never reached the collector ──
/// <summary>Verifies that first subscribe all tags resolve pushes resolution counts to health collector.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task FirstSubscribe_AllTagsResolve_PushesResolutionCountsToHealthCollector()
{
@@ -1839,7 +1912,7 @@ public class DataConnectionActorTests : TestKit
_mockHealthCollector.Received().UpdateTagResolution("row55-first-subscribe", 0, 0);
// One instance subscribes to two tags; both resolve on the first attempt, so the
// batch/probe path never runs — exactly the rig's rig-probe-1 shape.
// batch/probe path never runs — exactly the rig's rig probe 1 shape.
actor.Tell(new SubscribeTagsRequest(
"c1", "inst1", "row55-first-subscribe",
["sensor/temp", "sensor/pressure"], DateTimeOffset.UtcNow));