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
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.KpiHistory;
namespace ZB.MOM.WW.ScadaBridge.KpiHistory.Tests;
/// <summary>
/// K3 unit tests for <see cref="KpiHistoryOptionsValidator"/>: the default
/// Unit tests for <see cref="KpiHistoryOptionsValidator"/>: the default
/// options must validate, and each rule (SampleInterval / RetentionDays /
/// PurgeInterval / DefaultMaxSeriesPoints) must reject its out-of-range values.
/// </summary>
@@ -11,6 +11,7 @@ public class KpiHistoryOptionsValidatorTests
{
private static KpiHistoryOptionsValidator NewValidator() => new();
/// <summary>Verifies that a zero source timeout fails validation.</summary>
[Fact]
public void Validate_SourceTimeout_Zero_Fails()
{
@@ -19,6 +20,7 @@ public class KpiHistoryOptionsValidatorTests
Assert.Contains("SourceTimeout", result.FailureMessage);
}
/// <summary>Verifies that the default options pass validation.</summary>
[Fact]
public void Validate_Defaults_Pass()
{
@@ -30,6 +32,7 @@ public class KpiHistoryOptionsValidatorTests
// SampleInterval > TimeSpan.Zero
// ---------------------------------------------------------------------
/// <summary>Verifies that a zero sample interval fails validation.</summary>
[Fact]
public void Validate_SampleInterval_Zero_Fails()
{
@@ -42,6 +45,7 @@ public class KpiHistoryOptionsValidatorTests
f => f.Contains(nameof(KpiHistoryOptions.SampleInterval), StringComparison.Ordinal));
}
/// <summary>Verifies that a negative sample interval fails validation.</summary>
[Fact]
public void Validate_SampleInterval_Negative_Fails()
{
@@ -58,6 +62,8 @@ public class KpiHistoryOptionsValidatorTests
// RetentionDays in [1, 3650]
// ---------------------------------------------------------------------
/// <summary>Verifies that an in-range retention period passes validation.</summary>
/// <param name="value">The option value under test.</param>
[Theory]
[InlineData(1)] // min
[InlineData(90)] // default
@@ -71,6 +77,8 @@ public class KpiHistoryOptionsValidatorTests
Assert.True(NewValidator().Validate(null, opts).Succeeded);
}
/// <summary>Verifies that an out-of-range retention period fails validation.</summary>
/// <param name="value">The option value under test.</param>
[Theory]
[InlineData(0)]
[InlineData(3651)]
@@ -90,6 +98,7 @@ public class KpiHistoryOptionsValidatorTests
// PurgeInterval > TimeSpan.Zero
// ---------------------------------------------------------------------
/// <summary>Verifies that a zero purge interval fails validation.</summary>
[Fact]
public void Validate_PurgeInterval_Zero_Fails()
{
@@ -102,6 +111,7 @@ public class KpiHistoryOptionsValidatorTests
f => f.Contains(nameof(KpiHistoryOptions.PurgeInterval), StringComparison.Ordinal));
}
/// <summary>Verifies that a negative purge interval fails validation.</summary>
[Fact]
public void Validate_PurgeInterval_Negative_Fails()
{
@@ -118,6 +128,8 @@ public class KpiHistoryOptionsValidatorTests
// DefaultMaxSeriesPoints in [2, 5000]
// ---------------------------------------------------------------------
/// <summary>Verifies that an in-range default maximum series point count passes validation.</summary>
/// <param name="value">The option value under test.</param>
[Theory]
[InlineData(2)] // min
[InlineData(200)] // default
@@ -128,6 +140,8 @@ public class KpiHistoryOptionsValidatorTests
Assert.True(NewValidator().Validate(null, opts).Succeeded);
}
/// <summary>Verifies that an out-of-range default maximum series point count fails validation.</summary>
/// <param name="value">The option value under test.</param>
[Theory]
[InlineData(1)]
[InlineData(5001)]
@@ -147,6 +161,8 @@ public class KpiHistoryOptionsValidatorTests
// RollupInterval > TimeSpan.Zero
// ---------------------------------------------------------------------
/// <summary>Verifies that a non-positive rollup interval fails validation.</summary>
/// <param name="hours">The rollup interval in hours.</param>
[Theory]
[InlineData(0)]
[InlineData(-1)]
@@ -165,6 +181,8 @@ public class KpiHistoryOptionsValidatorTests
// RollupLookbackHours in [1, 168]
// ---------------------------------------------------------------------
/// <summary>Verifies that an in-range rollup lookback passes validation.</summary>
/// <param name="value">The option value under test.</param>
[Theory]
[InlineData(1)] // min
[InlineData(3)] // default
@@ -175,6 +193,8 @@ public class KpiHistoryOptionsValidatorTests
Assert.True(NewValidator().Validate(null, opts).Succeeded);
}
/// <summary>Verifies that an out-of-range rollup lookback fails validation.</summary>
/// <param name="value">The option value under test.</param>
[Theory]
[InlineData(0)]
[InlineData(169)]
@@ -194,6 +214,8 @@ public class KpiHistoryOptionsValidatorTests
// RollupRetentionDays in [1, 3650] AND >= RetentionDays
// ---------------------------------------------------------------------
/// <summary>Verifies that an in-range rollup retention period passes validation.</summary>
/// <param name="value">The option value under test.</param>
[Theory]
[InlineData(90)] // == RetentionDays default
[InlineData(365)] // default
@@ -204,6 +226,8 @@ public class KpiHistoryOptionsValidatorTests
Assert.True(NewValidator().Validate(null, opts).Succeeded);
}
/// <summary>Verifies that an out-of-range rollup retention period fails validation.</summary>
/// <param name="value">The option value under test.</param>
[Theory]
[InlineData(0)]
[InlineData(3651)]
@@ -219,6 +243,7 @@ public class KpiHistoryOptionsValidatorTests
f => f.Contains(nameof(KpiHistoryOptions.RollupRetentionDays), StringComparison.Ordinal));
}
/// <summary>Verifies that a rollup retention shorter than the raw retention fails validation.</summary>
[Fact]
public void Validate_RollupRetentionDays_LessThanRetentionDays_Fails()
{
@@ -237,6 +262,7 @@ public class KpiHistoryOptionsValidatorTests
&& f.Contains(nameof(KpiHistoryOptions.RetentionDays), StringComparison.Ordinal));
}
/// <summary>Verifies that a rollup retention equal to the raw retention passes validation.</summary>
[Fact]
public void Validate_RollupRetentionDays_EqualToRetentionDays_Passes()
{
@@ -249,6 +275,8 @@ public class KpiHistoryOptionsValidatorTests
// RollupThresholdHours >= 24
// ---------------------------------------------------------------------
/// <summary>Verifies that a rollup threshold at or above the minimum passes validation.</summary>
/// <param name="value">The option value under test.</param>
[Theory]
[InlineData(24)] // min
[InlineData(168)] // default
@@ -259,6 +287,8 @@ public class KpiHistoryOptionsValidatorTests
Assert.True(NewValidator().Validate(null, opts).Succeeded);
}
/// <summary>Verifies that a rollup threshold below the minimum fails validation.</summary>
/// <param name="value">The option value under test.</param>
[Theory]
[InlineData(23)]
[InlineData(0)]
@@ -274,6 +304,7 @@ public class KpiHistoryOptionsValidatorTests
f => f.Contains(nameof(KpiHistoryOptions.RollupThresholdHours), StringComparison.Ordinal));
}
/// <summary>Verifies that violating every rule accumulates every failure message.</summary>
[Fact]
public void Validate_AllRulesViolated_AccumulatesEveryFailure()
{
@@ -25,8 +25,10 @@ public class KpiHistoryRecorderActorTests : TestKit
/// </summary>
private sealed class HealthySource : IKpiSampleSource
{
/// <inheritdoc />
public string Source => KpiSources.NotificationOutbox;
/// <inheritdoc />
public Task<IReadOnlyList<KpiSample>> CollectAsync(
DateTime capturedAtUtc, CancellationToken cancellationToken = default)
{
@@ -58,8 +60,10 @@ public class KpiHistoryRecorderActorTests : TestKit
/// <summary>A source whose <see cref="CollectAsync"/> always throws — must not abort the pass.</summary>
private sealed class ThrowingSource : IKpiSampleSource
{
/// <inheritdoc />
public string Source => KpiSources.SiteCallAudit;
/// <inheritdoc />
public Task<IReadOnlyList<KpiSample>> CollectAsync(
DateTime capturedAtUtc, CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("simulated source failure");
@@ -74,8 +78,10 @@ public class KpiHistoryRecorderActorTests : TestKit
{
private int _callCount;
/// <inheritdoc />
public string Source => KpiSources.SiteCallAudit;
/// <inheritdoc />
public Task<IReadOnlyList<KpiSample>> CollectAsync(
DateTime capturedAtUtc, CancellationToken cancellationToken = default)
{
@@ -116,6 +122,7 @@ public class KpiHistoryRecorderActorTests : TestKit
private DateTime? _latestRollupHour;
private int _latestRollupHourQueryCount;
/// <summary>The samples recorded so far, as a snapshot.</summary>
public IReadOnlyList<KpiSample> Recorded
{
get { lock (_gate) { return _recorded.ToArray(); } }
@@ -123,6 +130,7 @@ public class KpiHistoryRecorderActorTests : TestKit
// PurgeOlderThanAsync runs on a threadpool thread; guard the field with
// the same _gate lock used by _recorded so test-thread reads are race-free.
/// <summary>The cutoff passed to the most recent purge, or <see langword="null"/> if none.</summary>
public DateTime? PurgeCutoff
{
get { lock (_gate) { return _purgeCutoff; } }
@@ -158,6 +166,7 @@ public class KpiHistoryRecorderActorTests : TestKit
get { lock (_gate) { return _foldWindows.ToArray(); } }
}
/// <inheritdoc />
public Task RecordSamplesAsync(
IReadOnlyCollection<KpiSample> samples, CancellationToken cancellationToken = default)
{
@@ -168,17 +177,20 @@ public class KpiHistoryRecorderActorTests : TestKit
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<IReadOnlyList<KpiSeriesPoint>> GetRawSeriesAsync(
string source, string metric, string scope, string? scopeKey,
DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<KpiSeriesPoint>>(Array.Empty<KpiSeriesPoint>());
/// <inheritdoc />
public Task<int> PurgeOlderThanAsync(DateTime before, CancellationToken cancellationToken = default)
{
lock (_gate) { _purgeCutoff = before; }
return Task.FromResult(0);
}
/// <inheritdoc />
public Task FoldHourlyRollupsAsync(
DateTime fromHourUtc, DateTime toHourUtc, CancellationToken cancellationToken = default)
{
@@ -192,11 +204,13 @@ public class KpiHistoryRecorderActorTests : TestKit
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<IReadOnlyList<KpiSeriesPoint>> GetHourlySeriesAsync(
string source, string metric, string scope, string? scopeKey,
DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<KpiSeriesPoint>>(Array.Empty<KpiSeriesPoint>());
/// <inheritdoc />
public Task PurgeRollupsOlderThanAsync(DateTime before, CancellationToken cancellationToken = default)
{
lock (_gate) { _rollupPurgeCutoff = before; }
@@ -216,6 +230,7 @@ public class KpiHistoryRecorderActorTests : TestKit
get { lock (_gate) { return _latestRollupHourQueryCount; } }
}
/// <inheritdoc />
public Task<DateTime?> GetLatestRollupHourAsync(CancellationToken cancellationToken = default)
{
lock (_gate)
@@ -242,6 +257,7 @@ public class KpiHistoryRecorderActorTests : TestKit
/// <summary>Releases all blocked writes so the gated passes can complete.</summary>
public void Release() => _release.Set();
/// <inheritdoc />
public Task RecordSamplesAsync(
IReadOnlyCollection<KpiSample> samples, CancellationToken cancellationToken = default)
{
@@ -251,26 +267,32 @@ public class KpiHistoryRecorderActorTests : TestKit
return Task.Run(() => _release.Wait(cancellationToken), cancellationToken);
}
/// <inheritdoc />
public Task<IReadOnlyList<KpiSeriesPoint>> GetRawSeriesAsync(
string source, string metric, string scope, string? scopeKey,
DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<KpiSeriesPoint>>(Array.Empty<KpiSeriesPoint>());
/// <inheritdoc />
public Task<int> PurgeOlderThanAsync(DateTime before, CancellationToken cancellationToken = default) =>
Task.FromResult(0);
/// <inheritdoc />
public Task FoldHourlyRollupsAsync(
DateTime fromHourUtc, DateTime toHourUtc, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
/// <inheritdoc />
public Task<IReadOnlyList<KpiSeriesPoint>> GetHourlySeriesAsync(
string source, string metric, string scope, string? scopeKey,
DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<KpiSeriesPoint>>(Array.Empty<KpiSeriesPoint>());
/// <inheritdoc />
public Task PurgeRollupsOlderThanAsync(DateTime before, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
/// <inheritdoc />
public Task<DateTime?> GetLatestRollupHourAsync(CancellationToken cancellationToken = default) =>
Task.FromResult<DateTime?>(null);
}
@@ -291,18 +313,22 @@ public class KpiHistoryRecorderActorTests : TestKit
/// <summary>Releases all blocked folds so the gated passes can complete.</summary>
public void Release() => _release.Set();
/// <inheritdoc />
public Task RecordSamplesAsync(
IReadOnlyCollection<KpiSample> samples, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
/// <inheritdoc />
public Task<IReadOnlyList<KpiSeriesPoint>> GetRawSeriesAsync(
string source, string metric, string scope, string? scopeKey,
DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<KpiSeriesPoint>>(Array.Empty<KpiSeriesPoint>());
/// <inheritdoc />
public Task<int> PurgeOlderThanAsync(DateTime before, CancellationToken cancellationToken = default) =>
Task.FromResult(0);
/// <inheritdoc />
public Task FoldHourlyRollupsAsync(
DateTime fromHourUtc, DateTime toHourUtc, CancellationToken cancellationToken = default)
{
@@ -312,14 +338,17 @@ public class KpiHistoryRecorderActorTests : TestKit
return Task.Run(() => _release.Wait(cancellationToken), cancellationToken);
}
/// <inheritdoc />
public Task<IReadOnlyList<KpiSeriesPoint>> GetHourlySeriesAsync(
string source, string metric, string scope, string? scopeKey,
DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<KpiSeriesPoint>>(Array.Empty<KpiSeriesPoint>());
/// <inheritdoc />
public Task PurgeRollupsOlderThanAsync(DateTime before, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
/// <inheritdoc />
public Task<DateTime?> GetLatestRollupHourAsync(CancellationToken cancellationToken = default) =>
Task.FromResult<DateTime?>(null);
}
@@ -347,18 +376,22 @@ public class KpiHistoryRecorderActorTests : TestKit
get { lock (_gate) { return _lastFoldToHourUtc; } }
}
/// <inheritdoc />
public Task RecordSamplesAsync(
IReadOnlyCollection<KpiSample> samples, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
/// <inheritdoc />
public Task<IReadOnlyList<KpiSeriesPoint>> GetRawSeriesAsync(
string source, string metric, string scope, string? scopeKey,
DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<KpiSeriesPoint>>(Array.Empty<KpiSeriesPoint>());
/// <inheritdoc />
public Task<int> PurgeOlderThanAsync(DateTime before, CancellationToken cancellationToken = default) =>
Task.FromResult(0);
/// <inheritdoc />
public Task FoldHourlyRollupsAsync(
DateTime fromHourUtc, DateTime toHourUtc, CancellationToken cancellationToken = default)
{
@@ -372,14 +405,17 @@ public class KpiHistoryRecorderActorTests : TestKit
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<IReadOnlyList<KpiSeriesPoint>> GetHourlySeriesAsync(
string source, string metric, string scope, string? scopeKey,
DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<KpiSeriesPoint>>(Array.Empty<KpiSeriesPoint>());
/// <inheritdoc />
public Task PurgeRollupsOlderThanAsync(DateTime before, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
/// <inheritdoc />
public Task<DateTime?> GetLatestRollupHourAsync(CancellationToken cancellationToken = default) =>
Task.FromResult<DateTime?>(null);
}
@@ -400,6 +436,8 @@ public class KpiHistoryRecorderActorTests : TestKit
private int _periodicFoldCount;
private int _backfillCountAtFirstPeriodic = -1;
/// <summary>Initializes the repository with the width that distinguishes a periodic fold window from a backfill slice.</summary>
/// <param name="periodicWindowWidth">The width of a periodic fold window.</param>
public InterleaveFoldRepository(TimeSpan periodicWindowWidth) =>
_periodicWindowWidth = periodicWindowWidth;
@@ -415,18 +453,22 @@ public class KpiHistoryRecorderActorTests : TestKit
/// <summary>Releases the first (held-in-flight) periodic fold so the deferred backfill resumes.</summary>
public void ReleaseFirstPeriodic() => _firstPeriodicRelease.Release();
/// <inheritdoc />
public Task RecordSamplesAsync(
IReadOnlyCollection<KpiSample> samples, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
/// <inheritdoc />
public Task<IReadOnlyList<KpiSeriesPoint>> GetRawSeriesAsync(
string source, string metric, string scope, string? scopeKey,
DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<KpiSeriesPoint>>(Array.Empty<KpiSeriesPoint>());
/// <inheritdoc />
public Task<int> PurgeOlderThanAsync(DateTime before, CancellationToken cancellationToken = default) =>
Task.FromResult(0);
/// <inheritdoc />
public Task FoldHourlyRollupsAsync(
DateTime fromHourUtc, DateTime toHourUtc, CancellationToken cancellationToken = default)
{
@@ -455,14 +497,17 @@ public class KpiHistoryRecorderActorTests : TestKit
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<IReadOnlyList<KpiSeriesPoint>> GetHourlySeriesAsync(
string source, string metric, string scope, string? scopeKey,
DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<KpiSeriesPoint>>(Array.Empty<KpiSeriesPoint>());
/// <inheritdoc />
public Task PurgeRollupsOlderThanAsync(DateTime before, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
/// <inheritdoc />
public Task<DateTime?> GetLatestRollupHourAsync(CancellationToken cancellationToken = default) =>
Task.FromResult<DateTime?>(null);
}
@@ -500,7 +545,7 @@ public class KpiHistoryRecorderActorTests : TestKit
}
/// <summary>
/// Row-40 house pattern for an ABSENCE assertion (mirrors the Medium-wave
/// House pattern for an ABSENCE assertion (mirrors the Medium-wave
/// <c>SiteAlarmAggregatorActorTests.AssertHoldsFor</c> treatment): proves
/// <paramref name="holds"/> keeps being true for the whole <paramref name="window"/> by
/// polling, rather than a single blind <c>Thread.Sleep(window)</c> followed by one
@@ -531,19 +576,24 @@ public class KpiHistoryRecorderActorTests : TestKit
private sealed class HangingSource(HangingState state) : IKpiSampleSource, IDisposable
{
private bool _called;
/// <inheritdoc />
public string Source => "hung";
/// <inheritdoc />
public Task<IReadOnlyList<KpiSample>> CollectAsync(DateTime capturedAtUtc, CancellationToken cancellationToken = default)
{
_called = true;
Interlocked.Increment(ref state.Calls);
return state.Completion.Task; // deliberately ignores cancellation
}
/// <summary>Releases the resources held by this test class.</summary>
public void Dispose()
{
if (_called) Interlocked.Increment(ref state.ActiveScopesDisposed);
}
}
/// <summary>Verifies that a hung source neither blocks healthy sources nor loses scope ownership.</summary>
/// <param name="replaceActor">Whether the recorder actor is replaced while the source is hung.</param>
[Theory]
[InlineData(false)]
[InlineData(true)]
@@ -595,6 +645,7 @@ public class KpiHistoryRecorderActorTests : TestKit
AwaitAssert(() => Assert.Equal(1, Volatile.Read(ref state.ActiveScopesDisposed)), TimeSpan.FromSeconds(2));
}
/// <summary>Verifies that a sample tick writes the healthy source's samples and a throwing source does not abort the tick.</summary>
[Fact]
public void SampleTick_WritesHealthySourceSamples_AndThrowingSourceDoesNotAbortTick()
{
@@ -618,6 +669,7 @@ public class KpiHistoryRecorderActorTests : TestKit
interval: TimeSpan.FromMilliseconds(50));
}
/// <summary>Verifies that a purge tick calls purge with a cutoff of UTC now minus the retention days.</summary>
[Fact]
public void PurgeTick_CallsPurgeWithCutoff_AtUtcNowMinusRetentionDays()
{
@@ -646,6 +698,7 @@ public class KpiHistoryRecorderActorTests : TestKit
interval: TimeSpan.FromMilliseconds(50));
}
/// <summary>Verifies that a faulted tick does not crash the actor and a subsequent tick still runs.</summary>
[Fact]
public void FaultedTick_DoesNotCrashActor_AndSubsequentTickStillRuns()
{
@@ -671,8 +724,8 @@ public class KpiHistoryRecorderActorTests : TestKit
// in-flight guard after the faulted first pass — a tick that lands before the guard
// clears is harmlessly skipped, and the next poll's tick runs. The recovered sample
// being recorded proves the actor's message loop is still alive after a faulted pass.
// (>= 1 rather than exactly-1: a later re-sent tick could run an extra recovering pass
// once the guard clears, which is not what this test pins — KH-001's one-pass-per-tick
// (>= 1 rather than exactly one: a later re-sent tick could run an extra recovering pass
// once the guard clears, which is not what this test pins — the one-pass-per-tick
// guard is pinned by OverlappingTick_WhileFirstPassInFlight_DoesNotStartSecondPass.)
AwaitAssert(
() =>
@@ -684,10 +737,11 @@ public class KpiHistoryRecorderActorTests : TestKit
interval: TimeSpan.FromMilliseconds(50));
}
/// <summary>Verifies that an overlapping tick while the first pass is in flight does not start a second pass.</summary>
[Fact]
public void OverlappingTick_WhileFirstPassInFlight_DoesNotStartSecondPass()
{
// KpiHistory-001 regression: the in-flight guard must coalesce a tick that arrives
// Regression: the in-flight guard must coalesce a tick that arrives
// while a prior sample pass is still awaiting its DB write. With a gated repository
// holding the first write open, a second SampleTick must NOT spawn a second pass —
// so RecordSamplesAsync is entered exactly once until the gate is released.
@@ -727,9 +781,10 @@ public class KpiHistoryRecorderActorTests : TestKit
}
// =====================================================================
// Hourly rollup fold (Task 4)
// Hourly rollup fold
// =====================================================================
/// <summary>Verifies that a rollup tick folds the trailing lookback window with the in-progress hour excluded.</summary>
[Fact]
public void RollupTick_FoldsTrailingLookback_WithInProgressHourExcluded()
{
@@ -779,6 +834,7 @@ public class KpiHistoryRecorderActorTests : TestKit
interval: TimeSpan.FromMilliseconds(50));
}
/// <summary>Verifies that a purge tick purges both raw samples and rollups with their own cutoffs.</summary>
[Fact]
public void PurgeTick_PurgesBothRawSamplesAndRollups_WithTheirOwnCutoffs()
{
@@ -818,6 +874,7 @@ public class KpiHistoryRecorderActorTests : TestKit
interval: TimeSpan.FromMilliseconds(50));
}
/// <summary>Verifies that an overlapping rollup tick while the first fold is in flight does not start a second fold.</summary>
[Fact]
public void OverlappingRollupTick_WhileFirstFoldInFlight_DoesNotStartSecondFold()
{
@@ -856,6 +913,7 @@ public class KpiHistoryRecorderActorTests : TestKit
interval: TimeSpan.FromMilliseconds(50));
}
/// <summary>Verifies that a faulted rollup fold does not crash the actor and a subsequent tick still folds.</summary>
[Fact]
public void FaultedRollupFold_DoesNotCrashActor_AndSubsequentTickStillFolds()
{
@@ -886,9 +944,10 @@ public class KpiHistoryRecorderActorTests : TestKit
}
// =====================================================================
// One-shot rollup backfill (Task 6)
// One-shot rollup backfill
// =====================================================================
/// <summary>Verifies that the backfill tick folds the full retention window once.</summary>
[Fact]
public void BackfillTick_FoldsFullRetentionWindow_Once()
{
@@ -950,6 +1009,7 @@ public class KpiHistoryRecorderActorTests : TestKit
Assert.Equal(foldedCount, repository.FoldWindows.Count);
}
/// <summary>Verifies that the backfill slices the retention window into bounded day-long folds, oldest first.</summary>
[Fact]
public void Backfill_SlicesRetentionWindow_IntoBoundedDayFolds_OldestFirst()
{
@@ -996,6 +1056,8 @@ public class KpiHistoryRecorderActorTests : TestKit
interval: TimeSpan.FromMilliseconds(50));
}
/// <summary>Verifies that a periodic rollup tick interleaves between backfill slices.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task PeriodicRollupTick_Interleaves_BetweenBackfillSlices()
{
@@ -1060,6 +1122,7 @@ public class KpiHistoryRecorderActorTests : TestKit
interval: TimeSpan.FromMilliseconds(50));
}
/// <summary>Verifies that a backfill in flight causes the periodic rollup tick to be skipped.</summary>
[Fact]
public void BackfillInFlight_SkipsPeriodicRollupTick()
{
@@ -1105,6 +1168,7 @@ public class KpiHistoryRecorderActorTests : TestKit
interval: TimeSpan.FromMilliseconds(50));
}
/// <summary>Verifies that the backfill is skipped when rollups are already current.</summary>
[Fact]
public void Backfill_Skips_WhenRollupsAlreadyCurrent()
{
@@ -1147,6 +1211,7 @@ public class KpiHistoryRecorderActorTests : TestKit
"the done-latch must make a second BackfillTick a no-op");
}
/// <summary>Verifies that the backfill shrinks to the watermark instead of the retention floor.</summary>
[Fact]
public void Backfill_ShrinksToWatermark_InsteadOfRetentionFloor()
{