using Xunit; using Microsoft.Extensions.Time.Testing; using StellaOps.Policy.Engine.Domain; using StellaOps.Policy.Engine.Ledger; using StellaOps.Policy.Engine.Orchestration; using StellaOps.Policy.Engine.Services; using StellaOps.Policy.Engine.Snapshots; using StellaOps.Policy.Engine.TrustWeighting; using StellaOps.Policy.Engine.Violations; using StellaOps.TestKit; namespace StellaOps.Policy.Engine.Tests; public sealed class PolicyDecisionServiceTests { private static (PolicyDecisionService service, string snapshotId) BuildService() { var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-27T10:00:00Z")); var jobStore = new InMemoryOrchestratorJobStore(); var resultStore = new InMemoryWorkerResultStore(jobStore); var exportStore = new InMemoryLedgerExportStore(); var ledger = new LedgerExportService(clock, jobStore, resultStore, exportStore); var snapshotStore = new InMemorySnapshotStore(); var violationStore = new InMemoryViolationEventStore(); var trust = new TrustWeightingService(clock); var snapshotService = new SnapshotService(clock, ledger, snapshotStore); var eventService = new ViolationEventService(snapshotStore, jobStore, violationStore); var fusionService = new SeverityFusionService(violationStore, trust); var conflictService = new ConflictHandlingService(violationStore); var evidenceService = new EvidenceSummaryService(clock); var decisionService = new PolicyDecisionService( eventService, fusionService, conflictService, evidenceService); // Setup test data var job = new OrchestratorJob( JobId: "job-decision-test", TenantId: "acme", ContextId: "ctx", PolicyProfileHash: "hash", RequestedAt: clock.GetUtcNow(), Priority: "normal", BatchItems: new[] { new OrchestratorJobItem("pkg:npm/lodash@4.17.21", "CVE-2021-23337"), new OrchestratorJobItem("pkg:npm/axios@0.21.1", "CVE-2021-3749"), new OrchestratorJobItem("pkg:maven/log4j@2.14.1", "CVE-2021-44228") }, Callbacks: null, TraceRef: "trace-decision", Status: "completed", DeterminismHash: "hash", CompletedAt: clock.GetUtcNow(), ResultHash: "res"); jobStore.SaveAsync(job).GetAwaiter().GetResult(); resultStore.SaveAsync(new WorkerRunResult( job.JobId, "worker-decision", clock.GetUtcNow(), clock.GetUtcNow(), new[] { new WorkerResultItem("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "violation", "trace-lodash"), new WorkerResultItem("pkg:npm/axios@0.21.1", "CVE-2021-3749", "warn", "trace-axios"), new WorkerResultItem("pkg:maven/log4j@2.14.1", "CVE-2021-44228", "violation", "trace-log4j") }, "hash")).GetAwaiter().GetResult(); ledger.BuildAsync(new LedgerExportRequest("acme")).GetAwaiter().GetResult(); var snapshot = snapshotService.CreateAsync(new SnapshotRequest("acme", "overlay-decision")).GetAwaiter().GetResult(); return (decisionService, snapshot.SnapshotId); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetDecisionsAsync_ReturnsDecisionsWithEvidence() { var (service, snapshotId) = BuildService(); var request = new PolicyDecisionRequest(snapshotId); var response = await service.GetDecisionsAsync(request); Assert.Equal(snapshotId, response.SnapshotId); Assert.Equal(3, response.Decisions.Count); Assert.All(response.Decisions, d => { Assert.False(string.IsNullOrWhiteSpace(d.SeverityFused)); Assert.NotNull(d.Evidence); Assert.NotNull(d.TopSources); Assert.True(d.TopSources.Count > 0); }); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetDecisionsAsync_BuildsSummaryStatistics() { var (service, snapshotId) = BuildService(); var request = new PolicyDecisionRequest(snapshotId); var response = await service.GetDecisionsAsync(request); Assert.Equal(3, response.Summary.TotalDecisions); Assert.NotEmpty(response.Summary.SeverityCounts); Assert.NotEmpty(response.Summary.TopSeveritySources); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetDecisionsAsync_FiltersById() { var (service, snapshotId) = BuildService(); var request = new PolicyDecisionRequest( SnapshotId: snapshotId, AdvisoryId: "CVE-2021-44228"); var response = await service.GetDecisionsAsync(request); Assert.Single(response.Decisions); Assert.Equal("CVE-2021-44228", response.Decisions[0].AdvisoryId); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetDecisionsAsync_FiltersByTenant() { var (service, snapshotId) = BuildService(); var request = new PolicyDecisionRequest( SnapshotId: snapshotId, TenantId: "acme"); var response = await service.GetDecisionsAsync(request); Assert.All(response.Decisions, d => Assert.Equal("acme", d.TenantId)); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetDecisionsAsync_LimitsTopSources() { var (service, snapshotId) = BuildService(); var request = new PolicyDecisionRequest( SnapshotId: snapshotId, MaxSources: 1); var response = await service.GetDecisionsAsync(request); Assert.All(response.Decisions, d => { Assert.True(d.TopSources.Count <= 1); }); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetDecisionsAsync_ExcludesEvidenceWhenNotRequested() { var (service, snapshotId) = BuildService(); var request = new PolicyDecisionRequest( SnapshotId: snapshotId, IncludeEvidence: false); var response = await service.GetDecisionsAsync(request); Assert.All(response.Decisions, d => Assert.Null(d.Evidence)); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetDecisionsAsync_ReturnsDeterministicOrder() { var (service, snapshotId) = BuildService(); var request = new PolicyDecisionRequest(snapshotId); var response1 = await service.GetDecisionsAsync(request); var response2 = await service.GetDecisionsAsync(request); Assert.Equal( response1.Decisions.Select(d => d.ComponentPurl), response2.Decisions.Select(d => d.ComponentPurl)); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetDecisionsAsync_ThrowsOnEmptySnapshotId() { var (service, _) = BuildService(); var request = new PolicyDecisionRequest(string.Empty); await Assert.ThrowsAsync(() => service.GetDecisionsAsync(request)); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetDecisionsAsync_TopSourcesHaveRanks() { var (service, snapshotId) = BuildService(); var request = new PolicyDecisionRequest(snapshotId); var response = await service.GetDecisionsAsync(request); foreach (var decision in response.Decisions) { for (var i = 0; i < decision.TopSources.Count; i++) { Assert.Equal(i + 1, decision.TopSources[i].Rank); } } } }