using Xunit; using Microsoft.Extensions.Time.Testing; using StellaOps.Policy.Engine.Ledger; using StellaOps.Policy.Engine.Orchestration; 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 ViolationServicesTests { private static (ViolationEventService events, SeverityFusionService fusion, ConflictHandlingService conflicts, string snapshotId) BuildPipeline() { var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-24T17: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 job = new OrchestratorJob( JobId: "job-viol", TenantId: "acme", ContextId: "ctx", PolicyProfileHash: "hash", RequestedAt: clock.GetUtcNow(), Priority: "normal", BatchItems: new[] { new OrchestratorJobItem("pkg:a", "ADV-1"), new OrchestratorJobItem("pkg:b", "ADV-2") }, Callbacks: null, TraceRef: "trace", Status: "completed", DeterminismHash: "hash", CompletedAt: clock.GetUtcNow(), ResultHash: "res"); jobStore.SaveAsync(job).GetAwaiter().GetResult(); resultStore.SaveAsync(new WorkerRunResult( job.JobId, "worker", clock.GetUtcNow(), clock.GetUtcNow(), new[] { new WorkerResultItem("pkg:a", "ADV-1", "violation", "trace-a"), new WorkerResultItem("pkg:b", "ADV-2", "warn", "trace-b") }, "hash")).GetAwaiter().GetResult(); ledger.BuildAsync(new LedgerExportRequest("acme")).GetAwaiter().GetResult(); var snapshot = snapshotService.CreateAsync(new SnapshotRequest("acme", "overlay-1")).GetAwaiter().GetResult(); return (eventService, fusionService, conflictService, snapshot.SnapshotId); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task EmitAsync_BuildsEvents() { var (eventService, _, _, snapshotId) = BuildPipeline(); var events = await eventService.EmitAsync(new ViolationEventRequest(snapshotId)); Assert.Equal(2, events.Count); Assert.All(events, e => Assert.Equal("policy.violation.detected", e.ViolationCode)); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task FuseAsync_ProducesWeightedSeverity() { var (eventService, fusionService, _, snapshotId) = BuildPipeline(); await eventService.EmitAsync(new ViolationEventRequest(snapshotId)); var fused = await fusionService.FuseAsync(snapshotId); Assert.Equal(2, fused.Count); Assert.All(fused, f => Assert.False(string.IsNullOrWhiteSpace(f.SeverityFused))); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task ConflictsAsync_DetectsDivergentSeverities() { var (eventService, fusionService, conflictService, snapshotId) = BuildPipeline(); await eventService.EmitAsync(new ViolationEventRequest(snapshotId)); var fused = await fusionService.FuseAsync(snapshotId); var conflicts = await conflictService.ComputeAsync(snapshotId, fused); // Only triggers when severities differ; in this stub they do, so expect at least one. Assert.NotNull(conflicts); } }