stabilizaiton work - projects rework for maintenanceability and ui livening

This commit is contained in:
master
2026-02-03 23:40:04 +02:00
parent 074ce117ba
commit 557feefdc3
3305 changed files with 186813 additions and 107843 deletions

View File

@@ -0,0 +1,35 @@
using FluentAssertions;
using StellaOps.TestKit;
using StellaOps.Testing.Determinism;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class DeterminismManifestTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ToCanonicalBytes_WithValidManifest_ProducesDeterministicOutput()
{
var manifest = CreateSampleManifest();
var bytes1 = DeterminismManifestWriter.ToCanonicalBytes(manifest);
var bytes2 = DeterminismManifestWriter.ToCanonicalBytes(manifest);
bytes1.Should().Equal(bytes2, "Same manifest should produce identical canonical bytes");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ToCanonicalString_WithValidManifest_ProducesDeterministicString()
{
var manifest = CreateSampleManifest();
var json1 = DeterminismManifestWriter.ToCanonicalString(manifest);
var json2 = DeterminismManifestWriter.ToCanonicalString(manifest);
json1.Should().Be(json2, "Same manifest should produce identical canonical JSON string");
json1.Should().NotContain("\n", "Canonical JSON should have no newlines");
json1.Should().NotContain(" ", "Canonical JSON should have no indentation");
}
}

View File

@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using StellaOps.Testing.Determinism;
namespace StellaOps.TestKit.Tests;
public sealed partial class DeterminismManifestTests
{
private static DeterminismManifest CreateComplexManifest()
{
return new DeterminismManifest
{
SchemaVersion = "1.0",
Artifact = new ArtifactInfo
{
Type = "evidence-bundle",
Name = "test-bundle",
Version = "2.0.0",
Format = "DSSE Envelope",
Metadata = new Dictionary<string, object?>
{
["predicateType"] = "https://in-toto.io/attestation/v1",
["subject"] = "pkg:docker/alpine@3.18"
}
},
CanonicalHash = new CanonicalHashInfo
{
Algorithm = "SHA-256",
Value = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
Encoding = "hex"
},
Inputs = new InputStamps
{
FeedSnapshotHash = "feed123abc",
PolicyManifestHash = "policy456def",
SourceCodeHash = "git789ghi",
VexDocumentHashes = new[] { "vex123", "vex456" },
Custom = new Dictionary<string, string>
{
["baselineVersion"] = "1.0.0",
["environment"] = "production"
}
},
Toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[]
{
new ComponentInfo { Name = "StellaOps.Attestor", Version = "2.0.0", Hash = "hash123" },
new ComponentInfo { Name = "StellaOps.Signer", Version = "2.1.0" }
},
Compiler = new CompilerInfo
{
Name = "Roslyn",
Version = "4.8.0"
}
},
GeneratedAt = new DateTimeOffset(2025, 12, 23, 18, 0, 0, TimeSpan.Zero),
Reproducibility = new ReproducibilityMetadata
{
DeterministicSeed = 12345,
ClockFixed = true,
OrderingGuarantee = "stable",
NormalizationRules = new[] { "UTF-8", "LF line endings", "no trailing whitespace" }
},
Verification = new VerificationInfo
{
Command = "dotnet test --verify-determinism",
ExpectedHash = "abc123def456",
Baseline = "baselines/test-bundle.json"
},
Signatures = new[]
{
new SignatureInfo
{
Algorithm = "ES256",
KeyId = "signing-key-1",
Signature = "base64encodedSig==",
Timestamp = new DateTimeOffset(2025, 12, 23, 18, 0, 30, TimeSpan.Zero)
}
}
};
}
}

View File

@@ -0,0 +1,21 @@
using FluentAssertions;
using StellaOps.TestKit;
using StellaOps.Testing.Determinism;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class DeterminismManifestTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ManifestSerialization_WithComplexMetadata_PreservesAllFields()
{
var manifest = CreateComplexManifest();
var json = DeterminismManifestWriter.ToCanonicalString(manifest);
var deserialized = DeterminismManifestReader.FromString(json);
deserialized.Should().BeEquivalentTo(manifest);
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Text.Json;
using FluentAssertions;
using StellaOps.TestKit;
using StellaOps.Testing.Determinism;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class DeterminismManifestTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FromBytes_WithValidJson_DeserializesSuccessfully()
{
var manifest = CreateSampleManifest();
var bytes = DeterminismManifestWriter.ToCanonicalBytes(manifest);
var deserialized = DeterminismManifestReader.FromBytes(bytes);
deserialized.Should().BeEquivalentTo(manifest);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FromString_WithValidJson_DeserializesSuccessfully()
{
var manifest = CreateSampleManifest();
var json = DeterminismManifestWriter.ToCanonicalString(manifest);
var deserialized = DeterminismManifestReader.FromString(json);
deserialized.Should().BeEquivalentTo(manifest);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FromBytes_WithInvalidSchemaVersion_ThrowsInvalidOperationException()
{
var manifest = CreateSampleManifest() with { SchemaVersion = "2.0" };
var bytes = JsonSerializer.SerializeToUtf8Bytes(manifest);
Action act = () => DeterminismManifestReader.FromBytes(bytes);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*schema version*2.0*");
}
}

View File

@@ -0,0 +1,63 @@
using System;
using FluentAssertions;
using StellaOps.Canonical.Json;
using StellaOps.TestKit;
using StellaOps.Testing.Determinism;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class DeterminismManifestTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeCanonicalHash_ProducesDeterministicHash()
{
var manifest = CreateSampleManifest();
var hash1 = DeterminismManifestWriter.ComputeCanonicalHash(manifest);
var hash2 = DeterminismManifestWriter.ComputeCanonicalHash(manifest);
hash1.Should().Be(hash2, "Same manifest should produce same hash");
hash1.Should().MatchRegex("^[0-9a-f]{64}$", "Hash should be 64-character hex string");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateManifest_WithValidInputs_CreatesManifestWithCorrectHash()
{
var artifactBytes = "Test artifact content"u8.ToArray();
var artifactInfo = new ArtifactInfo
{
Type = "sbom",
Name = "test-sbom",
Version = "1.0.0",
Format = "SPDX 3.0.1"
};
var toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[]
{
new ComponentInfo { Name = "StellaOps.Scanner", Version = "1.0.0" }
}
};
var manifest = DeterminismManifestWriter.CreateManifest(
artifactBytes,
artifactInfo,
toolchain);
manifest.SchemaVersion.Should().Be("1.0");
manifest.Artifact.Should().Be(artifactInfo);
manifest.Toolchain.Should().Be(toolchain);
manifest.CanonicalHash.Algorithm.Should().Be("SHA-256");
manifest.CanonicalHash.Encoding.Should().Be("hex");
manifest.CanonicalHash.Value.Should().MatchRegex("^[0-9a-f]{64}$");
manifest.GeneratedAt.Should().NotBe(default);
manifest.GeneratedAt.Offset.Should().Be(TimeSpan.Zero);
var expectedHash = CanonJson.Sha256Hex(artifactBytes);
manifest.CanonicalHash.Value.Should().Be(expectedHash);
}
}

View File

@@ -0,0 +1,81 @@
using FluentAssertions;
using StellaOps.Canonical.Json;
using StellaOps.TestKit;
using StellaOps.Testing.Determinism;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class DeterminismManifestTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateManifestForJsonArtifact_WithValidInputs_CreatesManifestWithCanonicalHash()
{
var artifact = new { Name = "test", Value = 123, Items = new[] { "a", "b", "c" } };
var artifactInfo = new ArtifactInfo
{
Type = "verdict",
Name = "test-verdict",
Version = "1.0.0"
};
var toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[]
{
new ComponentInfo { Name = "StellaOps.Policy.Engine", Version = "1.0.0" }
}
};
var manifest = DeterminismManifestWriter.CreateManifestForJsonArtifact(
artifact,
artifactInfo,
toolchain);
manifest.SchemaVersion.Should().Be("1.0");
manifest.Artifact.Should().Be(artifactInfo);
manifest.Toolchain.Should().Be(toolchain);
manifest.CanonicalHash.Algorithm.Should().Be("SHA-256");
manifest.CanonicalHash.Encoding.Should().Be("hex");
manifest.CanonicalHash.Value.Should().MatchRegex("^[0-9a-f]{64}$");
var expectedHash = CanonJson.Hash(artifact);
manifest.CanonicalHash.Value.Should().Be(expectedHash);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateManifest_WithInputStamps_IncludesInputStamps()
{
var artifactBytes = "Test artifact"u8.ToArray();
var artifactInfo = new ArtifactInfo
{
Type = "sbom",
Name = "test",
Version = "1.0.0"
};
var toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
};
var inputs = new InputStamps
{
FeedSnapshotHash = "abc123",
PolicyManifestHash = "def456",
SourceCodeHash = "789abc"
};
var manifest = DeterminismManifestWriter.CreateManifest(
artifactBytes,
artifactInfo,
toolchain,
inputs: inputs);
manifest.Inputs.Should().NotBeNull();
manifest.Inputs!.FeedSnapshotHash.Should().Be("abc123");
manifest.Inputs.PolicyManifestHash.Should().Be("def456");
manifest.Inputs.SourceCodeHash.Should().Be("789abc");
}
}

View File

@@ -0,0 +1,84 @@
using FluentAssertions;
using StellaOps.TestKit;
using StellaOps.Testing.Determinism;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class DeterminismManifestTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateManifest_WithReproducibilityMetadata_IncludesMetadata()
{
var artifactBytes = "Test artifact"u8.ToArray();
var artifactInfo = new ArtifactInfo
{
Type = "sbom",
Name = "test",
Version = "1.0.0"
};
var toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
};
var reproducibility = new ReproducibilityMetadata
{
DeterministicSeed = 42,
ClockFixed = true,
OrderingGuarantee = "sorted",
NormalizationRules = new[] { "UTF-8", "LF line endings", "sorted JSON keys" }
};
var manifest = DeterminismManifestWriter.CreateManifest(
artifactBytes,
artifactInfo,
toolchain,
reproducibility: reproducibility);
manifest.Reproducibility.Should().NotBeNull();
manifest.Reproducibility!.DeterministicSeed.Should().Be(42);
manifest.Reproducibility.ClockFixed.Should().BeTrue();
manifest.Reproducibility.OrderingGuarantee.Should().Be("sorted");
manifest.Reproducibility.NormalizationRules.Should().ContainInOrder(
"UTF-8",
"LF line endings",
"sorted JSON keys");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateManifest_WithVerificationInfo_IncludesVerification()
{
var artifactBytes = "Test artifact"u8.ToArray();
var artifactInfo = new ArtifactInfo
{
Type = "sbom",
Name = "test",
Version = "1.0.0"
};
var toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
};
var verification = new VerificationInfo
{
Command = "dotnet test --verify-determinism",
ExpectedHash = "abc123def456",
Baseline = "baselines/test-bundle.json"
};
var manifest = DeterminismManifestWriter.CreateManifest(
artifactBytes,
artifactInfo,
toolchain,
verification: verification);
manifest.Verification.Should().NotBeNull();
manifest.Verification!.Command.Should().Be("dotnet test --verify-determinism");
manifest.Verification.ExpectedHash.Should().Be("abc123def456");
manifest.Verification.Baseline.Should().Be("baselines/test-bundle.json");
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.IO;
using FluentAssertions;
using StellaOps.TestKit;
using StellaOps.Testing.Determinism;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class DeterminismManifestTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TryReadFromFileAsync_WithNonExistentFile_ReturnsNullAsync()
{
var nonExistentPath = GetMissingFilePath("missing.json");
var result = await DeterminismManifestReader.TryReadFromFileAsync(nonExistentPath);
result.Should().BeNull();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReadFromFile_WithNonExistentFile_ThrowsFileNotFoundException()
{
var nonExistentPath = GetMissingFilePath("missing-sync.json");
Action act = () => DeterminismManifestReader.ReadFromFile(nonExistentPath);
act.Should().Throw<FileNotFoundException>();
}
}

View File

@@ -0,0 +1,29 @@
using System.IO;
namespace StellaOps.TestKit.Tests;
public sealed partial class DeterminismManifestTests
{
private static readonly string DeterminismRoot = Path.Combine(
Path.GetTempPath(),
"stellaops-tests",
"testkit",
"determinism-manifest");
private static string GetFilePath(string fileName)
{
Directory.CreateDirectory(DeterminismRoot);
return Path.Combine(DeterminismRoot, fileName);
}
private static string GetMissingFilePath(string fileName)
{
var path = GetFilePath(fileName);
if (File.Exists(path))
{
File.Delete(path);
}
return path;
}
}

View File

@@ -0,0 +1,58 @@
using System.IO;
using FluentAssertions;
using StellaOps.TestKit;
using StellaOps.Testing.Determinism;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class DeterminismManifestTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void WriteToFile_AndReadFromFile_RoundTripsSuccessfully()
{
var manifest = CreateSampleManifest();
var tempFile = GetMissingFilePath("roundtrip.json");
try
{
DeterminismManifestWriter.WriteToFile(manifest, tempFile);
var readManifest = DeterminismManifestReader.ReadFromFile(tempFile);
readManifest.Should().BeEquivalentTo(manifest);
}
finally
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task WriteToFileAsync_AndReadFromFileAsync_RoundTripsSuccessfullyAsync()
{
var manifest = CreateSampleManifest();
var tempFile = GetMissingFilePath("roundtrip-async.json");
try
{
await DeterminismManifestWriter.WriteToFileAsync(manifest, tempFile);
var readManifest = await DeterminismManifestReader.ReadFromFileAsync(tempFile);
readManifest.Should().BeEquivalentTo(manifest);
}
finally
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
}
}

View File

@@ -0,0 +1,42 @@
using System;
using StellaOps.Testing.Determinism;
namespace StellaOps.TestKit.Tests;
public sealed partial class DeterminismManifestTests
{
private static DeterminismManifest CreateSampleManifest()
{
return new DeterminismManifest
{
SchemaVersion = "1.0",
Artifact = new ArtifactInfo
{
Type = "sbom",
Name = "test-sbom",
Version = "1.0.0",
Format = "SPDX 3.0.1"
},
CanonicalHash = new CanonicalHashInfo
{
Algorithm = "SHA-256",
Value = "abc123def4567890123456789012345678901234567890123456789901234",
Encoding = "hex"
},
Toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[]
{
new ComponentInfo
{
Name = "StellaOps.Scanner",
Version = "1.0.0",
Hash = "def456abc7890123456789012345678901234567890123455678901234567"
}
}
},
GeneratedAt = new DateTimeOffset(2025, 12, 23, 17, 45, 0, TimeSpan.Zero)
};
}
}

View File

