using FluentAssertions; using StellaOps.Signals.Models; using StellaOps.Signals.Parsing; using Xunit; namespace StellaOps.Signals.Reachability.Tests; /// /// Unit tests for CallgraphSchemaMigrator. /// Verifies schema migration from legacy format to stella.callgraph.v1. /// public class CallgraphSchemaMigratorTests { #region EnsureV1 - Schema Version Tests [Fact] public void EnsureV1_SetsSchemaToV1_WhenNotSet() { // Arrange var document = new CallgraphDocument { Schema = string.Empty }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Schema.Should().Be(CallgraphSchemaVersions.V1); } [Fact] public void EnsureV1_PreservesV1Schema_WhenAlreadySet() { // Arrange var document = new CallgraphDocument { Schema = CallgraphSchemaVersions.V1 }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Schema.Should().Be(CallgraphSchemaVersions.V1); } [Fact] public void EnsureV1_UpdatesLegacySchema_ToV1() { // Arrange var document = new CallgraphDocument { Schema = "legacy-schema-1.0" }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Schema.Should().Be(CallgraphSchemaVersions.V1); } #endregion #region EnsureV1 - Language Parsing Tests [Theory] [InlineData("dotnet", CallgraphLanguage.DotNet)] [InlineData(".net", CallgraphLanguage.DotNet)] [InlineData("csharp", CallgraphLanguage.DotNet)] [InlineData("c#", CallgraphLanguage.DotNet)] [InlineData("java", CallgraphLanguage.Java)] [InlineData("node", CallgraphLanguage.Node)] [InlineData("nodejs", CallgraphLanguage.Node)] [InlineData("javascript", CallgraphLanguage.Node)] [InlineData("typescript", CallgraphLanguage.Node)] [InlineData("python", CallgraphLanguage.Python)] [InlineData("go", CallgraphLanguage.Go)] [InlineData("golang", CallgraphLanguage.Go)] [InlineData("rust", CallgraphLanguage.Rust)] [InlineData("ruby", CallgraphLanguage.Ruby)] [InlineData("php", CallgraphLanguage.Php)] [InlineData("binary", CallgraphLanguage.Binary)] [InlineData("native", CallgraphLanguage.Binary)] [InlineData("elf", CallgraphLanguage.Binary)] [InlineData("swift", CallgraphLanguage.Swift)] [InlineData("kotlin", CallgraphLanguage.Kotlin)] public void EnsureV1_ParsesLanguageString_ToEnum(string languageString, CallgraphLanguage expected) { // Arrange var document = new CallgraphDocument { Language = languageString, LanguageType = CallgraphLanguage.Unknown }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.LanguageType.Should().Be(expected); } [Fact] public void EnsureV1_PreservesLanguageType_WhenAlreadySet() { // Arrange var document = new CallgraphDocument { Language = "java", LanguageType = CallgraphLanguage.DotNet // Already set to something different }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.LanguageType.Should().Be(CallgraphLanguage.DotNet); } #endregion #region EnsureV1 - Node Visibility Inference Tests [Fact] public void EnsureV1_InfersPublicVisibility_ForStandardNames() { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "node1", Name = "ProcessOrder", Visibility = SymbolVisibility.Unknown } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().ContainSingle() .Which.Visibility.Should().Be(SymbolVisibility.Public); } [Fact] public void EnsureV1_InfersPrivateVisibility_ForUnderscorePrefixed() { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "node1", Name = "_privateMethod", Visibility = SymbolVisibility.Unknown } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().ContainSingle() .Which.Visibility.Should().Be(SymbolVisibility.Private); } [Fact] public void EnsureV1_InfersPrivateVisibility_ForAngleBracketNames() { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "node1", Name = "
$", Visibility = SymbolVisibility.Unknown } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().ContainSingle() .Which.Visibility.Should().Be(SymbolVisibility.Private); } [Fact] public void EnsureV1_InfersInternalVisibility_ForInternalNamespace() { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "node1", Name = "Helper", Namespace = "MyApp.Internal.Utils", Visibility = SymbolVisibility.Unknown } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().ContainSingle() .Which.Visibility.Should().Be(SymbolVisibility.Internal); } [Fact] public void EnsureV1_PreservesVisibility_WhenAlreadySet() { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "node1", Name = "_privateMethod", Visibility = SymbolVisibility.Protected } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().ContainSingle() .Which.Visibility.Should().Be(SymbolVisibility.Protected); } #endregion #region EnsureV1 - Symbol Key Building Tests [Fact] public void EnsureV1_BuildsSymbolKey_FromNamespaceAndName() { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "node1", Name = "ProcessOrder", Namespace = "MyApp.Services" } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().ContainSingle() .Which.SymbolKey.Should().Be("MyApp.Services.ProcessOrder"); } [Fact] public void EnsureV1_BuildsSymbolKey_FromNameOnly_WhenNoNamespace() { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "node1", Name = "GlobalMethod" } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().ContainSingle() .Which.SymbolKey.Should().Be("GlobalMethod"); } [Fact] public void EnsureV1_PreservesSymbolKey_WhenAlreadySet() { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "node1", Name = "Method", Namespace = "Ns", SymbolKey = "Custom.Key" } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().ContainSingle() .Which.SymbolKey.Should().Be("Custom.Key"); } #endregion #region EnsureV1 - Entrypoint Candidate Detection Tests [Theory] [InlineData("Main")] [InlineData("main")] [InlineData("MAIN")] public void EnsureV1_DetectsEntrypointCandidate_ForMainMethod(string methodName) { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "node1", Name = methodName } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().ContainSingle() .Which.IsEntrypointCandidate.Should().BeTrue(); } [Theory] [InlineData("OrdersController")] [InlineData("UserController")] public void EnsureV1_DetectsEntrypointCandidate_ForControllerNames(string name) { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "node1", Name = name } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().ContainSingle() .Which.IsEntrypointCandidate.Should().BeTrue(); } [Theory] [InlineData("RequestHandler")] [InlineData("EventHandler")] public void EnsureV1_DetectsEntrypointCandidate_ForHandlerNames(string name) { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "node1", Name = name } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().ContainSingle() .Which.IsEntrypointCandidate.Should().BeTrue(); } [Theory] [InlineData(".cctor")] [InlineData("ModuleInitializer")] public void EnsureV1_DetectsEntrypointCandidate_ForModuleInitializers(string name) { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "node1", Name = name } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().ContainSingle() .Which.IsEntrypointCandidate.Should().BeTrue(); } #endregion #region EnsureV1 - Edge Reason Inference Tests [Theory] [InlineData("call", EdgeReason.DirectCall)] [InlineData("direct", EdgeReason.DirectCall)] [InlineData("virtual", EdgeReason.VirtualCall)] [InlineData("callvirt", EdgeReason.VirtualCall)] [InlineData("newobj", EdgeReason.NewObj)] [InlineData("new", EdgeReason.NewObj)] [InlineData("ldftn", EdgeReason.DelegateCreate)] [InlineData("delegate", EdgeReason.DelegateCreate)] [InlineData("reflection", EdgeReason.ReflectionString)] [InlineData("di", EdgeReason.DiBinding)] [InlineData("injection", EdgeReason.DiBinding)] [InlineData("async", EdgeReason.AsyncContinuation)] [InlineData("continuation", EdgeReason.AsyncContinuation)] [InlineData("event", EdgeReason.EventHandler)] [InlineData("generic", EdgeReason.GenericInstantiation)] [InlineData("native", EdgeReason.NativeInterop)] [InlineData("pinvoke", EdgeReason.NativeInterop)] [InlineData("ffi", EdgeReason.NativeInterop)] public void EnsureV1_InfersEdgeReason_FromLegacyType(string legacyType, EdgeReason expected) { // Arrange var document = new CallgraphDocument { Edges = new List { new() { SourceId = "n1", TargetId = "n2", Type = legacyType, Reason = EdgeReason.Unknown } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Edges.Should().ContainSingle() .Which.Reason.Should().Be(expected); } [Fact] public void EnsureV1_InfersRuntimeMinted_ForRuntimeKind() { // Arrange var document = new CallgraphDocument { Edges = new List { new() { SourceId = "n1", TargetId = "n2", Type = "unknown", Kind = EdgeKind.Runtime, Reason = EdgeReason.Unknown } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Edges.Should().ContainSingle() .Which.Reason.Should().Be(EdgeReason.RuntimeMinted); } [Fact] public void EnsureV1_InfersDynamicImport_ForHeuristicKind() { // Arrange var document = new CallgraphDocument { Edges = new List { new() { SourceId = "n1", TargetId = "n2", Type = "unknown", Kind = EdgeKind.Heuristic, Reason = EdgeReason.Unknown } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Edges.Should().ContainSingle() .Which.Reason.Should().Be(EdgeReason.DynamicImport); } [Fact] public void EnsureV1_PreservesEdgeReason_WhenAlreadySet() { // Arrange var document = new CallgraphDocument { Edges = new List { new() { SourceId = "n1", TargetId = "n2", Type = "call", Reason = EdgeReason.VirtualCall } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Edges.Should().ContainSingle() .Which.Reason.Should().Be(EdgeReason.VirtualCall); } #endregion #region EnsureV1 - Entrypoint Inference Tests [Fact] public void EnsureV1_InfersEntrypoints_FromEntrypointCandidateNodes() { // Arrange var document = new CallgraphDocument { LanguageType = CallgraphLanguage.DotNet, Nodes = new List { new() { Id = "main", Name = "Main", IsEntrypointCandidate = true }, new() { Id = "helper", Name = "Helper", IsEntrypointCandidate = false } }, Entrypoints = new List() }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Entrypoints.Should().ContainSingle() .Which.NodeId.Should().Be("main"); } [Fact] public void EnsureV1_InfersEntrypoints_FromExplicitRoots() { // Arrange var document = new CallgraphDocument { LanguageType = CallgraphLanguage.DotNet, Nodes = new List { new() { Id = "init", Name = "Initialize" } }, Roots = new List { new("init", "init", "module_init") }, Entrypoints = new List() }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Entrypoints.Should().ContainSingle() .Which.NodeId.Should().Be("init"); } [Fact] public void EnsureV1_PreservesEntrypoints_WhenAlreadyPresent() { // Arrange var existingEntrypoint = new CallgraphEntrypoint { NodeId = "existing", Kind = EntrypointKind.Http, Route = "/api/test" }; var document = new CallgraphDocument { Nodes = new List { new() { Id = "main", Name = "Main", IsEntrypointCandidate = true } }, Entrypoints = new List { existingEntrypoint } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Entrypoints.Should().ContainSingle() .Which.NodeId.Should().Be("existing"); } #endregion #region EnsureV1 - Ordering Tests [Fact] public void EnsureV1_SortsNodes_ByIdAlphabetically() { // Arrange var document = new CallgraphDocument { Nodes = new List { new() { Id = "z-node", Name = "Z" }, new() { Id = "a-node", Name = "A" }, new() { Id = "m-node", Name = "M" } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Select(n => n.Id).Should().BeInAscendingOrder(); } [Fact] public void EnsureV1_SortsEdges_BySourceThenTargetThenTypeThenOffset() { // Arrange var document = new CallgraphDocument { Edges = new List { new() { SourceId = "b", TargetId = "x", Type = "call", Offset = 10 }, new() { SourceId = "a", TargetId = "y", Type = "call", Offset = 5 }, new() { SourceId = "a", TargetId = "x", Type = "virtual", Offset = 0 }, new() { SourceId = "a", TargetId = "x", Type = "call", Offset = 20 } } }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert var sortedEdges = result.Edges.ToList(); sortedEdges[0].SourceId.Should().Be("a"); sortedEdges[0].TargetId.Should().Be("x"); sortedEdges[0].Type.Should().Be("call"); } [Fact] public void EnsureV1_SortsEntrypoints_ByPhaseThenOrder() { // Arrange var document = new CallgraphDocument { LanguageType = CallgraphLanguage.DotNet, Nodes = new List { new() { Id = "main", Name = "Main", IsEntrypointCandidate = true }, new() { Id = "init", Name = ".cctor", IsEntrypointCandidate = true } }, Entrypoints = new List() }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Entrypoints.Should().HaveCount(2); // ModuleInit phase should come before Runtime result.Entrypoints.First().NodeId.Should().Be("init"); } #endregion #region EnsureV1 - Null Handling Tests [Fact] public void EnsureV1_ThrowsArgumentNullException_ForNullDocument() { // Act & Assert Assert.Throws(() => CallgraphSchemaMigrator.EnsureV1(null!)); } [Fact] public void EnsureV1_HandlesEmptyNodes_Gracefully() { // Arrange var document = new CallgraphDocument { Nodes = new List() }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Nodes.Should().BeEmpty(); } [Fact] public void EnsureV1_HandlesEmptyEdges_Gracefully() { // Arrange var document = new CallgraphDocument { Edges = new List() }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Edges.Should().BeEmpty(); } #endregion #region Framework Inference Tests [Fact] public void EnsureV1_InfersAspNetCoreFramework_ForDotNetController() { // Arrange var document = new CallgraphDocument { LanguageType = CallgraphLanguage.DotNet, Nodes = new List { new() { Id = "ctrl", Name = "OrdersController", IsEntrypointCandidate = true } }, Entrypoints = new List() }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Entrypoints.Should().ContainSingle() .Which.Framework.Should().Be(EntrypointFramework.AspNetCore); } [Fact] public void EnsureV1_InfersSpringFramework_ForJavaController() { // Arrange var document = new CallgraphDocument { LanguageType = CallgraphLanguage.Java, Nodes = new List { new() { Id = "ctrl", Name = "OrderController", IsEntrypointCandidate = true } }, Entrypoints = new List() }; // Act var result = CallgraphSchemaMigrator.EnsureV1(document); // Assert result.Entrypoints.Should().ContainSingle() .Which.Framework.Should().Be(EntrypointFramework.Spring); } #endregion }