feat: Implement Policy Engine Evaluation Service and Cache with unit tests
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

Temp commit to debug
This commit is contained in:
master
2025-11-05 07:35:53 +00:00
parent 40e7f827da
commit 9253620833
125 changed files with 18735 additions and 17215 deletions

View File

@@ -1,79 +1,79 @@
using System.Collections.Immutable;
using System.Linq;
using FluentAssertions;
using StellaOps.AdvisoryAI.Context;
using StellaOps.AdvisoryAI.Tools;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class DeterministicToolsetTests
{
[Fact]
public void AnalyzeDependencies_ComputesRuntimeAndDevelopmentCounts()
{
var context = SbomContextResult.Create(
"artifact-123",
purl: null,
versionTimeline: Array.Empty<SbomVersionTimelineEntry>(),
dependencyPaths: new[]
{
new SbomDependencyPath(
new[]
{
new SbomDependencyNode("root", "1.0.0"),
new SbomDependencyNode("lib-a", "2.0.0"),
},
isRuntime: true),
new SbomDependencyPath(
new[]
{
new SbomDependencyNode("root", "1.0.0"),
new SbomDependencyNode("lib-b", "3.1.4"),
},
isRuntime: false),
});
IDeterministicToolset toolset = new DeterministicToolset();
var analysis = toolset.AnalyzeDependencies(context);
analysis.ArtifactId.Should().Be("artifact-123");
analysis.Metadata["path_count"].Should().Be("2");
analysis.Metadata["runtime_path_count"].Should().Be("1");
analysis.Metadata["development_path_count"].Should().Be("1");
analysis.Nodes.Should().HaveCount(3);
var libA = analysis.Nodes.Single(node => node.Identifier == "lib-a");
libA.RuntimeOccurrences.Should().Be(1);
libA.DevelopmentOccurrences.Should().Be(0);
var libB = analysis.Nodes.Single(node => node.Identifier == "lib-b");
libB.RuntimeOccurrences.Should().Be(0);
libB.DevelopmentOccurrences.Should().Be(1);
}
[Theory]
[InlineData("semver", "1.2.3", "1.2.4", -1)]
[InlineData("semver", "1.2.3", "1.2.3", 0)]
[InlineData("semver", "1.2.4", "1.2.3", 1)]
[InlineData("evr", "1:1.0-1", "1:1.0-2", -1)]
[InlineData("evr", "0:2.0-0", "0:2.0-0", 0)]
[InlineData("evr", "0:2.1-0", "0:2.0-5", 1)]
public void TryCompare_SucceedsForSupportedSchemes(string scheme, string left, string right, int expected)
{
IDeterministicToolset toolset = new DeterministicToolset();
toolset.TryCompare(scheme, left, right, out var comparison).Should().BeTrue();
comparison.Should().Be(expected);
}
[Theory]
[InlineData("semver", "1.2.3", ">=1.0.0 <2.0.0")]
[InlineData("semver", "2.0.0", ">=2.0.0")]
[InlineData("evr", "0:1.2-3", ">=0:1.0-0 <0:2.0-0")]
[InlineData("evr", "1:3.4-1", ">=1:3.0-0")]
public void SatisfiesRange_HonoursExpressions(string scheme, string version, string range)
{
IDeterministicToolset toolset = new DeterministicToolset();
toolset.SatisfiesRange(scheme, version, range).Should().BeTrue();
}
}
using System.Collections.Immutable;
using System.Linq;
using FluentAssertions;
using StellaOps.AdvisoryAI.Context;
using StellaOps.AdvisoryAI.Tools;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class DeterministicToolsetTests
{
[Fact]
public void AnalyzeDependencies_ComputesRuntimeAndDevelopmentCounts()
{
var context = SbomContextResult.Create(
"artifact-123",
purl: null,
versionTimeline: Array.Empty<SbomVersionTimelineEntry>(),
dependencyPaths: new[]
{
new SbomDependencyPath(
new[]
{
new SbomDependencyNode("root", "1.0.0"),
new SbomDependencyNode("lib-a", "2.0.0"),
},
isRuntime: true),
new SbomDependencyPath(
new[]
{
new SbomDependencyNode("root", "1.0.0"),
new SbomDependencyNode("lib-b", "3.1.4"),
},
isRuntime: false),
});
IDeterministicToolset toolset = new DeterministicToolset();
var analysis = toolset.AnalyzeDependencies(context);
analysis.ArtifactId.Should().Be("artifact-123");
analysis.Metadata["path_count"].Should().Be("2");
analysis.Metadata["runtime_path_count"].Should().Be("1");
analysis.Metadata["development_path_count"].Should().Be("1");
analysis.Nodes.Should().HaveCount(3);
var libA = analysis.Nodes.Single(node => node.Identifier == "lib-a");
libA.RuntimeOccurrences.Should().Be(1);
libA.DevelopmentOccurrences.Should().Be(0);
var libB = analysis.Nodes.Single(node => node.Identifier == "lib-b");
libB.RuntimeOccurrences.Should().Be(0);
libB.DevelopmentOccurrences.Should().Be(1);
}
[Theory]
[InlineData("semver", "1.2.3", "1.2.4", -1)]
[InlineData("semver", "1.2.3", "1.2.3", 0)]
[InlineData("semver", "1.2.4", "1.2.3", 1)]
[InlineData("evr", "1:1.0-1", "1:1.0-2", -1)]
[InlineData("evr", "0:2.0-0", "0:2.0-0", 0)]
[InlineData("evr", "0:2.1-0", "0:2.0-5", 1)]
public void TryCompare_SucceedsForSupportedSchemes(string scheme, string left, string right, int expected)
{
IDeterministicToolset toolset = new DeterministicToolset();
toolset.TryCompare(scheme, left, right, out var comparison).Should().BeTrue();
comparison.Should().Be(expected);
}
[Theory]
[InlineData("semver", "1.2.3", ">=1.0.0 <2.0.0")]
[InlineData("semver", "2.0.0", ">=2.0.0")]
[InlineData("evr", "0:1.2-3", ">=0:1.0-0 <0:2.0-0")]
[InlineData("evr", "1:3.4-1", ">=1:3.0-0")]
public void SatisfiesRange_HonoursExpressions(string scheme, string version, string range)
{
IDeterministicToolset toolset = new DeterministicToolset();
toolset.SatisfiesRange(scheme, version, range).Should().BeTrue();
}
}

