302 lines
9.2 KiB
C#
302 lines
9.2 KiB
C#
using System.Text.Json.Nodes;
|
|
using w4c_workflows.Models.Nodes;
|
|
using w4c_workflows.Services.Nodes;
|
|
using Xunit;
|
|
|
|
namespace w4c_workflows.Tests;
|
|
|
|
public class NodeGraphCompilerTests
|
|
{
|
|
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 }
|
|
""";
|
|
|
|
[Fact]
|
|
public void Compiles_a_node_workflow_and_lowers_next_into_an_edge()
|
|
{
|
|
var definition = NodeTestData.Parse(BranchYaml);
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog()).Compile(definition);
|
|
|
|
Assert.True(result.Success, string.Join("\n", result.Errors));
|
|
var graph = result.Graph!;
|
|
|
|
Assert.Equal(4, graph.Nodes.Count);
|
|
Assert.Equal("prepare", graph.EntryNodeId);
|
|
|
|
Assert.Contains(
|
|
graph.Edges,
|
|
e => e.FromNodeId == "prepare" && e.FromOutput == 0 && e.ToNodeId == "decide");
|
|
|
|
Assert.Contains(graph.Edges, e => e.FromNodeId == "decide" && e.FromOutput == 0 && e.ToNodeId == "ok");
|
|
Assert.Contains(graph.Edges, e => e.FromNodeId == "decide" && e.FromOutput == 1 && e.ToNodeId == "nope");
|
|
}
|
|
|
|
[Fact]
|
|
public void Parameters_keep_yaml_types_including_nested_mappings()
|
|
{
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog()).Compile(NodeTestData.Parse(BranchYaml));
|
|
var prepare = result.Graph!.Find("prepare")!;
|
|
|
|
Assert.Equal("manual", prepare.Parameters["mode"]!.GetValue<string>());
|
|
|
|
var fields = prepare.Parameters["fields"]!.AsArray();
|
|
Assert.Single(fields);
|
|
Assert.Equal("status", fields[0]!["name"]!.GetValue<string>());
|
|
Assert.Equal("ready", fields[0]!["value"]!.GetValue<string>());
|
|
|
|
var decide = result.Graph!.Find("decide")!;
|
|
Assert.Equal("={{ $json.status }}", decide.Parameters["left"]!.GetValue<string>());
|
|
Assert.Equal("equals", decide.Parameters["operator"]!.GetValue<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public void Unknown_node_type_is_reported()
|
|
{
|
|
var yaml = """
|
|
name: unknown
|
|
tasks:
|
|
- id: a
|
|
node: { type: does.not.exist }
|
|
""";
|
|
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog()).Compile(NodeTestData.Parse(yaml));
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Contains(result.Errors, e => e.Contains("unknown node type 'does.not.exist'"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Unknown_parameter_is_reported()
|
|
{
|
|
var yaml = """
|
|
name: bad-param
|
|
tasks:
|
|
- id: a
|
|
node: { type: core.set }
|
|
parameters:
|
|
nope: 1
|
|
""";
|
|
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog()).Compile(NodeTestData.Parse(yaml));
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Contains(result.Errors, e => e.Contains("has no parameter 'nope'"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Out_of_range_option_value_is_reported()
|
|
{
|
|
var yaml = """
|
|
name: bad-option
|
|
tasks:
|
|
- id: a
|
|
node: { type: core.set }
|
|
parameters:
|
|
mode: nope
|
|
""";
|
|
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog()).Compile(NodeTestData.Parse(yaml));
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Contains(result.Errors, e => e.Contains("parameter 'mode' has unsupported value"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Expression_parameter_values_skip_static_option_checks()
|
|
{
|
|
var yaml = """
|
|
name: expr-option
|
|
tasks:
|
|
- id: a
|
|
node: { type: core.set }
|
|
parameters:
|
|
mode: "={{ $json.mode }}"
|
|
""";
|
|
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog()).Compile(NodeTestData.Parse(yaml));
|
|
|
|
Assert.True(result.Success, string.Join("\n", result.Errors));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_cycle_is_rejected()
|
|
{
|
|
var yaml = """
|
|
name: cycle
|
|
tasks:
|
|
- id: a
|
|
node: { type: core.noop }
|
|
next: b
|
|
- id: b
|
|
node: { type: core.noop }
|
|
next: a
|
|
""";
|
|
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog()).Compile(NodeTestData.Parse(yaml));
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Contains(result.Errors, e => e.Contains("no entry node") || e.Contains("cycle"));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_cycle_through_a_loop_node_is_allowed_and_marked_loop_back()
|
|
{
|
|
var yaml = """
|
|
name: loop-ok
|
|
tasks:
|
|
- id: loop
|
|
node: { type: core.splitInBatches }
|
|
parameters: { batchSize: 2 }
|
|
edges:
|
|
- { output: 0, to: body }
|
|
- id: body
|
|
node: { type: core.noop }
|
|
edges:
|
|
- { output: 0, to: loop }
|
|
""";
|
|
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog()).Compile(NodeTestData.Parse(yaml));
|
|
|
|
Assert.True(result.Success, string.Join("\n", result.Errors));
|
|
var backEdge = Assert.Single(result.Graph!.Edges.Where(e => e.IsLoopBack));
|
|
Assert.Equal("body", backEdge.FromNodeId);
|
|
Assert.Equal("loop", backEdge.ToNodeId);
|
|
Assert.Equal("loop", result.Graph!.EntryNodeId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Multiple_entry_nodes_are_rejected()
|
|
{
|
|
var yaml = """
|
|
name: two-roots
|
|
tasks:
|
|
- id: a
|
|
node: { type: core.noop }
|
|
- id: b
|
|
node: { type: core.noop }
|
|
""";
|
|
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog()).Compile(NodeTestData.Parse(yaml));
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Contains(result.Errors, e => e.Contains("multiple entry nodes"));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_node_workflow_step_cannot_mix_node_and_entry()
|
|
{
|
|
var yaml = """
|
|
name: mixed
|
|
tasks:
|
|
- id: a
|
|
node: { type: core.noop }
|
|
entry: { file: main.sh }
|
|
""";
|
|
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog()).Compile(NodeTestData.Parse(yaml));
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Contains(result.Errors, e => e.Contains("mutually exclusive"));
|
|
}
|
|
|
|
[Fact]
|
|
public void OnError_lowers_to_the_error_output_port()
|
|
{
|
|
var catalog = NodeTestData.Catalog(NodeTestData.NodeBlueprint(
|
|
"test.fail",
|
|
outputs: new List<NodePort> { NodePort.Main, NodePort.Error }));
|
|
|
|
var yaml = """
|
|
name: error-branch
|
|
tasks:
|
|
- id: work
|
|
node: { type: test.fail }
|
|
onError: recover
|
|
- id: recover
|
|
node: { type: core.noop }
|
|
""";
|
|
|
|
var result = new NodeGraphCompiler(catalog).Compile(NodeTestData.Parse(yaml));
|
|
|
|
Assert.True(result.Success, string.Join("\n", result.Errors));
|
|
Assert.Contains(
|
|
result.Graph!.Edges,
|
|
e => e.FromNodeId == "work" && e.FromOutput == 1 && e.ToNodeId == "recover");
|
|
}
|
|
|
|
[Fact]
|
|
public void OnError_without_an_error_output_is_rejected()
|
|
{
|
|
var yaml = """
|
|
name: bad-error-branch
|
|
tasks:
|
|
- id: work
|
|
node: { type: core.noop }
|
|
onError: recover
|
|
- id: recover
|
|
node: { type: core.noop }
|
|
""";
|
|
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog()).Compile(NodeTestData.Parse(yaml));
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Contains(result.Errors, e => e.Contains("has no error output"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Pinned_version_resolves_to_that_blueprint()
|
|
{
|
|
var catalog = NodeTestData.Catalog(
|
|
NodeTestData.NodeBlueprint("demo", 1),
|
|
NodeTestData.NodeBlueprint("demo", 2));
|
|
|
|
var yaml = """
|
|
name: pinned
|
|
tasks:
|
|
- id: a
|
|
node: { type: demo, version: 1 }
|
|
""";
|
|
|
|
var result = new NodeGraphCompiler(catalog).Compile(NodeTestData.Parse(yaml));
|
|
|
|
Assert.True(result.Success, string.Join("\n", result.Errors));
|
|
Assert.Equal(1, result.Graph!.Find("a")!.Blueprint.Version);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsNodeWorkflow_detects_node_mode()
|
|
{
|
|
Assert.True(NodeGraphCompiler.IsNodeWorkflow(NodeTestData.Parse(BranchYaml)));
|
|
|
|
var legacy = NodeTestData.Parse("""
|
|
name: legacy
|
|
tasks:
|
|
- id: a
|
|
entry: { file: main.sh }
|
|
""");
|
|
Assert.False(NodeGraphCompiler.IsNodeWorkflow(legacy));
|
|
}
|
|
}
|