This commit is contained in:
StellaOps Bot
2025-11-24 07:49:18 +02:00
parent bb709b643e
commit 5970f0d9bd
16 changed files with 690 additions and 14 deletions

View File

@@ -0,0 +1,14 @@
using StellaOps.Policy.Engine.Overlay;
namespace StellaOps.Policy.Engine.Tests.Fakes;
internal sealed class FakeOverlayEventSink : IOverlayEventSink
{
public List<(string Topic, string Payload)> Published { get; } = new();
public Task PublishAsync(string topic, string payload, CancellationToken cancellationToken = default)
{
Published.Add((topic, payload));
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,14 @@
using StellaOps.Policy.Engine.Overlay;
namespace StellaOps.Policy.Engine.Tests.Fakes;
internal sealed class FakeOverlayStore : IOverlayStore
{
public List<OverlayProjection> Saved { get; } = new();
public Task SaveAsync(OverlayProjection projection, CancellationToken cancellationToken = default)
{
Saved.Add(projection);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,35 @@
using System.Threading.Tasks;
using StellaOps.Policy.Engine.Overlay;
using StellaOps.Policy.Engine.Services;
using StellaOps.Policy.Engine.Streaming;
namespace StellaOps.Policy.Engine.Tests;
public sealed class OverlayProjectionServiceTests
{
[Fact]
public async Task BuildSnapshotAsync_ProducesHeaderAndSortedProjections()
{
var service = new OverlayProjectionService(new PolicyEvaluationService(), TimeProvider.System);
var request = new PathScopeSimulationRequest(
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("b/file.js", "b/", "prefix", 0.5, null, null, null, "e2", null, null),
new PathScopeTarget("a/file.js", "a/", "prefix", 0.9, null, null, null, "e1", null, null)
},
Options: new SimulationOptions("path,finding,verdict", 100, IncludeTrace: true, Deterministic: true));
var lines = await service.BuildSnapshotAsync(request);
Assert.Equal(3, lines.Count); // header + 2 projections
Assert.Contains("overlay-projection-v1", lines[0]);
Assert.Contains("\"filePath\":\"a/file.js\"", lines[1]);
Assert.Contains("\"filePath\":\"b/file.js\"", lines[2]);
}
}

View File

@@ -0,0 +1,100 @@
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;
namespace StellaOps.Policy.Engine.Tests;
public sealed class PathScopeSimulationBridgeServiceTests
{
[Fact]
public async Task SimulateAsync_OrdersByInputAndProducesMetrics()
{
var bridge = CreateBridge();
var request = new PathScopeSimulationBridgeRequest(
Tenant: "acme",
Rules: Array.Empty<string>(),
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));
Assert.Contains("\"filePath\":\"a/file.js\"", JsonSerializer.Serialize(result.Decisions[1].PathScope));
Assert.Equal(2, result.Metrics.Evaluated);
}
[Fact]
public async Task SimulateAsync_WhatIfProducesDeltas()
{
var bridge = CreateBridge();
var request = new PathScopeSimulationBridgeRequest(
Tenant: "acme",
Rules: Array.Empty<string>(),
Overlays: null,
Paths: new[] { BuildPathRequest("a/file.js") },
Mode: "whatif",
Seed: 42);
var result = await bridge.SimulateAsync(request);
Assert.Single(result.Decisions);
Assert.Single(result.Deltas);
}
[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<string>(),
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));
}