Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ReplayManifestExporterTests.cs
|
||||
// Sprint: SPRINT_20251228_001_BE_replay_manifest_ci (T7)
|
||||
// Description: Integration tests for replay manifest export and verification
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Replay.Core.Export;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Replay.Core.Tests.Export;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ReplayManifestExporter"/>.
|
||||
/// </summary>
|
||||
public sealed class ReplayManifestExporterTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
private readonly ReplayManifestExporter _exporter;
|
||||
|
||||
public ReplayManifestExporterTests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"replay-export-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
_exporter = new ReplayManifestExporter();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_tempDir))
|
||||
{
|
||||
Directory.Delete(_tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_WithValidManifest_CreatesExportFile()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var outputPath = Path.Combine(_tempDir, "replay.json");
|
||||
var options = new ReplayExportOptions
|
||||
{
|
||||
OutputPath = outputPath,
|
||||
PrettyPrint = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _exporter.ExportAsync(manifest, options);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.ManifestPath.Should().Be(outputPath);
|
||||
result.ManifestDigest.Should().StartWith("sha256:");
|
||||
File.Exists(outputPath).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_ProducesValidJsonSchema()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var outputPath = Path.Combine(_tempDir, "replay-schema.json");
|
||||
var options = new ReplayExportOptions { OutputPath = outputPath };
|
||||
|
||||
// Act
|
||||
var result = await _exporter.ExportAsync(manifest, options);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
|
||||
var json = await File.ReadAllTextAsync(outputPath);
|
||||
var exportManifest = JsonSerializer.Deserialize<ReplayExportManifest>(json);
|
||||
|
||||
exportManifest.Should().NotBeNull();
|
||||
exportManifest!.Version.Should().Be("1.0.0");
|
||||
exportManifest.Snapshot.Should().NotBeNull();
|
||||
exportManifest.Toolchain.Should().NotBeNull();
|
||||
exportManifest.Inputs.Should().NotBeNull();
|
||||
exportManifest.Outputs.Should().NotBeNull();
|
||||
exportManifest.Verification.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_IncludesToolchainVersions_WhenEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
manifest.Reachability.Graphs.Add(new ReplayReachabilityGraphReference
|
||||
{
|
||||
Analyzer = "java-callgraph",
|
||||
Version = "1.2.3",
|
||||
Hash = "sha256:abc123",
|
||||
CasUri = "cas://graphs/java"
|
||||
});
|
||||
|
||||
var outputPath = Path.Combine(_tempDir, "replay-toolchain.json");
|
||||
var options = new ReplayExportOptions
|
||||
{
|
||||
OutputPath = outputPath,
|
||||
IncludeToolchainVersions = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _exporter.ExportAsync(manifest, options);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Manifest!.Toolchain.AnalyzerVersions.Should().ContainKey("java-callgraph");
|
||||
result.Manifest.Toolchain.AnalyzerVersions!["java-callgraph"].Should().Be("1.2.3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_IncludesFeedSnapshots_WhenEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
manifest.Scan.FeedSnapshot = "sha256:feedhash123456";
|
||||
|
||||
var outputPath = Path.Combine(_tempDir, "replay-feeds.json");
|
||||
var options = new ReplayExportOptions
|
||||
{
|
||||
OutputPath = outputPath,
|
||||
IncludeFeedSnapshots = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _exporter.ExportAsync(manifest, options);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Manifest!.Inputs.Feeds.Should().NotBeEmpty();
|
||||
result.Manifest.Inputs.Feeds[0].Digest.Should().Contain("feedhash123456");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_ExcludesFeedSnapshots_WhenDisabled()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var outputPath = Path.Combine(_tempDir, "replay-no-feeds.json");
|
||||
var options = new ReplayExportOptions
|
||||
{
|
||||
OutputPath = outputPath,
|
||||
IncludeFeedSnapshots = false
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _exporter.ExportAsync(manifest, options);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Manifest!.Inputs.Feeds.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_IncludesReachability_WhenEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
manifest.Reachability.Graphs.Add(new ReplayReachabilityGraphReference
|
||||
{
|
||||
Kind = "static",
|
||||
CasUri = "cas://graphs/main",
|
||||
Hash = "sha256:graphhash",
|
||||
CallgraphId = "main-entry",
|
||||
Analyzer = "dotnet-callgraph",
|
||||
Version = "2.0.0"
|
||||
});
|
||||
|
||||
var outputPath = Path.Combine(_tempDir, "replay-reach.json");
|
||||
var options = new ReplayExportOptions
|
||||
{
|
||||
OutputPath = outputPath,
|
||||
IncludeReachability = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _exporter.ExportAsync(manifest, options);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Manifest!.Inputs.Reachability.Should().NotBeNullOrEmpty();
|
||||
result.Manifest.Inputs.Reachability![0].EntryPoint.Should().Be("main-entry");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_GeneratesVerificationCommand_WhenEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var outputPath = Path.Combine(_tempDir, "replay-verify.json");
|
||||
var options = new ReplayExportOptions
|
||||
{
|
||||
OutputPath = outputPath,
|
||||
GenerateVerificationCommand = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _exporter.ExportAsync(manifest, options);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Manifest!.Verification.Command.Should().Contain("stella replay verify");
|
||||
result.Manifest.Verification.Command.Should().Contain("--manifest");
|
||||
result.Manifest.Verification.Command.Should().Contain("--fail-on-drift");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_SetsCorrectExitCodes()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var outputPath = Path.Combine(_tempDir, "replay-exit.json");
|
||||
var options = new ReplayExportOptions { OutputPath = outputPath };
|
||||
|
||||
// Act
|
||||
var result = await _exporter.ExportAsync(manifest, options);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Manifest!.Verification.ExitCodes.Should().NotBeNull();
|
||||
result.Manifest.Verification.ExitCodes!.Success.Should().Be(0);
|
||||
result.Manifest.Verification.ExitCodes.Drift.Should().Be(1);
|
||||
result.Manifest.Verification.ExitCodes.Error.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_IsDeterministic_SameInputsProduceSameDigest()
|
||||
{
|
||||
// Arrange
|
||||
var manifest1 = CreateTestManifest("fixed-scan-id", new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
var manifest2 = CreateTestManifest("fixed-scan-id", new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
|
||||
var outputPath1 = Path.Combine(_tempDir, "replay1.json");
|
||||
var outputPath2 = Path.Combine(_tempDir, "replay2.json");
|
||||
|
||||
var options1 = new ReplayExportOptions
|
||||
{
|
||||
OutputPath = outputPath1,
|
||||
IncludeCiEnvironment = false // Disable CI env to ensure determinism
|
||||
};
|
||||
var options2 = new ReplayExportOptions
|
||||
{
|
||||
OutputPath = outputPath2,
|
||||
IncludeCiEnvironment = false
|
||||
};
|
||||
|
||||
// Act
|
||||
var result1 = await _exporter.ExportAsync(manifest1, options1);
|
||||
var result2 = await _exporter.ExportAsync(manifest2, options2);
|
||||
|
||||
// Assert
|
||||
result1.Success.Should().BeTrue();
|
||||
result2.Success.Should().BeTrue();
|
||||
result1.ManifestDigest.Should().Be(result2.ManifestDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_CompactJson_OmitsIndentation()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var outputPath = Path.Combine(_tempDir, "replay-compact.json");
|
||||
var options = new ReplayExportOptions
|
||||
{
|
||||
OutputPath = outputPath,
|
||||
PrettyPrint = false
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _exporter.ExportAsync(manifest, options);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
var json = await File.ReadAllTextAsync(outputPath);
|
||||
json.Should().NotContain("\n "); // No indented newlines
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithNonExistentFile_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var options = new ReplayVerifyOptions();
|
||||
|
||||
// Act
|
||||
var result = await _exporter.VerifyAsync("/nonexistent/path.json", options);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeFalse();
|
||||
result.ExitCode.Should().Be(2); // Error exit code
|
||||
result.Error.Should().Contain("not found");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithValidManifest_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var outputPath = Path.Combine(_tempDir, "replay-to-verify.json");
|
||||
var exportOptions = new ReplayExportOptions { OutputPath = outputPath };
|
||||
|
||||
await _exporter.ExportAsync(manifest, exportOptions);
|
||||
|
||||
var verifyOptions = new ReplayVerifyOptions
|
||||
{
|
||||
FailOnSbomDrift = true,
|
||||
FailOnVerdictDrift = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _exporter.VerifyAsync(outputPath, verifyOptions);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.ExitCode.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_RoundTrip_PreservesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
manifest.Scan.PolicyDigest = "sha256:policy123";
|
||||
manifest.Scan.ScorePolicyDigest = "sha256:score456";
|
||||
manifest.Scan.AnalyzerSetDigest = "sha256:analyzer789";
|
||||
|
||||
var outputPath = Path.Combine(_tempDir, "replay-roundtrip.json");
|
||||
var options = new ReplayExportOptions { OutputPath = outputPath };
|
||||
|
||||
// Act
|
||||
var result = await _exporter.ExportAsync(manifest, options);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
|
||||
var json = await File.ReadAllTextAsync(outputPath);
|
||||
var reloaded = JsonSerializer.Deserialize<ReplayExportManifest>(json);
|
||||
|
||||
reloaded.Should().NotBeNull();
|
||||
reloaded!.Outputs.VerdictDigest.Should().Be("sha256:analyzer789");
|
||||
reloaded.Verification.ExpectedVerdictHash.Should().Be("sha256:analyzer789");
|
||||
reloaded.Verification.ExpectedSbomHash.Should().Be("sha256:score456");
|
||||
}
|
||||
|
||||
private static ReplayManifest CreateTestManifest(
|
||||
string? scanId = null,
|
||||
DateTimeOffset? time = null)
|
||||
{
|
||||
return new ReplayManifest
|
||||
{
|
||||
SchemaVersion = ReplayManifestVersions.V2,
|
||||
Scan = new ReplayScanMetadata
|
||||
{
|
||||
Id = scanId ?? $"scan-{Guid.NewGuid():N}",
|
||||
Time = time ?? DateTimeOffset.UtcNow,
|
||||
Toolchain = "stellaops-scanner/1.0.0"
|
||||
},
|
||||
Reachability = new ReplayReachabilitySection
|
||||
{
|
||||
AnalysisId = "test-analysis",
|
||||
Graphs = [],
|
||||
RuntimeTraces = []
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,19 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<NoWarn>$(NoWarn);NETSDK1188</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
|
||||
<ProjectReference Include="../StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" >
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
|
||||
<ProjectReference Include="../StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user