//
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
//
using System;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
namespace StellaOps.E2E.ReplayableVerdict;
///
/// E2E tests for reproducible verdict generation and replay
/// Sprint: SPRINT_20251229_004_005_E2E
///
[Trait("Category", "E2E")]
[Trait("Category", "Determinism")]
public sealed class ReplayableVerdictE2ETests : IAsyncLifetime
{
private const string BundlePath = "../../../fixtures/e2e/bundle-0001";
private GoldenBundle? _bundle;
public async Task InitializeAsync()
{
_bundle = await GoldenBundle.LoadAsync(BundlePath);
}
public Task DisposeAsync()
{
return Task.CompletedTask;
}
[Fact(Skip = "E2E-002: Requires full pipeline integration")]
public async Task FullPipeline_ProducesConsistentVerdict()
{
// Arrange
_bundle.Should().NotBeNull();
// This test requires:
// - Scanner service to process SBOM
// - VexLens to compute consensus
// - Verdict builder to generate final verdict
// Currently skipped until services are integrated
// Act
// var scanResult = await Scanner.ScanAsync(_bundle.ImageDigest);
// var vexConsensus = await VexLens.ComputeConsensusAsync(scanResult.SbomDigest, _bundle.FeedSnapshot);
// var verdict = await VerdictBuilder.BuildAsync(evidencePack, _bundle.PolicyLock);
// Assert
// verdict.CgsHash.Should().Be(_bundle.ExpectedVerdictHash);
}
[Fact(Skip = "E2E-003: Requires verdict builder service")]
public async Task ReplayFromBundle_ProducesIdenticalVerdict()
{
// Arrange
_bundle.Should().NotBeNull();
var originalVerdictHash = _bundle!.Manifest.ExpectedOutputs.VerdictHash;
// Act
// var replayedVerdict = await VerdictBuilder.ReplayAsync(_bundle.Manifest);
// Assert
// replayedVerdict.CgsHash.Should().Be(originalVerdictHash);
}
[Fact]
public void Bundle_LoadsSuccessfully()
{
// Assert
_bundle.Should().NotBeNull();
_bundle!.Manifest.Should().NotBeNull();
_bundle.Manifest.SchemaVersion.Should().Be("2.0");
_bundle.Manifest.BundleId.Should().Be("bundle-0001");
}
[Fact]
public void Bundle_HasValidInputs()
{
// Assert
_bundle.Should().NotBeNull();
_bundle!.Manifest.Inputs.Should().NotBeNull();
_bundle.Manifest.Inputs.Sbom.Should().NotBeNull();
_bundle.Manifest.Inputs.Feeds.Should().NotBeNull();
_bundle.Manifest.Inputs.Vex.Should().NotBeNull();
_bundle.Manifest.Inputs.Policy.Should().NotBeNull();
}
[Fact]
public void Bundle_SbomFile_Exists()
{
// Arrange
var sbomPath = Path.Combine(BundlePath, _bundle!.Manifest.Inputs.Sbom.Path);
// Assert
File.Exists(sbomPath).Should().BeTrue($"SBOM file should exist at {sbomPath}");
}
[Fact]
public async Task Bundle_SbomFile_IsValidJson()
{
// Arrange
var sbomPath = Path.Combine(BundlePath, _bundle!.Manifest.Inputs.Sbom.Path);
var json = await File.ReadAllTextAsync(sbomPath);
// Act
var doc = JsonDocument.Parse(json);
// Assert
doc.RootElement.TryGetProperty("bomFormat", out var bomFormat).Should().BeTrue();
bomFormat.GetString().Should().Be("CycloneDX");
doc.RootElement.TryGetProperty("components", out var components).Should().BeTrue();
components.GetArrayLength().Should().BeGreaterThan(0);
}
[Fact(Skip = "E2E-004: Requires verdict builder with delta support")]
public async Task DeltaVerdict_ShowsExpectedChanges()
{
// This test requires two bundles (v1 and v2) to compare
// var bundleV1 = await GoldenBundle.LoadAsync("../../../fixtures/e2e/bundle-0001");
// var bundleV2 = await GoldenBundle.LoadAsync("../../../fixtures/e2e/bundle-0002");
// var verdictV1 = await VerdictBuilder.BuildAsync(bundleV1.ToEvidencePack(), bundleV1.PolicyLock);
// var verdictV2 = await VerdictBuilder.BuildAsync(bundleV2.ToEvidencePack(), bundleV2.PolicyLock);
// var delta = await VerdictBuilder.DiffAsync(verdictV1.CgsHash, verdictV2.CgsHash);
// delta.AddedVulns.Should().Contain("CVE-2024-NEW");
// delta.RemovedVulns.Should().Contain("CVE-2024-FIXED");
}
[Fact(Skip = "E2E-005: Requires DSSE signing service")]
public async Task Verdict_HasValidDsseSignature()
{
// var verdict = await VerdictBuilder.BuildAsync(_bundle.ToEvidencePack(), _bundle.PolicyLock);
// var dsseEnvelope = await Signer.SignAsync(verdict);
// var verificationResult = await Signer.VerifyAsync(dsseEnvelope, _bundle.PublicKey);
// verificationResult.IsValid.Should().BeTrue();
// verificationResult.SignedBy.Should().Be("test-keypair");
}
[Fact(Skip = "E2E-006: Requires network isolation support")]
public async Task OfflineReplay_ProducesIdenticalVerdict()
{
// This test should run with network disabled
// AssertNoNetworkCalls();
// var verdict = await VerdictBuilder.ReplayAsync(_bundle.Manifest);
// verdict.CgsHash.Should().Be(_bundle.ExpectedVerdictHash);
}
[Fact(Skip = "E2E-008: Requires cross-platform CI")]
public async Task CrossPlatformReplay_ProducesIdenticalHash()
{
// This test runs on multiple CI runners (Ubuntu, Alpine, Debian)
// var platform = Environment.OSVersion;
// var verdict = await VerdictBuilder.BuildAsync(_bundle.ToEvidencePack(), _bundle.PolicyLock);
// verdict.CgsHash.Should().Be(_bundle.ExpectedVerdictHash,
// $"verdict on {platform} should match golden hash");
}
}
///
/// Golden bundle loader
///
internal sealed class GoldenBundle
{
public required BundleManifest Manifest { get; init; }
public required string BasePath { get; init; }
public static async Task LoadAsync(string path)
{
var manifestPath = Path.Combine(path, "manifest.json");
if (!File.Exists(manifestPath))
{
throw new FileNotFoundException($"Bundle manifest not found: {manifestPath}");
}
var json = await File.ReadAllTextAsync(manifestPath);
var manifest = JsonSerializer.Deserialize(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? throw new InvalidOperationException("Failed to deserialize manifest");
return new GoldenBundle
{
Manifest = manifest,
BasePath = path
};
}
}
///
/// Bundle manifest schema (ReplayManifest v2)
///
internal sealed record BundleManifest
{
public required string SchemaVersion { get; init; }
public required string BundleId { get; init; }
public string? Description { get; init; }
public required string CreatedAt { get; init; }
public required ScanInfo Scan { get; init; }
public required BundleInputs Inputs { get; init; }
public required BundleOutputs ExpectedOutputs { get; init; }
public string? Notes { get; init; }
}
internal sealed record ScanInfo
{
public required string Id { get; init; }
public required string ImageDigest { get; init; }
public required string PolicyDigest { get; init; }
public required string ScorePolicyDigest { get; init; }
public required string FeedSnapshotDigest { get; init; }
public required string Toolchain { get; init; }
public required string AnalyzerSetDigest { get; init; }
}
internal sealed record BundleInputs
{
public required InputFile Sbom { get; init; }
public required InputFile Feeds { get; init; }
public required InputFile Vex { get; init; }
public required InputFile Policy { get; init; }
}
internal sealed record InputFile
{
public required string Path { get; init; }
public required string Sha256 { get; init; }
}
internal sealed record BundleOutputs
{
public required InputFile Verdict { get; init; }
public required string VerdictHash { get; init; }
}