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

View File

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

View File

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

View File

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

View File

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