up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-14 15:50:38 +02:00
parent f1a39c4ce3
commit 233873f620
249 changed files with 29746 additions and 154 deletions

View File

@@ -0,0 +1,246 @@
using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Signals.Options;
using StellaOps.Signals.Services;
using Xunit;
namespace StellaOps.Signals.Tests;
public class EdgeBundleIngestionServiceTests
{
private readonly EdgeBundleIngestionService _service;
private const string TestTenantId = "test-tenant";
private const string TestGraphHash = "blake3:abc123def456";
public EdgeBundleIngestionServiceTests()
{
var opts = new SignalsOptions();
opts.Storage.RootPath = Path.GetTempPath();
var options = Microsoft.Extensions.Options.Options.Create(opts);
_service = new EdgeBundleIngestionService(NullLogger<EdgeBundleIngestionService>.Instance, options);
}
[Fact]
public async Task IngestAsync_ParsesBundleAndStoresDocument()
{
// Arrange
var bundle = new
{
schema = "edge-bundle-v1",
bundleId = "bundle:test123",
graphHash = TestGraphHash,
bundleReason = "RuntimeHits",
generatedAt = DateTimeOffset.UtcNow.ToString("O"),
edges = new[]
{
new { from = "func_a", to = "func_b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.9 },
new { from = "func_b", to = "func_c", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.8 }
}
};
var bundleJson = JsonSerializer.Serialize(bundle);
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(bundleJson));
// Act
var result = await _service.IngestAsync(TestTenantId, stream, null);
// Assert
Assert.Equal("bundle:test123", result.BundleId);
Assert.Equal(TestGraphHash, result.GraphHash);
Assert.Equal("RuntimeHits", result.BundleReason);
Assert.Equal(2, result.EdgeCount);
Assert.Equal(0, result.RevokedCount);
Assert.False(result.Quarantined);
Assert.Contains("cas://reachability/edges/", result.CasUri);
}
[Fact]
public async Task IngestAsync_TracksRevokedEdgesForQuarantine()
{
// Arrange
var bundle = new
{
schema = "edge-bundle-v1",
bundleId = "bundle:revoked123",
graphHash = TestGraphHash,
bundleReason = "Revoked",
edges = new[]
{
new { from = "func_a", to = "func_b", kind = "call", reason = "Revoked", revoked = true, confidence = 1.0 },
new { from = "func_b", to = "func_c", kind = "call", reason = "TargetRemoved", revoked = true, confidence = 1.0 },
new { from = "func_c", to = "func_d", kind = "call", reason = "LowConfidence", revoked = false, confidence = 0.3 }
}
};
var bundleJson = JsonSerializer.Serialize(bundle);
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(bundleJson));
// Act
var result = await _service.IngestAsync(TestTenantId, stream, null);
// Assert
Assert.Equal(3, result.EdgeCount);
Assert.Equal(2, result.RevokedCount);
Assert.True(result.Quarantined);
}
[Fact]
public async Task IsEdgeRevokedAsync_ReturnsTrueForRevokedEdges()
{
// Arrange
var bundle = new
{
bundleId = "bundle:revoke-check",
graphHash = TestGraphHash,
bundleReason = "Revoked",
edges = new[]
{
new { from = "vuln_func", to = "patched_func", kind = "call", reason = "Revoked", revoked = true, confidence = 1.0 }
}
};
var bundleJson = JsonSerializer.Serialize(bundle);
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(bundleJson));
await _service.IngestAsync(TestTenantId, stream, null);
// Act & Assert
Assert.True(await _service.IsEdgeRevokedAsync(TestTenantId, TestGraphHash, "vuln_func", "patched_func"));
Assert.False(await _service.IsEdgeRevokedAsync(TestTenantId, TestGraphHash, "other_func", "some_func"));
}
[Fact]
public async Task GetBundlesForGraphAsync_ReturnsAllBundlesForGraph()
{
// Arrange - ingest multiple bundles
var graphHash = $"blake3:graph_{Guid.NewGuid():N}";
var bundle1 = new { bundleId = "bundle:1", graphHash, bundleReason = "RuntimeHits", edges = new[] { new { from = "a", to = "b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 1.0 } } };
var bundle2 = new { bundleId = "bundle:2", graphHash, bundleReason = "InitArray", edges = new[] { new { from = "c", to = "d", kind = "call", reason = "InitArray", revoked = false, confidence = 1.0 } } };
using var stream1 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle1)));
using var stream2 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle2)));
await _service.IngestAsync(TestTenantId, stream1, null);
await _service.IngestAsync(TestTenantId, stream2, null);
// Act
var bundles = await _service.GetBundlesForGraphAsync(TestTenantId, graphHash);
// Assert
Assert.Equal(2, bundles.Length);
Assert.Contains(bundles, b => b.BundleId == "bundle:1");
Assert.Contains(bundles, b => b.BundleId == "bundle:2");
}
[Fact]
public async Task GetRevokedEdgesAsync_ReturnsOnlyRevokedEdges()
{
// Arrange
var graphHash = $"blake3:revoked_graph_{Guid.NewGuid():N}";
var bundle = new
{
bundleId = "bundle:mixed",
graphHash,
bundleReason = "Revoked",
edges = new[]
{
new { from = "a", to = "b", kind = "call", reason = "Revoked", revoked = true, confidence = 1.0 },
new { from = "c", to = "d", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.9 },
new { from = "e", to = "f", kind = "call", reason = "Revoked", revoked = true, confidence = 1.0 }
}
};
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle)));
await _service.IngestAsync(TestTenantId, stream, null);
// Act
var revokedEdges = await _service.GetRevokedEdgesAsync(TestTenantId, graphHash);
// Assert
Assert.Equal(2, revokedEdges.Length);
Assert.All(revokedEdges, e => Assert.True(e.Revoked));
}
[Fact]
public async Task IngestAsync_WithDsseStream_SetsVerifiedAndDsseFields()
{
// Arrange
var bundle = new
{
bundleId = "bundle:verified",
graphHash = TestGraphHash,
bundleReason = "RuntimeHits",
edges = new[] { new { from = "a", to = "b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 1.0 } }
};
var dsseEnvelope = new
{
payloadType = "application/vnd.stellaops.edgebundle.predicate+json",
payload = Convert.ToBase64String(Encoding.UTF8.GetBytes("{}")),
signatures = new[] { new { keyid = "test", sig = "abc123" } }
};
using var bundleStream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle)));
using var dsseStream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(dsseEnvelope)));
// Act
var result = await _service.IngestAsync(TestTenantId, bundleStream, dsseStream);
// Assert
Assert.NotNull(result.DsseCasUri);
Assert.EndsWith(".dsse", result.DsseCasUri);
}
[Fact]
public async Task IngestAsync_ThrowsOnMissingGraphHash()
{
// Arrange
var bundle = new
{
bundleId = "bundle:no-graph",
bundleReason = "RuntimeHits",
edges = Array.Empty<object>()
};
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle)));
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => _service.IngestAsync(TestTenantId, stream, null));
}
[Fact]
public async Task IngestAsync_UpdatesExistingBundleWithSameId()
{
// Arrange
var graphHash = $"blake3:update_test_{Guid.NewGuid():N}";
var bundle1 = new
{
bundleId = "bundle:same-id",
graphHash,
bundleReason = "RuntimeHits",
edges = new[] { new { from = "a", to = "b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.5 } }
};
var bundle2 = new
{
bundleId = "bundle:same-id",
graphHash,
bundleReason = "RuntimeHits",
edges = new[]
{
new { from = "a", to = "b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.9 },
new { from = "c", to = "d", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.8 }
}
};
using var stream1 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle1)));
using var stream2 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle2)));
// Act
await _service.IngestAsync(TestTenantId, stream1, null);
await _service.IngestAsync(TestTenantId, stream2, null);
// Assert
var bundles = await _service.GetBundlesForGraphAsync(TestTenantId, graphHash);
Assert.Single(bundles);
Assert.Equal(2, bundles[0].Edges.Count); // Updated to have 2 edges
}
}

