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

103 lines
3.4 KiB
C#
Raw Permalink Normal View History

2026-09-11 22:02:46 +00:00
using System.Text.Json.Nodes;
using Microsoft.Extensions.Configuration;
using w4c_workflows.Models.Nodes;
using w4c_workflows.Services;
using w4c_workflows.Services.Execution;
using w4c_workflows.Services.Nodes;
using w4c_workflows.Services.Nodes.Executors;
using Xunit;
namespace w4c_workflows.Tests;
/// <summary>
/// The <c>core.code</c> node bridges the node kernel to the language runtimes:
/// input items go in as JSON, the script's JSON output comes back as items.
/// </summary>
public class CodeNodeExecutorTests
{
private static CodeNodeExecutor Executor()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Workflows:TaskTimeoutSeconds"] = "60",
})
.Build();
var runtimes = new RuntimeRegistry(
new LanguageRegistry(),
new IScriptExecutor[] { new SubprocessScriptExecutor("shell", "sh", config) });
return new CodeNodeExecutor(runtimes);
}
private static NodeExecutionContext Context(
string? language,
string? entryFile,
IReadOnlyList<FlowItem> input,
string? workingDirectory)
=> new()
{
Blueprint = NodeTestData.CoreCatalog().Get("core.code")!,
Parameters = new JsonObject
{
["language"] = language,
["entryFile"] = entryFile,
},
Inputs = new IReadOnlyList<FlowItem>[] { input },
WorkingDirectory = workingDirectory,
TenantId = "tenant-1",
RunId = "run-1",
TaskId = "task-1",
NodeName = "code",
};
private static FlowItem Item(string json) => FlowItem.FromJson(JsonNode.Parse(json)!.AsObject());
[Fact]
public async Task Runs_a_shell_script_and_maps_its_json_output_to_items()
{
using var dir = new TempDir();
dir.Write("task.sh", "#!/bin/sh\nread input\necho '{\"ok\":true,\"seen\":\"input\"}'\n");
var outcome = await Executor().RunAsync(
Context("shell", "task.sh", new[] { Item("""{"x":1}""") }, dir.Path), default);
Assert.True(outcome.Succeeded, outcome.Failure?.Message);
var item = Assert.Single(outcome.Outputs[0]);
Assert.True(item.Json["ok"]!.GetValue<bool>());
}
[Fact]
public async Task Missing_language_fails_with_an_invalid_parameter_code()
{
var outcome = await Executor().RunAsync(
Context(null, "task.sh", Array.Empty<FlowItem>(), null), default);
Assert.False(outcome.Succeeded);
Assert.Equal("invalid_parameter", outcome.Failure!.Code);
}
[Fact]
public async Task Unknown_language_fails_with_a_clear_code()
{
var outcome = await Executor().RunAsync(
Context("cobol", "task.cob", Array.Empty<FlowItem>(), null), default);
Assert.False(outcome.Succeeded);
Assert.Equal("unknown_language", outcome.Failure!.Code);
}
[Fact]
public async Task A_non_zero_exit_fails_the_node()
{
using var dir = new TempDir();
dir.Write("task.sh", "#!/bin/sh\necho boom 1>&2\nexit 2\n");
var outcome = await Executor().RunAsync(
Context("shell", "task.sh", Array.Empty<FlowItem>(), dir.Path), default);
Assert.False(outcome.Succeeded);
Assert.Equal("script_failed", outcome.Failure!.Code);
Assert.Contains("boom", outcome.Failure.Message);
}
}