using Xunit; using System.Text.Json; using System.Threading.Tasks; using StellaOps.Policy.Engine.Overlay; using StellaOps.Policy.Engine.Tests.Fakes; using StellaOps.Policy.Engine.Services; using StellaOps.Policy.Engine.Streaming; using StellaOps.TestKit; namespace StellaOps.Policy.Engine.Tests; public sealed class PathScopeSimulationBridgeServiceTests { private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); [Trait("Category", TestCategories.Unit)] [Fact] public async Task SimulateAsync_OrdersByInputAndProducesMetrics() { var bridge = CreateBridge(); var request = new PathScopeSimulationBridgeRequest( Tenant: "acme", Rules: Array.Empty(), Overlays: null, Paths: new[] { BuildPathRequest("b/file.js"), BuildPathRequest("a/file.js") }, Mode: "preview", Seed: 123); var result = await bridge.SimulateAsync(request); Assert.Equal(2, result.Decisions.Count); Assert.Contains("\"filePath\":\"b/file.js\"", JsonSerializer.Serialize(result.Decisions[0].PathScope, SerializerOptions)); Assert.Contains("\"filePath\":\"a/file.js\"", JsonSerializer.Serialize(result.Decisions[1].PathScope, SerializerOptions)); Assert.Equal(2, result.Metrics.Evaluated); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task SimulateAsync_WhatIfProducesDeltas() { var bridge = CreateBridge(); var request = new PathScopeSimulationBridgeRequest( Tenant: "acme", Rules: Array.Empty(), Overlays: null, Paths: new[] { BuildPathRequest("a/file.js") }, Mode: "whatif", Seed: 42); var result = await bridge.SimulateAsync(request); Assert.Single(result.Decisions); Assert.NotNull(result.Deltas); Assert.Single(result.Deltas!); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task SimulateAsync_PublishesEventsAndSavesOverlays() { var sink = new FakeOverlayEventSink(); var store = new FakeOverlayStore(); var bridge = CreateBridge(sink, store); var request = new PathScopeSimulationBridgeRequest( Tenant: "acme", Rules: Array.Empty(), Overlays: null, Paths: new[] { BuildPathRequest("a/file.js") }, Mode: "whatif", Seed: 99); await bridge.SimulateAsync(request); Assert.NotEmpty(sink.Published); Assert.NotEmpty(store.Saved); } private static PathScopeSimulationBridgeService CreateBridge( IOverlayEventSink? sink = null, IOverlayStore? store = null) { sink ??= new FakeOverlayEventSink(); store ??= new FakeOverlayStore(); var overlayService = new OverlayProjectionService(new PolicyEvaluationService(), TimeProvider.System); var publisher = new OverlayChangeEventPublisher(sink, TimeProvider.System); return new PathScopeSimulationBridgeService(overlayService, publisher, store); } private static PathScopeSimulationRequest BuildPathRequest(string filePath) => new( SchemaVersion: "1.0.0", Tenant: "acme", BasePolicyRef: "policy://acme/main@sha256:1", CandidatePolicyRef: "policy://acme/feat@sha256:2", Subject: new PathScopeSubject("pkg:npm/lodash@4.17.21", null, "lib.js", null), Targets: new[] { new PathScopeTarget(filePath, "pat", "prefix", 0.9, null, null, null, "e1", null, null) }, Options: new SimulationOptions("path,finding,verdict", 100, IncludeTrace: true, Deterministic: true)); }