sprints work

This commit is contained in:
master
2026-01-10 20:32:13 +02:00
parent 0d5eda86fc
commit 17d0631b8e
189 changed files with 40667 additions and 497 deletions

View File

@@ -0,0 +1,599 @@
// <copyright file="ReachabilityIndexIntegrationTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace StellaOps.Reachability.Core.Tests;
/// <summary>
/// Integration tests for <see cref="ReachabilityIndex"/> verifying end-to-end
/// behavior with mock adapters that simulate real data sources.
/// </summary>
[Trait("Category", "Integration")]
public sealed class ReachabilityIndexIntegrationTests : IDisposable
{
private readonly ServiceProvider _serviceProvider;
private readonly IReachabilityIndex _reachabilityIndex;
private readonly MockReachGraphAdapter _reachGraphAdapter;
private readonly MockSignalsAdapter _signalsAdapter;
private readonly FakeTimeProvider _timeProvider;
public ReachabilityIndexIntegrationTests()
{
_reachGraphAdapter = new MockReachGraphAdapter();
_signalsAdapter = new MockSignalsAdapter();
_timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2026-01-10T10:00:00Z"));
var services = new ServiceCollection();
services.AddSingleton<IReachGraphAdapter>(_reachGraphAdapter);
services.AddSingleton<ISignalsAdapter>(_signalsAdapter);
services.AddSingleton<TimeProvider>(_timeProvider);
services.AddSingleton<IReachabilityIndex, ReachabilityIndex>();
_serviceProvider = services.BuildServiceProvider();
_reachabilityIndex = _serviceProvider.GetRequiredService<IReachabilityIndex>();
}
public void Dispose()
{
_serviceProvider.Dispose();
}
// -------------------------------------------------------------------------
// Static Query Tests
// -------------------------------------------------------------------------
[Fact]
public async Task QueryStaticAsync_ReturnsReachableResult_WhenSymbolInGraph()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/lodash@4.17.21", "lodash.merge", "JavaScript");
var artifactDigest = "sha256:abc123";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
pathCount: 2,
shortestPath: 3,
entrypoints: ["server.handleRequest", "api.process"]);
// Act
var result = await _reachabilityIndex.QueryStaticAsync(symbol, artifactDigest, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.IsReachable.Should().BeTrue();
result.PathCount.Should().Be(2);
result.ShortestPathLength.Should().Be(3);
result.Entrypoints.Should().HaveCount(2);
}
[Fact]
public async Task QueryStaticAsync_ReturnsUnreachable_WhenSymbolNotInGraph()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/unused@1.0.0", "unused.func", "JavaScript");
var artifactDigest = "sha256:abc123";
_reachGraphAdapter.SetupUnreachableSymbol(symbol, artifactDigest);
// Act
var result = await _reachabilityIndex.QueryStaticAsync(symbol, artifactDigest, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.IsReachable.Should().BeFalse();
result.PathCount.Should().Be(0);
}
// -------------------------------------------------------------------------
// Runtime Query Tests
// -------------------------------------------------------------------------
[Fact]
public async Task QueryRuntimeAsync_ReturnsObserved_WhenSignalExists()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/express@4.18.0", "express.Router", "JavaScript");
var artifactDigest = "sha256:def456";
var observationWindow = TimeSpan.FromDays(7);
_signalsAdapter.SetupObservedSymbol(symbol, artifactDigest,
hitCount: 42,
firstSeen: _timeProvider.GetUtcNow().AddDays(-5),
lastSeen: _timeProvider.GetUtcNow().AddMinutes(-30));
// Act
var result = await _reachabilityIndex.QueryRuntimeAsync(
symbol, artifactDigest, observationWindow, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.WasObserved.Should().BeTrue();
result.HitCount.Should().Be(42);
}
[Fact]
public async Task QueryRuntimeAsync_ReturnsNotObserved_WhenNoSignals()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/dead-code@1.0.0", "deadCode.func", "JavaScript");
var artifactDigest = "sha256:def456";
var observationWindow = TimeSpan.FromDays(7);
_signalsAdapter.SetupNotObservedSymbol(symbol, artifactDigest);
// Act
var result = await _reachabilityIndex.QueryRuntimeAsync(
symbol, artifactDigest, observationWindow, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.WasObserved.Should().BeFalse();
}
// -------------------------------------------------------------------------
// Hybrid Query Tests
// -------------------------------------------------------------------------
[Fact]
public async Task QueryHybridAsync_CombinesStaticAndRuntime()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/active@1.0.0", "active.process", "JavaScript");
var artifactDigest = "sha256:hybrid123";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
pathCount: 1, shortestPath: 2, entrypoints: ["main"]);
_signalsAdapter.SetupObservedSymbol(symbol, artifactDigest,
hitCount: 100,
firstSeen: _timeProvider.GetUtcNow().AddDays(-7),
lastSeen: _timeProvider.GetUtcNow().AddMinutes(-5));
var options = new HybridQueryOptions
{
IncludeStatic = true,
IncludeRuntime = true,
ObservationWindow = TimeSpan.FromDays(7)
};
// Act
var result = await _reachabilityIndex.QueryHybridAsync(
symbol, artifactDigest, options, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.LatticeState.Should().Be(LatticeState.RuntimeObserved);
result.StaticResult.Should().NotBeNull();
result.RuntimeResult.Should().NotBeNull();
result.Confidence.Should().BeGreaterOrEqualTo(0.8);
result.Verdict.VexStatus.Should().Be("affected");
}
[Fact]
public async Task QueryHybridAsync_StaticReachableButNotObserved_ReturnsStaticReachable()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/potential@1.0.0", "potential.risk", "JavaScript");
var artifactDigest = "sha256:hybrid456";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
pathCount: 1, shortestPath: 2, entrypoints: ["startup"]);
_signalsAdapter.SetupNotObservedSymbol(symbol, artifactDigest);
var options = new HybridQueryOptions
{
IncludeStatic = true,
IncludeRuntime = true,
ObservationWindow = TimeSpan.FromDays(7)
};
// Act
var result = await _reachabilityIndex.QueryHybridAsync(
symbol, artifactDigest, options, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.LatticeState.Should().Be(LatticeState.StaticReachable);
result.StaticResult!.IsReachable.Should().BeTrue();
result.RuntimeResult!.WasObserved.Should().BeFalse();
}
[Fact]
public async Task QueryHybridAsync_NotReachableAndNotObserved_ReturnsNotAffected()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/safe@1.0.0", "safe.unused", "JavaScript");
var artifactDigest = "sha256:safe789";
_reachGraphAdapter.SetupUnreachableSymbol(symbol, artifactDigest);
_signalsAdapter.SetupNotObservedSymbol(symbol, artifactDigest);
var options = new HybridQueryOptions
{
IncludeStatic = true,
IncludeRuntime = true,
ObservationWindow = TimeSpan.FromDays(30)
};
// Act
var result = await _reachabilityIndex.QueryHybridAsync(
symbol, artifactDigest, options, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.LatticeState.Should().BeOneOf(
LatticeState.StaticUnreachable,
LatticeState.RuntimeNotObserved);
result.Verdict.VexStatus.Should().Be("not_affected");
}
[Fact]
public async Task QueryHybridAsync_StaticOnlyMode()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/static-only@1.0.0", "staticOnly.func", "JavaScript");
var artifactDigest = "sha256:static";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
pathCount: 1, shortestPath: 1, entrypoints: ["app"]);
var options = new HybridQueryOptions
{
IncludeStatic = true,
IncludeRuntime = false,
ObservationWindow = TimeSpan.Zero
};
// Act
var result = await _reachabilityIndex.QueryHybridAsync(
symbol, artifactDigest, options, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.StaticResult.Should().NotBeNull();
result.RuntimeResult.Should().BeNull();
result.LatticeState.Should().Be(LatticeState.StaticReachable);
}
[Fact]
public async Task QueryHybridAsync_RuntimeOnlyMode()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/runtime-only@1.0.0", "runtimeOnly.func", "JavaScript");
var artifactDigest = "sha256:runtime";
_signalsAdapter.SetupObservedSymbol(symbol, artifactDigest,
hitCount: 50,
firstSeen: _timeProvider.GetUtcNow().AddHours(-1),
lastSeen: _timeProvider.GetUtcNow());
var options = new HybridQueryOptions
{
IncludeStatic = false,
IncludeRuntime = true,
ObservationWindow = TimeSpan.FromHours(24)
};
// Act
var result = await _reachabilityIndex.QueryHybridAsync(
symbol, artifactDigest, options, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.StaticResult.Should().BeNull();
result.RuntimeResult.Should().NotBeNull();
result.LatticeState.Should().Be(LatticeState.RuntimeObserved);
}
// -------------------------------------------------------------------------
// Evidence URI Integration Tests
// -------------------------------------------------------------------------
[Fact]
public async Task QueryHybridAsync_GeneratesValidEvidenceBundle()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/uri-test@1.0.0", "uriTest.check", "JavaScript");
var artifactDigest = "sha256:uri123";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
pathCount: 1, shortestPath: 2, entrypoints: ["main"]);
_signalsAdapter.SetupObservedSymbol(symbol, artifactDigest,
hitCount: 5,
firstSeen: _timeProvider.GetUtcNow().AddHours(-1),
lastSeen: _timeProvider.GetUtcNow());
var options = new HybridQueryOptions
{
IncludeStatic = true,
IncludeRuntime = true,
ObservationWindow = TimeSpan.FromHours(24)
};
// Act
var result = await _reachabilityIndex.QueryHybridAsync(
symbol, artifactDigest, options, CancellationToken.None);
// Assert
result.Evidence.Should().NotBeNull();
result.Evidence.Uris.Should().NotBeEmpty();
}
// -------------------------------------------------------------------------
// Content Digest Tests (Determinism)
// -------------------------------------------------------------------------
[Fact]
public async Task QueryHybridAsync_SameInput_ProducesSameContentDigest()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/deterministic@1.0.0", "det.func", "JavaScript");
var artifactDigest = "sha256:det123";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
pathCount: 1, shortestPath: 1, entrypoints: ["entry"]);
_signalsAdapter.SetupNotObservedSymbol(symbol, artifactDigest);
var options = new HybridQueryOptions
{
IncludeStatic = true,
IncludeRuntime = true,
ObservationWindow = TimeSpan.FromDays(1)
};
// Act
var result1 = await _reachabilityIndex.QueryHybridAsync(
symbol, artifactDigest, options, CancellationToken.None);
var result2 = await _reachabilityIndex.QueryHybridAsync(
symbol, artifactDigest, options, CancellationToken.None);
// Assert
result1.ContentDigest.Should().Be(result2.ContentDigest);
result1.ContentDigest.Should().StartWith("sha256:");
}
// -------------------------------------------------------------------------
// Cancellation Tests
// -------------------------------------------------------------------------
[Fact]
public async Task QueryHybridAsync_ThrowsOnCancellation()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/cancel@1.0.0", "cancel.func", "JavaScript");
var artifactDigest = "sha256:cancel";
var cts = new CancellationTokenSource();
await cts.CancelAsync();
var options = new HybridQueryOptions
{
IncludeStatic = true,
IncludeRuntime = true,
ObservationWindow = TimeSpan.FromHours(1)
};
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(() =>
_reachabilityIndex.QueryHybridAsync(symbol, artifactDigest, options, cts.Token));
}
// -------------------------------------------------------------------------
// Edge Cases
// -------------------------------------------------------------------------
[Fact]
public async Task QueryStaticAsync_HandlesSpecialCharactersInSymbol()
{
// Arrange
var symbol = new SymbolRef(
"pkg:npm/%40scope%2Fpackage@1.0.0",
"SomeClass<T>.Method(string, int)",
"CSharp");
var artifactDigest = "sha256:special";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
pathCount: 1, shortestPath: 1, entrypoints: ["entry"]);
// Act
var result = await _reachabilityIndex.QueryStaticAsync(symbol, artifactDigest, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.IsReachable.Should().BeTrue();
}
[Fact]
public async Task QueryHybridAsync_HandlesEmptyOptions()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/empty@1.0.0", "empty.func", "JavaScript");
var artifactDigest = "sha256:empty";
var options = new HybridQueryOptions
{
IncludeStatic = false,
IncludeRuntime = false,
ObservationWindow = TimeSpan.Zero
};
// Act
var result = await _reachabilityIndex.QueryHybridAsync(
symbol, artifactDigest, options, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.LatticeState.Should().Be(LatticeState.Unknown);
result.StaticResult.Should().BeNull();
result.RuntimeResult.Should().BeNull();
}
}
// -------------------------------------------------------------------------
// Mock Adapters
// -------------------------------------------------------------------------
/// <summary>
/// Mock implementation of <see cref="IReachGraphAdapter"/> for testing.
/// </summary>
internal sealed class MockReachGraphAdapter : IReachGraphAdapter
{
private readonly Dictionary<string, StaticReachabilityResult> _results = new();
private readonly FakeTimeProvider _timeProvider = new(DateTimeOffset.UtcNow);
public void SetupReachableSymbol(
SymbolRef symbol,
string artifactDigest,
int pathCount,
int shortestPath,
string[] entrypoints)
{
var key = MakeKey(symbol, artifactDigest);
_results[key] = new StaticReachabilityResult
{
Symbol = symbol,
ArtifactDigest = artifactDigest,
IsReachable = true,
PathCount = pathCount,
ShortestPathLength = shortestPath,
Entrypoints = entrypoints.ToImmutableArray(),
AnalyzedAt = _timeProvider.GetUtcNow()
};
}
public void SetupUnreachableSymbol(SymbolRef symbol, string artifactDigest)
{
var key = MakeKey(symbol, artifactDigest);
_results[key] = new StaticReachabilityResult
{
Symbol = symbol,
ArtifactDigest = artifactDigest,
IsReachable = false,
PathCount = 0,
AnalyzedAt = _timeProvider.GetUtcNow()
};
}
public Task<StaticReachabilityResult> QueryAsync(SymbolRef symbol, string artifactDigest, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var key = MakeKey(symbol, artifactDigest);
if (_results.TryGetValue(key, out var result))
{
return Task.FromResult(result);
}
return Task.FromResult(new StaticReachabilityResult
{
Symbol = symbol,
ArtifactDigest = artifactDigest,
IsReachable = false,
PathCount = 0,
AnalyzedAt = _timeProvider.GetUtcNow()
});
}
private static string MakeKey(SymbolRef symbol, string artifactDigest)
=> $"{symbol.Purl}:{symbol.Symbol}:{artifactDigest}";
}
/// <summary>
/// Mock implementation of <see cref="ISignalsAdapter"/> for testing.
/// </summary>
internal sealed class MockSignalsAdapter : ISignalsAdapter
{
private readonly Dictionary<string, (long hitCount, DateTimeOffset firstSeen, DateTimeOffset lastSeen)> _observations = new();
private readonly FakeTimeProvider _timeProvider = new(DateTimeOffset.UtcNow);
public void SetupObservedSymbol(
SymbolRef symbol,
string artifactDigest,
long hitCount,
DateTimeOffset firstSeen,
DateTimeOffset lastSeen)
{
var key = MakeKey(symbol, artifactDigest);
_observations[key] = (hitCount, firstSeen, lastSeen);
}
public void SetupNotObservedSymbol(SymbolRef symbol, string artifactDigest)
{
// Don't add to dictionary - will return not observed
}
public Task<RuntimeReachabilityResult> QueryAsync(
SymbolRef symbol,
string artifactDigest,
TimeSpan observationWindow,
string? tenantId,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var key = MakeKey(symbol, artifactDigest);
var now = _timeProvider.GetUtcNow();
var windowStart = now - observationWindow;
if (_observations.TryGetValue(key, out var obs) && obs.lastSeen >= windowStart)
{
return Task.FromResult(new RuntimeReachabilityResult
{
Symbol = symbol,
ArtifactDigest = artifactDigest,
WasObserved = true,
ObservationWindow = observationWindow,
WindowStart = windowStart,
WindowEnd = now,
HitCount = obs.hitCount,
FirstSeen = obs.firstSeen,
LastSeen = obs.lastSeen
});
}
return Task.FromResult(new RuntimeReachabilityResult
{
Symbol = symbol,
ArtifactDigest = artifactDigest,
WasObserved = false,
ObservationWindow = observationWindow,
WindowStart = windowStart,
WindowEnd = now,
HitCount = 0
});
}
private static string MakeKey(SymbolRef symbol, string artifactDigest)
=> $"{symbol.Purl}:{symbol.Symbol}:{artifactDigest}";
}
/// <summary>
/// Fake <see cref="TimeProvider"/> for deterministic time in tests.
/// </summary>
internal sealed class FakeTimeProvider : TimeProvider
{
private DateTimeOffset _now;
public FakeTimeProvider(DateTimeOffset initialTime)
{
_now = initialTime;
}
public override DateTimeOffset GetUtcNow() => _now;
public void Advance(TimeSpan duration)
{
_now = _now.Add(duration);
}
public void SetTime(DateTimeOffset time)
{
_now = time;
}
}

View File

@@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>
<ItemGroup>