product advisories, stella router improval, tests streghthening
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user