@@ -1,513 +0,0 @@
using System.Text.Json;
using FluentAssertions;
using StellaOps.Canonical.Json;
using StellaOps.Testing.Determinism;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.TestKit.Tests;
public sealed class DeterminismManifestTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ToCanonicalBytes_WithValidManifest_ProducesDeterministicOutput()
{
// Arrange
var manifest = CreateSampleManifest();
// Act
var bytes1 = DeterminismManifestWriter.ToCanonicalBytes(manifest);
var bytes2 = DeterminismManifestWriter.ToCanonicalBytes(manifest);
// Assert
bytes1.Should().Equal(bytes2, "Same manifest should produce identical canonical bytes");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ToCanonicalString_WithValidManifest_ProducesDeterministicString()
{
// Arrange
var manifest = CreateSampleManifest();
// Act
var json1 = DeterminismManifestWriter.ToCanonicalString(manifest);
var json2 = DeterminismManifestWriter.ToCanonicalString(manifest);
// Assert
json1.Should().Be(json2, "Same manifest should produce identical canonical JSON string");
json1.Should().NotContain("\n", "Canonical JSON should have no newlines");
json1.Should().NotContain(" ", "Canonical JSON should have no indentation");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void WriteToFile_AndReadFromFile_RoundTripsSuccessfully()
{
// Arrange
var manifest = CreateSampleManifest();
var tempFile = Path.GetTempFileName();
try
{
// Act - Write
DeterminismManifestWriter.WriteToFile(manifest, tempFile);
// Act - Read
var readManifest = DeterminismManifestReader.ReadFromFile(tempFile);
// Assert
readManifest.Should().BeEquivalentTo(manifest);
}
finally
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task WriteToFileAsync_AndReadFromFileAsync_RoundTripsSuccessfully()
{
// Arrange
var manifest = CreateSampleManifest();
var tempFile = Path.GetTempFileName();
try
{
// Act - Write
await DeterminismManifestWriter.WriteToFileAsync(manifest, tempFile);
// Act - Read
var readManifest = await DeterminismManifestReader.ReadFromFileAsync(tempFile);
// Assert
readManifest.Should().BeEquivalentTo(manifest);
}
finally
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FromBytes_WithValidJson_DeserializesSuccessfully()
{
// Arrange
var manifest = CreateSampleManifest();
var bytes = DeterminismManifestWriter.ToCanonicalBytes(manifest);
// Act
var deserialized = DeterminismManifestReader.FromBytes(bytes);
// Assert
deserialized.Should().BeEquivalentTo(manifest);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FromString_WithValidJson_DeserializesSuccessfully()
{
// Arrange
var manifest = CreateSampleManifest();
var json = DeterminismManifestWriter.ToCanonicalString(manifest);
// Act
var deserialized = DeterminismManifestReader.FromString(json);
// Assert
deserialized.Should().BeEquivalentTo(manifest);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FromBytes_WithInvalidSchemaVersion_ThrowsInvalidOperationException()
{
// Arrange
var manifest = CreateSampleManifest() with { SchemaVersion = "2.0" };
var bytes = JsonSerializer.SerializeToUtf8Bytes(manifest);
// Act
Action act = () => DeterminismManifestReader.FromBytes(bytes);
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*schema version*2.0*");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryReadFromFileAsync_WithNonExistentFile_ReturnsNull()
{
// Arrange
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
// Act
var result = DeterminismManifestReader.TryReadFromFileAsync(nonExistentPath).GetAwaiter().GetResult();
// Assert
result.Should().BeNull();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReadFromFile_WithNonExistentFile_ThrowsFileNotFoundException()
{
// Arrange
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
// Act
Action act = () => DeterminismManifestReader.ReadFromFile(nonExistentPath);
// Assert
act.Should().Throw<FileNotFoundException>();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeCanonicalHash_ProducesDeterministicHash()
{
// Arrange
var manifest = CreateSampleManifest();
// Act
var hash1 = DeterminismManifestWriter.ComputeCanonicalHash(manifest);
var hash2 = DeterminismManifestWriter.ComputeCanonicalHash(manifest);
// Assert
hash1.Should().Be(hash2, "Same manifest should produce same hash");
hash1.Should().MatchRegex("^[0-9a-f]{64}$", "Hash should be 64-character hex string");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateManifest_WithValidInputs_CreatesManifestWithCorrectHash()
{
// Arrange
var artifactBytes = "Test artifact content"u8.ToArray();
var artifactInfo = new ArtifactInfo
{
Type = "sbom",
Name = "test-sbom",
Version = "1.0.0",
Format = "SPDX 3.0.1"
};
var toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[]
{
new ComponentInfo { Name = "StellaOps.Scanner", Version = "1.0.0" }
}
};
// Act
var manifest = DeterminismManifestWriter.CreateManifest(
artifactBytes,
artifactInfo,
toolchain);
// Assert
manifest.SchemaVersion.Should().Be("1.0");
manifest.Artifact.Should().Be(artifactInfo);
manifest.Toolchain.Should().Be(toolchain);
manifest.CanonicalHash.Algorithm.Should().Be("SHA-256");
manifest.CanonicalHash.Encoding.Should().Be("hex");
manifest.CanonicalHash.Value.Should().MatchRegex("^[0-9a-f]{64}$");
manifest.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
// Verify hash is correct
var expectedHash = CanonJson.Sha256Hex(artifactBytes);
manifest.CanonicalHash.Value.Should().Be(expectedHash);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateManifestForJsonArtifact_WithValidInputs_CreatesManifestWithCanonicalHash()
{
// Arrange
var artifact = new { Name = "test", Value = 123, Items = new[] { "a", "b", "c" } };
var artifactInfo = new ArtifactInfo
{
Type = "verdict",
Name = "test-verdict",
Version = "1.0.0"
};
var toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[]
{
new ComponentInfo { Name = "StellaOps.Policy.Engine", Version = "1.0.0" }
}
};
// Act
var manifest = DeterminismManifestWriter.CreateManifestForJsonArtifact(
artifact,
artifactInfo,
toolchain);
// Assert
manifest.SchemaVersion.Should().Be("1.0");
manifest.Artifact.Should().Be(artifactInfo);
manifest.Toolchain.Should().Be(toolchain);
manifest.CanonicalHash.Algorithm.Should().Be("SHA-256");
manifest.CanonicalHash.Encoding.Should().Be("hex");
manifest.CanonicalHash.Value.Should().MatchRegex("^[0-9a-f]{64}$");
// Verify hash is correct (should use canonical JSON)
var expectedHash = CanonJson.Hash(artifact);
manifest.CanonicalHash.Value.Should().Be(expectedHash);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateManifest_WithInputStamps_IncludesInputStamps()
{
// Arrange
var artifactBytes = "Test artifact"u8.ToArray();
var artifactInfo = new ArtifactInfo
{
Type = "sbom",
Name = "test",
Version = "1.0.0"
};
var toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
};
var inputs = new InputStamps
{
FeedSnapshotHash = "abc123",
PolicyManifestHash = "def456",
SourceCodeHash = "789abc"
};
// Act
var manifest = DeterminismManifestWriter.CreateManifest(
artifactBytes,
artifactInfo,
toolchain,
inputs: inputs);
// Assert
manifest.Inputs.Should().NotBeNull();
manifest.Inputs!.FeedSnapshotHash.Should().Be("abc123");
manifest.Inputs.PolicyManifestHash.Should().Be("def456");
manifest.Inputs.SourceCodeHash.Should().Be("789abc");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateManifest_WithReproducibilityMetadata_IncludesMetadata()
{
// Arrange
var artifactBytes = "Test artifact"u8.ToArray();
var artifactInfo = new ArtifactInfo
{
Type = "sbom",
Name = "test",
Version = "1.0.0"
};
var toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
};
var reproducibility = new ReproducibilityMetadata
{
DeterministicSeed = 42,
ClockFixed = true,
OrderingGuarantee = "sorted",
NormalizationRules = new[] { "UTF-8", "LF line endings", "sorted JSON keys" }
};
// Act
var manifest = DeterminismManifestWriter.CreateManifest(
artifactBytes,
artifactInfo,
toolchain,
reproducibility: reproducibility);
// Assert
manifest.Reproducibility.Should().NotBeNull();
manifest.Reproducibility!.DeterministicSeed.Should().Be(42);
manifest.Reproducibility.ClockFixed.Should().BeTrue();
manifest.Reproducibility.OrderingGuarantee.Should().Be("sorted");
manifest.Reproducibility.NormalizationRules.Should().ContainInOrder("UTF-8", "LF line endings", "sorted JSON keys");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateManifest_WithVerificationInfo_IncludesVerification()
{
// Arrange
var artifactBytes = "Test artifact"u8.ToArray();
var artifactInfo = new ArtifactInfo
{
Type = "sbom",
Name = "test",
Version = "1.0.0"
};
var toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
};
var verification = new VerificationInfo
{
Command = "dotnet run --project Scanner -- scan container alpine:3.18",
ExpectedHash = "abc123def456",
Baseline = "tests/baselines/sbom-alpine-3.18.determinism.json"
};
// Act
var manifest = DeterminismManifestWriter.CreateManifest(
artifactBytes,
artifactInfo,
toolchain,
verification: verification);
// Assert
manifest.Verification.Should().NotBeNull();
manifest.Verification!.Command.Should().Be("dotnet run --project Scanner -- scan container alpine:3.18");
manifest.Verification.ExpectedHash.Should().Be("abc123def456");
manifest.Verification.Baseline.Should().Be("tests/baselines/sbom-alpine-3.18.determinism.json");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ManifestSerialization_WithComplexMetadata_PreservesAllFields()
{
// Arrange
var manifest = CreateComplexManifest();
// Act
var json = DeterminismManifestWriter.ToCanonicalString(manifest);
var deserialized = DeterminismManifestReader.FromString(json);
// Assert
deserialized.Should().BeEquivalentTo(manifest);
}
private static DeterminismManifest CreateSampleManifest()
{
return new DeterminismManifest
{
SchemaVersion = "1.0",
Artifact = new ArtifactInfo
{
Type = "sbom",
Name = "test-sbom",
Version = "1.0.0",
Format = "SPDX 3.0.1"
},
CanonicalHash = new CanonicalHashInfo
{
Algorithm = "SHA-256",
Value = "abc123def456789012345678901234567890123456789012345678901234",
Encoding = "hex"
},
Toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[]
{
new ComponentInfo
{
Name = "StellaOps.Scanner",
Version = "1.0.0",
Hash = "def456abc789012345678901234567890123456789012345678901234567"
}
}
},
GeneratedAt = new DateTimeOffset(2025, 12, 23, 17, 45, 0, TimeSpan.Zero)
};
}
private static DeterminismManifest CreateComplexManifest()
{
return new DeterminismManifest
{
SchemaVersion = "1.0",
Artifact = new ArtifactInfo
{
Type = "evidence-bundle",
Name = "test-bundle",
Version = "2.0.0",
Format = "DSSE Envelope",
Metadata = new Dictionary<string, object?>
{
["predicateType"] = "https://in-toto.io/attestation/v1",
["subject"] = "pkg:docker/alpine@3.18"
}
},
CanonicalHash = new CanonicalHashInfo
{
Algorithm = "SHA-256",
Value = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
Encoding = "hex"
},
Inputs = new InputStamps
{
FeedSnapshotHash = "feed123abc",
PolicyManifestHash = "policy456def",
SourceCodeHash = "git789ghi",
VexDocumentHashes = new[] { "vex123", "vex456" },
Custom = new Dictionary<string, string>
{
["baselineVersion"] = "1.0.0",
["environment"] = "production"
}
},
Toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[]
{
new ComponentInfo { Name = "StellaOps.Attestor", Version = "2.0.0", Hash = "hash123" },
new ComponentInfo { Name = "StellaOps.Signer", Version = "2.1.0" }
},
Compiler = new CompilerInfo
{
Name = "Roslyn",
Version = "4.8.0"
}
},
GeneratedAt = new DateTimeOffset(2025, 12, 23, 18, 0, 0, TimeSpan.Zero),
Reproducibility = new ReproducibilityMetadata
{
DeterministicSeed = 12345,
ClockFixed = true,
OrderingGuarantee = "stable",
NormalizationRules = new[] { "UTF-8", "LF line endings", "no trailing whitespace" }
},
Verification = new VerificationInfo
{
Command = "dotnet test --verify-determinism",
ExpectedHash = "abc123def456",
Baseline = "baselines/test-bundle.json"
},
Signatures = new[]
{
new SignatureInfo
{
Algorithm = "ES256",
KeyId = "signing-key-1",
Signature = "base64encodedSig==",
Timestamp = new DateTimeOffset(2025, 12, 23, 18, 0, 30, TimeSpan.Zero)
}
}
};
}
}

View File

@@ -0,0 +1,95 @@
using System;
using FluentAssertions;
using StellaOps.TestKit.Environment;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class EnvironmentSkewTests
{
[Fact]
public void EnvironmentProfile_Standard_HasCorrectDefaults()
{
var profile = EnvironmentProfile.Standard;
profile.Name.Should().Be("Standard");
profile.Cpu.Architecture.Should().Be(CpuArchitecture.X64);
profile.Network.Latency.Should().Be(TimeSpan.Zero);
profile.Runtime.Should().Be(ContainerRuntime.Docker);
}
[Fact]
public void EnvironmentProfile_HighLatency_Has100msLatency()
{
var profile = EnvironmentProfile.HighLatency;
profile.Name.Should().Be("HighLatency");
profile.Network.Latency.Should().Be(TimeSpan.FromMilliseconds(100));
}
[Fact]
public void EnvironmentProfile_LowBandwidth_Has10MbpsLimit()
{
var profile = EnvironmentProfile.LowBandwidth;
profile.Name.Should().Be("LowBandwidth");
profile.Network.BandwidthMbps.Should().Be(10);
}
[Fact]
public void EnvironmentProfile_PacketLoss_Has1PercentLoss()
{
var profile = EnvironmentProfile.PacketLoss;
profile.Name.Should().Be("PacketLoss");
profile.Network.PacketLossRate.Should().Be(0.01);
}
[Fact]
public void EnvironmentProfile_ArmCpu_HasArm64Architecture()
{
var profile = EnvironmentProfile.ArmCpu;
profile.Name.Should().Be("ArmCpu");
profile.Cpu.Architecture.Should().Be(CpuArchitecture.Arm64);
}
[Fact]
public void EnvironmentProfile_ResourceConstrained_HasLimits()
{
var profile = EnvironmentProfile.ResourceConstrained;
profile.Name.Should().Be("ResourceConstrained");
profile.ResourceLimits.MemoryMb.Should().Be(256);
profile.ResourceLimits.CpuCores.Should().Be(1);
}
[Fact]
public void EnvironmentProfile_All_ContainsExpectedProfiles()
{
var profiles = EnvironmentProfile.All;
profiles.Should().HaveCount(5);
profiles.Should().Contain(p => p.Name == "Standard");
profiles.Should().Contain(p => p.Name == "HighLatency");
profiles.Should().Contain(p => p.Name == "LowBandwidth");
profiles.Should().Contain(p => p.Name == "PacketLoss");
profiles.Should().Contain(p => p.Name == "ResourceConstrained");
}
[Fact]
public void NetworkProfile_RequiresNetworkShaping_ReturnsTrueWhenConfigured()
{
new NetworkProfile { Latency = TimeSpan.FromMilliseconds(50) }
.RequiresNetworkShaping.Should().BeTrue();
new NetworkProfile { PacketLossRate = 0.01 }
.RequiresNetworkShaping.Should().BeTrue();
new NetworkProfile { BandwidthMbps = 10 }
.RequiresNetworkShaping.Should().BeTrue();
new NetworkProfile()
.RequiresNetworkShaping.Should().BeFalse();
}
}

View File

@@ -0,0 +1,39 @@
using FluentAssertions;
using StellaOps.TestKit.Environment;
using Xunit;
using TestKitTestResult = StellaOps.TestKit.Environment.TestResult;
namespace StellaOps.TestKit.Tests;
public sealed partial class EnvironmentSkewTests
{
[Fact]
public async Task SkewReport_ToJson_ProducesValidJsonAsync()
{
var runner = new SkewTestRunner();
var report = await runner.RunAcrossProfiles(
test: () => Task.FromResult(new TestKitTestResult { Value = 1.0 }),
profiles: [EnvironmentProfile.Standard]);
var json = report.ToJson();
json.Should().Contain("\"generatedAt\"");
json.Should().Contain("\"profileCount\"");
json.Should().Contain("\"hasSkew\"");
}
[Fact]
public async Task SkewReport_ToMarkdown_ProducesValidMarkdownAsync()
{
var runner = new SkewTestRunner();
var report = await runner.RunAcrossProfiles(
test: () => Task.FromResult(new TestKitTestResult { Value = 1.0 }),
profiles: [EnvironmentProfile.Standard]);
var markdown = report.ToMarkdown();
markdown.Should().Contain("# Environment Skew Report");
markdown.Should().Contain("| Profile |");
markdown.Should().Contain("Standard");
}
}

View File

@@ -0,0 +1,27 @@
using FluentAssertions;
using StellaOps.TestKit.Environment;
using Xunit;
using TestKitTestResult = StellaOps.TestKit.Environment.TestResult;
namespace StellaOps.TestKit.Tests;
public sealed partial class EnvironmentSkewTests
{
[Fact]
public void TestResult_Defaults_AreCorrect()
{
var result = new TestKitTestResult();
result.Success.Should().BeTrue();
result.ProfileName.Should().BeEmpty();
result.Metadata.Should().BeEmpty();
}
[Fact]
public void SkewAssertException_SetsMessage()
{
var ex = new SkewAssertException("Test message");
ex.Message.Should().Be("Test message");
}
}

View File

@@ -0,0 +1,48 @@
using System.Collections.Generic;
using FluentAssertions;
using StellaOps.TestKit.Environment;
using Xunit;
using TestKitTestResult = StellaOps.TestKit.Environment.TestResult;
namespace StellaOps.TestKit.Tests;
public sealed partial class EnvironmentSkewTests
{
[Fact]
public async Task SkewTestRunner_AssertEquivalence_PassesWhenResultsAreEquivalentAsync()
{
var runner = new SkewTestRunner();
var report = await runner.RunAcrossProfiles(
test: () => Task.FromResult(new TestKitTestResult { Value = 100.0, DurationMs = 10 }),
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
act.Should().NotThrow();
}
[Fact]
public async Task SkewTestRunner_AssertEquivalence_FailsWhenSkewExceedsToleranceAsync()
{
var runner = new SkewTestRunner();
var values = new Queue<double>([100.0, 100.0, 100.0, 200.0, 200.0, 200.0]);
var report = await runner.RunAcrossProfiles(
test: () => Task.FromResult(new TestKitTestResult { Value = values.Dequeue(), DurationMs = 10 }),
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
act.Should().Throw<SkewAssertException>();
}
[Fact]
public async Task SkewTestRunner_AssertEquivalence_IgnoresSingleProfileAsync()
{
var runner = new SkewTestRunner();
var report = await runner.RunAcrossProfiles(
test: () => Task.FromResult(new TestKitTestResult { Value = 100.0, DurationMs = 10 }),
profiles: [EnvironmentProfile.Standard]);
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
act.Should().NotThrow();
}
}

View File

@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using StellaOps.TestKit.Environment;
using Xunit;
using TestKitTestResult = StellaOps.TestKit.Environment.TestResult;
namespace StellaOps.TestKit.Tests;
public sealed partial class EnvironmentSkewTests
{
[Fact]
public async Task SkewTestRunner_RunAcrossProfiles_ExecutesTestForEachProfileAsync()
{
var runner = new SkewTestRunner();
var executedProfiles = new List<string>();
var report = await runner.RunAcrossProfiles(
test: () =>
{
executedProfiles.Add("executed");
return Task.FromResult(new TestKitTestResult { Value = 1.0, DurationMs = 10 });
},
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
report.ProfileCount.Should().Be(2);
report.Results.Should().HaveCount(2);
executedProfiles.Should().HaveCount(report.Results.Sum(r => r.Results.Count));
}
[Fact]
public async Task SkewTestRunner_RunWithProfile_ExecutesMultipleIterationsAsync()
{
var runner = new SkewTestRunner();
var executionCount = 0;
var result = await runner.RunWithProfile(
test: () =>
{
executionCount++;
return Task.FromResult(new TestKitTestResult { Value = executionCount, DurationMs = 10 });
},
profile: EnvironmentProfile.Standard,
iterations: 5);
executionCount.Should().Be(5);
result.Results.Should().HaveCount(5);
}
[Fact]
public async Task SkewTestRunner_RunWithProfile_CalculatesAveragesAsync()
{
var runner = new SkewTestRunner();
var values = new[] { 10.0, 20.0, 30.0 };
var index = 0;
var result = await runner.RunWithProfile(
test: () => Task.FromResult(new TestKitTestResult
{
Value = values[index++],
DurationMs = 100
}),
profile: EnvironmentProfile.Standard,
iterations: 3);
result.AverageValue.Should().Be(20.0);
}
[Fact]
public async Task SkewTestRunner_RunWithProfile_HandlesErrorsAsync()
{
var runner = new SkewTestRunner();
var iteration = 0;
var result = await runner.RunWithProfile(
test: () =>
{
iteration++;
if (iteration == 2)
{
throw new InvalidOperationException("Test error");
}
return Task.FromResult(new TestKitTestResult { Value = 1.0, Success = true });
},
profile: EnvironmentProfile.Standard,
iterations: 3);
result.Results.Should().HaveCount(3);
result.SuccessRate.Should().BeApproximately(2.0 / 3.0, 0.01);
}
}

View File

@@ -1,327 +1,9 @@
using FluentAssertions;
using StellaOps.TestKit.Environment;
using StellaOps.TestKit;
using Xunit;
using TestKitTestResult = StellaOps.TestKit.Environment.TestResult;
namespace StellaOps.TestKit.Tests;
/// <summary>
/// Unit tests for environment skew testing infrastructure.
/// </summary>
[Trait("Category", TestCategories.Unit)]
public sealed class EnvironmentSkewTests
public sealed partial class EnvironmentSkewTests
{
#region EnvironmentProfile Tests
[Fact]
public void EnvironmentProfile_Standard_HasCorrectDefaults()
{
// Arrange & Act
var profile = EnvironmentProfile.Standard;
// Assert
profile.Name.Should().Be("Standard");
profile.Cpu.Architecture.Should().Be(CpuArchitecture.X64);
profile.Network.Latency.Should().Be(TimeSpan.Zero);
profile.Runtime.Should().Be(ContainerRuntime.Docker);
}
[Fact]
public void EnvironmentProfile_HighLatency_Has100msLatency()
{
// Arrange & Act
var profile = EnvironmentProfile.HighLatency;
// Assert
profile.Name.Should().Be("HighLatency");
profile.Network.Latency.Should().Be(TimeSpan.FromMilliseconds(100));
}
[Fact]
public void EnvironmentProfile_LowBandwidth_Has10MbpsLimit()
{
// Arrange & Act
var profile = EnvironmentProfile.LowBandwidth;
// Assert
profile.Name.Should().Be("LowBandwidth");
profile.Network.BandwidthMbps.Should().Be(10);
}
[Fact]
public void EnvironmentProfile_PacketLoss_Has1PercentLoss()
{
// Arrange & Act
var profile = EnvironmentProfile.PacketLoss;
// Assert
profile.Name.Should().Be("PacketLoss");
profile.Network.PacketLossRate.Should().Be(0.01);
}
[Fact]
public void EnvironmentProfile_ArmCpu_HasArm64Architecture()
{
// Arrange & Act
var profile = EnvironmentProfile.ArmCpu;
// Assert
profile.Name.Should().Be("ArmCpu");
profile.Cpu.Architecture.Should().Be(CpuArchitecture.Arm64);
}
[Fact]
public void EnvironmentProfile_ResourceConstrained_HasLimits()
{
// Arrange & Act
var profile = EnvironmentProfile.ResourceConstrained;
// Assert
profile.Name.Should().Be("ResourceConstrained");
profile.ResourceLimits.MemoryMb.Should().Be(256);
profile.ResourceLimits.CpuCores.Should().Be(1);
}
[Fact]
public void EnvironmentProfile_All_ContainsExpectedProfiles()
{
// Arrange & Act
var profiles = EnvironmentProfile.All;
// Assert
profiles.Should().HaveCount(5);
profiles.Should().Contain(p => p.Name == "Standard");
profiles.Should().Contain(p => p.Name == "HighLatency");
profiles.Should().Contain(p => p.Name == "LowBandwidth");
profiles.Should().Contain(p => p.Name == "PacketLoss");
profiles.Should().Contain(p => p.Name == "ResourceConstrained");
}
[Fact]
public void NetworkProfile_RequiresNetworkShaping_ReturnsTrueWhenConfigured()
{
// Arrange & Act & Assert
new NetworkProfile { Latency = TimeSpan.FromMilliseconds(50) }
.RequiresNetworkShaping.Should().BeTrue();
new NetworkProfile { PacketLossRate = 0.01 }
.RequiresNetworkShaping.Should().BeTrue();
new NetworkProfile { BandwidthMbps = 10 }
.RequiresNetworkShaping.Should().BeTrue();
new NetworkProfile()
.RequiresNetworkShaping.Should().BeFalse();
}
#endregion
#region SkewTestRunner Tests
[Fact]
public async Task SkewTestRunner_RunAcrossProfiles_ExecutesTestForEachProfile()
{
// Arrange
var runner = new SkewTestRunner();
var executedProfiles = new List<string>();
// Act
var report = await runner.RunAcrossProfiles(
test: () =>
{
executedProfiles.Add("executed");
return Task.FromResult(new TestKitTestResult { Value = 1.0, DurationMs = 10 });
},
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
// Assert
report.ProfileCount.Should().Be(2);
report.Results.Should().HaveCount(2);
}
[Fact]
public async Task SkewTestRunner_RunWithProfile_ExecutesMultipleIterations()
{
// Arrange
var runner = new SkewTestRunner();
var executionCount = 0;
// Act
var result = await runner.RunWithProfile(
test: () =>
{
executionCount++;
return Task.FromResult(new TestKitTestResult { Value = executionCount, DurationMs = 10 });
},
profile: EnvironmentProfile.Standard,
iterations: 5);
// Assert
executionCount.Should().Be(5);
result.Results.Should().HaveCount(5);
}
[Fact]
public async Task SkewTestRunner_RunWithProfile_CalculatesAverages()
{
// Arrange
var runner = new SkewTestRunner();
var values = new[] { 10.0, 20.0, 30.0 };
var index = 0;
// Act
var result = await runner.RunWithProfile(
test: () => Task.FromResult(new TestKitTestResult
{
Value = values[index++],
DurationMs = 100
}),
profile: EnvironmentProfile.Standard,
iterations: 3);
// Assert
result.AverageValue.Should().Be(20.0); // (10 + 20 + 30) / 3
}
[Fact]
public async Task SkewTestRunner_RunWithProfile_HandlesErrors()
{
// Arrange
var runner = new SkewTestRunner();
var iteration = 0;
// Act
var result = await runner.RunWithProfile(
test: () =>
{
iteration++;
if (iteration == 2)
{
throw new InvalidOperationException("Test error");
}
return Task.FromResult(new TestKitTestResult { Value = 1.0, Success = true });
},
profile: EnvironmentProfile.Standard,
iterations: 3);
// Assert
result.Results.Should().HaveCount(3);
result.SuccessRate.Should().BeApproximately(2.0 / 3.0, 0.01);
}
[Fact]
public async Task SkewTestRunner_AssertEquivalence_PassesWhenResultsAreEquivalent()
{
// Arrange
var runner = new SkewTestRunner();
var report = await runner.RunAcrossProfiles(
test: () => Task.FromResult(new TestKitTestResult { Value = 100.0, DurationMs = 10 }),
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
// Act & Assert
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
act.Should().NotThrow();
}
[Fact]
public async Task SkewTestRunner_AssertEquivalence_FailsWhenSkewExceedsTolerance()
{
// Arrange
var runner = new SkewTestRunner();
var values = new Queue<double>([100.0, 100.0, 100.0, 200.0, 200.0, 200.0]); // 100% difference
var report = await runner.RunAcrossProfiles(
test: () => Task.FromResult(new TestKitTestResult { Value = values.Dequeue(), DurationMs = 10 }),
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
// Act & Assert
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
act.Should().Throw<SkewAssertException>();
}
[Fact]
public async Task SkewTestRunner_AssertEquivalence_IgnoresSingleProfile()
{
// Arrange
var runner = new SkewTestRunner();
var report = await runner.RunAcrossProfiles(
test: () => Task.FromResult(new TestKitTestResult { Value = 100.0, DurationMs = 10 }),
profiles: [EnvironmentProfile.Standard]);
// Act & Assert - should not throw for single profile
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
act.Should().NotThrow();
}
#endregion
#region SkewReport Tests
[Fact]
public async Task SkewReport_ToJson_ProducesValidJson()
{
// Arrange
var runner = new SkewTestRunner();
var report = await runner.RunAcrossProfiles(
test: () => Task.FromResult(new TestKitTestResult { Value = 1.0 }),
profiles: [EnvironmentProfile.Standard]);
// Act
var json = report.ToJson();
// Assert
json.Should().Contain("\"generatedAt\"");
json.Should().Contain("\"profileCount\"");
json.Should().Contain("\"hasSkew\"");
}
[Fact]
public async Task SkewReport_ToMarkdown_ProducesValidMarkdown()
{
// Arrange
var runner = new SkewTestRunner();
var report = await runner.RunAcrossProfiles(
test: () => Task.FromResult(new TestKitTestResult { Value = 1.0 }),
profiles: [EnvironmentProfile.Standard]);
// Act
var markdown = report.ToMarkdown();
// Assert
markdown.Should().Contain("# Environment Skew Report");
markdown.Should().Contain("| Profile |");
markdown.Should().Contain("Standard");
}
#endregion
#region TestResult Tests
[Fact]
public void TestResult_Defaults_AreCorrect()
{
// Arrange & Act
var result = new TestKitTestResult();
// Assert
result.Success.Should().BeTrue();
result.ProfileName.Should().BeEmpty();
result.Metadata.Should().BeEmpty();
}
#endregion
#region SkewAssertException Tests
[Fact]
public void SkewAssertException_SetsMessage()
{
// Arrange & Act
var ex = new SkewAssertException("Test message");
// Assert
ex.Message.Should().Be("Test message");
}
#endregion
}

View File

@@ -0,0 +1,74 @@
using System;
using FluentAssertions;
using StellaOps.TestKit.Evidence;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class EvidenceChainTests
{
[Fact]
public void ArtifactHashStable_PassesWithCorrectHash()
{
var content = "test artifact";
var expectedHash = EvidenceChainAssert.ComputeSha256(content);
var act = () => EvidenceChainAssert.ArtifactHashStable(content, expectedHash);
act.Should().NotThrow();
}
[Fact]
public void ArtifactHashStable_ThrowsWithIncorrectHash()
{
var content = "test artifact";
var wrongHash = new string('0', 64);
var act = () => EvidenceChainAssert.ArtifactHashStable(content, wrongHash);
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*Artifact hash mismatch*");
}
[Fact]
public void ArtifactHashStable_ThrowsOnNullArtifact()
{
var act = () => EvidenceChainAssert.ArtifactHashStable((byte[])null!, "hash");
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void ArtifactImmutable_PassesWithDeterministicGenerator()
{
var counter = 0;
Func<string> generator = () =>
{
counter++;
return "immutable content";
};
var act = () => EvidenceChainAssert.ArtifactImmutable(generator, iterations: 5);
act.Should().NotThrow();
counter.Should().Be(5);
}
[Fact]
public void ArtifactImmutable_ThrowsWithNonDeterministicGenerator()
{
var counter = 0;
Func<string> generator = () =>
{
counter++;
return $"non-deterministic content {counter}";
};
var act = () => EvidenceChainAssert.ArtifactImmutable(generator, iterations: 5);
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*Artifact not immutable*");
}
[Fact]
public void ArtifactImmutable_ThrowsWithLessThanTwoIterations()
{
var act = () => EvidenceChainAssert.ArtifactImmutable(() => "content", iterations: 1);
act.Should().Throw<ArgumentOutOfRangeException>();
}
}

View File

@@ -0,0 +1,28 @@
using System;
using FluentAssertions;
using StellaOps.TestKit.Evidence;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class EvidenceChainTests
{
[Fact]
public void EvidenceTraceabilityException_ConstructorWithMessage_SetsMessage()
{
var ex = new EvidenceTraceabilityException("Test error");
ex.Message.Should().Be("Test error");
}
[Fact]
public void EvidenceTraceabilityException_ConstructorWithInnerException_SetsInnerException()
{
var inner = new InvalidOperationException("Inner");
var ex = new EvidenceTraceabilityException("Outer", inner);
ex.Message.Should().Be("Outer");
ex.InnerException.Should().Be(inner);
}
}

View File

@@ -0,0 +1,43 @@
using System.Text;
using FluentAssertions;
using StellaOps.TestKit.Evidence;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class EvidenceChainTests
{
[Fact]
public void ComputeSha256_ReturnsLowercaseHex()
{
var content = "test content";
var hash = EvidenceChainAssert.ComputeSha256(content);
hash.Should().NotBeNullOrEmpty();
hash.Should().MatchRegex("^[0-9a-f]{64}$");
}
[Fact]
public void ComputeSha256_IsDeterministic()
{
var content = "deterministic test";
var hash1 = EvidenceChainAssert.ComputeSha256(content);
var hash2 = EvidenceChainAssert.ComputeSha256(content);
hash1.Should().Be(hash2);
}
[Fact]
public void ComputeSha256_Bytes_MatchesStringVersion()
{
var content = "test content";
var bytes = Encoding.UTF8.GetBytes(content);
var hashFromString = EvidenceChainAssert.ComputeSha256(content);
var hashFromBytes = EvidenceChainAssert.ComputeSha256(bytes);
hashFromString.Should().Be(hashFromBytes);
}
}

View File

@@ -0,0 +1,56 @@
using FluentAssertions;
using StellaOps.TestKit.Evidence;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class EvidenceChainTests
{
[Fact]
public void EvidenceChainReporter_GenerateReport_ReturnsEmptyReportForNoAssemblies()
{
var reporter = new EvidenceChainReporter();
var report = reporter.GenerateReport();
report.TotalRequirements.Should().Be(0);
report.TotalTests.Should().Be(0);
report.AssembliesScanned.Should().BeEmpty();
}
[Fact]
public void EvidenceChainReporter_GenerateReport_ScansAssemblyForRequirements()
{
var reporter = new EvidenceChainReporter();
reporter.AddAssembly(typeof(EvidenceChainTests).Assembly);
var report = reporter.GenerateReport();
report.AssembliesScanned.Should().Contain("StellaOps.TestKit.Tests");
}
[Fact]
public void EvidenceChainReport_ToJson_ProducesValidJson()
{
var reporter = new EvidenceChainReporter();
var report = reporter.GenerateReport();
var json = report.ToJson();
json.Should().NotBeNullOrEmpty();
json.Should().Contain("\"totalRequirements\"");
json.Should().Contain("\"totalTests\"");
}
[Fact]
public void EvidenceChainReport_ToMarkdown_ProducesValidMarkdown()
{
var reporter = new EvidenceChainReporter();
var report = reporter.GenerateReport();
var markdown = report.ToMarkdown();
markdown.Should().Contain("# Evidence Chain Traceability Report");
markdown.Should().Contain("## Traceability Matrix");
}
}

View File

@@ -0,0 +1,66 @@
using System;
using FluentAssertions;
using StellaOps.TestKit.Evidence;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class EvidenceChainTests
{
[Fact]
public void RequirementAttribute_Constructor_SetsRequirementId()
{
var attr = new RequirementAttribute("REQ-TEST-001");
attr.RequirementId.Should().Be("REQ-TEST-001");
}
[Fact]
public void RequirementAttribute_Constructor_ThrowsOnNullRequirementId()
{
var act = () => new RequirementAttribute(null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void RequirementAttribute_OptionalProperties_DefaultToEmpty()
{
var attr = new RequirementAttribute("REQ-TEST-001");
attr.SprintTaskId.Should().BeEmpty();
attr.ComplianceControl.Should().BeEmpty();
attr.SourceDocument.Should().BeEmpty();
}
[Fact]
public void RequirementAttribute_GetTraits_ReturnsRequirementTrait()
{
var attr = new RequirementAttribute("REQ-TEST-001");
var traits = attr.GetTraits();
traits.Should().ContainSingle(t => t.Key == "Requirement" && t.Value == "REQ-TEST-001");
}
[Fact]
public void RequirementAttribute_GetTraits_IncludesSprintTaskWhenSet()
{
var attr = new RequirementAttribute("REQ-TEST-001") { SprintTaskId = "SPRINT-001" };
var traits = attr.GetTraits();
traits.Should().Contain(t => t.Key == "Requirement" && t.Value == "REQ-TEST-001");
traits.Should().Contain(t => t.Key == "SprintTask" && t.Value == "SPRINT-001");
}
[Fact]
public void RequirementAttribute_GetTraits_IncludesComplianceControlWhenSet()
{
var attr = new RequirementAttribute("REQ-TEST-001") { ComplianceControl = "SOC2-CC6.1" };
var traits = attr.GetTraits();
traits.Should().Contain(t => t.Key == "ComplianceControl" && t.Value == "SOC2-CC6.1");
}
}

View File

@@ -0,0 +1,83 @@
using FluentAssertions;
using StellaOps.TestKit.Evidence;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class EvidenceChainTests
{
[Fact]
public void RequirementLinked_PassesWithValidRequirementId()
{
var act = () => EvidenceChainAssert.RequirementLinked("REQ-TEST-001");
act.Should().NotThrow();
}
[Fact]
public void RequirementLinked_ThrowsWithEmptyRequirementId()
{
var act = () => EvidenceChainAssert.RequirementLinked("");
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*cannot be empty*");
}
[Fact]
public void RequirementLinked_ThrowsWithWhitespaceRequirementId()
{
var act = () => EvidenceChainAssert.RequirementLinked(" ");
act.Should().Throw<EvidenceTraceabilityException>();
}
[Fact]
public void TraceabilityComplete_PassesWithAllComponents()
{
var act = () => EvidenceChainAssert.TraceabilityComplete(
"REQ-001",
"MyTests.TestMethod",
"sha256:abc123");
act.Should().NotThrow();
}
[Fact]
public void TraceabilityComplete_ThrowsWithMissingRequirement()
{
var act = () => EvidenceChainAssert.TraceabilityComplete(
"",
"MyTests.TestMethod",
"sha256:abc123");
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*Requirement ID is missing*");
}
[Fact]
public void TraceabilityComplete_ThrowsWithMissingTestId()
{
var act = () => EvidenceChainAssert.TraceabilityComplete(
"REQ-001",
null!,
"sha256:abc123");
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*Test ID is missing*");
}
[Fact]
public void TraceabilityComplete_ThrowsWithMissingArtifactId()
{
var act = () => EvidenceChainAssert.TraceabilityComplete(
"REQ-001",
"MyTests.TestMethod",
" ");
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*Artifact ID is missing*");
}
[Fact]
public void TraceabilityComplete_ReportsAllMissingComponents()
{
var act = () => EvidenceChainAssert.TraceabilityComplete("", "", "");
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*Requirement ID is missing*")
.WithMessage("*Test ID is missing*")
.WithMessage("*Artifact ID is missing*");
}
}

View File

@@ -1,400 +1,9 @@
using System.Reflection;
using FluentAssertions;
using StellaOps.TestKit.Evidence;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.TestKit.Tests;
/// <summary>
/// Unit tests for evidence chain traceability infrastructure.
/// </summary>
[Trait("Category", TestCategories.Unit)]
public sealed class EvidenceChainTests
public sealed partial class EvidenceChainTests
{
#region RequirementAttribute Tests
[Fact]
public void RequirementAttribute_Constructor_SetsRequirementId()
{
// Arrange & Act
var attr = new RequirementAttribute("REQ-TEST-001");
// Assert
attr.RequirementId.Should().Be("REQ-TEST-001");
}
[Fact]
public void RequirementAttribute_Constructor_ThrowsOnNullRequirementId()
{
// Arrange & Act
var act = () => new RequirementAttribute(null!);
// Assert
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void RequirementAttribute_OptionalProperties_DefaultToEmpty()
{
// Arrange & Act
var attr = new RequirementAttribute("REQ-TEST-001");
// Assert
attr.SprintTaskId.Should().BeEmpty();
attr.ComplianceControl.Should().BeEmpty();
attr.SourceDocument.Should().BeEmpty();
}
[Fact]
public void RequirementAttribute_GetTraits_ReturnsRequirementTrait()
{
// Arrange
var attr = new RequirementAttribute("REQ-TEST-001");
// Act
var traits = attr.GetTraits();
// Assert
traits.Should().ContainSingle(t => t.Key == "Requirement" && t.Value == "REQ-TEST-001");
}
[Fact]
public void RequirementAttribute_GetTraits_IncludesSprintTaskWhenSet()
{
// Arrange
var attr = new RequirementAttribute("REQ-TEST-001") { SprintTaskId = "SPRINT-001" };
// Act
var traits = attr.GetTraits();
// Assert
traits.Should().Contain(t => t.Key == "Requirement" && t.Value == "REQ-TEST-001");
traits.Should().Contain(t => t.Key == "SprintTask" && t.Value == "SPRINT-001");
}
[Fact]
public void RequirementAttribute_GetTraits_IncludesComplianceControlWhenSet()
{
// Arrange
var attr = new RequirementAttribute("REQ-TEST-001") { ComplianceControl = "SOC2-CC6.1" };
// Act
var traits = attr.GetTraits();
// Assert
traits.Should().Contain(t => t.Key == "ComplianceControl" && t.Value == "SOC2-CC6.1");
}
#endregion
#region EvidenceChainAssert Tests
[Fact]
public void ComputeSha256_ReturnsLowercaseHex()
{
// Arrange
var content = "test content";
// Act
var hash = EvidenceChainAssert.ComputeSha256(content);
// Assert
hash.Should().NotBeNullOrEmpty();
hash.Should().MatchRegex("^[0-9a-f]{64}$");
}
[Fact]
public void ComputeSha256_IsDeterministic()
{
// Arrange
var content = "deterministic test";
// Act
var hash1 = EvidenceChainAssert.ComputeSha256(content);
var hash2 = EvidenceChainAssert.ComputeSha256(content);
// Assert
hash1.Should().Be(hash2);
}
[Fact]
public void ComputeSha256_Bytes_MatchesStringVersion()
{
// Arrange
var content = "test content";
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
// Act
var hashFromString = EvidenceChainAssert.ComputeSha256(content);
var hashFromBytes = EvidenceChainAssert.ComputeSha256(bytes);
// Assert
hashFromString.Should().Be(hashFromBytes);
}
[Fact]
public void ArtifactHashStable_PassesWithCorrectHash()
{
// Arrange
var content = "test artifact";
var expectedHash = EvidenceChainAssert.ComputeSha256(content);
// Act & Assert
var act = () => EvidenceChainAssert.ArtifactHashStable(content, expectedHash);
act.Should().NotThrow();
}
[Fact]
public void ArtifactHashStable_ThrowsWithIncorrectHash()
{
// Arrange
var content = "test artifact";
var wrongHash = new string('0', 64);
// Act & Assert
var act = () => EvidenceChainAssert.ArtifactHashStable(content, wrongHash);
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*Artifact hash mismatch*");
}
[Fact]
public void ArtifactHashStable_ThrowsOnNullArtifact()
{
// Arrange & Act & Assert
var act = () => EvidenceChainAssert.ArtifactHashStable((byte[])null!, "hash");
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void ArtifactImmutable_PassesWithDeterministicGenerator()
{
// Arrange
var counter = 0;
Func<string> generator = () =>
{
counter++;
return "immutable content";
};
// Act & Assert
var act = () => EvidenceChainAssert.ArtifactImmutable(generator, iterations: 5);
act.Should().NotThrow();
counter.Should().Be(5);
}
[Fact]
public void ArtifactImmutable_ThrowsWithNonDeterministicGenerator()
{
// Arrange
var counter = 0;
Func<string> generator = () =>
{
counter++;
return $"non-deterministic content {counter}";
};
// Act & Assert
var act = () => EvidenceChainAssert.ArtifactImmutable(generator, iterations: 5);
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*Artifact not immutable*");
}
[Fact]
public void ArtifactImmutable_ThrowsWithLessThanTwoIterations()
{
// Arrange & Act & Assert
var act = () => EvidenceChainAssert.ArtifactImmutable(() => "content", iterations: 1);
act.Should().Throw<ArgumentOutOfRangeException>();
}
[Fact]
public void RequirementLinked_PassesWithValidRequirementId()
{
// Arrange & Act & Assert
var act = () => EvidenceChainAssert.RequirementLinked("REQ-TEST-001");
act.Should().NotThrow();
}
[Fact]
public void RequirementLinked_ThrowsWithEmptyRequirementId()
{
// Arrange & Act & Assert
var act = () => EvidenceChainAssert.RequirementLinked("");
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*cannot be empty*");
}
[Fact]
public void RequirementLinked_ThrowsWithWhitespaceRequirementId()
{
// Arrange & Act & Assert
var act = () => EvidenceChainAssert.RequirementLinked(" ");
act.Should().Throw<EvidenceTraceabilityException>();
}
[Fact]
public void TraceabilityComplete_PassesWithAllComponents()
{
// Arrange & Act & Assert
var act = () => EvidenceChainAssert.TraceabilityComplete(
"REQ-001",
"MyTests.TestMethod",
"sha256:abc123");
act.Should().NotThrow();
}
[Fact]
public void TraceabilityComplete_ThrowsWithMissingRequirement()
{
// Arrange & Act & Assert
var act = () => EvidenceChainAssert.TraceabilityComplete(
"",
"MyTests.TestMethod",
"sha256:abc123");
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*Requirement ID is missing*");
}
[Fact]
public void TraceabilityComplete_ThrowsWithMissingTestId()
{
// Arrange & Act & Assert
var act = () => EvidenceChainAssert.TraceabilityComplete(
"REQ-001",
null!,
"sha256:abc123");
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*Test ID is missing*");
}
[Fact]
public void TraceabilityComplete_ThrowsWithMissingArtifactId()
{
// Arrange & Act & Assert
var act = () => EvidenceChainAssert.TraceabilityComplete(
"REQ-001",
"MyTests.TestMethod",
" ");
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*Artifact ID is missing*");
}
[Fact]
public void TraceabilityComplete_ReportsAllMissingComponents()
{
// Arrange & Act & Assert
var act = () => EvidenceChainAssert.TraceabilityComplete("", "", "");
act.Should().Throw<EvidenceTraceabilityException>()
.WithMessage("*Requirement ID is missing*")
.WithMessage("*Test ID is missing*")
.WithMessage("*Artifact ID is missing*");
}
#endregion
#region EvidenceChainReporter Tests
[Fact]
public void EvidenceChainReporter_GenerateReport_ReturnsEmptyReportForNoAssemblies()
{
// Arrange
var reporter = new EvidenceChainReporter();
// Act
var report = reporter.GenerateReport();
// Assert
report.TotalRequirements.Should().Be(0);
report.TotalTests.Should().Be(0);
report.AssembliesScanned.Should().BeEmpty();
}
[Fact]
public void EvidenceChainReporter_GenerateReport_ScansAssemblyForRequirements()
{
// Arrange
var reporter = new EvidenceChainReporter();
reporter.AddAssembly(typeof(EvidenceChainTests).Assembly);
// Act
var report = reporter.GenerateReport();
// Assert
report.AssembliesScanned.Should().Contain("StellaOps.TestKit.Tests");
}
[Fact]
public void EvidenceChainReport_ToJson_ProducesValidJson()
{
// Arrange
var reporter = new EvidenceChainReporter();
var report = reporter.GenerateReport();
// Act
var json = report.ToJson();
// Assert
json.Should().NotBeNullOrEmpty();
json.Should().Contain("\"totalRequirements\"");
json.Should().Contain("\"totalTests\"");
}
[Fact]
public void EvidenceChainReport_ToMarkdown_ProducesValidMarkdown()
{
// Arrange
var reporter = new EvidenceChainReporter();
var report = reporter.GenerateReport();
// Act
var markdown = report.ToMarkdown();
// Assert
markdown.Should().Contain("# Evidence Chain Traceability Report");
markdown.Should().Contain("## Traceability Matrix");
}
#endregion
#region EvidenceTraceabilityException Tests
[Fact]
public void EvidenceTraceabilityException_ConstructorWithMessage_SetsMessage()
{
// Arrange & Act
var ex = new EvidenceTraceabilityException("Test error");
// Assert
ex.Message.Should().Be("Test error");
}
[Fact]
public void EvidenceTraceabilityException_ConstructorWithInnerException_SetsInnerException()
{
// Arrange
var inner = new InvalidOperationException("Inner");
// Act
var ex = new EvidenceTraceabilityException("Outer", inner);
// Assert
ex.Message.Should().Be("Outer");
ex.InnerException.Should().Be(inner);
}
#endregion
}
/// <summary>
/// Test class with [Requirement] attribute for reporter testing.
/// </summary>
[Requirement("REQ-REPORTER-TEST-001")]
public sealed class RequirementTestFixture
{
[Fact]
[Requirement("REQ-REPORTER-TEST-002", SprintTaskId = "TEST-001")]
public void SampleTestWithRequirement()
{
// This test exists to verify the reporter can scan for [Requirement] attributes
}
}

View File

@@ -0,0 +1,43 @@
using System;
using StellaOps.TestKit.Incident;
namespace StellaOps.TestKit.Tests;
public sealed partial class IncidentTestGeneratorTests
{
private static readonly DateTimeOffset SampleOccurredAt =
new(2026, 1, 15, 10, 30, 0, TimeSpan.Zero);
private static readonly string _sampleManifestJson = """
{
"schemaVersion": "2.0",
"scan": {
"id": "scan-001",
"time": "2026-01-15T10:30:00Z",
"policyDigest": "sha256:abc123",
"scorePolicyDigest": "sha256:def456"
},
"reachability": {
"analysisId": "analysis-001",
"graphs": [
{
"kind": "static",
"hash": "sha256:graph123",
"analyzer": "java-callgraph"
}
],
"runtimeTraces": []
}
}
""";
private static readonly IncidentMetadata _sampleMetadata = new()
{
IncidentId = "INC-2026-001",
OccurredAt = SampleOccurredAt,
RootCause = "Race condition in concurrent writes",
AffectedModules = ["EvidenceLocker", "Policy"],
Severity = IncidentSeverity.P1,
Title = "Evidence bundle duplication"
};
}

View File

@@ -0,0 +1,85 @@
using System;
using FluentAssertions;
using StellaOps.TestKit.Incident;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class IncidentTestGeneratorTests
{
[Fact]
public void GenerateFromManifestJson_CreatesValidScaffold()
{
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
scaffold.Should().NotBeNull();
scaffold.Metadata.Should().Be(_sampleMetadata);
scaffold.TestClassName.Should().Contain("INC_2026_001");
scaffold.TestMethodName.Should().Contain("Validates");
scaffold.ReplayManifestHash.Should().StartWith("sha256:");
}
[Fact]
public void GenerateFromManifestJson_ExtractsInputFixtures()
{
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
scaffold.InputFixtures.Should().ContainKey("scan");
scaffold.InputFixtures.Should().ContainKey("reachabilityGraphs");
}
[Fact]
public void GenerateFromManifestJson_ExtractsExpectedOutputs()
{
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
scaffold.ExpectedOutputs.Should().ContainKey("policyDigest");
scaffold.ExpectedOutputs["policyDigest"].Should().Be("sha256:abc123");
}
[Fact]
public void GenerateFromManifestJson_GeneratesImplementationNotes()
{
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
scaffold.ImplementationNotes.Should().NotBeEmpty();
scaffold.ImplementationNotes.Should().Contain(n => n.Contains("INC-2026-001"));
scaffold.ImplementationNotes.Should().Contain(n => n.Contains("Race condition"));
}
[Fact]
public void GenerateFromManifestJson_SetsNamespaceFromModule()
{
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
scaffold.Namespace.Should().Contain("EvidenceLocker");
}
[Fact]
public void GenerateFromManifestJson_ThrowsOnNullManifest()
{
var generator = new IncidentTestGenerator();
var act = () => generator.GenerateFromManifestJson(null!, _sampleMetadata);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void GenerateFromManifestJson_ThrowsOnNullMetadata()
{
var generator = new IncidentTestGenerator();
var act = () => generator.GenerateFromManifestJson(_sampleManifestJson, null!);
act.Should().Throw<ArgumentNullException>();
}
}

View File

@@ -0,0 +1,54 @@
using FluentAssertions;
using StellaOps.TestKit.Incident;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class IncidentTestGeneratorTests
{
[Fact]
public void IncidentMetadata_RequiredProperties_AreSet()
{
var metadata = new IncidentMetadata
{
IncidentId = "INC-001",
OccurredAt = SampleOccurredAt,
RootCause = "Test cause",
AffectedModules = ["Module1"],
Severity = IncidentSeverity.P2
};
metadata.IncidentId.Should().Be("INC-001");
metadata.RootCause.Should().Be("Test cause");
metadata.Severity.Should().Be(IncidentSeverity.P2);
}
[Fact]
public void IncidentMetadata_OptionalProperties_HaveDefaults()
{
var metadata = new IncidentMetadata
{
IncidentId = "INC-001",
OccurredAt = SampleOccurredAt,
RootCause = "Test",
AffectedModules = ["Module1"],
Severity = IncidentSeverity.P3
};
metadata.Title.Should().BeEmpty();
metadata.ReportUrl.Should().BeEmpty();
metadata.ResolvedAt.Should().BeNull();
metadata.CorrelationIds.Should().BeEmpty();
metadata.FixTaskId.Should().BeEmpty();
metadata.Tags.Should().BeEmpty();
}
[Fact]
public void IncidentSeverity_P1_HasCorrectValue()
{
((int)IncidentSeverity.P1).Should().Be(1);
((int)IncidentSeverity.P2).Should().Be(2);
((int)IncidentSeverity.P3).Should().Be(3);
((int)IncidentSeverity.P4).Should().Be(4);
}
}

View File

@@ -0,0 +1,55 @@
using System;
using FluentAssertions;
using StellaOps.TestKit.Incident;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class IncidentTestGeneratorTests
{
[Fact]
public void RegisterIncidentTest_AddsToRegistry()
{
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
generator.RegisterIncidentTest("INC-2026-001", scaffold);
generator.RegisteredTests.Should().ContainKey("INC-2026-001");
generator.RegisteredTests["INC-2026-001"].Should().Be(scaffold);
}
[Fact]
public void RegisterIncidentTest_OverwritesExisting()
{
var generator = new IncidentTestGenerator();
var scaffold1 = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
var scaffold2 = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata with { Title = "Updated" });
generator.RegisterIncidentTest("INC-2026-001", scaffold1);
generator.RegisterIncidentTest("INC-2026-001", scaffold2);
generator.RegisteredTests["INC-2026-001"].Metadata.Title.Should().Be("Updated");
}
[Fact]
public void RegisterIncidentTest_ThrowsOnNullId()
{
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
var act = () => generator.RegisterIncidentTest(null!, scaffold);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void RegisterIncidentTest_ThrowsOnNullScaffold()
{
var generator = new IncidentTestGenerator();
var act = () => generator.RegisterIncidentTest("INC-2026-001", null!);
act.Should().Throw<ArgumentNullException>();
}
}

View File

@@ -0,0 +1,56 @@
using FluentAssertions;
using StellaOps.TestKit.Incident;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class IncidentTestGeneratorTests
{
[Fact]
public void GenerateReport_ReturnsEmptyForNoTests()
{
var generator = new IncidentTestGenerator();
var report = generator.GenerateReport();
report.TotalTests.Should().Be(0);
report.Tests.Should().BeEmpty();
}
[Fact]
public void GenerateReport_CountsBySeverity()
{
var generator = new IncidentTestGenerator();
var p1Metadata = _sampleMetadata with { IncidentId = "INC-001", Severity = IncidentSeverity.P1 };
var p2Metadata = _sampleMetadata with { IncidentId = "INC-002", Severity = IncidentSeverity.P2 };
generator.RegisterIncidentTest(
"INC-001",
generator.GenerateFromManifestJson(_sampleManifestJson, p1Metadata));
generator.RegisterIncidentTest(
"INC-002",
generator.GenerateFromManifestJson(_sampleManifestJson, p2Metadata));
var report = generator.GenerateReport();
report.TotalTests.Should().Be(2);
report.BySeveority.Should().ContainKey(IncidentSeverity.P1);
report.BySeveority.Should().ContainKey(IncidentSeverity.P2);
report.BySeveority[IncidentSeverity.P1].Should().Be(1);
report.BySeveority[IncidentSeverity.P2].Should().Be(1);
}
[Fact]
public void GenerateReport_CountsByModule()
{
var generator = new IncidentTestGenerator();
generator.RegisterIncidentTest(
"INC-001",
generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata));
var report = generator.GenerateReport();
report.ByModule.Should().ContainKey("EvidenceLocker");
report.ByModule.Should().ContainKey("Policy");
}
}

View File

@@ -0,0 +1,63 @@
using FluentAssertions;
using StellaOps.TestKit.Incident;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class IncidentTestGeneratorTests
{
[Fact]
public void TestScaffold_GenerateTestCode_ProducesValidCSharp()
{
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
var code = scaffold.GenerateTestCode();
code.Should().Contain("namespace StellaOps.EvidenceLocker.Tests.PostIncident");
code.Should().Contain($"public sealed class {scaffold.TestClassName}");
code.Should().Contain("[Fact]");
code.Should().Contain("[Trait(\"Category\", TestCategories.PostIncident)]");
code.Should().Contain($"[Trait(\"Incident\", \"{_sampleMetadata.IncidentId}\")]");
}
[Fact]
public void TestScaffold_GenerateTestCode_IncludesIncidentMetadata()
{
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
var code = scaffold.GenerateTestCode();
code.Should().Contain("INC-2026-001");
code.Should().Contain("Race condition in concurrent writes");
code.Should().Contain("IncidentSeverity.P1");
}
[Fact]
public void TestScaffold_ToJson_ProducesValidJson()
{
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
var json = scaffold.ToJson();
json.Should().Contain("\"incidentId\"");
json.Should().Contain("\"testClassName\"");
json.Should().Contain("\"inputFixtures\"");
}
[Fact]
public void TestScaffold_FromJson_DeserializesCorrectly()
{
var generator = new IncidentTestGenerator();
var original = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
var json = original.ToJson();
var deserialized = TestScaffold.FromJson(json);
deserialized.Should().NotBeNull();
deserialized!.Metadata.IncidentId.Should().Be(original.Metadata.IncidentId);
deserialized.TestClassName.Should().Be(original.TestClassName);
}
}

View File

@@ -1,360 +1,9 @@
using FluentAssertions;
using StellaOps.TestKit.Incident;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.TestKit.Tests;
/// <summary>
/// Unit tests for post-incident test generation infrastructure.
/// </summary>
[Trait("Category", TestCategories.Unit)]
public sealed class IncidentTestGeneratorTests
public sealed partial class IncidentTestGeneratorTests
{
private static readonly string SampleManifestJson = """
{
"schemaVersion": "2.0",
"scan": {
"id": "scan-001",
"time": "2026-01-15T10:30:00Z",
"policyDigest": "sha256:abc123",
"scorePolicyDigest": "sha256:def456"
},
"reachability": {
"analysisId": "analysis-001",
"graphs": [
{
"kind": "static",
"hash": "sha256:graph123",
"analyzer": "java-callgraph"
}
],
"runtimeTraces": []
}
}
""";
private static readonly IncidentMetadata SampleMetadata = new()
{
IncidentId = "INC-2026-001",
OccurredAt = DateTimeOffset.Parse("2026-01-15T10:30:00Z"),
RootCause = "Race condition in concurrent writes",
AffectedModules = ["EvidenceLocker", "Policy"],
Severity = IncidentSeverity.P1,
Title = "Evidence bundle duplication"
};
#region IncidentMetadata Tests
[Fact]
public void IncidentMetadata_RequiredProperties_AreSet()
{
// Arrange & Act
var metadata = new IncidentMetadata
{
IncidentId = "INC-001",
OccurredAt = DateTimeOffset.UtcNow,
RootCause = "Test cause",
AffectedModules = ["Module1"],
Severity = IncidentSeverity.P2
};
// Assert
metadata.IncidentId.Should().Be("INC-001");
metadata.RootCause.Should().Be("Test cause");
metadata.Severity.Should().Be(IncidentSeverity.P2);
}
[Fact]
public void IncidentMetadata_OptionalProperties_HaveDefaults()
{
// Arrange & Act
var metadata = new IncidentMetadata
{
IncidentId = "INC-001",
OccurredAt = DateTimeOffset.UtcNow,
RootCause = "Test",
AffectedModules = ["Module1"],
Severity = IncidentSeverity.P3
};
// Assert
metadata.Title.Should().BeEmpty();
metadata.ReportUrl.Should().BeEmpty();
metadata.ResolvedAt.Should().BeNull();
metadata.CorrelationIds.Should().BeEmpty();
metadata.FixTaskId.Should().BeEmpty();
metadata.Tags.Should().BeEmpty();
}
[Fact]
public void IncidentSeverity_P1_HasCorrectValue()
{
// Assert
((int)IncidentSeverity.P1).Should().Be(1);
((int)IncidentSeverity.P2).Should().Be(2);
((int)IncidentSeverity.P3).Should().Be(3);
((int)IncidentSeverity.P4).Should().Be(4);
}
#endregion
#region IncidentTestGenerator Tests
[Fact]
public void GenerateFromManifestJson_CreatesValidScaffold()
{
// Arrange
var generator = new IncidentTestGenerator();
// Act
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
// Assert
scaffold.Should().NotBeNull();
scaffold.Metadata.Should().Be(SampleMetadata);
scaffold.TestClassName.Should().Contain("INC_2026_001");
scaffold.TestMethodName.Should().Contain("Validates");
scaffold.ReplayManifestHash.Should().StartWith("sha256:");
}
[Fact]
public void GenerateFromManifestJson_ExtractsInputFixtures()
{
// Arrange
var generator = new IncidentTestGenerator();
// Act
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
// Assert
scaffold.InputFixtures.Should().ContainKey("scan");
scaffold.InputFixtures.Should().ContainKey("reachabilityGraphs");
}
[Fact]
public void GenerateFromManifestJson_ExtractsExpectedOutputs()
{
// Arrange
var generator = new IncidentTestGenerator();
// Act
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
// Assert
scaffold.ExpectedOutputs.Should().ContainKey("policyDigest");
scaffold.ExpectedOutputs["policyDigest"].Should().Be("sha256:abc123");
}
[Fact]
public void GenerateFromManifestJson_GeneratesImplementationNotes()
{
// Arrange
var generator = new IncidentTestGenerator();
// Act
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
// Assert
scaffold.ImplementationNotes.Should().NotBeEmpty();
scaffold.ImplementationNotes.Should().Contain(n => n.Contains("INC-2026-001"));
scaffold.ImplementationNotes.Should().Contain(n => n.Contains("Race condition"));
}
[Fact]
public void GenerateFromManifestJson_SetsNamespaceFromModule()
{
// Arrange
var generator = new IncidentTestGenerator();
// Act
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
// Assert
scaffold.Namespace.Should().Contain("EvidenceLocker");
}
[Fact]
public void GenerateFromManifestJson_ThrowsOnNullManifest()
{
// Arrange
var generator = new IncidentTestGenerator();
// Act & Assert
var act = () => generator.GenerateFromManifestJson(null!, SampleMetadata);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void GenerateFromManifestJson_ThrowsOnNullMetadata()
{
// Arrange
var generator = new IncidentTestGenerator();
// Act & Assert
var act = () => generator.GenerateFromManifestJson(SampleManifestJson, null!);
act.Should().Throw<ArgumentNullException>();
}
#endregion
#region RegisterIncidentTest Tests
[Fact]
public void RegisterIncidentTest_AddsToRegistry()
{
// Arrange
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
// Act
generator.RegisterIncidentTest("INC-2026-001", scaffold);
// Assert
generator.RegisteredTests.Should().ContainKey("INC-2026-001");
generator.RegisteredTests["INC-2026-001"].Should().Be(scaffold);
}
[Fact]
public void RegisterIncidentTest_OverwritesExisting()
{
// Arrange
var generator = new IncidentTestGenerator();
var scaffold1 = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
var scaffold2 = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata with { Title = "Updated" });
// Act
generator.RegisterIncidentTest("INC-2026-001", scaffold1);
generator.RegisterIncidentTest("INC-2026-001", scaffold2);
// Assert
generator.RegisteredTests["INC-2026-001"].Metadata.Title.Should().Be("Updated");
}
#endregion
#region GenerateReport Tests
[Fact]
public void GenerateReport_ReturnsEmptyForNoTests()
{
// Arrange
var generator = new IncidentTestGenerator();
// Act
var report = generator.GenerateReport();
// Assert
report.TotalTests.Should().Be(0);
report.Tests.Should().BeEmpty();
}
[Fact]
public void GenerateReport_CountsBySeverity()
{
// Arrange
var generator = new IncidentTestGenerator();
var p1Metadata = SampleMetadata with { IncidentId = "INC-001", Severity = IncidentSeverity.P1 };
var p2Metadata = SampleMetadata with { IncidentId = "INC-002", Severity = IncidentSeverity.P2 };
generator.RegisterIncidentTest("INC-001", generator.GenerateFromManifestJson(SampleManifestJson, p1Metadata));
generator.RegisterIncidentTest("INC-002", generator.GenerateFromManifestJson(SampleManifestJson, p2Metadata));
// Act
var report = generator.GenerateReport();
// Assert
report.TotalTests.Should().Be(2);
report.BySeveority.Should().ContainKey(IncidentSeverity.P1);
report.BySeveority.Should().ContainKey(IncidentSeverity.P2);
report.BySeveority[IncidentSeverity.P1].Should().Be(1);
report.BySeveority[IncidentSeverity.P2].Should().Be(1);
}
[Fact]
public void GenerateReport_CountsByModule()
{
// Arrange
var generator = new IncidentTestGenerator();
generator.RegisterIncidentTest("INC-001", generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata));
// Act
var report = generator.GenerateReport();
// Assert
report.ByModule.Should().ContainKey("EvidenceLocker");
report.ByModule.Should().ContainKey("Policy");
}
#endregion
#region TestScaffold Tests
[Fact]
public void TestScaffold_GenerateTestCode_ProducesValidCSharp()
{
// Arrange
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
// Act
var code = scaffold.GenerateTestCode();
// Assert
code.Should().Contain("namespace StellaOps.EvidenceLocker.Tests.PostIncident");
code.Should().Contain($"public sealed class {scaffold.TestClassName}");
code.Should().Contain("[Fact]");
code.Should().Contain("[Trait(\"Category\", TestCategories.PostIncident)]");
code.Should().Contain($"[Trait(\"Incident\", \"{SampleMetadata.IncidentId}\")]");
}
[Fact]
public void TestScaffold_GenerateTestCode_IncludesIncidentMetadata()
{
// Arrange
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
// Act
var code = scaffold.GenerateTestCode();
// Assert
code.Should().Contain("INC-2026-001");
code.Should().Contain("Race condition in concurrent writes");
code.Should().Contain("IncidentSeverity.P1");
}
[Fact]
public void TestScaffold_ToJson_ProducesValidJson()
{
// Arrange
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
// Act
var json = scaffold.ToJson();
// Assert
json.Should().Contain("\"incidentId\"");
json.Should().Contain("\"testClassName\"");
json.Should().Contain("\"inputFixtures\"");
}
[Fact]
public void TestScaffold_FromJson_DeserializesCorrectly()
{
// Arrange
var generator = new IncidentTestGenerator();
var original = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
var json = original.ToJson();
// Act
var deserialized = TestScaffold.FromJson(json);
// Assert
deserialized.Should().NotBeNull();
deserialized!.Metadata.IncidentId.Should().Be(original.Metadata.IncidentId);
deserialized.TestClassName.Should().Be(original.TestClassName);
}
#endregion
}

View File

@@ -0,0 +1,53 @@
using System;
using FluentAssertions;
using StellaOps.TestKit.Analysis;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class IntentCoverageReportTests
{
[Fact]
public void IntentCoverageReportGenerator_AddAssembly_ThrowsOnNull()
{
var generator = new IntentCoverageReportGenerator();
var act = () => generator.AddAssembly(null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void IntentCoverageReportGenerator_EmptyAssemblies_ReturnsEmptyReport()
{
var generator = new IntentCoverageReportGenerator();
var report = generator.Generate();
report.TotalTests.Should().Be(0);
report.TaggedTests.Should().Be(0);
report.UntaggedTests.Should().Be(0);
report.TagCoveragePercent.Should().Be(0);
report.ModuleStats.Should().BeEmpty();
}
[Fact]
public void IntentCoverageReportGenerator_ScansSelfAssembly()
{
var generator = new IntentCoverageReportGenerator();
generator.AddAssembly(typeof(IntentCoverageReportTests).Assembly);
var report = generator.Generate();
report.TotalTests.Should().BeGreaterThan(0);
}
[Fact]
public void IntentCoverageReport_GeneratesWarning_WhenSafetyMissing()
{
var generator = new IntentCoverageReportGenerator();
var report = generator.Generate();
report.Warnings.Should().Contain("No tests tagged with Safety intent");
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
using FluentAssertions;
using StellaOps.TestKit.Traits;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class IntentCoverageReportTests
{
[Fact]
public void IntentAttribute_CreatesTraits()
{
var attr = new IntentAttribute(TestIntents.Safety, "Security requirement");
attr.Intent.Should().Be(TestIntents.Safety);
attr.Rationale.Should().Be("Security requirement");
var traits = attr.GetTraits();
traits.Should().Contain(new KeyValuePair<string, string>("Intent", "Safety"));
traits.Should().Contain(new KeyValuePair<string, string>("IntentRationale", "Security requirement"));
}
[Fact]
public void IntentAttribute_WithoutRationale_OnlyIntentTrait()
{
var attr = new IntentAttribute(TestIntents.Operational);
attr.Intent.Should().Be(TestIntents.Operational);
attr.Rationale.Should().BeEmpty();
var traits = attr.GetTraits();
traits.Should().ContainSingle();
traits.Should().Contain(new KeyValuePair<string, string>("Intent", "Operational"));
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using StellaOps.TestKit.Analysis;
using StellaOps.TestKit.Traits;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class IntentCoverageReportTests
{
[Fact]
public void IntentCoverageReport_ToMarkdown_GeneratesValidOutput()
{
var report = new IntentCoverageReport
{
GeneratedAt = new DateTimeOffset(2026, 1, 27, 12, 0, 0, TimeSpan.Zero),
TotalTests = 100,
TaggedTests = 60,
UntaggedTests = 40,
TagCoveragePercent = 60.0,
IntentDistribution = new Dictionary<string, int>
{
[TestIntents.Safety] = 20,
[TestIntents.Regulatory] = 15,
[TestIntents.Operational] = 25,
[TestIntents.Performance] = 0,
[TestIntents.Competitive] = 0
},
ModuleStats = new Dictionary<string, ModuleIntentStatsReadOnly>
{
["Policy"] = new ModuleIntentStatsReadOnly
{
ModuleName = "Policy",
TotalTests = 50,
TaggedTests = 30,
TestsWithRationale = 10,
TagCoveragePercent = 60.0,
IntentCounts = new Dictionary<string, int>
{
[TestIntents.Safety] = 15,
[TestIntents.Regulatory] = 15
}
}
},
Warnings = new List<string>
{
"Low intent coverage: only 60.0% of tests have intent tags"
}
};
var markdown = report.ToMarkdown();
markdown.Should().Contain("# Intent Coverage Report");
markdown.Should().Contain("Total tests: 100");
markdown.Should().Contain("Tagged: 60 (60.0%)");
markdown.Should().Contain("## Intent Distribution");
markdown.Should().Contain("| Safety |");
markdown.Should().Contain("## Per-Module Coverage");
markdown.Should().Contain("| Policy |");
markdown.Should().Contain("## Warnings");
markdown.Should().Contain("Low intent coverage");
}
}

View File

@@ -0,0 +1,38 @@
using FluentAssertions;
using StellaOps.TestKit.Traits;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class IntentCoverageReportTests
{
[Fact]
public void TestIntents_All_ContainsAllCategories()
{
TestIntents.All.Should().BeEquivalentTo(new[]
{
TestIntents.Regulatory,
TestIntents.Safety,
TestIntents.Performance,
TestIntents.Competitive,
TestIntents.Operational
});
}
[Fact]
public void TestIntents_IsValid_ValidatesKnownIntents()
{
TestIntents.IsValid("Regulatory").Should().BeTrue();
TestIntents.IsValid("Safety").Should().BeTrue();
TestIntents.IsValid("Performance").Should().BeTrue();
TestIntents.IsValid("Competitive").Should().BeTrue();
TestIntents.IsValid("Operational").Should().BeTrue();
TestIntents.IsValid("regulatory").Should().BeTrue();
TestIntents.IsValid("SAFETY").Should().BeTrue();
TestIntents.IsValid("Unknown").Should().BeFalse();
TestIntents.IsValid("").Should().BeFalse();
TestIntents.IsValid(" ").Should().BeFalse();
}
}

View File

@@ -1,159 +1,9 @@
using FluentAssertions;
using StellaOps.TestKit.Analysis;
using StellaOps.TestKit.Traits;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.TestKit.Tests;
/// <summary>
/// Unit tests for <see cref="IntentCoverageReportGenerator"/> and <see cref="IntentCoverageReport"/>.
/// </summary>
[Trait("Category", TestCategories.Unit)]
public sealed class IntentCoverageReportTests
public sealed partial class IntentCoverageReportTests
{
[Fact]
public void TestIntents_All_ContainsAllCategories()
{
TestIntents.All.Should().BeEquivalentTo(new[]
{
TestIntents.Regulatory,
TestIntents.Safety,
TestIntents.Performance,
TestIntents.Competitive,
TestIntents.Operational
});
}
[Fact]
public void TestIntents_IsValid_ValidatesKnownIntents()
{
TestIntents.IsValid("Regulatory").Should().BeTrue();
TestIntents.IsValid("Safety").Should().BeTrue();
TestIntents.IsValid("Performance").Should().BeTrue();
TestIntents.IsValid("Competitive").Should().BeTrue();
TestIntents.IsValid("Operational").Should().BeTrue();
// Case insensitive
TestIntents.IsValid("regulatory").Should().BeTrue();
TestIntents.IsValid("SAFETY").Should().BeTrue();
// Invalid
TestIntents.IsValid("Unknown").Should().BeFalse();
TestIntents.IsValid("").Should().BeFalse();
}
[Fact]
public void IntentAttribute_CreatesTraits()
{
var attr = new IntentAttribute(TestIntents.Safety, "Security requirement");
attr.Intent.Should().Be(TestIntents.Safety);
attr.Rationale.Should().Be("Security requirement");
var traits = attr.GetTraits();
traits.Should().Contain(new KeyValuePair<string, string>("Intent", "Safety"));
traits.Should().Contain(new KeyValuePair<string, string>("IntentRationale", "Security requirement"));
}
[Fact]
public void IntentAttribute_WithoutRationale_OnlyIntentTrait()
{
var attr = new IntentAttribute(TestIntents.Operational);
attr.Intent.Should().Be(TestIntents.Operational);
attr.Rationale.Should().BeEmpty();
var traits = attr.GetTraits();
traits.Should().ContainSingle();
traits.Should().Contain(new KeyValuePair<string, string>("Intent", "Operational"));
}
[Fact]
public void IntentCoverageReportGenerator_EmptyAssemblies_ReturnsEmptyReport()
{
var generator = new IntentCoverageReportGenerator();
var report = generator.Generate();
report.TotalTests.Should().Be(0);
report.TaggedTests.Should().Be(0);
report.UntaggedTests.Should().Be(0);
report.TagCoveragePercent.Should().Be(0);
report.ModuleStats.Should().BeEmpty();
}
[Fact]
public void IntentCoverageReportGenerator_ScansSelfAssembly()
{
var generator = new IntentCoverageReportGenerator();
generator.AddAssembly(typeof(IntentCoverageReportTests).Assembly);
var report = generator.Generate();
// This test class has tests, so we should find something
report.TotalTests.Should().BeGreaterThan(0);
}
[Fact]
public void IntentCoverageReport_ToMarkdown_GeneratesValidOutput()
{
var report = new IntentCoverageReport
{
GeneratedAt = new DateTimeOffset(2026, 1, 27, 12, 0, 0, TimeSpan.Zero),
TotalTests = 100,
TaggedTests = 60,
UntaggedTests = 40,
TagCoveragePercent = 60.0,
IntentDistribution = new Dictionary<string, int>
{
[TestIntents.Safety] = 20,
[TestIntents.Regulatory] = 15,
[TestIntents.Operational] = 25,
[TestIntents.Performance] = 0,
[TestIntents.Competitive] = 0
},
ModuleStats = new Dictionary<string, ModuleIntentStatsReadOnly>
{
["Policy"] = new ModuleIntentStatsReadOnly
{
ModuleName = "Policy",
TotalTests = 50,
TaggedTests = 30,
TestsWithRationale = 10,
TagCoveragePercent = 60.0,
IntentCounts = new Dictionary<string, int>
{
[TestIntents.Safety] = 15,
[TestIntents.Regulatory] = 15
}
}
},
Warnings = new List<string>
{
"Low intent coverage: only 60.0% of tests have intent tags"
}
};
var markdown = report.ToMarkdown();
markdown.Should().Contain("# Intent Coverage Report");
markdown.Should().Contain("Total tests: 100");
markdown.Should().Contain("Tagged: 60 (60.0%)");
markdown.Should().Contain("## Intent Distribution");
markdown.Should().Contain("| Safety |");
markdown.Should().Contain("## Per-Module Coverage");
markdown.Should().Contain("| Policy |");
markdown.Should().Contain("## Warnings");
markdown.Should().Contain("Low intent coverage");
}
[Fact]
public void IntentCoverageReport_GeneratesWarning_WhenSafetyMissing()
{
var generator = new IntentCoverageReportGenerator();
// Empty assemblies means no Safety tests
var report = generator.Generate();
report.Warnings.Should().Contain("No tests tagged with Safety intent");
}
}

View File

@@ -0,0 +1,37 @@
using FluentAssertions;
using StellaOps.TestKit.Interop;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class InteropTests
{
[Fact]
public void CompatibilityReport_ToMarkdown_ProducesValidMarkdown()
{
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
matrix.AddVersion("2.0", new SchemaDefinition { RequiredFields = ["id"] });
var report = matrix.Analyze();
var markdown = report.ToMarkdown();
markdown.Should().Contain("# Schema Compatibility Report");
markdown.Should().Contain("| From | To |");
markdown.Should().Contain("1.0");
markdown.Should().Contain("2.0");
}
[Fact]
public void CompatibilityReport_ToJson_ProducesValidJson()
{
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
var report = matrix.Analyze();
var json = report.ToJson();
json.Should().Contain("\"generatedAt\"");
json.Should().Contain("\"versions\"");
}
}

View File

@@ -0,0 +1,29 @@
using FluentAssertions;
using StellaOps.TestKit.Interop;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class InteropTests
{
[Fact]
public void ServiceEndpoint_DefaultValues_AreSet()
{
var endpoint = new ServiceEndpoint();
endpoint.ServiceName.Should().BeEmpty();
endpoint.Version.Should().BeEmpty();
endpoint.BaseUrl.Should().BeEmpty();
endpoint.IsHealthy.Should().BeFalse();
}
[Fact]
public void CompatibilityResult_DefaultValues_AreSet()
{
var result = new CompatibilityResult();
result.IsSuccess.Should().BeFalse();
result.Errors.Should().BeEmpty();
result.Warnings.Should().BeEmpty();
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Linq;
using FluentAssertions;
using StellaOps.TestKit.Interop;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class InteropTests
{
[Fact]
public void SchemaVersionMatrix_Analyze_GeneratesReport()
{
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
matrix.AddVersion("2.0", new SchemaDefinition { RequiredFields = ["id", "name"] });
var report = matrix.Analyze();
report.Versions.Should().Contain(["1.0", "2.0"]);
report.Pairs.Should().HaveCount(2);
report.GeneratedAt.Should().NotBe(default);
report.GeneratedAt.Offset.Should().Be(TimeSpan.Zero);
}
[Fact]
public void SchemaVersionMatrix_Analyze_SortsVersions()
{
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("2.0", new SchemaDefinition { RequiredFields = ["id"] });
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
var report = matrix.Analyze();
report.Versions.Should().ContainInOrder("1.0", "2.0");
}
[Fact]
public void SchemaVersionMatrix_Analyze_DetectsTypeChanges()
{
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition
{
RequiredFields = ["id"],
FieldTypes = new() { ["id"] = "int" }
});
matrix.AddVersion("2.0", new SchemaDefinition
{
RequiredFields = ["id"],
FieldTypes = new() { ["id"] = "string" }
});
var report = matrix.Analyze();
var pair = report.Pairs.First(p => p.FromVersion == "1.0" && p.ToVersion == "2.0");
pair.IsBackwardCompatible.Should().BeFalse();
pair.BackwardIssues.Should().Contain(i => i.Contains("Type changed"));
}
}

View File

@@ -0,0 +1,89 @@
using FluentAssertions;
using StellaOps.TestKit.Interop;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class InteropTests
{
[Fact]
public void SchemaVersionMatrix_AddVersion_StoresSchema()
{
var matrix = new SchemaVersionMatrix();
var schema = new SchemaDefinition
{
RequiredFields = ["id", "name"]
};
matrix.AddVersion("1.0", schema);
matrix.Versions.Should().Contain("1.0");
matrix.GetVersion("1.0").Should().Be(schema);
}
[Fact]
public void SchemaVersionMatrix_IsBackwardCompatible_ReturnsTrueWhenNoFieldsRemoved()
{
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition
{
RequiredFields = ["id", "name"]
});
matrix.AddVersion("2.0", new SchemaDefinition
{
RequiredFields = ["id", "name", "type"],
OptionalFields = ["description"]
});
matrix.IsBackwardCompatible("1.0", "2.0").Should().BeTrue();
}
[Fact]
public void SchemaVersionMatrix_IsBackwardCompatible_ReturnsFalseWhenFieldsRemoved()
{
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition
{
RequiredFields = ["id", "name", "oldField"]
});
matrix.AddVersion("2.0", new SchemaDefinition
{
RequiredFields = ["id", "name"]
});
matrix.IsBackwardCompatible("1.0", "2.0").Should().BeFalse();
}
[Fact]
public void SchemaVersionMatrix_IsForwardCompatible_ReturnsTrueWhenNewFieldsHaveDefaults()
{
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition
{
RequiredFields = ["id", "name"]
});
matrix.AddVersion("2.0", new SchemaDefinition
{
RequiredFields = ["id", "name", "type"],
FieldDefaults = new() { ["type"] = "default" }
});
matrix.IsForwardCompatible("1.0", "2.0").Should().BeTrue();
}
[Fact]
public void SchemaVersionMatrix_IsForwardCompatible_ReturnsFalseWhenNewRequiredFieldsHaveNoDefaults()
{
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition
{
RequiredFields = ["id", "name"]
});
matrix.AddVersion("2.0", new SchemaDefinition
{
RequiredFields = ["id", "name", "type"]
});
matrix.IsForwardCompatible("1.0", "2.0").Should().BeFalse();
}
}

View File

@@ -0,0 +1,57 @@
using FluentAssertions;
using StellaOps.TestKit.Interop;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class InteropTests
{
[Fact]
public async Task VersionCompatibilityFixture_TestHandshake_ReturnsSuccessAsync()
{
var fixture = new VersionCompatibilityFixture();
await fixture.InitializeAsync();
var server = await fixture.StartVersion("1.0", "Service");
var result = await fixture.TestHandshake(fixture.CurrentEndpoint!, server);
result.IsSuccess.Should().BeTrue();
result.ClientVersion.Should().Be(fixture.CurrentEndpoint!.Version);
result.ServerVersion.Should().Be("1.0");
await fixture.DisposeAsync();
}
[Fact]
public async Task VersionCompatibilityFixture_TestMessageFormat_ReturnsSuccessAsync()
{
var fixture = new VersionCompatibilityFixture();
await fixture.InitializeAsync();
var producer = await fixture.StartVersion("1.0", "Producer");
var consumer = await fixture.StartVersion("2.0", "Consumer");
var result = await fixture.TestMessageFormat(producer, consumer, "EvidenceBundle");
result.IsSuccess.Should().BeTrue();
result.Message.Should().Contain("EvidenceBundle");
await fixture.DisposeAsync();
}
[Fact]
public async Task VersionCompatibilityFixture_TestSchemaMigration_ReturnsSuccessAsync()
{
var fixture = new VersionCompatibilityFixture();
await fixture.InitializeAsync();
var result = await fixture.TestSchemaMigration("1.0", "2.0", new { id = 1 });
result.IsSuccess.Should().BeTrue();
result.FromVersion.Should().Be("1.0");
result.ToVersion.Should().Be("2.0");
result.DataPreserved.Should().BeTrue();
result.RollbackSupported.Should().BeTrue();
await fixture.DisposeAsync();
}
}

View File

@@ -0,0 +1,69 @@
using FluentAssertions;
using StellaOps.TestKit.Interop;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class InteropTests
{
[Fact]
public async Task VersionCompatibilityFixture_Initialize_CreatesCurrentEndpointAsync()
{
var fixture = new VersionCompatibilityFixture
{
Config = new VersionCompatibilityConfig { CurrentVersion = "3.0" }
};
await fixture.InitializeAsync();
fixture.CurrentEndpoint.Should().NotBeNull();
fixture.CurrentEndpoint!.Version.Should().Be("3.0");
fixture.CurrentEndpoint.IsHealthy.Should().BeTrue();
await fixture.DisposeAsync();
}
[Fact]
public async Task VersionCompatibilityFixture_StartVersion_CreatesEndpointAsync()
{
var fixture = new VersionCompatibilityFixture();
await fixture.InitializeAsync();
var endpoint = await fixture.StartVersion("1.0", "EvidenceLocker");
endpoint.Should().NotBeNull();
endpoint.Version.Should().Be("1.0");
endpoint.ServiceName.Should().Be("EvidenceLocker");
await fixture.DisposeAsync();
}
[Fact]
public async Task VersionCompatibilityFixture_StartVersion_ReturnsSameEndpointForSameVersionAsync()
{
var fixture = new VersionCompatibilityFixture();
await fixture.InitializeAsync();
var endpoint1 = await fixture.StartVersion("1.0", "Service");
var endpoint2 = await fixture.StartVersion("1.0", "Service");
endpoint1.Should().BeSameAs(endpoint2);
await fixture.DisposeAsync();
}
[Fact]
public async Task VersionCompatibilityFixture_StopVersion_RemovesEndpointAsync()
{
var fixture = new VersionCompatibilityFixture();
await fixture.InitializeAsync();
await fixture.StartVersion("1.0", "Service");
await fixture.StopVersion("1.0", "Service");
var newEndpoint = await fixture.StartVersion("1.0", "Service");
newEndpoint.Should().NotBeNull();
await fixture.DisposeAsync();
}
}

View File

@@ -1,360 +1,9 @@
using FluentAssertions;
using StellaOps.TestKit.Interop;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.TestKit.Tests;
/// <summary>
/// Unit tests for cross-version interoperability testing infrastructure.
/// </summary>
[Trait("Category", TestCategories.Unit)]
public sealed class InteropTests
public sealed partial class InteropTests
{
#region SchemaVersionMatrix Tests
[Fact]
public void SchemaVersionMatrix_AddVersion_StoresSchema()
{
// Arrange
var matrix = new SchemaVersionMatrix();
var schema = new SchemaDefinition
{
RequiredFields = ["id", "name"]
};
// Act
matrix.AddVersion("1.0", schema);
// Assert
matrix.Versions.Should().Contain("1.0");
matrix.GetVersion("1.0").Should().Be(schema);
}
[Fact]
public void SchemaVersionMatrix_IsBackwardCompatible_ReturnsTrueWhenNoFieldsRemoved()
{
// Arrange
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition
{
RequiredFields = ["id", "name"]
});
matrix.AddVersion("2.0", new SchemaDefinition
{
RequiredFields = ["id", "name", "type"], // Added field, none removed
OptionalFields = ["description"]
});
// Act & Assert
matrix.IsBackwardCompatible("1.0", "2.0").Should().BeTrue();
}
[Fact]
public void SchemaVersionMatrix_IsBackwardCompatible_ReturnsFalseWhenFieldsRemoved()
{
// Arrange
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition
{
RequiredFields = ["id", "name", "oldField"]
});
matrix.AddVersion("2.0", new SchemaDefinition
{
RequiredFields = ["id", "name"] // oldField removed
});
// Act & Assert
matrix.IsBackwardCompatible("1.0", "2.0").Should().BeFalse();
}
[Fact]
public void SchemaVersionMatrix_IsForwardCompatible_ReturnsTrueWhenNewFieldsHaveDefaults()
{
// Arrange
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition
{
RequiredFields = ["id", "name"]
});
matrix.AddVersion("2.0", new SchemaDefinition
{
RequiredFields = ["id", "name", "type"],
FieldDefaults = new() { ["type"] = "default" }
});
// Act & Assert
matrix.IsForwardCompatible("1.0", "2.0").Should().BeTrue();
}
[Fact]
public void SchemaVersionMatrix_IsForwardCompatible_ReturnsFalseWhenNewRequiredFieldsHaveNoDefaults()
{
// Arrange
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition
{
RequiredFields = ["id", "name"]
});
matrix.AddVersion("2.0", new SchemaDefinition
{
RequiredFields = ["id", "name", "type"] // No default for "type"
});
// Act & Assert
matrix.IsForwardCompatible("1.0", "2.0").Should().BeFalse();
}
[Fact]
public void SchemaVersionMatrix_Analyze_GeneratesReport()
{
// Arrange
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
matrix.AddVersion("2.0", new SchemaDefinition { RequiredFields = ["id", "name"] });
// Act
var report = matrix.Analyze();
// Assert
report.Versions.Should().Contain(["1.0", "2.0"]);
report.Pairs.Should().HaveCount(2); // 1.0->2.0 and 2.0->1.0
report.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
}
[Fact]
public void SchemaVersionMatrix_Analyze_DetectsTypeChanges()
{
// Arrange
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition
{
RequiredFields = ["id"],
FieldTypes = new() { ["id"] = "int" }
});
matrix.AddVersion("2.0", new SchemaDefinition
{
RequiredFields = ["id"],
FieldTypes = new() { ["id"] = "string" } // Type changed
});
// Act
var report = matrix.Analyze();
// Assert
var pair = report.Pairs.First(p => p.FromVersion == "1.0" && p.ToVersion == "2.0");
pair.IsBackwardCompatible.Should().BeFalse();
pair.BackwardIssues.Should().Contain(i => i.Contains("Type changed"));
}
[Fact]
public void CompatibilityReport_ToMarkdown_ProducesValidMarkdown()
{
// Arrange
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
matrix.AddVersion("2.0", new SchemaDefinition { RequiredFields = ["id"] });
var report = matrix.Analyze();
// Act
var markdown = report.ToMarkdown();
// Assert
markdown.Should().Contain("# Schema Compatibility Report");
markdown.Should().Contain("| From | To |");
markdown.Should().Contain("1.0");
markdown.Should().Contain("2.0");
}
[Fact]
public void CompatibilityReport_ToJson_ProducesValidJson()
{
// Arrange
var matrix = new SchemaVersionMatrix();
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
var report = matrix.Analyze();
// Act
var json = report.ToJson();
// Assert
json.Should().Contain("\"generatedAt\"");
json.Should().Contain("\"versions\"");
}
#endregion
#region VersionCompatibilityFixture Tests
[Fact]
public async Task VersionCompatibilityFixture_Initialize_CreatesCurrentEndpoint()
{
// Arrange
var fixture = new VersionCompatibilityFixture
{
Config = new VersionCompatibilityConfig { CurrentVersion = "3.0" }
};
// Act
await fixture.InitializeAsync();
// Assert
fixture.CurrentEndpoint.Should().NotBeNull();
fixture.CurrentEndpoint!.Version.Should().Be("3.0");
fixture.CurrentEndpoint.IsHealthy.Should().BeTrue();
// Cleanup
await fixture.DisposeAsync();
}
[Fact]
public async Task VersionCompatibilityFixture_StartVersion_CreatesEndpoint()
{
// Arrange
var fixture = new VersionCompatibilityFixture();
await fixture.InitializeAsync();
// Act
var endpoint = await fixture.StartVersion("1.0", "EvidenceLocker");
// Assert
endpoint.Should().NotBeNull();
endpoint.Version.Should().Be("1.0");
endpoint.ServiceName.Should().Be("EvidenceLocker");
// Cleanup
await fixture.DisposeAsync();
}
[Fact]
public async Task VersionCompatibilityFixture_StartVersion_ReturnsSameEndpointForSameVersion()
{
// Arrange
var fixture = new VersionCompatibilityFixture();
await fixture.InitializeAsync();
// Act
var endpoint1 = await fixture.StartVersion("1.0", "Service");
var endpoint2 = await fixture.StartVersion("1.0", "Service");
// Assert
endpoint1.Should().BeSameAs(endpoint2);
// Cleanup
await fixture.DisposeAsync();
}
[Fact]
public async Task VersionCompatibilityFixture_TestHandshake_ReturnsSuccess()
{
// Arrange
var fixture = new VersionCompatibilityFixture();
await fixture.InitializeAsync();
var server = await fixture.StartVersion("1.0", "Service");
// Act
var result = await fixture.TestHandshake(fixture.CurrentEndpoint!, server);
// Assert
result.IsSuccess.Should().BeTrue();
result.ClientVersion.Should().Be(fixture.CurrentEndpoint!.Version);
result.ServerVersion.Should().Be("1.0");
// Cleanup
await fixture.DisposeAsync();
}
[Fact]
public async Task VersionCompatibilityFixture_TestMessageFormat_ReturnsSuccess()
{
// Arrange
var fixture = new VersionCompatibilityFixture();
await fixture.InitializeAsync();
var producer = await fixture.StartVersion("1.0", "Producer");
var consumer = await fixture.StartVersion("2.0", "Consumer");
// Act
var result = await fixture.TestMessageFormat(producer, consumer, "EvidenceBundle");
// Assert
result.IsSuccess.Should().BeTrue();
result.Message.Should().Contain("EvidenceBundle");
// Cleanup
await fixture.DisposeAsync();
}
[Fact]
public async Task VersionCompatibilityFixture_TestSchemaMigration_ReturnsSuccess()
{
// Arrange
var fixture = new VersionCompatibilityFixture();
await fixture.InitializeAsync();
// Act
var result = await fixture.TestSchemaMigration("1.0", "2.0", new { id = 1 });
// Assert
result.IsSuccess.Should().BeTrue();
result.FromVersion.Should().Be("1.0");
result.ToVersion.Should().Be("2.0");
result.DataPreserved.Should().BeTrue();
result.RollbackSupported.Should().BeTrue();
// Cleanup
await fixture.DisposeAsync();
}
[Fact]
public async Task VersionCompatibilityFixture_StopVersion_RemovesEndpoint()
{
// Arrange
var fixture = new VersionCompatibilityFixture();
await fixture.InitializeAsync();
await fixture.StartVersion("1.0", "Service");
// Act
await fixture.StopVersion("1.0", "Service");
var newEndpoint = await fixture.StartVersion("1.0", "Service");
// Assert - new endpoint should be created (different base URL due to increment)
newEndpoint.Should().NotBeNull();
// Cleanup
await fixture.DisposeAsync();
}
#endregion
#region ServiceEndpoint Tests
[Fact]
public void ServiceEndpoint_DefaultValues_AreSet()
{
// Arrange & Act
var endpoint = new ServiceEndpoint();
// Assert
endpoint.ServiceName.Should().BeEmpty();
endpoint.Version.Should().BeEmpty();
endpoint.BaseUrl.Should().BeEmpty();
endpoint.IsHealthy.Should().BeFalse();
}
#endregion
#region CompatibilityResult Tests
[Fact]
public void CompatibilityResult_DefaultValues_AreSet()
{
// Arrange & Act
var result = new CompatibilityResult();
// Assert
result.IsSuccess.Should().BeFalse();
result.Errors.Should().BeEmpty();
result.Warnings.Should().BeEmpty();
}
#endregion
}

View File

@@ -0,0 +1,86 @@
using System;
using FluentAssertions;
using StellaOps.TestKit.Longevity;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class LongevityTests
{
[Fact]
public void StabilityMetrics_HasMemoryLeak_ReturnsFalseInitially()
{
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
metrics.HasMemoryLeak().Should().BeFalse();
}
[Fact]
public void StabilityMetrics_HasConnectionPoolLeak_DetectsLeaks()
{
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
metrics.RecordConnectionPool(active: 10, leaked: 2);
metrics.HasConnectionPoolLeak(maxLeaked: 0).Should().BeTrue();
metrics.HasConnectionPoolLeak(maxLeaked: 2).Should().BeFalse();
}
[Fact]
public void StabilityMetrics_HasDrift_DetectsDriftingCounters()
{
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
metrics.RecordCounter("counter", 100);
metrics.CaptureSnapshot();
metrics.RecordCounter("counter", 2000);
metrics.HasDrift("counter", threshold: 1000).Should().BeTrue();
metrics.HasDrift("counter", threshold: 5000).Should().BeFalse();
}
[Fact]
public void StabilityMetrics_HasDrift_ReturnsFalseWhenCounterMissing()
{
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
metrics.CaptureSnapshot();
metrics.HasDrift("missing", threshold: 1).Should().BeFalse();
}
[Fact]
public void StabilityMetrics_MemoryGrowthRate_CalculatesSlope()
{
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
for (int i = 0; i < 5; i++)
{
metrics.CaptureSnapshot();
}
var growthRate = metrics.MemoryGrowthRate;
double.IsNaN(growthRate).Should().BeFalse();
double.IsInfinity(growthRate).Should().BeFalse();
}
[Fact]
public void StabilityMetrics_GenerateReport_CreatesValidReport()
{
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
metrics.CaptureSnapshot();
metrics.RecordCounter("test", 42);
var report = metrics.GenerateReport();
report.Should().NotBeNull();
report.SnapshotCount.Should().Be(2);
report.BaselineMemory.Should().BeGreaterThan(0);
report.GeneratedAt.Should().NotBe(default);
report.GeneratedAt.Offset.Should().Be(TimeSpan.Zero);
}
}

View File

@@ -0,0 +1,55 @@
using FluentAssertions;
using StellaOps.TestKit.Longevity;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class LongevityTests
{
[Fact]
public void StabilityMetrics_CaptureBaseline_SetsBaseline()
{
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
metrics.MemoryBaseline.Should().BeGreaterThan(0);
metrics.Snapshots.Should().HaveCount(1);
}
[Fact]
public void StabilityMetrics_CaptureSnapshot_AddsSnapshot()
{
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
metrics.CaptureSnapshot();
metrics.CaptureSnapshot();
metrics.Snapshots.Should().HaveCount(3);
}
[Fact]
public void StabilityMetrics_RecordCounter_StoresValue()
{
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
metrics.RecordCounter("requests_total", 100);
metrics.CounterValues.Should().ContainKey("requests_total");
metrics.CounterValues["requests_total"].Should().Be(100);
}
[Fact]
public void StabilityMetrics_RecordConnectionPool_StoresValues()
{
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
metrics.RecordConnectionPool(active: 5, leaked: 1);
metrics.ConnectionPoolActive.Should().Be(5);
metrics.ConnectionPoolLeaked.Should().Be(1);
}
}

View File

@@ -0,0 +1,48 @@
using FluentAssertions;
using StellaOps.TestKit.Longevity;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class LongevityTests
{
[Fact]
public void StabilityReport_ToJson_ProducesValidJson()
{
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
var report = metrics.GenerateReport();
var json = report.ToJson();
json.Should().Contain("\"snapshotCount\"");
json.Should().Contain("\"baselineMemory\"");
json.Should().Contain("\"hasMemoryLeak\"");
}
[Fact]
public void StabilityReport_Passed_ReturnsTrueWhenNoIssues()
{
var report = new StabilityReport
{
HasMemoryLeak = false,
HasConnectionPoolLeak = false,
DriftingCounters = []
};
report.Passed.Should().BeTrue();
}
[Fact]
public void StabilityReport_Passed_ReturnsFalseWhenMemoryLeak()
{
var report = new StabilityReport
{
HasMemoryLeak = true,
HasConnectionPoolLeak = false,
DriftingCounters = []
};
report.Passed.Should().BeFalse();
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Threading;
using FluentAssertions;
using StellaOps.TestKit.Longevity;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class LongevityTests
{
[Fact]
public async Task StabilityTestRunner_RunExtended_RunsForDurationAsync()
{
var runner = new StabilityTestRunner
{
Config = new StabilityTestConfig { SnapshotInterval = 100 }
};
var executionCount = 0;
var report = await runner.RunExtended(
scenario: () =>
{
executionCount++;
return Task.CompletedTask;
},
duration: TimeSpan.FromMilliseconds(100));
executionCount.Should().BeGreaterThan(0);
report.Should().NotBeNull();
}
[Fact]
public async Task StabilityTestRunner_RunExtended_RespectsCancellationAsync()
{
var runner = new StabilityTestRunner();
using var cts = new CancellationTokenSource();
var executionCount = 0;
cts.CancelAfter(50);
await runner.RunExtended(
scenario: async () =>
{
executionCount++;
await Task.Delay(10);
},
duration: TimeSpan.FromHours(1),
cancellationToken: cts.Token);
executionCount.Should().BeLessThan(100);
}
[Fact]
public void StabilityTestRunner_Metrics_ExposesUnderlyingMetrics()
{
var runner = new StabilityTestRunner();
runner.Metrics.Should().NotBeNull();
runner.Metrics.Should().BeOfType<StabilityMetrics>();
}
}

View File

@@ -0,0 +1,40 @@
using System;
using FluentAssertions;
using StellaOps.TestKit.Longevity;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class LongevityTests
{
[Fact]
public async Task StabilityTestRunner_RunIterations_ContinuesOnErrorIfNotConfiguredAsync()
{
var runner = new StabilityTestRunner
{
Config = new StabilityTestConfig
{
StopOnError = false,
SnapshotInterval = 10
}
};
var executionCount = 0;
var errorCount = 0;
await runner.RunIterations(
scenario: () =>
{
executionCount++;
if (executionCount % 3 == 0)
{
errorCount++;
throw new InvalidOperationException("Test error");
}
return Task.CompletedTask;
},
iterations: 10);
executionCount.Should().Be(10);
errorCount.Should().BeGreaterThan(0);
}
}

View File

@@ -0,0 +1,74 @@
using System;
using FluentAssertions;
using StellaOps.TestKit.Longevity;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class LongevityTests
{
[Fact]
public async Task StabilityTestRunner_RunIterations_ExecutesScenarioAsync()
{
var runner = new StabilityTestRunner
{
Config = new StabilityTestConfig { SnapshotInterval = 5 }
};
var executionCount = 0;
var report = await runner.RunIterations(
scenario: () =>
{
executionCount++;
return Task.CompletedTask;
},
iterations: 10);
executionCount.Should().Be(10);
report.Should().NotBeNull();
}
[Fact]
public async Task StabilityTestRunner_RunIterations_CapturesSnapshotsAsync()
{
var runner = new StabilityTestRunner
{
Config = new StabilityTestConfig { SnapshotInterval = 2 }
};
var report = await runner.RunIterations(
scenario: () => Task.CompletedTask,
iterations: 10);
report.SnapshotCount.Should().BeGreaterThan(1);
}
[Fact]
public async Task StabilityTestRunner_RunIterations_StopsOnErrorIfConfiguredAsync()
{
var runner = new StabilityTestRunner
{
Config = new StabilityTestConfig
{
StopOnError = true,
SnapshotInterval = 1
}
};
var executionCount = 0;
await runner.RunIterations(
scenario: () =>
{
executionCount++;
if (executionCount == 5)
{
throw new InvalidOperationException("Test error");
}
return Task.CompletedTask;
},
iterations: 100);
executionCount.Should().Be(5);
}
}

View File

@@ -1,388 +1,9 @@
using FluentAssertions;
using StellaOps.TestKit.Longevity;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.TestKit.Tests;
/// <summary>
/// Unit tests for time-extended stability testing infrastructure.
/// </summary>
[Trait("Category", TestCategories.Unit)]
public sealed class LongevityTests
public sealed partial class LongevityTests
{
#region StabilityMetrics Tests
[Fact]
public void StabilityMetrics_CaptureBaseline_SetsBaseline()
{
// Arrange
var metrics = new StabilityMetrics();
// Act
metrics.CaptureBaseline();
// Assert
metrics.MemoryBaseline.Should().BeGreaterThan(0);
metrics.Snapshots.Should().HaveCount(1);
}
[Fact]
public void StabilityMetrics_CaptureSnapshot_AddsSnapshot()
{
// Arrange
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
// Act
metrics.CaptureSnapshot();
metrics.CaptureSnapshot();
// Assert
metrics.Snapshots.Should().HaveCount(3); // Baseline + 2 snapshots
}
[Fact]
public void StabilityMetrics_RecordCounter_StoresValue()
{
// Arrange
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
// Act
metrics.RecordCounter("requests_total", 100);
// Assert
metrics.CounterValues.Should().ContainKey("requests_total");
metrics.CounterValues["requests_total"].Should().Be(100);
}
[Fact]
public void StabilityMetrics_RecordConnectionPool_StoresValues()
{
// Arrange
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
// Act
metrics.RecordConnectionPool(active: 5, leaked: 1);
// Assert
metrics.ConnectionPoolActive.Should().Be(5);
metrics.ConnectionPoolLeaked.Should().Be(1);
}
[Fact]
public void StabilityMetrics_HasMemoryLeak_ReturnsFalseInitially()
{
// Arrange
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
// Act & Assert
metrics.HasMemoryLeak().Should().BeFalse();
}
[Fact]
public void StabilityMetrics_HasConnectionPoolLeak_DetectsLeaks()
{
// Arrange
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
metrics.RecordConnectionPool(active: 10, leaked: 2);
// Act & Assert
metrics.HasConnectionPoolLeak(maxLeaked: 0).Should().BeTrue();
metrics.HasConnectionPoolLeak(maxLeaked: 2).Should().BeFalse();
}
[Fact]
public void StabilityMetrics_HasDrift_DetectsDriftingCounters()
{
// Arrange
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
metrics.RecordCounter("counter", 100);
metrics.CaptureSnapshot();
metrics.RecordCounter("counter", 2000);
// Act & Assert
metrics.HasDrift("counter", threshold: 1000).Should().BeTrue();
metrics.HasDrift("counter", threshold: 5000).Should().BeFalse();
}
[Fact]
public void StabilityMetrics_MemoryGrowthRate_CalculatesSlope()
{
// Arrange
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
// Capture multiple snapshots (growth rate requires at least 2)
for (int i = 0; i < 5; i++)
{
metrics.CaptureSnapshot();
}
// Act
var growthRate = metrics.MemoryGrowthRate;
// Assert - just verify it's calculated and is a valid value
double.IsNaN(growthRate).Should().BeFalse();
double.IsInfinity(growthRate).Should().BeFalse();
}
[Fact]
public void StabilityMetrics_GenerateReport_CreatesValidReport()
{
// Arrange
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
metrics.CaptureSnapshot();
metrics.RecordCounter("test", 42);
// Act
var report = metrics.GenerateReport();
// Assert
report.Should().NotBeNull();
report.SnapshotCount.Should().Be(2);
report.BaselineMemory.Should().BeGreaterThan(0);
report.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
}
[Fact]
public void StabilityReport_ToJson_ProducesValidJson()
{
// Arrange
var metrics = new StabilityMetrics();
metrics.CaptureBaseline();
var report = metrics.GenerateReport();
// Act
var json = report.ToJson();
// Assert
json.Should().Contain("\"snapshotCount\"");
json.Should().Contain("\"baselineMemory\"");
json.Should().Contain("\"hasMemoryLeak\"");
}
[Fact]
public void StabilityReport_Passed_ReturnsTrueWhenNoIssues()
{
// Arrange
var report = new StabilityReport
{
HasMemoryLeak = false,
HasConnectionPoolLeak = false,
DriftingCounters = []
};
// Act & Assert
report.Passed.Should().BeTrue();
}
[Fact]
public void StabilityReport_Passed_ReturnsFalseWhenMemoryLeak()
{
// Arrange
var report = new StabilityReport
{
HasMemoryLeak = true,
HasConnectionPoolLeak = false,
DriftingCounters = []
};
// Act & Assert
report.Passed.Should().BeFalse();
}
#endregion
#region StabilityTestRunner Tests
[Fact]
public async Task StabilityTestRunner_RunIterations_ExecutesScenario()
{
// Arrange
var runner = new StabilityTestRunner
{
Config = new StabilityTestConfig { SnapshotInterval = 5 }
};
var executionCount = 0;
// Act
var report = await runner.RunIterations(
scenario: () =>
{
executionCount++;
return Task.CompletedTask;
},
iterations: 10);
// Assert
executionCount.Should().Be(10);
report.Should().NotBeNull();
}
[Fact]
public async Task StabilityTestRunner_RunIterations_CapturesSnapshots()
{
// Arrange
var runner = new StabilityTestRunner
{
Config = new StabilityTestConfig { SnapshotInterval = 2 }
};
// Act
var report = await runner.RunIterations(
scenario: () => Task.CompletedTask,
iterations: 10);
// Assert
report.SnapshotCount.Should().BeGreaterThan(1);
}
[Fact]
public async Task StabilityTestRunner_RunIterations_StopsOnErrorIfConfigured()
{
// Arrange
var runner = new StabilityTestRunner
{
Config = new StabilityTestConfig
{
StopOnError = true,
SnapshotInterval = 1
}
};
var executionCount = 0;
// Act
var report = await runner.RunIterations(
scenario: () =>
{
executionCount++;
if (executionCount == 5)
{
throw new InvalidOperationException("Test error");
}
return Task.CompletedTask;
},
iterations: 100);
// Assert
executionCount.Should().Be(5);
}
[Fact]
public async Task StabilityTestRunner_RunIterations_ContinuesOnErrorIfNotConfigured()
{
// Arrange
var runner = new StabilityTestRunner
{
Config = new StabilityTestConfig
{
StopOnError = false,
SnapshotInterval = 10
}
};
var executionCount = 0;
var errorCount = 0;
// Act
var report = await runner.RunIterations(
scenario: () =>
{
executionCount++;
if (executionCount % 3 == 0)
{
errorCount++;
throw new InvalidOperationException("Test error");
}
return Task.CompletedTask;
},
iterations: 10);
// Assert
executionCount.Should().Be(10);
errorCount.Should().BeGreaterThan(0);
}
[Fact]
public async Task StabilityTestRunner_RunExtended_RunsForDuration()
{
// Arrange
var runner = new StabilityTestRunner
{
Config = new StabilityTestConfig { SnapshotInterval = 100 }
};
var executionCount = 0;
// Act
var report = await runner.RunExtended(
scenario: () =>
{
executionCount++;
return Task.CompletedTask;
},
duration: TimeSpan.FromMilliseconds(100));
// Assert
executionCount.Should().BeGreaterThan(0);
report.Should().NotBeNull();
}
[Fact]
public async Task StabilityTestRunner_RunExtended_RespectsCancellation()
{
// Arrange
var runner = new StabilityTestRunner();
using var cts = new CancellationTokenSource();
var executionCount = 0;
// Act
cts.CancelAfter(50);
var report = await runner.RunExtended(
scenario: async () =>
{
executionCount++;
await Task.Delay(10);
},
duration: TimeSpan.FromHours(1),
cancellationToken: cts.Token);
// Assert
executionCount.Should().BeLessThan(100);
}
[Fact]
public void StabilityTestRunner_Metrics_ExposesUnderlyingMetrics()
{
// Arrange
var runner = new StabilityTestRunner();
// Act & Assert
runner.Metrics.Should().NotBeNull();
runner.Metrics.Should().BeOfType<StabilityMetrics>();
}
#endregion
#region StabilityTestConfig Tests
[Fact]
public void StabilityTestConfig_Defaults_AreReasonable()
{
// Arrange & Act
var config = new StabilityTestConfig();
// Assert
config.SnapshotInterval.Should().Be(100);
config.MemoryLeakThresholdPercent.Should().Be(10);
config.MaxConnectionPoolLeaks.Should().Be(0);
config.StopOnError.Should().BeFalse();
config.IterationDelay.Should().Be(TimeSpan.Zero);
}
#endregion
}

View File

@@ -0,0 +1,27 @@
using System;
using FluentAssertions;
using StellaOps.TestKit.Observability;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class ObservabilityContractTests
{
[Fact]
public void ContractViolationException_ContainsMessage()
{
var ex = new ContractViolationException("Test violation");
ex.Message.Should().Be("Test violation");
}
[Fact]
public void ContractViolationException_WithInnerException()
{
var inner = new InvalidOperationException("Inner error");
var ex = new ContractViolationException("Outer error", inner);
ex.Message.Should().Be("Outer error");
ex.InnerException.Should().Be(inner);
}
}

View File

@@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using StellaOps.TestKit.Observability;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class ObservabilityContractTests
{
[Fact]
public void NoSensitiveData_ContainsEmail_ThrowsContractViolation()
{
var records = new[]
{
new CapturedLogRecord
{
LogLevel = LogLevel.Information,
Message = "User test@example.com logged in",
StateValues = new Dictionary<string, object?>()
}
};
var piiPatterns = new[]
{
new Regex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
};
var act = () => LogContractAssert.NoSensitiveData(records, piiPatterns);
act.Should().Throw<ContractViolationException>()
.WithMessage("*PII*");
}
[Fact]
public void LogLevelAppropriate_WithinRange_NoException()
{
var record = new CapturedLogRecord
{
LogLevel = LogLevel.Warning,
Message = "Test warning"
};
var act = () => LogContractAssert.LogLevelAppropriate(
record,
LogLevel.Information,
LogLevel.Error);
act.Should().NotThrow();
}
[Fact]
public void LogLevelAppropriate_OutsideRange_ThrowsContractViolation()
{
var record = new CapturedLogRecord
{
LogLevel = LogLevel.Critical,
Message = "Critical error"
};
var act = () => LogContractAssert.LogLevelAppropriate(
record,
LogLevel.Information,
LogLevel.Warning);
act.Should().Throw<ContractViolationException>()
.WithMessage("*Critical*outside*range*");
}
}

View File

@@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using StellaOps.TestKit.Observability;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class ObservabilityContractTests
{
[Fact]
public void HasRequiredFields_AllPresent_NoException()
{
var record = new CapturedLogRecord
{
LogLevel = LogLevel.Information,
Message = "Test message",
StateValues = new Dictionary<string, object?>
{
["CorrelationId"] = "abc-123",
["TenantId"] = "acme"
}
};
var act = () => LogContractAssert.HasRequiredFields(record, "CorrelationId", "TenantId");
act.Should().NotThrow();
}
[Fact]
public void HasRequiredFields_Missing_ThrowsContractViolation()
{
var record = new CapturedLogRecord
{
LogLevel = LogLevel.Information,
Message = "Test message",
StateValues = new Dictionary<string, object?>
{
["CorrelationId"] = "abc-123"
}
};
var act = () => LogContractAssert.HasRequiredFields(record, "CorrelationId", "MissingField");
act.Should().Throw<ContractViolationException>()
.WithMessage("*MissingField*");
}
[Fact]
public void NoSensitiveData_Clean_NoException()
{
var records = new[]
{
new CapturedLogRecord
{
LogLevel = LogLevel.Information,
Message = "User logged in successfully",
StateValues = new Dictionary<string, object?>
{
["UserId"] = "user-123"
}
}
};
var piiPatterns = new[]
{
new Regex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
};
var act = () => LogContractAssert.NoSensitiveData(records, piiPatterns);
act.Should().NotThrow();
}
}

View File

@@ -0,0 +1,44 @@
using System.Diagnostics.Metrics;
using FluentAssertions;
using StellaOps.TestKit.Observability;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class ObservabilityContractTests
{
[Fact]
public void MetricNamesMatchPattern_PassesForSnakeCase()
{
using var meter = new Meter("TestMeter7");
using var capture = new MetricsCapture("TestMeter7");
var counter = meter.CreateCounter<long>("requests_total");
counter.Add(1);
var act = () => MetricsContractAssert.MetricNamesMatchPattern(
capture,
"^[a-z_]+$");
act.Should().NotThrow();
}
[Fact]
public void MetricNamesMatchPattern_FindsViolations()
{
using var meter = new Meter("TestMeter8");
using var capture = new MetricsCapture("TestMeter8");
var valid = meter.CreateCounter<long>("requests_total");
var invalid = meter.CreateCounter<long>("InvalidMetric");
valid.Add(1);
invalid.Add(1);
var act = () => MetricsContractAssert.MetricNamesMatchPattern(
capture,
"^[a-z_]+$");
act.Should().Throw<ContractViolationException>()
.WithMessage("*InvalidMetric*");
}
}

View File

@@ -0,0 +1,43 @@
using System.Diagnostics.Metrics;
using FluentAssertions;
using StellaOps.TestKit.Observability;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class ObservabilityContractTests
{
[Fact]
public void CounterMonotonic_AlwaysIncreasing_NoException()
{
using var meter = new Meter("TestMeter5");
using var capture = new MetricsCapture("TestMeter5");
var counter = meter.CreateCounter<long>("monotonic_counter");
counter.Add(1);
counter.Add(2);
counter.Add(3);
var act = () => MetricsContractAssert.CounterMonotonic(capture, "monotonic_counter");
act.Should().NotThrow();
}
[Fact]
public void GaugeInBounds_WithinRange_NoException()
{
using var meter = new Meter("TestMeter6");
using var capture = new MetricsCapture("TestMeter6");
var gauge = meter.CreateObservableGauge("memory_usage_bytes", () => 500);
_ = capture.GetValues("memory_usage_bytes");
var act = () => MetricsContractAssert.GaugeInBounds(
capture,
"memory_usage_bytes",
0,
1000);
act.Should().NotThrow();
}
}

View File

@@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using FluentAssertions;
using StellaOps.TestKit.Observability;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class ObservabilityContractTests
{
[Fact]
public void MetricExists_Present_NoException()
{
using var meter = new Meter("TestMeter1");
using var capture = new MetricsCapture("TestMeter1");
var counter = meter.CreateCounter<long>("test_requests_total");
counter.Add(1);
var act = () => MetricsContractAssert.MetricExists(capture, "test_requests_total");
act.Should().NotThrow();
}
[Fact]
public void MetricExists_Missing_ThrowsContractViolation()
{
using var meter = new Meter("TestMeter2");
using var capture = new MetricsCapture("TestMeter2");
var counter = meter.CreateCounter<long>("some_other_metric");
counter.Add(1);
var act = () => MetricsContractAssert.MetricExists(capture, "missing_metric");
act.Should().Throw<ContractViolationException>()
.WithMessage("*missing_metric*not found*");
}
[Fact]
public void LabelCardinalityBounded_WithinThreshold_NoException()
{
using var meter = new Meter("TestMeter3");
using var capture = new MetricsCapture("TestMeter3");
var counter = meter.CreateCounter<long>("http_requests_total");
counter.Add(1, new KeyValuePair<string, object?>("method", "GET"));
counter.Add(1, new KeyValuePair<string, object?>("method", "POST"));
var act = () => MetricsContractAssert.LabelCardinalityBounded(
capture,
"http_requests_total",
maxLabels: 10);
act.Should().NotThrow();
}
[Fact]
public void LabelCardinalityBounded_ExceedsThreshold_ThrowsContractViolation()
{
using var meter = new Meter("TestMeter4");
using var capture = new MetricsCapture("TestMeter4");
var counter = meter.CreateCounter<long>("requests_by_user");
for (int i = 0; i < 10; i++)
{
counter.Add(1, new KeyValuePair<string, object?>("user_id", $"user-{i}"));
}
var act = () => MetricsContractAssert.LabelCardinalityBounded(
capture,
"requests_by_user",
maxLabels: 5);
act.Should().Throw<ContractViolationException>()
.WithMessage("*cardinality*exceeds*");
}
}

View File

@@ -0,0 +1,29 @@
using System.Diagnostics;
using FluentAssertions;
using StellaOps.TestKit.Observability;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class ObservabilityContractTests
{
[Fact]
public void AttributeCardinality_ExceedsThreshold_ThrowsContractViolation()
{
using var source = new ActivitySource("TestSource6");
using var capture = new OtelCapture("TestSource6");
for (int i = 0; i < 10; i++)
{
using (var activity = source.StartActivity($"Span{i}"))
{
activity?.SetTag("request_id", $"id-{i}");
}
}
var act = () => OTelContractAssert.AttributeCardinality(capture, "request_id", maxCardinality: 5);
act.Should().Throw<ContractViolationException>()
.WithMessage("*cardinality*exceeds*");
}
}

View File

@@ -0,0 +1,93 @@
using System.Diagnostics;
using System.Linq;
using FluentAssertions;
using StellaOps.TestKit.Observability;
using Xunit;
namespace StellaOps.TestKit.Tests;
public sealed partial class ObservabilityContractTests
{
[Fact]
public void HasRequiredSpans_AllPresent_NoException()
{
using var source = new ActivitySource("TestSource");
using var capture = new OtelCapture("TestSource");
using (source.StartActivity("Span1")) { }
using (source.StartActivity("Span2")) { }
var act = () => OTelContractAssert.HasRequiredSpans(capture, "Span1", "Span2");
act.Should().NotThrow();
}
[Fact]
public void HasRequiredSpans_Missing_ThrowsContractViolation()
{
using var source = new ActivitySource("TestSource2");
using var capture = new OtelCapture("TestSource2");
using (source.StartActivity("Span1")) { }
var act = () => OTelContractAssert.HasRequiredSpans(capture, "Span1", "MissingSpan");
act.Should().Throw<ContractViolationException>()
.WithMessage("*MissingSpan*");
}
[Fact]
public void SpanHasAttributes_AllPresent_NoException()
{
using var source = new ActivitySource("TestSource3");
using var capture = new OtelCapture("TestSource3");
using (var activity = source.StartActivity("TestSpan"))
{
activity?.SetTag("user_id", "123");
activity?.SetTag("tenant_id", "acme");
}
var span = capture.CapturedActivities.First();
var act = () => OTelContractAssert.SpanHasAttributes(span, "user_id", "tenant_id");
act.Should().NotThrow();
}
[Fact]
public void SpanHasAttributes_Missing_ThrowsContractViolation()
{
using var source = new ActivitySource("TestSource4");
using var capture = new OtelCapture("TestSource4");
using (var activity = source.StartActivity("TestSpan"))
{
activity?.SetTag("user_id", "123");
}
var span = capture.CapturedActivities.First();
var act = () => OTelContractAssert.SpanHasAttributes(span, "user_id", "missing_attr");
act.Should().Throw<ContractViolationException>()
.WithMessage("*missing_attr*");
}
[Fact]
public void AttributeCardinality_WithinThreshold_NoException()
{
using var source = new ActivitySource("TestSource5");
using var capture = new OtelCapture("TestSource5");
for (int i = 0; i < 5; i++)
{
using (var activity = source.StartActivity($"Span{i}"))
{
activity?.SetTag("status", i % 3 == 0 ? "ok" : "error");
}
}
var act = () => OTelContractAssert.AttributeCardinality(capture, "status", maxCardinality: 10);
act.Should().NotThrow();
}
}

View File

@@ -1,360 +1,9 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Text.RegularExpressions;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using StellaOps.TestKit.Observability;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.TestKit.Tests;
/// <summary>
/// Unit tests for observability contract assertions.
/// </summary>
[Trait("Category", TestCategories.Unit)]
public sealed class ObservabilityContractTests
public sealed partial class ObservabilityContractTests
{
#region OTelContractAssert Tests
[Fact]
public void HasRequiredSpans_AllPresent_NoException()
{
using var source = new ActivitySource("TestSource");
using var capture = new OtelCapture("TestSource");
using (source.StartActivity("Span1")) { }
using (source.StartActivity("Span2")) { }
var act = () => OTelContractAssert.HasRequiredSpans(capture, "Span1", "Span2");
act.Should().NotThrow();
}
[Fact]
public void HasRequiredSpans_Missing_ThrowsContractViolation()
{
using var source = new ActivitySource("TestSource2");
using var capture = new OtelCapture("TestSource2");
using (source.StartActivity("Span1")) { }
var act = () => OTelContractAssert.HasRequiredSpans(capture, "Span1", "MissingSpan");
act.Should().Throw<ContractViolationException>()
.WithMessage("*MissingSpan*");
}
[Fact]
public void SpanHasAttributes_AllPresent_NoException()
{
using var source = new ActivitySource("TestSource3");
using var capture = new OtelCapture("TestSource3");
using (var activity = source.StartActivity("TestSpan"))
{
activity?.SetTag("user_id", "123");
activity?.SetTag("tenant_id", "acme");
}
var span = capture.CapturedActivities.First();
var act = () => OTelContractAssert.SpanHasAttributes(span, "user_id", "tenant_id");
act.Should().NotThrow();
}
[Fact]
public void SpanHasAttributes_Missing_ThrowsContractViolation()
{
using var source = new ActivitySource("TestSource4");
using var capture = new OtelCapture("TestSource4");
using (var activity = source.StartActivity("TestSpan"))
{
activity?.SetTag("user_id", "123");
}
var span = capture.CapturedActivities.First();
var act = () => OTelContractAssert.SpanHasAttributes(span, "user_id", "missing_attr");
act.Should().Throw<ContractViolationException>()
.WithMessage("*missing_attr*");
}
[Fact]
public void AttributeCardinality_WithinThreshold_NoException()
{
using var source = new ActivitySource("TestSource5");
using var capture = new OtelCapture("TestSource5");
for (int i = 0; i < 5; i++)
{
using (var activity = source.StartActivity($"Span{i}"))
{
activity?.SetTag("status", i % 3 == 0 ? "ok" : "error"); // 2 unique values
}
}
var act = () => OTelContractAssert.AttributeCardinality(capture, "status", maxCardinality: 10);
act.Should().NotThrow();
}
[Fact]
public void AttributeCardinality_ExceedsThreshold_ThrowsContractViolation()
{
using var source = new ActivitySource("TestSource6");
using var capture = new OtelCapture("TestSource6");
for (int i = 0; i < 10; i++)
{
using (var activity = source.StartActivity($"Span{i}"))
{
activity?.SetTag("request_id", $"id-{i}"); // 10 unique values
}
}
var act = () => OTelContractAssert.AttributeCardinality(capture, "request_id", maxCardinality: 5);
act.Should().Throw<ContractViolationException>()
.WithMessage("*cardinality*exceeds*");
}
#endregion
#region LogContractAssert Tests
[Fact]
public void HasRequiredFields_AllPresent_NoException()
{
var record = new CapturedLogRecord
{
LogLevel = LogLevel.Information,
Message = "Test message",
StateValues = new Dictionary<string, object?>
{
["CorrelationId"] = "abc-123",
["TenantId"] = "acme"
}
};
var act = () => LogContractAssert.HasRequiredFields(record, "CorrelationId", "TenantId");
act.Should().NotThrow();
}
[Fact]
public void HasRequiredFields_Missing_ThrowsContractViolation()
{
var record = new CapturedLogRecord
{
LogLevel = LogLevel.Information,
Message = "Test message",
StateValues = new Dictionary<string, object?>
{
["CorrelationId"] = "abc-123"
}
};
var act = () => LogContractAssert.HasRequiredFields(record, "CorrelationId", "MissingField");
act.Should().Throw<ContractViolationException>()
.WithMessage("*MissingField*");
}
[Fact]
public void NoSensitiveData_Clean_NoException()
{
var records = new[]
{
new CapturedLogRecord
{
LogLevel = LogLevel.Information,
Message = "User logged in successfully",
StateValues = new Dictionary<string, object?>
{
["UserId"] = "user-123"
}
}
};
var piiPatterns = new[] { new Regex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b") };
var act = () => LogContractAssert.NoSensitiveData(records, piiPatterns);
act.Should().NotThrow();
}
[Fact]
public void NoSensitiveData_ContainsEmail_ThrowsContractViolation()
{
var records = new[]
{
new CapturedLogRecord
{
LogLevel = LogLevel.Information,
Message = "User test@example.com logged in",
StateValues = new Dictionary<string, object?>()
}
};
var piiPatterns = new[] { new Regex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b") };
var act = () => LogContractAssert.NoSensitiveData(records, piiPatterns);
act.Should().Throw<ContractViolationException>()
.WithMessage("*PII*");
}
[Fact]
public void LogLevelAppropriate_WithinRange_NoException()
{
var record = new CapturedLogRecord
{
LogLevel = LogLevel.Warning,
Message = "Test warning"
};
var act = () => LogContractAssert.LogLevelAppropriate(record, LogLevel.Information, LogLevel.Error);
act.Should().NotThrow();
}
[Fact]
public void LogLevelAppropriate_OutsideRange_ThrowsContractViolation()
{
var record = new CapturedLogRecord
{
LogLevel = LogLevel.Critical,
Message = "Critical error"
};
var act = () => LogContractAssert.LogLevelAppropriate(record, LogLevel.Information, LogLevel.Warning);
act.Should().Throw<ContractViolationException>()
.WithMessage("*Critical*outside*range*");
}
#endregion
#region MetricsContractAssert Tests
[Fact]
public void MetricExists_Present_NoException()
{
using var meter = new Meter("TestMeter1");
using var capture = new MetricsCapture("TestMeter1");
var counter = meter.CreateCounter<long>("test_requests_total");
counter.Add(1);
var act = () => MetricsContractAssert.MetricExists(capture, "test_requests_total");
act.Should().NotThrow();
}
[Fact]
public void MetricExists_Missing_ThrowsContractViolation()
{
using var meter = new Meter("TestMeter2");
using var capture = new MetricsCapture("TestMeter2");
var counter = meter.CreateCounter<long>("some_other_metric");
counter.Add(1);
var act = () => MetricsContractAssert.MetricExists(capture, "missing_metric");
act.Should().Throw<ContractViolationException>()
.WithMessage("*missing_metric*not found*");
}
[Fact]
public void LabelCardinalityBounded_WithinThreshold_NoException()
{
using var meter = new Meter("TestMeter3");
using var capture = new MetricsCapture("TestMeter3");
var counter = meter.CreateCounter<long>("http_requests_total");
counter.Add(1, new KeyValuePair<string, object?>("method", "GET"));
counter.Add(1, new KeyValuePair<string, object?>("method", "POST"));
var act = () => MetricsContractAssert.LabelCardinalityBounded(capture, "http_requests_total", maxLabels: 10);
act.Should().NotThrow();
}
[Fact]
public void LabelCardinalityBounded_ExceedsThreshold_ThrowsContractViolation()
{
using var meter = new Meter("TestMeter4");
using var capture = new MetricsCapture("TestMeter4");
var counter = meter.CreateCounter<long>("requests_by_user");
for (int i = 0; i < 10; i++)
{
counter.Add(1, new KeyValuePair<string, object?>("user_id", $"user-{i}"));
}
var act = () => MetricsContractAssert.LabelCardinalityBounded(capture, "requests_by_user", maxLabels: 5);
act.Should().Throw<ContractViolationException>()
.WithMessage("*cardinality*exceeds*");
}
[Fact]
public void CounterMonotonic_AlwaysIncreasing_NoException()
{
using var meter = new Meter("TestMeter5");
using var capture = new MetricsCapture("TestMeter5");
var counter = meter.CreateCounter<long>("monotonic_counter");
counter.Add(1);
counter.Add(2);
counter.Add(3);
var act = () => MetricsContractAssert.CounterMonotonic(capture, "monotonic_counter");
act.Should().NotThrow();
}
[Fact]
public void GaugeInBounds_WithinRange_NoException()
{
using var meter = new Meter("TestMeter6");
using var capture = new MetricsCapture("TestMeter6");
var gauge = meter.CreateObservableGauge("memory_usage_bytes", () => 500);
// Force a measurement
capture.GetValues("memory_usage_bytes");
// This test validates the API structure - actual observable gauge testing
// requires meter listener callbacks which are triggered asynchronously
var act = () => MetricsContractAssert.GaugeInBounds(capture, "memory_usage_bytes", 0, 1000);
act.Should().NotThrow();
}
#endregion
#region ContractViolationException Tests
[Fact]
public void ContractViolationException_ContainsMessage()
{
var ex = new ContractViolationException("Test violation");
ex.Message.Should().Be("Test violation");
}
[Fact]
public void ContractViolationException_WithInnerException()
{
var inner = new InvalidOperationException("Inner error");
var ex = new ContractViolationException("Outer error", inner);
ex.Message.Should().Be("Outer error");
ex.InnerException.Should().Be(inner);
}
#endregion
}

View File

@@ -0,0 +1,14 @@
using StellaOps.TestKit.Evidence;
using Xunit;
namespace StellaOps.TestKit.Tests;
[Requirement("REQ-REPORTER-TEST-001")]
public sealed class RequirementTestFixture
{
[Fact]
[Requirement("REQ-REPORTER-TEST-002", SprintTaskId = "TEST-001")]
public void SampleTestWithRequirement()
{
}
}

View File

@@ -12,7 +12,7 @@ public sealed class TestKitFixtureTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task ConnectorHttpFixture_CreateClient_ReturnsCannedResponse()
public async Task ConnectorHttpFixture_CreateClient_ReturnsCannedResponseAsync()
{
using var fixture = new ConnectorHttpFixture();
fixture.AddJsonResponse("https://example.test/api", "{\"status\":\"ok\"}");