Files
VolcanicCore/tests/VolcanicCore.Tests/TemplateEngineTests.cs
Claude 46372e8e38 feat(executor): {input.foo} template interpolation on node config strings
Every action/terminal node's config string fields are now run through
TemplateEngine.Render before the node sees them, so operators can pull
values from the incoming item into any config field.

- {input}, {input.a.b}, {input.tags[0]}, {input["weird.key"]}
- {{ / }} escape to literal braces
- Missing paths / null input → empty string (no 500s on absent query params)
- Unrecognised / malformed placeholders pass through so typos are visible
- Strings render raw; numbers/bools stringified; objects/arrays compact JSON

Interpolation runs executor-side (Executor.RunNodeAsync walks ConfigJson
and rewrites string leaves), so every node opts in for free. 20 new tests
(17 TemplateEngine + 2 Executor + 1 end-to-end webhook→WebResponse).
2026-08-29 18:44:03 +00:00

119 lines
3.7 KiB
C#

using System.Text.Json;
using VolcanicCore.Core;
namespace VolcanicCore.Tests;
public class TemplateEngineTests
{
private static JsonElement J(string json) => JsonDocument.Parse(json).RootElement;
[Fact]
public void No_placeholders_returns_the_template_unchanged()
{
Assert.Equal("hello world", TemplateEngine.Render("hello world", J("""{"x":1}""")));
}
[Fact]
public void Whole_input_renders_as_compact_json()
{
var input = J("""{"a":1,"b":"x"}""");
Assert.Equal("""got: {"a":1,"b":"x"}""", TemplateEngine.Render("got: {input}", input));
}
[Fact]
public void Top_level_string_field_renders_without_json_quotes()
{
Assert.Equal("value: hello", TemplateEngine.Render("value: {input.test}", J("""{"test":"hello"}""")));
}
[Fact]
public void Nested_path_lookup()
{
Assert.Equal("42", TemplateEngine.Render("{input.a.b.c}", J("""{"a":{"b":{"c":42}}}""")));
}
[Fact]
public void Array_index_lookup()
{
Assert.Equal("y", TemplateEngine.Render("{input.tags[1]}", J("""{"tags":["x","y","z"]}""")));
}
[Fact]
public void Missing_property_renders_as_empty_string()
{
Assert.Equal("value: ", TemplateEngine.Render("value: {input.missing}", J("""{"other":1}""")));
}
[Fact]
public void Missing_nested_path_renders_as_empty_string()
{
Assert.Equal("[]", TemplateEngine.Render("[{input.a.b.c}]", J("""{"a":{"b":{}}}""")));
}
[Fact]
public void Array_index_out_of_range_renders_as_empty_string()
{
Assert.Equal("<>", TemplateEngine.Render("<{input.tags[9]}>", J("""{"tags":["only"]}""")));
}
[Fact]
public void Numeric_and_boolean_and_null_leaves_render_as_their_string_form()
{
var input = J("""{"n":42,"f":1.5,"b":true,"z":null}""");
Assert.Equal("42|1.5|true|", TemplateEngine.Render("{input.n}|{input.f}|{input.b}|{input.z}", input));
}
[Fact]
public void Object_and_array_leaves_render_as_compact_json()
{
var input = J("""{"o":{"k":1},"a":[1,2]}""");
Assert.Equal("""{"k":1}/[1,2]""", TemplateEngine.Render("{input.o}/{input.a}", input));
}
[Fact]
public void Doubled_braces_render_as_literal_single_braces()
{
Assert.Equal("{not a template}", TemplateEngine.Render("{{not a template}}", J("{}")));
}
[Fact]
public void Doubled_braces_around_a_real_placeholder()
{
Assert.Equal("{hello}", TemplateEngine.Render("{{{input.x}}}", J("""{"x":"hello"}""")));
}
[Fact]
public void Null_input_renders_placeholders_as_empty_string()
{
Assert.Equal("hi ", TemplateEngine.Render("hi {input.name}", input: null));
}
[Fact]
public void Whole_input_placeholder_with_null_input_renders_empty()
{
Assert.Equal("", TemplateEngine.Render("{input}", input: null));
}
[Fact]
public void Unrecognised_placeholder_is_left_as_literal_text()
{
// Only `input`-rooted expressions are supported; anything else is passed
// through unchanged so operators notice their typo instead of silent-emptying.
Assert.Equal("{node.foo}", TemplateEngine.Render("{node.foo}", J("""{"foo":1}""")));
}
[Fact]
public void Unclosed_brace_is_left_as_literal_text()
{
Assert.Equal("start {input.x and more", TemplateEngine.Render("start {input.x and more", J("""{"x":1}""")));
}
[Fact]
public void Bracketed_string_key_lookup()
{
// Bracket form also works for property names, useful for keys with dots
// or dashes (`weird.key` can't be reached via dot syntax).
Assert.Equal("v", TemplateEngine.Render("""{input["weird.key"]}""", J("""{"weird.key":"v"}""")));
}
}