DET-004: Refactor Policy Replay and Deltas for determinism

- ReplayEngine: inject TimeProvider
- ReplayReport: inject TimeProvider and IGuidProvider via builder
- ReplayResult: add TimeProvider parameter to Failed() method
- DeltaComputer: inject TimeProvider
- DeltaVerdictBuilder: inject TimeProvider

Replace DateTimeOffset.UtcNow and Guid.NewGuid() with injected providers

Sprint: SPRINT_20260104_001_BE_determinism_timeprovider_injection
This commit is contained in:
StellaOps Bot
2026-01-04 13:25:15 +02:00
parent 8c10b7203b
commit ae78af4692
5 changed files with 30 additions and 11 deletions

View File

@@ -16,13 +16,16 @@ public sealed class DeltaComputer : IDeltaComputer
{
private readonly ISnapshotService _snapshotService;
private readonly ILogger<DeltaComputer> _logger;
private readonly TimeProvider _timeProvider;
public DeltaComputer(
ISnapshotService snapshotService,
ILogger<DeltaComputer> logger)
ILogger<DeltaComputer> logger,
TimeProvider? timeProvider = null)
{
_snapshotService = snapshotService;
_logger = logger;
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
@@ -58,7 +61,7 @@ public sealed class DeltaComputer : IDeltaComputer
var delta = new SecurityStateDelta
{
DeltaId = "", // Computed below
ComputedAt = DateTimeOffset.UtcNow,
ComputedAt = _timeProvider.GetUtcNow(),
BaselineSnapshotId = baselineSnapshotId,
TargetSnapshotId = targetSnapshotId,
Artifact = artifact,

View File

@@ -127,6 +127,7 @@ public sealed class DeltaVerdictBuilder
private static readonly IVerdictIdGenerator DefaultIdGenerator = new VerdictIdGenerator();
private readonly IVerdictIdGenerator _idGenerator;
private readonly TimeProvider _timeProvider;
private DeltaVerdictStatus _status = DeltaVerdictStatus.Pass;
private DeltaGateLevel _gate = DeltaGateLevel.G1;
private int _riskPoints;
@@ -139,7 +140,7 @@ public sealed class DeltaVerdictBuilder
/// <summary>
/// Creates a new <see cref="DeltaVerdictBuilder"/> with the default ID generator.
/// </summary>
public DeltaVerdictBuilder() : this(DefaultIdGenerator)
public DeltaVerdictBuilder() : this(DefaultIdGenerator, TimeProvider.System)
{
}
@@ -147,9 +148,11 @@ public sealed class DeltaVerdictBuilder
/// Creates a new <see cref="DeltaVerdictBuilder"/> with a custom ID generator.
/// </summary>
/// <param name="idGenerator">Custom verdict ID generator for testing or specialized scenarios.</param>
public DeltaVerdictBuilder(IVerdictIdGenerator idGenerator)
/// <param name="timeProvider">Time provider for deterministic timestamps.</param>
public DeltaVerdictBuilder(IVerdictIdGenerator idGenerator, TimeProvider? timeProvider = null)
{
_idGenerator = idGenerator ?? throw new ArgumentNullException(nameof(idGenerator));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public DeltaVerdictBuilder WithStatus(DeltaVerdictStatus status)
@@ -241,7 +244,7 @@ public sealed class DeltaVerdictBuilder
{
VerdictId = verdictId,
DeltaId = deltaId,
EvaluatedAt = DateTimeOffset.UtcNow,
EvaluatedAt = _timeProvider.GetUtcNow(),
Status = _status,
RecommendedGate = _gate,
RiskPoints = _riskPoints,

View File

@@ -14,16 +14,19 @@ public sealed class ReplayEngine : IReplayEngine
private readonly IKnowledgeSourceResolver _sourceResolver;
private readonly IVerdictComparer _verdictComparer;
private readonly ILogger<ReplayEngine> _logger;
private readonly TimeProvider _timeProvider;
public ReplayEngine(
ISnapshotService snapshotService,
IKnowledgeSourceResolver sourceResolver,
IVerdictComparer verdictComparer,
TimeProvider? timeProvider = null,
ILogger<ReplayEngine>? logger = null)
{
_snapshotService = snapshotService ?? throw new ArgumentNullException(nameof(snapshotService));
_sourceResolver = sourceResolver ?? throw new ArgumentNullException(nameof(sourceResolver));
_verdictComparer = verdictComparer ?? throw new ArgumentNullException(nameof(verdictComparer));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? NullLogger<ReplayEngine>.Instance;
}
@@ -89,7 +92,7 @@ public sealed class ReplayEngine : IReplayEngine
OriginalVerdict = originalVerdict,
DeltaReport = deltaReport,
SnapshotId = request.SnapshotId,
ReplayedAt = DateTimeOffset.UtcNow,
ReplayedAt = _timeProvider.GetUtcNow(),
Duration = stopwatch.Elapsed
};
}

View File

@@ -1,3 +1,5 @@
using StellaOps.Determinism;
namespace StellaOps.Policy.Replay;
/// <summary>
@@ -111,11 +113,19 @@ public sealed class ReplayReportBuilder
private readonly ReplayResult _result;
private readonly ReplayRequest _request;
private readonly List<string> _recommendations = [];
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public ReplayReportBuilder(ReplayRequest request, ReplayResult result)
public ReplayReportBuilder(
ReplayRequest request,
ReplayResult result,
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null)
{
_request = request ?? throw new ArgumentNullException(nameof(request));
_result = result ?? throw new ArgumentNullException(nameof(result));
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
public ReplayReportBuilder AddRecommendation(string recommendation)
@@ -150,8 +160,8 @@ public sealed class ReplayReportBuilder
{
return new ReplayReport
{
ReportId = $"rpt:{Guid.NewGuid():N}",
GeneratedAt = DateTimeOffset.UtcNow,
ReportId = $"rpt:{_guidProvider.NewGuid():N}",
GeneratedAt = _timeProvider.GetUtcNow(),
ArtifactDigest = _request.ArtifactDigest,
SnapshotId = _request.SnapshotId,
OriginalVerdictId = _request.OriginalVerdictId,

View File

@@ -43,12 +43,12 @@ public sealed record ReplayResult
/// <summary>
/// Creates a failed result.
/// </summary>
public static ReplayResult Failed(string snapshotId, string error) => new()
public static ReplayResult Failed(string snapshotId, string error, TimeProvider? timeProvider = null) => new()
{
MatchStatus = ReplayMatchStatus.ReplayFailed,
ReplayedVerdict = ReplayedVerdict.Empty,
SnapshotId = snapshotId,
ReplayedAt = DateTimeOffset.UtcNow,
ReplayedAt = (timeProvider ?? TimeProvider.System).GetUtcNow(),
DeltaReport = new ReplayDeltaReport
{
Summary = error,