using System.Text.Json; using Microsoft.Extensions.Configuration; using w4c_workflows.Services.Execution; using Xunit; namespace w4c_workflows.Tests; public class SubprocessScriptExecutorTests { private static SubprocessScriptExecutor Shell(int timeoutSeconds = 60) { var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Workflows:TaskTimeoutSeconds"] = timeoutSeconds.ToString(), }) .Build(); return new SubprocessScriptExecutor("shell", "sh", config); } [Fact] public async Task Shell_reads_stdin_and_returns_json_output() { using var dir = new TempDir(); dir.Write("task.sh", """ #!/bin/sh read input echo "{\"ok\":true,\"value\":$input}" """); var result = await Shell().ExecuteAsync( Invocation.For("shell", "task.sh", "{\"x\":1}", dir.Path), default); Assert.True(result.Success, result.Error); Assert.Equal(0, result.ExitCode); using var output = JsonDocument.Parse(result.Output!); Assert.True(output.RootElement.GetProperty("ok").GetBoolean()); } [Fact] public async Task Shell_nonzero_exit_reports_stderr_as_error() { using var dir = new TempDir(); dir.Write("task.sh", """ #!/bin/sh echo "boom" 1>&2 exit 3 """); var result = await Shell().ExecuteAsync( Invocation.For("shell", "task.sh", "{}", dir.Path), default); Assert.False(result.Success); Assert.Equal(3, result.ExitCode); Assert.Contains("boom", result.Error); } [Fact] public async Task Shell_non_json_stdout_is_wrapped_as_string() { using var dir = new TempDir(); dir.Write("task.sh", "#!/bin/sh\necho \"hello world\"\n"); var result = await Shell().ExecuteAsync( Invocation.For("shell", "task.sh", "{}", dir.Path), default); Assert.True(result.Success, result.Error); using var output = JsonDocument.Parse(result.Output!); Assert.Equal("hello world", output.RootElement.GetString()); } [Fact] public async Task Shell_is_killed_when_it_exceeds_the_time_limit() { using var dir = new TempDir(); dir.Write("task.sh", "#!/bin/sh\nsleep 5\n"); var result = await Shell(timeoutSeconds: 1).ExecuteAsync( Invocation.For("shell", "task.sh", "{}", dir.Path), default); Assert.False(result.Success); Assert.Contains("time limit", result.Error); } [Fact] public async Task Shell_honours_bash_shebang_for_bash_only_scripts() { if (ProcessRunner.FindInPath("bash") is null) return; // no bash on this host — the fallback path is covered elsewhere using var dir = new TempDir(); dir.Write("task.sh", """ #!/usr/bin/env bash set -euo pipefail cat >/dev/null || true echo '{"ok":true}' """); var result = await Shell().ExecuteAsync( Invocation.For("shell", "task.sh", "{}", dir.Path), default); Assert.True(result.Success, result.Error); using var output = JsonDocument.Parse(result.Output!); Assert.True(output.RootElement.GetProperty("ok").GetBoolean()); } [Fact] public async Task Python_reads_stdin_and_returns_json_output() { var executor = new SubprocessScriptExecutor("python", "python3", new ConfigurationBuilder().Build()); if (!executor.IsAvailable()) return; // python3 not installed on this host — availability is covered by RuntimeRegistryTests using var dir = new TempDir(); dir.Write("task.py", """ import sys, json print(json.dumps({"echo": sys.stdin.read().strip()})) """); var result = await executor.ExecuteAsync( Invocation.For("python", "task.py", "{\"x\":1}", dir.Path), default); Assert.True(result.Success, result.Error); Assert.Contains("\\\"x\\\":1", result.Output); } [Fact] public async Task Javascript_runs_under_node() { var executor = new SubprocessScriptExecutor("javascript", "node", new ConfigurationBuilder().Build()); if (!executor.IsAvailable()) return; // node not installed on this host — availability is covered by RuntimeRegistryTests using var dir = new TempDir(); dir.Write("task.js", """ let input = ""; process.stdin.on("data", (c) => (input += c)); process.stdin.on("end", () => { const data = JSON.parse(input); console.log(JSON.stringify({ doubled: data.n * 2 })); }); """); var result = await executor.ExecuteAsync( Invocation.For("javascript", "task.js", "{\"n\":21}", dir.Path), default); Assert.True(result.Success, result.Error); using var output = JsonDocument.Parse(result.Output!); Assert.Equal(42, output.RootElement.GetProperty("doubled").GetInt32()); } }