using System.Text.Json; using System.Text.Json.Nodes; using FluentAssertions; using StellaOps.Graph.Indexer.Schema; using Xunit; namespace StellaOps.Graph.Indexer.Tests; public sealed class GraphIdentityTests { private static readonly string FixturesRoot = Path.Combine(AppContext.BaseDirectory, "Fixtures", "v1"); [Fact] public void NodeIds_are_stable() { var nodes = LoadArray("nodes.json"); foreach (var node in nodes.Cast()) { var tenant = node["tenant"]!.GetValue(); var kind = node["kind"]!.GetValue(); var canonicalKey = (JsonObject)node["canonical_key"]!; var tuple = GraphIdentity.ExtractIdentityTuple(canonicalKey); var expectedId = node["id"]!.GetValue(); var actualId = GraphIdentity.ComputeNodeId(tenant, kind, tuple); actualId.Should() .Be(expectedId, $"node {kind} with canonical tuple {canonicalKey.ToJsonString()} must have deterministic id"); var documentClone = JsonNode.Parse(node.ToJsonString())!.AsObject(); documentClone.Remove("hash"); var expectedHash = node["hash"]!.GetValue(); var actualHash = GraphIdentity.ComputeDocumentHash(documentClone); actualHash.Should() .Be(expectedHash, $"node {kind}:{expectedId} must have deterministic document hash"); } } [Fact] public void EdgeIds_are_stable() { var edges = LoadArray("edges.json"); foreach (var edge in edges.Cast()) { var tenant = edge["tenant"]!.GetValue(); var kind = edge["kind"]!.GetValue(); var canonicalKey = (JsonObject)edge["canonical_key"]!; var tuple = GraphIdentity.ExtractIdentityTuple(canonicalKey); var expectedId = edge["id"]!.GetValue(); var actualId = GraphIdentity.ComputeEdgeId(tenant, kind, tuple); actualId.Should() .Be(expectedId, $"edge {kind} with canonical tuple {canonicalKey.ToJsonString()} must have deterministic id"); var documentClone = JsonNode.Parse(edge.ToJsonString())!.AsObject(); documentClone.Remove("hash"); var expectedHash = edge["hash"]!.GetValue(); var actualHash = GraphIdentity.ComputeDocumentHash(documentClone); actualHash.Should() .Be(expectedHash, $"edge {kind}:{expectedId} must have deterministic document hash"); } } [Fact] public void AttributeCoverage_matches_matrix() { var matrix = LoadObject("schema-matrix.json"); var nodeExpectations = (JsonObject)matrix["nodes"]!; var edgeExpectations = (JsonObject)matrix["edges"]!; var nodes = LoadArray("nodes.json"); foreach (var node in nodes.Cast()) { var kind = node["kind"]!.GetValue(); var expectedAttributes = nodeExpectations[kind]!.AsArray().Select(x => x!.GetValue()).OrderBy(x => x, StringComparer.Ordinal).ToArray(); var actualAttributes = ((JsonObject)node["attributes"]!).Select(pair => pair.Key).OrderBy(x => x, StringComparer.Ordinal).ToArray(); actualAttributes.Should() .Equal(expectedAttributes, $"node kind {kind} must align with schema matrix"); } var edges = LoadArray("edges.json"); foreach (var edge in edges.Cast()) { var kind = edge["kind"]!.GetValue(); var expectedAttributes = edgeExpectations[kind]!.AsArray().Select(x => x!.GetValue()).OrderBy(x => x, StringComparer.Ordinal).ToArray(); var actualAttributes = ((JsonObject)edge["attributes"]!).Select(pair => pair.Key).OrderBy(x => x, StringComparer.Ordinal).ToArray(); actualAttributes.Should() .Equal(expectedAttributes, $"edge kind {kind} must align with schema matrix"); } } private static JsonArray LoadArray(string fileName) => (JsonArray)JsonNode.Parse(File.ReadAllText(GetFixturePath(fileName)))!; private static JsonObject LoadObject(string fileName) => (JsonObject)JsonNode.Parse(File.ReadAllText(GetFixturePath(fileName)))!; private static string GetFixturePath(string fileName) => Path.Combine(FixturesRoot, fileName); }