using System; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Graph.Indexer.Ingestion.Policy; using StellaOps.Graph.Indexer.Ingestion.Sbom; using Xunit; namespace StellaOps.Graph.Indexer.Tests; public sealed class PolicyOverlayProcessorTests { [Fact] public async Task ProcessAsync_persists_overlay_and_records_success_metrics() { var snapshot = CreateSnapshot(); var transformer = new PolicyOverlayTransformer(); var writer = new CaptureWriter(); var metrics = new CaptureMetrics(); var processor = new PolicyOverlayProcessor( transformer, writer, metrics, NullLogger.Instance); await processor.ProcessAsync(snapshot, CancellationToken.None); writer.LastBatch.Should().NotBeNull(); metrics.LastRecord.Should().NotBeNull(); metrics.LastRecord!.Success.Should().BeTrue(); metrics.LastRecord.NodeCount.Should().Be(writer.LastBatch!.Nodes.Length); metrics.LastRecord.EdgeCount.Should().Be(writer.LastBatch!.Edges.Length); } [Fact] public async Task ProcessAsync_records_failure_when_writer_throws() { var snapshot = CreateSnapshot(); var transformer = new PolicyOverlayTransformer(); var writer = new CaptureWriter(shouldThrow: true); var metrics = new CaptureMetrics(); var processor = new PolicyOverlayProcessor( transformer, writer, metrics, NullLogger.Instance); var act = () => processor.ProcessAsync(snapshot, CancellationToken.None); await act.Should().ThrowAsync(); metrics.LastRecord.Should().NotBeNull(); metrics.LastRecord!.Success.Should().BeFalse(); } private static PolicyOverlaySnapshot CreateSnapshot() { return new PolicyOverlaySnapshot { Tenant = "tenant-alpha", Source = "policy.engine.v1", CollectedAt = DateTimeOffset.Parse("2025-10-30T12:07:00Z"), EventOffset = 4200, Policy = new PolicyVersionDetails { Source = "policy.engine.v1", PolicyPackDigest = "sha256:fff666", PolicyName = "Default Runtime Policy", EffectiveFrom = DateTimeOffset.Parse("2025-10-28T00:00:00Z"), ExpiresAt = DateTimeOffset.Parse("2026-01-01T00:00:00Z"), ExplainHash = "sha256:explain001", CollectedAt = DateTimeOffset.Parse("2025-10-28T00:00:05Z"), EventOffset = 4100 }, Evaluations = new[] { new PolicyEvaluation { ComponentPurl = "pkg:nuget/Newtonsoft.Json@13.0.3", ComponentSourceType = "inventory", FindingExplainHash = "sha256:explain001", ExplainHash = "sha256:explain001", PolicyRuleId = "rule:runtime/critical-dependency", Verdict = "fail", EvaluationTimestamp = DateTimeOffset.Parse("2025-10-30T12:07:00Z"), SbomDigest = "sha256:sbom111", Source = "policy.engine.v1", CollectedAt = DateTimeOffset.Parse("2025-10-30T12:07:00Z"), EventOffset = 4200 } } }; } private sealed class CaptureWriter : IGraphDocumentWriter { private readonly bool _shouldThrow; public CaptureWriter(bool shouldThrow = false) { _shouldThrow = shouldThrow; } public GraphBuildBatch? LastBatch { get; private set; } public Task WriteAsync(GraphBuildBatch batch, CancellationToken cancellationToken) { LastBatch = batch; if (_shouldThrow) { throw new InvalidOperationException("Simulated persistence failure"); } return Task.CompletedTask; } } private sealed class CaptureMetrics : IPolicyOverlayMetrics { public MetricRecord? LastRecord { get; private set; } public void RecordBatch(string source, string tenant, int nodeCount, int edgeCount, TimeSpan duration, bool success) { LastRecord = new MetricRecord(source, tenant, nodeCount, edgeCount, duration, success); } } private sealed record MetricRecord( string Source, string Tenant, int NodeCount, int EdgeCount, TimeSpan Duration, bool Success); }