using System.Collections.Generic; using System.Text.Json; using Microsoft.Extensions.Caching.Memory; using StellaOps.Graph.Api.Contracts; using StellaOps.Graph.Api.Services; using Xunit; using StellaOps.TestKit; namespace StellaOps.Graph.Api.Tests; public class QueryServiceTests { [Trait("Category", TestCategories.Unit)] [Fact] public async Task QueryAsync_EmitsNodesEdgesStatsAndCursor() { var repo = new InMemoryGraphRepository(); var service = CreateService(repo); var request = new GraphQueryRequest { Kinds = new[] { "component", "artifact" }, Query = "component", Limit = 1, IncludeEdges = true, IncludeStats = true }; var lines = new List(); await foreach (var line in service.QueryAsync("acme", request)) { lines.Add(line); } Assert.Contains(lines, l => l.Contains("\"type\":\"node\"")); Assert.Contains(lines, l => l.Contains("\"type\":\"edge\"")); Assert.Contains(lines, l => l.Contains("\"type\":\"stats\"")); Assert.Contains(lines, l => l.Contains("\"type\":\"cursor\"")); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task QueryAsync_ReturnsBudgetExceededError() { var repo = new InMemoryGraphRepository(); var service = CreateService(repo); var request = new GraphQueryRequest { Kinds = new[] { "component", "artifact" }, Query = "component", Budget = new GraphQueryBudget { Nodes = 1, Edges = 0, Tiles = 2 }, Limit = 10 }; var lines = new List(); await foreach (var line in service.QueryAsync("acme", request)) { lines.Add(line); } Assert.Single(lines); Assert.Contains("GRAPH_BUDGET_EXCEEDED", lines[0]); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task QueryAsync_IncludesOverlaysAndSamplesExplainOnce() { 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()); 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; 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) { 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) { explainCount++; } } } } 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); return new InMemoryGraphQueryService(repository ?? new InMemoryGraphRepository(), cache, overlays, metrics); } }