// ----------------------------------------------------------------------------- // GraphQueryDeterminismTests.cs // Sprint: SPRINT_5100_0010_0002_graph_timeline_tests // Task: GRAPH-5100-005 // Description: S1 Query determinism tests (same input → same result ordering) // ----------------------------------------------------------------------------- using System.Security.Cryptography; using System.Text; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using MicrosoftOptions = Microsoft.Extensions.Options; using StellaOps.Graph.Indexer.Storage.Postgres.Repositories; using Xunit; namespace StellaOps.Graph.Indexer.Storage.Postgres.Tests; /// /// S1 Storage Layer Tests: Query Determinism Tests /// Task GRAPH-5100-005: Query determinism (same input → same result ordering) /// [Collection(GraphIndexerPostgresCollection.Name)] public sealed class GraphQueryDeterminismTests : IAsyncLifetime { private readonly GraphIndexerPostgresFixture _fixture; private readonly PostgresIdempotencyStore _idempotencyStore; public GraphQueryDeterminismTests(GraphIndexerPostgresFixture fixture) { _fixture = fixture; var options = fixture.Fixture.CreateOptions(); options.SchemaName = fixture.SchemaName; var dataSource = new GraphIndexerDataSource(MicrosoftOptions.Options.Create(options), NullLogger.Instance); _idempotencyStore = new PostgresIdempotencyStore(dataSource, NullLogger.Instance); } public async Task InitializeAsync() { await _fixture.TruncateAllTablesAsync(); } public Task DisposeAsync() => Task.CompletedTask; #region Result Ordering Determinism [Fact] public async Task MultipleIdempotencyQueries_ReturnSameOrder() { // Arrange - Insert multiple tokens var tokens = Enumerable.Range(1, 100) .Select(i => $"seq-determinism-{i:D4}") .ToList(); foreach (var token in tokens) { await _idempotencyStore.MarkSeenAsync(token, CancellationToken.None); } // Act - Query multiple times var results1 = new List(); var results2 = new List(); var results3 = new List(); foreach (var token in tokens) { results1.Add(await _idempotencyStore.HasSeenAsync(token, CancellationToken.None)); } foreach (var token in tokens) { results2.Add(await _idempotencyStore.HasSeenAsync(token, CancellationToken.None)); } foreach (var token in tokens) { results3.Add(await _idempotencyStore.HasSeenAsync(token, CancellationToken.None)); } // Assert - All results should be identical results1.Should().BeEquivalentTo(results2, "First and second query should return identical results"); results2.Should().BeEquivalentTo(results3, "Second and third query should return identical results"); results1.Should().AllBeEquivalentTo(true, "All tokens should be marked as seen"); } [Fact] public async Task ConcurrentQueries_ProduceDeterministicResults() { // Arrange var token = $"seq-concurrent-{Guid.NewGuid():N}"; await _idempotencyStore.MarkSeenAsync(token, CancellationToken.None); // Act - Run concurrent queries var tasks = Enumerable.Range(1, 50) .Select(_ => _idempotencyStore.HasSeenAsync(token, CancellationToken.None)) .ToList(); var results = await Task.WhenAll(tasks); // Assert - All concurrent queries should return the same result results.Should().AllBeEquivalentTo(true, "All concurrent queries should return identical result"); } #endregion #region Input Stability [Fact] public async Task SameInput_ProducesSameHash() { // Arrange var input = "determinism-test-input"; // Act var hash1 = ComputeHash(input); var hash2 = ComputeHash(input); var hash3 = ComputeHash(input); // Assert hash1.Should().Be(hash2, "Same input should produce same hash"); hash2.Should().Be(hash3, "Hash should be stable across multiple computations"); } [Fact] public async Task ShuffledInputs_ProduceSameCanonicalOrdering() { // Arrange var originalOrder = new[] { "node-a", "node-b", "node-c", "node-d", "node-e" }; var shuffledOrder = new[] { "node-c", "node-a", "node-e", "node-b", "node-d" }; // Act - Sort both to canonical order var canonical1 = originalOrder.OrderBy(x => x).ToList(); var canonical2 = shuffledOrder.OrderBy(x => x).ToList(); // Assert canonical1.Should().BeEquivalentTo(canonical2, options => options.WithStrictOrdering(), "Shuffled inputs should produce identical canonical ordering"); } [Fact] public async Task Timestamps_DoNotAffectOrdering() { // Arrange - Insert tokens at "different" times (same logical batch) var tokens = Enumerable.Range(1, 10) .Select(i => $"seq-timestamp-{i:D3}") .ToList(); // Insert in reverse order foreach (var token in tokens.AsEnumerable().Reverse()) { await _idempotencyStore.MarkSeenAsync(token, CancellationToken.None); } // Act - Query in original order var results = new List<(string Token, bool Seen)>(); foreach (var token in tokens) { results.Add((token, await _idempotencyStore.HasSeenAsync(token, CancellationToken.None))); } // Assert - All should be seen regardless of insertion order results.Should().AllSatisfy(r => r.Seen.Should().BeTrue()); } #endregion #region Cross-Tenant Isolation with Determinism [Fact] public async Task CrossTenant_QueriesRemainIsolated() { // Arrange - Create tokens that could collide without tenant isolation var baseToken = "seq-shared-token"; var tenant1Token = $"tenant1:{baseToken}"; var tenant2Token = $"tenant2:{baseToken}"; await _idempotencyStore.MarkSeenAsync(tenant1Token, CancellationToken.None); // Act var tenant1Seen = await _idempotencyStore.HasSeenAsync(tenant1Token, CancellationToken.None); var tenant2Seen = await _idempotencyStore.HasSeenAsync(tenant2Token, CancellationToken.None); // Assert tenant1Seen.Should().BeTrue("Tenant 1 token was marked as seen"); tenant2Seen.Should().BeFalse("Tenant 2 token was not marked as seen"); } #endregion #region Helpers private static string ComputeHash(string input) { var bytes = Encoding.UTF8.GetBytes(input); var hash = SHA256.HashData(bytes); return Convert.ToHexString(hash).ToLowerInvariant(); } #endregion }