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 ILogger<BudgetThresholdNotifier> _logger;
private readonly TimeProvider _timeProvider;
/// <summary>
/// Thresholds for different budget status levels.
@@ -37,10 +38,12 @@ public sealed class BudgetThresholdNotifier
/// </summary>
public BudgetThresholdNotifier(
INotifyEventPublisher publisher,
ILogger<BudgetThresholdNotifier> logger)
ILogger<BudgetThresholdNotifier> logger,
TimeProvider? timeProvider = null)
{
_publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <summary>
@@ -73,7 +76,7 @@ public sealed class BudgetThresholdNotifier
{
if (budget.Status >= BudgetStatus.Yellow)
{
var payload = CreatePayload(budget, "warning");
var payload = CreatePayload(budget, "warning", _timeProvider);
await _publisher.PublishAsync(
BudgetEventKinds.PolicyBudgetWarning,
tenantId,
@@ -95,7 +98,7 @@ public sealed class BudgetThresholdNotifier
string tenantId,
CancellationToken ct = default)
{
var payload = CreatePayload(budget, "exceeded");
var payload = CreatePayload(budget, "exceeded", _timeProvider);
await _publisher.PublishAsync(
BudgetEventKinds.PolicyBudgetExceeded,
tenantId,
@@ -118,7 +121,7 @@ public sealed class BudgetThresholdNotifier
? BudgetEventKinds.PolicyBudgetExceeded
: 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();
await _publisher.PublishAsync(eventKind, tenantId, payload, ct);
@@ -130,7 +133,7 @@ public sealed class BudgetThresholdNotifier
after.Status);
}
private static JsonObject CreatePayload(RiskBudget budget, string severity)
private static JsonObject CreatePayload(RiskBudget budget, string severity, TimeProvider timeProvider)
{
return new JsonObject
{
@@ -144,7 +147,7 @@ public sealed class BudgetThresholdNotifier
["percentageUsed"] = budget.PercentageUsed,
["status"] = budget.Status.ToString().ToLowerInvariant(),
["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 IBudgetStore _budgetStore;
private readonly EarnedCapacityOptions _options;
private readonly TimeProvider _timeProvider;
/// <summary>
/// Create a new earned capacity evaluator.
@@ -23,11 +24,13 @@ public sealed class EarnedCapacityEvaluator
public EarnedCapacityEvaluator(
IPerformanceMetricsStore metricsStore,
IBudgetStore budgetStore,
EarnedCapacityOptions? options = null)
EarnedCapacityOptions? options = null,
TimeProvider? timeProvider = null)
{
_metricsStore = metricsStore ?? throw new ArgumentNullException(nameof(metricsStore));
_budgetStore = budgetStore ?? throw new ArgumentNullException(nameof(budgetStore));
_options = options ?? new EarnedCapacityOptions();
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <summary>
@@ -116,7 +119,7 @@ public sealed class EarnedCapacityEvaluator
var updatedBudget = budget with
{
Allocated = budget.Allocated + additionalPoints,
UpdatedAt = DateTimeOffset.UtcNow
UpdatedAt = _timeProvider.GetUtcNow()
};
await _budgetStore.UpdateAsync(updatedBudget, ct);