163 lines
5.4 KiB
C#
163 lines
5.4 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Covers the node-mode half of <see cref="WorkflowCompiler"/>: steps become
|
|
/// tasks with node columns and their next/explicit connections become persisted
|
|
/// edge rows. The legacy (script) path is covered by WorkflowCompilerTests.
|
|
/// </summary>
|
|
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<string>());
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|