diff --git a/TriasDev.Templify.Tests/Engine/CompatEdgeCharacterizationTests.cs b/TriasDev.Templify.Tests/Engine/CompatEdgeCharacterizationTests.cs
new file mode 100644
index 0000000..759970e
--- /dev/null
+++ b/TriasDev.Templify.Tests/Engine/CompatEdgeCharacterizationTests.cs
@@ -0,0 +1,59 @@
+// Copyright (c) 2026 TriasDev GmbH & Co. KG
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+
+using TriasDev.Templify.Conditionals;
+using TriasDev.Templify.Conditionals.Engine;
+using TriasDev.Templify.Core;
+
+namespace TriasDev.Templify.Tests.Engine;
+
+///
+/// Characterization tests that pin the CURRENT (post-migration) behavior of edge cases where the new
+/// expression engine intentionally differs from the legacy ConditionalEvaluator. These are
+/// accepted, deliberate changes (F2/F3/F4); the tests exist to lock the behavior against regressions.
+///
+public class CompatEdgeCharacterizationTests
+{
+ private static bool EvalInline(string expr, Dictionary data)
+ {
+ IReadOnlyList tokens = new ConditionLexer().Tokenize(expr);
+ ConditionNode node = new ConditionParser(ConditionOperatorRegistry.Shared).Parse(tokens);
+ return new ConditionEvaluatorCore(new GlobalEvaluationContext(data), InlineConditionDialect.Instance)
+ .EvaluateBool(node);
+ }
+
+ // F2: In the inline dialect a variable-to-variable comparison now resolves the RHS as a variable
+ // (rather than treating it as an opaque literal). This is an intentional change from the legacy engine.
+ [Fact]
+ public void F2_Inline_VariableToVariableEquality_ResolvesRhs_IsTrue()
+ {
+ Assert.True(EvalInline("(A = B)", new() { ["A"] = "x", ["B"] = "x" }));
+ }
+
+ // F3: In the inline dialect, an ordered comparison between incomparable operands yields false
+ // (IComparable.CompareTo throws for the mismatched types, and TryCompare maps that to false).
+ // Intentional change: incomparable => false rather than an error.
+ [Fact]
+ public void F3_Inline_IncomparableGreaterOrEqual_IsFalse()
+ {
+ Assert.False(EvalInline("(Amount >= \"abc\")", new() { ["Amount"] = 10 }));
+ }
+
+ // F4: The operator keywords are reserved. A quoted literal still compares as a string...
+ [Fact]
+ public void F4_Default_QuotedReservedWord_ComparesAsLiteral_IsTrue()
+ {
+ Assert.True(new ConditionEvaluator().Evaluate(
+ "Category = \"empty\"", new Dictionary { ["Category"] = "empty" }));
+ }
+
+ // ...but the same word UNQUOTED is parsed as the reserved `empty` keyword, which is not a valid
+ // right-hand operand — so the expression fails to parse and Evaluate returns false. Intentional
+ // change: unquoted reserved words are no longer treated as bareword string literals.
+ [Fact]
+ public void F4_Default_UnquotedReservedWord_IsNotLiteral_IsFalse()
+ {
+ Assert.False(new ConditionEvaluator().Evaluate(
+ "Category = empty", new Dictionary { ["Category"] = "empty" }));
+ }
+}
diff --git a/TriasDev.Templify.Tests/Engine/ConditionEvaluatorCoreTests.cs b/TriasDev.Templify.Tests/Engine/ConditionEvaluatorCoreTests.cs
new file mode 100644
index 0000000..25ef41f
--- /dev/null
+++ b/TriasDev.Templify.Tests/Engine/ConditionEvaluatorCoreTests.cs
@@ -0,0 +1,50 @@
+// Copyright (c) 2026 TriasDev GmbH & Co. KG
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+
+using TriasDev.Templify.Conditionals.Engine;
+using TriasDev.Templify.Core;
+
+namespace TriasDev.Templify.Tests.Engine;
+
+public class ConditionEvaluatorCoreTests
+{
+ private static ConditionEvaluatorCore Default(Dictionary data)
+ => new(new GlobalEvaluationContext(data), DefaultConditionDialect.Instance);
+
+ private static readonly ConditionOperatorRegistry _reg = ConditionOperatorRegistry.Shared;
+
+ [Fact]
+ public void Equal_MatchingStrings_IsTrue()
+ {
+ var node = new OperatorNode(_reg.FindInfix("=")!,
+ new ConditionNode[] { new VariableNode("Status"), new LiteralNode("Active") });
+ Assert.True(Default(new() { ["Status"] = "Active" }).EvaluateBool(node));
+ }
+
+ [Fact]
+ public void And_ShortCircuits_And_Combines()
+ {
+ var left = new OperatorNode(_reg.FindInfix(">")!,
+ new ConditionNode[] { new VariableNode("Count"), new LiteralNode(0) });
+ var node = new OperatorNode(_reg.FindInfix("and")!,
+ new ConditionNode[] { left, new VariableNode("IsOn") });
+ Assert.True(Default(new() { ["Count"] = 5, ["IsOn"] = true }).EvaluateBool(node));
+ Assert.False(Default(new() { ["Count"] = 0, ["IsOn"] = true }).EvaluateBool(node));
+ }
+
+ [Fact]
+ public void Not_NegatesOperand()
+ {
+ var node = new OperatorNode(_reg.FindPrefix("not")!,
+ new ConditionNode[] { new VariableNode("IsOff") });
+ Assert.True(Default(new() { ["IsOff"] = false }).EvaluateBool(node));
+ }
+
+ [Fact]
+ public void Greater_UsesNumericComparison()
+ {
+ var node = new OperatorNode(_reg.FindInfix(">")!,
+ new ConditionNode[] { new VariableNode("Count"), new LiteralNode(3) });
+ Assert.True(Default(new() { ["Count"] = 5 }).EvaluateBool(node));
+ }
+}
diff --git a/TriasDev.Templify.Tests/Engine/ConditionLexerTests.cs b/TriasDev.Templify.Tests/Engine/ConditionLexerTests.cs
new file mode 100644
index 0000000..d6776ac
--- /dev/null
+++ b/TriasDev.Templify.Tests/Engine/ConditionLexerTests.cs
@@ -0,0 +1,71 @@
+// Copyright (c) 2026 TriasDev GmbH & Co. KG
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+
+using TriasDev.Templify.Conditionals.Engine;
+
+namespace TriasDev.Templify.Tests.Engine;
+
+public class ConditionLexerTests
+{
+ private static List Lex(string s) => new ConditionLexer().Tokenize(s).ToList();
+
+ [Fact]
+ public void Tokenize_Comparison_ProducesIdentifierOperatorString()
+ {
+ List t = Lex("Status = \"Active\"");
+ Assert.Equal(ConditionTokenType.Identifier, t[0].Type);
+ Assert.Equal("Status", t[0].Text);
+ Assert.Equal(ConditionTokenType.Operator, t[1].Type);
+ Assert.Equal("=", t[1].Text);
+ Assert.Equal(ConditionTokenType.String, t[2].Type);
+ Assert.Equal("Active", t[2].Text);
+ Assert.Equal(ConditionTokenType.End, t[3].Type);
+ }
+
+ [Fact]
+ public void Tokenize_GreaterOrEqual_MatchesLongestOperator()
+ {
+ List t = Lex("Count >= 3");
+ Assert.Equal(ConditionTokenType.Operator, t[1].Type);
+ Assert.Equal(">=", t[1].Text);
+ Assert.Equal(ConditionTokenType.Number, t[2].Type);
+ Assert.Equal(3, t[2].LiteralValue);
+ }
+
+ [Fact]
+ public void Tokenize_ListLiteral_ProducesParensAndCommas()
+ {
+ List t = Lex("Status in (\"A\", \"B\")");
+ Assert.Equal("in", t[1].Text);
+ Assert.Equal(ConditionTokenType.LParen, t[2].Type);
+ Assert.Equal(ConditionTokenType.String, t[3].Type);
+ Assert.Equal(ConditionTokenType.Comma, t[4].Type);
+ Assert.Equal(ConditionTokenType.RParen, t[6].Type);
+ }
+
+ [Fact]
+ public void Tokenize_WordOperators_AreLowercasedOperators()
+ {
+ List t = Lex("A AND B IS EMPTY");
+ Assert.Equal(ConditionTokenType.Operator, t[1].Type);
+ Assert.Equal("and", t[1].Text);
+ Assert.Equal("is", t[3].Text);
+ Assert.Equal("empty", t[4].Text);
+ }
+
+ [Fact]
+ public void Tokenize_CurlyQuotes_AreNormalized()
+ {
+ List t = Lex("Status = “Active”");
+ Assert.Equal(ConditionTokenType.String, t[2].Type);
+ Assert.Equal("Active", t[2].Text);
+ }
+
+ [Fact]
+ public void Tokenize_BooleanAndNull_ProduceLiteralTokens()
+ {
+ List t = Lex("Flag = true");
+ Assert.Equal(ConditionTokenType.Boolean, t[2].Type);
+ Assert.Equal(true, t[2].LiteralValue);
+ }
+}
diff --git a/TriasDev.Templify.Tests/Engine/ConditionParserTests.cs b/TriasDev.Templify.Tests/Engine/ConditionParserTests.cs
new file mode 100644
index 0000000..b63014e
--- /dev/null
+++ b/TriasDev.Templify.Tests/Engine/ConditionParserTests.cs
@@ -0,0 +1,48 @@
+// Copyright (c) 2026 TriasDev GmbH & Co. KG
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+
+using TriasDev.Templify.Conditionals.Engine;
+using TriasDev.Templify.Core;
+
+namespace TriasDev.Templify.Tests.Engine;
+
+public class ConditionParserTests
+{
+ private static bool Eval(string expr, Dictionary data)
+ {
+ var tokens = new ConditionLexer().Tokenize(expr);
+ ConditionNode node = new ConditionParser(ConditionOperatorRegistry.Shared).Parse(tokens);
+ return new ConditionEvaluatorCore(new GlobalEvaluationContext(data), DefaultConditionDialect.Instance).EvaluateBool(node);
+ }
+
+ [Fact]
+ public void AndBindsTighterThanOr()
+ {
+ // false or (true and true) => true ; if or bound tighter, (false or true) and false path differs.
+ Assert.True(Eval("A or B and C", new() { ["A"] = false, ["B"] = true, ["C"] = true }));
+ Assert.False(Eval("A or B and C", new() { ["A"] = false, ["B"] = true, ["C"] = false }));
+ }
+
+ [Fact]
+ public void ParenthesesOverridePrecedence()
+ {
+ Assert.False(Eval("(A or B) and C", new() { ["A"] = false, ["B"] = true, ["C"] = false }));
+ Assert.True(Eval("(A or B) and C", new() { ["A"] = false, ["B"] = true, ["C"] = true }));
+ }
+
+ [Fact]
+ public void ComparisonBindsTighterThanNot()
+ {
+ // not Status = "Active" => not (Status = "Active")
+ Assert.False(Eval("not Status = \"Active\"", new() { ["Status"] = "Active" }));
+ Assert.True(Eval("not Status = \"Active\"", new() { ["Status"] = "Inactive" }));
+ }
+
+ [Fact]
+ public void MalformedExpression_Throws()
+ {
+ var tokens = new ConditionLexer().Tokenize("Status =");
+ Assert.Throws(() =>
+ new ConditionParser(ConditionOperatorRegistry.Shared).Parse(tokens));
+ }
+}
diff --git a/TriasDev.Templify.Tests/Engine/ConditionValueOpsTests.cs b/TriasDev.Templify.Tests/Engine/ConditionValueOpsTests.cs
new file mode 100644
index 0000000..33fe253
--- /dev/null
+++ b/TriasDev.Templify.Tests/Engine/ConditionValueOpsTests.cs
@@ -0,0 +1,29 @@
+// Copyright (c) 2026 TriasDev GmbH & Co. KG
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+
+using TriasDev.Templify.Conditionals.Engine;
+
+namespace TriasDev.Templify.Tests.Engine;
+
+public class ConditionValueOpsTests
+{
+ [Fact]
+ public void AreEqual_SameStrings_ReturnsTrue()
+ => Assert.True(ConditionValueOps.AreEqual("Active", "Active"));
+
+ [Fact]
+ public void AreEqual_DifferentCaseStrings_ReturnsFalse()
+ => Assert.False(ConditionValueOps.AreEqual("active", "Active"));
+
+ [Fact]
+ public void AreEqual_BoolAndLowercaseLiteral_ReturnsTrue()
+ => Assert.True(ConditionValueOps.AreEqual(true, "true"));
+
+ [Fact]
+ public void AreEqual_BothNull_ReturnsTrue()
+ => Assert.True(ConditionValueOps.AreEqual(null, null));
+
+ [Fact]
+ public void AreEqual_OneNull_ReturnsFalse()
+ => Assert.False(ConditionValueOps.AreEqual(null, "x"));
+}
diff --git a/TriasDev.Templify.Tests/Engine/ExistenceOperatorsTests.cs b/TriasDev.Templify.Tests/Engine/ExistenceOperatorsTests.cs
new file mode 100644
index 0000000..84fccd5
--- /dev/null
+++ b/TriasDev.Templify.Tests/Engine/ExistenceOperatorsTests.cs
@@ -0,0 +1,57 @@
+// Copyright (c) 2026 TriasDev GmbH & Co. KG
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+
+using System.Collections;
+using TriasDev.Templify.Conditionals.Engine;
+using TriasDev.Templify.Core;
+
+namespace TriasDev.Templify.Tests.Engine;
+
+public class ExistenceOperatorsTests
+{
+ private static bool Eval(string expr, Dictionary data)
+ {
+ var tokens = new ConditionLexer().Tokenize(expr);
+ ConditionNode node = new ConditionParser(ConditionOperatorRegistry.Shared).Parse(tokens);
+ return new ConditionEvaluatorCore(new GlobalEvaluationContext(data), DefaultConditionDialect.Instance).EvaluateBool(node);
+ }
+
+ [Fact]
+ public void Exists_PresentVariable_IsTrue()
+ => Assert.True(Eval("Notes exists", new() { ["Notes"] = "x" }));
+
+ [Fact]
+ public void Exists_MissingVariable_IsFalse()
+ => Assert.False(Eval("Notes exists", new()));
+
+ [Fact]
+ public void IsEmpty_EmptyString_IsTrue()
+ => Assert.True(Eval("Notes is empty", new() { ["Notes"] = " " }));
+
+ [Fact]
+ public void IsEmpty_EmptyCollection_IsTrue()
+ => Assert.True(Eval("Items is empty", new() { ["Items"] = new List