View File

@@ -196,6 +196,18 @@ public class ReachabilityScoringServiceTests
Last = document;
return Task.FromResult(document);
}
public Task<IReadOnlyList<ReachabilityFactDocument>> GetExpiredAsync(DateTimeOffset olderThan, int limit, CancellationToken cancellationToken)
=> Task.FromResult<IReadOnlyList<ReachabilityFactDocument>>(Array.Empty<ReachabilityFactDocument>());
public Task<bool> DeleteAsync(string id, CancellationToken cancellationToken)
=> Task.FromResult(true);
public Task<int> GetRuntimeFactsCountAsync(string subjectKey, CancellationToken cancellationToken)
=> Task.FromResult(0);
public Task TrimRuntimeFactsAsync(string subjectKey, int maxRuntimeFacts, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class InMemoryReachabilityCache : IReachabilityCache

View File

@@ -0,0 +1,314 @@
using System.IO.Compression;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Cryptography;
using StellaOps.Signals.Models;
using StellaOps.Signals.Persistence;
using StellaOps.Signals.Services;
using StellaOps.Signals.Storage;
using StellaOps.Signals.Storage.Models;
using Xunit;
namespace StellaOps.Signals.Tests;
public class RuntimeFactsBatchIngestionTests
{
private const string TestTenantId = "test-tenant";
private const string TestCallgraphId = "test-callgraph-123";
[Fact]
public async Task IngestBatchAsync_ParsesNdjsonAndStoresArtifact()
{
// Arrange
var repository = new InMemoryReachabilityFactRepository();
var artifactStore = new InMemoryRuntimeFactsArtifactStore();
var cryptoHash = DefaultCryptoHash.CreateForTests();
var service = CreateService(repository, artifactStore, cryptoHash);
var events = new[]
{
new { symbolId = "func_a", hitCount = 5, callgraphId = TestCallgraphId, subject = new { scanId = "scan-1" } },
new { symbolId = "func_b", hitCount = 3, callgraphId = TestCallgraphId, subject = new { scanId = "scan-1" } },
new { symbolId = "func_c", hitCount = 1, callgraphId = TestCallgraphId, subject = new { scanId = "scan-1" } }
};
var ndjson = string.Join("\n", events.Select(e => JsonSerializer.Serialize(e)));
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(ndjson));
// Act
var result = await service.IngestBatchAsync(TestTenantId, stream, "application/x-ndjson", CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.StartsWith("cas://reachability/runtime-facts/", result.CasUri);
Assert.StartsWith("blake3:", result.BatchHash);
Assert.Equal(1, result.ProcessedCount);
Assert.Equal(3, result.TotalEvents);
Assert.Equal(9, result.TotalHitCount);
Assert.Contains("scan-1", result.SubjectKeys);
Assert.True(artifactStore.StoredArtifacts.Count > 0);
}
[Fact]
public async Task IngestBatchAsync_HandlesGzipCompressedContent()
{
// Arrange
var repository = new InMemoryReachabilityFactRepository();
var artifactStore = new InMemoryRuntimeFactsArtifactStore();
var cryptoHash = DefaultCryptoHash.CreateForTests();
var service = CreateService(repository, artifactStore, cryptoHash);
var events = new[]
{
new { symbolId = "func_gzip", hitCount = 10, callgraphId = TestCallgraphId, subject = new { scanId = "scan-gzip" } }
};
var ndjson = string.Join("\n", events.Select(e => JsonSerializer.Serialize(e)));
using var compressedStream = new MemoryStream();
await using (var gzipStream = new GZipStream(compressedStream, CompressionMode.Compress, leaveOpen: true))
{
await gzipStream.WriteAsync(Encoding.UTF8.GetBytes(ndjson));
}
compressedStream.Position = 0;
// Act
var result = await service.IngestBatchAsync(TestTenantId, compressedStream, "application/gzip", CancellationToken.None);
// Assert
Assert.Equal(1, result.ProcessedCount);
Assert.Equal(1, result.TotalEvents);
Assert.Equal(10, result.TotalHitCount);
}
[Fact]
public async Task IngestBatchAsync_GroupsEventsBySubject()
{
// Arrange
var repository = new InMemoryReachabilityFactRepository();
var artifactStore = new InMemoryRuntimeFactsArtifactStore();
var cryptoHash = DefaultCryptoHash.CreateForTests();
var service = CreateService(repository, artifactStore, cryptoHash);
var events = new[]
{
new { symbolId = "func_a", hitCount = 1, callgraphId = "cg-1", subject = new { scanId = "scan-1" } },
new { symbolId = "func_b", hitCount = 2, callgraphId = "cg-1", subject = new { scanId = "scan-1" } },
new { symbolId = "func_c", hitCount = 3, callgraphId = "cg-2", subject = new { scanId = "scan-2" } },
new { symbolId = "func_d", hitCount = 4, callgraphId = "cg-2", subject = new { scanId = "scan-2" } }
};
var ndjson = string.Join("\n", events.Select(e => JsonSerializer.Serialize(e)));
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(ndjson));
// Act
var result = await service.IngestBatchAsync(TestTenantId, stream, "application/x-ndjson", CancellationToken.None);
// Assert
Assert.Equal(2, result.ProcessedCount);
Assert.Equal(4, result.TotalEvents);
Assert.Equal(10, result.TotalHitCount);
Assert.Contains("scan-1", result.SubjectKeys);
Assert.Contains("scan-2", result.SubjectKeys);
}
[Fact]
public async Task IngestBatchAsync_LinksCasUriToFactDocument()
{
// Arrange
var repository = new InMemoryReachabilityFactRepository();
var artifactStore = new InMemoryRuntimeFactsArtifactStore();
var cryptoHash = DefaultCryptoHash.CreateForTests();
var service = CreateService(repository, artifactStore, cryptoHash);
var events = new[]
{
new { symbolId = "func_link", hitCount = 1, callgraphId = TestCallgraphId, subject = new { scanId = "scan-link" } }
};
var ndjson = string.Join("\n", events.Select(e => JsonSerializer.Serialize(e)));
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(ndjson));
// Act
var result = await service.IngestBatchAsync(TestTenantId, stream, "application/x-ndjson", CancellationToken.None);
// Assert
var fact = await repository.GetBySubjectAsync("scan-link", CancellationToken.None);
Assert.NotNull(fact);
Assert.Equal(result.CasUri, fact.RuntimeFactsBatchUri);
Assert.Equal(result.BatchHash, fact.RuntimeFactsBatchHash);
}
[Fact]
public async Task IngestBatchAsync_SkipsInvalidLines()
{
// Arrange
var repository = new InMemoryReachabilityFactRepository();
var artifactStore = new InMemoryRuntimeFactsArtifactStore();
var cryptoHash = DefaultCryptoHash.CreateForTests();
var service = CreateService(repository, artifactStore, cryptoHash);
var ndjson = """
{"symbolId": "func_valid", "hitCount": 1, "callgraphId": "cg-1", "subject": {"scanId": "scan-skip"}}
invalid json line
{"symbolId": "", "hitCount": 1, "callgraphId": "cg-1", "subject": {"scanId": "scan-skip"}}
{"symbolId": "func_valid2", "hitCount": 2, "callgraphId": "cg-1", "subject": {"scanId": "scan-skip"}}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(ndjson));
// Act
var result = await service.IngestBatchAsync(TestTenantId, stream, "application/x-ndjson", CancellationToken.None);
// Assert
Assert.Equal(1, result.ProcessedCount);
Assert.Equal(2, result.TotalEvents); // Only valid lines
Assert.Equal(3, result.TotalHitCount);
}
[Fact]
public async Task IngestBatchAsync_WorksWithoutArtifactStore()
{
// Arrange
var repository = new InMemoryReachabilityFactRepository();
var service = CreateService(repository, artifactStore: null, cryptoHash: null);
var events = new[]
{
new { symbolId = "func_no_cas", hitCount = 5, callgraphId = TestCallgraphId, subject = new { scanId = "scan-no-cas" } }
};
var ndjson = string.Join("\n", events.Select(e => JsonSerializer.Serialize(e)));
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(ndjson));
// Act
var result = await service.IngestBatchAsync(TestTenantId, stream, "application/x-ndjson", CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.StartsWith("cas://reachability/runtime-facts/", result.CasUri);
Assert.Equal(1, result.ProcessedCount);
}
private static RuntimeFactsIngestionService CreateService(
IReachabilityFactRepository repository,
IRuntimeFactsArtifactStore? artifactStore,
ICryptoHash? cryptoHash)
{
var cache = new InMemoryReachabilityCache();
var eventsPublisher = new NullEventsPublisher();
var scoringService = new StubReachabilityScoringService();
var provenanceNormalizer = new StubProvenanceNormalizer();
return new RuntimeFactsIngestionService(
repository,
TimeProvider.System,
cache,
eventsPublisher,
scoringService,
provenanceNormalizer,
NullLogger<RuntimeFactsIngestionService>.Instance,
artifactStore,
cryptoHash);
}
private sealed class InMemoryReachabilityFactRepository : IReachabilityFactRepository
{
private readonly Dictionary<string, ReachabilityFactDocument> _facts = new(StringComparer.Ordinal);
public Task<ReachabilityFactDocument> UpsertAsync(ReachabilityFactDocument document, CancellationToken cancellationToken)
{
_facts[document.SubjectKey] = document;
return Task.FromResult(document);
}
public Task<ReachabilityFactDocument?> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken)
=> Task.FromResult(_facts.TryGetValue(subjectKey, out var doc) ? doc : null);
public Task<IReadOnlyList<ReachabilityFactDocument>> GetExpiredAsync(DateTimeOffset cutoff, int limit, CancellationToken cancellationToken)
=> Task.FromResult<IReadOnlyList<ReachabilityFactDocument>>([]);
public Task<bool> DeleteAsync(string subjectKey, CancellationToken cancellationToken)
{
var removed = _facts.Remove(subjectKey);
return Task.FromResult(removed);
}
public Task<int> GetRuntimeFactsCountAsync(string subjectKey, CancellationToken cancellationToken)
=> Task.FromResult(_facts.TryGetValue(subjectKey, out var doc) ? doc.RuntimeFacts?.Count ?? 0 : 0);
public Task TrimRuntimeFactsAsync(string subjectKey, int maxCount, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class InMemoryRuntimeFactsArtifactStore : IRuntimeFactsArtifactStore
{
public Dictionary<string, StoredRuntimeFactsArtifact> StoredArtifacts { get; } = new(StringComparer.Ordinal);
public async Task<StoredRuntimeFactsArtifact> SaveAsync(RuntimeFactsArtifactSaveRequest request, Stream content, CancellationToken cancellationToken)
{
using var ms = new MemoryStream();
await content.CopyToAsync(ms, cancellationToken);
var artifact = new StoredRuntimeFactsArtifact(
Path: $"cas/reachability/runtime-facts/{request.Hash[..2]}/{request.Hash}/{request.FileName}",
Length: ms.Length,
Hash: request.Hash,
ContentType: request.ContentType,
CasUri: $"cas://reachability/runtime-facts/{request.Hash}");
StoredArtifacts[request.Hash] = artifact;
return artifact;
}
public Task<Stream?> GetAsync(string hash, CancellationToken cancellationToken)
=> Task.FromResult<Stream?>(null);
public Task<bool> ExistsAsync(string hash, CancellationToken cancellationToken)
=> Task.FromResult(StoredArtifacts.ContainsKey(hash));
public Task<bool> DeleteAsync(string hash, CancellationToken cancellationToken)
{
return Task.FromResult(StoredArtifacts.Remove(hash));
}
}
private sealed class InMemoryReachabilityCache : IReachabilityCache
{
public Task<ReachabilityFactDocument?> GetAsync(string subjectKey, CancellationToken cancellationToken)
=> Task.FromResult<ReachabilityFactDocument?>(null);
public Task SetAsync(ReachabilityFactDocument document, CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task InvalidateAsync(string subjectKey, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class NullEventsPublisher : IEventsPublisher
{
public Task PublishFactUpdatedAsync(ReachabilityFactDocument fact, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class StubReachabilityScoringService : IReachabilityScoringService
{
public Task<ReachabilityFactDocument> RecomputeAsync(ReachabilityRecomputeRequest request, CancellationToken cancellationToken)
=> Task.FromResult(new ReachabilityFactDocument { SubjectKey = request.Subject.ToSubjectKey() });
}
private sealed class StubProvenanceNormalizer : IRuntimeFactsProvenanceNormalizer
{
public ContextFacts CreateContextFacts(
IEnumerable<RuntimeFactEvent> events,
ReachabilitySubject subject,
string callgraphId,
Dictionary<string, string?>? metadata,
DateTimeOffset timestamp)
=> new();
public ProvenanceFeed NormalizeToFeed(
IEnumerable<RuntimeFactEvent> events,
ReachabilitySubject subject,
string callgraphId,
Dictionary<string, string?>? metadata,
DateTimeOffset generatedAt)
=> new() { FeedId = "test-feed", GeneratedAt = generatedAt };
}
}

View File

@@ -96,6 +96,18 @@ public class RuntimeFactsIngestionServiceTests
Last = document;
return Task.FromResult(document);
}
public Task<IReadOnlyList<ReachabilityFactDocument>> GetExpiredAsync(DateTimeOffset olderThan, int limit, CancellationToken cancellationToken)
=> Task.FromResult<IReadOnlyList<ReachabilityFactDocument>>(Array.Empty<ReachabilityFactDocument>());
public Task<bool> DeleteAsync(string id, CancellationToken cancellationToken)
=> Task.FromResult(true);
public Task<int> GetRuntimeFactsCountAsync(string subjectKey, CancellationToken cancellationToken)
=> Task.FromResult(0);
public Task TrimRuntimeFactsAsync(string subjectKey, int maxRuntimeFacts, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class InMemoryReachabilityCache : IReachabilityCache