w4c-workflows-api/w4c-workflows-api.Tests/NodeWorkflowCompilerTests.cs

163 lines
5.4 KiB
C#
Raw Permalink Normal View History

2026-09-11 22:02:46 +00:00
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);
}
}