using System.Collections.Immutable; using FluentAssertions; using Xunit; namespace StellaOps.BinaryIndex.GroundTruth.Abstractions.Tests; /// /// Unit tests for AOC (Aggregation-Only Contract) write guard invariants. /// public class SymbolObservationWriteGuardTests { private readonly SymbolObservationWriteGuard _guard = new(); #region ValidateWrite Tests [Fact] public void ValidateWrite_NewObservation_ReturnsProceed() { // Arrange var observation = CreateValidObservation(); // Act var result = _guard.ValidateWrite(observation, existingContentHash: null); // Assert result.Should().Be(WriteDisposition.Proceed); } [Fact] public void ValidateWrite_IdenticalContentHash_ReturnsSkipIdentical() { // Arrange var observation = CreateValidObservation(); var existingHash = observation.ContentHash; // Act var result = _guard.ValidateWrite(observation, existingHash); // Assert result.Should().Be(WriteDisposition.SkipIdentical); } [Fact] public void ValidateWrite_DifferentContentHash_ReturnsRejectMutation() { // Arrange var observation = CreateValidObservation(); var existingHash = "sha256:differenthash"; // Act var result = _guard.ValidateWrite(observation, existingHash); // Assert result.Should().Be(WriteDisposition.RejectMutation); } [Fact] public void ValidateWrite_CaseInsensitiveHashComparison_ReturnsSkipIdentical() { // Arrange var observation = CreateValidObservation(); var existingHash = observation.ContentHash.ToUpperInvariant(); // Act var result = _guard.ValidateWrite(observation, existingHash); // Assert result.Should().Be(WriteDisposition.SkipIdentical); } #endregion #region EnsureValid - Required Fields Tests [Fact] public void EnsureValid_ValidObservation_DoesNotThrow() { // Arrange var observation = CreateValidObservation(); // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().NotThrow(); } [Fact] public void EnsureValid_MissingObservationId_ThrowsWithCorrectCode() { // Arrange var observation = CreateValidObservation() with { ObservationId = "" }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Any(v => v.Code == AocViolationCodes.MissingRequiredField)) .Where(ex => ex.Violations.Any(v => v.Path == "observationId")); } [Fact] public void EnsureValid_MissingSourceId_ThrowsWithCorrectCode() { // Arrange var observation = CreateValidObservation() with { SourceId = "" }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Any(v => v.Code == AocViolationCodes.MissingRequiredField)) .Where(ex => ex.Violations.Any(v => v.Path == "sourceId")); } [Fact] public void EnsureValid_MissingDebugId_ThrowsWithCorrectCode() { // Arrange var observation = CreateValidObservation() with { DebugId = "" }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Any(v => v.Code == AocViolationCodes.MissingRequiredField)) .Where(ex => ex.Violations.Any(v => v.Path == "debugId")); } [Fact] public void EnsureValid_MissingBinaryName_ThrowsWithCorrectCode() { // Arrange var observation = CreateValidObservation() with { BinaryName = "" }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Any(v => v.Code == AocViolationCodes.MissingRequiredField)) .Where(ex => ex.Violations.Any(v => v.Path == "binaryName")); } [Fact] public void EnsureValid_MissingArchitecture_ThrowsWithCorrectCode() { // Arrange var observation = CreateValidObservation() with { Architecture = "" }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Any(v => v.Code == AocViolationCodes.MissingRequiredField)) .Where(ex => ex.Violations.Any(v => v.Path == "architecture")); } #endregion #region EnsureValid - Provenance Tests (GTAOC_001) [Fact] public void EnsureValid_MissingProvenance_ThrowsWithCorrectCode() { // Arrange var observation = CreateValidObservation() with { Provenance = null! }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Any(v => v.Code == AocViolationCodes.MissingProvenance)); } [Fact] public void EnsureValid_MissingProvenanceSourceId_ThrowsWithCorrectCode() { // Arrange var observation = CreateValidObservation() with { Provenance = CreateValidProvenance() with { SourceId = "" } }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Any(v => v.Code == AocViolationCodes.MissingProvenance)) .Where(ex => ex.Violations.Any(v => v.Path == "provenance.sourceId")); } [Fact] public void EnsureValid_MissingProvenanceDocumentUri_ThrowsWithCorrectCode() { // Arrange var observation = CreateValidObservation() with { Provenance = CreateValidProvenance() with { DocumentUri = "" } }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Any(v => v.Code == AocViolationCodes.MissingProvenance)) .Where(ex => ex.Violations.Any(v => v.Path == "provenance.documentUri")); } [Fact] public void EnsureValid_MissingProvenanceDocumentHash_ThrowsWithCorrectCode() { // Arrange var observation = CreateValidObservation() with { Provenance = CreateValidProvenance() with { DocumentHash = "" } }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Any(v => v.Code == AocViolationCodes.MissingProvenance)) .Where(ex => ex.Violations.Any(v => v.Path == "provenance.documentHash")); } [Fact] public void EnsureValid_DefaultProvenanceFetchedAt_ThrowsWithCorrectCode() { // Arrange var observation = CreateValidObservation() with { Provenance = CreateValidProvenance() with { FetchedAt = default } }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Any(v => v.Code == AocViolationCodes.MissingProvenance)) .Where(ex => ex.Violations.Any(v => v.Path == "provenance.fetchedAt")); } #endregion #region EnsureValid - Content Hash Tests (GTAOC_004) [Fact] public void EnsureValid_InvalidContentHash_ThrowsWithCorrectCode() { // Arrange var observation = CreateValidObservation() with { ContentHash = "sha256:invalidhash" }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Any(v => v.Code == AocViolationCodes.InvalidContentHash)); } [Fact] public void ComputeContentHash_DeterministicForSameInput() { // Arrange var observation = CreateValidObservation(); // Act var hash1 = SymbolObservationWriteGuard.ComputeContentHash(observation); var hash2 = SymbolObservationWriteGuard.ComputeContentHash(observation); // Assert hash1.Should().Be(hash2); } [Fact] public void ComputeContentHash_DifferentForDifferentInput() { // Arrange var observation1 = CreateValidObservation(); var observation2 = CreateValidObservation() with { DebugId = "different-debug-id" }; // Act var hash1 = SymbolObservationWriteGuard.ComputeContentHash(observation1); var hash2 = SymbolObservationWriteGuard.ComputeContentHash(observation2); // Assert hash1.Should().NotBe(hash2); } [Fact] public void ComputeContentHash_StartsWithSha256Prefix() { // Arrange var observation = CreateValidObservation(); // Act var hash = SymbolObservationWriteGuard.ComputeContentHash(observation); // Assert hash.Should().StartWith("sha256:"); } #endregion #region EnsureValid - Supersession Chain Tests (GTAOC_006) [Fact] public void EnsureValid_SupersedesItself_ThrowsWithCorrectCode() { // Arrange var observationId = "groundtruth:test-source:build123:1"; var observation = CreateValidObservation() with { ObservationId = observationId, SupersedesId = observationId }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Any(v => v.Code == AocViolationCodes.InvalidSupersession)); } [Fact] public void EnsureValid_ValidSupersession_DoesNotThrow() { // Arrange var observation = CreateValidObservation() with { ObservationId = "groundtruth:test-source:build123:2", SupersedesId = "groundtruth:test-source:build123:1" }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().NotThrow(); } [Fact] public void EnsureValid_NullSupersedes_DoesNotThrow() { // Arrange var observation = CreateValidObservation() with { SupersedesId = null }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().NotThrow(); } #endregion #region Multiple Violations Tests [Fact] public void EnsureValid_MultipleViolations_ReportsAll() { // Arrange var observation = CreateValidObservation() with { ObservationId = "", SourceId = "", DebugId = "" }; // Act & Assert var act = () => _guard.EnsureValid(observation); act.Should().Throw() .Where(ex => ex.Violations.Count >= 3); } #endregion #region AocViolation Record Tests [Fact] public void AocViolation_RecordEquality() { // Arrange var v1 = new AocViolation(AocViolationCodes.MissingProvenance, "test", "path", AocViolationSeverity.Error); var v2 = new AocViolation(AocViolationCodes.MissingProvenance, "test", "path", AocViolationSeverity.Error); var v3 = new AocViolation(AocViolationCodes.MissingRequiredField, "test", "path", AocViolationSeverity.Error); // Assert v1.Should().Be(v2); v1.Should().NotBe(v3); } #endregion #region Helper Methods private static SymbolObservation CreateValidObservation() { var provenance = CreateValidProvenance(); var symbols = ImmutableArray.Create(new ObservedSymbol { Name = "main", Address = 0x1000, Size = 100, Type = SymbolType.Function, Binding = SymbolBinding.Global }); var baseObservation = new SymbolObservation { ObservationId = "groundtruth:test-source:abcd1234:1", SourceId = "test-source", DebugId = "abcd1234", BinaryName = "test.so", Architecture = "x86_64", Symbols = symbols, SymbolCount = 1, Provenance = provenance, ContentHash = "", // Will be computed CreatedAt = DateTimeOffset.UtcNow }; // Compute and set the correct content hash var hash = SymbolObservationWriteGuard.ComputeContentHash(baseObservation); return baseObservation with { ContentHash = hash }; } private static ObservationProvenance CreateValidProvenance() { return new ObservationProvenance { SourceId = "test-source", DocumentUri = "https://example.com/test.elf", FetchedAt = DateTimeOffset.UtcNow.AddMinutes(-5), RecordedAt = DateTimeOffset.UtcNow, DocumentHash = "sha256:abc123", SignatureState = SignatureState.None }; } #endregion }