Files
git.stella-ops.org/src/Graph/StellaOps.Graph.Indexer.Storage.Postgres.Tests/GraphQueryDeterminismTests.cs

199 lines
6.8 KiB
C#

// -----------------------------------------------------------------------------
// 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;
/// <summary>
/// S1 Storage Layer Tests: Query Determinism Tests
/// Task GRAPH-5100-005: Query determinism (same input → same result ordering)
/// </summary>
[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<GraphIndexerDataSource>.Instance);
_idempotencyStore = new PostgresIdempotencyStore(dataSource, NullLogger<PostgresIdempotencyStore>.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<bool>();
var results2 = new List<bool>();
var results3 = new List<bool>();
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
}