using System.Net; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using w4c_workflows.Services.Execution; using Xunit; namespace w4c_workflows.Tests; /// /// The remote (SSH) executor stages the task script on the target server. These /// tests pin the generated shell: the staged file must be removed on every /// completion path and the interpreter's exit code must still be propagated. /// public class RemoteServerExecutorTests { private sealed class CapturingHandler : HttpMessageHandler { public string? LastBody { get; private set; } protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { if (request.Content != null) LastBody = await request.Content.ReadAsStringAsync(cancellationToken); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent( "{\"exitCode\":0,\"stdout\":\"ok\",\"stderr\":\"\",\"timedOut\":false}", Encoding.UTF8, "application/json"), }; } } private sealed class Factory : IHttpClientFactory { private readonly HttpMessageHandler _handler; public Factory(HttpMessageHandler handler) => _handler = handler; public HttpClient CreateClient(string name) => new(_handler, disposeHandler: false); } [Fact] public async Task Staged_script_is_removed_and_exit_code_propagated() { var workDir = Directory.CreateTempSubdirectory("w4c-remote-"); try { await File.WriteAllTextAsync(Path.Combine(workDir.FullName, "main.sh"), "echo hi"); var handler = new CapturingHandler(); var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Workflows:ServerApiUrl"] = "http://server", }) .Build(); var executor = new RemoteServerExecutor( new Factory(handler), config, NullLogger.Instance); var invocation = new TaskInvocation( TaskInvocation.TypeValue, "run-1", "task-12345678", "key", "shell", "main.sh", null, new Dictionary(), null, workDir.FullName, "tenant-1", 1, "srv-1"); var result = await executor.ExecuteAsync(invocation, default); Assert.True(result.Success); Assert.NotNull(handler.LastBody); Assert.Contains(".w4c-run-", handler.LastBody); Assert.Contains("rm -f", handler.LastBody); Assert.Contains("exit $rc", handler.LastBody); } finally { workDir.Delete(recursive: true); } } [Fact] public async Task Typescript_is_rejected_for_remote_with_a_clear_error() { var workDir = Directory.CreateTempSubdirectory("w4c-remote-ts-"); try { await File.WriteAllTextAsync( Path.Combine(workDir.FullName, "main.ts"), "const value: number = 1;"); var handler = new CapturingHandler(); var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Workflows:ServerApiUrl"] = "http://server", }) .Build(); var executor = new RemoteServerExecutor( new Factory(handler), config, NullLogger.Instance); var invocation = new TaskInvocation( TaskInvocation.TypeValue, "run-1", "task-12345678", "key", "typescript", "main.ts", null, new Dictionary(), null, workDir.FullName, "tenant-1", 1, "srv-1"); var result = await executor.ExecuteAsync(invocation, default); Assert.False(result.Success); Assert.NotNull(result.Error); Assert.Contains("typescript", result.Error); Assert.Contains("not supported", result.Error); // Raw TypeScript must never be shipped to a remote `node`: the local // path bundles it with esbuild first, and nothing is sent here. Assert.Null(handler.LastBody); } finally { workDir.Delete(recursive: true); } } }