From 87b1abae4e19476bb6823cdd2c9cf53d07d2b74d Mon Sep 17 00:00:00 2001 From: Vitali sharp8n Date: Mon, 7 Sep 2026 11:15:03 +0300 Subject: [PATCH] wf test fix --- .../MermaidGeneratorServiceTests.cs | 122 ++++++++++++++++-- 1 file changed, 108 insertions(+), 14 deletions(-) diff --git a/w4c-workflows-api.Tests/MermaidGeneratorServiceTests.cs b/w4c-workflows-api.Tests/MermaidGeneratorServiceTests.cs index 670c1e3..25efd15 100644 --- a/w4c-workflows-api.Tests/MermaidGeneratorServiceTests.cs +++ b/w4c-workflows-api.Tests/MermaidGeneratorServiceTests.cs @@ -1,4 +1,4 @@ -using w4c_workflows.Models; +using System.Text.RegularExpressions; using w4c_workflows.Services; using Xunit; @@ -9,6 +9,20 @@ public class MermaidGeneratorServiceTests private const string Tenant = "t1"; private const string Path = "workflows/main-task.yaml"; + /// Diagram names accepted by . + /// Centralising them removes magic strings and keeps tests in sync with the service. + private static class Diagram + { + public const string Flowchart = "flowchart"; + public const string Sequence = "sequence"; + public const string State = "state"; + public const string Class = "class"; + } + + /// is stateless (its methods are all static), so a + /// single instance is shared instead of allocating one per call. + private static readonly MermaidGeneratorService Generator = new(); + private static readonly string ExampleYaml = """ name: main-task mode: handler @@ -43,11 +57,15 @@ public class MermaidGeneratorServiceTests => new WorkflowCompiler(new WorkflowValidator(new LanguageRegistry())) .Compile(ExampleYaml, Path, Tenant).Workflow!; + private static string Render(string type) => Render(Compile(), type); + + private static string Render(CompiledWorkflow compiled, string type) + => Generator.Generate(type, compiled.Workflow, compiled.Tasks); + [Fact] public void Flowchart_renders_next_solid_and_onError_dashed() { - var compiled = Compile(); - var source = new MermaidGeneratorService().Generate("flowchart", compiled.Workflow, compiled.Tasks); + var source = Render(Diagram.Flowchart); Assert.Contains("flowchart TD", source); Assert.Contains("root --> validate", source); @@ -59,8 +77,7 @@ public class MermaidGeneratorServiceTests [Fact] public void Sequence_renders_happy_path_and_alt_error_block() { - var compiled = Compile(); - var source = new MermaidGeneratorService().Generate("sequence", compiled.Workflow, compiled.Tasks); + var source = Render(Diagram.Sequence); Assert.Contains("sequenceDiagram", source); Assert.Contains("root->>validate", source); @@ -73,19 +90,51 @@ public class MermaidGeneratorServiceTests public void State_renders_entry_terminal_and_error_transitions() { var compiled = Compile(); - var source = new MermaidGeneratorService().Generate("state", compiled.Workflow, compiled.Tasks); + var source = Render(compiled, Diagram.State); + var (aliasOf, edges) = ParseStateDiagram(source); - Assert.Contains("stateDiagram-v2", source); - Assert.Contains("[*] --> root", source); - Assert.Contains("persist --> [*]", source); - Assert.Contains("validate --> compensate: error", source); + Assert.StartsWith("stateDiagram-v2", source.TrimStart(), StringComparison.Ordinal); + + // Every task (including the synthesized "root") is declared exactly once as a + // labelled state. We must not assert literal transition text such as + // "[*] --> root": State() aliases every key to a reserved-word-safe id (s0, + // s1, ...) and prints the human key only inside the quoted label. Comparing + // the declared labels against the compiled keys is order- and index-agnostic. + Assert.Equal( + compiled.Tasks.Select(t => t.Key).OrderBy(k => k, StringComparer.Ordinal), + aliasOf.Keys.OrderBy(k => k, StringComparer.Ordinal)); + + // Entry: exactly one initial transition, and it starts at "root". + var entryEdges = edges.Where(e => e.From == "[*]").ToList(); + Assert.Single(entryEdges); + Assert.Equal(aliasOf["root"], entryEdges[0].To); + + // Terminal: each task with no success/error successor ends at "[*]" + // (persist and compensate in this fixture), so no other task is terminal. + Assert.Equal( + compiled.Tasks.Where(t => t.NextId == null && t.OnErrorId == null) + .Select(t => aliasOf[t.Key]).OrderBy(a => a, StringComparer.Ordinal), + edges.Where(e => e.To == "[*]").Select(e => e.From).OrderBy(a => a, StringComparer.Ordinal)); + + // Error path: validate's onError: compensate is rendered as a labelled edge. + var errorEdge = Assert.Single(edges.Where(e => e.Label == "error")); + Assert.Equal(aliasOf["validate"], errorEdge.From); + Assert.Equal(aliasOf["compensate"], errorEdge.To); + + // Structural integrity: no edge may reference an undeclared state, which would + // render as a raw id instead of a labelled node. + var declared = aliasOf.Values.Append("[*]").ToHashSet(StringComparer.Ordinal); + foreach (var edge in edges) + { + Assert.Contains(edge.From, declared); + Assert.Contains(edge.To, declared); + } } [Fact] public void Class_renders_language_not_runtime() { - var compiled = Compile(); - var source = new MermaidGeneratorService().Generate("class", compiled.Workflow, compiled.Tasks); + var source = Render(Diagram.Class); Assert.Contains("classDiagram", source); Assert.Contains("<>", source); // root language @@ -98,7 +147,52 @@ public class MermaidGeneratorServiceTests public void Unknown_type_throws() { var compiled = Compile(); - var generator = new MermaidGeneratorService(); - Assert.Throws(() => generator.Generate("pie", compiled.Workflow, compiled.Tasks)); + Assert.Throws(() => Render(compiled, "pie")); + } + + // ------------------------------------------------------------------ state helpers + // State() is the only generator that aliases task keys to generated ids, so its output + // is verified structurally (labelled declarations + edges) rather than by exact + // substrings, which would silently couple the test to the alias numbering. + + private sealed record StateEdge(string From, string To, string? Label); + + private static readonly Regex StateDeclRegex = new( + @"^state ""(?