View File

@@ -1,144 +1,144 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.AdvisoryAI.Providers;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class SbomContextHttpClientTests
{
[Fact]
public async Task GetContextAsync_MapsPayloadToDocument()
{
const string payload = """
{
"artifactId": "artifact-001",
"purl": "pkg:npm/react@18.3.0",
"versions": [
{
"version": "18.3.0",
"firstObserved": "2025-10-01T00:00:00Z",
"lastObserved": null,
"status": "affected",
"source": "inventory",
"isFixAvailable": false,
"metadata": { "note": "current" }
}
],
"dependencyPaths": [
{
"nodes": [
{ "identifier": "app", "version": "1.0.0" },
{ "identifier": "react", "version": "18.3.0" }
],
"isRuntime": true,
"source": "scanner",
"metadata": { "scope": "production" }
}
],
"environmentFlags": {
"environment/prod": "true"
},
"blastRadius": {
"impactedAssets": 10,
"impactedWorkloads": 4,
"impactedNamespaces": 2,
"impactedPercentage": 0.25,
"metadata": { "note": "simulated" }
},
"metadata": {
"source": "sbom-service"
}
}
""";
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(payload, Encoding.UTF8, "application/json")
});
var httpClient = new HttpClient(handler)
{
BaseAddress = new Uri("https://sbom.example/")
};
var options = Options.Create(new SbomContextClientOptions
{
ContextEndpoint = "api/sbom/context",
Tenant = "tenant-alpha",
TenantHeaderName = "X-StellaOps-Tenant"
});
var client = new SbomContextHttpClient(httpClient, options, NullLogger<SbomContextHttpClient>.Instance);
var query = new SbomContextQuery("artifact-001", "pkg:npm/react@18.3.0", 25, 10, includeEnvironmentFlags: true, includeBlastRadius: true);
var document = await client.GetContextAsync(query, CancellationToken.None);
Assert.NotNull(document);
Assert.Equal("artifact-001", document!.ArtifactId);
Assert.Equal("pkg:npm/react@18.3.0", document.Purl);
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.AdvisoryAI.Providers;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class SbomContextHttpClientTests
{
[Fact]
public async Task GetContextAsync_MapsPayloadToDocument()
{
const string payload = """
{
"artifactId": "artifact-001",
"purl": "pkg:npm/react@18.3.0",
"versions": [
{
"version": "18.3.0",
"firstObserved": "2025-10-01T00:00:00Z",
"lastObserved": null,
"status": "affected",
"source": "inventory",
"isFixAvailable": false,
"metadata": { "note": "current" }
}
],
"dependencyPaths": [
{
"nodes": [
{ "identifier": "app", "version": "1.0.0" },
{ "identifier": "react", "version": "18.3.0" }
],
"isRuntime": true,
"source": "scanner",
"metadata": { "scope": "production" }
}
],
"environmentFlags": {
"environment/prod": "true"
},
"blastRadius": {
"impactedAssets": 10,
"impactedWorkloads": 4,
"impactedNamespaces": 2,
"impactedPercentage": 0.25,
"metadata": { "note": "simulated" }
},
"metadata": {
"source": "sbom-service"
}
}
""";
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(payload, Encoding.UTF8, "application/json")
});
var httpClient = new HttpClient(handler)
{
BaseAddress = new Uri("https://sbom.example/")
};
var options = Options.Create(new SbomContextClientOptions
{
ContextEndpoint = "api/sbom/context",
Tenant = "tenant-alpha",
TenantHeaderName = "X-StellaOps-Tenant"
});
var client = new SbomContextHttpClient(httpClient, options, NullLogger<SbomContextHttpClient>.Instance);
var query = new SbomContextQuery("artifact-001", "pkg:npm/react@18.3.0", 25, 10, includeEnvironmentFlags: true, includeBlastRadius: true);
var document = await client.GetContextAsync(query, CancellationToken.None);
Assert.NotNull(document);
Assert.Equal("artifact-001", document!.ArtifactId);
Assert.Equal("pkg:npm/react@18.3.0", document.Purl);
Assert.Single(document.Versions);
Assert.Single(document.DependencyPaths);
Assert.Single(document.EnvironmentFlags);
Assert.NotNull(document.BlastRadius);
Assert.Equal("sbom-service", document.Metadata["source"]);
Assert.NotNull(handler.LastRequest);
Assert.Equal("tenant-alpha", handler.LastRequest!.Headers.GetValues("X-StellaOps-Tenant").Single());
Assert.Contains("artifactId=artifact-001", handler.LastRequest.RequestUri!.Query);
Assert.Contains("purl=pkg%3Anpm%2Freact%4018.3.0", handler.LastRequest.RequestUri!.Query);
Assert.Contains("includeEnvironmentFlags=true", handler.LastRequest.RequestUri!.Query);
Assert.Contains("includeBlastRadius=true", handler.LastRequest.RequestUri!.Query);
}
[Fact]
public async Task GetContextAsync_ReturnsNullOnNotFound()
{
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.NotFound));
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://sbom.example/") };
var options = Options.Create(new SbomContextClientOptions());
var client = new SbomContextHttpClient(httpClient, options, NullLogger<SbomContextHttpClient>.Instance);
var result = await client.GetContextAsync(new SbomContextQuery("missing", null, 10, 5, false, false), CancellationToken.None);
Assert.Null(result);
}
[Fact]
public async Task GetContextAsync_ThrowsForServerError()
{
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent("{\"error\":\"boom\"}", Encoding.UTF8, "application/json")
});
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://sbom.example/") };
var options = Options.Create(new SbomContextClientOptions());
var client = new SbomContextHttpClient(httpClient, options, NullLogger<SbomContextHttpClient>.Instance);
await Assert.ThrowsAsync<HttpRequestException>(() => client.GetContextAsync(new SbomContextQuery("artifact", null, 5, 5, false, false), CancellationToken.None));
}
private sealed class StubHttpMessageHandler : HttpMessageHandler
{
private readonly Func<HttpRequestMessage, HttpResponseMessage> responder;
public StubHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responder)
{
this.responder = responder ?? throw new ArgumentNullException(nameof(responder));
}
public HttpRequestMessage? LastRequest { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
LastRequest = request;
return Task.FromResult(responder(request));
}
}
}
Assert.Single(document.DependencyPaths);
Assert.Single(document.EnvironmentFlags);
Assert.NotNull(document.BlastRadius);
Assert.Equal("sbom-service", document.Metadata["source"]);
Assert.NotNull(handler.LastRequest);
Assert.Equal("tenant-alpha", handler.LastRequest!.Headers.GetValues("X-StellaOps-Tenant").Single());
Assert.Contains("artifactId=artifact-001", handler.LastRequest.RequestUri!.Query);
Assert.Contains("purl=pkg%3Anpm%2Freact%4018.3.0", handler.LastRequest.RequestUri!.Query);
Assert.Contains("includeEnvironmentFlags=true", handler.LastRequest.RequestUri!.Query);
Assert.Contains("includeBlastRadius=true", handler.LastRequest.RequestUri!.Query);
}
[Fact]
public async Task GetContextAsync_ReturnsNullOnNotFound()
{
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.NotFound));
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://sbom.example/") };
var options = Options.Create(new SbomContextClientOptions());
var client = new SbomContextHttpClient(httpClient, options, NullLogger<SbomContextHttpClient>.Instance);
var result = await client.GetContextAsync(new SbomContextQuery("missing", null, 10, 5, false, false), CancellationToken.None);
Assert.Null(result);
}
[Fact]
public async Task GetContextAsync_ThrowsForServerError()
{
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent("{\"error\":\"boom\"}", Encoding.UTF8, "application/json")
});
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://sbom.example/") };
var options = Options.Create(new SbomContextClientOptions());
var client = new SbomContextHttpClient(httpClient, options, NullLogger<SbomContextHttpClient>.Instance);
await Assert.ThrowsAsync<HttpRequestException>(() => client.GetContextAsync(new SbomContextQuery("artifact", null, 5, 5, false, false), CancellationToken.None));
}
private sealed class StubHttpMessageHandler : HttpMessageHandler
{
private readonly Func<HttpRequestMessage, HttpResponseMessage> responder;
public StubHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responder)
{
this.responder = responder ?? throw new ArgumentNullException(nameof(responder));
}
public HttpRequestMessage? LastRequest { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
LastRequest = request;
return Task.FromResult(responder(request));
}
}
}

