From 13f9f730331294f800f6f014fb5ea02f3267a1b6 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 10 Sep 2026 15:18:59 -0400 Subject: [PATCH] feat(flow-debugger): predict the reset write + echo wait of the reset-aware handshake (#36) Claude-Session: https://claude.ai/code/session_01XPsuy2ZZ5Gbyfpt8PrK5BV --- .../FlowShapePredictor.cs | 23 +++++++++- .../FlowShapePredictorTests.cs | 42 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs index 9aa53e71..7b5840eb 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs @@ -45,6 +45,8 @@ public sealed record PredictedFlow(IReadOnlyList Nodes, IReadOnly /// script API exposes (Route.To(x).Call(y), Scripts.Call(y), /// ….Attributes.WriteBatchAndWaitAsync(…), ….Attributes[x] = v, …) by text with /// whitespace stripped, exactly as 's hardening walker does. +/// The nine-argument reset-aware WriteBatchAndWaitAsync overload (Gitea #36) additionally +/// predicts a reset attribute write + echo wait ahead of the batch write + wait pair. /// String-literal arguments resolve to titles; anything else is flagged /// . Branching is not modelled: every recognised call /// is emitted once in source order, and an if (…) return …; guard that precedes the next @@ -369,9 +371,18 @@ public static class FlowShapePredictor { case "WriteBatchAndWaitAsync": { - // (batch, triggerAttr, triggerValue, waitAttr, waitValue, timeout) + // (batch, triggerAttr, triggerValue, waitAttr, waitValue, timeout + // [, resetAttr, resetValue, resetTimeout]) — Gitea #36: the reset-aware + // overload writes + waits on the reset attribute BEFORE the batch. var (trigger, triggerDependent) = LiteralOrExpr(args.ElementAtOrDefault(1)?.Expression); var (wait, waitDependent) = LiteralOrExpr(args.ElementAtOrDefault(3)?.Expression); + var resetArg = ArgAt(args, 6, "resetKey"); + if (resetArg is not null) + { + var (reset, resetDependent) = LiteralOrExpr(resetArg.Expression); + _b.AddNode(FlowStepKind.AttributeWrite, FlowLane.Tags, Qualify(reset), resetDependent, "reset"); + _b.AddNode(FlowStepKind.AttributeWait, FlowLane.Site, Qualify(reset), resetDependent, "reset echo"); + } _b.AddNode(FlowStepKind.AttributeWrite, FlowLane.Tags, Qualify($"batch + {trigger}"), triggerDependent, null); _b.AddNode(FlowStepKind.AttributeWait, FlowLane.Site, Qualify(wait), waitDependent, null); return true; @@ -463,6 +474,16 @@ public static class FlowShapePredictor private static string TrimAsyncSuffix(string member) => member.Length > 5 && member.EndsWith("Async", StringComparison.Ordinal) ? member[..^5] : member; + /// An argument by name (name: expr) or, failing that, by position among the unnamed arguments. + private static ArgumentSyntax? ArgAt(SeparatedSyntaxList args, int index, string name) + { + var named = args.FirstOrDefault(a => a.NameColon?.Name.Identifier.ValueText == name); + if (named is not null) + return named; + var positional = args.ElementAtOrDefault(index); + return positional is { NameColon: null } ? positional : null; + } + /// A string literal resolves to its value; anything else is input-dependent and shown as its source text. private static (string Text, bool Dependent) LiteralOrExpr(ExpressionSyntax? expr) { diff --git a/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/FlowShapePredictorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/FlowShapePredictorTests.cs index 572df0a9..3f1622e8 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/FlowShapePredictorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/FlowShapePredictorTests.cs @@ -313,4 +313,46 @@ public class FlowShapePredictorTests Assert.True(write.InputDependent); Assert.Equal("name", write.Title); } + + /// Gitea #36: the reset-aware overload predicts the reset write + echo wait ahead of the batch. + [Fact] + public void ResetAwareWriteBatchAndWait_PredictsResetWriteAndEchoWaitBeforeTheBatch() + { + const string body = """ + var data = new System.Collections.Generic.Dictionary { { "MoveOutBatchID", 1 } }; + var hs = await Children["RightMESReceiver"].Attributes.WriteBatchAndWaitAsync( + data, "MoveOutFlag", true, "MoveOutCompleteFlag", true, System.TimeSpan.FromSeconds(25), + "MoveOutCompleteFlag", false, System.TimeSpan.FromSeconds(10)); + if (hs.ResetFailed) return new { WasSuccessful = false }; + """; + + var flow = FlowShapePredictor.PredictSiteScript("MesMoveOut", body); + + Assert.Equal( + [ + (FlowStepKind.ScriptRun, FlowLane.Site, "MesMoveOut"), + (FlowStepKind.AttributeWrite, FlowLane.Tags, "MoveOutCompleteFlag"), + (FlowStepKind.AttributeWait, FlowLane.Site, "MoveOutCompleteFlag"), + (FlowStepKind.AttributeWrite, FlowLane.Tags, "batch + MoveOutFlag"), + (FlowStepKind.AttributeWait, FlowLane.Site, "MoveOutCompleteFlag"), + ], flow.Nodes.Select(n => (n.Kind, n.Lane, n.Title)).ToList()); + Assert.Equal("reset", flow.Nodes[1].Note); + Assert.Equal("reset echo", flow.Nodes[2].Note); + } + + /// Named reset arguments resolve by name, not position. + [Fact] + public void ResetAwareWriteBatchAndWait_NamedArguments_ResolveTheResetKey() + { + const string body = """ + var hs = await Attributes.WriteBatchAndWaitAsync(data, "Trig", true, "Done", true, t, + resetKey: "Done", resetValue: false); + """; + + var flow = FlowShapePredictor.PredictSiteScript("S", body); + + Assert.Equal("Done", flow.Nodes[1].Title); + Assert.Equal(FlowStepKind.AttributeWrite, flow.Nodes[1].Kind); + Assert.Equal(5, flow.Nodes.Count); + } }