using System.Text.Json.Nodes; using w4c_workflows.Models; using w4c_workflows.Models.Nodes; using w4c_workflows.Services; using w4c_workflows.Services.Nodes; using Xunit; namespace w4c_workflows.Tests; /// /// Covers the node-mode half of : steps become /// tasks with node columns and their next/explicit connections become persisted /// edge rows. The legacy (script) path is covered by WorkflowCompilerTests. /// public class NodeWorkflowCompilerTests { private const string Tenant = "t1"; private const string Path = "workflows/branch.yaml"; private const string BranchYaml = """ name: branch-demo tasks: - id: prepare node: { type: core.set } parameters: mode: manual fields: - name: status value: ready next: decide - id: decide node: { type: core.if } parameters: left: "={{ $json.status }}" operator: equals right: ready edges: - { output: 0, to: ok } - { output: 1, to: nope } - id: ok node: { type: core.noop } - id: nope node: { type: core.noop } """; private static WorkflowCompiler NewCompiler() => new(new WorkflowValidator(new LanguageRegistry())); private static Guid TaskId(string id) => DeterministicGuid.For($"task::{Tenant}::workflows::{Path}::{id}"); [Fact] public void Compiles_node_tasks_with_node_columns_and_parameters_json() { var result = NewCompiler().Compile(BranchYaml, Path, Tenant); Assert.True(result.Success, string.Join("\n", result.Errors)); var compiled = result.Workflow!; Assert.Equal(WorkflowMode.Function, compiled.Workflow.Mode); Assert.Equal(4, compiled.Tasks.Count); Assert.Empty(compiled.Tasks.Where(t => t.NodeType == null)); var prepare = compiled.Tasks.Single(t => t.Key == "prepare"); Assert.Equal("core.set", prepare.NodeType); Assert.Equal(1, prepare.NodeVersion); Assert.Equal("node", prepare.Language); Assert.Null(prepare.NextId); // node mode never uses the linear chain var parameters = JsonNode.Parse(prepare.ParametersJson!)!.AsObject(); Assert.Equal("manual", parameters["mode"]!.GetValue()); } [Fact] public void Lowers_next_and_explicit_outputs_into_edges() { var result = NewCompiler().Compile(BranchYaml, Path, Tenant); Assert.True(result.Success, string.Join("\n", result.Errors)); var edges = result.Workflow!.Edges; Assert.Contains(edges, e => e.FromTaskId == TaskId("prepare") && e.FromOutput == 0 && e.ToTaskId == TaskId("decide")); Assert.Contains(edges, e => e.FromTaskId == TaskId("decide") && e.FromOutput == 0 && e.ToTaskId == TaskId("ok")); Assert.Contains(edges, e => e.FromTaskId == TaskId("decide") && e.FromOutput == 1 && e.ToTaskId == TaskId("nope")); } [Fact] public void Compile_is_idempotent_for_node_workflows() { var compiler = NewCompiler(); var first = compiler.Compile(BranchYaml, Path, Tenant); var second = compiler.Compile(BranchYaml, Path, Tenant); Assert.Equal( first.Workflow!.Edges.Select(e => e.Id).OrderBy(x => x), second.Workflow!.Edges.Select(e => e.Id).OrderBy(x => x)); Assert.Equal( first.Workflow!.Tasks.Select(t => t.Id).OrderBy(x => x), second.Workflow!.Tasks.Select(t => t.Id).OrderBy(x => x)); } [Fact] public void Rejects_an_unknown_node_type() { var yaml = """ name: unknown tasks: - id: a node: { type: does.not.exist } """; var result = NewCompiler().Compile(yaml, Path, Tenant); Assert.False(result.Success); Assert.Contains(result.Errors, e => e.Contains("unknown node type 'does.not.exist'")); } [Fact] public void Rejects_a_node_workflow_step_without_a_node() { var yaml = """ name: mixed tasks: - id: a node: { type: core.noop } - id: b entry: { file: b.sh } """; var result = NewCompiler().Compile(yaml, Path, Tenant); Assert.False(result.Success); Assert.Contains(result.Errors, e => e.Contains("must declare 'node'")); } [Fact] public void Legacy_workflow_compiles_without_edges_and_keeps_the_next_chain() { var yaml = """ name: legacy mode: function language: shell entry: { file: root.sh } tasks: - id: a parent: root next: b entry: { file: a.sh } - id: b parent: root entry: { file: b.sh } """; var result = NewCompiler().Compile(yaml, Path, Tenant); Assert.True(result.Success, string.Join("\n", result.Errors)); Assert.Empty(result.Workflow!.Edges); Assert.All(result.Workflow.Tasks, t => Assert.Null(t.NodeType)); var a = result.Workflow.Tasks.Single(t => t.Key == "a"); var b = result.Workflow.Tasks.Single(t => t.Key == "b"); Assert.Equal(b.Id, a.NextId); } }