View File

@@ -1,29 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
<ProjectReference Include="..\..\StellaOps.AdvisoryAI.Hosting\StellaOps.AdvisoryAI.Hosting.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj" />
<ProjectReference Include="..\..\..\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="TestData/*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData/*.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj" />
<ProjectReference Include="..\..\..\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="TestData/*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData/*.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -10,30 +10,30 @@ using StellaOps.AdvisoryAI.Tools;
using StellaOps.AdvisoryAI.Abstractions;
using StellaOps.AdvisoryAI.Documents;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class ToolsetServiceCollectionExtensionsTests
{
[Fact]
public void AddAdvisoryDeterministicToolset_RegistersSingleton()
{
var services = new ServiceCollection();
services.AddAdvisoryDeterministicToolset();
var provider = services.BuildServiceProvider();
var toolsetA = provider.GetRequiredService<IDeterministicToolset>();
var toolsetB = provider.GetRequiredService<IDeterministicToolset>();
Assert.Same(toolsetA, toolsetB);
}
[Fact]
public void AddAdvisoryPipeline_RegistersOrchestrator()
{
var services = new ServiceCollection();
namespace StellaOps.AdvisoryAI.Tests;
public sealed class ToolsetServiceCollectionExtensionsTests
{
[Fact]
public void AddAdvisoryDeterministicToolset_RegistersSingleton()
{
var services = new ServiceCollection();
services.AddAdvisoryDeterministicToolset();
var provider = services.BuildServiceProvider();
var toolsetA = provider.GetRequiredService<IDeterministicToolset>();
var toolsetB = provider.GetRequiredService<IDeterministicToolset>();
Assert.Same(toolsetA, toolsetB);
}
[Fact]
public void AddAdvisoryPipeline_RegistersOrchestrator()
{
var services = new ServiceCollection();
services.AddSbomContext(options =>
{
options.BaseAddress = new Uri("https://sbom.example/");