DET-004: Refactor Policy Gates for determinism

- EarnedCapacityEvaluator: inject TimeProvider
- BudgetThresholdNotifier: inject TimeProvider

Replace DateTimeOffset.UtcNow with _timeProvider.GetUtcNow()

Sprint: SPRINT_20260104_001_BE_determinism_timeprovider_injection
This commit is contained in:
StellaOps Bot
2026-01-04 12:40:10 +02:00
parent 8e0cc71b2e
commit 406c6c119f
2 changed files with 14 additions and 8 deletions

View File

@@ -18,6 +18,7 @@ public sealed class BudgetThresholdNotifier
{ {
private readonly INotifyEventPublisher _publisher; private readonly INotifyEventPublisher _publisher;
private readonly ILogger<BudgetThresholdNotifier> _logger; private readonly ILogger<BudgetThresholdNotifier> _logger;
private readonly TimeProvider _timeProvider;
/// <summary> /// <summary>
/// Thresholds for different budget status levels. /// Thresholds for different budget status levels.
@@ -37,10 +38,12 @@ public sealed class BudgetThresholdNotifier
/// </summary> /// </summary>
public BudgetThresholdNotifier( public BudgetThresholdNotifier(
INotifyEventPublisher publisher, INotifyEventPublisher publisher,
ILogger<BudgetThresholdNotifier> logger) ILogger<BudgetThresholdNotifier> logger,
TimeProvider? timeProvider = null)
{ {
_publisher = publisher ?? throw new ArgumentNullException(nameof(publisher)); _publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
} }
/// <summary> /// <summary>
@@ -73,7 +76,7 @@ public sealed class BudgetThresholdNotifier
{ {
if (budget.Status >= BudgetStatus.Yellow) if (budget.Status >= BudgetStatus.Yellow)
{ {
var payload = CreatePayload(budget, "warning"); var payload = CreatePayload(budget, "warning", _timeProvider);
await _publisher.PublishAsync( await _publisher.PublishAsync(
BudgetEventKinds.PolicyBudgetWarning, BudgetEventKinds.PolicyBudgetWarning,
tenantId, tenantId,
@@ -95,7 +98,7 @@ public sealed class BudgetThresholdNotifier
string tenantId, string tenantId,
CancellationToken ct = default) CancellationToken ct = default)
{ {
var payload = CreatePayload(budget, "exceeded"); var payload = CreatePayload(budget, "exceeded", _timeProvider);
await _publisher.PublishAsync( await _publisher.PublishAsync(
BudgetEventKinds.PolicyBudgetExceeded, BudgetEventKinds.PolicyBudgetExceeded,
tenantId, tenantId,
@@ -118,7 +121,7 @@ public sealed class BudgetThresholdNotifier
? BudgetEventKinds.PolicyBudgetExceeded ? BudgetEventKinds.PolicyBudgetExceeded
: BudgetEventKinds.PolicyBudgetWarning; : BudgetEventKinds.PolicyBudgetWarning;
var payload = CreatePayload(after, after.Status.ToString().ToLowerInvariant()); var payload = CreatePayload(after, after.Status.ToString().ToLowerInvariant(), _timeProvider);
payload["previousStatus"] = before.Status.ToString().ToLowerInvariant(); payload["previousStatus"] = before.Status.ToString().ToLowerInvariant();
await _publisher.PublishAsync(eventKind, tenantId, payload, ct); await _publisher.PublishAsync(eventKind, tenantId, payload, ct);
@@ -130,7 +133,7 @@ public sealed class BudgetThresholdNotifier
after.Status); after.Status);
} }
private static JsonObject CreatePayload(RiskBudget budget, string severity) private static JsonObject CreatePayload(RiskBudget budget, string severity, TimeProvider timeProvider)
{ {
return new JsonObject return new JsonObject
{ {
@@ -144,7 +147,7 @@ public sealed class BudgetThresholdNotifier
["percentageUsed"] = budget.PercentageUsed, ["percentageUsed"] = budget.PercentageUsed,
["status"] = budget.Status.ToString().ToLowerInvariant(), ["status"] = budget.Status.ToString().ToLowerInvariant(),
["severity"] = severity, ["severity"] = severity,
["timestamp"] = DateTimeOffset.UtcNow.ToString("O") ["timestamp"] = timeProvider.GetUtcNow().ToString("O")
}; };
} }
} }

View File

@@ -16,6 +16,7 @@ public sealed class EarnedCapacityEvaluator
private readonly IPerformanceMetricsStore _metricsStore; private readonly IPerformanceMetricsStore _metricsStore;
private readonly IBudgetStore _budgetStore; private readonly IBudgetStore _budgetStore;
private readonly EarnedCapacityOptions _options; private readonly EarnedCapacityOptions _options;
private readonly TimeProvider _timeProvider;
/// <summary> /// <summary>
/// Create a new earned capacity evaluator. /// Create a new earned capacity evaluator.
@@ -23,11 +24,13 @@ public sealed class EarnedCapacityEvaluator
public EarnedCapacityEvaluator( public EarnedCapacityEvaluator(
IPerformanceMetricsStore metricsStore, IPerformanceMetricsStore metricsStore,
IBudgetStore budgetStore, IBudgetStore budgetStore,
EarnedCapacityOptions? options = null) EarnedCapacityOptions? options = null,
TimeProvider? timeProvider = null)
{ {
_metricsStore = metricsStore ?? throw new ArgumentNullException(nameof(metricsStore)); _metricsStore = metricsStore ?? throw new ArgumentNullException(nameof(metricsStore));
_budgetStore = budgetStore ?? throw new ArgumentNullException(nameof(budgetStore)); _budgetStore = budgetStore ?? throw new ArgumentNullException(nameof(budgetStore));
_options = options ?? new EarnedCapacityOptions(); _options = options ?? new EarnedCapacityOptions();
_timeProvider = timeProvider ?? TimeProvider.System;
} }
/// <summary> /// <summary>
@@ -116,7 +119,7 @@ public sealed class EarnedCapacityEvaluator
var updatedBudget = budget with var updatedBudget = budget with
{ {
Allocated = budget.Allocated + additionalPoints, Allocated = budget.Allocated + additionalPoints,
UpdatedAt = DateTimeOffset.UtcNow UpdatedAt = _timeProvider.GetUtcNow()
}; };
await _budgetStore.UpdateAsync(updatedBudget, ct); await _budgetStore.UpdateAsync(updatedBudget, ct);