up
Some checks failed
api-governance / spectral-lint (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-26 20:23:28 +02:00
parent 4831c7fcb0
commit d63af51f84
139 changed files with 8010 additions and 2795 deletions

View File

@@ -158,9 +158,9 @@ public sealed class InMemoryGraphQueryService : IGraphQueryService
: string.Join(";", request.Filters.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase)
.Select(kvp => $"{kvp.Key}={kvp.Value}"));
var kinds = request.Kinds is null ? string.Empty : string.Join(",", request.Kinds.OrderBy(k => k, StringComparer.OrdinalIgnoreCase));
var kinds = request.Kinds?.OrderBy(k => k, StringComparer.OrdinalIgnoreCase).ToArray() ?? Array.Empty<string>();
var budget = request.Budget is null ? "budget:none" : $"tiles:{request.Budget.Tiles};nodes:{request.Budget.Nodes};edges:{request.Budget.Edges}";
return $"{tenant}|{kinds}|{request.Query}|{limit}|{request.Cursor}|{filters}|edges:{request.IncludeEdges}|stats:{request.IncludeStats}|{budget}|tb:{tileBudget}|nb:{nodeBudget}|eb:{edgeBudget}";
return $"{tenant}|{string.Join(",", kinds)}|{request.Query}|{limit}|{request.Cursor}|{filters}|edges:{request.IncludeEdges}|stats:{request.IncludeStats}|{budget}|tb:{tileBudget}|nb:{nodeBudget}|eb:{edgeBudget}";
}
private static int Score(NodeTile node, GraphQueryRequest request)

View File

@@ -93,8 +93,8 @@ public sealed class InMemoryGraphSearchService : IGraphSearchService
: string.Join(";", request.Filters.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase)
.Select(kvp => $"{kvp.Key}={kvp.Value}"));
var kinds = request.Kinds is null ? string.Empty : string.Join(",", request.Kinds.OrderBy(k => k, StringComparer.OrdinalIgnoreCase));
return $"{tenant}|{kinds}|{request.Query}|{limit}|{request.Ordering}|{request.Cursor}|{filters}";
var kinds = request.Kinds?.OrderBy(k => k, StringComparer.OrdinalIgnoreCase).ToArray() ?? Array.Empty<string>();
return $"{tenant}|{string.Join(",", kinds)}|{request.Query}|{limit}|{request.Ordering}|{request.Cursor}|{filters}";
}
private static int Score(NodeTile node, GraphSearchRequest request)

View File

