// // Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later. // // Sprint: SPRINT_20260105_002_002_TEST_trace_replay_evidence // Task: TREP-007, TREP-008 using System.Collections.Immutable; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using StellaOps.Replay.Anonymization; using StellaOps.Testing.Temporal; using Xunit; namespace StellaOps.Testing.Replay; /// /// Base class for integration tests that replay production traces. /// public abstract class ReplayIntegrationTestBase : IAsyncLifetime { /// /// Gets the trace corpus manager. /// protected ITraceCorpusManager CorpusManager { get; private set; } = null!; /// /// Gets the replay orchestrator. /// protected IReplayOrchestrator ReplayOrchestrator { get; private set; } = null!; /// /// Gets the simulated time provider. /// protected SimulatedTimeProvider TimeProvider { get; private set; } = null!; /// /// Gets the service provider. /// protected IServiceProvider Services { get; private set; } = null!; /// public virtual async ValueTask InitializeAsync() { var services = new ServiceCollection(); ConfigureServices(services); Services = services.BuildServiceProvider(); CorpusManager = Services.GetRequiredService(); ReplayOrchestrator = Services.GetRequiredService(); TimeProvider = Services.GetRequiredService(); await OnInitializedAsync(); } /// /// Configure services for the test. /// /// The service collection. protected virtual void ConfigureServices(IServiceCollection services) { services.AddReplayTesting(); } /// /// Called after initialization is complete. /// protected virtual Task OnInitializedAsync() => Task.CompletedTask; /// /// Replay a trace and verify behavior matches expected outcome. /// /// The trace to replay. /// Expected outcome. /// The replay result. protected async Task ReplayAndVerifyAsync( TraceCorpusEntry trace, ReplayExpectation expectation) { var result = await ReplayOrchestrator.ReplayAsync( trace.Trace, TimeProvider); VerifyExpectation(result, expectation); return result; } /// /// Replay all traces matching query and collect results. /// /// Query for traces to replay. /// Factory to create expectations per trace. /// Batch replay results. protected async Task ReplayBatchAsync( TraceQuery query, Func expectationFactory) { var results = new List<(TraceCorpusEntry Trace, ReplayResult Result, bool Passed)>(); await foreach (var trace in CorpusManager.QueryAsync(query)) { var expectation = expectationFactory(trace); var result = await ReplayOrchestrator.ReplayAsync(trace.Trace, TimeProvider); var passed = VerifyExpectationSafe(result, expectation); results.Add((trace, result, passed)); } return new ReplayBatchResult([.. results]); } private static void VerifyExpectation(ReplayResult result, ReplayExpectation expectation) { if (expectation.ShouldSucceed) { result.Success.Should().BeTrue( $"Replay should succeed: {result.FailureReason}"); } else { result.Success.Should().BeFalse( $"Replay should fail with: {expectation.ExpectedFailure}"); } if (expectation.ExpectedOutputHash is not null) { result.OutputHash.Should().Be(expectation.ExpectedOutputHash, "Output hash should match expected"); } } private static bool VerifyExpectationSafe(ReplayResult result, ReplayExpectation expectation) { try { VerifyExpectation(result, expectation); return true; } catch { return false; } } /// public virtual ValueTask DisposeAsync() => ValueTask.CompletedTask; } /// /// Expected outcome of a trace replay. /// /// Whether replay should succeed. /// Expected failure reason, if should fail. /// Expected output hash for determinism check. /// Expected warnings. public sealed record ReplayExpectation( bool ShouldSucceed, string? ExpectedFailure = null, string? ExpectedOutputHash = null, ImmutableArray ExpectedWarnings = default); /// /// Result of a batch replay operation. /// /// Individual trace results. public sealed record ReplayBatchResult( ImmutableArray<(TraceCorpusEntry Trace, ReplayResult Result, bool Passed)> Results) { /// /// Gets the total number of traces replayed. /// public int TotalCount => Results.Length; /// /// Gets the number of traces that passed. /// public int PassedCount => Results.Count(r => r.Passed); /// /// Gets the number of traces that failed. /// public int FailedCount => Results.Count(r => !r.Passed); /// /// Gets the pass rate as a decimal (0-1). /// public decimal PassRate => TotalCount > 0 ? (decimal)PassedCount / TotalCount : 0; }