Files
git.stella-ops.org/src/BinaryIndex/__Tests/StellaOps.BinaryIndex.GroundTruth.Abstractions.Tests/SymbolObservationWriteGuardTests.cs
2026-01-20 00:45:38 +02:00

427 lines
13 KiB
C#

using System.Collections.Immutable;
using FluentAssertions;
using Xunit;
namespace StellaOps.BinaryIndex.GroundTruth.Abstractions.Tests;
/// <summary>
/// Unit tests for AOC (Aggregation-Only Contract) write guard invariants.
/// </summary>
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<GroundTruthAocGuardException>()
.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<GroundTruthAocGuardException>()
.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<GroundTruthAocGuardException>()
.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<GroundTruthAocGuardException>()
.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<GroundTruthAocGuardException>()
.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<GroundTruthAocGuardException>()
.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<GroundTruthAocGuardException>()
.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<GroundTruthAocGuardException>()
.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<GroundTruthAocGuardException>()
.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<GroundTruthAocGuardException>()
.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<GroundTruthAocGuardException>()
.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<GroundTruthAocGuardException>()
.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<GroundTruthAocGuardException>()
.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
}