@@ -44,7 +44,7 @@ namespace StellaOps.Graph.Api.Services;
}
// Always return a fresh copy so we can inject a single explain trace without polluting cache.
var overlays = new Dictionary<string, OverlayPayload>(cachedBase!, StringComparer.Ordinal);
var overlays = new Dictionary<string, OverlayPayload>(cachedBase, StringComparer.Ordinal);
if (sampleExplain && !explainEmitted)
{

View File

@@ -7,11 +7,11 @@ using Xunit;
namespace StellaOps.Graph.Api.Tests;
public class QueryServiceTests
public class QueryServiceTests
{
[Fact]
public async Task QueryAsync_EmitsNodesEdgesStatsAndCursor()
{
[Fact]
public async Task QueryAsync_EmitsNodesEdgesStatsAndCursor()
{
var repo = new InMemoryGraphRepository();
var service = CreateService(repo);
@@ -37,10 +37,10 @@ namespace StellaOps.Graph.Api.Tests;
}
[Fact]
public async Task QueryAsync_ReturnsBudgetExceededError()
{
var repo = new InMemoryGraphRepository();
var service = CreateService(repo);
public async Task QueryAsync_ReturnsBudgetExceededError()
{
var repo = new InMemoryGraphRepository();
var service = CreateService(repo);
var request = new GraphQueryRequest
{
@@ -51,62 +51,63 @@ namespace StellaOps.Graph.Api.Tests;
};
var lines = new List<string>();
await foreach (var line in service.QueryAsync("acme", request))
{
lines.Add(line);
}
Assert.Single(lines);
Assert.Contains("GRAPH_BUDGET_EXCEEDED", lines[0]);
await foreach (var line in service.QueryAsync("acme", request))
{
lines.Add(line);
}
[Fact]
public async Task QueryAsync_IncludesOverlaysAndSamplesExplainOnce()
Assert.Single(lines);
Assert.Contains("GRAPH_BUDGET_EXCEEDED", lines[0]);
}
[Fact]
public async Task QueryAsync_IncludesOverlaysAndSamplesExplainOnce()
{
var repo = new InMemoryGraphRepository(new[]
{
var repo = new InMemoryGraphRepository(new[]
{
new NodeTile { Id = "gn:acme:component:one", Kind = "component", Tenant = "acme" },
new NodeTile { Id = "gn:acme:component:two", Kind = "component", Tenant = "acme" }
}, Array.Empty<EdgeTile>());
new NodeTile { Id = "gn:acme:component:one", Kind = "component", Tenant = "acme" },
new NodeTile { Id = "gn:acme:component:two", Kind = "component", Tenant = "acme" }
}, Array.Empty<EdgeTile>());
var cache = new MemoryCache(new MemoryCacheOptions());
var overlays = new InMemoryOverlayService(cache);
var service = new InMemoryGraphQueryService(repo, cache, overlays);
var request = new GraphQueryRequest
{
Kinds = new[] { "component" },
IncludeOverlays = true,
Limit = 5
};
var cache = new MemoryCache(new MemoryCacheOptions());
var metrics = new GraphMetrics();
var overlays = new InMemoryOverlayService(cache, metrics);
var service = new InMemoryGraphQueryService(repo, cache, overlays, metrics);
var request = new GraphQueryRequest
{
Kinds = new[] { "component" },
IncludeOverlays = true,
Limit = 5
};
var overlayNodes = 0;
var explainCount = 0;
var overlayNodes = 0;
var explainCount = 0;
await foreach (var line in service.QueryAsync("acme", request))
await foreach (var line in service.QueryAsync("acme", request))
{
if (!line.Contains("\"type\":\"node\"")) continue;
using var doc = JsonDocument.Parse(line);
var data = doc.RootElement.GetProperty("data");
if (data.TryGetProperty("overlays", out var overlaysElement) && overlaysElement.ValueKind == JsonValueKind.Object)
{
if (!line.Contains("\"type\":\"node\"")) continue;
using var doc = JsonDocument.Parse(line);
var data = doc.RootElement.GetProperty("data");
if (data.TryGetProperty("overlays", out var overlaysElement) && overlaysElement.ValueKind == JsonValueKind.Object)
overlayNodes++;
foreach (var overlay in overlaysElement.EnumerateObject())
{
overlayNodes++;
foreach (var overlay in overlaysElement.EnumerateObject())
if (overlay.Value.ValueKind != JsonValueKind.Object) continue;
if (overlay.Value.TryGetProperty("data", out var payload) && payload.TryGetProperty("explainTrace", out var trace) && trace.ValueKind == JsonValueKind.Array)
{
if (overlay.Value.ValueKind != JsonValueKind.Object) continue;
if (overlay.Value.TryGetProperty("data", out var payload) && payload.TryGetProperty("explainTrace", out var trace) && trace.ValueKind == JsonValueKind.Array)
{
explainCount++;
}
explainCount++;
}
}
}
Assert.True(overlayNodes >= 1);
Assert.Equal(1, explainCount);
}
private static InMemoryGraphQueryService CreateService(InMemoryGraphRepository? repository = null)
{
Assert.True(overlayNodes >= 1);
Assert.Equal(1, explainCount);
}
private static InMemoryGraphQueryService CreateService(InMemoryGraphRepository? repository = null)
{
var cache = new MemoryCache(new MemoryCacheOptions());
var metrics = new GraphMetrics();
var overlays = new InMemoryOverlayService(cache, metrics);

View File

@@ -139,6 +139,7 @@ public class SearchServiceTests
var nodeCount = lines.Count(l => l.Contains("\"type\":\"node\""));
Assert.True(lines.Count <= 2);
Assert.Contains(lines, l => l.Contains("\"type\":\"cursor\""));
Assert.True(nodeCount <= 2);
}