From 73ebe87f30b9c0da8a725ebead1c848c00dc5683 Mon Sep 17 00:00:00 2001 From: Ian Lang Date: Thu, 4 Jun 2026 17:45:14 -0700 Subject: [PATCH 1/9] feat: Add CacheVariables to TreeNode for node-scoped variable caching Add CacheVariables property on TreeNode that evaluates Roslyn expressions after actions complete and makes results available in ShouldSelect via Cache.x. Key design: - Node-scoped: Cache is cleared at the start of each node visit - Uses EvaluateDynamicProperty: supports C#| expressions and string literals - Accessible as Cache.x in Roslyn expressions or Session.GetCache(name) - No persistence to ForgeState (ephemeral within node execution) Changes: - ForgeTree.cs: Add CacheVariables (Dictionary) to TreeNode - ExpressionExecutor.cs: Add Cache (ExpandoObject) to CodeGenInputParams - ITreeSession.cs: Add GetCache(name) method - TreeWalkerSession.cs: Add ResolveCacheVariables, ClearCache in VisitNode - ForgeExceptions.cs: Add CacheVariableException - ForgeSchemaValidationRules.json: Add CacheVariables to node definitions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/ForgeSchemaHelper.cs | 314 ++++++++++++++++++ .../test/TreeWalkerUnitTests.cs | 124 +++++++ .../contracts/ForgeSchemaValidationRules.json | 18 + Forge.TreeWalker/contracts/ForgeTree.cs | 11 + Forge.TreeWalker/src/ExpressionExecutor.cs | 28 +- Forge.TreeWalker/src/ForgeExceptions.cs | 25 ++ Forge.TreeWalker/src/ITreeSession.cs | 8 + Forge.TreeWalker/src/TreeWalkerSession.cs | 65 ++++ 8 files changed, 592 insertions(+), 1 deletion(-) diff --git a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs index c265099..20ea962 100644 --- a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs +++ b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs @@ -570,5 +570,319 @@ public static class ForgeSchemaHelper } } "; + + #region CacheVariables Schemas + + /// + /// Basic CacheVariables test — static Roslyn expression evaluated after action, used in ShouldSelect. + /// + public const string CacheVariables_StaticExpression = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Action"", + ""Actions"": { + ""Root_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, + ""CacheVariables"": { + ""myVal"": ""C#|42"" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|(int)Cache.myVal == 42"", + ""Child"": ""CorrectValue"" + }, + { + ""Child"": ""WrongValue"" + } + ] + }, + ""CorrectValue"": { + ""Type"": ""Leaf"" + }, + ""WrongValue"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVariables referencing Session.GetOutput to extract ActionResponse data. + /// + public const string CacheVariables_SessionGetOutput = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Action"", + ""Actions"": { + ""Root_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, + ""CacheVariables"": { + ""actionStatus"": ""C#|Session.GetOutput(\""Root_CollectDiagnosticsAction\"").Status"" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.actionStatus == \""Success\"""", + ""Child"": ""Found"" + }, + { + ""Child"": ""NotFound"" + } + ] + }, + ""Found"": { + ""Type"": ""Leaf"" + }, + ""NotFound"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVariables using UserContext. + /// + public const string CacheVariables_UserContext = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Action"", + ""Actions"": { + ""Root_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, + ""CacheVariables"": { + ""userName"": ""C#|UserContext.Name"" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.userName == \""MyName\"""", + ""Child"": ""Found"" + }, + { + ""Child"": ""NotFound"" + } + ] + }, + ""Found"": { + ""Type"": ""Leaf"" + }, + ""NotFound"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVariables are node-scoped — second node should NOT see first node's cache variables. + /// + public const string CacheVariables_NodeScoped = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Action"", + ""Actions"": { + ""Root_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, + ""CacheVariables"": { + ""firstNodeVar"": ""C#|99"" + }, + ""ChildSelector"": [ + { + ""Child"": ""SecondNode"" + } + ] + }, + ""SecondNode"": { + ""Type"": ""Selection"", + ""CacheVariables"": { + ""secondNodeCheck"": ""C#|Session.GetCache(\""firstNodeVar\"") == null ? \""isolated\"" : \""leaked\"""" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.secondNodeCheck == \""isolated\"""", + ""Child"": ""Isolated"" + }, + { + ""Child"": ""Leaked"" + } + ] + }, + ""Isolated"": { + ""Type"": ""Leaf"" + }, + ""Leaked"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVariables with invalid expression — should throw CacheVariableException. + /// + public const string CacheVariables_InvalidExpression = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Action"", + ""Actions"": { + ""Root_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, + ""CacheVariables"": { + ""badVar"": ""C#|NonExistentObject.Property"" + }, + ""ChildSelector"": [ + { + ""Child"": ""End"" + } + ] + }, + ""End"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// Multiple CacheVariables on the same node. + /// + public const string CacheVariables_Multiple = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Action"", + ""Actions"": { + ""Root_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, + ""CacheVariables"": { + ""a"": ""C#|10"", + ""b"": ""C#|20"", + ""sum"": ""C#|30"" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|(int)Cache.a + (int)Cache.b == (int)Cache.sum"", + ""Child"": ""Found"" + }, + { + ""Child"": ""NotFound"" + } + ] + }, + ""Found"": { + ""Type"": ""Leaf"" + }, + ""NotFound"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVariables with string literal (non-Roslyn value). + /// + public const string CacheVariables_StringLiteral = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Action"", + ""Actions"": { + ""Root_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, + ""CacheVariables"": { + ""literal"": ""hello world"" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.literal.ToString() == \""hello world\"""", + ""Child"": ""Found"" + }, + { + ""Child"": ""NotFound"" + } + ] + }, + ""Found"": { + ""Type"": ""Leaf"" + }, + ""NotFound"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// Schema for testing GetCache API. + /// + public const string CacheVariables_GetCacheApi = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Action"", + ""Actions"": { + ""Root_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, + ""CacheVariables"": { + ""testVal"": ""C#|\""cached_value\"""" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Session.GetCache(\""testVal\"").ToString() == \""cached_value\"""", + ""Child"": ""Found"" + }, + { + ""Child"": ""NotFound"" + } + ] + }, + ""Found"": { + ""Type"": ""Leaf"" + }, + ""NotFound"": { + ""Type"": ""Leaf"" + } + } + }"; + + #endregion CacheVariables Schemas } } \ No newline at end of file diff --git a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs index 8dbc702..b2556ab 100644 --- a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs +++ b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs @@ -1346,5 +1346,129 @@ private TreeWalkerSession InitializeSubroutineTree(SubroutineInput subroutineInp return new TreeWalkerSession(subroutineParameters); } + + #region CacheVariables + + [TestMethod] + public void TestCacheVariables_StaticExpression() + { + // Test - CacheVariables with a static Roslyn expression binds value available in ShouldSelect. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_StaticExpression); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("CorrectValue", currentNode, "Expected Cache.myVal == 42 to route to CorrectValue."); + } + + [TestMethod] + public void TestCacheVariables_SessionGetOutput() + { + // Test - CacheVariables can reference Session.GetOutput to extract ActionResponse data. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_SessionGetOutput); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("Found", currentNode, "Expected Cache.actionStatus == 'Success'."); + } + + [TestMethod] + public void TestCacheVariables_UserContext() + { + // Test - CacheVariables can bind UserContext properties. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_UserContext); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("Found", currentNode, "Expected Cache.userName == 'MyName'."); + } + + [TestMethod] + public void TestCacheVariables_NodeScoped_ClearedBetweenNodes() + { + // Test - CacheVariables are scoped to the current node only. Second node should not see first node's vars. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_NodeScoped); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("Isolated", currentNode, "Expected Cache to be cleared between nodes."); + } + + [TestMethod] + public void TestCacheVariables_InvalidExpression_ThrowsCacheVariableException() + { + // Test - Invalid CacheVariable expression throws CacheVariableException, status is Failed_CacheVariable. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_InvalidExpression); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("Failed_CacheVariable", actualStatus); + } + + [TestMethod] + public void TestCacheVariables_MultipleCacheVariables() + { + // Test - Multiple CacheVariables on the same node all resolve correctly. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_Multiple); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("Found", currentNode, "Expected all three cache variables to resolve."); + } + + [TestMethod] + public void TestCacheVariables_StringLiteral() + { + // Test - CacheVariables with a non-Roslyn string literal value. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_StringLiteral); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("Found", currentNode, "Expected string literal to be accessible as Cache.literal."); + } + + [TestMethod] + public void TestCacheVariables_GetCacheApi() + { + // Test - Session.GetCache() API works in Roslyn expressions. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_GetCacheApi); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("Found", currentNode, "Expected Session.GetCache to return the cached value."); + } + + [TestMethod] + public void TestCacheVariables_BackwardCompat_NoCacheVariables() + { + // Test - Schemas without CacheVariables work identically (backward compat). + this.TestFromFileInitialize(filePath: TardigradeSchemaPath); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus); + } + + [TestMethod] + public void TestCacheVariables_GetCacheReturnsNullForMissing() + { + // Test - GetCache returns null for non-existent cache variable. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_StaticExpression); + + Assert.IsNull(this.session.GetCache("nonExistent"), "Expected GetCache to return null for missing variable."); + } + + #endregion CacheVariables } } \ No newline at end of file diff --git a/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json b/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json index 3af7616..2f06727 100644 --- a/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json +++ b/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json @@ -50,6 +50,12 @@ }, "Properties": { "type": "object" + }, + "CacheVariables": { + "type": "object", + "patternProperties": { + ".*": { "type": "string" } + } } }, "additionalProperties": false, @@ -95,6 +101,12 @@ }, "Properties": { "type": "object" + }, + "CacheVariables": { + "type": "object", + "patternProperties": { + ".*": { "type": "string" } + } } }, "additionalProperties": false, @@ -156,6 +168,12 @@ }, "Properties": { "type": "object" + }, + "CacheVariables": { + "type": "object", + "patternProperties": { + ".*": { "type": "string" } + } } }, "additionalProperties": false, diff --git a/Forge.TreeWalker/contracts/ForgeTree.cs b/Forge.TreeWalker/contracts/ForgeTree.cs index 38d9834..20099fb 100644 --- a/Forge.TreeWalker/contracts/ForgeTree.cs +++ b/Forge.TreeWalker/contracts/ForgeTree.cs @@ -58,6 +58,17 @@ public class TreeNode [DataMember] public ChildSelector[] ChildSelector { get; private set; } + /// + /// Optional cache variables that evaluate Roslyn expressions after actions complete + /// and bind results to named variables for use in ShouldSelect expressions. + /// The key is the variable name accessible via Cache in Roslyn expressions (e.g., "Cache.myVar"). + /// The value is an expression evaluated via EvaluateDynamicProperty + /// (e.g., "C#|Session.GetOutput(\"ActionKey\").Output.PropertyName"). + /// Cache variables are scoped to the current node only — they are cleared when moving to the next node. + /// + [DataMember] + public Dictionary CacheVariables { get; set; } + #region Properties used only by TreeNodeType.Action nodes /// diff --git a/Forge.TreeWalker/src/ExpressionExecutor.cs b/Forge.TreeWalker/src/ExpressionExecutor.cs index 51bf3bd..c81b459 100644 --- a/Forge.TreeWalker/src/ExpressionExecutor.cs +++ b/Forge.TreeWalker/src/ExpressionExecutor.cs @@ -13,6 +13,7 @@ namespace Microsoft.Forge.TreeWalker using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; + using System.Dynamic; using System.Reflection; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -79,7 +80,8 @@ public ExpressionExecutor(ITreeSession session, object userContext, List d { UserContext = userContext, Session = session, - TreeInput = treeInput + TreeInput = treeInput, + Cache = new System.Dynamic.ExpandoObject() }; this.scriptCache = scriptCache ?? new ConcurrentDictionary>(); @@ -208,6 +210,23 @@ public bool ScriptCacheContainsKey(string expression) return this.scriptCache.ContainsKey(expression); } + /// + /// Gets the Cache dictionary (the underlying IDictionary of the ExpandoObject). + /// + /// The cache dictionary. + public IDictionary GetCache() + { + return (IDictionary)this.parameters.Cache; + } + + /// + /// Clears the Cache ExpandoObject, removing all node-scoped variables. + /// + public void ClearCache() + { + ((IDictionary)this.parameters.Cache).Clear(); + } + /// /// This class defines the global parameter that will be passed into the Roslyn expression evaluator. /// @@ -232,6 +251,13 @@ public class CodeGenInputParams /// For Subroutines, this is evaluated from the SubroutineInput on the schema. /// public dynamic TreeInput { get; set; } + + /// + /// The dynamic Cache object that holds node-scoped CacheVariables. + /// Variables are set after actions complete and are available in ShouldSelect expressions. + /// Cache is cleared at the start of each node visit. + /// + public dynamic Cache { get; set; } } /// diff --git a/Forge.TreeWalker/src/ForgeExceptions.cs b/Forge.TreeWalker/src/ForgeExceptions.cs index c894e52..b293abf 100644 --- a/Forge.TreeWalker/src/ForgeExceptions.cs +++ b/Forge.TreeWalker/src/ForgeExceptions.cs @@ -90,4 +90,29 @@ public ActionNotFoundException(string message) { } } + + /// + /// Exception thrown when a CacheVariable expression fails to evaluate. + /// + public class CacheVariableException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message. + public CacheVariableException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The inner exception. + public CacheVariableException(string message, Exception inner) + : base(message, inner) + { + } + } } diff --git a/Forge.TreeWalker/src/ITreeSession.cs b/Forge.TreeWalker/src/ITreeSession.cs index 56f599d..6bedb18 100644 --- a/Forge.TreeWalker/src/ITreeSession.cs +++ b/Forge.TreeWalker/src/ITreeSession.cs @@ -46,5 +46,13 @@ public interface ITreeSession /// Gets the string context if the actions in the current tree node were skipped, or null if actions were not skipped. /// string GetCurrentNodeSkipActionContext(); + + /// + /// Gets the value of a node-scoped cache variable by name. + /// Cache variables are populated from CacheVariables expressions after actions complete. + /// + /// The variable name as defined in CacheVariables. + /// The cached value if it exists, otherwise null. + object GetCache(string name); } } \ No newline at end of file diff --git a/Forge.TreeWalker/src/TreeWalkerSession.cs b/Forge.TreeWalker/src/TreeWalkerSession.cs index 38efbf3..442af2c 100644 --- a/Forge.TreeWalker/src/TreeWalkerSession.cs +++ b/Forge.TreeWalker/src/TreeWalkerSession.cs @@ -282,6 +282,18 @@ public string GetCurrentNodeSkipActionContext() return this.currentNodeSkipActionContext; } + /// + /// Gets the value of a node-scoped cache variable by name. + /// Cache variables are populated from CacheVariables expressions after actions complete. + /// + /// The variable name as defined in CacheVariables. + /// The cached value if it exists, otherwise null. + public object GetCache(string name) + { + IDictionary cache = this.expressionExecutor.GetCache(); + return cache.TryGetValue(name, out object value) ? value : null; + } + /// /// Signals the WalkTree and VisitNode cancellation token sources to cancel. /// @@ -435,6 +447,10 @@ await this.EvaluateDynamicProperty(this.Schema.Tree[current].Properties, null), { this.Status = "Failed_EvaluateDynamicProperty"; } + catch (CacheVariableException) + { + this.Status = "Failed_CacheVariable"; + } catch (ActionNotFoundException) { this.Status = "Failed_ActionNotFound"; @@ -460,6 +476,10 @@ await this.EvaluateDynamicProperty(this.Schema.Tree[current].Properties, null), { // For now, suppressing this exception so that its treated as successful end stage. } + catch (CacheVariableException) + { + // Status already set to Failed_CacheVariable above. Suppress re-throw. + } return this.Status; } @@ -477,6 +497,9 @@ public async Task VisitNode(string treeNodeKey) { TreeNode treeNode = this.Schema.Tree[treeNodeKey]; + // Clear node-scoped cache variables at the start of each node visit. + this.expressionExecutor.ClearCache(); + if (string.IsNullOrWhiteSpace(this.currentNodeSkipActionContext)) { // Do not skip actions when this.currentNodeSkipActionContext is null or whitespace. @@ -508,6 +531,9 @@ public async Task VisitNode(string treeNodeKey) } } + // Resolve CacheVariables after actions complete, before selecting child. + await this.ResolveCacheVariables(treeNode, treeNodeKey).ConfigureAwait(false); + if (treeNode.Type == TreeNodeType.Leaf) { // Leaf type can't have ChildSelector so we return here. @@ -1259,5 +1285,44 @@ public static void GetActionsMapFromAssembly(Assembly forgeActionsAssembly, out } } } + + /// + /// Resolves CacheVariables defined on the TreeNode. + /// Each expression is evaluated via EvaluateDynamicProperty and the result is set on the Cache ExpandoObject. + /// Called after actions complete and before SelectChild. + /// + /// The current TreeNode. + /// The current TreeNode key (for error messages). + private async Task ResolveCacheVariables(TreeNode treeNode, string treeNodeKey) + { + if (treeNode.CacheVariables == null || treeNode.CacheVariables.Count == 0) + { + return; + } + + IDictionary cache = this.expressionExecutor.GetCache(); + + foreach (KeyValuePair binding in treeNode.CacheVariables) + { + string varName = binding.Key; + string expression = binding.Value; + + try + { + object result = await this.EvaluateDynamicProperty(expression, null).ConfigureAwait(false); + cache[varName] = result; + } + catch (Exception e) + { + throw new CacheVariableException( + string.Format( + "Failed to resolve cache variable. TreeNodeKey: {0}, Variable: {1}, Expression: {2}.", + treeNodeKey, + varName, + expression), + e); + } + } + } } } \ No newline at end of file From 64654ee90420550bd9235a7ab47c9a295f4a597f Mon Sep 17 00:00:00 2001 From: Ian Lang Date: Fri, 5 Jun 2026 11:09:49 -0700 Subject: [PATCH 2/9] Refactor CacheVariables to use EvaluateDynamicProperty directly Simplify CacheVariables evaluation to match the existing pattern used by Properties, Input, and TreeInput. Instead of a custom ResolveCacheVariables method with per-variable iteration, CacheVariables is now typed as dynamic and evaluated via EvaluateDynamicProperty(treeNode.CacheVariables, null). Changes: - ForgeTree.cs: CacheVariables type changed from Dictionary to dynamic - ExpressionExecutor.cs: Cache uses SetCache/ClearCache with JObject instead of ExpandoObject - TreeWalkerSession.cs: Removed ResolveCacheVariables, inline EvaluateDynamicProperty call - ForgeExceptions.cs: Removed CacheVariableException (uses existing EvaluateDynamicPropertyException) - Tests: Updated InvalidExpression test to expect EvaluateDynamicPropertyException Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CacheVariables-Design.docx | Bin 0 -> 40412 bytes .../test/TreeWalkerUnitTests.cs | 11 ++-- Forge.TreeWalker/contracts/ForgeTree.cs | 9 ++- Forge.TreeWalker/src/ExpressionExecutor.cs | 24 ++++--- Forge.TreeWalker/src/ForgeExceptions.cs | 25 ------- Forge.TreeWalker/src/TreeWalkerSession.cs | 62 ++++-------------- 6 files changed, 38 insertions(+), 93 deletions(-) create mode 100644 CacheVariables-Design.docx diff --git a/CacheVariables-Design.docx b/CacheVariables-Design.docx new file mode 100644 index 0000000000000000000000000000000000000000..bb0adc04fda724c09608b0ef3d2119f999e02ce1 GIT binary patch literal 40412 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 => + { + this.session.WalkTree("Root").GetAwaiter().GetResult(); + }); + Assert.AreEqual("Failed_EvaluateDynamicProperty", this.session.Status); } [TestMethod] diff --git a/Forge.TreeWalker/contracts/ForgeTree.cs b/Forge.TreeWalker/contracts/ForgeTree.cs index 20099fb..aea6382 100644 --- a/Forge.TreeWalker/contracts/ForgeTree.cs +++ b/Forge.TreeWalker/contracts/ForgeTree.cs @@ -59,15 +59,14 @@ public class TreeNode public ChildSelector[] ChildSelector { get; private set; } /// - /// Optional cache variables that evaluate Roslyn expressions after actions complete - /// and bind results to named variables for use in ShouldSelect expressions. - /// The key is the variable name accessible via Cache in Roslyn expressions (e.g., "Cache.myVar"). - /// The value is an expression evaluated via EvaluateDynamicProperty + /// Optional cache variables that are evaluated via EvaluateDynamicProperty after actions complete + /// and made available in ShouldSelect expressions via the Cache object (e.g., "Cache.myVar"). + /// Each property key is the variable name, and the value is an expression /// (e.g., "C#|Session.GetOutput(\"ActionKey\").Output.PropertyName"). /// Cache variables are scoped to the current node only — they are cleared when moving to the next node. /// [DataMember] - public Dictionary CacheVariables { get; set; } + public dynamic CacheVariables { get; set; } #region Properties used only by TreeNodeType.Action nodes diff --git a/Forge.TreeWalker/src/ExpressionExecutor.cs b/Forge.TreeWalker/src/ExpressionExecutor.cs index c81b459..0141d3c 100644 --- a/Forge.TreeWalker/src/ExpressionExecutor.cs +++ b/Forge.TreeWalker/src/ExpressionExecutor.cs @@ -13,7 +13,6 @@ namespace Microsoft.Forge.TreeWalker using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; - using System.Dynamic; using System.Reflection; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -81,7 +80,7 @@ public ExpressionExecutor(ITreeSession session, object userContext, List d UserContext = userContext, Session = session, TreeInput = treeInput, - Cache = new System.Dynamic.ExpandoObject() + Cache = null }; this.scriptCache = scriptCache ?? new ConcurrentDictionary>(); @@ -211,20 +210,29 @@ public bool ScriptCacheContainsKey(string expression) } /// - /// Gets the Cache dictionary (the underlying IDictionary of the ExpandoObject). + /// Gets the Cache object (a JObject with evaluated CacheVariable values, or null). /// - /// The cache dictionary. - public IDictionary GetCache() + /// The cache object. + public dynamic GetCache() { - return (IDictionary)this.parameters.Cache; + return this.parameters.Cache; } /// - /// Clears the Cache ExpandoObject, removing all node-scoped variables. + /// Sets the Cache object to the evaluated CacheVariables result. + /// + /// The evaluated cache object (typically a JObject from EvaluateDynamicProperty). + public void SetCache(object cache) + { + this.parameters.Cache = cache; + } + + /// + /// Clears the Cache, resetting it to null for the next node visit. /// public void ClearCache() { - ((IDictionary)this.parameters.Cache).Clear(); + this.parameters.Cache = null; } /// diff --git a/Forge.TreeWalker/src/ForgeExceptions.cs b/Forge.TreeWalker/src/ForgeExceptions.cs index b293abf..c894e52 100644 --- a/Forge.TreeWalker/src/ForgeExceptions.cs +++ b/Forge.TreeWalker/src/ForgeExceptions.cs @@ -90,29 +90,4 @@ public ActionNotFoundException(string message) { } } - - /// - /// Exception thrown when a CacheVariable expression fails to evaluate. - /// - public class CacheVariableException : Exception - { - /// - /// Initializes a new instance of the class. - /// - /// The message. - public CacheVariableException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The inner exception. - public CacheVariableException(string message, Exception inner) - : base(message, inner) - { - } - } } diff --git a/Forge.TreeWalker/src/TreeWalkerSession.cs b/Forge.TreeWalker/src/TreeWalkerSession.cs index 442af2c..726c734 100644 --- a/Forge.TreeWalker/src/TreeWalkerSession.cs +++ b/Forge.TreeWalker/src/TreeWalkerSession.cs @@ -290,8 +290,13 @@ public string GetCurrentNodeSkipActionContext() /// The cached value if it exists, otherwise null. public object GetCache(string name) { - IDictionary cache = this.expressionExecutor.GetCache(); - return cache.TryGetValue(name, out object value) ? value : null; + dynamic cache = this.expressionExecutor.GetCache(); + if (cache == null) + { + return null; + } + + return cache[name]; } /// @@ -447,10 +452,6 @@ await this.EvaluateDynamicProperty(this.Schema.Tree[current].Properties, null), { this.Status = "Failed_EvaluateDynamicProperty"; } - catch (CacheVariableException) - { - this.Status = "Failed_CacheVariable"; - } catch (ActionNotFoundException) { this.Status = "Failed_ActionNotFound"; @@ -476,10 +477,6 @@ await this.EvaluateDynamicProperty(this.Schema.Tree[current].Properties, null), { // For now, suppressing this exception so that its treated as successful end stage. } - catch (CacheVariableException) - { - // Status already set to Failed_CacheVariable above. Suppress re-throw. - } return this.Status; } @@ -531,8 +528,10 @@ public async Task VisitNode(string treeNodeKey) } } - // Resolve CacheVariables after actions complete, before selecting child. - await this.ResolveCacheVariables(treeNode, treeNodeKey).ConfigureAwait(false); + // Evaluate CacheVariables after actions complete, before selecting child. + // Uses the same EvaluateDynamicProperty pattern as Properties/Input. + object cacheResult = await this.EvaluateDynamicProperty(treeNode.CacheVariables, null).ConfigureAwait(false); + this.expressionExecutor.SetCache(cacheResult); if (treeNode.Type == TreeNodeType.Leaf) { @@ -1285,44 +1284,5 @@ public static void GetActionsMapFromAssembly(Assembly forgeActionsAssembly, out } } } - - /// - /// Resolves CacheVariables defined on the TreeNode. - /// Each expression is evaluated via EvaluateDynamicProperty and the result is set on the Cache ExpandoObject. - /// Called after actions complete and before SelectChild. - /// - /// The current TreeNode. - /// The current TreeNode key (for error messages). - private async Task ResolveCacheVariables(TreeNode treeNode, string treeNodeKey) - { - if (treeNode.CacheVariables == null || treeNode.CacheVariables.Count == 0) - { - return; - } - - IDictionary cache = this.expressionExecutor.GetCache(); - - foreach (KeyValuePair binding in treeNode.CacheVariables) - { - string varName = binding.Key; - string expression = binding.Value; - - try - { - object result = await this.EvaluateDynamicProperty(expression, null).ConfigureAwait(false); - cache[varName] = result; - } - catch (Exception e) - { - throw new CacheVariableException( - string.Format( - "Failed to resolve cache variable. TreeNodeKey: {0}, Variable: {1}, Expression: {2}.", - treeNodeKey, - varName, - expression), - e); - } - } - } } } \ No newline at end of file From dc210b1fba2275e4346e3727fa57f8502ff628a3 Mon Sep 17 00:00:00 2001 From: Ian Lang Date: Fri, 5 Jun 2026 11:21:33 -0700 Subject: [PATCH 3/9] Update design doc to reflect EvaluateDynamicProperty refactor - CacheVariables type: Dictionary -> dynamic - Cache implementation: ExpandoObject -> JObject via EvaluateDynamicProperty - Error handling: CacheVariableException -> EvaluateDynamicPropertyException - Status: Failed_CacheVariable -> Failed_EvaluateDynamicProperty - Design decision 7.4: Updated rationale for JObject approach Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CacheVariables-Design.docx | Bin 40412 -> 40723 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/CacheVariables-Design.docx b/CacheVariables-Design.docx index bb0adc04fda724c09608b0ef3d2119f999e02ce1..ff47eaa90f46af4f355c1c57ddc492364f8f0bea 100644 GIT binary patch delta 5226 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 delta 4930 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 From 0488e574c88d67a4f5a358714b2001b46f581b7e Mon Sep 17 00:00:00 2001 From: Ian Lang Date: Fri, 5 Jun 2026 13:35:42 -0700 Subject: [PATCH 4/9] Address PR review comments - GetCache: Use TryGetValue for safe null-return on missing keys (JObject and IDictionary) - ForgeTree.cs: Change CacheVariables to private set for contract immutability - ForgeSchemaHelper.cs: Fix comment to reference EvaluateDynamicPropertyException Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/ForgeSchemaHelper.cs | 2 +- Forge.TreeWalker/contracts/ForgeTree.cs | 2 +- Forge.TreeWalker/src/TreeWalkerSession.cs | 13 ++++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs index 20ea962..5e87b51 100644 --- a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs +++ b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs @@ -737,7 +737,7 @@ public static class ForgeSchemaHelper }"; /// - /// CacheVariables with invalid expression — should throw CacheVariableException. + /// CacheVariables with invalid expression — should throw EvaluateDynamicPropertyException. /// public const string CacheVariables_InvalidExpression = @" { diff --git a/Forge.TreeWalker/contracts/ForgeTree.cs b/Forge.TreeWalker/contracts/ForgeTree.cs index aea6382..2b51315 100644 --- a/Forge.TreeWalker/contracts/ForgeTree.cs +++ b/Forge.TreeWalker/contracts/ForgeTree.cs @@ -66,7 +66,7 @@ public class TreeNode /// Cache variables are scoped to the current node only — they are cleared when moving to the next node. /// [DataMember] - public dynamic CacheVariables { get; set; } + public dynamic CacheVariables { get; private set; } #region Properties used only by TreeNodeType.Action nodes diff --git a/Forge.TreeWalker/src/TreeWalkerSession.cs b/Forge.TreeWalker/src/TreeWalkerSession.cs index 726c734..6257d19 100644 --- a/Forge.TreeWalker/src/TreeWalkerSession.cs +++ b/Forge.TreeWalker/src/TreeWalkerSession.cs @@ -296,7 +296,18 @@ public object GetCache(string name) return null; } - return cache[name]; + // JObject returns null for missing keys via indexer; handle other types safely. + if (cache is Newtonsoft.Json.Linq.JObject jObj) + { + return jObj.TryGetValue(name, out Newtonsoft.Json.Linq.JToken token) ? token : null; + } + + if (cache is IDictionary dict) + { + return dict.TryGetValue(name, out object value) ? value : null; + } + + return null; } /// From 56a1bcb334814683ca9d13f5be8f888605a1d776 Mon Sep 17 00:00:00 2001 From: Ian Lang Date: Fri, 5 Jun 2026 14:22:58 -0700 Subject: [PATCH 5/9] Remove design document from PR Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CacheVariables-Design.docx | Bin 40723 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 CacheVariables-Design.docx diff --git a/CacheVariables-Design.docx b/CacheVariables-Design.docx deleted file mode 100644 index ff47eaa90f46af4f355c1c57ddc492364f8f0bea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40723 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 From aa3cb2ab13dda4c95665dd1a8124d5a325f9b372 Mon Sep 17 00:00:00 2001 From: Ian Lang Date: Fri, 5 Jun 2026 16:26:05 -0700 Subject: [PATCH 6/9] Add Newtonsoft.Json assembly to Roslyn scripts and init Cache to empty JObject - Add Newtonsoft.Json.Linq.JObject assembly reference to Roslyn script options so dynamic member access on the Cache JObject resolves at runtime - Initialize Cache to empty JObject (not null) to prevent NullReferenceException when ShouldSelect references Cache on nodes without CacheVariables - ClearCache resets to empty JObject instead of null Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Forge.TreeWalker/src/ExpressionExecutor.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Forge.TreeWalker/src/ExpressionExecutor.cs b/Forge.TreeWalker/src/ExpressionExecutor.cs index 0141d3c..b6ca554 100644 --- a/Forge.TreeWalker/src/ExpressionExecutor.cs +++ b/Forge.TreeWalker/src/ExpressionExecutor.cs @@ -80,7 +80,7 @@ public ExpressionExecutor(ITreeSession session, object userContext, List d UserContext = userContext, Session = session, TreeInput = treeInput, - Cache = null + Cache = new Newtonsoft.Json.Linq.JObject() }; this.scriptCache = scriptCache ?? new ConcurrentDictionary>(); @@ -164,7 +164,8 @@ private void Initialize() Assembly mscorlib = typeof(object).Assembly; Assembly systemCore = typeof(System.Linq.Enumerable).Assembly; Assembly cSharpAssembly = typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly; - scriptOptions = scriptOptions.AddReferences(mscorlib, systemCore, cSharpAssembly); + Assembly jsonAssembly = typeof(Newtonsoft.Json.Linq.JObject).Assembly; + scriptOptions = scriptOptions.AddReferences(mscorlib, systemCore, cSharpAssembly, jsonAssembly); // Add required namespaces. scriptOptions = scriptOptions.AddImports( @@ -228,11 +229,11 @@ public void SetCache(object cache) } /// - /// Clears the Cache, resetting it to null for the next node visit. + /// Clears the Cache, resetting it to an empty JObject for the next node visit. /// public void ClearCache() { - this.parameters.Cache = null; + this.parameters.Cache = new Newtonsoft.Json.Linq.JObject(); } /// From 83d6a6875d5ea47c7b2a6ca203bbe4234d0f0b43 Mon Sep 17 00:00:00 2001 From: Ian Lang Date: Mon, 8 Jun 2026 16:27:56 -0700 Subject: [PATCH 7/9] Address PR review feedback: rename CacheVariables to CacheVars, restructure tests - Rename CacheVariables -> CacheVars in schema, contracts, and tests - Move ClearCache to after SelectChild (not top of VisitNode) - Move CacheVars evaluation after Leaf check - Add public SetCache(dynamic cacheVars) method on TreeWalkerSession - Remove GetCache from ITreeSession and TreeWalkerSession - Revert Cache init to null, remove Newtonsoft.Json assembly reference - Simplify test schemas to use Selection nodes where Actions unnecessary - Rename test variables from a/b/sum to first/second/total - Remove GetCacheApi and GetCacheReturnsNullForMissing tests - Add new tests: SetCachePublicApi, ObjectValue, BooleanExpression, AllNodeTypes, SameNameAcrossNodes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/ForgeSchemaHelper.cs | 252 ++++++++++++------ .../test/TreeWalkerUnitTests.cs | 112 +++++--- .../contracts/ForgeSchemaValidationRules.json | 8 +- Forge.TreeWalker/contracts/ForgeTree.cs | 4 +- Forge.TreeWalker/src/ExpressionExecutor.cs | 21 +- Forge.TreeWalker/src/ITreeSession.cs | 8 - Forge.TreeWalker/src/TreeWalkerSession.cs | 50 ++-- 7 files changed, 277 insertions(+), 178 deletions(-) diff --git a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs index 5e87b51..7034844 100644 --- a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs +++ b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs @@ -571,25 +571,17 @@ public static class ForgeSchemaHelper } "; - #region CacheVariables Schemas + #region CacheVars Schemas /// - /// Basic CacheVariables test — static Roslyn expression evaluated after action, used in ShouldSelect. + /// Basic CacheVars test — static Roslyn expression used in ShouldSelect. /// - public const string CacheVariables_StaticExpression = @" + public const string CacheVars_StaticExpression = @" { ""Tree"": { ""Root"": { - ""Type"": ""Action"", - ""Actions"": { - ""Root_CollectDiagnosticsAction"": { - ""Action"": ""CollectDiagnosticsAction"", - ""Input"": { - ""Command"": ""TheCommand"" - } - } - }, - ""CacheVariables"": { + ""Type"": ""Selection"", + ""CacheVars"": { ""myVal"": ""C#|42"" }, ""ChildSelector"": [ @@ -612,9 +604,9 @@ public static class ForgeSchemaHelper }"; /// - /// CacheVariables referencing Session.GetOutput to extract ActionResponse data. + /// CacheVars referencing Session.GetOutput to extract ActionResponse data. /// - public const string CacheVariables_SessionGetOutput = @" + public const string CacheVars_SessionGetOutput = @" { ""Tree"": { ""Root"": { @@ -627,7 +619,7 @@ public static class ForgeSchemaHelper } } }, - ""CacheVariables"": { + ""CacheVars"": { ""actionStatus"": ""C#|Session.GetOutput(\""Root_CollectDiagnosticsAction\"").Status"" }, ""ChildSelector"": [ @@ -650,22 +642,14 @@ public static class ForgeSchemaHelper }"; /// - /// CacheVariables using UserContext. + /// CacheVars using UserContext. /// - public const string CacheVariables_UserContext = @" + public const string CacheVars_UserContext = @" { ""Tree"": { ""Root"": { - ""Type"": ""Action"", - ""Actions"": { - ""Root_CollectDiagnosticsAction"": { - ""Action"": ""CollectDiagnosticsAction"", - ""Input"": { - ""Command"": ""TheCommand"" - } - } - }, - ""CacheVariables"": { + ""Type"": ""Selection"", + ""CacheVars"": { ""userName"": ""C#|UserContext.Name"" }, ""ChildSelector"": [ @@ -688,22 +672,14 @@ public static class ForgeSchemaHelper }"; /// - /// CacheVariables are node-scoped — second node should NOT see first node's cache variables. + /// CacheVars are node-scoped — second node should NOT see first node's cache variables. /// - public const string CacheVariables_NodeScoped = @" + public const string CacheVars_NodeScoped = @" { ""Tree"": { ""Root"": { - ""Type"": ""Action"", - ""Actions"": { - ""Root_CollectDiagnosticsAction"": { - ""Action"": ""CollectDiagnosticsAction"", - ""Input"": { - ""Command"": ""TheCommand"" - } - } - }, - ""CacheVariables"": { + ""Type"": ""Selection"", + ""CacheVars"": { ""firstNodeVar"": ""C#|99"" }, ""ChildSelector"": [ @@ -714,8 +690,8 @@ public static class ForgeSchemaHelper }, ""SecondNode"": { ""Type"": ""Selection"", - ""CacheVariables"": { - ""secondNodeCheck"": ""C#|Session.GetCache(\""firstNodeVar\"") == null ? \""isolated\"" : \""leaked\"""" + ""CacheVars"": { + ""secondNodeCheck"": ""C#|Cache == null ? \""isolated\"" : \""leaked\"""" }, ""ChildSelector"": [ { @@ -737,22 +713,14 @@ public static class ForgeSchemaHelper }"; /// - /// CacheVariables with invalid expression — should throw EvaluateDynamicPropertyException. + /// CacheVars with invalid expression — should throw EvaluateDynamicPropertyException. /// - public const string CacheVariables_InvalidExpression = @" + public const string CacheVars_InvalidExpression = @" { ""Tree"": { ""Root"": { - ""Type"": ""Action"", - ""Actions"": { - ""Root_CollectDiagnosticsAction"": { - ""Action"": ""CollectDiagnosticsAction"", - ""Input"": { - ""Command"": ""TheCommand"" - } - } - }, - ""CacheVariables"": { + ""Type"": ""Selection"", + ""CacheVars"": { ""badVar"": ""C#|NonExistentObject.Property"" }, ""ChildSelector"": [ @@ -768,29 +736,51 @@ public static class ForgeSchemaHelper }"; /// - /// Multiple CacheVariables on the same node. + /// Multiple CacheVars on the same node. /// - public const string CacheVariables_Multiple = @" + public const string CacheVars_Multiple = @" { ""Tree"": { ""Root"": { - ""Type"": ""Action"", - ""Actions"": { - ""Root_CollectDiagnosticsAction"": { - ""Action"": ""CollectDiagnosticsAction"", - ""Input"": { - ""Command"": ""TheCommand"" - } - } + ""Type"": ""Selection"", + ""CacheVars"": { + ""first"": ""C#|10"", + ""second"": ""C#|20"", + ""total"": ""C#|30"" }, - ""CacheVariables"": { - ""a"": ""C#|10"", - ""b"": ""C#|20"", - ""sum"": ""C#|30"" + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|(int)Cache.first + (int)Cache.second == (int)Cache.total"", + ""Child"": ""Found"" + }, + { + ""Child"": ""NotFound"" + } + ] + }, + ""Found"": { + ""Type"": ""Leaf"" + }, + ""NotFound"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVars with string literal (non-Roslyn value). + /// + public const string CacheVars_StringLiteral = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""literal"": ""hello world"" }, ""ChildSelector"": [ { - ""ShouldSelect"": ""C#|(int)Cache.a + (int)Cache.b == (int)Cache.sum"", + ""ShouldSelect"": ""C#|Cache.literal.ToString() == \""hello world\"""", ""Child"": ""Found"" }, { @@ -808,9 +798,9 @@ public static class ForgeSchemaHelper }"; /// - /// CacheVariables with string literal (non-Roslyn value). + /// CacheVars with ActionResponse object value — access nested property via Cache. /// - public const string CacheVariables_StringLiteral = @" + public const string CacheVars_ObjectValue = @" { ""Tree"": { ""Root"": { @@ -823,12 +813,12 @@ public static class ForgeSchemaHelper } } }, - ""CacheVariables"": { - ""literal"": ""hello world"" + ""CacheVars"": { + ""response"": ""C#|Session.GetOutput(\""Root_CollectDiagnosticsAction\"")"" }, ""ChildSelector"": [ { - ""ShouldSelect"": ""C#|Cache.literal.ToString() == \""hello world\"""", + ""ShouldSelect"": ""C#|Cache.response != null"", ""Child"": ""Found"" }, { @@ -846,43 +836,133 @@ public static class ForgeSchemaHelper }"; /// - /// Schema for testing GetCache API. + /// CacheVars with boolean expression — use Cache.IsReady in ShouldSelect. /// - public const string CacheVariables_GetCacheApi = @" + public const string CacheVars_BooleanExpression = @" { ""Tree"": { ""Root"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""IsReady"": ""C#|true"" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|(bool)Cache.IsReady"", + ""Child"": ""Ready"" + }, + { + ""Child"": ""NotReady"" + } + ] + }, + ""Ready"": { + ""Type"": ""Leaf"" + }, + ""NotReady"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVars on multiple node types — Selection and Action. + /// + public const string CacheVars_AllNodeTypes = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""nodeType"": ""C#|\""selection\"""" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.nodeType.ToString() == \""selection\"""", + ""Child"": ""ActionNode"" + }, + { + ""Child"": ""End"" + } + ] + }, + ""ActionNode"": { ""Type"": ""Action"", ""Actions"": { - ""Root_CollectDiagnosticsAction"": { + ""ActionNode_CollectDiagnosticsAction"": { ""Action"": ""CollectDiagnosticsAction"", ""Input"": { ""Command"": ""TheCommand"" } } }, - ""CacheVariables"": { - ""testVal"": ""C#|\""cached_value\"""" + ""CacheVars"": { + ""nodeType"": ""C#|\""action\"""" }, ""ChildSelector"": [ { - ""ShouldSelect"": ""C#|Session.GetCache(\""testVal\"").ToString() == \""cached_value\"""", - ""Child"": ""Found"" + ""ShouldSelect"": ""C#|Cache.nodeType.ToString() == \""action\"""", + ""Child"": ""End"" }, { - ""Child"": ""NotFound"" + ""Child"": ""Fail"" } ] }, - ""Found"": { + ""End"": { ""Type"": ""Leaf"" }, - ""NotFound"": { + ""Fail"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVars same-named property across two nodes — confirms isolation (second node re-defines same var). + /// + public const string CacheVars_SameNameAcrossNodes = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""status"": ""C#|\""first\"""" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.status.ToString() == \""first\"""", + ""Child"": ""SecondNode"" + }, + { + ""Child"": ""Fail"" + } + ] + }, + ""SecondNode"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""status"": ""C#|\""second\"""" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.status.ToString() == \""second\"""", + ""Child"": ""End"" + }, + { + ""Child"": ""Fail"" + } + ] + }, + ""End"": { + ""Type"": ""Leaf"" + }, + ""Fail"": { ""Type"": ""Leaf"" } } }"; - #endregion CacheVariables Schemas + #endregion CacheVars Schemas } -} \ No newline at end of file +} diff --git a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs index 25c385f..5367d38 100644 --- a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs +++ b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs @@ -1347,13 +1347,13 @@ private TreeWalkerSession InitializeSubroutineTree(SubroutineInput subroutineInp return new TreeWalkerSession(subroutineParameters); } - #region CacheVariables + #region CacheVars [TestMethod] - public void TestCacheVariables_StaticExpression() + public void TestCacheVars_StaticExpression() { - // Test - CacheVariables with a static Roslyn expression binds value available in ShouldSelect. - this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_StaticExpression); + // Test - CacheVars with a static Roslyn expression binds value available in ShouldSelect. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_StaticExpression); string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); Assert.AreEqual("RanToCompletion", actualStatus); @@ -1363,10 +1363,10 @@ public void TestCacheVariables_StaticExpression() } [TestMethod] - public void TestCacheVariables_SessionGetOutput() + public void TestCacheVars_SessionGetOutput() { - // Test - CacheVariables can reference Session.GetOutput to extract ActionResponse data. - this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_SessionGetOutput); + // Test - CacheVars can reference Session.GetOutput to extract ActionResponse data. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_SessionGetOutput); string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); Assert.AreEqual("RanToCompletion", actualStatus); @@ -1376,10 +1376,10 @@ public void TestCacheVariables_SessionGetOutput() } [TestMethod] - public void TestCacheVariables_UserContext() + public void TestCacheVars_UserContext() { - // Test - CacheVariables can bind UserContext properties. - this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_UserContext); + // Test - CacheVars can bind UserContext properties. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_UserContext); string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); Assert.AreEqual("RanToCompletion", actualStatus); @@ -1389,10 +1389,10 @@ public void TestCacheVariables_UserContext() } [TestMethod] - public void TestCacheVariables_NodeScoped_ClearedBetweenNodes() + public void TestCacheVars_NodeScoped_ClearedBetweenNodes() { - // Test - CacheVariables are scoped to the current node only. Second node should not see first node's vars. - this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_NodeScoped); + // Test - CacheVars are scoped to the current node only. Second node should not see first node's vars. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_NodeScoped); string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); Assert.AreEqual("RanToCompletion", actualStatus); @@ -1402,10 +1402,10 @@ public void TestCacheVariables_NodeScoped_ClearedBetweenNodes() } [TestMethod] - public void TestCacheVariables_InvalidExpression_ThrowsEvaluateDynamicPropertyException() + public void TestCacheVars_InvalidExpression_ThrowsEvaluateDynamicPropertyException() { // Test - Invalid CacheVariable expression throws EvaluateDynamicPropertyException, status is Failed_EvaluateDynamicProperty. - this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_InvalidExpression); + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_InvalidExpression); Assert.ThrowsException(() => { @@ -1415,10 +1415,10 @@ public void TestCacheVariables_InvalidExpression_ThrowsEvaluateDynamicPropertyEx } [TestMethod] - public void TestCacheVariables_MultipleCacheVariables() + public void TestCacheVars_MultipleCacheVars() { - // Test - Multiple CacheVariables on the same node all resolve correctly. - this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_Multiple); + // Test - Multiple CacheVars on the same node all resolve correctly. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_Multiple); string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); Assert.AreEqual("RanToCompletion", actualStatus); @@ -1428,10 +1428,10 @@ public void TestCacheVariables_MultipleCacheVariables() } [TestMethod] - public void TestCacheVariables_StringLiteral() + public void TestCacheVars_StringLiteral() { - // Test - CacheVariables with a non-Roslyn string literal value. - this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_StringLiteral); + // Test - CacheVars with a non-Roslyn string literal value. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_StringLiteral); string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); Assert.AreEqual("RanToCompletion", actualStatus); @@ -1441,37 +1441,81 @@ public void TestCacheVariables_StringLiteral() } [TestMethod] - public void TestCacheVariables_GetCacheApi() + public void TestCacheVars_BackwardCompat_NoCacheVars() { - // Test - Session.GetCache() API works in Roslyn expressions. - this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_GetCacheApi); + // Test - Schemas without CacheVars work identically (backward compat). + this.TestFromFileInitialize(filePath: TardigradeSchemaPath); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus); + } + + [TestMethod] + public void TestCacheVars_SetCachePublicApi() + { + // Test - Public SetCache API pre-fills the Cache for ShouldSelect evaluation. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_StaticExpression); + + // Manually set Cache via public API before WalkTree + this.session.SetCache(new { myVal = 42 }).GetAwaiter().GetResult(); + + // The schema will evaluate its own CacheVars (overwriting), but this tests that SetCache doesn't throw. + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus); + } + + [TestMethod] + public void TestCacheVars_ObjectValue() + { + // Test - CacheVars can store an object (ActionResponse) and access it in ShouldSelect. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_ObjectValue); string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); Assert.AreEqual("RanToCompletion", actualStatus); string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); - Assert.AreEqual("Found", currentNode, "Expected Session.GetCache to return the cached value."); + Assert.AreEqual("Found", currentNode, "Expected Cache.response to not be null."); } [TestMethod] - public void TestCacheVariables_BackwardCompat_NoCacheVariables() + public void TestCacheVars_BooleanExpression() { - // Test - Schemas without CacheVariables work identically (backward compat). - this.TestFromFileInitialize(filePath: TardigradeSchemaPath); + // Test - CacheVars with boolean value used directly in ShouldSelect. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_BooleanExpression); + + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("Ready", currentNode, "Expected Cache.IsReady == true to route to Ready."); + } + + [TestMethod] + public void TestCacheVars_AllNodeTypes() + { + // Test - CacheVars works on Selection and Action node types. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_AllNodeTypes); string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); Assert.AreEqual("RanToCompletion", actualStatus); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("End", currentNode, "Expected CacheVars to work on all node types."); } [TestMethod] - public void TestCacheVariables_GetCacheReturnsNullForMissing() + public void TestCacheVars_SameNameAcrossNodes() { - // Test - GetCache returns null for non-existent cache variable. - this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVariables_StaticExpression); + // Test - Same-named CacheVar across two nodes confirms isolation (each node gets fresh value). + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_SameNameAcrossNodes); - Assert.IsNull(this.session.GetCache("nonExistent"), "Expected GetCache to return null for missing variable."); + string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); + Assert.AreEqual("RanToCompletion", actualStatus); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("End", currentNode, "Expected second node to have its own 'status' = 'second'."); } - #endregion CacheVariables + #endregion CacheVars } -} \ No newline at end of file +} diff --git a/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json b/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json index 2f06727..feda591 100644 --- a/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json +++ b/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json @@ -51,7 +51,7 @@ "Properties": { "type": "object" }, - "CacheVariables": { + "CacheVars": { "type": "object", "patternProperties": { ".*": { "type": "string" } @@ -102,7 +102,7 @@ "Properties": { "type": "object" }, - "CacheVariables": { + "CacheVars": { "type": "object", "patternProperties": { ".*": { "type": "string" } @@ -169,7 +169,7 @@ "Properties": { "type": "object" }, - "CacheVariables": { + "CacheVars": { "type": "object", "patternProperties": { ".*": { "type": "string" } @@ -299,4 +299,4 @@ ] } } -} \ No newline at end of file +} diff --git a/Forge.TreeWalker/contracts/ForgeTree.cs b/Forge.TreeWalker/contracts/ForgeTree.cs index 2b51315..b69f266 100644 --- a/Forge.TreeWalker/contracts/ForgeTree.cs +++ b/Forge.TreeWalker/contracts/ForgeTree.cs @@ -63,10 +63,10 @@ public class TreeNode /// and made available in ShouldSelect expressions via the Cache object (e.g., "Cache.myVar"). /// Each property key is the variable name, and the value is an expression /// (e.g., "C#|Session.GetOutput(\"ActionKey\").Output.PropertyName"). - /// Cache variables are scoped to the current node only — they are cleared when moving to the next node. + /// Cache variables are scoped to the current node only — they are cleared after SelectChild. /// [DataMember] - public dynamic CacheVariables { get; private set; } + public dynamic CacheVars { get; private set; } #region Properties used only by TreeNodeType.Action nodes diff --git a/Forge.TreeWalker/src/ExpressionExecutor.cs b/Forge.TreeWalker/src/ExpressionExecutor.cs index b6ca554..886f009 100644 --- a/Forge.TreeWalker/src/ExpressionExecutor.cs +++ b/Forge.TreeWalker/src/ExpressionExecutor.cs @@ -80,7 +80,7 @@ public ExpressionExecutor(ITreeSession session, object userContext, List d UserContext = userContext, Session = session, TreeInput = treeInput, - Cache = new Newtonsoft.Json.Linq.JObject() + Cache = null }; this.scriptCache = scriptCache ?? new ConcurrentDictionary>(); @@ -164,8 +164,7 @@ private void Initialize() Assembly mscorlib = typeof(object).Assembly; Assembly systemCore = typeof(System.Linq.Enumerable).Assembly; Assembly cSharpAssembly = typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly; - Assembly jsonAssembly = typeof(Newtonsoft.Json.Linq.JObject).Assembly; - scriptOptions = scriptOptions.AddReferences(mscorlib, systemCore, cSharpAssembly, jsonAssembly); + scriptOptions = scriptOptions.AddReferences(mscorlib, systemCore, cSharpAssembly); // Add required namespaces. scriptOptions = scriptOptions.AddImports( @@ -211,29 +210,29 @@ public bool ScriptCacheContainsKey(string expression) } /// - /// Gets the Cache object (a JObject with evaluated CacheVariable values, or null). + /// Gets the Cache object (the evaluated CacheVars result, or null). /// /// The cache object. - public dynamic GetCache() + public object GetCache() { return this.parameters.Cache; } /// - /// Sets the Cache object to the evaluated CacheVariables result. + /// Sets the Cache object to the evaluated CacheVars result. /// - /// The evaluated cache object (typically a JObject from EvaluateDynamicProperty). + /// The evaluated cache object from EvaluateDynamicProperty. public void SetCache(object cache) { this.parameters.Cache = cache; } /// - /// Clears the Cache, resetting it to an empty JObject for the next node visit. + /// Clears the Cache, resetting it to null for the next node visit. /// public void ClearCache() { - this.parameters.Cache = new Newtonsoft.Json.Linq.JObject(); + this.parameters.Cache = null; } /// @@ -262,9 +261,9 @@ public class CodeGenInputParams public dynamic TreeInput { get; set; } /// - /// The dynamic Cache object that holds node-scoped CacheVariables. + /// The dynamic Cache object that holds node-scoped CacheVars. /// Variables are set after actions complete and are available in ShouldSelect expressions. - /// Cache is cleared at the start of each node visit. + /// Cache is cleared after SelectChild. /// public dynamic Cache { get; set; } } diff --git a/Forge.TreeWalker/src/ITreeSession.cs b/Forge.TreeWalker/src/ITreeSession.cs index 6bedb18..56f599d 100644 --- a/Forge.TreeWalker/src/ITreeSession.cs +++ b/Forge.TreeWalker/src/ITreeSession.cs @@ -46,13 +46,5 @@ public interface ITreeSession /// Gets the string context if the actions in the current tree node were skipped, or null if actions were not skipped. /// string GetCurrentNodeSkipActionContext(); - - /// - /// Gets the value of a node-scoped cache variable by name. - /// Cache variables are populated from CacheVariables expressions after actions complete. - /// - /// The variable name as defined in CacheVariables. - /// The cached value if it exists, otherwise null. - object GetCache(string name); } } \ No newline at end of file diff --git a/Forge.TreeWalker/src/TreeWalkerSession.cs b/Forge.TreeWalker/src/TreeWalkerSession.cs index 6257d19..92c0683 100644 --- a/Forge.TreeWalker/src/TreeWalkerSession.cs +++ b/Forge.TreeWalker/src/TreeWalkerSession.cs @@ -283,31 +283,14 @@ public string GetCurrentNodeSkipActionContext() } /// - /// Gets the value of a node-scoped cache variable by name. - /// Cache variables are populated from CacheVariables expressions after actions complete. + /// Evaluates the given CacheVars schema object and sets the result on the Cache. + /// This is useful for external UTs that need to pre-fill the Cache before evaluating ShouldSelect expressions. /// - /// The variable name as defined in CacheVariables. - /// The cached value if it exists, otherwise null. - public object GetCache(string name) + /// The CacheVars schema object to evaluate (typically from TreeNode.CacheVars). + public async Task SetCache(dynamic cacheVars) { - dynamic cache = this.expressionExecutor.GetCache(); - if (cache == null) - { - return null; - } - - // JObject returns null for missing keys via indexer; handle other types safely. - if (cache is Newtonsoft.Json.Linq.JObject jObj) - { - return jObj.TryGetValue(name, out Newtonsoft.Json.Linq.JToken token) ? token : null; - } - - if (cache is IDictionary dict) - { - return dict.TryGetValue(name, out object value) ? value : null; - } - - return null; + object cacheResult = await this.EvaluateDynamicProperty(cacheVars, null).ConfigureAwait(false); + this.expressionExecutor.SetCache(cacheResult); } /// @@ -505,9 +488,6 @@ public async Task VisitNode(string treeNodeKey) { TreeNode treeNode = this.Schema.Tree[treeNodeKey]; - // Clear node-scoped cache variables at the start of each node visit. - this.expressionExecutor.ClearCache(); - if (string.IsNullOrWhiteSpace(this.currentNodeSkipActionContext)) { // Do not skip actions when this.currentNodeSkipActionContext is null or whitespace. @@ -539,19 +519,23 @@ public async Task VisitNode(string treeNodeKey) } } - // Evaluate CacheVariables after actions complete, before selecting child. - // Uses the same EvaluateDynamicProperty pattern as Properties/Input. - object cacheResult = await this.EvaluateDynamicProperty(treeNode.CacheVariables, null).ConfigureAwait(false); - this.expressionExecutor.SetCache(cacheResult); - if (treeNode.Type == TreeNodeType.Leaf) { // Leaf type can't have ChildSelector so we return here. return null; } - // Return next child to visit, if possible. - return await this.SelectChild(treeNode).ConfigureAwait(false); + // Evaluate CacheVars after actions complete, before selecting child. + // Uses the same EvaluateDynamicProperty pattern as Properties/Input. + await this.SetCache(treeNode.CacheVars).ConfigureAwait(false); + + // Select next child to visit. + string result = await this.SelectChild(treeNode).ConfigureAwait(false); + + // Clear Cache after SelectChild — locks Cache access to ShouldSelect only. + this.expressionExecutor.ClearCache(); + + return result; } /// From 266996ede9c363b1fcf5b9fc4166c277bed2282a Mon Sep 17 00:00:00 2001 From: Ian Lang Date: Mon, 8 Jun 2026 16:43:58 -0700 Subject: [PATCH 8/9] Update CacheVars_Multiple test to use string verification instead of arithmetic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs | 8 ++++---- Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs index 7034844..a9678f2 100644 --- a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs +++ b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs @@ -744,13 +744,13 @@ public static class ForgeSchemaHelper ""Root"": { ""Type"": ""Selection"", ""CacheVars"": { - ""first"": ""C#|10"", - ""second"": ""C#|20"", - ""total"": ""C#|30"" + ""greeting"": ""C#|\""hello\"""", + ""target"": ""C#|\""world\"""", + ""suffix"": ""C#|\""!\"""" }, ""ChildSelector"": [ { - ""ShouldSelect"": ""C#|(int)Cache.first + (int)Cache.second == (int)Cache.total"", + ""ShouldSelect"": ""C#|Cache.greeting.ToString() == \""hello\"" && Cache.target.ToString() == \""world\"" && Cache.suffix.ToString() == \""!\"""", ""Child"": ""Found"" }, { diff --git a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs index 5367d38..4369ae8 100644 --- a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs +++ b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs @@ -1424,7 +1424,7 @@ public void TestCacheVars_MultipleCacheVars() Assert.AreEqual("RanToCompletion", actualStatus); string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); - Assert.AreEqual("Found", currentNode, "Expected all three cache variables to resolve."); + Assert.AreEqual("Found", currentNode, "Expected all three string cache variables to resolve correctly."); } [TestMethod] From cc819775185f78d73f36e8c96659e09329b3ff37 Mon Sep 17 00:00:00 2001 From: Ian Lang Date: Fri, 12 Jun 2026 13:48:53 -0700 Subject: [PATCH 9/9] Updating unit tests for clarity and demonstrate best practices --- .../test/ForgeSchemaHelper.cs | 166 ++++++++++++------ .../test/ForgeSchemaValidationTests.cs | 7 + .../test/TreeWalkerUnitTests.cs | 40 ++++- 3 files changed, 156 insertions(+), 57 deletions(-) diff --git a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs index a9678f2..1b11744 100644 --- a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs +++ b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs @@ -691,11 +691,11 @@ public static class ForgeSchemaHelper ""SecondNode"": { ""Type"": ""Selection"", ""CacheVars"": { - ""secondNodeCheck"": ""C#|Cache == null ? \""isolated\"" : \""leaked\"""" + ""secondNodeVar"": ""C#|\""present\"""" }, ""ChildSelector"": [ { - ""ShouldSelect"": ""C#|Cache.secondNodeCheck == \""isolated\"""", + ""ShouldSelect"": ""C#|Cache.secondNodeVar == \""present\"" && Cache.firstNodeVar == null"", ""Child"": ""Isolated"" }, { @@ -750,7 +750,7 @@ public static class ForgeSchemaHelper }, ""ChildSelector"": [ { - ""ShouldSelect"": ""C#|Cache.greeting.ToString() == \""hello\"" && Cache.target.ToString() == \""world\"" && Cache.suffix.ToString() == \""!\"""", + ""ShouldSelect"": ""C#|Cache.greeting == \""hello\"" && Cache.target == \""world\"" && Cache.suffix == \""!\"""", ""Child"": ""Found"" }, { @@ -780,7 +780,7 @@ public static class ForgeSchemaHelper }, ""ChildSelector"": [ { - ""ShouldSelect"": ""C#|Cache.literal.ToString() == \""hello world\"""", + ""ShouldSelect"": ""C#|Cache.literal == \""hello world\"""", ""Child"": ""Found"" }, { @@ -818,7 +818,7 @@ public static class ForgeSchemaHelper }, ""ChildSelector"": [ { - ""ShouldSelect"": ""C#|Cache.response != null"", + ""ShouldSelect"": ""C#|Cache.response.Status == \""Success\"" && Cache.response.Output == \""TheCommand_Results\"""", ""Child"": ""Found"" }, { @@ -836,19 +836,27 @@ public static class ForgeSchemaHelper }"; /// - /// CacheVars with boolean expression — use Cache.IsReady in ShouldSelect. + /// CacheVars with boolean expression — IsReady is evaluated from an action result, then used in ShouldSelect. /// public const string CacheVars_BooleanExpression = @" { ""Tree"": { ""Root"": { - ""Type"": ""Selection"", + ""Type"": ""Action"", + ""Actions"": { + ""Root_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, ""CacheVars"": { - ""IsReady"": ""C#|true"" + ""IsSuccess"": ""C#|Session.GetOutput(\""Root_CollectDiagnosticsAction\"").Status == \""Success\"""" }, ""ChildSelector"": [ { - ""ShouldSelect"": ""C#|(bool)Cache.IsReady"", + ""ShouldSelect"": ""C#|(bool)Cache.IsSuccess"", ""Child"": ""Ready"" }, { @@ -866,54 +874,88 @@ public static class ForgeSchemaHelper }"; /// - /// CacheVars on multiple node types — Selection and Action. + /// CacheVars on multiple node types — Selection, Action, and Subroutine. + /// Multi-tree dictionary schema (RootTree + SubroutineTree); initialize via TestSubroutineInitialize. /// public const string CacheVars_AllNodeTypes = @" { - ""Tree"": { - ""Root"": { - ""Type"": ""Selection"", - ""CacheVars"": { - ""nodeType"": ""C#|\""selection\"""" - }, - ""ChildSelector"": [ - { - ""ShouldSelect"": ""C#|Cache.nodeType.ToString() == \""selection\"""", - ""Child"": ""ActionNode"" + ""RootTree"": { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""nodeType"": ""C#|\""selection\"""" }, - { - ""Child"": ""End"" - } - ] - }, - ""ActionNode"": { - ""Type"": ""Action"", - ""Actions"": { - ""ActionNode_CollectDiagnosticsAction"": { - ""Action"": ""CollectDiagnosticsAction"", - ""Input"": { - ""Command"": ""TheCommand"" + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.nodeType == \""selection\"""", + ""Child"": ""ActionNode"" + }, + { + ""Child"": ""Fail"" } - } + ] }, - ""CacheVars"": { - ""nodeType"": ""C#|\""action\"""" + ""ActionNode"": { + ""Type"": ""Action"", + ""Actions"": { + ""ActionNode_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, + ""CacheVars"": { + ""nodeType"": ""C#|\""action\"""" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.nodeType == \""action\"""", + ""Child"": ""SubroutineNode"" + }, + { + ""Child"": ""Fail"" + } + ] }, - ""ChildSelector"": [ - { - ""ShouldSelect"": ""C#|Cache.nodeType.ToString() == \""action\"""", - ""Child"": ""End"" + ""SubroutineNode"": { + ""Type"": ""Subroutine"", + ""Actions"": { + ""SubroutineNode_Subroutine"": { + ""Action"": ""SubroutineAction"", + ""Input"": { + ""TreeName"": ""SubroutineTree"", + ""TreeInput"": ""TestValue"" + } + } }, - { - ""Child"": ""Fail"" - } - ] - }, - ""End"": { - ""Type"": ""Leaf"" - }, - ""Fail"": { - ""Type"": ""Leaf"" + ""CacheVars"": { + ""nodeType"": ""C#|\""subroutine\"""" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.nodeType == \""subroutine\"""", + ""Child"": ""End"" + }, + { + ""Child"": ""Fail"" + } + ] + }, + ""End"": { + ""Type"": ""Leaf"" + }, + ""Fail"": { + ""Type"": ""Leaf"" + } + } + }, + ""SubroutineTree"": { + ""Tree"": { + ""Root"": { + ""Type"": ""Leaf"" + } } } }"; @@ -931,7 +973,7 @@ public static class ForgeSchemaHelper }, ""ChildSelector"": [ { - ""ShouldSelect"": ""C#|Cache.status.ToString() == \""first\"""", + ""ShouldSelect"": ""C#|Cache.status == \""first\"""", ""Child"": ""SecondNode"" }, { @@ -946,7 +988,7 @@ public static class ForgeSchemaHelper }, ""ChildSelector"": [ { - ""ShouldSelect"": ""C#|Cache.status.ToString() == \""second\"""", + ""ShouldSelect"": ""C#|Cache.status == \""second\"""", ""Child"": ""End"" }, { @@ -963,6 +1005,28 @@ public static class ForgeSchemaHelper } }"; + /// + /// Invalid schema — CacheVars is a string instead of a dictionary/object. + /// Expected to fail ForgeSchemaValidationRules (CacheVars must be an object). + /// + public const string CacheVars_InvalidType_NotADictionary = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""CacheVars"": ""UnexpectedStringNotADictionary"", + ""ChildSelector"": [ + { + ""Child"": ""End"" + } + ] + }, + ""End"": { + ""Type"": ""Leaf"" + } + } + }"; + #endregion CacheVars Schemas } } diff --git a/Forge.TreeWalker.UnitTests/test/ForgeSchemaValidationTests.cs b/Forge.TreeWalker.UnitTests/test/ForgeSchemaValidationTests.cs index f58b315..c8459e9 100644 --- a/Forge.TreeWalker.UnitTests/test/ForgeSchemaValidationTests.cs +++ b/Forge.TreeWalker.UnitTests/test/ForgeSchemaValidationTests.cs @@ -53,6 +53,13 @@ public void TestInitialize() { "RootTree" } + }, + { + nameof(ForgeSchemaHelper.CacheVars_InvalidType_NotADictionary), + new List + { + "NA" + } } }; } diff --git a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs index 4369ae8..802ff91 100644 --- a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs +++ b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs @@ -1464,37 +1464,65 @@ public void TestCacheVars_SetCachePublicApi() Assert.AreEqual("RanToCompletion", actualStatus); } + [TestMethod] + public void TestCacheVars_SchemaValidationPattern_EvaluatesWithoutError() + { + // Test - Demonstrates a safety-check pattern a caller can run to confirm a TreeNode is properly using CacheVars. + // By evaluating the node's CacheVars into the Cache and then evaluating each ShouldSelect expression, + // we confirm the CacheVars and ShouldSelect statements evaluate without issue - i.e. they reference the + // named properties correctly. A misnamed/invalid property reference would throw here. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_Multiple); + + TreeNode node = this.session.Schema.Tree["Root"]; + + // Evaluate the node's CacheVars into the Cache, mirroring how the TreeWalker primes the Cache before SelectChild. + this.session.SetCache(node.CacheVars).GetAwaiter().GetResult(); + + // Evaluate each ShouldSelect expression as a bool, confirming they reference the cached named properties correctly. + foreach (ChildSelector cs in node.ChildSelector) + { + if (string.IsNullOrEmpty(cs.ShouldSelect)) + { + // Empty ShouldSelect defaults to true during SelectChild, so there is nothing to evaluate. + continue; + } + + object result = this.session.EvaluateDynamicProperty(cs.ShouldSelect, typeof(bool)).GetAwaiter().GetResult(); + Assert.IsInstanceOfType(result, typeof(bool), "Expected ShouldSelect to evaluate to a bool without error."); + } + } + [TestMethod] public void TestCacheVars_ObjectValue() { - // Test - CacheVars can store an object (ActionResponse) and access it in ShouldSelect. + // Test - CacheVars can store an object (ActionResponse) and access its properties in ShouldSelect. this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_ObjectValue); string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); Assert.AreEqual("RanToCompletion", actualStatus); string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); - Assert.AreEqual("Found", currentNode, "Expected Cache.response to not be null."); + Assert.AreEqual("Found", currentNode, "Expected Cache.response.Status == 'Success' and Cache.response.Output == 'TheCommand_Results'."); } [TestMethod] public void TestCacheVars_BooleanExpression() { - // Test - CacheVars with boolean value used directly in ShouldSelect. + // Test - CacheVars boolean value evaluated from an action result (Status == "Success") used directly in ShouldSelect. this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_BooleanExpression); string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); Assert.AreEqual("RanToCompletion", actualStatus); string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); - Assert.AreEqual("Ready", currentNode, "Expected Cache.IsReady == true to route to Ready."); + Assert.AreEqual("Ready", currentNode, "Expected Cache.IsSuccess == true to route to Ready."); } [TestMethod] public void TestCacheVars_AllNodeTypes() { - // Test - CacheVars works on Selection and Action node types. - this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_AllNodeTypes); + // Test - CacheVars works on Selection, Action, and Subroutine node types. + this.TestSubroutineInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_AllNodeTypes, treeName: "RootTree"); string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult(); Assert.AreEqual("RanToCompletion", actualStatus);