using System.Reflection; using System.Security.Cryptography; using System.Text; using System.Text.Json; using Microsoft.Extensions.Configuration; using w4c_workflows.Services.Execution; using Xunit; namespace w4c_workflows.Tests; public class CSharpScriptExecutorTests { private readonly CSharpScriptExecutor _executor = new(); [Fact] public async Task Executes_program_with_string_input() { using var dir = new TempDir(); dir.Write("Program.cs", """ using System.Text.Json; public static class Program { public static object Main(string input) { using var doc = JsonDocument.Parse(input); var n = doc.RootElement.GetProperty("n").GetInt32(); return new { squared = n * n }; } } """); var result = await _executor.ExecuteAsync( Invocation.For("csharp", "Program.cs", "{\"n\":5}", dir.Path), default); Assert.True(result.Success, result.Error); Assert.NotNull(result.Output); using var output = JsonDocument.Parse(result.Output!); Assert.Equal(25, output.RootElement.GetProperty("squared").GetInt32()); } [Fact] public async Task Compiles_only_entry_file_not_other_cs_files() { // Only the entry file (Program.cs) is compiled. Helper.cs exists in the // directory but is NOT included, so referencing Helper.Add causes a // compilation error. This prevents duplicate-type errors when multiple // tasks each define their own Program class in the same workflow directory. using var dir = new TempDir(); dir.Write("Helper.cs", """ public static class Helper { public static int Add(int a, int b) => a + b; } """); dir.Write("Program.cs", """ using System.Text.Json; public static class Program { public static object Main(string input) { using var doc = JsonDocument.Parse(input); var a = doc.RootElement.GetProperty("a").GetInt32(); var b = doc.RootElement.GetProperty("b").GetInt32(); return new { sum = Helper.Add(a, b) }; } } """); var result = await _executor.ExecuteAsync( Invocation.For("csharp", "Program.cs", "{\"a\":2,\"b\":3}", dir.Path), default); Assert.False(result.Success); Assert.Contains("compilation failed", result.Error, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task Supports_custom_entry_function_name() { using var dir = new TempDir(); dir.Write("Program.cs", """ public static class Program { public static object Transform(string input) => new { echo = input }; } """); var result = await _executor.ExecuteAsync( Invocation.For("csharp", "Program.cs", "{\"x\":1}", dir.Path, entryFunction: "Transform"), default); Assert.True(result.Success, result.Error); using var output = JsonDocument.Parse(result.Output!); Assert.Equal("{\"x\":1}", output.RootElement.GetProperty("echo").GetString()); } [Fact] public async Task Supports_async_entry() { using var dir = new TempDir(); dir.Write("Program.cs", """ using System.Threading.Tasks; public static class Program { public static async Task Main(string input) { await Task.Yield(); return new { ok = true }; } } """); var result = await _executor.ExecuteAsync( Invocation.For("csharp", "Program.cs", "{}", dir.Path), default); Assert.True(result.Success, result.Error); Assert.Contains("\"ok\":true", result.Output); } [Fact] public async Task Fails_with_compile_error() { using var dir = new TempDir(); dir.Write("Program.cs", "public static class Program { public static object Main(string input) { return ; } }"); var result = await _executor.ExecuteAsync( Invocation.For("csharp", "Program.cs", "{}", dir.Path), default); Assert.False(result.Success); Assert.Contains("compilation failed", result.Error, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task Fails_when_entry_method_missing() { using var dir = new TempDir(); dir.Write("Program.cs", "public static class Program { public static int NotTheEntry() => 1; }"); var result = await _executor.ExecuteAsync( Invocation.For("csharp", "Program.cs", "{}", dir.Path), default); Assert.False(result.Success); Assert.Contains("no static entry method", result.Error); } [Fact] public async Task Runs_program_with_dynamic_input() { // Frontend stubs generate `dynamic input` (compiles to `object`), but the // executor used to only match `string` params — so a valid stub compiled // yet was never found at invoke time. using var dir = new TempDir(); dir.Write("Main.cs", """ public static class Program { public static object Main(dynamic input) { return new { ok = true, input = input }; } } """); var result = await _executor.ExecuteAsync( Invocation.For("csharp", "Main.cs", "{\"x\":1}", dir.Path), default); Assert.True(result.Success, result.Error); using var output = JsonDocument.Parse(result.Output!); Assert.True(output.RootElement.GetProperty("ok").GetBoolean()); Assert.Equal("{\"x\":1}", output.RootElement.GetProperty("input").GetString()); } [Fact] public async Task Reports_expression_body_followed_by_block_as_compile_error() { // Regression: an LLM/agent edit turned the stub body into `=> { ... }`, // which is invalid C#. This is exactly the reported failure // "Main.cs(5,48): error CS1525: Invalid expression term '{'". using var dir = new TempDir(); dir.Write("Main.cs", """ public static class Program { public static object Main(dynamic input) => { return new { ok = true, input = input }; } } """); var result = await _executor.ExecuteAsync( Invocation.For("csharp", "Main.cs", "{}", dir.Path), default); Assert.False(result.Success); Assert.Contains("compilation failed", result.Error, StringComparison.OrdinalIgnoreCase); Assert.Contains("CS1525", result.Error, StringComparison.OrdinalIgnoreCase); } // ------------------------------------------------------- P0-6 hardening [Fact] public async Task Compiled_assembly_is_cached_by_source_hash() { using var dir = new TempDir(); // Unique source so the cache key is unique to this test. var marker = Guid.NewGuid().ToString("N"); dir.Write("Program.cs", $$""" public static class Program { public static object Main(string input) => new { marker = "{{marker}}" }; } """); var source = File.ReadAllText(Path.Combine(dir.Path, "Program.cs")); var key = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(source))); Assert.False(CacheContains(key)); var invocation = Invocation.For("csharp", "Program.cs", "{}", dir.Path); var first = await _executor.ExecuteAsync(invocation, default); Assert.True(first.Success, first.Error); Assert.True(CacheContains(key)); // Second run is a cache hit; it must succeed and the image is reused. var second = await _executor.ExecuteAsync(invocation, default); Assert.True(second.Success, second.Error); Assert.Contains(marker, second.Output); } [Fact] public async Task Times_out_when_the_entry_exceeds_the_deadline() { var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Workflows:CSharpTimeoutSeconds"] = "1" }) .Build(); var executor = new CSharpScriptExecutor(config); using var dir = new TempDir(); dir.Write("Program.cs", """ using System.Threading.Tasks; public static class Program { public static async Task Main(string input) { await Task.Delay(10000); return new { ok = true }; } } """); var result = await executor.ExecuteAsync( Invocation.For("csharp", "Program.cs", "{}", dir.Path), default); Assert.False(result.Success); Assert.Contains("timed out", result.Error, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task Cancellation_is_propagated_not_swallowed() { using var dir = new TempDir(); dir.Write("Program.cs", """ using System.Threading.Tasks; public static class Program { public static async Task Main(string input) { await Task.Delay(10000); return new { ok = true }; } } """); using var cts = new CancellationTokenSource(); cts.Cancel(); await Assert.ThrowsAnyAsync( () => _executor.ExecuteAsync(Invocation.For("csharp", "Program.cs", "{}", dir.Path), cts.Token)); } private static bool CacheContains(string key) { var field = typeof(CSharpScriptExecutor).GetField( "AssemblyCache", BindingFlags.NonPublic | BindingFlags.Static)!; var cache = field.GetValue(null)!; return (bool)cache.GetType().GetMethod("ContainsKey")!.Invoke(cache, new object[] { key })!; } }