diff --git a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs index c265099..1b11744 100644 --- a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs +++ b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs @@ -570,5 +570,463 @@ public static class ForgeSchemaHelper } } "; + + #region CacheVars Schemas + + /// + /// Basic CacheVars test — static Roslyn expression used in ShouldSelect. + /// + public const string CacheVars_StaticExpression = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""myVal"": ""C#|42"" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|(int)Cache.myVal == 42"", + ""Child"": ""CorrectValue"" + }, + { + ""Child"": ""WrongValue"" + } + ] + }, + ""CorrectValue"": { + ""Type"": ""Leaf"" + }, + ""WrongValue"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVars referencing Session.GetOutput to extract ActionResponse data. + /// + public const string CacheVars_SessionGetOutput = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Action"", + ""Actions"": { + ""Root_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, + ""CacheVars"": { + ""actionStatus"": ""C#|Session.GetOutput(\""Root_CollectDiagnosticsAction\"").Status"" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.actionStatus == \""Success\"""", + ""Child"": ""Found"" + }, + { + ""Child"": ""NotFound"" + } + ] + }, + ""Found"": { + ""Type"": ""Leaf"" + }, + ""NotFound"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVars using UserContext. + /// + public const string CacheVars_UserContext = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""userName"": ""C#|UserContext.Name"" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.userName == \""MyName\"""", + ""Child"": ""Found"" + }, + { + ""Child"": ""NotFound"" + } + ] + }, + ""Found"": { + ""Type"": ""Leaf"" + }, + ""NotFound"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVars are node-scoped — second node should NOT see first node's cache variables. + /// + public const string CacheVars_NodeScoped = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""firstNodeVar"": ""C#|99"" + }, + ""ChildSelector"": [ + { + ""Child"": ""SecondNode"" + } + ] + }, + ""SecondNode"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""secondNodeVar"": ""C#|\""present\"""" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.secondNodeVar == \""present\"" && Cache.firstNodeVar == null"", + ""Child"": ""Isolated"" + }, + { + ""Child"": ""Leaked"" + } + ] + }, + ""Isolated"": { + ""Type"": ""Leaf"" + }, + ""Leaked"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVars with invalid expression — should throw EvaluateDynamicPropertyException. + /// + public const string CacheVars_InvalidExpression = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""badVar"": ""C#|NonExistentObject.Property"" + }, + ""ChildSelector"": [ + { + ""Child"": ""End"" + } + ] + }, + ""End"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// Multiple CacheVars on the same node. + /// + public const string CacheVars_Multiple = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""greeting"": ""C#|\""hello\"""", + ""target"": ""C#|\""world\"""", + ""suffix"": ""C#|\""!\"""" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.greeting == \""hello\"" && Cache.target == \""world\"" && Cache.suffix == \""!\"""", + ""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#|Cache.literal == \""hello world\"""", + ""Child"": ""Found"" + }, + { + ""Child"": ""NotFound"" + } + ] + }, + ""Found"": { + ""Type"": ""Leaf"" + }, + ""NotFound"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVars with ActionResponse object value — access nested property via Cache. + /// + public const string CacheVars_ObjectValue = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Action"", + ""Actions"": { + ""Root_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, + ""CacheVars"": { + ""response"": ""C#|Session.GetOutput(\""Root_CollectDiagnosticsAction\"")"" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.response.Status == \""Success\"" && Cache.response.Output == \""TheCommand_Results\"""", + ""Child"": ""Found"" + }, + { + ""Child"": ""NotFound"" + } + ] + }, + ""Found"": { + ""Type"": ""Leaf"" + }, + ""NotFound"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVars with boolean expression — IsReady is evaluated from an action result, then used in ShouldSelect. + /// + public const string CacheVars_BooleanExpression = @" + { + ""Tree"": { + ""Root"": { + ""Type"": ""Action"", + ""Actions"": { + ""Root_CollectDiagnosticsAction"": { + ""Action"": ""CollectDiagnosticsAction"", + ""Input"": { + ""Command"": ""TheCommand"" + } + } + }, + ""CacheVars"": { + ""IsSuccess"": ""C#|Session.GetOutput(\""Root_CollectDiagnosticsAction\"").Status == \""Success\"""" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|(bool)Cache.IsSuccess"", + ""Child"": ""Ready"" + }, + { + ""Child"": ""NotReady"" + } + ] + }, + ""Ready"": { + ""Type"": ""Leaf"" + }, + ""NotReady"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// CacheVars on multiple node types — Selection, Action, and Subroutine. + /// Multi-tree dictionary schema (RootTree + SubroutineTree); initialize via TestSubroutineInitialize. + /// + public const string CacheVars_AllNodeTypes = @" + { + ""RootTree"": { + ""Tree"": { + ""Root"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""nodeType"": ""C#|\""selection\"""" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.nodeType == \""selection\"""", + ""Child"": ""ActionNode"" + }, + { + ""Child"": ""Fail"" + } + ] + }, + ""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"" + } + ] + }, + ""SubroutineNode"": { + ""Type"": ""Subroutine"", + ""Actions"": { + ""SubroutineNode_Subroutine"": { + ""Action"": ""SubroutineAction"", + ""Input"": { + ""TreeName"": ""SubroutineTree"", + ""TreeInput"": ""TestValue"" + } + } + }, + ""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"" + } + } + } + }"; + + /// + /// 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 == \""first\"""", + ""Child"": ""SecondNode"" + }, + { + ""Child"": ""Fail"" + } + ] + }, + ""SecondNode"": { + ""Type"": ""Selection"", + ""CacheVars"": { + ""status"": ""C#|\""second\"""" + }, + ""ChildSelector"": [ + { + ""ShouldSelect"": ""C#|Cache.status == \""second\"""", + ""Child"": ""End"" + }, + { + ""Child"": ""Fail"" + } + ] + }, + ""End"": { + ""Type"": ""Leaf"" + }, + ""Fail"": { + ""Type"": ""Leaf"" + } + } + }"; + + /// + /// 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 } -} \ No newline at end of file +} 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 8dbc702..802ff91 100644 --- a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs +++ b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs @@ -1346,5 +1346,204 @@ private TreeWalkerSession InitializeSubroutineTree(SubroutineInput subroutineInp return new TreeWalkerSession(subroutineParameters); } + + #region CacheVars + + [TestMethod] + public void TestCacheVars_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); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("CorrectValue", currentNode, "Expected Cache.myVal == 42 to route to CorrectValue."); + } + + [TestMethod] + public void TestCacheVars_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); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("Found", currentNode, "Expected Cache.actionStatus == 'Success'."); + } + + [TestMethod] + public void TestCacheVars_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); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("Found", currentNode, "Expected Cache.userName == 'MyName'."); + } + + [TestMethod] + public void TestCacheVars_NodeScoped_ClearedBetweenNodes() + { + // 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); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("Isolated", currentNode, "Expected Cache to be cleared between nodes."); + } + + [TestMethod] + public void TestCacheVars_InvalidExpression_ThrowsEvaluateDynamicPropertyException() + { + // Test - Invalid CacheVariable expression throws EvaluateDynamicPropertyException, status is Failed_EvaluateDynamicProperty. + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_InvalidExpression); + + Assert.ThrowsException(() => + { + this.session.WalkTree("Root").GetAwaiter().GetResult(); + }); + Assert.AreEqual("Failed_EvaluateDynamicProperty", this.session.Status); + } + + [TestMethod] + public void TestCacheVars_MultipleCacheVars() + { + // 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); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("Found", currentNode, "Expected all three string cache variables to resolve correctly."); + } + + [TestMethod] + public void TestCacheVars_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); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("Found", currentNode, "Expected string literal to be accessible as Cache.literal."); + } + + [TestMethod] + public void TestCacheVars_BackwardCompat_NoCacheVars() + { + // 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_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 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.Status == 'Success' and Cache.response.Output == 'TheCommand_Results'."); + } + + [TestMethod] + public void TestCacheVars_BooleanExpression() + { + // 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.IsSuccess == true to route to Ready."); + } + + [TestMethod] + public void TestCacheVars_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); + + string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult(); + Assert.AreEqual("End", currentNode, "Expected CacheVars to work on all node types."); + } + + [TestMethod] + public void TestCacheVars_SameNameAcrossNodes() + { + // Test - Same-named CacheVar across two nodes confirms isolation (each node gets fresh value). + this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_SameNameAcrossNodes); + + 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 CacheVars } -} \ No newline at end of file +} diff --git a/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json b/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json index 3af7616..feda591 100644 --- a/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json +++ b/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json @@ -50,6 +50,12 @@ }, "Properties": { "type": "object" + }, + "CacheVars": { + "type": "object", + "patternProperties": { + ".*": { "type": "string" } + } } }, "additionalProperties": false, @@ -95,6 +101,12 @@ }, "Properties": { "type": "object" + }, + "CacheVars": { + "type": "object", + "patternProperties": { + ".*": { "type": "string" } + } } }, "additionalProperties": false, @@ -156,6 +168,12 @@ }, "Properties": { "type": "object" + }, + "CacheVars": { + "type": "object", + "patternProperties": { + ".*": { "type": "string" } + } } }, "additionalProperties": false, @@ -281,4 +299,4 @@ ] } } -} \ No newline at end of file +} diff --git a/Forge.TreeWalker/contracts/ForgeTree.cs b/Forge.TreeWalker/contracts/ForgeTree.cs index 38d9834..b69f266 100644 --- a/Forge.TreeWalker/contracts/ForgeTree.cs +++ b/Forge.TreeWalker/contracts/ForgeTree.cs @@ -58,6 +58,16 @@ public class TreeNode [DataMember] public ChildSelector[] ChildSelector { get; private set; } + /// + /// 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 after SelectChild. + /// + [DataMember] + 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 51bf3bd..886f009 100644 --- a/Forge.TreeWalker/src/ExpressionExecutor.cs +++ b/Forge.TreeWalker/src/ExpressionExecutor.cs @@ -79,7 +79,8 @@ public ExpressionExecutor(ITreeSession session, object userContext, List d { UserContext = userContext, Session = session, - TreeInput = treeInput + TreeInput = treeInput, + Cache = null }; this.scriptCache = scriptCache ?? new ConcurrentDictionary>(); @@ -208,6 +209,32 @@ public bool ScriptCacheContainsKey(string expression) return this.scriptCache.ContainsKey(expression); } + /// + /// Gets the Cache object (the evaluated CacheVars result, or null). + /// + /// The cache object. + public object GetCache() + { + return this.parameters.Cache; + } + + /// + /// Sets the Cache object to the evaluated CacheVars result. + /// + /// The evaluated cache object 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() + { + this.parameters.Cache = null; + } + /// /// This class defines the global parameter that will be passed into the Roslyn expression evaluator. /// @@ -232,6 +259,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 CacheVars. + /// Variables are set after actions complete and are available in ShouldSelect expressions. + /// Cache is cleared after SelectChild. + /// + public dynamic Cache { get; set; } } /// diff --git a/Forge.TreeWalker/src/TreeWalkerSession.cs b/Forge.TreeWalker/src/TreeWalkerSession.cs index 38efbf3..92c0683 100644 --- a/Forge.TreeWalker/src/TreeWalkerSession.cs +++ b/Forge.TreeWalker/src/TreeWalkerSession.cs @@ -282,6 +282,17 @@ public string GetCurrentNodeSkipActionContext() return this.currentNodeSkipActionContext; } + /// + /// 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 CacheVars schema object to evaluate (typically from TreeNode.CacheVars). + public async Task SetCache(dynamic cacheVars) + { + object cacheResult = await this.EvaluateDynamicProperty(cacheVars, null).ConfigureAwait(false); + this.expressionExecutor.SetCache(cacheResult); + } + /// /// Signals the WalkTree and VisitNode cancellation token sources to cancel. /// @@ -514,8 +525,17 @@ public async Task VisitNode(string treeNodeKey) 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; } ///