//
// 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;
}