using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using VolcanicCore.Api.Engine; using VolcanicCore.Core; using VolcanicCore.Nodes; // ImplicitUsings pulls System.Threading → collision with our ExecutionContext. using ExecutionContext = VolcanicCore.Api.Engine.ExecutionContext; namespace VolcanicCore.Tests; public class PythonTransformNodeTests { private sealed class NullReg : IConnectionInstanceRegistry { public Task GetAsync(string id, CancellationToken ct) => throw new InvalidOperationException(); } private static ExecutionContext Ctx(string script) => new( "e", "wf", "n", JsonDocument.Parse(JsonSerializer.Serialize(new { script })).RootElement, null, NullLogger.Instance, null, new NullReg()); [Fact] public async Task Doubles_each_items_value_field() { var node = new PythonTransformNode(); var script = "output = [{'value': item['value'] * 2} for item in items]"; var input = new IReadOnlyList[] { new [] { new Item(JsonDocument.Parse("""{"value":3}""").RootElement.Clone()), new Item(JsonDocument.Parse("""{"value":7}""").RootElement.Clone()), } }; var outs = await node.ExecuteAsync(Ctx(script), input, CancellationToken.None); Assert.Single(outs); Assert.Equal(2, outs[0].Count); Assert.Equal(6, outs[0][0].Json.GetProperty("value").GetInt32()); Assert.Equal(14, outs[0][1].Json.GetProperty("value").GetInt32()); } [Fact] public async Task Missing_output_variable_yields_empty_list() { var node = new PythonTransformNode(); var outs = await node.ExecuteAsync(Ctx("x = 1"), new IReadOnlyList[] { Array.Empty() }, CancellationToken.None); Assert.Single(outs); Assert.Empty(outs[0]); } [Fact] public async Task Python_exception_propagates_as_node_failure() { var node = new PythonTransformNode(); var input = new IReadOnlyList[] { new [] { new Item(JsonDocument.Parse("{}").RootElement) } }; await Assert.ThrowsAnyAsync(() => node.ExecuteAsync(Ctx("raise ValueError('bad')"), input, CancellationToken.None)); } [Fact] public async Task Runtime_error_message_contains_python_traceback_with_line_and_type() { var node = new PythonTransformNode(); var input = new IReadOnlyList[] { new [] { new Item(JsonDocument.Parse("{}").RootElement) } }; var script = "x = 1\noutput = [notdefined + 1]"; var ex = await Assert.ThrowsAnyAsync(() => node.ExecuteAsync(Ctx(script), input, CancellationToken.None)); Assert.Contains("Traceback", ex.Message); Assert.Contains("NameError", ex.Message); Assert.Contains("notdefined", ex.Message); Assert.Contains("line 2", ex.Message); } [Fact] public async Task Bare_string_output_is_a_single_item_not_one_char_per_item() { var node = new PythonTransformNode(); var outs = await node.ExecuteAsync(Ctx("output = 'hello'"), new IReadOnlyList[] { Array.Empty() }, CancellationToken.None); Assert.Single(outs); Assert.Single(outs[0]); Assert.Equal("hello", outs[0][0].Json.GetString()); } [Fact] public async Task Bare_dict_output_is_a_single_item_not_one_key_per_item() { var node = new PythonTransformNode(); var outs = await node.ExecuteAsync(Ctx("output = {'test': 'hi', 'n': 3}"), new IReadOnlyList[] { Array.Empty() }, CancellationToken.None); Assert.Single(outs); Assert.Single(outs[0]); Assert.Equal(JsonValueKind.Object, outs[0][0].Json.ValueKind); Assert.Equal("hi", outs[0][0].Json.GetProperty("test").GetString()); Assert.Equal(3, outs[0][0].Json.GetProperty("n").GetInt32()); } [Fact] public async Task Bare_number_output_is_a_single_item() { var node = new PythonTransformNode(); var outs = await node.ExecuteAsync(Ctx("output = 42"), new IReadOnlyList[] { Array.Empty() }, CancellationToken.None); Assert.Single(outs); Assert.Single(outs[0]); Assert.Equal(42, outs[0][0].Json.GetInt64()); } [Fact] public async Task None_output_yields_empty_list() { var node = new PythonTransformNode(); var outs = await node.ExecuteAsync(Ctx("output = None"), new IReadOnlyList[] { Array.Empty() }, CancellationToken.None); Assert.Single(outs); Assert.Empty(outs[0]); } [Fact] public async Task Syntax_error_message_contains_python_traceback() { var node = new PythonTransformNode(); var input = new IReadOnlyList[] { new [] { new Item(JsonDocument.Parse("{}").RootElement) } }; var script = "output = ["; var ex = await Assert.ThrowsAnyAsync(() => node.ExecuteAsync(Ctx(script), input, CancellationToken.None)); Assert.Contains("SyntaxError", ex.Message); } }