Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

@@ -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 = []
}
};
}
}

View File

@@ -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>